diff --git a/TODO.md b/TODO.md index 469f781..55fae48 100644 --- a/TODO.md +++ b/TODO.md @@ -402,7 +402,48 @@ loop) whose concrete yield references a local declared *past* the first yield (annotate); same-label loop/block shadowing resolves innermost-wins -21. unions and tagged unions +21. unions and tagged unions (implemented; first pass — native untagged unions only; see below) + - inspired by zig + - ``` + # unions (untagged; named fields like a struct — Zig union members are always named) + SomeStuff :: union { + f float + i int + a Animal + } + + # tagged unions (constrained to the backing enum) — DEFERRED to 21.5 + AnimalNameOrHeight :: union(Animal) { # use just `enum` instead of `Animal` for unconstrained tagged union + dog []u8 + cat []u8 + bird int + lizard int + } + ``` + - this first pass ships **native untagged unions with named fields**. an untagged union is a + carrier sized to its largest/most-aligned member with no runtime tag (a C union); reading a + non-active field reinterprets the bytes (unsafe, like a Zig `union {}` in ReleaseFast) and an + untagged union is not matchable. the TODO's original bare type-only spelling + (`union { float, int, Animal }`) was dropped in favor of named fields to match Zig and to + reuse the existing field-access path + - the entire untagged-union backend already existed from the C-interop work (milestone 4): the + `.Union` type kind, carrier-based LLVM layout (`emit_types`), `size`/`alignment`, keyed-literal + construction `Val{ field = value }` (exactly one initializer; the active-field index rides in + `hir.Expr.integer`), and field access (GEP-at-offset-0 reinterpret). none of these are gated on + `c_layout`, so a *native* union flows through them unchanged. record-declaration validation + (`is_runtime_value` per field) already covers native unions too + - the change is front-end only: a new `union` keyword (token/lexer), and `parse_union` is just + `parse_struct` parametrized with `is_union=true` routing through `types.define_record` + (native-only, so `c_layout=false` and the no-body case errors). no `types`/`checker`/`hir`/ + `lower`/`llvm` change (mirrors the minimal-footprint slices of 18/19/20) + - deferred to **21.5**: tagged unions (`union(Enum)` constrained + `union(enum)` inferred) with a + 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 22. match statements with tagged unions payload unwrapping diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index 8d99479..4594236 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -18,6 +18,7 @@ keyword_kind :: proc(text: string) -> token.Kind { case "c_func": return .Keyword_C_Func case "struct": return .Keyword_Struct case "c_struct": return .Keyword_C_Struct + case "union": return .Keyword_Union case "enum": return .Keyword_Enum case "distinct": return .Keyword_Distinct case "alias": return .Keyword_Alias diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index ffcf4a5..5c9dcf2 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -1727,16 +1727,19 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { }) } -parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout: bool) { +parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout: bool, is_union := false) { start := advance(parser) id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol)) ended_by_newline := current(parser).kind == .Newline skip_newlines(parser) if current(parser).kind != .Left_Brace { if !c_layout { - source.add(parser.diagnostics, start.span, "native struct declarations require a body") + source.add( + parser.diagnostics, start.span, + "native union declarations require a body" if is_union else "native struct declarations require a body", + ) } - if !types.define_struct(&parser.module.type_store, id, nil, c_layout, true) { + if !types.define_record(&parser.module.type_store, id, nil, c_layout, true, is_union) { source.addf(parser.diagnostics, name.span, "duplicate type declaration '%s'", token_text(parser, name)) } if !ended_by_newline { @@ -1772,7 +1775,7 @@ parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout: bool) { if _, ok := allow(parser, .Right_Brace); !ok { source.add(parser.diagnostics, current(parser).span, "expected '}' after struct fields") } - if !types.define_struct(&parser.module.type_store, id, fields[:], c_layout, false) { + if !types.define_record(&parser.module.type_store, id, fields[:], c_layout, false, is_union) { source.addf(parser.diagnostics, name.span, "duplicate type declaration '%s'", token_text(parser, name)) } _ = finish_statement(parser) @@ -2054,6 +2057,10 @@ parse_top_level :: proc(parser: ^Parser) { parse_struct(parser, name, current(parser).kind == .Keyword_C_Struct) return } + if operator.kind == .Colon_Colon && current(parser).kind == .Keyword_Union { + parse_struct(parser, name, false, is_union=true) + return + } if operator.kind == .Colon_Colon && current(parser).kind == .Keyword_Enum { parse_enum(parser, name) return diff --git a/compiler/token/token.odin b/compiler/token/token.odin index 7d30e08..58ba846 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -53,6 +53,7 @@ Kind :: enum u8 { Keyword_C_Func, Keyword_Struct, Keyword_C_Struct, + Keyword_Union, Keyword_Enum, Keyword_Distinct, Keyword_Alias, diff --git a/compiler_tests.odin b/compiler_tests.odin index 56a6f6a..bd81cfd 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2139,6 +2139,19 @@ yield_compiles_and_runs :: proc(t: ^testing.T) { testing.expect_value(t, state.exit_code, 42) } +@(test) +native_union_compiles_and_runs :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-unions" + defer _ = os.remove(output) + status := compiler_core.compile_package("examples/programs/unions", output) + testing.expect_value(t, status, 0) + state := run_executable(output) + // A native untagged union (`Val :: union { n i32, f f64 }`) constructed via a keyed + // literal, stored into a local, and read back through field access reinterprets the + // carrier and yields 42 (declaration → construct → store → load → field read). + testing.expect_value(t, state.exit_code, 42) +} + @(test) yield_misuse_is_diagnosed :: proc(t: ^testing.T) { // A value block that does not end in `yield`, and a `yield` nested inside an diff --git a/examples/programs/unions/main.bro b/examples/programs/unions/main.bro new file mode 100644 index 0000000..1a41222 --- /dev/null +++ b/examples/programs/unions/main.bro @@ -0,0 +1,9 @@ +Val :: union { + n i32 + f f64 +} + +main :: func() i32 { + x Val = Val{ n = 42 } + return x.n +}