Operators & Expressions

Arithmetic

tape
let sum = a + b;
let diff = a - b;
let prod = a * b;
let quot = a / b;
let rem = a % b;

Integer arithmetic wraps around on overflow (two’s complement): +, - and * never trap, so 9223372036854775807 + 1 is -9223372036854775808. To notice an overflow, use math.checked_add_i64, checked_sub_i64 or checked_mul_i64, which return null instead of a wrapped result. / and % truncate toward zero (-7 / 2 is -3); math.div_floor_i64 and math.mod_floor_i64 round toward negative infinity instead.

The result has the operands’ type, and wraps at that type’s width. On the narrow types (i8, i16, i32, u8, u16, u32) that means:

tape
let a: u8 = 250;
let b = a + 10;        // u8: 4
let c: i8 = 127;
let d = c + 1;         // i8: -128
let x: u32 = 5;
let n = ~x;            // u32: 0xFFFFFFFA, not a 64-bit value

An integer literal takes the type of the other operand (a + 10 is a u8 operation). Two different narrow types give the wider of the two; a 64-bit operand makes the operation 64-bit. x op= v keeps x’s type, and a value stored into a narrow variable, parameter or return value wraps to it.

A float operand mixes with an integer literal, which becomes that float: x * 2, x > 0 and x += 1 with x: f32 use 2.0, 0.0 and 1.0. A typed integer must be converted with as first; x + n with n: i64 is an error (“cannot mix ‘f32’ and ‘i64’: convert the integer with ‘as’ (n as f32)”). The rule is arithmetic’s alone: an initializer, an assignment, an argument or a return of float type takes a float value, so let r: f64 = 2; is an error ('r' is f64, given an integer literal); write 2.0.

tape
var x: f32 = 1.5;
let n: i64 = 2;
x += 1;            // 2.5
x += n as f32;     // 4.5

An expression of float literals is one float literal. + - * /, unary - and parentheses over float literals (an integer literal beside them read as a float: 1.0 / 3 is 1.0 / 3.0) are worked out when the program compiles, in f64, and the result behaves as a single literal does: next to an f32 it is an f32, rounded once; alone it is an f64; given to an f32 variable, argument or return it is that f32; and an operator method taking f32 takes it. Dividing by zero there gives an infinity or NaN, as float division does at run time. An operand of integer literals alone is still integer arithmetic (x * (1 / 2) is an error), and a named constant is not a literal.

tape
let a: f32 = 133.0;
let b = a * (2.0 / 255.0);   // f32: 1.0431373
let k = 1.0 / 3.0;           // f64: 0.3333333333333333
let h: f32 = 1.0 / 3;        // f32: 0.33333334

A typed f64 is not a literal: stored into an f32 (an initializer, assignment, argument, return, field or element, and x += d on an f32 x) it needs as, where the rounding happens: let y: f32 = d; with d: f64 is an error (“‘y’ is f32, given f64: convert with ‘as’ (d as f32)”).

tape
let d: f64 = 0.1;
let y: f32 = d as f32;       // 0.1
var z: f32 = 1.5;
z += d as f32;               // 1.6

Dividing an integer by zero (/ or %) stops the program with trap: division by zero at file:line:col and exit code 101, in every profile, in tape run and in a native build. The one other edge, the most negative value divided by -1, wraps: -9223372036854775808 / -1 is -9223372036854775808, and x % -1 is 0.

On strings, + concatenates into a new owned string. Either operand can also be a []const u8 view, as long as the other is a string: "name: " + part and part + ".txt" both work with part: []const u8. See Strings.

Compound assignment

tape
x += 1;
x -= 1;
x *= 2;
x /= 2;
x %= 3;
x &= mask;
x |= flag;
x ^= bits;
x <<= 1;
x >>= 1;

They work on any place: fields (p.x += dx;), array and pointer elements (counts[i] += 1;, edges[k].x += edges[k].dx;) and pointees (*q -= 1;).

A struct place takes +=, -=, *=, /= and %= when the struct has the operator method for what is on the right and it returns the same struct:

tape
pos += vel * dt;       // pos = pos + vel * dt, by Vec3.op_add
boids[i].dir *= 0.5;
*p -= offset;

