diff --git a/TODO.md b/TODO.md index 0e3aa62..032901c 100644 --- a/TODO.md +++ b/TODO.md @@ -535,10 +535,26 @@ (`match get()`) is a pre-existing gap (assign to a variable first, as the spec examples do); the rvalue pointer-capture guard is defensive for when that lands -22.6. (`// ponytail:` follow-ups): contextual void construction (`e Event = .quit`) needs - enum-literal→union coercion. Separately, a **call expression directly as a match subject** - (`match get()`) is a pre-existing gap (assign to a variable first, as the spec examples do); - the rvalue pointer-capture guard is defensive for when that lands +22.6. contextual void construction + call-as-match-subject (implemented; see below) + - **contextual void construction**: a bare enum literal in a tagged-union context constructs a + void-payload variant — `e Event = .quit` (and any expected-union position: `=`, return, call + argument) coerces `.quit` to the union, equivalent to `Event{ quit }`. Build-pass only: the + `.Enum_Literal` case, given a tagged-union `expected`, looks the variant up and emits the + tag-only union `.Struct` HIR for a void variant. A payload variant via a bare `.variant` + ("needs a payload") and an unknown variant are diagnosed; the payload-carrying contextual form + `.variant{...}` stays deferred to milestone 23 (error channel) + - **call expression directly as a match subject**: `match get() { … }` now specializes the call. + Root cause was that the inference/spec-request walker `infer_statements` had no `.Match` case, + so a match's subject and arm bodies were never visited and their calls never got a + specialization (`find_spec` → "could not resolve specialization"). Added a `.Match` case that + infers the subject and recurses into arm bodies (with the capture local typed from the variant + payload, mirroring the `.For`/unwrap-`.If` handling). This also covers value-`match` and calls + inside arm bodies. As a side effect the rvalue pointer-capture guard from 22.5 is now reachable + (`match make_box() { .v |@p|: … }` correctly errors "requires an addressable subject") + - checker-only; no parser/AST/HIR/IR/lowering/codegen change + - deferred: payload-carrying contextual construction (`.variant{…}`, milestone 23); a + `yield call()` inside a value-block/value-match arm still misses its spec (`infer_statements` + has no `.Yield` case either — a separate pre-existing gap) 23. error types (see below) diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 32c0494..8bfb0c5 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -1984,6 +1984,42 @@ infer_statements :: proc( case .Defer: deferred := [1]ast.Stmt_Id{statement.update} infer_statements(checker, deferred[:], locals, local_types, pkg, file, demanded, result, result_hint) + case .Match: + // The build pass desugars `match` to an if/else chain, but inference runs first + // and must still visit the subject and arm bodies so calls there get specialized + // (e.g. `match get()`). Mirror the `.For`/unwrap-`.If` capture handling. + subject_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types) + is_tagged := types.is_tagged_union(subject_type, &checker.module.types) + for arm_id in statement.body { + arm := checker.ast_module.statements[arm_id] + if arm.kind != .Match_Arm { + continue + } + for pattern in arm.patterns { + _ = infer_expr(checker, pattern, locals^[:], pkg, file, demanded, local_types) + } + capture_start := len(locals^) + if len(arm.captures) > 0 && is_tagged && len(arm.patterns) > 0 { + capture := arm.captures[0] + if capture != checker.sink_symbol { + capture_type := types.INVALID + pattern := checker.ast_module.exprs[arm.patterns[0]] + if pattern.kind == .Enum_Literal { + if _, field, ok := find_struct_field(checker, subject_type, pattern.name); ok { + capture_type = field.type + if arm.pointer_capture { + // Mutability is best-effort here; the build pass finalizes + // the exact pointer type and coerces the captured value. + capture_type = types.pointer(&checker.module.types, field.type, true, false) + } + } + } + append(locals, Infer_Local{name=capture, type=capture_type, declared=capture_type, statement=ast.INVALID_STMT}) + } + } + infer_statements(checker, arm.body, locals, local_types, pkg, file, demanded, result, result_hint) + resize(locals, capture_start) + } } } resize(locals, scope_start) @@ -3101,6 +3137,29 @@ build_compound_expr :: proc( ) return invalid_hir_expr(checker, expr.span, id, expected) case .Enum_Literal: + // A bare enum literal in a tagged-union context constructs a variant. Only a + // void-payload variant can be built this way (it has no value); a payload variant + // must use `T{ variant = ... }`. (`.variant{...}` payload construction is the + // milestone-23 error-channel form.) + if types.is_tagged_union(expected, store) { + index, field, found := find_struct_field(checker, expected, expr.name) + if !found { + id := source.addf(checker.diagnostics, expr.span, "unknown variant '.%s' on '%s'", symbol_text(checker, expr.name), type_label(checker, expected)) + return invalid_hir_expr(checker, expr.span, id, expected) + } + if !types.is_void(field.type) { + id := source.addf(checker.diagnostics, expr.span, "variant '.%s' on '%s' needs a payload; only void variants can be built from a bare '.%s'", + symbol_text(checker, expr.name), type_label(checker, expected), symbol_text(checker, expr.name)) + return invalid_hir_expr(checker, expr.span, id, expected) + } + values := make([]hir.Expr_Id, 1, checker.allocator) + values[0] = hir.INVALID_EXPR + return add_hir_expr(checker, hir.Expr{ + kind=.Struct, span=expr.span, type=expected, args=values, integer=i64(index), + target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } if !types.is_enum(expected, store) { id := source.addf( checker.diagnostics, diff --git a/compiler_tests.odin b/compiler_tests.odin index a54656f..40357a8 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2527,6 +2527,98 @@ main :: func() i32 { testing.expect(t, found_incompatible) } +@(test) +match_call_subject_and_contextual_void_compile :: proc(t: ^testing.T) { + // Milestone 22.6: (1) a call expression directly as the match subject (`match get()`) + // now specializes — previously "could not resolve specialization of 'get'" — because the + // inference pass visits the match subject; (2) a void variant constructed contextually + // (`e Box = .empty`) coerces the bare enum literal to the union. Both compile clean to IR. + text := `Animal :: enum { + dog + cat + bird +} +Box :: union(enum) { + count i32 + empty void +} +get :: func() Animal { + return .bird +} +main :: func() i32 { + e Box = .empty + r i32 = 0 + match get() { + .dog: r = 1 + .cat: r = 2 + .bird: r = 3 + } + match e { + .count |c|: r = r + c + .empty: r = r + 7 + } + return r +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + ir_module := lower.lower(&hir_module) + defer ir.destroy_module(&ir_module) + llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) + defer delete(llvm_text) + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, strings.contains(llvm_text, "icmp eq")) // the call subject still dispatches +} + +@(test) +match_contextual_payload_variant_is_diagnosed :: proc(t: ^testing.T) { + // Contextual construction is only for void variants; a bare `.point` for a payload + // variant must use `Box{ point = ... }` instead. + text := `Point :: struct { + x i32 + y i32 +} +Box :: union(enum) { + point Point + empty void +} +bad :: func() i32 { + e Box = .point + return 0 +} +main :: func() i32 { + return bad() +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + found := false + for diagnostic in diagnostics.items { + found = found || strings.contains(diagnostic.message, "needs a payload") + } + testing.expect(t, found) +} + @(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/match/main.bro b/examples/programs/match/main.bro index 656c9a5..ca52d8e 100644 --- a/examples/programs/match/main.bro +++ b/examples/programs/match/main.bro @@ -58,6 +58,10 @@ payload_of :: func(d Data) i32 { return v } +make_box :: func() Box { + return Box{ point = Point{ x = 4, y = 0 } } +} + main :: func() i32 { acc i32 = 0 @@ -93,8 +97,8 @@ main :: func() i32 { } acc = acc + bucket # +5 - # void-payload variant: bare-key construction + a no-capture arm - e Box = Box{ empty } + # void-payload variant: contextual construction (`.empty` coerces to Box) + no-capture arm + e Box = .empty hit i32 = 0 match e { .point |pt|: hit = pt.x @@ -110,6 +114,12 @@ main :: func() i32 { } acc = acc + b.point.x # +10 (mutated through the @mut Point) - # 50 + 47 + 3 + 2 + 5 + 7 + 10 = 124 - return acc - 124 + # match directly on a call result (no need to bind it to a variable first) + match make_box() { + .point |p|: acc = acc + p.x # +4 + .empty: hit = hit + } + + # 50 + 47 + 3 + 2 + 5 + 7 + 10 + 4 = 128 + return acc - 128 }