feat: execute break and continue in while loops

This commit is contained in:
2026-10-10 01:26:32 +02:00
parent 0add5ab187
commit d51ca57c9e
16 changed files with 612 additions and 21 deletions
+3 -1
View File
@@ -51,6 +51,8 @@ function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. ca
`while` repeats a block or single statement while its `bool` condition holds, re-evaluating the condition on every iteration. guards follow the same braced/braceless convention as `if`. loop bodies introduce child scopes; their locals, including locals in nested conditional arms, begin uninitialized on each scope entry.
`break` exits the nearest enclosing `while`; `continue` restarts its entire condition, including short-circuit expressions and calls. both are operand-free statements permitted within loop bodies and nested conditional arms; labels and values are unsupported. controls outside loops produce keyword-spanned diagnostics, including in untaken or unreachable code, and trap only if reached. code after a transfer is still resolved and type-checked but does not execute on that path. body locals are reset on re-entry after `continue`; loop transfers do not guarantee a function return. both controls lower to existing IR jumps, without new interpreter instructions or loop frames. [programs/loop_control.hon](programs/loop_control.hon) skips an iteration and exits early; `./build/honey run programs/loop_control.hon` prints `12`.
name resolution (`@source/sema`) runs after parsing. it predeclares package names and each block's direct locals, allowing forward references and mutually recursive functions. parameters and top-level function-body locals share a scope; each conditional arm and loop body introduces a child scope whose names do not escape it. signatures resolve names in the enclosing package scope. shadowing is forbidden, including reuse of builtin type names (`int`, `float`, `bool`, `string`, `void`); sibling scopes may independently reuse names.
`sema.resolve` returns an owned binding table indexed by AST `NodeId`, without modifying the AST. `sema.binding` identifies a declaration, builtin type, or invalid binding carrying its diagnostic ID; nodes with no binding return `.none`. release the result with `sema.deinit`. unknown names and duplicate declarations add source-spanned diagnostics; invalid syntax retains its existing diagnostic identity. same-scope duplicates retain the first declaration for recovery, while forbidden inner declarations invalidate that name's uses rather than silently binding them to an outer declaration.
@@ -65,4 +67,4 @@ type errors carry source spans and diagnostic identities; invalid operands propa
`interpreter.run` consumes only IR and returns a value or a diagnostic trap. string values carry a module-local `ir.StringId`, not a slice; resolve bytes with `ir.string_bytes(&module, id)` before releasing the module. decoded bytes and their full backing allocations remain module-owned, without copying strings during calls or returns. heap-backed invocation frames track their current block and resume it after calls or global initialization; loop iterations reuse the invocation's storage without allocating new frames. parameters are initialized on entry and local declarations execute in statement order. premature local reads and assignments trap; assignment evaluates its right-hand side before checking the destination's initialization. globals initialize lazily on their first read, cache their result for that run, and trap on initialization cycles. integers use signed 64-bit storage with checked arithmetic; floats use 64-bit IEEE arithmetic and comparisons, including NaN behavior. numeric literal overflow and float underflow to zero are diagnosed; representable subnormals are accepted. strings support `\\`, `\"`, `\n`, `\r`, `\t`, `\0` and `\xNN` escapes.
recoverable source errors do not stop execution of unaffected code. unused invalid functions, globals and nested bodies retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not paths that already return or trap. diagnostics include the filename, line and byte column. exit status is `0` for clean execution, `1` for source diagnostics or a runtime trap, and `2` for usage or infrastructure failure. native code generation, `for`, `break`/`continue` and compound assignment remain unimplemented.
recoverable source errors do not stop execution of unaffected code. unused invalid functions, globals and nested bodies retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not paths that already return or trap. diagnostics include the filename, line and byte column. exit status is `0` for clean execution, `1` for source diagnostics or a runtime trap, and `2` for usage or infrastructure failure. native code generation, `for` and compound assignment remain unimplemented.
+13
View File
@@ -0,0 +1,13 @@
main proc() int {
number := 0
total := 0
while true {
number = number + 1
if (number == 3) continue
if (number > 5) break
total = total + number
}
return total
}
+15 -1
View File
@@ -7,7 +7,7 @@
#! * decl ..... -> identifier [type] binding expr
#! * block .... -> '{' {stmt} '}'
#! * arm ...... -> block | stmt (single statements become canonical blocks)
#! * stmt ..... -> decl | expr | identifier '=' expr | 'return' [expr] | 'if' expr arm ['else' arm] | 'while' expr arm
#! * stmt ..... -> decl | expr | identifier '=' expr | 'return' [expr] | 'break' | 'continue' | 'if' expr arm ['else' arm] | 'while' expr arm
#! braceless guards require parentheses unless the condition is a call; else binds to the nearest if.
#! precedence (tightest first): calls, unary -/!, multiplication/division, addition/subtraction, relational comparisons, equality, and, or.
#! binary operators associate left; unary operators associate right.
@@ -51,6 +51,8 @@ StmtKind :: enum {
expression,
assignment_stmt,
return_stmt,
break_stmt,
continue_stmt,
if_stmt,
while_stmt,
invalid
@@ -84,6 +86,8 @@ NodeKind :: enum {
stmt_expr,
stmt_decl,
stmt_return,
stmt_break,
stmt_continue,
stmt_if,
stmt_assignment,
stmt_while,
@@ -314,6 +318,14 @@ add_return_stmt proc(builder @mut Builder, span Span, value ?NodeId) NodeId ! me
}, span)
}
add_break_stmt proc(builder @mut Builder, span Span) NodeId ! mem.AllocError {
return try add_node(builder, Node{ kind = .stmt_break }, span)
}
add_continue_stmt proc(builder @mut Builder, span Span) NodeId ! mem.AllocError {
return try add_node(builder, Node{ kind = .stmt_continue }, span)
}
add_if_stmt proc(
builder @mut Builder,
span Span,
@@ -491,6 +503,8 @@ stmt_kind proc(tree @Tree, id NodeId) StmtKind {
.stmt_decl: .declaration
.stmt_expr: .expression
.stmt_return: .return_stmt
.stmt_break: .break_stmt
.stmt_continue: .continue_stmt
.stmt_if: .if_stmt
.stmt_assignment: .assignment_stmt
.stmt_while: .while_stmt
+2
View File
@@ -57,6 +57,8 @@ render_block proc(renderer @mut Renderer, tree @ast.Tree, block ast.NodeId, dept
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, expression)))
try append_text(renderer, "\n")
}
.break_stmt: try append_text(renderer, "break\n")
.continue_stmt: try append_text(renderer, "continue\n")
.return_stmt: {
try append_text(renderer, "return")
if (ast.return_stmt(tree, statement).value) |value| {
+4
View File
@@ -30,6 +30,8 @@ Code :: enum {
return_type_mismatch,
condition_not_bool,
invalid_assignment_target,
break_outside_loop,
continue_outside_loop,
missing_entry,
invalid_entry,
literal_out_of_range,
@@ -98,6 +100,8 @@ messages :: $enummap.init(Code, []u8, {
return_type_mismatch = "return does not match the function result type",
condition_not_bool = "condition must have type bool",
invalid_assignment_target = "assignment requires a mutable local binding",
break_outside_loop = "break requires an enclosing loop",
continue_outside_loop = "continue requires an enclosing loop",
missing_entry = "program requires a main function",
invalid_entry = "main must be a function with no parameters",
literal_out_of_range = "numeric literal is outside its runtime type's range",
+2
View File
@@ -22,6 +22,8 @@ SourceError :: enum { source_too_large }
keywords strmap.StaticStringMap(TokenKind) :: strmap.init([
{ "proc", .proc },
{ "return", .return },
{ "break", .break },
{ "continue", .continue },
{ "if", .if },
{ "for", .for },
{ "else", .else },
+21
View File
@@ -79,3 +79,24 @@ scans_logical_operators_without_splitting_identifiers test {
try testing.expect_equal(u32(35), state.tokens.items[7].span.end)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
scans_loop_controls_with_exact_keyword_boundaries test {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
state := init(mem.c_allocator)
defer deinit(&state)
source :: "break continue breakfast continued break_1 continue2"
try scan(&state, source, &diagnostic_store)
expected [7]TokenKind :: [.break, .continue, .ident, .ident, .ident, .ident, .eof]
try testing.expect_equal(expected.len, state.tokens.items.len)
for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind)
try testing.expect_equal(u32(0), state.tokens.items[0].span.start)
try testing.expect_equal(u32(5), state.tokens.items[0].span.end)
try testing.expect_equal(u32(6), state.tokens.items[1].span.start)
try testing.expect_equal(u32(14), state.tokens.items[1].span.end)
try testing.expect_equal("breakfast", strpool.get_str(&strpool.strings, state.tokens.items[2].data.str_id)?)
try testing.expect_equal("continued", strpool.get_str(&strpool.strings, state.tokens.items[3].data.str_id)?)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
+1
View File
@@ -21,6 +21,7 @@ TokenKind :: enum {
if, else
for, while
proc, return
break, continue
true, false
and, or
+22
View File
@@ -16,6 +16,12 @@ Storage :: struct {
index u32 = 0
}
@hide:file
LoopTargets :: struct {
guard ir.BlockId
exit ir.BlockId
}
@hide:file
State :: struct {
tree @ast.Tree
@@ -28,6 +34,7 @@ State :: struct {
function usize = 0
block ir.BlockId = ir.BlockId(0)
terminated bool = false
loop ?LoopTargets = null
}
#! lowers checked source into caller-owned executable storage; release with ir.deinit.
@@ -357,6 +364,9 @@ lower_while proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagn
alternative = exit,
span = ast.node_span(state.tree, loop.condition),
}
previous_loop :: state.loop
state.loop = LoopTargets{ guard = guard, exit = exit }
defer state.loop = previous_loop
state.block = body
try lower_block(state, loop.body, true)
if (!state.terminated) {
@@ -374,6 +384,18 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d
match ast.stmt_kind(tree, id) {
.if_stmt: try lower_if(state, id)
.while_stmt: try lower_while(state, id)
.break_stmt, .continue_stmt: {
if (trap_type(state, id)) return
if (state.loop) |targets| {
target := targets.exit
# continue restarts the whole guard, before any short-circuit branches.
if (ast.stmt_kind(tree, id) == .continue_stmt) target = targets.guard
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = target, span = ast.node_span(tree, id),
}
state.terminated = true
} else unreachable
}
.assignment_stmt: {
assignment :: ast.assignment_stmt(tree, id)
if (trap_binding(state, assignment.target) or trap_type(state, id)) return
+167 -12
View File
@@ -443,6 +443,12 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
` pass :: identity(text)
` identity(pass)
` remaining = remaining - 1
` inner := 2
` while inner > 0 {
` inner = inner - 1
` if (inner > 0) continue else break
` }
` if (remaining > 0) continue else break
` }
` if computed > 0 and (enabled or false) {
` saved :: identity(text)
@@ -475,12 +481,11 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
try testing.expect_equal(usize(0), state.live)
continue
}
outcome :: interpreter.run(mem.c_allocator, &module, &diagnostic_store) catch |error| {
ir.deinit(&module)
return error
}
ir.deinit(&module)
errdefer ir.deinit(&module)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect(mem.eql(outcome.value.data.string, "owned\nstring"))
ir.deinit(&module)
try testing.expect_equal(usize(0), state.live)
succeeded = true
break
@@ -763,14 +768,26 @@ forward_assignments_check_initialization_after_evaluating_the_rhs test {
}
loop_and_reentered_arm_locals_are_uninitialized_before_later_declarations test {
sources [5][]u8 :: [
sources [11][]u8 :: [
"main proc() int {\n n := 2\n while n > 0 {\n if n == 1 { return later }\n later :: 42\n n = n - 1\n }\n return 0\n}\n",
"main proc() int {\n n := 2\n while n > 0 {\n if n == 1 {\n later = 42\n return later\n }\n later := 0\n n = n - 1\n }\n return 0\n}\n",
"main proc() int {\n n := 2\n while n > 0 {\n if true {\n if n == 1 { return later }\n later :: 42\n }\n n = n - 1\n }\n return 0\n}\n",
"main proc() int {\n n := 2\n while n > 0 {\n if true {\n if n == 1 {\n later = 42\n return later\n }\n later := 0\n }\n n = n - 1\n }\n return 0\n}\n",
"main proc() int {\n n := 2\n while n > 0 {\n if false {} else {\n if n == 1 { return later }\n later :: 42\n }\n n = n - 1\n }\n return 0\n}\n",
"main proc() int {\n n := 3\n while n > 0 {\n n = n - 1\n if n == 0 { return later }\n if (n == 1) continue\n later :: 42\n continue\n }\n return 0\n}\n",
"main proc() int {\n n := 3\n while n > 0 {\n n = n - 1\n if n == 0 { later = 42 }\n if (n == 1) continue\n later := 0\n continue\n }\n return 0\n}\n",
"main proc() int {\n n := 3\n while n > 0 {\n n = n - 1\n if true {\n if n == 0 { return later }\n if (n == 1) continue\n later :: 42\n continue\n }\n }\n return 0\n}\n",
"main proc() int {\n n := 3\n while n > 0 {\n n = n - 1\n if true {\n if n == 0 { later = 42 }\n if (n == 1) continue\n later := 0\n continue\n }\n }\n return 0\n}\n",
"main proc() int {\n n := 3\n while n > 0 {\n n = n - 1\n if false {} else {\n if n == 0 { return later }\n if (n == 1) continue\n later :: 42\n continue\n }\n }\n return 0\n}\n",
"main proc() int {\n n := 3\n while n > 0 {\n n = n - 1\n if false {} else {\n if n == 0 { later = 42 }\n if (n == 1) continue\n later := 0\n continue\n }\n }\n return 0\n}\n",
]
codes [11]diagnostics.Code :: [
.read_before_initialization, .write_before_initialization,
.read_before_initialization, .write_before_initialization, .read_before_initialization,
.read_before_initialization, .write_before_initialization,
.read_before_initialization, .write_before_initialization,
.read_before_initialization, .write_before_initialization,
]
codes [5]diagnostics.Code :: [.read_before_initialization, .write_before_initialization, .read_before_initialization, .write_before_initialization, .read_before_initialization]
for (sources) |source, index| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
@@ -835,17 +852,19 @@ zero_iteration_body_errors_are_checked_but_not_executed test {
}
loop_returns_do_not_guarantee_return_and_assignment_arithmetic_still_checks_overflow test {
sources [3][]u8 :: [
sources [5][]u8 :: [
"main proc() int {\n while true { return 42 }\n}\n",
"main proc() int {\n while false { return 42 }\n}\n",
"main proc() int {\n value := 9223372036854775807\n while true {\n value = value + 1\n return value\n }\n return 0\n}\n",
"main proc() int {\n while true { break }\n}\n",
"main proc() int {\n while false { continue }\n}\n",
]
for (sources) |source, index| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
if (index < 2) {
if (index != 2) {
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
diagnostic :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
try testing.expect_equal(diagnostics.Code.missing_return, diagnostic.code)
@@ -855,16 +874,17 @@ loop_returns_do_not_guarantee_return_and_assignment_arithmetic_still_checks_over
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
} else {
code diagnostics.Code :: if (index == 1) .missing_return else .integer_overflow
code diagnostics.Code :: if (index == 2) .integer_overflow else .missing_return
try expect_trap(outcome, &diagnostic_store, code)
if (index != 2) try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?)
}
}
}
bounded_pure_loops_do_not_allocate_per_iteration_and_release_runtime_storage test {
sources [2][]u8 :: [
"main proc() int {\n n := 0\n while (n < 1 and !(n == 1)) or false {\n local :: n\n n = local + 1\n }\n return n\n}\n",
"main proc() int {\n n := 0\n while (n < 10000 and !(n == 10000)) or false {\n local :: n\n n = local + 1\n }\n return n\n}\n",
"main proc() int {\n n := 0\n while (n < 1 and !(n == 1)) or false {\n local :: n\n n = local + 1\n if (n == 1) break\n continue\n later :: 0\n }\n return n\n}\n",
"main proc() int {\n n := 0\n while (n < 10000 and !(n == 10000)) or false {\n local :: n\n n = local + 1\n if (n == 10000) break\n continue\n later :: 0\n }\n return n\n}\n",
]
expected [2]i64 :: [1, 10000]
allocations := usize(0)
@@ -1098,6 +1118,11 @@ lazy_branchful_global_initializers_and_calls_release_every_runtime_allocation te
`later :: false or !false
`both proc(left bool, right bool) bool { return left and right }
`main proc() bool {
` n := 3
` while both(first and first, both(false or later, !false and later)) and n > 0 {
` n = n - 1
` if (n > 0) continue else break
` }
` return both(first and first, both(false or later, !false and later))
`}
module := try compile_owned(source, &diagnostic_store)
@@ -1144,3 +1169,133 @@ skipped_rhs_does_not_hide_a_demanded_outer_type_mismatch test {
try testing.expect_equal(count, diagnostics.count(&diagnostic_store))
}
}
break_and_continue_skip_iterations_exit_early_and_restore_outer_targets test {
sources [3][]u8 :: [
"main proc() int {\n n := 0\n total := 0\n while n < 10 {\n n = n + 1\n if (n == 2 or n == 4) continue\n if (n == 6) break\n total = total + n\n }\n return total + 33\n}\n",
"main proc() int {\n outer := 3\n total := 0\n while outer > 0 {\n outer = outer - 1\n inner := 3\n while inner > 0 {\n inner = inner - 1\n if (inner == 1) continue\n if (inner == 0) break\n total = total + 7\n }\n if (outer > 0) continue else break\n }\n return total + 21\n}\n",
"main proc() int {\n n := 0\n while n < 5 {\n n = n + 1\n if (n < 3) continue else break\n n = n + 100\n }\n return n + 39\n}\n",
]
for (sources) |source| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
}
}
continue_restarts_composed_calling_guards_after_branchful_body_calls test {
guards [3][]u8 :: [
"(positive(3 - n) and !(n == 2)) or n == 2",
"!(n >= 3) and (positive(3 - n) or false)",
"positive(3 - n) and ((n < 3 or false) and !false)",
]
for (guards) |guard| {
source := arraylist.init(u8, mem.c_allocator)
defer arraylist.deinit(&source)
for ("positive proc(n int) bool { return n > 0 }\nadvance proc(flag bool, n int) int {\n if flag { return n + 1 }\n return n + 1\n}\nmain proc() int {\n n := 0\n while ") |byte| try arraylist.append(&source, byte)
for (guard) |byte| try arraylist.append(&source, byte)
# bound a stale guard too, so incorrect continue lowering fails instead of hanging.
for (" {\n n = advance(n == 0 or n == 1, n)\n if (n > 5) break\n continue\n n = n + 100\n }\n return n + 39\n}\n") |byte| try arraylist.append(&source, byte)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source.items, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
}
}
reached_outside_controls_preserve_diagnostic_identity_and_keyword_span test {
sources [4][]u8 :: [
"main proc() int {\n break\n return 42\n}\n",
"main proc() int {\n continue\n return 42\n}\n",
"bad proc() void {\n break\n}\nmain proc() int {\n while true { bad()\n break }\n return 42\n}\n",
"main proc() int {\n while true { break }\n continue\n return 42\n}\n",
]
codes [4]diagnostics.Code :: [.break_outside_loop, .continue_outside_loop, .break_outside_loop, .continue_outside_loop]
keywords [4][]u8 :: ["break", "continue", "break", "continue"]
for (sources) |source, index| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
try testing.expect_equal(codes[index], original.code)
try testing.expect(mem.eql(source[usize(original.span.start)..usize(original.span.end)], keywords[index]))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
}
}
untaken_outside_controls_remain_checked_without_trapping test {
sources [5][]u8 :: [
"main proc() int {\n if (false) break\n return 42\n}\n",
"main proc() int {\n if true { return 42 } else continue\n}\n",
"main proc() int {\n return 42\n break\n}\n",
"main proc() int {\n return 42\n continue\n}\n",
"bad proc() void {\n continue\n}\nmain proc() int {\n while false { bad() }\n return 42\n}\n",
]
codes [5]diagnostics.Code :: [.break_outside_loop, .continue_outside_loop, .break_outside_loop, .continue_outside_loop, .continue_outside_loop]
for (sources) |source, index| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
try testing.expect_equal(codes[index], original.code)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
}
}
source_errors_after_loop_transfers_stay_checked_but_unreached test {
sources [4][]u8 :: [
"main proc() int {\n n := 2\n while n > 0 {\n n = n - 1\n continue\n missing\n }\n return 42\n}\n",
"main proc() int {\n while true {\n break\n missing\n }\n return 42\n}\n",
"main proc() int {\n n := 2\n while n > 0 {\n n = n - 1\n if (n > 0) continue else break\n missing\n }\n return 42\n}\n",
"main proc() int {\n while true {\n return 42\n break\n continue\n missing\n }\n return 0\n}\n",
]
for (sources) |source| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
try testing.expect_equal(diagnostics.Code.unknown_name, original.code)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
}
}
traps_before_loop_controls_keep_the_original_error test {
sources [2][]u8 :: [
"main proc() int {\n while true {\n missing\n break\n }\n return 42\n}\n",
"main proc() int {\n while true {\n missing\n continue\n }\n return 42\n}\n",
]
for (sources) |source| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try expect_trap(outcome, &diagnostic_store, .unknown_name)
try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
}
}
+29 -1
View File
@@ -186,6 +186,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
.if: return try parse_if(state)
.while: return try parse_while(state, allow_else)
.return: return try parse_return(state, allow_else)
.break, .continue: return try parse_loop_control(state, allow_else)
.ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren: {
expression :: try parse_expr(state)
return try ast.add_expression_stmt(&state.builder, expression)
@@ -219,6 +220,23 @@ parse_return proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
)
}
@hide:file
parse_loop_control proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
keyword :: current(state)
state.next_token += 1
if (
!next_is(state, .newline)
and !next_is(state, .close_curly)
and !next_is(state, .eof)
and !(allow_else and next_is(state, .else))
) return .unexpected_token
if (keyword.kind == .break)
return try ast.add_break_stmt(&state.builder, keyword.span)
return try ast.add_continue_stmt(&state.builder, keyword.span)
}
@hide:file
parse_assignment proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
name :: try consume(state, .ident)
@@ -302,6 +320,7 @@ parse_guard proc(state @mut State) ast.NodeId ! ParseError {
and !next_is(state, .newline) and !next_is(state, .eof)
and !next_is(state, .else) and !next_is(state, .return)
and !next_is(state, .if) and !next_is(state, .while)
and !next_is(state, .break) and !next_is(state, .continue)
) {
if (next_is(state, .ident) and next_n_is(state, 1, .equal)) break
state.next_token += 1
@@ -467,7 +486,7 @@ parse_call proc(state @mut State, callee ast.NodeId) ast.NodeId ! ParseError {
arguments arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
defer arraylist.deinit(&arguments)
while (!is_expression_boundary(current(state).kind)) {
while (!is_call_boundary(current(state).kind)) {
argument :: try parse_expr(state)
try arraylist.append(&arguments, argument)
if (!next_is(state, .comma)) break
@@ -552,6 +571,15 @@ is_expression_boundary proc(kind lexer.TokenKind) bool {
}
}
@hide:file
is_call_boundary proc(kind lexer.TokenKind) bool {
return (
kind == .break
or kind == .continue
or is_expression_boundary(kind)
)
}
@hide:file
is_expression_recovery_boundary proc(kind lexer.TokenKind) bool {
return (
+122
View File
@@ -961,3 +961,125 @@ recovers_missing_logical_operands_and_preserves_enclosing_nodes test {
after :: ast.statement_decl(&tree, ast.package_decl(&tree, 1))
try testing.expect_equal("after", ast.source_text(&tree, after.name))
}
parses_leaf_loop_controls_and_nearest_else test {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`main proc() void {
` while (true) {
` if (true) if (false) break else continue
` while (false) continue
` break
` }
` while (false) if (true) break else continue
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &diagnostic_store)
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
try testing.expect_equal(2, ast.block_statement_count(&tree, body))
loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 0))
try testing.expect_equal(3, ast.block_statement_count(&tree, loop.body))
outer_if :: ast.if_stmt(&tree, ast.block_statement(&tree, loop.body, 0))
try testing.expect(outer_if.else_block == null)
inner_if :: ast.if_stmt(&tree, ast.block_statement(&tree, outer_if.then_block, 0))
transfer :: ast.block_statement(&tree, inner_if.then_block, 0)
try testing.expect_equal(ast.StmtKind.break_stmt, ast.stmt_kind(&tree, transfer))
try testing.expect_equal("break", ast.source_text(&tree, ast.node_span(&tree, transfer)))
if (inner_if.else_block) |other| {
alternate :: ast.block_statement(&tree, other, 0)
try testing.expect_equal(ast.StmtKind.continue_stmt, ast.stmt_kind(&tree, alternate))
try testing.expect_equal("continue", ast.source_text(&tree, ast.node_span(&tree, alternate)))
} else return .expectation_failed
nested :: ast.while_stmt(&tree, ast.block_statement(&tree, loop.body, 1))
try testing.expect_equal(ast.StmtKind.continue_stmt, ast.stmt_kind(&tree, ast.block_statement(&tree, nested.body, 0)))
braceless :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 1))
conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, braceless.body, 0))
try testing.expect(conditional.else_block != null)
}
rejects_control_tails_and_preserves_following_declarations test {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`break
`continue
`bad :: break
`bad_continue :: continue
`main proc() void {
` while (true) {
` break 1
` first :: 1
` continue label
` second :: 2
` if (true) break label else continue
` if (true) continue 2 else break
` break 3 }
` after :: 3
`}
`last :: 4
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &diagnostic_store)
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(9, diagnostics.count(&diagnostic_store))
try testing.expect_equal(6, ast.package_decl_count(&tree))
for (usize(0)..usize(2)) |index| try testing.expect_equal(ast.DeclKind.invalid, ast.decl_kind(&tree, ast.package_decl(&tree, index)))
for (usize(2)..usize(4)) |index| {
declaration :: ast.package_decl(&tree, index)
try testing.expect_equal(ast.DeclKind.statement, ast.decl_kind(&tree, declaration))
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, ast.statement_decl(&tree, declaration).initializer))
}
body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body
try testing.expect_equal(2, ast.block_statement_count(&tree, body))
loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 0))
try testing.expect_equal(7, ast.block_statement_count(&tree, loop.body))
for ([usize(1), usize(3)]) |index| {
try testing.expect_equal(ast.StmtKind.declaration, ast.stmt_kind(&tree, ast.block_statement(&tree, loop.body, index)))
}
conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, loop.body, 4))
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, conditional.then_block, 0)))
if (conditional.else_block) |other| {
try testing.expect_equal(ast.StmtKind.continue_stmt, ast.stmt_kind(&tree, ast.block_statement(&tree, other, 0)))
} else return .expectation_failed
try testing.expect_equal(ast.StmtKind.declaration, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 1)))
try testing.expect_equal(ast.DeclKind.statement, ast.decl_kind(&tree, ast.package_decl(&tree, 5)))
}
preserves_control_bodies_after_malformed_guards test {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`main proc() void {
` while (true break
` while predicate( continue
` while (true) { continue }
` after :: 1
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &diagnostic_store)
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
try testing.expect_equal(4, ast.block_statement_count(&tree, body))
expected [3]ast.StmtKind :: [.break_stmt, .continue_stmt, .continue_stmt]
for (expected) |kind, index| {
loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, index))
try testing.expect_equal(kind, ast.stmt_kind(&tree, ast.block_statement(&tree, loop.body, 0)))
if (index < 2) try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, loop.condition))
}
try testing.expect_equal(ast.StmtKind.declaration, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 3)))
}
+20 -6
View File
@@ -40,6 +40,7 @@ Role :: enum {
if_stmt,
assignment_stmt,
while_stmt,
loop_control,
}
@hide:file
@@ -510,11 +511,11 @@ collect_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError |
state.cells[usize(id)].type = invalid_type(binding.data.diagnostic)
state.cells[usize(id)].type.function = id
}
try collect_block(state, function.body, function.return_type)
try collect_block(state, function.body, function.return_type, false)
}
@hide:file
collect_block proc(state @mut State, id, return_annotation ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop bool) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
state.cells[usize(id)].role = .body
for (0..ast.block_statement_count(tree, id)) |i| {
@@ -540,10 +541,10 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId) void ! (m
state.cells[usize(statement)].type = Type{ kind = .void }
try collect_expression(state, conditional.condition)
try depend(state, conditional.condition, statement)
try collect_block(state, conditional.then_block, return_annotation)
try collect_block(state, conditional.then_block, return_annotation, in_loop)
try depend(state, conditional.then_block, id)
if (conditional.else_block) |other| {
try collect_block(state, other, return_annotation)
try collect_block(state, other, return_annotation, in_loop)
try depend(state, other, id)
}
}
@@ -573,9 +574,22 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId) void ! (m
state.cells[usize(statement)].type = Type{ kind = .void }
try collect_expression(state, loop.condition)
try depend(state, loop.condition, statement)
try collect_block(state, loop.body, return_annotation)
try collect_block(state, loop.body, return_annotation, true)
try depend(state, loop.body, id)
}
.break_stmt, .continue_stmt: {
state.cells[usize(statement)].role = .loop_control
state.cells[usize(statement)].type = Type{ kind = .void }
if (!in_loop) {
code :: if (ast.stmt_kind(tree, statement) == .break_stmt)
diagnostics.Code.break_outside_loop
else
diagnostics.Code.continue_outside_loop
state.cells[usize(statement)].type = invalid_type(try diagnostics.add(
state.diagnostics, code, ast.node_span(tree, statement),
))
}
}
.invalid: collect_invalid(state, statement)
}
source :: if (ast.stmt_kind(tree, statement) == .expression) ast.statement_expr(tree, statement) else statement
@@ -889,7 +903,7 @@ block_falls_through proc(state @State, id ast.NodeId) bool {
source :: if (kind == .expression) ast.statement_expr(tree, statement) else statement
if (state.types[usize(source)].kind == .invalid) return false
match kind {
.return_stmt, .invalid: return false
.return_stmt, .break_stmt, .continue_stmt, .invalid: return false
.if_stmt: {
conditional :: ast.if_stmt(tree, statement)
then_falls :: block_falls_through(state, conditional.then_block)
+144
View File
@@ -1548,3 +1548,147 @@ short_circuit_skip_paths_do_not_guarantee_a_return test {
try testing.expect_equal(3, code_count(&diagnostic_store, .invalid_operand))
try testing.expect_equal(18, diagnostics.count(&diagnostic_store))
}
loop_controls_follow_lexical_bodies_and_check_unreachable_code test {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`scopes proc(flag bool) void {
` while flag {
` if (flag) break else continue
` while flag {
` if (flag) continue else break
` }
` absent
` }
` break
` continue
`}
`caller proc() void {
` while true {
` callee()
` break
` }
`}
`callee proc() void { continue }
`dead proc() void {
` return
` break
` continue
`}
`arms proc() void {
` if (false) break else continue
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &diagnostic_store)
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
defer sema.deinit(&resolution)
result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
defer deinit(&result)
scopes_body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
outer_statement :: ast.block_statement(&tree, scopes_body, 0)
outer :: ast.while_stmt(&tree, outer_statement)
outer_if :: ast.if_stmt(&tree, ast.block_statement(&tree, outer.body, 0))
inner_statement :: ast.block_statement(&tree, outer.body, 1)
inner :: ast.while_stmt(&tree, inner_statement)
inner_if :: ast.if_stmt(&tree, ast.block_statement(&tree, inner.body, 0))
caller_body :: ast.function_decl(&tree, ast.package_decl(&tree, 1)).body
caller_loop :: ast.while_stmt(&tree, ast.block_statement(&tree, caller_body, 0))
valid [5]ast.NodeId :: [
ast.block_statement(&tree, outer_if.then_block, 0),
ast.block_statement(&tree, outer_if.else_block?, 0),
ast.block_statement(&tree, inner_if.then_block, 0),
ast.block_statement(&tree, inner_if.else_block?, 0),
ast.block_statement(&tree, caller_loop.body, 1),
]
for (valid) |statement| try expect_kind(&result, statement, .void)
try expect_kind(&result, outer_statement, .void)
try expect_kind(&result, inner_statement, .void)
unknown :: ast.statement_expr(&tree, ast.block_statement(&tree, outer.body, 2))
original :: sema.binding(&resolution, unknown).data.diagnostic
try testing.expect_equal(original, type_of(&result, unknown).diagnostic)
outside_break :: ast.block_statement(&tree, scopes_body, 1)
outside_continue :: ast.block_statement(&tree, scopes_body, 2)
_ = try expect_error(&result, &diagnostic_store, outside_break, .break_outside_loop, ast.node_span(&tree, outside_break))
_ = try expect_error(&result, &diagnostic_store, outside_continue, .continue_outside_loop, ast.node_span(&tree, outside_continue))
callee_body :: ast.function_decl(&tree, ast.package_decl(&tree, 2)).body
callee_control :: ast.block_statement(&tree, callee_body, 0)
_ = try expect_error(&result, &diagnostic_store, callee_control, .continue_outside_loop, ast.node_span(&tree, callee_control))
dead_body :: ast.function_decl(&tree, ast.package_decl(&tree, 3)).body
dead_break :: ast.block_statement(&tree, dead_body, 1)
dead_continue :: ast.block_statement(&tree, dead_body, 2)
_ = try expect_error(&result, &diagnostic_store, dead_break, .break_outside_loop, ast.node_span(&tree, dead_break))
_ = try expect_error(&result, &diagnostic_store, dead_continue, .continue_outside_loop, ast.node_span(&tree, dead_continue))
arms_body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body
arms :: ast.if_stmt(&tree, ast.block_statement(&tree, arms_body, 0))
arm_break :: ast.block_statement(&tree, arms.then_block, 0)
arm_continue :: ast.block_statement(&tree, arms.else_block?, 0)
_ = try expect_error(&result, &diagnostic_store, arm_break, .break_outside_loop, ast.node_span(&tree, arm_break))
_ = try expect_error(&result, &diagnostic_store, arm_continue, .continue_outside_loop, ast.node_span(&tree, arm_continue))
for (usize(0)..usize(5)) |index| try expect_kind(&result, ast.package_decl(&tree, index), .function)
try testing.expect_equal(3, code_count(&diagnostic_store, .break_outside_loop))
try testing.expect_equal(4, code_count(&diagnostic_store, .continue_outside_loop))
try testing.expect_equal(1, code_count(&diagnostic_store, .unknown_name))
try testing.expect_equal(8, diagnostics.count(&diagnostic_store))
}
loop_controls_do_not_guarantee_function_returns test {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`breaking proc() int { while true { break } }
`continuing proc() int { while false { continue } }
`mixed proc(flag bool) int {
` while flag {
` if (flag) return 42 else break
` }
`}
`closed proc(flag bool) int {
` while flag {
` if (flag) break else continue
` -"bad"
` }
` return 42
`}
`invalid proc() int { break }
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &diagnostic_store)
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
defer sema.deinit(&resolution)
result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
defer deinit(&result)
for (usize(0)..usize(3)) |index| {
body :: ast.function_decl(&tree, ast.package_decl(&tree, index)).body
_ = try expect_error(&result, &diagnostic_store, body, .missing_return, ast.node_span(&tree, body))
try expect_kind(&result, ast.block_statement(&tree, body, 0), .void)
}
closed_body :: ast.function_decl(&tree, ast.package_decl(&tree, 3)).body
loop_statement :: ast.block_statement(&tree, closed_body, 0)
loop :: ast.while_stmt(&tree, loop_statement)
dead_expression :: ast.statement_expr(&tree, ast.block_statement(&tree, loop.body, 1))
original :: try expect_error(&result, &diagnostic_store, dead_expression, .invalid_operand, ast.node_span(&tree, dead_expression))
try testing.expect_equal(original, type_of(&result, closed_body).diagnostic)
try expect_kind(&result, loop_statement, .void)
try expect_kind(&result, ast.block_statement(&tree, closed_body, 1), .void)
invalid_body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body
invalid_control :: ast.block_statement(&tree, invalid_body, 0)
control_error :: try expect_error(&result, &diagnostic_store, invalid_control, .break_outside_loop, ast.node_span(&tree, invalid_control))
try testing.expect_equal(control_error, type_of(&result, invalid_body).diagnostic)
for (usize(0)..usize(5)) |index| try expect_kind(&result, ast.package_decl(&tree, index), .function)
try testing.expect_equal(3, code_count(&diagnostic_store, .missing_return))
try testing.expect_equal(1, code_count(&diagnostic_store, .invalid_operand))
try testing.expect_equal(1, code_count(&diagnostic_store, .break_outside_loop))
try testing.expect_equal(5, diagnostics.count(&diagnostic_store))
}
+1
View File
@@ -203,6 +203,7 @@ resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (me
.return_stmt: {
if (ast.return_stmt(tree, statement).value) |value| try resolve_expr(state, scope, value)
}
.break_stmt, .continue_stmt: {}
.if_stmt: {
conditional :: ast.if_stmt(tree, statement)
try resolve_expr(state, scope, conditional.condition)
+46
View File
@@ -439,3 +439,49 @@ resolves_loop_assignments_in_lexical_scopes_with_forward_local_bindings test {
try testing.expect_equal(BindingKind.invalid, binding(&resolution, builtin).kind)
try testing.expect_equal(5, diagnostics.count(&diagnostic_store))
}
resolves_all_code_and_preserves_scopes_after_loop_controls test {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`main proc() void {
` while (true) {
` break
` before :: later
` continue
` later :: 1
` missing_after_break
` if (true) {
` continue
` private :: later
` int :: 2
` }
` private
` }
` later
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &diagnostic_store)
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
resolution := try resolve(mem.c_allocator, &tree, &diagnostic_store)
defer deinit(&resolution)
try testing.expect_equal(4, diagnostics.count(&diagnostic_store))
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 0))
before :: ast.block_statement(&tree, loop.body, 1)
later :: ast.block_statement(&tree, loop.body, 3)
reference :: ast.statement_decl(&tree, before).initializer
try testing.expect_equal(BindingKind.declaration, binding(&resolution, reference).kind)
try testing.expect_equal(later, binding(&resolution, reference).data.declaration)
conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, loop.body, 5))
private_decl :: ast.block_statement(&tree, conditional.then_block, 1)
private_ref :: ast.statement_decl(&tree, private_decl).initializer
try testing.expect_equal(later, binding(&resolution, private_ref).data.declaration)
forbidden :: ast.block_statement(&tree, conditional.then_block, 2)
try testing.expect_equal(BindingKind.invalid, binding(&resolution, forbidden).kind)
}