machdocs
Home GitHub

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)
Not yet implemented

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
floatyesyesyesno-nosame-shape unsigned mask
integeryesyesnonoyesnosame-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