tagged unions

This commit is contained in:
2026-06-28 22:54:20 +02:00
parent f328c44154
commit 981ccb047a
7 changed files with 396 additions and 14 deletions
+115 -5
View File
@@ -412,7 +412,7 @@
a Animal
}
# tagged unions (constrained to the backing enum) — DEFERRED to 21.5
# tagged unions (constrained to the backing enum) — see 21.5
AnimalNameOrHeight :: union(Animal) { # use just `enum` instead of `Animal` for unconstrained tagged union
dog []u8
cat []u8
@@ -440,10 +440,33 @@
runtime `{tag, payload}` layout, tag init at construction, and tag extraction — the real codegen
work and the foundation for milestone 22 (`match` with payload unwrapping)
21.5. tagged unions with a runtime tag (DEFERRED)
- `union(Enum)` (variant names constrained to the backing enum's members) and `union(enum)`
(auto-generated tag, one member per variant); both store an enum tag beside the payload
(`{tag, payload}`), switchable in milestone 22
21.5. tagged unions with a runtime tag (implemented; see below)
- `union(Enum)` (variant names constrained to an existing enum's members) and `union(enum)`
(compiler-synthesized anonymous tag enum, one dense 0-based member per variant); both store
the discriminant beside the payload as `{tag, payload}`
- a tagged union is a first-class `.Union` type node whose `child` holds the tag enum (untagged
unions and structs leave `child` INVALID). its fields are the variants (payload types), still
looked up by name. `union(enum)` synthesizes its tag enum up front (`types.enum_anonymous`,
variant names as members), so both forms converge on one representation
- **key reuse:** the per-variant tag value is *derivable* from `(union type, active variant
index)` — the variant's field name matches a member of the tag enum, whose value is the tag.
so codegen computes the tag itself and the existing `.Struct`/`.Field` HIR (carrying `type` +
active/field `integer`) is sufficient. **no HIR/IR/lowering change** (like 18/19/20); the delta
is parser + types layout + one checker validation + three LLVM emit sites
- runtime layout `{tag, payload-carrier}`: tag at offset 0, payload carrier at
`payload_offset = round_up(sizeof(tag), payload_align)` (`types.union_payload_offset`, shared by
`size` and the emitter). field access uses byte-offset GEPs, so offsets stay self-consistent
- construction `T{ variant = value }` reuses the union-literal path and additionally stores the
derived tag; payload read `x.variant` reuses field access, reading at the payload offset
(unchecked reinterpret, Zig-style). safe tag dispatch + payload capture is milestone 22 (`match`)
- changes: `parse_struct` parses `union(...)` (`enum` → synthesize; else an existing enum);
`types.define_record` takes a `tag` param stored in `child`, plus `is_tagged_union` /
`union_tag_enum` / `union_payload_offset` helpers and tagged `size`/`alignment`; the checker
record-decl pass requires the tag to be an enum and each variant to name a member of it; the
LLVM emitter lays out `{tag, [pad], carrier, [pad]}`, stores the tag in construction, and offsets
payload field access
- deferred to milestone 22 (`match`): safe tag dispatch + payload capture (`match x { .bird |v| … }`),
exhaustiveness checking, and any first-class tag-read accessor
22. match statements with tagged unions payload unwrapping
@@ -741,6 +764,93 @@ for 0..10 |i| blk: { # bad, no name binds returned value
}
```
## A word on match statements
```
# matching on enums
match status {
.ok: print("success")
.error: print("failure")
.pending: {
log("still waiting")
retry()
}
}
# matching on integers and other values
match code {
0: print("zero")
1: print("one")
2: print("two")
else: print("other") # needed - missing variants
}
Status :: enum { ok, error, pending }
status :: get_status() # returns a `Status`
match status {
.ok: handle_ok()
.error: handle_error()
.pending: handle_pending()
# no else needed - all variants covered
}
# matching on tagged unions
Result :: union(enum) {
success Data
failure struct {
msg []u8
code i32
}
pending void
}
match result {
.success |data|: { # use `|name|` to capture the variant's payload
process(data)
}
.failure |info|: {
print("error {d}: {s}", { info.code, info.msg })
}
.pending: {
# void payload - no capture needed
wait()
}
}
# when a union variant has a `void` payload, omit the capture
Event :: union(enum) {
click struct { x i32, y i32 }
keypress KeyCode
quit void
}
event :: get_event() # returns an `Event`
match event {
.click |pos|: handle_click(pos.x, pos.y)
.keypress |key|: handle_key(key)
.quit: should_exit = true
}
# single-expression arms yield value implicitly
label :: match priority {
.critical: "CRIT"
.high: "HIGH"
.normal: "NORM"
.low: " LOW"
}
# multi-statement arms use `yield`
message :: match code {
0: "success"
1: {
log("warning encountered")
yield "warning"
}
else: "unknown"
}
```
## A word on memory allocation
(NOTE THAT SYNTAX MAY NOT MATCH BROLANG EXACTLY AND SHOULD BE TAKEN WITH A GRAIN OF SALT - INSPIRATION ONLY)