machdocs
Home Ecosystem Changelog

Records, unions, and tags

A rec is a named collection of typed fields, each with its own storage. A uni overlaps its fields in the same memory. A tag defines a discriminated value with compiler-enforced lexical guards.

Records

A rec lays out its fields side by side. Each field has independent storage, and the record's size is the sum of the field sizes plus any padding the compiler inserts for alignment. A record may be generic over type parameters.

rec NAME {
    field1: type;
    field2: type;
    ...
}

rec NAME[T, U] { ... }      # generic over type parameters
pub rec Point {
    x: i64;
    y: i64;
}

pub rec Pair[T, U] {
    left:  T;
    right: U;
}

Construction and access

A record literal names the type and supplies each field by name. Read a field with ..

val p: Point          = Point{ x: 1, y: 2 };
val q: Pair[i64, u8]  = Pair[i64, u8]{ left: 5, right: 6u8 };
val n: i64            = p.x;            # field access via .

Memory layout

By default the compiler may insert padding between fields to satisfy each field's alignment, following the natural C-style rule. The #[align(N)] decorator on a record raises its minimum type alignment to N bytes, where N is a power of two.

#[align(16)]
pub rec Aligned {
    a: u8;
    b: i64;
}

#[packed] is the inverse: every field sits immediately after the previous one, there is no tail padding, and the type takes no alignment from its fields. That is what describes a shape whose layout is not mach's to choose - a C struct, a file header, a wire frame. The two compose, each owning one question: packed decides padding, align decides the record's own alignment.

#[packed]
rec Header {
    magic:    u8;    # offset 0
    version:  u16;   # offset 1
    length:   u32;   # offset 3
    checksum: u64;   # offset 7
}                    # $size_of == 15, $align_of == 1

Packing is not transitive: a record a packed record contains keeps its own internal padding and is merely placed without any. Taking the address of a packed field is refused, since the resulting pointer type would state an alignment its storage does not have; ?r on the whole record stays legal. See Decorators for the full rule set.

Unions

A uni is a collection of named fields that share the same memory. Writing one field overwrites whatever bytes the others held. A union's size is the size of its largest field plus any alignment padding. Unions may be generic.

pub uni Number {
    i: i64;
    f: f64;
}

pub uni Maybe[T] {
    some: T;
    none: u8;
}
Restriction

The compiler does not track which field of a union is live. Reading a field other than the one last written reinterprets raw bytes; keeping the active field straight is the programmer's responsibility.

Tags

A tag defines a discriminated value with compiler-enforced payload safety. Unlike an untracked union, a tag stores a discriminator alongside an overlapping payload area, and the compiler prevents reading any payload unless the active case is proven by a dominating guard.

The discriminator type is written after the tag name and must be an unsigned integer type from u8 through u64. Cases may carry a single typed payload or remain payload-free.

pub tag Shape: u8 {
    circle: f64;
    rect: Rect;
    point;
}

pub tag Option[T]: u8 {
    none;
    some: T;
}

Values are constructed using the type name, case name, and curly braces. A payload case supplies its value inside the braces, while a unit case uses empty braces.

val c: Shape = Shape.circle{3.14};
val p: Shape = Shape.point{};

Payload guards and sel

The sel keyword tests whether a tag instance currently holds a specific case. The expression sel place.case yields a bool.

Reading a case payload is permitted only where a lexical guard guarantees that case is active. Accessing a payload without a dominating guard produces a compile error.

fun area(s: Shape) f64 {
    if (sel s.circle) {
        ret 3.141592653589793 * s.circle * s.circle;
    }
    if (sel s.rect) {
        ret s.rect.w * s.rect.h;
    }
    ret 0.0;
}

Guards obey strict lexical scope rules:

Guard restrictions

Guards must be unambiguous. An or branch is not guarded because the condition evaluated to false. Logical || and negation with ! do not open a payload guard. Test one condition at a time.

See also

  • Decorators - #[deprecated] on declarations and cases
  • Intrinsics - $cases, $is_tag, and $discriminant_of for tag reflection
  • Statements - control flow and conditional guards
  • Types - aggregate and primitive type definitions