Operators & Expressions
Arithmetic
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:
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 valueAn 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.
var x: f32 = 1.5;
let n: i64 = 2;
x += 1; // 2.5
x += n as f32; // 4.5An 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.
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.33333334A 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)”).
let d: f64 = 0.1;
let y: f32 = d as f32; // 0.1
var z: f32 = 1.5;
z += d as f32; // 1.6Dividing 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
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:
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:
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.
let p: *Node = get(k);
p.n -= 1;A string has no +=: s = s + x.
Bitwise
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
a == b a != b
a < b a <= b
a > b a >= bFloats 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
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.
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
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
let wide: i64 = narrow as i64;
let truncated: u8 = big as u8;
let ptr: *u8 = &status as *u8; // address-of then castPrefix operators (&, *, -, !, ~) bind tighter than as/is:
&x as *u8 // → (&x) as *u8
*ptr as i64 // → (*ptr) as i64
-x as f64 // → (-x) as f64To cast before applying a prefix, use explicit parentheses:
*(ptr as *i64) // cast pointer, then dereference
-(x as f64) // cast to float, then negateA 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:
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 nullA ?*T is an address already, and casts like a pointer.
Type checking with is
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
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
let items = [1, 2, 3, 4, 5];
let matrix = [[1, 0], [0, 1]];Last modified: