ADR-0038: Enum Struct Variants (Named-Field Enum Variants)
Status
Implemented
Summary
Extend Gruel enums to support struct-like variants with named fields, in addition to the existing unit and tuple-style variants. This enables Enum::Variant { field: value } construction and Enum::Variant { field } pattern matching — the same syntax used for struct expressions, applied to enum variants.
Context
What Exists
ADR-0004 implemented C-style enums (unit variants only). ADR-0037 added tuple-style data variants:
enum IntOption { Some(i32), None }
let x = IntOption::Some(42);
match x {
IntOption::Some(v) => v,
IntOption::None => 0,
}
Enum data variants have been stabilized as of ADR-0037 Phase 6.
What's Missing
There is no way to define enum variants with named fields:
// Not yet possible:
enum Shape {
Circle { radius: i32 },
Rectangle { width: i32, height: i32 },
Point,
}
let s = Shape::Circle { radius: 5 };
match s {
Shape::Circle { radius } => radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Point => 0,
}
Named fields improve readability when variants carry multiple fields of the same type, where positional arguments are error-prone. Compare:
// Tuple-style: which is width, which is height?
let r = Shape::Rectangle(10, 20);
// Struct-style: self-documenting
let r = Shape::Rectangle { width: 10, height: 20 };
Scope of This ADR
This ADR covers:
- Struct-like variant definitions —
Variant { field: Type, ... }in enum declarations - Struct-like variant construction —
Enum::Variant { field: expr, ... }expressions - Struct-like variant pattern matching —
Enum::Variant { field: binding, ... }in match arms - Field init shorthand —
{ field }means{ field: field }in both construction and patterns
Out of scope (future ADRs):
- Generic enum variants
- The
..rest pattern for ignoring fields in match arms - Nested patterns in bindings
- Update syntax (
Enum::Variant { field: new_val, ..existing })
Decision
1. Three Variant Kinds
Each enum variant is exactly one of:
| Kind | Definition | Construction | Example |
|---|---|---|---|
| Unit | Variant | Enum::Variant | Color::Red |
| Tuple | Variant(T1, T2) | Enum::Variant(e1, e2) | Option::Some(42) |
| Struct | Variant { f: T } | Enum::Variant { f: e } | Shape::Circle { radius: 5 } |
An enum can freely mix all three kinds:
enum Message {
Quit, // unit
Echo(String), // tuple
Move { x: i32, y: i32 }, // struct
}
A variant with ( ... ) is always tuple-style. A variant with { ... } is always struct-style. The two cannot be combined on a single variant.
2. Syntax
Definition Grammar
enum_variant = IDENT [ variant_fields ] ;
variant_fields = "(" type_list ")" (* tuple variant *)
| "{" struct_variant_fields "}" ; (* struct variant *)
struct_variant_fields = struct_variant_field { "," struct_variant_field } [ "," ] ;
struct_variant_field = IDENT ":" type ;
Construction Grammar
enum_struct_expr = IDENT "::" IDENT "{" [ field_inits ] "}" ;
field_inits = field_init { "," field_init } [ "," ] ;
field_init = IDENT ":" expression
| IDENT ; (* shorthand: name inferred from variable *)
Construction rules (same as struct expressions):
- All fields MUST be initialized — no partial initialization.
- Field initializers MAY be in any order.
- Field initializer expressions are evaluated left-to-right in source order.
- Field init shorthand:
{ x }is equivalent to{ x: x }.
Pattern Grammar
enum_struct_pattern = IDENT "::" IDENT "{" [ field_patterns ] "}" ;
field_patterns = field_pattern { "," field_pattern } [ "," ] ;
field_pattern = IDENT ":" pattern_binding
| IDENT ; (* shorthand: bind to same name *)
pattern_binding = "_" | [ "mut" ] IDENT ;
Pattern rules:
- All fields MUST be listed — no partial matching (no
..in v1). - Field patterns MAY be in any order.
- Field punning:
{ radius }binds theradiusfield to variableradius. { radius: r }binds theradiusfield to variabler.{ radius: _ }discards theradiusfield.{ radius: mut r }binds mutably.
3. Memory Layout
Struct variants use the identical memory layout as tuple variants — fields are stored sequentially in declaration order within the payload byte array. The name-to-index mapping is resolved entirely at compile time; there is no runtime difference between a struct variant and a tuple variant with the same field types in the same order.
This means struct variants inherit all layout properties from ADR-0037:
{ iD, [N x i8] }tagged union representation- Discriminant sizing (u8/u16/u32/u64)
- Packed payload with unaligned loads/stores
- Largest-variant payload sizing
4. Type System Changes
EnumVariantDef in gruel-air/src/types.rs is extended with optional field names:
This is a minimal extension: tuple variants continue to have field_names: vec![], and all existing code that only uses fields is unaffected.
5. Parser Changes
The parser's IdentSuffix enum gets a new variant for ::Variant { ... }:
There is no ambiguity because :: clearly introduces a path, and the { after the variant name unambiguously starts a field list (as opposed to ( for tuple construction or nothing for unit variants).
The AST EnumVariant node is extended to support named fields:
A new Pattern variant is added for struct variant patterns:
6. Semantic Analysis
Construction of struct variants follows the same logic as struct expression analysis:
- Look up the enum and variant by name
- Verify the variant is a struct variant (has
field_names) - Match each field initializer name to a declared field
- Check for missing and duplicate fields
- Type-check each field value against the declared type
- Reorder fields to declaration order for the
EnumCreateAIR instruction - Track source evaluation order (left-to-right)
Pattern matching follows similar logic:
- Look up the enum and variant by name
- Verify the variant is a struct variant
- Match each field pattern name to a declared field
- Check for missing and duplicate fields
- Resolve field names to indices
- Emit
EnumPayloadGetwith the resolved field indices
7. Codegen
No new codegen instructions are needed. Struct variants reuse:
EnumCreatefor construction (fields passed in declaration order)EnumPayloadGetfor field extraction in patterns
The field name resolution happens entirely in sema; by the time we reach AIR/codegen, struct variants are identical to tuple variants.
8. Error Messages
New diagnostic cases:
- "unknown field
fooin variantShape::Circle" - "missing field
radiusin variantShape::Circle" - "duplicate field
radiusin variantShape::Circle" - "variant
Shape::Circlehas named fields; useShape::Circle { ... }instead ofShape::Circle(...)" - "variant
Option::Somehas positional fields; useOption::Some(...)instead ofOption::Some { ... }"
The last two catch attempts to use the wrong construction syntax for a variant's kind.
9. Interaction with Existing Features
Tuple variants: Unaffected. Tuple and struct variants are distinct at the definition site and cannot be mixed on the same variant.
C-style/unit variants: Unaffected.
Drop dispatch (ADR-0037): Struct variants participate in drop dispatch identically to tuple variants — the discriminant check dispatches to per-variant drop code that drops each field.
Ownership: Same as tuple variants. When matched, fields are moved out. Wildcard _ discards (drops). Copy types are copied.
Exhaustiveness: Struct variant patterns exhaust the variant regardless of field binding order. The field bindings don't affect exhaustiveness — only variant coverage matters.
Implementation Phases
Phase 1: Struct variant declarations (parsing + type system)
- Extend AST
EnumVariantwithEnumVariantKind(unit/tuple/struct) - Parser: parse
Variant { field: Type, ... }in enum definitions - RIR: extend enum variant encoding to store field names
- AIR: add
field_names: Vec<String>toEnumVariantDef - Sema gather: collect field names, check uniqueness, type-check field types
- Add
PreviewFeature::EnumStructVariantsingruel-error - Gate behind preview feature
- Ensure existing enum tests still pass
- Extend AST
Phase 2: Struct variant construction expressions
- Parser: add
PathStructLit(Ident, Vec<FieldInit>)toIdentSuffix - AST: new
Exprvariant or extend existing forEnum::Variant { ... } - RIR: lower struct variant construction (reuse
EnumVariantinst with field refs, or add a new instruction) - Sema: resolve field names to indices, type-check, reorder to declaration order
- Support field init shorthand (
{ x }means{ x: x }) - Support fields in any order
- Error diagnostics for missing/unknown/duplicate fields
- Error when using
( )on a struct variant or{ }on a tuple variant - Codegen: no changes needed —
EnumCreatealready handles this
- Parser: add
Phase 3: Struct variant pattern matching
- Parser: parse
Enum::Variant { field: binding }patterns - AST: new
Pattern::StructVariantvariant - RIR: new
RirPattern::StructVariantwith named field bindings - AIR: extend
AirPatternfor struct variant patterns - Sema: resolve field names to indices, bind variables by name
- Support field punning (
{ radius }binds to variableradius) - All fields must be listed (no
..) - Codegen: reuse
EnumPayloadGetwith resolved field indices
- Parser: parse
Phase 4: Spec, tests, and stabilization
- Update spec section 6.3 with struct variant rules
- Update grammar appendix
- Add comprehensive spec tests with traceability
- Ensure
make testpasses with full traceability - Remove preview gate
Consequences
Positive
- Named fields make multi-field variants self-documenting and order-independent
- Consistent with struct expression syntax — users learn one pattern
- No new codegen complexity — struct variants are tuple variants with compile-time name resolution
- Minimal type system extension (just adding
field_namestoEnumVariantDef)
Negative
- Three variant kinds increases the surface area of enum syntax
- More parser complexity (new
IdentSuffixvariant, new pattern kind) - Error messages must distinguish tuple vs struct variants and guide users to the right syntax
Open Questions
- Should field init shorthand be included from the start, or deferred? (Proposed: include from start for consistency with struct expressions.)
- Should we allow
..in struct variant patterns to ignore remaining fields? (Proposed: not in v1, future ADR.)
Future Work
..rest pattern in struct variant patterns (ignore remaining fields)- Nested patterns in field bindings (e.g.,
Shape::Nested { inner: Shape::Circle { radius } }) - Update syntax for struct variants