machdocs
Home GitHub

Types

Mach ships a small set of compiler-seeded primitive types plus a uniform grammar for building pointers, arrays, and function types out of them. There are no compiler-known aliases: names like bool, usize, and str are stdlib defs.

Primitive scalars

Eleven names make up the complete set of primitive types the compiler seeds. Everything else is built on top of them.

FamilyMembers
Unsigned intu8, u16, u32, u64
Signed inti8, i16, i32, i64
Floatf32, f64
Untyped pointerptr
Note

There is no compiler bool. It is a stdlib alias def bool: u8;, with true and false as stdlib vals (1 and 0).

SIMD vectors

A vector type is a form, not a fixed list: any primitive numeric element followed by x and a lane count.

f32x4           # 4 lanes of f32
i32x8           # 8 lanes of i32
u8x16          # 16 lanes of u8

The spelling is <u|i|f><width>x<count> with a single x. A name like f32x4x4 is not a vector type and resolves as an ordinary identifier: a matrix is an algorithm over vectors and belongs in a library, not in the language. ptr is not a lane element, since its width is target-defined rather than a scalar bit count.

Two rules bound the form, and neither depends on the target: at least 2 lanes (f32x1 is refused - a one-lane vector is just its scalar), and at most 65535 (a compiler limit; the lane count rides a 16-bit field through codegen). Everything else is legal at every width. f32x3 is 96 bits, f32x8 is 256, and both compile on every target - including one with no vector unit at all.

Width is a realization question, not a legality one. How a shape is realized does depend on the target, and there are three answers: one packed instruction where the shape fits a vector register and the operation has a packed form; a value placed in memory and worked one lane at a time where it does not; and per-lane scalar code on a target with no vector unit (riscv64 today). All three compute identical lanes - the expansion is a fixed unroll, never a reassociation - so only performance varies. A target that gains wider vector registers therefore gets better code, not new spellings. The simd profile key reports or refuses the scalar cases when a project cannot afford them; see Manifest.

A vector spelling is recognized only in type position, so a value may still be named f32x4. A type may not: rec, uni, and def reject a name spelled as a vector form, because such a type would be silently unreachable - every use in type position resolves to the vector instead.

val f32x4: i64 = 7;             # fine: values are a different position
rec f32x3 { x: f32; }           # error: `f32x3` is spelled as a vector type

Size and alignment

$size_of is lane-derived - lanes x element size, packed, with no padding at any width.

Type$size_of$align_of
f32x284
f32x3124
f32x41616
i16x482
f32x83216

$align_of has two rungs, each for its own reason. A vector narrower than the vector register is a packed aggregate that loads piecewise, so it aligns to a single lane - which is what makes [N]f32x3 a usable packed vertex buffer, since padding f32x3 to 16 bytes would make it indistinguishable from f32x4 in memory. One that fills the register aligns to its whole size, because the machine's vector load requires it. One wider than the register is placed as several register-width pieces and so aligns to the register width - 16 for f32x8, not 32.

Literals and lanes

Literals are full-arity: one initializer per lane, mirroring array literals. Too few or too many lanes is a compile error. An uninitialized vector local default-initializes to all-zero lanes.

val v: f32x4 = f32x4{1.0, 2.0, 3.0, 4.0};
var z: i32x4;                   # every lane is 0

v[i] reads or writes a single lane. The index must be a comptime constant in [0, lanes), checked the same way an array index is: v[4] on a u32x4 reports index 4 is out of bounds for `u32x4` of length 4. A dynamic lane index is not supported in this increment.

var v: f32x4 = f32x4{1.0, 2.0, 3.0, 4.0};
val x: f32 = v[0];              # read lane 0
v[3] = 9.0;                     # write lane 3

Arrays of vectors ([4]f32x4) and pointers to vectors (*f32x4) are ordinary composite types over a vector element. There are no scalar-to-vector casts in this increment: neither an implicit conversion nor a 1.0::f32x4 reinterpret is legal.

Handles

A handle is a type whose representation is not the program's: the owning target mints it and the pipeline binds it. A shader reads a texture through one.

The language knows only the machinery. A handle is a bodyless def carrying #[handle(target, constructor, operands...)], and which constructors exist, what operands each takes, and what an operand means all belong to the named target.

#[handle("spirv", "image", TEXEL_F32, DIM_2D, NO_DEPTH, NONARRAYED, SINGLE_SAMPLED, SAMPLED)]
pub def Texture2D;

#[handle("spirv", "sampled_image", Texture2D)]
pub def Sampler2D;

#[handle("spirv", "sampler")]
pub def Sampler;

def is the carrier because it already means "this name denotes a type" and promises no fields and no storage, which is exactly what a handle is. There is no body because the target supplies the definition. Operands are ordinary comptime constants, with one exception: a constructor composing over another handle takes a type name, so Sampler2D names the image it wraps rather than restating that image's operands and cannot disagree with it.

Every rule a handle carries follows from the one fact the directive states, and the set is fixed and closed rather than varied per declaration: no fields, no indexing, no construction; it cannot be a local binding, a record or union field, or sit behind a pointer or inside an array; it reaches an operation only by being passed to one, bound as a descriptor; and its extent is declared by the owning target.

$size_of a handle is the target's pointer size - it is a name for a resource, and a pointer is the shape every target already has for that. On a target that mints no such type the declaration is inert: it still denotes a type and still sizes, and an operation over it is an undefined symbol at link. A target refuses an operand combination its constructor spells but it cannot emit, naming the operand rather than the declaration.

See GPU shaders for binding a handle and sampling through one.

Pointers

*T is a pointer to a value of type T. Take an address with ? and read through it with @.

var x: i64;
var p: *i64 = ?x;       # address-of yields a pointer
val v: i64  = @p;       # dereference reads through it

Arrays

[N]T is an array of exactly N values of type T. Arrays nest as [N][M]T.

val a: [4]i64    = [4]i64{1, 2, 3, 4};
val g: [2][2]i64 = [2][2]i64{ [2]i64{1, 2}, [2]i64{3, 4} };

Constant indices are bounds-checked at compile time. N is part of the type, so an index the compiler can fold must land in [0, N).

var xs: [4]i32;
val a: i32 = xs[3];             # ok
val b: i32 = xs[4];             # error: index 4 is out of bounds for `[4]i32` of length 4

The rule is keyed on the length the type carries, not on how the array was spelled, so a def alias, an array field of a generic instance, a nested array, and a ^-qualified array are all checked the same way. It is exactly the length $length_of reports, and a vector's lane count takes the identical rule. Only a constant index is checked: a runtime index is not, and a pointer is not indexed against any length at all, since *T carries none.

Function types

fun(T1, T2) R is a first-class function-pointer type, usable as a value's type just like any scalar.

def BinOp: fun(i64, i64) i64;
val op: BinOp = add;
val r:  i64   = op(2, 3);

Named types and aliases

rec and uni declarations produce named types. A def introduces an alias for any type, including the constructed forms above.

def Bytes: [16]u8;     # Bytes now names an array type
def bool: u8;          # the stdlib's own bool alias

See also