Expressions
Expressions evaluate to values. They appear on the right of a binding, as conditions, and as call arguments.
Names
A bare identifier references a name in scope. Module-qualified names use the dot path.
counter # local or module-level binding
core.add # symbol from module core
Literals
The primitive literal forms are numeric, char, string, and nil.
42 # numeric literal
'a' # char literal
"hello" # string literal
nil # the nil value
Composite literals
A type name followed by a brace-delimited initializer builds a record, array, or union value. For generics, the type arguments appear in brackets before the body.
val p: Point = Point{ x: 1, y: 2 };
val a: [3]i64 = [3]i64{ 10, 20, 30 };
val u: Number = Number{ i: 99 };
val pair: Pair[i64, u8] = Pair[i64, u8]{ left: 5, right: 6u8 };
A vector literal takes the same shape and is full-arity: one initializer per lane, with too few or too many a compile error. See Types.
val v: f32x4 = f32x4{1.0, 2.0, 3.0, 4.0};
Field and index access
A field is read with the dot, an array element with a bracketed index.
val x: i64 = p.x; # record field
val first: i64 = a[0]; # array index
Function calls
A call applies a function to a parenthesized argument list.
add(2, 3)
Generic calls
Type arguments for a generic function appear in brackets before the call parentheses.
identity[i64](42) # generic call: type args in [ ]
Variadic calls
A variadic call passes extra trailing arguments past the fixed parameters.
sum(3, 10i64, 20i64, 30i64)
Variadic call sites parse, but the callee-side va_list machinery is not yet implemented. See Functions. For the compile-time alternative, see Variadic packs.
Comptime arguments
A comptime argument is passed positionally, just like a runtime argument. The function signature decides whether a given argument must be comptime-knowable.
checked_add(MODE_FAST, 1, 2) # MODE_FAST is comptime-knowable
Operators
Operators combine expressions into larger expressions. Precedence follows the usual C-family conventions.
val n: i64 = a + b * c; # * binds tighter than +
Lane-wise operators
Vector types carry lane-wise operators at every lane count, not only the 128-bit shapes, and which operators are legal is target-independent: an f32x8 add is as legal as an f32x4 one, and the two differ only in how they are realized.
| Lane family | + - | * | / | % | & | ^ ~ | << >> | comparisons |
|---|---|---|---|---|---|---|---|
| float | yes | yes | yes | no | - | no | same-shape unsigned mask |
| integer | yes | yes | no | no | yes | no | same-shape unsigned mask |
Both operands must be the same vector shape: there is no implicit scalar-to-vector mixing and no cross-shape widening. Anything the table marks no is a compile error, not a silent fallback.
A comparison produces the same-shape unsigned mask vector - one lane per input lane, all-ones bits for true and all-zeros for false, exactly what the hardware compare yields. There is no vector-bool type, and select is not an operator: it is the library idiom (mask & a) | (~mask & b) over matching integer lanes.
val a: f32x4 = f32x4{1.0, 2.0, 3.0, 4.0};
val b: f32x4 = f32x4{4.0, 3.0, 2.0, 1.0};
val sum: f32x4 = a + b; # lane-wise -> {5.0, 5.0, 5.0, 5.0}
val mask: u32x4 = a < b; # -> {0xFFFFFFFF, 0xFFFFFFFF, 0, 0}
A legal operator means the same thing on every target, but not every target has a packed instruction for every one. The backend picks the packed form where the target has one, else a defined unrolled scalar expansion with lane-identical results - so neither choice changes the answer. Integer * is where that is most visible: i32x4 * i32x4 is packed on NEON and a scalar expansion on the SSE2 baseline. A project that cannot afford an expansion sets simd = "require" in its profile, which turns the shortfall into a build error naming the operation, its lane width, the function, and the target.
See also
- Statements - how expressions appear inside statements
- Functions - declarations, generic and comptime parameters
- Types - record, array, union, and vector types
- Values and variables - bindings and literal forms