Structs

Basic structs

tape
struct Point {
    x: f64;
    y: f64;
}

let origin = Point { x: 0.0, y: 0.0 };

Field separators accept both , and ; in declarations and literals.

A literal names every field. Leaving one out is a compile error that names every field missing:

tape
let p = Point { x: 3.0 };
// error: Point { ... } leaves out y (give it, add '..' to zero the rest, or declare a default)

Field defaults

A field can declare a default, which a literal that leaves it out gets:

tape
struct Window {
    title: string;              // required
    width: i64 = 800;
    height: i64 = 600;
    resizable: bool = true;
    icon: ?string = null;
}

let w = Window { title: "Notes"; height: 400; };   // width 800, resizable, no icon

A default is a constant of the field’s type: a literal (number, string, bool, color, null), [] for an empty slice, an array literal of these for a fixed array (up: [3]f64 = [0.0, 1.0, 0.0];), an enum variant, a const (an array const too), or integer arithmetic on them (= PAGE * 4). It is evaluated once, at compile time, where the struct is declared, so another module’s literal gets the same value. Defaults can be on any field, in any order. Adding a field with a default to a struct keeps every existing literal compiling.

..: zero the rest

A last .. zeroes every field the literal leaves out that has no default: 0, false, null, an empty slice or string. A defaulted field still gets its default.

tape
let p = Point { x: 3.0; .. };              // y is 0.0
var wc = WNDCLASSEXW { cbSize: 80; lpfnWndProc: wnd_proc; .. };
let w = Window { title: "x"; .. };         // width 800: defaults still apply

Use .. where zero is deliberate for many fields (OS and C structs, scratch state); name a field where its value matters; give a field whose zero is its natural value a default. var p: Point; (a declaration without a literal) is still all zero. Tagged-union payloads follow the same rules: Ev.Key { code: 1; .. }.

Field access

tape
let px = origin.x;

Methods

Use Type.name syntax to declare methods:

tape
struct Vec2 { x: f64; y: f64; }

fn Vec2.length(*const self) -> f64 {
    return math.sqrt_f64(self.x * self.x + self.y * self.y);
}

fn Vec2.normalize(*const self) -> Vec2 {
    let len = self.length();
    return Vec2 { x: self.x / len, y: self.y / len };
}

fn Vec2.flip(*self) {
    self.x = -self.x;
}

The first parameter is self (by value), *self (by pointer, may change the struct: needs a var) or *const self (by read-only pointer: works on a let too). See Methods.

A method is found by the receiver’s type alone: v.m() is m of v’s type, whether v is a local, a constant (UP.extend(0.0), geo.UP.extend(0.0)), a field, an element or a call’s result. A type with no m is a compile error, never another type’s m.

Swizzle access

Structs can declare swizzle patterns with @swizzle to enable GLSL-style field access:

tape
@swizzle xy: Vec2;
@swizzle xyz: Vec3;
struct Vec4 { x: f64; y: f64; z: f64; w: f64; }

let v = Vec4 { x: 1.0, y: 2.0, z: 3.0, w: 4.0 };
let xy = v.xy;    // Vec2 { x: 1.0, y: 2.0 }
let zw = v.zw;    // Vec2 { x: 3.0, y: 4.0 }: fields fill in order

The @swizzle attribute maps a field-name pattern to a result type. It cannot be used with @packed or @opaque structs.

A swizzle works on any value of the struct, not only a variable: a field (box.v.xy), a call’s result (corner().yx), an operator’s result ((a + b).zw).

Struct attributes

tape
@packed
struct PacketHeader {
    magic: u32;
    length: u16;
    flags: u8;
}

@align(16)
struct SimdVec {
    data: [4]f32;
}

@opaque
struct Handle {
    id: u64;
}
  • @packed — no padding between fields (alignment 1): PacketHeader is 7 bytes, length at offset 4 and flags at 6, like C’s #pragma pack(1). Unaligned fields read and write normally. It packs bytes, not bits.

  • @align(N) — minimum alignment (a power of two, at most 16): SimdVec’s alignment is 16 and its size rounds up to a multiple of 16. Locals, heap values and globals of the type are 16-byte aligned, as C’s _Alignas(16).

  • @opaque — fields not visible outside the module

@sizeof and @alignof give the same numbers in @tape code and at runtime.

Tape has no bitfields: a field is as wide as its type (flags: u8 : 3 and u3 are errors). C leaves bitfield layout up to each compiler, so it can’t describe a register or wire format reliably. Declare the integer that holds the bits and use shifts and masks:

tape
@packed
struct UartRegs { data: u32; status: u32; }     // status: [0] ready, [1..4] level

fn ready(r: *const UartRegs) -> bool { return (r.status & 1) != 0; }
fn level(r: *const UartRegs) -> u32 { return (r.status >> 1) & 0x7; }

Last modified: