condtional multi-unwrap and guard clauses

This commit is contained in:
2026-06-22 20:37:37 +02:00
parent 27f42dd253
commit 663f4dc658
9 changed files with 567 additions and 96 deletions
+181 -2
View File
@@ -3823,11 +3823,121 @@ conditional_unwrap_compiles_and_runs :: proc(t: ^testing.T) {
status := compiler_core.compile_package("examples/programs/conditional_unwrap", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
// present scalar binds and unwraps (40), none takes the else (+2), a present
// optional pointer binds and derefs (+0), a none optional pointer is skipped.
// Single unwrap, guarded two/three-value unwraps, optional pointers, false
// guards, and failed short-circuit chains preserve the expected total.
testing.expect_value(t, state.exit_code, 42)
}
@(test)
conditional_unwrap_parser_captures_guard_and_parenthesized_chain :: proc(t: ^testing.T) {
text := `main :: func() void {
first ?i32 = 1
second ?i32 = 2
if (first and second) |a, b : a == 1 and b == 2| {
_ = a
_ = b
}
}
`
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)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
statement := module.statements[module.functions[0].body[2]]
testing.expect_value(t, statement.kind, ast.Stmt_Kind.If)
testing.expect_value(t, len(statement.captures), 2)
testing.expect_value(t, module.exprs[statement.expr].kind, ast.Expr_Kind.And)
testing.expect(t, module.exprs[statement.expr].parenthesized)
testing.expect(t, statement.guard != ast.INVALID_EXPR)
testing.expect_value(t, module.exprs[statement.guard].kind, ast.Expr_Kind.And)
}
@(test)
conditional_unwrap_allows_sink_captures :: proc(t: ^testing.T) {
text := `main :: func() void {
first ?i32 = 1
second ?i32 = 2
if first and second |_, value : value == 2| {
_ = value
}
if first |_| {}
}
`
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)
testing.expect_value(t, len(diagnostics.items), 0)
main := hir_module.functions[0]
first_if := hir_module.statements[main.body[2]]
second_if := hir_module.statements[main.body[3]]
testing.expect_value(t, first_if.unwraps[0].local, hir.INVALID_LOCAL)
testing.expect(t, first_if.unwraps[1].local != hir.INVALID_LOCAL)
testing.expect_value(t, second_if.unwraps[0].local, hir.INVALID_LOCAL)
}
@(test)
parser_diagnoses_malformed_conditional_unwrap_captures_and_guards :: proc(t: ^testing.T) {
cases := [4]struct {
text: string,
needle: string,
}{
{`main :: func() void {
value ?i32 = 1
if value || {}
}
`, "expected an unwrap capture name"},
{`main :: func() void {
value ?i32 = 1
if value |capture,| {}
}
`, "expected an unwrap capture after ','"},
{`main :: func() void {
value ?i32 = 1
if value |capture :| {}
}
`, "expected a guard expression after ':'"},
{`main :: func() void {
value ?i32 = 1
if value |capture {}
}
`, "expected '|' to close unwrap captures"},
}
for test_case in cases {
source_file := source.Source{path="test.bro", text=test_case.text}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
module := parser.parse(&stream, &source_file, &diagnostics)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, test_case.needle)
}
testing.expect(t, found)
ast.destroy_module(&module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
}
@(test)
if_unwrap_on_non_optional_is_diagnosed :: proc(t: ^testing.T) {
text := `main :: func() i32 {
@@ -3857,6 +3967,75 @@ if_unwrap_on_non_optional_is_diagnosed :: proc(t: ^testing.T) {
testing.expect(t, found)
}
@(test)
conditional_unwrap_diagnostics_cover_counts_guards_and_capture_scope :: proc(t: ^testing.T) {
text := `main :: func() void {
first ?i32 = 1
second ?i32 = 2
plain i32 = 3
if first and second |one| {}
if first |one, two| {}
if first and second |same, same| {}
if plain |value| {}
if first |value : value| {}
if first and earlier |earlier, later| {}
if first |value| {
value = 2
}
if first |value| {
value i32 = 2
_ = value
}
if first |value| {
_ = value
} else {
_ = value
}
_ = value
}
`
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)
count_mismatches := 0
duplicate := false
non_optional := false
guard := false
outer_operand_scope := false
immutable := false
redeclaration := false
capture_scope := 0
for diagnostic in diagnostics.items {
count_mismatches += 1 if strings.contains(diagnostic.message, "unwrap has") else 0
duplicate = duplicate || strings.contains(diagnostic.message, "unwrap captures must have distinct names")
non_optional = non_optional || strings.contains(diagnostic.message, "unwrap requires an optional value")
guard = guard || strings.contains(diagnostic.message, "unwrap guard must be a bool")
outer_operand_scope = outer_operand_scope || strings.contains(diagnostic.message, "unresolved global 'earlier'")
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'value'")
redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'value'")
capture_scope += 1 if strings.contains(diagnostic.message, "unresolved global 'value'") else 0
}
testing.expect_value(t, count_mismatches, 2)
testing.expect(t, duplicate)
testing.expect(t, non_optional)
testing.expect(t, guard)
testing.expect(t, outer_operand_scope)
testing.expect(t, immutable)
testing.expect(t, redeclaration)
testing.expect_value(t, capture_scope, 2)
}
@(test)
if_unwrap_binding_is_scoped_to_then_block :: proc(t: ^testing.T) {
// The binding `v` is usable in the then-block but not in the else-block.