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 values: data with no identity

Area: Getting started Teaches: the VALUE side of the value/ontology boundary — struct and enum as language built-ins (§5.1). Pure data: structural equality, immutability, no metatype, no fact-store identity. How to construct a struct value, project a field, do a functional update with ..spread, build an enum payload value, read a payload back out with a value-position match, and read an optional field’s payload with the rule-body is Some(a) form. Prerequisites: hello (the shape of a source file). Run: ox check examples/struct_enum_values && ox test examples/struct_enum_values

Argon draws a line between ontology and data. A type (or any metatype-introduced concept) declares an entity with identity — an individual the fact store mints an id for, classifies, and reasons over. A struct or enum declares plain data: a value that is its contents, with no identity and no place in the metatype calculus. The two keywords are language built-ins, always available; the classifying vocabulary (type, rel) is imported.

This example is the data side. Everything here is a value.

A struct is its fields

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

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

A struct value is constructed by naming the type and giving every field. It is an ordinary expression — it can be a let binding, a field value, a function argument or return, a list element, or either side of a comparison. The postfix .x projects a field back out.

Equality is structural. Two struct values are equal when their fields are equal, field by field — there is no identity to tell them apart, and field order in the literal does not matter:

test "struct values are equal field-wise, order-independent" {
    assert Point { x: 1, y: 2 } == Point { y: 2, x: 1 };
    assert Point { x: 1, y: 2 } != Point { x: 1, y: 3 };
}

Immutability and ..spread

A struct value never changes. To get a value that differs in a few fields, copy an existing one and override those fields with the ..base spread — it yields a new value and leaves the base untouched:

test "spread overrides one field and leaves the base untouched" {
    let p = Point { x: 1, y: 2 };
    assert Point { ..p, x: 5 } == Point { x: 5, y: 2 };
    assert p == Point { x: 1, y: 2 };
}

Because the value is immutable, mut on a struct field is meaningless and refused (OE0253), and insert — which mints identity — is refused on a struct (OE0252). A struct is constructed as a value, never inserted.

Enum values, with and without a payload

pub enum Price { Cents(Int), Free }

A payloadless variant is a path: Price::Free. A payload variant carries one value and is built by applying the variant to it: Price::Cents(500). Enum values compare structurally too — same variant, same payload:

test "enum payload values compare structurally" {
    assert Price::Cents(500) == Price::Cents(500);
    assert Price::Cents(500) != Price::Cents(400);
    assert Price::Free != Price::Cents(0);
}

The struct-row table: a parameterized test

A struct value is the natural carrier for a parameterized test: one Case per scenario, looped with for. Each row is a value; the loop runs the same assertion over all of them.

pub struct Case { lhs: Int, rhs: Int, sum: Int }
test "addition over a table of struct cases" {
    for c in [
        Case { lhs: 0,  rhs: 0,  sum: 0 },
        Case { lhs: 2,  rhs: 3,  sum: 5 },
        Case { lhs: 20, rhs: 22, sum: 42 },
    ] {
        assert c.lhs + c.rhs == c.sum;
    }
}

Reading a payload back out

The idiomatic way to read or consume an enum value is a value-position match expression. Each arm names a variant; a payload arm binds the carried value into a fresh variable over the arm’s result. As an expression it sits anywhere a value can — a let right-hand side, a comparison operand, a fn body:

test "value-position match reads an enum payload" {
    let p = Price::Cents(500);
    assert (match p { Price::Cents(c) => c, Price::Free => 0 }) == 500;

    let free = Price::Free;
    assert (match free { Price::Cents(c) => c, Price::Free => 0 }) == 0;
}

For an optional field (age: Int?) inside a rule body, the reader is the membership test path is Some(binder): a present field binds the variable and contributes one row; an absent field contributes none, so the surrounding conjunction fails. is None is the absence test.

pub type Person { mut age: Int? }

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

Running it

$ ox check examples/struct_enum_values --codes
ok

$ ox test examples/struct_enum_values
PASS  value-position match reads an enum payload
PASS  struct values are equal field-wise, order-independent
PASS  spread overrides one field and leaves the base untouched
PASS  projection reads a field off a struct value
PASS  enum payload values compare structurally
PASS  addition over a table of struct cases

6 passed, 0 failed, 0 errored, 0 inconclusive

Honest caveats (what runs today)

  • A user-declared generic struct/enum (enum Opt<T> { … }) parses, but its type parameters reach no storage slot, so applying it (Opt<Int>) is refused (OE0235). The library generics Option<T> / Result<T, E> and the optional-field form T? are the supported parameterized shapes.
  • A payload-binding match arm executes in value position (let r = match opt { Some(x) => x, None => 0 };, shown above) — but only there. The same arm in statement position (an effectful arm inside a mutate statement-match, e.g. match opt { Some(v) => { let _ = insert T { n: v }; }, None => {} }) is not yet executable — it is refused with OE1319 rather than mis-evaluated. Bind the payload in value position first, then run the effect. match over constant patterns (payloadless variants, literals, _) executes in both positions (§14).
  • A match over an enum declared in a different module (e.g. a test in tests/mod.ar matching an enum from root.ar) currently can’t resolve the scrutinee’s enum statically and is refused OE1319; keep the payload match in the same file as the enum (this example’s value-match test lives in root.ar).
  • Calling a user fn inside a test assertion is not yet wired in the term evaluator, so the tests above construct and compare values directly rather than through the fn wrappers in root.ar. The functions still build and check; they are exercised by ox check / ox build.

This example is compiled, checked, and run through the same ox pipeline as the rest of the corpus; the whole-corpus gate (oxc-driver/tests/examples_corpus_gate.rs) ox checks it at source HEAD, so a language change that breaks its parse or elaboration fails CI rather than letting the docs go stale. The value-position payload-match semantics this example shows are independently gated end-to-end (construct, bind, select) by enum_payload_construct_equality_and_match_binding in oxc-driver/tests/cli_pipeline.rs.