diff --git a/README.md b/README.md index 92a0fe1..89d1eba 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ source loading uses the supplied `io.Io` for opening, reading, and closing files a recursive example is in [programs/factorial.hon](programs/factorial.hon); `./build/honey run programs/factorial.hon` prints `120`. +an iterative example is in [programs/iterative_factorial.hon](programs/iterative_factorial.hon); `./build/honey run programs/iterative_factorial.hon` also prints `120`. + the parser supports arithmetic expressions with nested parenthesized groups, such as `a + (b - c) * 2 / 3`. groups control precedence without adding AST nodes; inner expression spans exclude the grouping parentheses. parsing accepts integer division syntax, but type checking requires float operands for `/`. function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. calls bind tighter than unary and binary arithmetic and can appear in initializers or as expression statements inside function bodies. argument lists allow a trailing comma; newlines remain statement boundaries. type checking validates callable values, argument counts and argument types. @@ -43,7 +45,11 @@ function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. ca `if`/`else` supports braced arms, single statements and `else if` chains. conditions must have type `bool`. a braceless guard requires parentheses unless it is a function call: `if (n <= 1) return 1` or `if ready() return`. `else` binds to the nearest `if`; after a braced arm it may start on the next line. branches are represented as AST blocks even when braceless. -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 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 function and arm scopes may independently reuse names. +`name = expression` updates a local `:=` binding. `::` bindings, parameters, named functions, builtin types and globals are not assignment targets. the established binding type is preserved: assignment inputs do not widen or infer that type, though numeric literals can adopt its float context. function-valued locals accept replacement functions with the same parameter and result types. + +`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. + +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. @@ -51,10 +57,10 @@ type checking (`@source/sema/check`) runs after name resolution. `check.check` b numeric typing follows contextual rules within Honey's current `int` and `float` families. integer literals can adopt either family; float literals remain float. numeric demands propagate through arithmetic and inferred bindings before undemanded integers default to `int`. conflicting demands are errors; fixed `int` values do not implicitly convert to `float`. `+`, `-`, `*` and unary `-` require compatible numeric operands; `/` requires and supplies float context. `3 / 2`, `3.0 / 2`, and `ratio float :: 3 / 2` are valid; division involving fixed `int` values requires explicit conversion. -type errors carry source spans and diagnostic identities; invalid operands propagate their existing identity without duplicate errors, while independent expressions, arguments and conditional arms are still checked. inference cycles without a type anchor are diagnosed. function signatures are checked independently of their bodies. bare `return` and same-line `return expression` are supported inside function bodies and must match the declared result type. nonvoid return-path checking is structural: every path must return or terminate in a diagnostic trap, and both conditional successors are considered without constant folding. a missing `else` permits fallthrough, even for a literal `true` condition; a later return can close that path. statements after a return and untaken branches are still type-checked but are not executed. +type errors carry source spans and diagnostic identities; invalid operands propagate their existing identity without duplicate errors, while independent expressions, arguments and nested bodies are still checked. inference cycles without a type anchor are diagnosed. function signatures are checked independently of their bodies. bare `return` and same-line `return expression` are supported inside function bodies and must match the declared result type. nonvoid return-path checking is structural: every path must return or terminate in a diagnostic trap, and both conditional successors are considered without constant folding. a missing `else` or a loop permits fallthrough, even for a literal `true` condition; a later return can close that path. statements after a return and untaken branches or loops are still type-checked but are not executed. -`lower.lower` turns the checked AST and binding table into an owned `@source/ir` module. functions, globals, local slots and instruction-result temporaries have explicit IDs; typed instructions execute in source evaluation order and basic blocks end in jump, conditional-branch, return or diagnostic-trap terminators. conditional arms join only when execution can continue; local slots and temporaries are function-wide, without SSA or phi nodes. literals are parsed and strings decoded during lowering; the module retains no AST, binding-table or source-buffer references. release it with `ir.deinit`; keep and reuse the diagnostic store used during lowering so diagnostic IDs remain valid. the IR is target-independent and is intended to feed later native backends as well as the current interpreter. +`lower.lower` turns the checked AST and binding table into an owned `@source/ir` module. functions, globals, local slots and instruction-result temporaries have explicit IDs; typed instructions execute in source evaluation order and basic blocks end in jump, conditional-branch, return or diagnostic-trap terminators. conditional arms join only when execution can continue; loops use condition/body/exit blocks and backedges. local slots and temporaries are function-wide, without SSA or phi nodes. literals are parsed and strings decoded during lowering; the module retains no AST, binding-table or source-buffer references. release it with `ir.deinit`; keep and reuse the diagnostic store used during lowering so diagnostic IDs remain valid. the IR is target-independent and is intended to feed later native backends as well as the current interpreter. -`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. parameters are initialized on entry and locals execute in statement order; premature local reads trap. 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. +`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 conditional arms retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not arms 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, loops, assignment and boolean logical operators 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`, `break`/`continue`, compound assignment and boolean logical operators remain unimplemented. diff --git a/programs/iterative_factorial.hon b/programs/iterative_factorial.hon new file mode 100644 index 0000000..bfaaa58 --- /dev/null +++ b/programs/iterative_factorial.hon @@ -0,0 +1,11 @@ +main proc() int { + product := 1 + remaining := 5 + + while remaining > 1 { + product = product * remaining + remaining = remaining - 1 + } + + return product +} diff --git a/source/ast/ast.hon b/source/ast/ast.hon index 2b2d37b..dea482c 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -1,13 +1,13 @@ #! grammar: #! * type ..... -> identifier -#! * assignment -> :: | := +#! * binding ... -> :: | := #! * literal .. -> int | float | string | 'true' | 'false' #! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>=' #! * expr ..... -> identifier | literal | '(' expr ')' | '-' expr | expr op expr | expr '(' [expr {',' expr} [',']] ')' -#! * decl ..... -> identifier [type] assignment expr +#! * decl ..... -> identifier [type] binding expr #! * block .... -> '{' {stmt} '}' #! * arm ...... -> block | stmt (single statements become canonical blocks) -#! * stmt ..... -> decl | expr | 'return' [expr] | 'if' expr arm ['else' arm] +#! * stmt ..... -> decl | expr | identifier '=' expr | 'return' [expr] | 'if' expr arm ['else' arm] | 'while' expr arm #! braceless guards require parentheses unless the condition is a call; else binds to the nearest if. import "@std/debug" @@ -47,7 +47,11 @@ DeclKind :: enum { StmtKind :: enum { declaration, expression, - return_stmt, if_stmt, invalid + assignment_stmt, + return_stmt, + if_stmt, + while_stmt, + invalid } Mutability :: enum { @@ -78,6 +82,8 @@ NodeKind :: enum { stmt_decl, stmt_return, stmt_if, + stmt_assignment, + stmt_while, func_decl, param_decl, block, @@ -176,6 +182,18 @@ IfView :: struct { else_block ?NodeId } +AssignmentView :: struct { + span Span + target NodeId + value NodeId +} + +WhileView :: struct { + span Span + condition NodeId + body NodeId +} + InvalidView :: struct { span Span diagnostic DiagnosticId @@ -309,6 +327,32 @@ add_if_stmt proc( }, span) } +add_assignment_stmt proc( + builder @mut Builder, + span Span, + target NodeId, + value NodeId, +) NodeId ! mem.AllocError { + return try add_node(builder, Node{ + kind = .stmt_assignment, + main = NodeData{ node_id = target }, + data0 = NodeData{ node_id = value }, + }, span) +} + +add_while_stmt proc( + builder @mut Builder, + span Span, + condition NodeId, + body NodeId, +) NodeId ! mem.AllocError { + return try add_node(builder, Node{ + kind = .stmt_while, + main = NodeData{ node_id = condition }, + data0 = NodeData{ node_id = body }, + }, span) +} + add_statement_decl proc( builder @mut Builder, span Span, @@ -445,6 +489,8 @@ stmt_kind proc(tree @Tree, id NodeId) StmtKind { .stmt_expr: .expression .stmt_return: .return_stmt .stmt_if: .if_stmt + .stmt_assignment: .assignment_stmt + .stmt_while: .while_stmt .invalid_stmt: .invalid else: unreachable } @@ -476,6 +522,26 @@ if_stmt proc(tree @Tree, id NodeId) IfView { } } +assignment_stmt proc(tree @Tree, id NodeId) AssignmentView { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .stmt_assignment) + return AssignmentView{ + span = get_span(&tree.storage, id), + target = node.main.node_id, + value = node.data0.node_id, + } +} + +while_stmt proc(tree @Tree, id NodeId) WhileView { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .stmt_while) + return WhileView{ + span = get_span(&tree.storage, id), + condition = node.main.node_id, + body = node.data0.node_id, + } +} + statement_decl proc(tree @Tree, id NodeId) StatementDeclView { node :: get_node(&tree.storage, id) debug.assert(node.kind == .stmt_decl) diff --git a/source/ast/renderer/renderer.hon b/source/ast/renderer/renderer.hon index 6d5c10b..3d67484 100644 --- a/source/ast/renderer/renderer.hon +++ b/source/ast/renderer/renderer.hon @@ -77,6 +77,21 @@ render_block proc(renderer @mut Renderer, tree @ast.Tree, block ast.NodeId, dept try render_block(renderer, tree, alternative, depth + 1) } } + .assignment_stmt: { + assignment :: ast.assignment_stmt(tree, statement) + try append_text(renderer, "assignment ") + try append_text(renderer, ast.source_text(tree, ast.node_span(tree, assignment.target))) + try append_text(renderer, " = ") + try append_text(renderer, ast.source_text(tree, ast.node_span(tree, assignment.value))) + try append_text(renderer, "\n") + } + .while_stmt: { + loop :: ast.while_stmt(tree, statement) + try append_text(renderer, "while ") + try append_text(renderer, ast.source_text(tree, ast.node_span(tree, loop.condition))) + try append_text(renderer, "\n") + try render_block(renderer, tree, loop.body, depth + 1) + } .invalid: try append_text(renderer, "invalid statement\n") } } diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index 2348ee9..8e8b94a 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -28,11 +28,13 @@ Code :: enum { missing_return, return_type_mismatch, condition_not_bool, + invalid_assignment_target, missing_entry, invalid_entry, literal_out_of_range, invalid_escape, read_before_initialization, + write_before_initialization, initialization_cycle, integer_overflow, } @@ -93,12 +95,14 @@ message proc(code Code) []u8 { .cannot_infer_type: "cannot infer a type without a type anchor" .missing_return: "nonvoid function can finish without returning a value" .return_type_mismatch: "return does not match the function result type" - .condition_not_bool: "if condition must have type bool" + .condition_not_bool: "condition must have type bool" + .invalid_assignment_target: "assignment requires a mutable local binding" .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" .invalid_escape: "invalid string escape" .read_before_initialization: "local value was read before initialization" + .write_before_initialization: "local value was assigned before initialization" .initialization_cycle: "global initialization cycle" .integer_overflow: "integer arithmetic overflow" } diff --git a/source/interpreter/interpreter.hon b/source/interpreter/interpreter.hon index 7c61ff8..9804cd6 100644 --- a/source/interpreter/interpreter.hon +++ b/source/interpreter/interpreter.hon @@ -113,6 +113,18 @@ run proc( } continue } + .assign_local: { + local :: &frames.items[frame_index].locals[usize(instruction.slot)] + if (!local.initialized) { + return try runtime_trap(diagnostic_store, .write_before_initialization, instruction.span) + } + local.value = frames.items[frame_index].values[usize(instruction.left)] + continue + } + .reset_local: { + frames.items[frame_index].locals[usize(instruction.slot)].initialized = false + continue + } .load_global: { global_index :: usize(instruction.slot) match globals[global_index].status { diff --git a/source/interpreter/interpreter.test.hon b/source/interpreter/interpreter.test.hon index f4cf82a..a5c07fa 100644 --- a/source/interpreter/interpreter.test.hon +++ b/source/interpreter/interpreter.test.hon @@ -671,3 +671,111 @@ boolean_equality_and_branching_use_boolean_payloads test { } } } + +assign_local_replaces_initialized_values_without_producing_a_result test { + module := ir.init(mem.c_allocator) + defer ir.deinit(&module) + store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&store) + types [1]ir.Type :: [.int] + main :: try add_function(&module, types[..], 0, .int, 3) + module.entry = main + try emit(&module, main, integer(0, 1)) + try emit(&module, main, ir.Instruction{ op = .store_local, slot = 0, left = ir.ValueId(0) }) + try emit(&module, main, integer(1, 42)) + try emit(&module, main, ir.Instruction{ op = .assign_local, slot = 0, left = ir.ValueId(1) }) + try emit(&module, main, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(2), slot = 0 }) + return_value(&module, main, ir.ValueId(2)) + outcome :: try run(mem.c_allocator, &module, &store) + try testing.expect(outcome.trap == null) + try testing.expect_equal(ir.Type.int, outcome.value.type) + try testing.expect_equal(i64(42), outcome.value.data.integer) + try testing.expect_equal(usize(0), diagnostics.count(&store)) +} + +reset_local_invalidates_reads_and_assignments_at_the_instruction_span test { + operations [2]ir.Op :: [.load_local, .assign_local] + codes [2]diagnostics.Code :: [.read_before_initialization, .write_before_initialization] + for (operations) |op, index| { + module := ir.init(mem.c_allocator) + defer ir.deinit(&module) + store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&store) + types [1]ir.Type :: [.int] + main :: try add_function(&module, types[..], 0, .int, 2) + module.entry = main + span :: diagnostics.Span{ start = 21, end = 26 } + try emit(&module, main, integer(0, 42)) + try emit(&module, main, ir.Instruction{ op = .store_local, slot = 0, left = ir.ValueId(0) }) + try emit(&module, main, ir.Instruction{ op = .reset_local, slot = 0 }) + destination ir.ValueId :: if (op == .load_local) ir.ValueId(1) else ir.no_value + try emit(&module, main, ir.Instruction{ + op = op, type = .int, destination = destination, slot = 0, left = ir.ValueId(0), span = span, + }) + outcome :: try run(mem.c_allocator, &module, &store) + try expect_trap(outcome, &store, codes[index], span) + try testing.expect_equal(usize(1), diagnostics.count(&store)) + } +} + +premature_assignment_and_reset_traps_release_frames_at_all_allocation_boundaries test { + module := ir.init(mem.c_allocator) + defer ir.deinit(&module) + store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&store) + types [1]ir.Type :: [.int] + main :: try add_function(&module, types[..], 0, .int, 1) + module.entry = main + span :: diagnostics.Span{ start = 14, end = 19 } + try emit(&module, main, integer(0, 42)) + try emit(&module, main, ir.Instruction{ op = .store_local, slot = 0, left = ir.ValueId(0) }) + try emit(&module, main, ir.Instruction{ op = .reset_local, slot = 0 }) + try emit(&module, main, ir.Instruction{ op = .assign_local, slot = 0, left = ir.ValueId(0), span = span }) + failures := usize(0) + succeeded := false + for (usize(0)..usize(16)) |limit| { + state := AllocationState{ remaining = limit } + allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } + outcome :: run(allocator, &module, &store) catch |error| { + match error { + .out_of_memory: {} + else: return .expectation_failed + } + failures += 1 + try testing.expect_equal(usize(0), state.live) + continue + } + succeeded = true + try expect_trap(outcome, &store, .write_before_initialization, span) + try testing.expect_equal(usize(0), state.live) + } + try testing.expect(succeeded) + try testing.expect(failures > 0) +} + +assignment_diagnostic_allocation_failure_releases_runtime_storage test { + module := ir.init(mem.c_allocator) + defer ir.deinit(&module) + types [1]ir.Type :: [.int] + main :: try add_function(&module, types[..], 0, .int, 1) + module.entry = main + try emit(&module, main, integer(0, 42)) + try emit(&module, main, ir.Instruction{ op = .assign_local, slot = 0, left = ir.ValueId(0) }) + diagnostic_state := AllocationState{ remaining = 0 } + diagnostic_allocator :: mem.Allocator{ context = &diagnostic_state, vtable = &limited_vtable } + store := diagnostics.init(diagnostic_allocator) + defer diagnostics.deinit(&store) + state := AllocationState{ remaining = 64 } + allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } + _ = run(allocator, &module, &store) catch |error| { + match error { + .out_of_memory: {} + else: return .expectation_failed + } + try testing.expect_equal(usize(0), state.live) + try testing.expect_equal(usize(0), diagnostic_state.live) + try testing.expect_equal(usize(0), diagnostics.count(&store)) + return + } + return .expectation_failed +} diff --git a/source/ir/ir.hon b/source/ir/ir.hon index 49a0b33..7e12d31 100644 --- a/source/ir/ir.hon +++ b/source/ir/ir.hon @@ -36,6 +36,8 @@ Op :: enum { constant load_local store_local + assign_local + reset_local load_global negate add diff --git a/source/lower/lower.hon b/source/lower/lower.hon index c62ce5f..a0a99e4 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -175,7 +175,7 @@ finish proc(state @mut State, value ir.ValueId, span diagnostics.Span) void { emit proc(state @mut State, instruction ir.Instruction) ir.ValueId ! mem.AllocError { function := &state.module.functions.items[state.function] operation := instruction - if (operation.op != .store_local) { + if (operation.op != .store_local and operation.op != .assign_local and operation.op != .reset_local) { operation.destination = ir.ValueId(function.value_count) function.value_count += 1 } @@ -226,7 +226,7 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di if (trap_type(state, ast.function_param(tree, id, i))) return } if (trap_type(state, id)) return - try lower_block(state, declaration.body) + try lower_block(state, declaration.body, false) if (state.terminated) return # aggregate arm errors are not demanded by fallthrough; only missing_return is. body_type :: typecheck.type_of(state.checked, declaration.body) @@ -256,13 +256,26 @@ predeclare_locals proc(state @mut State, block ast.NodeId) void ! mem.AllocError try predeclare_locals(state, conditional.then_block) if (conditional.else_block) |alternative| try predeclare_locals(state, alternative) } + .while_stmt: try predeclare_locals(state, ast.while_stmt(state.tree, statement).body) else: {} } } } @hide:file -lower_block proc(state @mut State, block ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { +lower_block proc(state @mut State, block ast.NodeId, fresh_scope bool) void ! (mem.AllocError | diagnostics.CapacityError) { + # reset every direct declaration before control can return or trap within the scope. + if (fresh_scope) { + for (0..ast.block_statement_count(state.tree, block)) |i| { + declaration :: ast.block_statement(state.tree, block, i) + if (ast.stmt_kind(state.tree, declaration) != .declaration) continue + _ = try emit(state, ir.Instruction{ + op = .reset_local, + slot = state.storage[usize(declaration)].index, + span = ast.node_span(state.tree, declaration), + }) + } + } for (0..ast.block_statement_count(state.tree, block)) |i| { if (state.terminated) return try lower_statement(state, ast.block_statement(state.tree, block, i)) @@ -293,12 +306,12 @@ lower_if proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost span = ast.node_span(state.tree, conditional.condition), } state.block = consequent - try lower_block(state, conditional.then_block) + try lower_block(state, conditional.then_block, true) consequent_end :: state.block consequent_falls :: !state.terminated state.block = alternative state.terminated = false - if (conditional.else_block) |block| try lower_block(state, block) + if (conditional.else_block) |block| try lower_block(state, block, true) alternative_end :: state.block alternative_falls :: !state.terminated if (!consequent_falls and !alternative_falls) { @@ -320,11 +333,57 @@ lower_if proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost state.terminated = false } +@hide:file +lower_while proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { + loop :: ast.while_stmt(state.tree, id) + origin :: state.block + guard :: try add_block(state) + state.module.functions.items[state.function].blocks.items[usize(origin)].terminator = ir.Terminator{ + kind = .jump, target = guard, span = loop.span, + } + state.block = guard + condition :: try lower_expression(state, loop.condition) + if (state.terminated) return + body :: try add_block(state) + exit :: try add_block(state) + state.module.functions.items[state.function].blocks.items[usize(guard)].terminator = ir.Terminator{ + kind = .branch, + value = condition, + target = body, + alternative = exit, + span = ast.node_span(state.tree, loop.condition), + } + state.block = body + try lower_block(state, loop.body, true) + if (!state.terminated) { + state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{ + kind = .jump, target = guard, span = loop.span, + } + } + state.block = exit + state.terminated = false +} + @hide:file lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { tree :: state.tree match ast.stmt_kind(tree, id) { .if_stmt: try lower_if(state, id) + .while_stmt: try lower_while(state, id) + .assignment_stmt: { + assignment :: ast.assignment_stmt(tree, id) + if (trap_binding(state, assignment.target) or trap_type(state, id)) return + value :: try lower_expression(state, assignment.value) + if (state.terminated) return + binding :: sema.binding(state.resolution, assignment.target) + _ = try emit(state, ir.Instruction{ + op = .assign_local, + type = lower_type(typecheck.type_of(state.checked, assignment.target)), + left = value, + slot = state.storage[usize(binding.data.declaration)].index, + span = ast.node_span(tree, assignment.target), + }) + } .invalid: { invalid :: ast.invalid(tree, id) trap(state, invalid.diagnostic, invalid.span) diff --git a/source/lower/lower.test.hon b/source/lower/lower.test.hon index 6e062f5..ab7a29e 100644 --- a/source/lower/lower.test.hon +++ b/source/lower/lower.test.hon @@ -431,6 +431,12 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test { `} `main proc() string { ` computed float :: factor * 2 + ` remaining := 2 + ` while remaining > 0 { + ` pass :: identity(text) + ` identity(pass) + ` remaining = remaining - 1 + ` } ` if computed > 0 { ` saved :: identity(text) ` if computed < 10 { @@ -654,3 +660,220 @@ nested_locals_are_predeclared_before_any_arm_executes test { outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) try expect_trap(outcome, &diagnostic_store, .read_before_initialization) } + +iterative_factorial_executes_from_owned_ir_after_frontend_cleanup test { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `factorial proc(n int) int { + ` remaining := n + ` result := 1 + ` while remaining > 1 { + ` result = result * remaining + ` remaining = remaining - 1 + ` } + ` return result + `} + `main proc() int { + ` return factorial(5) + `} + 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(ir.Type.int, outcome.value.type) + try testing.expect_equal(i64(120), outcome.value.data.integer) +} + +zero_nested_early_return_and_predicate_call_loops_execute test { + sources [6][]u8 :: [ + "main proc() int {\n n := 0\n while n > 0 {\n n = n - 1\n }\n return 42 + n\n}\n", + "main proc() int {\n outer := 3\n total := 0\n while outer > 0 {\n inner := 2\n while inner > 0 {\n total = total + 7\n inner = inner - 1\n }\n outer = outer - 1\n }\n return total\n}\n", + "main proc() int {\n n := 2\n while n > 0 {\n if n == 1 { return 42 }\n n = n - 1\n }\n return 0\n}\n", + "enabled :: positive(1)\npositive proc(n int) bool { return n > 0 }\nmain proc() int {\n n := 3\n total := 0\n while positive(n) {\n if enabled { total = total + 14 }\n n = n - 1\n }\n return total\n}\n", + "main proc() int {\n n := 0\n while (n < 42) n = n + 1\n return n\n}\n", + "main proc() int {\n while true { return 42 }\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(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(ir.Type.int, outcome.value.type) + try testing.expect_equal(i64(42), outcome.value.data.integer) + } +} + +float_bool_string_and_compatible_function_updates_execute test { + sources [4][]u8 :: [ + "main proc() float {\n value float := 0\n value = 3 / 2\n return value\n}\n", + "main proc() bool {\n flag := false\n flag = true\n return flag\n}\n", + "main proc() string {\n text := \"before\"\n text = \"owned\\nupdated\"\n return text\n}\n", + "first proc(value int) int { return value + 1 }\nsecond proc(renamed int) int { return renamed + 2 }\nmain proc() int {\n invoke := first\n invoke = second\n return invoke(40)\n}\n", + ] + expected_types [4]ir.Type :: [.float, .bool, .string, .int] + 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(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(expected_types[index], outcome.value.type) + match index { + 0: try testing.expect_equal(f64(1.5), outcome.value.data.real) + 1: try testing.expect(outcome.value.data.boolean) + 2: try testing.expect(mem.eql(ir.string_bytes(&module, outcome.value.data.string), "owned\nupdated")) + 3: try testing.expect_equal(i64(42), outcome.value.data.integer) + else: unreachable + } + } +} + +forward_assignments_check_initialization_after_evaluating_the_rhs test { + sources [2][]u8 :: [ + "main proc() int {\n later = 42\n later := 0\n return later\n}\n", + "main proc() int {\n later = later + 1\n later := 0\n return later\n}\n", + ] + codes [2]diagnostics.Code :: [.write_before_initialization, .read_before_initialization] + 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(0, diagnostics.count(&diagnostic_store)) + outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + try expect_trap(outcome, &diagnostic_store, codes[index]) + diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)? + try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], "later")) + try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) + } +} + +loop_and_reentered_arm_locals_are_uninitialized_before_later_declarations test { + sources [5][]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", + ] + 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) + 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 expect_trap(outcome, &diagnostic_store, codes[index]) + diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)? + try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], "later")) + try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) + } +} + +invalid_assignments_and_loop_conditions_preserve_original_diagnostics test { + sources [9][]u8 :: [ + "main proc() int {\n value :: 0\n value = 1\n return 42\n}\n", + "update proc(value int) int {\n value = 1\n return value\n}\nmain proc() int { return update(0) }\n", + "value := 0\nmain proc() int {\n value = 1\n return value\n}\n", + "main proc() int {\n unknown = 1\n return 42\n}\n", + "main proc() int {\n value := 0\n value = unknown\n return value\n}\n", + "main proc() int {\n value := 0\n value = 1.5\n return value\n}\n", + "first proc(value int) int { return value }\nsecond proc(value bool) int { return 42 }\nmain proc() int {\n invoke := first\n invoke = second\n return 42\n}\n", + "main proc() int {\n while 1 { return 42 }\n return 0\n}\n", + "main proc() int {\n while unknown { return 42 }\n return 0\n}\n", + ] + codes [9]diagnostics.Code :: [.invalid_assignment_target, .invalid_assignment_target, .invalid_assignment_target, .unknown_name, .unknown_name, .type_mismatch, .type_mismatch, .condition_not_bool, .unknown_name] + 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_equal(diagnostics.DiagnosticId(0), outcome.trap?) + try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) + } +} + +zero_iteration_body_errors_are_checked_but_not_executed test { + sources [5][]u8 :: [ + "main proc() int {\n while false {\n unknown = 1\n }\n return 42\n}\n", + "main proc() int {\n while false {\n value :: 0\n value = 1\n }\n return 42\n}\n", + "main proc() int {\n while false {\n value := 0\n value = true\n }\n return 42\n}\n", + "main proc() int {\n while false {\n return unknown\n }\n return 42\n}\n", + "main proc() int {\n while false {\n while unknown {}\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(diagnostics.count(&diagnostic_store) > 0) + count :: 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) + try testing.expect_equal(count, diagnostics.count(&diagnostic_store)) + } +} + +loop_returns_do_not_guarantee_return_and_assignment_arithmetic_still_checks_overflow test { + sources [3][]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", + ] + 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) { + 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) + } else try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + if (index == 0) { + 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 + try expect_trap(outcome, &diagnostic_store, code) + } + } +} + +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 {\n local :: n\n n = local + 1\n }\n return n\n}\n", + "main proc() int {\n n := 0\n while n < 10000 {\n local :: n\n n = local + 1\n }\n return n\n}\n", + ] + expected [2]i64 :: [1, 10000] + allocations := usize(0) + 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(0, diagnostics.count(&diagnostic_store)) + state := AllocationState{ remaining = 64 } + allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } + outcome :: try interpreter.run(allocator, &module, &diagnostic_store) + try testing.expect(outcome.trap == null) + try testing.expect_equal(expected[index], outcome.value.data.integer) + try testing.expect_equal(usize(0), state.live) + if (index == 0) allocations = usize(64) - state.remaining + else try testing.expect_equal(allocations, usize(64) - state.remaining) + } +} diff --git a/source/parser/parse.hon b/source/parser/parse.hon index f16bcec..2c41ac3 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -168,6 +168,9 @@ parse_block proc(state @mut State) ast.NodeId ! ParseError { @hide:file parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { + if (next_is(state, .ident) and next_n_is(state, 1, .equal)) { + return try parse_assignment(state, allow_else) + } if ( next_is(state, .ident) and ( @@ -180,6 +183,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError } match current(state).kind { .if: return try parse_if(state) + .while: return try parse_while(state, allow_else) .return: return try parse_return(state, allow_else) .ident, .int, .float, .string, .true, .false, .minus, .open_paren: { expression :: try parse_expr(state) @@ -214,9 +218,73 @@ parse_return proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { ) } +@hide:file +parse_assignment proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { + name :: try consume(state, .ident) + target :: try ast.add_identifier(&state.builder, name.span) + _ = try consume(state, .equal) + value :: try parse_expr(state) + 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 + } + end :: state.tokens[state.next_token - 1].span.end + return try ast.add_assignment_stmt( + &state.builder, + ast.Span{ start = name.span.start, end = end }, + target, + value, + ) +} + @hide:file parse_if proc(state @mut State) ast.NodeId ! ParseError { keyword :: try consume(state, .if) + condition :: try parse_guard(state) + braced :: next_is(state, .open_curly) + then_block :: try parse_arm(state, true) + # only a braced arm permits its else on a later line. + after_then :: state.next_token + if (braced) skip_newlines(state) + alternative ?ast.NodeId := null + if next_is(state, .else) { + state.next_token += 1 + alternative = try parse_arm(state, true) + } else { + state.next_token = after_then + } + end :: if (alternative) |block| + ast.builder_span(&state.builder, block).end + else + ast.builder_span(&state.builder, then_block).end + return try ast.add_if_stmt( + &state.builder, + ast.Span{ start = keyword.span.start, end = end }, + condition, + then_block, + alternative, + ) +} + +@hide:file +parse_while proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { + keyword :: try consume(state, .while) + condition :: try parse_guard(state) + body :: try parse_arm(state, allow_else) + return try ast.add_while_stmt( + &state.builder, + ast.Span{ start = keyword.span.start, end = ast.builder_span(&state.builder, body).end }, + condition, + body, + ) +} + +@hide:file +parse_guard proc(state @mut State) ast.NodeId ! ParseError { condition_start :: state.next_token condition := parse_expr(state) catch |err| { match err { @@ -227,12 +295,14 @@ parse_if proc(state @mut State) ast.NodeId ! ParseError { yield try ast.add_invalid_expr(&state.builder, current(state).span, diagnostic) } if (ast.builder_expr_kind(&state.builder, condition) == .invalid) { - # discard a malformed guard's tail, but leave its arm and enclosing block intact. + # discard a malformed guard's tail, but leave its body and enclosing block intact. while ( !next_is(state, .open_curly) and !next_is(state, .close_curly) 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, .else) and !next_is(state, .return) + and !next_is(state, .if) and !next_is(state, .while) ) { + if (next_is(state, .ident) and next_n_is(state, 1, .equal)) break state.next_token += 1 } } @@ -249,29 +319,7 @@ parse_if proc(state @mut State) ast.NodeId ! ParseError { diagnostic, ) } - braced :: next_is(state, .open_curly) - then_block :: try parse_arm(state) - # only a braced arm permits its else on a later line. - after_then :: state.next_token - if (braced) skip_newlines(state) - alternative ?ast.NodeId := null - if next_is(state, .else) { - state.next_token += 1 - alternative = try parse_arm(state) - } else { - state.next_token = after_then - } - end :: if (alternative) |block| - ast.builder_span(&state.builder, block).end - else - ast.builder_span(&state.builder, then_block).end - return try ast.add_if_stmt( - &state.builder, - ast.Span{ start = keyword.span.start, end = end }, - condition, - then_block, - alternative, - ) + return condition } @hide:file @@ -290,10 +338,10 @@ parenthesized_guard proc(state @State, start usize) bool { } @hide:file -parse_arm proc(state @mut State) ast.NodeId ! ParseError { +parse_arm proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { if next_is(state, .open_curly) return try parse_block(state) start :: state.next_token - statement :: parse_statement(state, true) catch |err| { + statement :: parse_statement(state, allow_else) catch |err| { match err { .unexpected_token: {} else: return err @@ -314,10 +362,14 @@ parse_arm proc(state @mut State) ast.NodeId ! ParseError { synchronize_arm proc(state @mut State, start usize) ast.Span { first :: state.tokens[start].span end := first.end - while ( - !next_is(state, .eof) and !next_is(state, .newline) - and !next_is(state, .close_curly) and !next_is(state, .else) - ) { + depth usize := 0 + while !next_is(state, .eof) { + if ( + depth == 0 + and (next_is(state, .newline) or next_is(state, .close_curly) or next_is(state, .else)) + ) break + if next_is(state, .open_curly) depth += 1 + if (next_is(state, .close_curly) and depth > 0) depth -= 1 end = current(state).span.end state.next_token += 1 } @@ -485,7 +537,7 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError { @hide:file is_expression_boundary proc(kind lexer.TokenKind) bool { return match kind { - .close_paren, .newline, .close_curly, .open_curly, .else, .return, .if, .eof: true + .close_paren, .newline, .close_curly, .open_curly, .else, .return, .if, .while, .eof: true else: false } } @@ -514,8 +566,12 @@ recover_statement proc( synchronize proc(state @mut State, start usize, stop_at_curly bool) ast.Span { first :: state.tokens[start].span end := first.end - while !next_is(state, .eof) and !next_is(state, .newline) { - if (stop_at_curly and next_is(state, .close_curly)) break + depth usize := 0 + while !next_is(state, .eof) { + if (depth == 0 and next_is(state, .newline)) break + if (stop_at_curly and depth == 0 and next_is(state, .close_curly)) break + if (stop_at_curly and next_is(state, .open_curly)) depth += 1 + if (stop_at_curly and next_is(state, .close_curly) and depth > 0) depth -= 1 end = current(state).span.end state.next_token += 1 } diff --git a/source/parser/parse.test.hon b/source/parser/parse.test.hon index b4a4481..9a96cfb 100644 --- a/source/parser/parse.test.hon +++ b/source/parser/parse.test.hon @@ -510,3 +510,232 @@ recovers_invalid_guard_syntax_and_missing_arm_at_enclosing_brace test { try testing.expect_equal("}", ast.source_text(&tree, arm_error.span)) try testing.expect_equal(3, diagnostics.count(&diagnostic_store)) } + +parses_iterative_factorial_assignments_and_loop 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 :: + `factorial proc(n int) int { + ` remaining := n + ` product := 1 + ` while remaining > 1 { + ` product = product * remaining + ` remaining = remaining - 1 + ` } + ` return product + `} + 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(4, ast.block_statement_count(&tree, body)) + loop_id :: ast.block_statement(&tree, body, 2) + try testing.expect_equal(ast.StmtKind.while_stmt, ast.stmt_kind(&tree, loop_id)) + loop :: ast.while_stmt(&tree, loop_id) + try testing.expect_equal(ast.BinaryOperator.greater, ast.binary_expr(&tree, loop.condition).operator) + try testing.expect_equal(2, ast.block_statement_count(&tree, loop.body)) + update_id :: ast.block_statement(&tree, loop.body, 0) + try testing.expect_equal(ast.StmtKind.assignment_stmt, ast.stmt_kind(&tree, update_id)) + update :: ast.assignment_stmt(&tree, update_id) + try testing.expect_equal(ast.ExprKind.identifier, ast.expr_kind(&tree, update.target)) + try testing.expect_equal("product", ast.source_text(&tree, ast.node_span(&tree, update.target))) + try testing.expect_equal(ast.BinaryOperator.multiply, ast.binary_expr(&tree, update.value).operator) + decrement :: ast.assignment_stmt(&tree, ast.block_statement(&tree, loop.body, 1)) + try testing.expect_equal(ast.BinaryOperator.subtract, ast.binary_expr(&tree, decrement.value).operator) +} + +parses_assignment_precedence_grouped_span_and_rhs_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) + source :: + `main proc() void { + ` value = (1 + 2 * 3) + ` if (true) value = 4 else value = 5 + ` while ready() value = value - 1 + ` value = 6 } + 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(4, ast.block_statement_count(&tree, body)) + assignment :: ast.assignment_stmt(&tree, ast.block_statement(&tree, body, 0)) + try testing.expect_equal("value = (1 + 2 * 3)", ast.source_text(&tree, assignment.span)) + add :: ast.binary_expr(&tree, assignment.value) + try testing.expect_equal(ast.BinaryOperator.add, add.operator) + try testing.expect_equal(ast.BinaryOperator.multiply, ast.binary_expr(&tree, add.right).operator) + conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 1)) + first :: ast.assignment_stmt(&tree, ast.block_statement(&tree, conditional.then_block, 0)) + second :: ast.assignment_stmt(&tree, ast.block_statement(&tree, conditional.else_block?, 0)) + try testing.expect_equal("4", ast.source_text(&tree, ast.node_span(&tree, first.value))) + try testing.expect_equal("5", ast.source_text(&tree, ast.node_span(&tree, second.value))) + loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 2)) + try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, loop.condition)) + try testing.expect_equal(1, ast.block_statement_count(&tree, loop.body)) + try testing.expect_equal(ast.StmtKind.assignment_stmt, ast.stmt_kind(&tree, ast.block_statement(&tree, loop.body, 0))) + last :: ast.assignment_stmt(&tree, ast.block_statement(&tree, body, 3)) + try testing.expect_equal("6", ast.source_text(&tree, ast.node_span(&tree, last.value))) +} + +parses_nested_loops_and_preserves_nearest_if_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 { + ` if (true) while (false) if (true) x = 1 else x = 2 else x = 3 + ` while (true) while (false) x = 4 + ` while (true) { + ` while false { x = 5 } + ` } + `} + 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(3, ast.block_statement_count(&tree, body)) + outer :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 0)) + loop :: ast.while_stmt(&tree, ast.block_statement(&tree, outer.then_block, 0)) + inner :: ast.if_stmt(&tree, ast.block_statement(&tree, loop.body, 0)) + inner_else :: ast.assignment_stmt(&tree, ast.block_statement(&tree, inner.else_block?, 0)) + outer_else :: ast.assignment_stmt(&tree, ast.block_statement(&tree, outer.else_block?, 0)) + try testing.expect_equal("2", ast.source_text(&tree, ast.node_span(&tree, inner_else.value))) + try testing.expect_equal("3", ast.source_text(&tree, ast.node_span(&tree, outer_else.value))) + braceless :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 1)) + nested :: ast.while_stmt(&tree, ast.block_statement(&tree, braceless.body, 0)) + try testing.expect_equal(1, ast.block_statement_count(&tree, nested.body)) + braced :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 2)) + child :: ast.while_stmt(&tree, ast.block_statement(&tree, braced.body, 0)) + try testing.expect_equal(ast.ExprKind.literal_bool, ast.expr_kind(&tree, child.condition)) + try testing.expect_equal(1, ast.block_statement_count(&tree, child.body)) +} + +recovers_loop_guards_bodies_and_assignments_without_losing_braces 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 { x = 1 } + ` while true x = 2 + ` while broken( { x = 3 } + ` x = + ` x = 1 junk + ` if (true) x = else x = 4 + ` kept :: 7 + ` while (false) } + `tail :: 8 + 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(diagnostics.count(&diagnostic_store) > 0) + try testing.expect_equal(2, ast.package_decl_count(&tree)) + body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body + try testing.expect_equal(8, ast.block_statement_count(&tree, body)) + for 0..3 |index| { + loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, usize(index))) + try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, loop.condition)) + try testing.expect_equal(1, ast.block_statement_count(&tree, loop.body)) + try testing.expect_equal(ast.StmtKind.assignment_stmt, ast.stmt_kind(&tree, ast.block_statement(&tree, loop.body, 0))) + } + missing :: ast.assignment_stmt(&tree, ast.block_statement(&tree, body, 3)) + try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, missing.value)) + try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 4))) + conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 5)) + missing_arm :: ast.assignment_stmt(&tree, ast.block_statement(&tree, conditional.then_block, 0)) + try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, missing_arm.value)) + try testing.expect(conditional.else_block != null) + kept :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 6)) + try testing.expect_equal("kept", ast.source_text(&tree, kept.name)) + last :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 7)) + try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, last.body, 0))) + tail :: ast.statement_decl(&tree, ast.package_decl(&tree, 1)) + try testing.expect_equal("tail", ast.source_text(&tree, tail.name)) +} + +rejects_top_level_assignment_loop_else_and_assignment_expressions 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 :: + `x = 1 + `main proc() void { + ` while (true) x = 2 else x = 3 + ` x = y = 4 + ` kept :: 5 + `} + `tail :: 6 + 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(diagnostics.count(&diagnostic_store) > 0) + try testing.expect_equal(3, ast.package_decl_count(&tree)) + try testing.expect_equal(ast.DeclKind.invalid, ast.decl_kind(&tree, ast.package_decl(&tree, 0))) + body :: ast.function_decl(&tree, ast.package_decl(&tree, 1)).body + try testing.expect_equal(4, ast.block_statement_count(&tree, body)) + loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 0)) + try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, loop.body, 0))) + try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 1))) + try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 2))) + kept :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 3)) + try testing.expect_equal("kept", ast.source_text(&tree, kept.name)) +} + +recovers_nested_braces_in_malformed_loop_body_and_assignment 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) @ { + ` bad() + ` } + ` x = { + ` bad() + ` } + ` kept :: 7 + `} + `tail :: 8 + 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(diagnostics.count(&diagnostic_store) > 0) + try testing.expect_equal(2, ast.package_decl_count(&tree)) + body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body + try testing.expect_equal(3, ast.block_statement_count(&tree, body)) + loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 0)) + try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, loop.body, 0))) + try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 1))) + kept :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 2)) + try testing.expect_equal("kept", ast.source_text(&tree, kept.name)) + tail :: ast.statement_decl(&tree, ast.package_decl(&tree, 1)) + try testing.expect_equal("tail", ast.source_text(&tree, tail.name)) +} diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index 00ced9a..f318036 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -27,7 +27,20 @@ Result :: struct { storage Storage } no_index :: maxval!(usize) @hide:file -Role :: enum { none, expression, declaration, parameter, function, annotation, invalid, body, return_stmt, if_stmt } +Role :: enum { + none, + expression, + declaration, + parameter, + function, + annotation, + invalid, + body, + return_stmt, + if_stmt, + assignment_stmt, + while_stmt, +} @hide:file Cell :: struct { @@ -39,6 +52,8 @@ Cell :: struct { anchored bool = false demand TypeKind = .none fixed bool = false + local bool = false + assignment_value bool = false first_edge usize = no_index return_annotation ast.NodeId = ast.NodeId(maxval!(u32)) } @@ -57,6 +72,8 @@ State :: struct { types []mut Type cells []mut Cell edges arraylist.ArrayList(Edge) + collecting_assignment bool = false + has_assignments bool = false } #! borrows the AST, resolution, and diagnostic store only during checking. @@ -95,6 +112,19 @@ check proc( .invalid: collect_invalid(&state, id) } } + for (0..count) |i| { + if (cells[i].role != .assignment_stmt) continue + target :: ast.assignment_stmt(tree, ast.NodeId(i)).target + binding :: sema.binding(resolution, target) + if (binding.kind == .invalid) { + cells[i].type = invalid_type(binding.data.diagnostic) + continue + } + if (binding.kind != .declaration or mutable_target(&state, target)) continue + cells[i].type = invalid_type(try diagnostics.add( + diagnostic_store, .invalid_assignment_target, ast.node_span(tree, target), + )) + } # aliases share a type variable; explicit annotations and parameters are boundaries. for (0..count) |i| { @@ -113,138 +143,155 @@ check proc( } if (cells[i].role != .expression or ast.expr_kind(tree, id) != .identifier) continue binding :: sema.binding(resolution, id) - if (binding.kind == .declaration and !cells[usize(binding.data.declaration)].fixed) { + if (binding.kind == .declaration and !cells[i].assignment_value and !cells[usize(binding.data.declaration)].fixed) { join(&state, id, binding.data.declaration) } } - # function aliases are known before any calls or bodies are checked. Honey's - # return annotations are scalar, so a call cannot introduce a function type. - for (0..count) |i| { - id :: ast.NodeId(i) - if (cells[i].role != .expression or ast.expr_kind(tree, id) != .call) continue - callee :: ast.call_expr(tree, id).callee - signature :: group_type(&state, callee) - if (signature.function != ast.NodeId(maxval!(u32))) { - result :: ast.function_decl(tree, signature.function).return_type - seed(&state, id, types[usize(result)]) - } else if (signature.kind == .invalid) { - seed(&state, id, signature) - } - } - for (0..count) |i| { - id :: ast.NodeId(i) - if (cells[i].role != .expression or ast.expr_kind(tree, id) != .call) continue - callee :: ast.call_expr(tree, id).callee - signature :: group_type(&state, callee) - if (signature.function != ast.NodeId(maxval!(u32))) continue - if (signature.kind == .invalid) { - seed(&state, id, signature) - } else if (signature.kind != .none or cells[root(&state, usize(callee))].numeric) { - seed(&state, id, invalid_type(try diagnostics.add(diagnostic_store, .not_callable, ast.node_span(tree, callee)))) - } else { - # without a function anchor this dependency cannot become callable; - # share its eventual inference error instead of emitting a cascade. - join(&state, id, callee) - } - } - for (0..count) |i| { - if (cells[i].role == .none or cells[i].role == .body) continue - types[i] = cells[root(&state, i)].type - } - # reject inherently nonnumeric operations before connecting their operands. - # their otherwise valid inputs must not join a failed constraint component. - for (0..count) |i| { - id :: ast.NodeId(i) - if (cells[i].role != .expression) continue - match ast.expr_kind(tree, id) { - .unary: try require_numeric(&state, id, ast.unary_expr(tree, id).operand) - .binary: { - expression :: ast.binary_expr(tree, id) - if (bool_equality(&state, id)) continue - try require_numeric(&state, id, expression.left) - try require_numeric(&state, id, expression.right) + # assignment inputs observe established declaration types, never contribute + # constraints back into those declarations. + for 0..2 |phase| { + if (phase == 1 and !state.has_assignments) break + assignment_values :: phase == 1 + if (assignment_values) { + for (0..count) |i| { + id :: ast.NodeId(i) + if (!cells[i].assignment_value or cells[i].role != .expression or ast.expr_kind(tree, id) != .identifier) continue + binding :: sema.binding(resolution, id) + if (binding.kind == .declaration) cells[i].type = types[usize(binding.data.declaration)] } - else: {} } - } - propagate(&state, queue) + # function aliases are known before calls; scalar return annotations + # mean a call cannot introduce a function type. + for (0..count) |i| { + id :: ast.NodeId(i) + if (cells[i].role != .expression or cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .call) continue + callee :: ast.call_expr(tree, id).callee + signature :: group_type(&state, callee) + if (signature.function != ast.NodeId(maxval!(u32))) { + result :: ast.function_decl(tree, signature.function).return_type + seed(&state, id, types[usize(result)]) + } else if (signature.kind == .invalid) { + seed(&state, id, signature) + } + } + for (0..count) |i| { + id :: ast.NodeId(i) + if (cells[i].role != .expression or cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .call) continue + callee :: ast.call_expr(tree, id).callee + signature :: group_type(&state, callee) + if (signature.function != ast.NodeId(maxval!(u32))) continue + if (signature.kind == .invalid) { + seed(&state, id, signature) + } else if (signature.kind != .none or cells[root(&state, usize(callee))].numeric) { + seed(&state, id, invalid_type(try diagnostics.add(diagnostic_store, .not_callable, ast.node_span(tree, callee)))) + } else { + # without a function anchor, share the eventual inference error. + join(&state, id, callee) + } + } + for (0..count) |i| { + if (cells[i].role == .none or cells[i].role == .body or cells[i].assignment_value != assignment_values or types[i].kind == .invalid) continue + types[i] = cells[root(&state, i)].type + } + # reject nonnumeric operations before connecting otherwise valid inputs. + for (0..count) |i| { + id :: ast.NodeId(i) + if (cells[i].role != .expression or cells[i].assignment_value != assignment_values) continue + match ast.expr_kind(tree, id) { + .unary: try require_numeric(&state, id, ast.unary_expr(tree, id).operand) + .binary: { + expression :: ast.binary_expr(tree, id) + if (bool_equality(&state, id)) continue + try require_numeric(&state, id, expression.left) + try require_numeric(&state, id, expression.right) + } + else: {} + } + } + propagate(&state, queue) - # numeric edges are undirected constraints, not a traversal-time conversion. - # rigid values supply demands without joining the component they constrain. - for (0..count) |i| { - id :: ast.NodeId(i) - if (cells[i].role != .expression) continue - match ast.expr_kind(tree, id) { - .unary: try numeric_operand(&state, id, ast.unary_expr(tree, id).operand) - .binary: { - expression :: ast.binary_expr(tree, id) - if (comparison(expression.operator)) { - if (!bool_equality(&state, id)) try comparison_operands(&state, id) - } else { - try numeric_operand(&state, id, expression.left) - try numeric_operand(&state, id, expression.right) - if (expression.operator == .divide) { - try impose(&state, id, .float, .integer_division, expression.span) + # numeric edges join flexible operands; rigid inputs supply only demands. + for (0..count) |i| { + id :: ast.NodeId(i) + if (cells[i].role != .expression or cells[i].assignment_value != assignment_values) continue + match ast.expr_kind(tree, id) { + .unary: try numeric_operand(&state, id, ast.unary_expr(tree, id).operand) + .binary: { + expression :: ast.binary_expr(tree, id) + if (comparison(expression.operator)) { + if (!bool_equality(&state, id)) try comparison_operands(&state, id) + } else { + try numeric_operand(&state, id, expression.left) + try numeric_operand(&state, id, expression.right) + if (expression.operator == .divide) { + try impose(&state, id, .float, .integer_division, expression.span) + } } } - } - else: {} - } - } - for (0..count) |i| { - id :: ast.NodeId(i) - if (cells[i].role == .declaration) { - declaration :: ast.statement_decl(tree, id) - if (declaration.type) |annotation| { - try demand(&state, declaration.initializer, types[usize(annotation)], .type_mismatch) + else: {} } } - if (cells[i].role == .return_stmt) { - if (ast.return_stmt(tree, id).value) |value| { - try demand(&state, value, types[usize(cells[i].return_annotation)], .return_type_mismatch) + for (0..count) |i| { + id :: ast.NodeId(i) + if (!assignment_values and cells[i].role == .declaration) { + declaration :: ast.statement_decl(tree, id) + if (declaration.type) |annotation| { + try demand(&state, declaration.initializer, types[usize(annotation)], .type_mismatch) + } + } + if (!assignment_values and cells[i].role == .return_stmt) { + if (ast.return_stmt(tree, id).value) |value| { + try demand(&state, value, types[usize(cells[i].return_annotation)], .return_type_mismatch) + } + } + if (assignment_values and cells[i].role == .assignment_stmt) { + assignment :: ast.assignment_stmt(tree, id) + if (mutable_target(&state, assignment.target)) { + try demand(&state, assignment.value, types[usize(assignment.target)], .type_mismatch) + } + } + if (cells[i].role != .expression or cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .call) continue + signature :: group_type(&state, ast.call_expr(tree, id).callee) + if (signature.function == ast.NodeId(maxval!(u32))) continue + parameter_count :: ast.function_param_count(tree, signature.function) + for (0..ast.call_arg_count(tree, id)) |j| { + if (j >= parameter_count) continue + parameter :: ast.function_param(tree, signature.function, j) + try demand(&state, ast.call_arg(tree, id, j), cells[usize(parameter)].type, .type_mismatch) } } - if (cells[i].role != .expression or ast.expr_kind(tree, id) != .call) continue - signature :: group_type(&state, ast.call_expr(tree, id).callee) - if (signature.function == ast.NodeId(maxval!(u32))) continue - parameter_count :: ast.function_param_count(tree, signature.function) - for (0..ast.call_arg_count(tree, id)) |j| { - if (j >= parameter_count) continue - parameter :: ast.function_param(tree, signature.function, j) - try demand(&state, ast.call_arg(tree, id, j), cells[usize(parameter)].type, .type_mismatch) - } - } - # default only after the complete program has supplied its numeric demands. - for (0..count) |i| { - if (root(&state, i) != i) continue - if (cells[i].type.kind == .invalid) continue - if (cells[i].numeric and cells[i].anchored) { - kind TypeKind :: if (cells[i].demand == .float) .float else .int - cells[i].type = Type{ kind = kind } + # defaults follow ordinary program demands, then isolated assignment demands. + for (0..count) |i| { + if (root(&state, i) != i or cells[i].assignment_value != assignment_values) continue + if (cells[i].type.kind == .invalid) continue + if (cells[i].numeric and cells[i].anchored) { + kind TypeKind :: if (cells[i].demand == .float) .float else .int + cells[i].type = Type{ kind = kind } + } } - } - for (0..count) |i| { - id :: ast.NodeId(i) - if (cells[i].role != .expression or ast.expr_kind(tree, id) != .call) continue - callee :: ast.call_expr(tree, id).callee - signature :: group_type(&state, callee) - if (signature.function != ast.NodeId(maxval!(u32)) or signature.kind == .none or signature.kind == .invalid) continue - if (group_type(&state, id).kind != .invalid) { - seed(&state, id, invalid_type(try diagnostics.add(diagnostic_store, .not_callable, ast.node_span(tree, callee)))) + for (0..count) |i| { + id :: ast.NodeId(i) + if (cells[i].role != .expression or cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .call) continue + callee :: ast.call_expr(tree, id).callee + signature :: group_type(&state, callee) + if (signature.function != ast.NodeId(maxval!(u32)) or signature.kind == .none or signature.kind == .invalid) continue + if (group_type(&state, id).kind != .invalid) { + seed(&state, id, invalid_type(try diagnostics.add(diagnostic_store, .not_callable, ast.node_span(tree, callee)))) + } + } + for (0..count) |i| { + if (assignment_values or cells[i].role != .declaration or types[i].kind == .invalid) continue + index :: root(&state, i) + if (cells[index].type.kind != .none) continue + diagnostic :: try diagnostics.add(diagnostic_store, .cannot_infer_type, ast.statement_decl(tree, ast.NodeId(i)).name) + cells[index].type = invalid_type(diagnostic) + } + for (0..count) |i| { + if (cells[i].role == .none or cells[i].role == .body or cells[i].assignment_value != assignment_values or types[i].kind == .invalid) continue + types[i] = cells[root(&state, i)].type } - } - for (0..count) |i| { - if (cells[i].role != .declaration or types[i].kind == .invalid) continue - index :: root(&state, i) - if (cells[index].type.kind != .none) continue - diagnostic :: try diagnostics.add(diagnostic_store, .cannot_infer_type, ast.statement_decl(tree, ast.NodeId(i)).name) - cells[index].type = invalid_type(diagnostic) - } - for (0..count) |i| { - if (cells[i].role == .none or cells[i].role == .body or types[i].kind == .invalid) continue - types[i] = cells[root(&state, i)].type } for (0..count) |i| try validate(&state, ast.NodeId(i)) @@ -253,8 +300,9 @@ check proc( propagate(&state, queue) # validate conditions after operand errors have reached their users. for (0..count) |i| { - if (cells[i].role != .if_stmt) continue - condition :: ast.if_stmt(tree, ast.NodeId(i)).condition + id :: ast.NodeId(i) + if (cells[i].role != .if_stmt and cells[i].role != .while_stmt) continue + condition :: if (cells[i].role == .if_stmt) ast.if_stmt(tree, id).condition else ast.while_stmt(tree, id).condition actual :: types[usize(condition)] if (actual.kind != .invalid and actual.kind != .bool) { try fail(&state, condition, .condition_not_bool, ast.node_span(tree, condition)) @@ -465,7 +513,10 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId) void ! (m for (0..ast.block_statement_count(tree, id)) |i| { statement :: ast.block_statement(tree, id, i) match ast.stmt_kind(tree, statement) { - .declaration: try collect_declaration(state, statement) + .declaration: { + state.cells[usize(statement)].local = true + try collect_declaration(state, statement) + } .expression: try collect_expression(state, ast.statement_expr(tree, statement)) .return_stmt: { state.cells[usize(statement)].role = .return_stmt @@ -489,6 +540,35 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId) void ! (m try depend(state, other, id) } } + .assignment_stmt: { + assignment :: ast.assignment_stmt(tree, statement) + state.cells[usize(statement)].role = .assignment_stmt + state.has_assignments = true + state.cells[usize(statement)].type = Type{ kind = .void } + binding :: sema.binding(state.resolution, assignment.target) + if (binding.kind == .builtin) { + state.cells[usize(assignment.target)].role = .expression + state.cells[usize(assignment.target)].type = invalid_type(try diagnostics.add( + state.diagnostics, .invalid_assignment_target, ast.node_span(tree, assignment.target), + )) + } else { + try collect_expression(state, assignment.target) + } + state.collecting_assignment = true + try collect_expression(state, assignment.value) + state.collecting_assignment = false + try depend(state, assignment.target, statement) + try depend(state, assignment.value, statement) + } + .while_stmt: { + loop :: ast.while_stmt(tree, statement) + state.cells[usize(statement)].role = .while_stmt + 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 depend(state, loop.body, id) + } .invalid: collect_invalid(state, statement) } source :: if (ast.stmt_kind(tree, statement) == .expression) ast.statement_expr(tree, statement) else statement @@ -501,6 +581,7 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError if (state.cells[usize(id)].role != .none) return tree :: state.tree state.cells[usize(id)].role = .expression + state.cells[usize(id)].assignment_value = state.collecting_assignment match ast.expr_kind(tree, id) { .literal_int: { state.cells[usize(id)].numeric = true @@ -641,11 +722,39 @@ fail proc(state @mut State, id ast.NodeId, code diagnostics.Code, span ast.Span) state.types[usize(id)] = invalid_type(try diagnostics.add(state.diagnostics, code, span)) } +@hide:file +mutable_target proc(state @State, id ast.NodeId) bool { + binding :: sema.binding(state.resolution, id) + if (binding.kind != .declaration) return false + target :: binding.data.declaration + if (state.cells[usize(target)].role != .declaration or !state.cells[usize(target)].local) return false + return ast.statement_decl(state.tree, target).mutability == .variable +} + +@hide:file +same_signature proc(state @State, left, right ast.NodeId) bool { + tree :: state.tree + if (ast.function_param_count(tree, left) != ast.function_param_count(tree, right)) return false + left_result :: ast.function_decl(tree, left).return_type + right_result :: ast.function_decl(tree, right).return_type + if (state.types[usize(left_result)].kind != state.types[usize(right_result)].kind) return false + for (0..ast.function_param_count(tree, left)) |i| { + left_param :: ast.function_param(tree, left, i) + right_param :: ast.function_param(tree, right, i) + if (state.types[usize(left_param)].kind != state.types[usize(right_param)].kind) return false + } + return true +} + @hide:file compatible proc(state @mut State, value ast.NodeId, expected Type) void ! (mem.AllocError | diagnostics.CapacityError) { actual :: state.types[usize(value)] if (actual.kind == .invalid or expected.kind == .invalid) return - if (actual.kind != expected.kind) try fail(state, value, .type_mismatch, ast.node_span(state.tree, value)) + if (actual.kind != expected.kind) { + try fail(state, value, .type_mismatch, ast.node_span(state.tree, value)) + } else if (actual.kind == .function and !same_signature(state, actual.function, expected.function)) { + try fail(state, value, .type_mismatch, ast.node_span(state.tree, value)) + } } @hide:file @@ -659,6 +768,12 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost } if (declaration.type) |type| try compatible(state, declaration.initializer, state.types[usize(type)]) } + .assignment_stmt: { + assignment :: ast.assignment_stmt(tree, id) + if (mutable_target(state, assignment.target)) { + try compatible(state, assignment.value, state.types[usize(assignment.target)]) + } + } .return_stmt: { statement :: ast.return_stmt(tree, id) expected :: state.types[usize(state.cells[usize(id)].return_annotation)] diff --git a/source/sema/check/check.test.hon b/source/sema/check/check.test.hon index 8f376a9..1b11490 100644 --- a/source/sema/check/check.test.hon +++ b/source/sema/check/check.test.hon @@ -968,3 +968,284 @@ analyzes_return_paths_after_trap_propagation_without_poisoning_signatures test { try testing.expect_equal(2, code_count(&diagnostic_store, .missing_return)) try testing.expect_equal(1, code_count(&diagnostic_store, .condition_not_bool)) } + +assignments_preserve_established_types_and_contextualize_numeric_values 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 :: + `take_float proc(value float) int { return 1 } + `main proc() void { + ` number := 0 + ` number = 1.5 + ` number = number + 1.5 + ` number = take_float(number) + ` observed :: number + ` quotient float := 0 + ` quotient = 3 / 2 + ` inferred := 0 + ` context float :: inferred + ` inferred = 5 / 2 + ` rigid int := 1 + ` quotient = rigid + ` flag := true + ` flag = false + ` text := "before" + ` text = "after" + `} + 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) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store) + defer deinit(&result) + + body :: ast.function_decl(&tree, ast.package_decl(&tree, 1)).body + number :: ast.block_statement(&tree, body, 0) + observed :: ast.block_statement(&tree, body, 4) + quotient :: ast.block_statement(&tree, body, 5) + inferred :: ast.block_statement(&tree, body, 7) + rigid :: ast.block_statement(&tree, body, 10) + declarations [3]ast.NodeId :: [number, observed, rigid] + for declarations |id| { + try expect_kind(&result, id, .int) + try expect_kind(&result, ast.statement_decl(&tree, id).initializer, .int) + } + try expect_kind(&result, quotient, .float) + try expect_kind(&result, inferred, .float) + bad_values [2]usize :: [1, 11] + for bad_values |index| { + statement :: ast.block_statement(&tree, body, index) + assignment :: ast.assignment_stmt(&tree, statement) + original :: try expect_error(&result, &diagnostic_store, assignment.value, .type_mismatch, ast.node_span(&tree, assignment.value)) + try testing.expect_equal(original, type_of(&result, statement).diagnostic) + expected :: if (index == 1) TypeKind.int else TypeKind.float + try expect_kind(&result, assignment.target, expected) + } + arithmetic_statement :: ast.block_statement(&tree, body, 2) + arithmetic :: ast.assignment_stmt(&tree, arithmetic_statement) + original :: try expect_error(&result, &diagnostic_store, arithmetic.value, .invalid_operand, ast.node_span(&tree, arithmetic.value)) + try testing.expect_equal(original, type_of(&result, arithmetic_statement).diagnostic) + try expect_kind(&result, arithmetic.target, .int) + try expect_kind(&result, ast.binary_expr(&tree, arithmetic.value).left, .int) + call_statement :: ast.block_statement(&tree, body, 3) + call :: ast.assignment_stmt(&tree, call_statement) + argument :: ast.call_arg(&tree, call.value, 0) + call_error :: try expect_error(&result, &diagnostic_store, argument, .type_mismatch, ast.node_span(&tree, argument)) + try testing.expect_equal(call_error, type_of(&result, call_statement).diagnostic) + try expect_kind(&result, call.target, .int) + divisions [2]usize :: [6, 9] + for divisions |index| { + statement :: ast.block_statement(&tree, body, index) + assignment :: ast.assignment_stmt(&tree, statement) + division :: ast.binary_expr(&tree, assignment.value) + try expect_kind(&result, statement, .void) + try expect_kind(&result, assignment.target, .float) + try expect_kind(&result, assignment.value, .float) + try expect_kind(&result, division.left, .float) + try expect_kind(&result, division.right, .float) + } + scalar_assignments [2]usize :: [13, 15] + for scalar_assignments |index| { + statement :: ast.block_statement(&tree, body, index) + assignment :: ast.assignment_stmt(&tree, statement) + expected :: if (index == 13) TypeKind.bool else TypeKind.string + try expect_kind(&result, statement, .void) + try expect_kind(&result, assignment.target, expected) + try expect_kind(&result, assignment.value, expected) + } + try testing.expect_equal(3, code_count(&diagnostic_store, .type_mismatch)) + try testing.expect_equal(1, code_count(&diagnostic_store, .invalid_operand)) + try testing.expect_equal(0, code_count(&diagnostic_store, .integer_division)) +} + +assignment_target_errors_preserve_bindings_and_independent_rhs_diagnostics 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 :: + `global := 1 + `named proc() void {} + `main proc(parameter int) void { + ` constant :: 1 + ` constant = 2 + ` parameter = 2 + ` global = 2 + ` named = named + ` int = 2 + ` absent = other_missing + ` constant = -false + `} + 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) + + function :: ast.package_decl(&tree, 2) + body :: ast.function_decl(&tree, function).body + for usize(1)..usize(6) |index| { + statement :: ast.block_statement(&tree, body, index) + assignment :: ast.assignment_stmt(&tree, statement) + _ = try expect_error(&result, &diagnostic_store, statement, .invalid_assignment_target, ast.node_span(&tree, assignment.target)) + expected :: if (index == 4) TypeKind.function else TypeKind.int + try expect_kind(&result, assignment.value, expected) + if (index != 5) try testing.expect(type_of(&result, assignment.target).kind != .invalid) + } + unknown_statement :: ast.block_statement(&tree, body, 6) + unknown :: ast.assignment_stmt(&tree, unknown_statement) + target_error :: sema.binding(&resolution, unknown.target).data.diagnostic + value_error :: sema.binding(&resolution, unknown.value).data.diagnostic + try testing.expect(target_error != value_error) + try testing.expect_equal(target_error, type_of(&result, unknown.target).diagnostic) + try testing.expect_equal(target_error, type_of(&result, unknown_statement).diagnostic) + try testing.expect_equal(value_error, type_of(&result, unknown.value).diagnostic) + independent_statement :: ast.block_statement(&tree, body, 7) + independent :: ast.assignment_stmt(&tree, independent_statement) + _ = try expect_error(&result, &diagnostic_store, independent.value, .invalid_operand, ast.node_span(&tree, independent.value)) + _ = try expect_error(&result, &diagnostic_store, independent_statement, .invalid_assignment_target, ast.node_span(&tree, independent.target)) + try expect_kind(&result, ast.block_statement(&tree, body, 0), .int) + try expect_kind(&result, ast.function_param(&tree, function, 0), .int) + try expect_kind(&result, ast.package_decl(&tree, 0), .int) + try expect_kind(&result, ast.package_decl(&tree, 1), .function) + try testing.expect_equal(6, code_count(&diagnostic_store, .invalid_assignment_target)) + try testing.expect_equal(2, code_count(&diagnostic_store, .unknown_name)) + try testing.expect_equal(0, code_count(&diagnostic_store, .type_not_value)) +} + +function_assignment_checks_complete_signatures_without_retargeting_calls 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 :: + `first proc(a int) int { return a } + `same proc(renamed int) int { return renamed + 1 } + `wrong_parameter proc(a float) int { return 1 } + `wrong_result proc(a int) bool { return true } + `wrong_count proc() int { return 2 } + `main proc() int { + ` chosen := first + ` chosen = same + ` chosen = wrong_parameter + ` chosen = wrong_result + ` chosen = wrong_count + ` answer :: chosen(7) + ` return answer + `} + 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) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store) + defer deinit(&result) + + first :: ast.package_decl(&tree, 0) + body :: ast.function_decl(&tree, ast.package_decl(&tree, 5)).body + chosen :: ast.block_statement(&tree, body, 0) + try expect_kind(&result, chosen, .function) + try testing.expect_equal(first, type_of(&result, chosen).function) + try expect_kind(&result, ast.block_statement(&tree, body, 1), .void) + for usize(2)..usize(5) |index| { + statement :: ast.block_statement(&tree, body, index) + assignment :: ast.assignment_stmt(&tree, statement) + original :: try expect_error(&result, &diagnostic_store, assignment.value, .type_mismatch, ast.node_span(&tree, assignment.value)) + try testing.expect_equal(original, type_of(&result, statement).diagnostic) + try expect_kind(&result, assignment.target, .function) + try testing.expect_equal(first, type_of(&result, assignment.target).function) + } + answer :: ast.block_statement(&tree, body, 5) + call :: ast.statement_decl(&tree, answer).initializer + try expect_kind(&result, answer, .int) + try expect_kind(&result, call, .int) + try testing.expect_equal(first, type_of(&result, ast.call_expr(&tree, call).callee).function) + try testing.expect_equal(3, code_count(&diagnostic_store, .type_mismatch)) + try testing.expect_equal(0, code_count(&diagnostic_store, .argument_count_mismatch)) +} + +while_checks_nested_bodies_without_claiming_a_zero_iteration_return 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 :: + `missing proc() int { while true { return 1 } } + `bad_body proc() int { while false { -"bad" } } + `continues proc() int { + ` while false { -"bad" } + ` return 7 + `} + `bad_condition proc() int { while 1 { -"bad" } } + `unknown_condition proc() int { while absent { missing_body } } + `nested proc() void { + ` counter := 2 + ` while counter > 0 { + ` inner := 1 + ` while inner > 0 { + ` inner = inner - 1 + ` counter = counter - 1 + ` } + ` } + `} + 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(2) |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) + } + continues_body :: ast.function_decl(&tree, ast.package_decl(&tree, 2)).body + loop_statement :: ast.block_statement(&tree, continues_body, 0) + loop :: ast.while_stmt(&tree, loop_statement) + invalid_body :: ast.statement_expr(&tree, ast.block_statement(&tree, loop.body, 0)) + original :: try expect_error(&result, &diagnostic_store, invalid_body, .invalid_operand, ast.node_span(&tree, invalid_body)) + try testing.expect_equal(original, type_of(&result, loop.body).diagnostic) + try testing.expect_equal(original, type_of(&result, continues_body).diagnostic) + try expect_kind(&result, loop_statement, .void) + try expect_kind(&result, ast.block_statement(&tree, continues_body, 1), .void) + bad_condition_body :: ast.function_decl(&tree, ast.package_decl(&tree, 3)).body + bad_condition_statement :: ast.block_statement(&tree, bad_condition_body, 0) + condition :: ast.while_stmt(&tree, bad_condition_statement).condition + condition_error :: try expect_error(&result, &diagnostic_store, condition, .condition_not_bool, ast.node_span(&tree, condition)) + try testing.expect_equal(condition_error, type_of(&result, bad_condition_statement).diagnostic) + unknown_body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body + unknown_statement :: ast.block_statement(&tree, unknown_body, 0) + unknown :: ast.while_stmt(&tree, unknown_statement).condition + try testing.expect_equal(sema.binding(&resolution, unknown).data.diagnostic, type_of(&result, unknown_statement).diagnostic) + nested_body :: ast.function_decl(&tree, ast.package_decl(&tree, 5)).body + try expect_kind(&result, nested_body, .none) + outer_loop :: ast.while_stmt(&tree, ast.block_statement(&tree, nested_body, 1)) + inner_loop :: ast.while_stmt(&tree, ast.block_statement(&tree, outer_loop.body, 1)) + for usize(0)..usize(2) |index| { + statement :: ast.block_statement(&tree, inner_loop.body, index) + assignment :: ast.assignment_stmt(&tree, statement) + try expect_kind(&result, statement, .void) + try expect_kind(&result, assignment.target, .int) + try expect_kind(&result, assignment.value, .int) + } + try testing.expect_equal(2, code_count(&diagnostic_store, .missing_return)) + try testing.expect_equal(1, code_count(&diagnostic_store, .condition_not_bool)) + try testing.expect_equal(3, code_count(&diagnostic_store, .invalid_operand)) +} diff --git a/source/sema/resolve.hon b/source/sema/resolve.hon index 9af2ab9..4095973 100644 --- a/source/sema/resolve.hon +++ b/source/sema/resolve.hon @@ -209,6 +209,16 @@ resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (me try resolve_arm(state, scope, conditional.then_block) if (conditional.else_block) |other| try resolve_arm(state, scope, other) } + .assignment_stmt: { + assignment :: ast.assignment_stmt(tree, statement) + try resolve_expr(state, scope, assignment.target) + try resolve_expr(state, scope, assignment.value) + } + .while_stmt: { + loop :: ast.while_stmt(tree, statement) + try resolve_expr(state, scope, loop.condition) + try resolve_arm(state, scope, loop.body) + } .invalid: resolve_invalid(state, statement) } } diff --git a/source/sema/resolve.test.hon b/source/sema/resolve.test.hon index 8f97417..4d61e30 100644 --- a/source/sema/resolve.test.hon +++ b/source/sema/resolve.test.hon @@ -366,3 +366,76 @@ resolves_conditional_arm_scopes_without_leaks_or_shadowing test { try testing.expect_equal(binding(&resolution, shadow).data.diagnostic, binding(&resolution, use).data.diagnostic) try testing.expect_equal(4, diagnostics.count(&diagnostic_store)) } + +resolves_loop_assignments_in_lexical_scopes_with_forward_local_bindings 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(flag bool) void { + ` outer := 2 + ` while flag { + ` later = outer + ` later := 1 + ` while later > 0 { + ` outer = later + ` inner := later + ` } + ` inner + ` } + ` while flag { later := 3 } + ` later = unknown_rhs + ` while flag { + ` outer := 1 + ` outer = 2 + ` bool := false + ` } + `} + 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) + + function :: ast.package_decl(&tree, 0) + body :: ast.function_decl(&tree, function).body + flag :: ast.function_param(&tree, function, 0) + outer :: ast.block_statement(&tree, body, 0) + first :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 1)) + try testing.expect_equal(flag, binding(&resolution, first.condition).data.declaration) + forward :: ast.assignment_stmt(&tree, ast.block_statement(&tree, first.body, 0)) + later :: ast.block_statement(&tree, first.body, 1) + try testing.expect_equal(BindingKind.declaration, binding(&resolution, forward.target).kind) + try testing.expect_equal(later, binding(&resolution, forward.target).data.declaration) + try testing.expect_equal(outer, binding(&resolution, forward.value).data.declaration) + nested :: ast.while_stmt(&tree, ast.block_statement(&tree, first.body, 2)) + guard :: ast.binary_expr(&tree, nested.condition) + try testing.expect_equal(later, binding(&resolution, guard.left).data.declaration) + update :: ast.assignment_stmt(&tree, ast.block_statement(&tree, nested.body, 0)) + try testing.expect_equal(outer, binding(&resolution, update.target).data.declaration) + try testing.expect_equal(later, binding(&resolution, update.value).data.declaration) + inner :: ast.block_statement(&tree, nested.body, 1) + try testing.expect_equal(later, binding(&resolution, ast.statement_decl(&tree, inner).initializer).data.declaration) + escaped_inner :: ast.statement_expr(&tree, ast.block_statement(&tree, first.body, 3)) + try testing.expect_equal(BindingKind.invalid, binding(&resolution, escaped_inner).kind) + sibling :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 2)) + sibling_local :: ast.block_statement(&tree, sibling.body, 0) + try testing.expect_equal(BindingKind.declaration, binding(&resolution, sibling_local).kind) + try testing.expect(sibling_local != later) + escaped :: ast.assignment_stmt(&tree, ast.block_statement(&tree, body, 3)) + try testing.expect_equal(BindingKind.invalid, binding(&resolution, escaped.target).kind) + try testing.expect_equal(BindingKind.invalid, binding(&resolution, escaped.value).kind) + try testing.expect(binding(&resolution, escaped.target).data.diagnostic != binding(&resolution, escaped.value).data.diagnostic) + forbidden :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 4)) + shadow :: ast.block_statement(&tree, forbidden.body, 0) + poisoned :: ast.assignment_stmt(&tree, ast.block_statement(&tree, forbidden.body, 1)) + builtin :: ast.block_statement(&tree, forbidden.body, 2) + try testing.expect_equal(BindingKind.invalid, binding(&resolution, shadow).kind) + try testing.expect_equal(binding(&resolution, shadow).data.diagnostic, binding(&resolution, poisoned.target).data.diagnostic) + try testing.expect_equal(BindingKind.invalid, binding(&resolution, builtin).kind) + try testing.expect_equal(5, diagnostics.count(&diagnostic_store)) +}