Without such a method it is a compile error that names the method it needs; a method returning another type is an error too, naming both.

The order is the same for every place and type: the place’s address once (its indices and the like evaluated once, left to right), the place read, the right side evaluated, the operation, the store to that same address:

tape
a[next()] += 1;        // next runs once
a[i] += f();           // the element i was before f, whatever f does to i
a[0] += g();           // the a[0] read before g, as in a[0] + g()

A place reached through a call, not just indexed by one, is an error (*find(x) += 1;, get(k).n -= 1;): put the pointer in a local first.

tape
let p: *Node = get(k);
p.n -= 1;

A string has no +=: s = s + x.

Bitwise

tape
let bits_and = a & b;
let bits_or = a | b;
let bits_xor = a ^ b;
let bits_not = ~a;
let shl = a << 2;
let shr = a >> 1;

On unsigned integers (u8 to u64), >> shifts in zeros, and comparisons, / and % treat the value as unsigned: 0x8000_0000_0000_0000 > 1 is true for a u64. On signed integers >> copies the sign bit (-8 >> 1 is -4).

Comparison

tape
a == b    a != b
a < b     a <= b
a > b     a >= b

Floats compare as IEEE 754 says: a NaN is unequal to everything, itself included, so every comparison with a NaN is false except !=. -0.0 == 0.0 is true. Use math.is_nan_f64(x) to test for NaN.

Logical

tape
let both = x && y;
let either = x || y;
let negated = !flag;

&& and || short-circuit: the right side runs only when the left side does not decide (ready() && send() calls send only when ready() is true).

&, | and ^ also work on two bools: and, or, exclusive or, with both sides always evaluated and no branch in between.

tape
let all_ok = check_a() & check_b();   // both checks run
var ok: bool = true;
ok &= step1();
ok &= step2();                        // runs even when step1 failed
let differ = (a < b) ^ (c < d);

They bind tighter than comparisons, so put comparisons in parentheses: (a == b) & (c == d). Mixing a bool and an integer (flag & 1) is an error; convert first (flag as u8 & 1). In constant-time code they are how secret conditions combine, since && and || would branch.

Pipe operator

tape
let result = data
    |> parse()
    |> validate()
    |> transform();

The pipe operator passes the left-hand value as the first argument to the right-hand function: a |> f(b, c) is the call f(a, b, c), a |> f is f(a), and a |> m.f(b) calls another module’s function the same way. Defaults, named arguments and every other rule of a call apply. The right-hand side must be a call or a function name; anything else (x |> 3) is a compile error.

Type cast

tape
let wide: i64 = narrow as i64;
let truncated: u8 = big as u8;
let ptr: *u8 = &status as *u8;    // address-of then cast

Prefix operators (&, *, -, !, ~) bind tighter than as/is:

tape
&x as *u8      // → (&x) as *u8
*ptr as i64    // → (*ptr) as i64
-x as f64     // → (-x) as f64

To cast before applying a prefix, use explicit parentheses:

tape
*(ptr as *i64)   // cast pointer, then dereference
-(x as f64)      // cast to float, then negate

A value that may be null or an error is taken out before it is cast. as doesn’t look inside a ?T or a T or E, so these are compile errors:

tape
mem.alloc(16) as *i64     // error: cannot cast '*u8 or MemError' as '*i64'
mem.alloc(16)! as *i64    // the pointer, or a trap if the allocation failed
maybe as f64              // error: cannot cast '?i64' as 'f64'
maybe! as f64             // the value, or a trap if it is null

A ?*T is an address already, and casts like a pointer.

Type checking with is

tape
if (widget is *component[@dispatch render]) {
    widget.render();
}

The is operator checks whether a value satisfies a component constraint at runtime. It has the same precedence as as.

String interpolation

tape
let name = "world";
let msg = "Hello, {name}!";
let expr = "2 + 2 = {2 + 2}";

Expressions inside {} within string literals are evaluated and converted to string: integers in decimal, floats in their shortest exact form (3.0 is 3), bools as true/false, enums by description or name. See Strings.

Array literals

tape
let items = [1, 2, 3, 4, 5];
let matrix = [[1, 0], [0, 1]];

Last modified: