Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

struct and enum — language built-ins (data declarations)

struct-decl ::= attribute* 'pub'? 'struct' Ident generic-params? where-clause? '{' field-list '}'
enum-decl   ::= attribute* 'pub'? 'enum'   Ident generic-params? where-clause?
                  '{' variant (',' …)* ','? '}'
field-list  ::= field-decl (',' …)* ','?
field-decl  ::= attribute* 'mut'? Ident ':' TypeExpr ('=' expr)? ('from' relation-ref)?
variant     ::= Ident ( '(' TypeExpr (',' …)* ')' | '{' field-list '}' )?
pub struct Diagnostic {
    severity: Severity,
    code:     String,
    message:  String,
    range:    Option<Range>,
}

pub enum Severity { Error, Warning, Info, Hint }
pub enum Option<T> { Some(T), None }
pub enum Result<T, E> { Ok(T), Err(E) }
pub enum Shape { Circle, Square, Triangle }

One enum spelling. enum has a single declaration form — the brace list above — matching Rust and the only form that carries payloads. An earlier enum Name = A | B | C pipe spelling is removed (OE0009). Concept cover declarations (Vocabulary concepts and the generic type metatype) take the matching brace form pub kind Name { A, B } (OE0012 on the old = A | B pipe), so no =-pipe surface survives.

struct/enum declarations carry no metatype classification. They are language-level data shapes — structural equality, no fact-store identity beyond storage, no participation in the metatype calculus. They cover stdlib data records (Diagnostic, Severity, Option, Result, Path, Range, Truth4) and user plumbing types. meta() reflection (Reflection) does not apply.

Struct and enum values

A struct value is constructed by naming the type and giving every field: Row { a: 1, b: 2 }. This is an ordinary expression — it can be a let binding, a field value, a function argument, a return value, a list element, or either side of a comparison. The full construction and projection grammar lives in the expression grammar (Expression grammar); this section states the data semantics.

pub struct Point { x: Int, y: Int }

pub fn origin() -> Point = Point { x: 0, y: 0 };
pub fn project_x(p: Point) -> Int = p.x;

Structural equality. Two struct values are equal when their fields are equal, field by field — there is no identity to compare. Field order in the literal does not matter: Row { a: 1, b: 2 } and Row { b: 2, a: 1 } are the same value. Equality is recursive: a struct whose field is itself a struct compares that field structurally too. This is what “pure data” means concretely — a struct is its fields, nothing more.

pub struct Row { a: Int, b: Int }

test "structural equality is field-wise and order-independent" {
    assert Row { a: 1, b: 2 } == Row { b: 2, a: 1 };
    assert Row { a: 1, b: 2 } != Row { a: 1, b: 3 };
}

Immutability. A struct value is immutable: its fields are fixed at construction and never change. There is no in-place update. To produce a struct that differs from an existing one in a few fields, use functional update — the ..base spread — which yields a new value and leaves the base untouched (Expression grammar):

pub struct Row { a: Int, b: Int }

test "spread is a functional update yielding a new value" {
    let base = Row { a: 1, b: 2 };
    assert Row { ..base, a: 9 } == Row { a: 9, b: 2 };   // a overridden, b carried
    assert base == Row { a: 1, b: 2 };                   // base is unchanged
}

Because a struct value is immutable, mut on a struct field is meaningless and is refused — mut field: Type on a struct raises OE0253 MutOnStructField. The mut modifier marks a concept field updatable by an update … set { … } statement inside a mutate body (mutate); that notion belongs to identity-bearing concept individuals, not to value-semantic data.

pub struct Point { mut x: Int, y: Int }

No identity — insert is refused. insert T { … } mints an identity and records an iof classification; it is how concept individuals are born. A struct has no metatype and no identity, so insert on a struct type is refused — OE0252 InsertOfStruct. Construct the struct as a plain value instead (drop the insert).

pub struct Point { x: Int, y: Int }

pub mutate make() {
    let p = insert Point { x: 1, y: 2 };
}

A struct literal must state every field exactly. A missing field is OE0249 StructFieldMissing, an undeclared field is OE0250 StructFieldExtra, and a field given the wrong type is OE0251 StructFieldTypeMismatch. These are build-time refusals, named to the field, not deferred to runtime.

Enum values. A payloadless variant is written as a path: Severity::Error, Shape::Circle. A payload variant carries one value and is constructed by applying the variant to that value: Msg::Text("hi"). Enum values compare structurally, the same as structs — Msg::Text("hi") == Msg::Text("hi"), and a payload variant never equals a different variant. A payload variant takes exactly one value; any other count is OE0256 EnumPayloadArity.

pub enum Msg { Text(String), Empty }
pub struct Mail { body: Msg }

pub fn greeting() -> Mail = Mail { body: Msg::Text("hi") };
pub fn blank()    -> Mail = Mail { body: Msg::Empty };

User-concept generics are decorative. A user-declared generic enum Opt<T> (or struct) parses, but its type parameters reach no storage slot, so applying it — Opt<Int> — is refused (OE0235). The optional and result shapes used in practice are the library generics Option<T> / Result<T, E> and the optional-field form T?, whose argument shapes the elaborator does interpret. Payload binding for the optional form is the rule-body is Some(a) / is None test (next paragraph) or a value-position match expression (let v = match opt { Some(x) => x, None => d };, below); a payload-binding arm in statement position refuses with OE1319.

Reading an enum payload — is Some(a). Inside a rule body, an optional field’s payload binds with the membership test path is Some(binder), and absence tests with path is None. A present field materializes one row binding the payload; an absent field materializes none, so the surrounding conjunction fails (RFD 0007 Rule-atom grammar):

pub type Person { mut age: Int? }

pub derive Adult(p, a)   :- p: Person, p.age is Some(a), a >= 18;
pub derive HasNoAge(p)   :- p: Person, p.age is None;

Reading an enum payload — value-position match. A match expression in value position — a let right-hand side, a comparison operand, a fn body, a return/require value — binds an enum payload and evaluates. let r = match opt { Some(x) => x, None => 0 }; reads the payload into x and yields the selected arm’s value; this is the idiomatic way to read or consume an enum value. The arm binds exactly one variable positionally; a zero-, multi-, or record-binder arm is OE0257 EnumPayloadPattern (Expression grammar).

pub enum Opt { Some(Int), None }

test "value-position match binds the payload and evaluates" {
    let some = Opt::Some(7);
    let none = Opt::None;
    assert (match some { Opt::Some(x) => x, Opt::None => 0 })  == 7;
    assert (match none { Opt::Some(x) => x, Opt::None => 99 }) == 99;
}

A payload-binding arm in statement position — an effectful arm body inside a mutate statement-match, e.g. match opt { Some(v) => { let _ = insert T { n: v }; }, None => {} } — refuses with OE1319 rather than silently mis-evaluating. Bind the payload in value position (let v = match opt { … };) first, then run the effect. The rule-body is Some(a) form above is the other supported reader; the constant-pattern subset of match (payloadless variants, literals, _) executes in both positions (Pattern matching).

Field defaults. The field-default form field: Type = expr refuses with OE0237 FieldDefaultUnsupported: the parser recognizes the = expr clause and rejects it loudly rather than silently dropping the default. Supply the value at the construction or kind-level binding site, or use the from-navigation form (next paragraph) for a derived value.

Field navigation views — from. Unaffected by the above: a field may carry a from Rel.endpoint navigation-view clause supplying a derived value (RFD 0005, relation subsumption). This form is parsed and elaborated normally; it is the supported way to compute a field’s value from related entities.

Field mutability — mut. A field declaration may carry the mut modifier between any leading attributes and the field name: mut field: Type. Without mut, the field is immutable post-construction — its value is fixed at the construction site (or by #[intrinsic] kind-level binding, or by from-clause derivation) and may not subsequently be re-assigned. With mut, the field admits update-stmt writes inside mutate bodies (mutate).

pub type Person {
    name: String,                          // set at construction; not updatable
    dob: Date,                             // set at construction; not updatable
    #[intrinsic] ssn: String,              // must be set at kind binding; not updatable
    mut current_address: String,           // updatable via `update`-stmt
    mut current_employer: Company?,
    #[intrinsic] mut current_role: Role,   // required at construction AND updatable later
}

mut is orthogonal to the other field-decl modifiers: #[intrinsic] governs construction-time required-ness, not updatability (both can apply); a mut on a from-derived field is contradictory and rejected with OE0822 MutOnDerivedField; a default = expr supplies a fallback at construction; the metatype’s fixed modifier (metatype, mutate) governs whether iof classification can change, not whether fields can, so a fixed kind admits mut fields. The modifier reads identically on struct, enum-variant, and vocabulary-concept fields. Mutations lower to append-only retract/assert event pairs on the axiom log (Storage layer); the proposition’s (entity_id, property_id) identity is stable across edits, so bitemporal queries reconstruct prior values (Temporal substrate).