feat: execute local assignments and while loops

This commit is contained in:
2026-10-10 00:17:12 +02:00
parent 021cbecac8
commit b35a68dff8
16 changed files with 1440 additions and 170 deletions
+11 -5
View File
@@ -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`. 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 `/`. 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. 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. `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. `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. 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.
+11
View File
@@ -0,0 +1,11 @@
main proc() int {
product := 1
remaining := 5
while remaining > 1 {
product = product * remaining
remaining = remaining - 1
}
return product
}
+70 -4
View File
@@ -1,13 +1,13 @@
#! grammar: #! grammar:
#! * type ..... -> identifier #! * type ..... -> identifier
#! * assignment -> :: | := #! * binding ... -> :: | :=
#! * literal .. -> int | float | string | 'true' | 'false' #! * literal .. -> int | float | string | 'true' | 'false'
#! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>=' #! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>='
#! * expr ..... -> identifier | literal | '(' expr ')' | '-' expr | expr op expr | expr '(' [expr {',' expr} [',']] ')' #! * expr ..... -> identifier | literal | '(' expr ')' | '-' expr | expr op expr | expr '(' [expr {',' expr} [',']] ')'
#! * decl ..... -> identifier [type] assignment expr #! * decl ..... -> identifier [type] binding expr
#! * block .... -> '{' {stmt} '}' #! * block .... -> '{' {stmt} '}'
#! * arm ...... -> block | stmt (single statements become canonical blocks) #! * 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. #! braceless guards require parentheses unless the condition is a call; else binds to the nearest if.
import "@std/debug" import "@std/debug"
@@ -47,7 +47,11 @@ DeclKind :: enum {
StmtKind :: enum { StmtKind :: enum {
declaration, declaration,
expression, expression,
return_stmt, if_stmt, invalid assignment_stmt,
return_stmt,
if_stmt,
while_stmt,
invalid
} }
Mutability :: enum { Mutability :: enum {
@@ -78,6 +82,8 @@ NodeKind :: enum {
stmt_decl, stmt_decl,
stmt_return, stmt_return,
stmt_if, stmt_if,
stmt_assignment,
stmt_while,
func_decl, func_decl,
param_decl, param_decl,
block, block,
@@ -176,6 +182,18 @@ IfView :: struct {
else_block ?NodeId else_block ?NodeId
} }
AssignmentView :: struct {
span Span
target NodeId
value NodeId
}
WhileView :: struct {
span Span
condition NodeId
body NodeId
}
InvalidView :: struct { InvalidView :: struct {
span Span span Span
diagnostic DiagnosticId diagnostic DiagnosticId
@@ -309,6 +327,32 @@ add_if_stmt proc(
}, span) }, 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( add_statement_decl proc(
builder @mut Builder, builder @mut Builder,
span Span, span Span,
@@ -445,6 +489,8 @@ stmt_kind proc(tree @Tree, id NodeId) StmtKind {
.stmt_expr: .expression .stmt_expr: .expression
.stmt_return: .return_stmt .stmt_return: .return_stmt
.stmt_if: .if_stmt .stmt_if: .if_stmt
.stmt_assignment: .assignment_stmt
.stmt_while: .while_stmt
.invalid_stmt: .invalid .invalid_stmt: .invalid
else: unreachable 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 { statement_decl proc(tree @Tree, id NodeId) StatementDeclView {
node :: get_node(&tree.storage, id) node :: get_node(&tree.storage, id)
debug.assert(node.kind == .stmt_decl) debug.assert(node.kind == .stmt_decl)
+15
View File
@@ -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) 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") .invalid: try append_text(renderer, "invalid statement\n")
} }
} }
+5 -1
View File
@@ -28,11 +28,13 @@ Code :: enum {
missing_return, missing_return,
return_type_mismatch, return_type_mismatch,
condition_not_bool, condition_not_bool,
invalid_assignment_target,
missing_entry, missing_entry,
invalid_entry, invalid_entry,
literal_out_of_range, literal_out_of_range,
invalid_escape, invalid_escape,
read_before_initialization, read_before_initialization,
write_before_initialization,
initialization_cycle, initialization_cycle,
integer_overflow, integer_overflow,
} }
@@ -93,12 +95,14 @@ message proc(code Code) []u8 {
.cannot_infer_type: "cannot infer a type without a type anchor" .cannot_infer_type: "cannot infer a type without a type anchor"
.missing_return: "nonvoid function can finish without returning a value" .missing_return: "nonvoid function can finish without returning a value"
.return_type_mismatch: "return does not match the function result type" .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" .missing_entry: "program requires a main function"
.invalid_entry: "main must be a function with no parameters" .invalid_entry: "main must be a function with no parameters"
.literal_out_of_range: "numeric literal is outside its runtime type's range" .literal_out_of_range: "numeric literal is outside its runtime type's range"
.invalid_escape: "invalid string escape" .invalid_escape: "invalid string escape"
.read_before_initialization: "local value was read before initialization" .read_before_initialization: "local value was read before initialization"
.write_before_initialization: "local value was assigned before initialization"
.initialization_cycle: "global initialization cycle" .initialization_cycle: "global initialization cycle"
.integer_overflow: "integer arithmetic overflow" .integer_overflow: "integer arithmetic overflow"
} }
+12
View File
@@ -113,6 +113,18 @@ run proc(
} }
continue 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: { .load_global: {
global_index :: usize(instruction.slot) global_index :: usize(instruction.slot)
match globals[global_index].status { match globals[global_index].status {
+108
View File
@@ -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
}
+2
View File
@@ -36,6 +36,8 @@ Op :: enum {
constant constant
load_local load_local
store_local store_local
assign_local
reset_local
load_global load_global
negate negate
add add
+64 -5
View File
@@ -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 { emit proc(state @mut State, instruction ir.Instruction) ir.ValueId ! mem.AllocError {
function := &state.module.functions.items[state.function] function := &state.module.functions.items[state.function]
operation := instruction 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) operation.destination = ir.ValueId(function.value_count)
function.value_count += 1 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, ast.function_param(tree, id, i))) return
} }
if (trap_type(state, id)) return if (trap_type(state, id)) return
try lower_block(state, declaration.body) try lower_block(state, declaration.body, false)
if (state.terminated) return if (state.terminated) return
# aggregate arm errors are not demanded by fallthrough; only missing_return is. # aggregate arm errors are not demanded by fallthrough; only missing_return is.
body_type :: typecheck.type_of(state.checked, declaration.body) 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) try predeclare_locals(state, conditional.then_block)
if (conditional.else_block) |alternative| try predeclare_locals(state, alternative) if (conditional.else_block) |alternative| try predeclare_locals(state, alternative)
} }
.while_stmt: try predeclare_locals(state, ast.while_stmt(state.tree, statement).body)
else: {} else: {}
} }
} }
} }
@hide:file @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| { for (0..ast.block_statement_count(state.tree, block)) |i| {
if (state.terminated) return if (state.terminated) return
try lower_statement(state, ast.block_statement(state.tree, block, i)) 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), span = ast.node_span(state.tree, conditional.condition),
} }
state.block = consequent state.block = consequent
try lower_block(state, conditional.then_block) try lower_block(state, conditional.then_block, true)
consequent_end :: state.block consequent_end :: state.block
consequent_falls :: !state.terminated consequent_falls :: !state.terminated
state.block = alternative state.block = alternative
state.terminated = false 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_end :: state.block
alternative_falls :: !state.terminated alternative_falls :: !state.terminated
if (!consequent_falls and !alternative_falls) { 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 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 @hide:file
lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree tree :: state.tree
match ast.stmt_kind(tree, id) { match ast.stmt_kind(tree, id) {
.if_stmt: try lower_if(state, 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: {
invalid :: ast.invalid(tree, id) invalid :: ast.invalid(tree, id)
trap(state, invalid.diagnostic, invalid.span) trap(state, invalid.diagnostic, invalid.span)
+223
View File
@@ -431,6 +431,12 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
`} `}
`main proc() string { `main proc() string {
` computed float :: factor * 2 ` computed float :: factor * 2
` remaining := 2
` while remaining > 0 {
` pass :: identity(text)
` identity(pass)
` remaining = remaining - 1
` }
` if computed > 0 { ` if computed > 0 {
` saved :: identity(text) ` saved :: identity(text)
` if computed < 10 { ` 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) outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try expect_trap(outcome, &diagnostic_store, .read_before_initialization) 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)
}
}
+90 -34
View File
@@ -168,6 +168,9 @@ parse_block proc(state @mut State) ast.NodeId ! ParseError {
@hide:file @hide:file
parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { 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 ( if (
next_is(state, .ident) next_is(state, .ident)
and ( and (
@@ -180,6 +183,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
} }
match current(state).kind { match current(state).kind {
.if: return try parse_if(state) .if: return try parse_if(state)
.while: return try parse_while(state, allow_else)
.return: return try parse_return(state, allow_else) .return: return try parse_return(state, allow_else)
.ident, .int, .float, .string, .true, .false, .minus, .open_paren: { .ident, .int, .float, .string, .true, .false, .minus, .open_paren: {
expression :: try parse_expr(state) 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 @hide:file
parse_if proc(state @mut State) ast.NodeId ! ParseError { parse_if proc(state @mut State) ast.NodeId ! ParseError {
keyword :: try consume(state, .if) 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_start :: state.next_token
condition := parse_expr(state) catch |err| { condition := parse_expr(state) catch |err| {
match 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) yield try ast.add_invalid_expr(&state.builder, current(state).span, diagnostic)
} }
if (ast.builder_expr_kind(&state.builder, condition) == .invalid) { 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 ( while (
!next_is(state, .open_curly) and !next_is(state, .close_curly) !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, .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 state.next_token += 1
} }
} }
@@ -249,29 +319,7 @@ parse_if proc(state @mut State) ast.NodeId ! ParseError {
diagnostic, diagnostic,
) )
} }
braced :: next_is(state, .open_curly) return condition
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,
)
} }
@hide:file @hide:file
@@ -290,10 +338,10 @@ parenthesized_guard proc(state @State, start usize) bool {
} }
@hide:file @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) if next_is(state, .open_curly) return try parse_block(state)
start :: state.next_token start :: state.next_token
statement :: parse_statement(state, true) catch |err| { statement :: parse_statement(state, allow_else) catch |err| {
match err { match err {
.unexpected_token: {} .unexpected_token: {}
else: return err 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 { synchronize_arm proc(state @mut State, start usize) ast.Span {
first :: state.tokens[start].span first :: state.tokens[start].span
end := first.end end := first.end
while ( depth usize := 0
!next_is(state, .eof) and !next_is(state, .newline) while !next_is(state, .eof) {
and !next_is(state, .close_curly) and !next_is(state, .else) 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 end = current(state).span.end
state.next_token += 1 state.next_token += 1
} }
@@ -485,7 +537,7 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
@hide:file @hide:file
is_expression_boundary proc(kind lexer.TokenKind) bool { is_expression_boundary proc(kind lexer.TokenKind) bool {
return match kind { 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 else: false
} }
} }
@@ -514,8 +566,12 @@ recover_statement proc(
synchronize proc(state @mut State, start usize, stop_at_curly bool) ast.Span { synchronize proc(state @mut State, start usize, stop_at_curly bool) ast.Span {
first :: state.tokens[start].span first :: state.tokens[start].span
end := first.end end := first.end
while !next_is(state, .eof) and !next_is(state, .newline) { depth usize := 0
if (stop_at_curly and next_is(state, .close_curly)) break 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 end = current(state).span.end
state.next_token += 1 state.next_token += 1
} }
+229
View File
@@ -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("}", ast.source_text(&tree, arm_error.span))
try testing.expect_equal(3, diagnostics.count(&diagnostic_store)) 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))
}
+142 -27
View File
@@ -27,7 +27,20 @@ Result :: struct { storage Storage }
no_index :: maxval!(usize) no_index :: maxval!(usize)
@hide:file @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 @hide:file
Cell :: struct { Cell :: struct {
@@ -39,6 +52,8 @@ Cell :: struct {
anchored bool = false anchored bool = false
demand TypeKind = .none demand TypeKind = .none
fixed bool = false fixed bool = false
local bool = false
assignment_value bool = false
first_edge usize = no_index first_edge usize = no_index
return_annotation ast.NodeId = ast.NodeId(maxval!(u32)) return_annotation ast.NodeId = ast.NodeId(maxval!(u32))
} }
@@ -57,6 +72,8 @@ State :: struct {
types []mut Type types []mut Type
cells []mut Cell cells []mut Cell
edges arraylist.ArrayList(Edge) edges arraylist.ArrayList(Edge)
collecting_assignment bool = false
has_assignments bool = false
} }
#! borrows the AST, resolution, and diagnostic store only during checking. #! borrows the AST, resolution, and diagnostic store only during checking.
@@ -95,6 +112,19 @@ check proc(
.invalid: collect_invalid(&state, id) .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. # aliases share a type variable; explicit annotations and parameters are boundaries.
for (0..count) |i| { for (0..count) |i| {
@@ -113,16 +143,29 @@ check proc(
} }
if (cells[i].role != .expression or ast.expr_kind(tree, id) != .identifier) continue if (cells[i].role != .expression or ast.expr_kind(tree, id) != .identifier) continue
binding :: sema.binding(resolution, id) 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) join(&state, id, binding.data.declaration)
} }
} }
# function aliases are known before any calls or bodies are checked. Honey's # assignment inputs observe established declaration types, never contribute
# return annotations are scalar, so a call cannot introduce a function type. # 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| { for (0..count) |i| {
id :: ast.NodeId(i) id :: ast.NodeId(i)
if (cells[i].role != .expression or ast.expr_kind(tree, id) != .call) continue 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)]
}
}
# 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 callee :: ast.call_expr(tree, id).callee
signature :: group_type(&state, callee) signature :: group_type(&state, callee)
if (signature.function != ast.NodeId(maxval!(u32))) { if (signature.function != ast.NodeId(maxval!(u32))) {
@@ -134,7 +177,7 @@ check proc(
} }
for (0..count) |i| { for (0..count) |i| {
id :: ast.NodeId(i) id :: ast.NodeId(i)
if (cells[i].role != .expression or ast.expr_kind(tree, id) != .call) continue 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 callee :: ast.call_expr(tree, id).callee
signature :: group_type(&state, callee) signature :: group_type(&state, callee)
if (signature.function != ast.NodeId(maxval!(u32))) continue if (signature.function != ast.NodeId(maxval!(u32))) continue
@@ -143,20 +186,18 @@ check proc(
} else if (signature.kind != .none or cells[root(&state, usize(callee))].numeric) { } 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)))) seed(&state, id, invalid_type(try diagnostics.add(diagnostic_store, .not_callable, ast.node_span(tree, callee))))
} else { } else {
# without a function anchor this dependency cannot become callable; # without a function anchor, share the eventual inference error.
# share its eventual inference error instead of emitting a cascade.
join(&state, id, callee) join(&state, id, callee)
} }
} }
for (0..count) |i| { for (0..count) |i| {
if (cells[i].role == .none or cells[i].role == .body) continue 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 types[i] = cells[root(&state, i)].type
} }
# reject inherently nonnumeric operations before connecting their operands. # reject nonnumeric operations before connecting otherwise valid inputs.
# their otherwise valid inputs must not join a failed constraint component.
for (0..count) |i| { for (0..count) |i| {
id :: ast.NodeId(i) id :: ast.NodeId(i)
if (cells[i].role != .expression) continue if (cells[i].role != .expression or cells[i].assignment_value != assignment_values) continue
match ast.expr_kind(tree, id) { match ast.expr_kind(tree, id) {
.unary: try require_numeric(&state, id, ast.unary_expr(tree, id).operand) .unary: try require_numeric(&state, id, ast.unary_expr(tree, id).operand)
.binary: { .binary: {
@@ -170,11 +211,10 @@ check proc(
} }
propagate(&state, queue) propagate(&state, queue)
# numeric edges are undirected constraints, not a traversal-time conversion. # numeric edges join flexible operands; rigid inputs supply only demands.
# rigid values supply demands without joining the component they constrain.
for (0..count) |i| { for (0..count) |i| {
id :: ast.NodeId(i) id :: ast.NodeId(i)
if (cells[i].role != .expression) continue if (cells[i].role != .expression or cells[i].assignment_value != assignment_values) continue
match ast.expr_kind(tree, id) { match ast.expr_kind(tree, id) {
.unary: try numeric_operand(&state, id, ast.unary_expr(tree, id).operand) .unary: try numeric_operand(&state, id, ast.unary_expr(tree, id).operand)
.binary: { .binary: {
@@ -194,18 +234,24 @@ check proc(
} }
for (0..count) |i| { for (0..count) |i| {
id :: ast.NodeId(i) id :: ast.NodeId(i)
if (cells[i].role == .declaration) { if (!assignment_values and cells[i].role == .declaration) {
declaration :: ast.statement_decl(tree, id) declaration :: ast.statement_decl(tree, id)
if (declaration.type) |annotation| { if (declaration.type) |annotation| {
try demand(&state, declaration.initializer, types[usize(annotation)], .type_mismatch) try demand(&state, declaration.initializer, types[usize(annotation)], .type_mismatch)
} }
} }
if (cells[i].role == .return_stmt) { if (!assignment_values and cells[i].role == .return_stmt) {
if (ast.return_stmt(tree, id).value) |value| { if (ast.return_stmt(tree, id).value) |value| {
try demand(&state, value, types[usize(cells[i].return_annotation)], .return_type_mismatch) try demand(&state, value, types[usize(cells[i].return_annotation)], .return_type_mismatch)
} }
} }
if (cells[i].role != .expression or ast.expr_kind(tree, id) != .call) continue 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) signature :: group_type(&state, ast.call_expr(tree, id).callee)
if (signature.function == ast.NodeId(maxval!(u32))) continue if (signature.function == ast.NodeId(maxval!(u32))) continue
parameter_count :: ast.function_param_count(tree, signature.function) parameter_count :: ast.function_param_count(tree, signature.function)
@@ -216,9 +262,9 @@ check proc(
} }
} }
# default only after the complete program has supplied its numeric demands. # defaults follow ordinary program demands, then isolated assignment demands.
for (0..count) |i| { for (0..count) |i| {
if (root(&state, i) != i) continue if (root(&state, i) != i or cells[i].assignment_value != assignment_values) continue
if (cells[i].type.kind == .invalid) continue if (cells[i].type.kind == .invalid) continue
if (cells[i].numeric and cells[i].anchored) { if (cells[i].numeric and cells[i].anchored) {
kind TypeKind :: if (cells[i].demand == .float) .float else .int kind TypeKind :: if (cells[i].demand == .float) .float else .int
@@ -227,7 +273,7 @@ check proc(
} }
for (0..count) |i| { for (0..count) |i| {
id :: ast.NodeId(i) id :: ast.NodeId(i)
if (cells[i].role != .expression or ast.expr_kind(tree, id) != .call) continue 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 callee :: ast.call_expr(tree, id).callee
signature :: group_type(&state, callee) signature :: group_type(&state, callee)
if (signature.function != ast.NodeId(maxval!(u32)) or signature.kind == .none or signature.kind == .invalid) continue if (signature.function != ast.NodeId(maxval!(u32)) or signature.kind == .none or signature.kind == .invalid) continue
@@ -236,16 +282,17 @@ check proc(
} }
} }
for (0..count) |i| { for (0..count) |i| {
if (cells[i].role != .declaration or types[i].kind == .invalid) continue if (assignment_values or cells[i].role != .declaration or types[i].kind == .invalid) continue
index :: root(&state, i) index :: root(&state, i)
if (cells[index].type.kind != .none) continue if (cells[index].type.kind != .none) continue
diagnostic :: try diagnostics.add(diagnostic_store, .cannot_infer_type, ast.statement_decl(tree, ast.NodeId(i)).name) diagnostic :: try diagnostics.add(diagnostic_store, .cannot_infer_type, ast.statement_decl(tree, ast.NodeId(i)).name)
cells[index].type = invalid_type(diagnostic) cells[index].type = invalid_type(diagnostic)
} }
for (0..count) |i| { for (0..count) |i| {
if (cells[i].role == .none or cells[i].role == .body or types[i].kind == .invalid) continue 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 types[i] = cells[root(&state, i)].type
} }
}
for (0..count) |i| try validate(&state, ast.NodeId(i)) for (0..count) |i| try validate(&state, ast.NodeId(i))
# errors travel only from dependencies to users. A rejected use never poisons # errors travel only from dependencies to users. A rejected use never poisons
@@ -253,8 +300,9 @@ check proc(
propagate(&state, queue) propagate(&state, queue)
# validate conditions after operand errors have reached their users. # validate conditions after operand errors have reached their users.
for (0..count) |i| { for (0..count) |i| {
if (cells[i].role != .if_stmt) continue id :: ast.NodeId(i)
condition :: ast.if_stmt(tree, ast.NodeId(i)).condition 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)] actual :: types[usize(condition)]
if (actual.kind != .invalid and actual.kind != .bool) { if (actual.kind != .invalid and actual.kind != .bool) {
try fail(&state, condition, .condition_not_bool, ast.node_span(tree, condition)) 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| { for (0..ast.block_statement_count(tree, id)) |i| {
statement :: ast.block_statement(tree, id, i) statement :: ast.block_statement(tree, id, i)
match ast.stmt_kind(tree, statement) { 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)) .expression: try collect_expression(state, ast.statement_expr(tree, statement))
.return_stmt: { .return_stmt: {
state.cells[usize(statement)].role = .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) 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) .invalid: collect_invalid(state, statement)
} }
source :: if (ast.stmt_kind(tree, statement) == .expression) ast.statement_expr(tree, statement) else 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 if (state.cells[usize(id)].role != .none) return
tree :: state.tree tree :: state.tree
state.cells[usize(id)].role = .expression state.cells[usize(id)].role = .expression
state.cells[usize(id)].assignment_value = state.collecting_assignment
match ast.expr_kind(tree, id) { match ast.expr_kind(tree, id) {
.literal_int: { .literal_int: {
state.cells[usize(id)].numeric = true 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)) 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 @hide:file
compatible proc(state @mut State, value ast.NodeId, expected Type) void ! (mem.AllocError | diagnostics.CapacityError) { compatible proc(state @mut State, value ast.NodeId, expected Type) void ! (mem.AllocError | diagnostics.CapacityError) {
actual :: state.types[usize(value)] actual :: state.types[usize(value)]
if (actual.kind == .invalid or expected.kind == .invalid) return 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 @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)]) 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: { .return_stmt: {
statement :: ast.return_stmt(tree, id) statement :: ast.return_stmt(tree, id)
expected :: state.types[usize(state.cells[usize(id)].return_annotation)] expected :: state.types[usize(state.cells[usize(id)].return_annotation)]
+281
View File
@@ -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(2, code_count(&diagnostic_store, .missing_return))
try testing.expect_equal(1, code_count(&diagnostic_store, .condition_not_bool)) 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))
}
+10
View File
@@ -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) try resolve_arm(state, scope, conditional.then_block)
if (conditional.else_block) |other| try resolve_arm(state, scope, other) 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) .invalid: resolve_invalid(state, statement)
} }
} }
+73
View File
@@ -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(binding(&resolution, shadow).data.diagnostic, binding(&resolution, use).data.diagnostic)
try testing.expect_equal(4, diagnostics.count(&diagnostic_store)) 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))
}