Luce Base
LuceSourceLuciaOS

10. Structs, enums, and unions

10.1 Structs#

pub struct Style:
    pub let color: Color = Color(0.0, 0.0, 0.0, 1.0)
    pub let width: f64 = 1.0
    var cache: Layout*?

pub struct Percentage:
    let value: f64

    pub func init(value: f64) -> !:
        if value < 0.0 or value > 100.0:
            error(out_of_range, "percentage must be 0 through 100")
        self.value = value

func main(arguments: str[]) -> i32!:
    let thin = Style(width = 0.5)                 # color defaulted; cache is nullable and defaults to none
    let opacity = try Percentage(75.0)
    return 0
  • Fields use let or var and are private to the module unless pub. Declaration order is layout order (§5.11).
  • Without an init, the compiler synthesises a memberwise initialiser, Style(color = ..., width = ...), positional or named. A field with a default may be omitted; a field without one whose type has a zero value (§6.1) may be omitted and receives that value; every other field is required. The initialiser is public when the struct and every required field are public.
  • A custom init(...) returns unit or !, assigns every field exactly once, and cannot read a field or call a method before every field is assigned. Declaring one suppresses the memberwise form. Construction Percentage(75.0) produces Percentage or Percentage!.
  • A struct copies by value. There is no spread or update syntax; a mutable local updates var fields, and immutable transformations are named methods.
  • A struct with all-hashable fields is equatable and hashable structurally (§7.4).
  • A struct cannot contain itself directly; use a pointer.

10.2 Enums#

pub enum Direction:
    north
    east
    south
    west

pub enum Command:
    open(path: str)
    save(path: str)
    resize(width: u32, height: u32)
    quit

A case is written Direction.north or .north where the type is known. A payload is reached only by match. The tag and payload layout is the compiler's; ordinary code never sees an ordinal. Adding a case is a source-compatibility change because every exhaustive match must be updated, which is intended. A case may not be named none. An enum cannot contain itself by value through a payload, whether directly, through an optional, a tuple, an array or a struct, as a struct cannot (§10.1); a pointer breaks the cycle.

10.3 Integer-backed enums#

enum Access as u32:
    empty = 0
    read = 1
    write = 2
    execute = 4

let mode = Access.read | Access.write
if (mode & Access.write) != Access.empty: ...
let bits = (u32)mode                     # 3
let kind = Access(value)                 # traps unless value is a declared case
let raw = (Access)value                  # no check

An enum declared as an integer type with explicit values has that representation. |, &, ^, and ~ are defined and produce the same enum type, so flags combine and test without conversion. Because a combination need not be a declared case, a match on an integer-backed enum requires a _ arm. (u32)flag reads the representation; Access(n) is the checked conversion and traps on an undeclared value; (Access)n is the C cast and does not. Its C header form is §17.6.

Why. C enums are integers and are used as flags and array indices; a Base that refused that could not bind C libraries. The operators produce undeclared values by design, which is why the _ arm is mandatory: the compiler cannot enumerate what a flag set may hold.

10.4 Unions#

union Value:
    integer: i64
    real: f64
    bytes: u8[8]

A union stores one member at one address. Its members are written name: Type, without let or var. Its alignment is its most-aligned member's, and its size is its largest member's size rounded up to that alignment. Reading a member other than the last written reinterprets the bytes, the rule of C11 §6.5.2.3 footnote 95. A member may be of any type, and pub before a member opens it to other modules as it does a field; reading a member whose type has an invariant, a bool, an enum, a bare pointer, a str, an optional, after another member was written is undefined (§12.6), as it is in C. A union is zeroed as bytes, may declare methods, and cannot implement interfaces or cross into full Luce.

Why. The tagged enum is the safe sum type. The raw union exists because C has it, C libraries expose it, and type punning through it is defined C. Members of every type are admitted because a union declared in Base must be able to mirror one bound from C.