match statements

This commit is contained in:
2026-06-28 23:44:01 +02:00
parent 981ccb047a
commit 462632554c
12 changed files with 1057 additions and 6 deletions
+151
View File
@@ -2251,6 +2251,157 @@ main :: func() i32 {
testing.expect(t, found_variant)
}
@(test)
match_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-match"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/match", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
// Exercises every match form tagged-union statement match with payload capture,
// tagged-union value match (implicit + explicit yield), enum statement/value match,
// and integer match with `else` all summed to a self-checking 0 on success.
testing.expect_value(t, state.exit_code, 0)
}
@(test)
match_dispatches_on_the_tag :: proc(t: ^testing.T) {
// A tagged-union match desugars to: read the discriminant once (a load of the tag
// enum at the union's offset 0), then compare it against each variant's tag value.
// `Animal` is dense in a u8 backing (dog=0, bird=2), so the dog arm compares the
// loaded i8 tag against 0.
text := `Animal :: enum {
dog
cat
bird
}
Data :: union(Animal) {
dog i32
bird i32
}
main :: func() i32 {
d Data = Data{ bird = 7 }
out i32 = 0
match d {
.dog |v|: out = v
.bird |v|: out = v + 1
}
return out
}
`
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)
// The tag is loaded as an i8 and compared (icmp eq) against the dog variant's tag (0).
testing.expect(t, strings.contains(llvm_text, "load i8"))
testing.expect(t, strings.contains(llvm_text, "icmp eq i8"))
}
@(test)
match_misuse_is_diagnosed :: proc(t: ^testing.T) {
// Five rejected matches: (1) a non-exhaustive enum match with no `else`, (2) a scalar
// match missing its mandatory `else`, (3) a payload capture on a non-union arm, (4) a
// redundant `else` on an already-exhaustive match, and (5) an unknown variant.
text := `Animal :: enum {
dog
cat
bird
}
Data :: union(Animal) {
dog i32
bird i32
}
not_exhaustive :: func(a Animal) i32 {
match a {
.dog: { return 1 }
.cat: { return 2 }
}
return 0
}
missing_else :: func(n i32) i32 {
match n {
0: { return 1 }
1: { return 2 }
}
return 0
}
bad_capture :: func(a Animal) i32 {
match a {
.dog |v|: { return 1 }
.cat: { return 2 }
.bird: { return 3 }
}
return 0
}
redundant_else :: func(a Animal) i32 {
match a {
.dog: { return 1 }
.cat: { return 2 }
.bird: { return 3 }
else: { return 4 }
}
return 0
}
unknown_variant :: func(d Data) i32 {
match d {
.dog: { return 1 }
.snake: { return 2 }
else: { return 3 }
}
return 0
}
main :: func() i32 {
# Functions are specialized on use, so call each so its body is type-checked.
d Data = Data{ dog = 0 }
return not_exhaustive(.dog) + missing_else(0) + bad_capture(.dog) +
redundant_else(.dog) + unknown_variant(d)
}
`
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_exhaustive := false
found_missing_else := false
found_capture := false
found_redundant := false
found_unknown := false
for diagnostic in diagnostics.items {
found_exhaustive = found_exhaustive || strings.contains(diagnostic.message, "is not exhaustive")
found_missing_else = found_missing_else || strings.contains(diagnostic.message, "requires an 'else' arm")
found_capture = found_capture || strings.contains(diagnostic.message, "only tagged-union variants can capture")
found_redundant = found_redundant || strings.contains(diagnostic.message, "redundant 'else'")
found_unknown = found_unknown || strings.contains(diagnostic.message, "unknown variant '.snake'")
}
testing.expect(t, found_exhaustive)
testing.expect(t, found_missing_else)
testing.expect(t, found_capture)
testing.expect(t, found_redundant)
testing.expect(t, found_unknown)
}
@(test)
yield_misuse_is_diagnosed :: proc(t: ^testing.T) {
// A value block that does not end in `yield`, and a `yield` nested inside an