feat: execute local assignments and while loops
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
main proc() int {
|
||||||
|
product := 1
|
||||||
|
remaining := 5
|
||||||
|
|
||||||
|
while remaining > 1 {
|
||||||
|
product = product * remaining
|
||||||
|
remaining = remaining - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return product
|
||||||
|
}
|
||||||
+70
-4
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
|||||||
+236
-121
@@ -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,138 +143,155 @@ 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..count) |i| {
|
for 0..2 |phase| {
|
||||||
id :: ast.NodeId(i)
|
if (phase == 1 and !state.has_assignments) break
|
||||||
if (cells[i].role != .expression or ast.expr_kind(tree, id) != .call) continue
|
assignment_values :: phase == 1
|
||||||
callee :: ast.call_expr(tree, id).callee
|
if (assignment_values) {
|
||||||
signature :: group_type(&state, callee)
|
for (0..count) |i| {
|
||||||
if (signature.function != ast.NodeId(maxval!(u32))) {
|
id :: ast.NodeId(i)
|
||||||
result :: ast.function_decl(tree, signature.function).return_type
|
if (!cells[i].assignment_value or cells[i].role != .expression or ast.expr_kind(tree, id) != .identifier) continue
|
||||||
seed(&state, id, types[usize(result)])
|
binding :: sema.binding(resolution, id)
|
||||||
} else if (signature.kind == .invalid) {
|
if (binding.kind == .declaration) cells[i].type = types[usize(binding.data.declaration)]
|
||||||
seed(&state, id, signature)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (0..count) |i| {
|
|
||||||
id :: ast.NodeId(i)
|
|
||||||
if (cells[i].role != .expression or ast.expr_kind(tree, id) != .call) continue
|
|
||||||
callee :: ast.call_expr(tree, id).callee
|
|
||||||
signature :: group_type(&state, callee)
|
|
||||||
if (signature.function != ast.NodeId(maxval!(u32))) continue
|
|
||||||
if (signature.kind == .invalid) {
|
|
||||||
seed(&state, id, signature)
|
|
||||||
} else if (signature.kind != .none or cells[root(&state, usize(callee))].numeric) {
|
|
||||||
seed(&state, id, invalid_type(try diagnostics.add(diagnostic_store, .not_callable, ast.node_span(tree, callee))))
|
|
||||||
} else {
|
|
||||||
# without a function anchor this dependency cannot become callable;
|
|
||||||
# share its eventual inference error instead of emitting a cascade.
|
|
||||||
join(&state, id, callee)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (0..count) |i| {
|
|
||||||
if (cells[i].role == .none or cells[i].role == .body) continue
|
|
||||||
types[i] = cells[root(&state, i)].type
|
|
||||||
}
|
|
||||||
# reject inherently nonnumeric operations before connecting their operands.
|
|
||||||
# their otherwise valid inputs must not join a failed constraint component.
|
|
||||||
for (0..count) |i| {
|
|
||||||
id :: ast.NodeId(i)
|
|
||||||
if (cells[i].role != .expression) continue
|
|
||||||
match ast.expr_kind(tree, id) {
|
|
||||||
.unary: try require_numeric(&state, id, ast.unary_expr(tree, id).operand)
|
|
||||||
.binary: {
|
|
||||||
expression :: ast.binary_expr(tree, id)
|
|
||||||
if (bool_equality(&state, id)) continue
|
|
||||||
try require_numeric(&state, id, expression.left)
|
|
||||||
try require_numeric(&state, id, expression.right)
|
|
||||||
}
|
}
|
||||||
else: {}
|
|
||||||
}
|
}
|
||||||
}
|
# function aliases are known before calls; scalar return annotations
|
||||||
propagate(&state, queue)
|
# mean a call cannot introduce a function type.
|
||||||
|
for (0..count) |i| {
|
||||||
|
id :: ast.NodeId(i)
|
||||||
|
if (cells[i].role != .expression or cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .call) continue
|
||||||
|
callee :: ast.call_expr(tree, id).callee
|
||||||
|
signature :: group_type(&state, callee)
|
||||||
|
if (signature.function != ast.NodeId(maxval!(u32))) {
|
||||||
|
result :: ast.function_decl(tree, signature.function).return_type
|
||||||
|
seed(&state, id, types[usize(result)])
|
||||||
|
} else if (signature.kind == .invalid) {
|
||||||
|
seed(&state, id, signature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (0..count) |i| {
|
||||||
|
id :: ast.NodeId(i)
|
||||||
|
if (cells[i].role != .expression or cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .call) continue
|
||||||
|
callee :: ast.call_expr(tree, id).callee
|
||||||
|
signature :: group_type(&state, callee)
|
||||||
|
if (signature.function != ast.NodeId(maxval!(u32))) continue
|
||||||
|
if (signature.kind == .invalid) {
|
||||||
|
seed(&state, id, signature)
|
||||||
|
} else if (signature.kind != .none or cells[root(&state, usize(callee))].numeric) {
|
||||||
|
seed(&state, id, invalid_type(try diagnostics.add(diagnostic_store, .not_callable, ast.node_span(tree, callee))))
|
||||||
|
} else {
|
||||||
|
# without a function anchor, share the eventual inference error.
|
||||||
|
join(&state, id, callee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (0..count) |i| {
|
||||||
|
if (cells[i].role == .none or cells[i].role == .body or cells[i].assignment_value != assignment_values or types[i].kind == .invalid) continue
|
||||||
|
types[i] = cells[root(&state, i)].type
|
||||||
|
}
|
||||||
|
# reject nonnumeric operations before connecting otherwise valid inputs.
|
||||||
|
for (0..count) |i| {
|
||||||
|
id :: ast.NodeId(i)
|
||||||
|
if (cells[i].role != .expression or cells[i].assignment_value != assignment_values) continue
|
||||||
|
match ast.expr_kind(tree, id) {
|
||||||
|
.unary: try require_numeric(&state, id, ast.unary_expr(tree, id).operand)
|
||||||
|
.binary: {
|
||||||
|
expression :: ast.binary_expr(tree, id)
|
||||||
|
if (bool_equality(&state, id)) continue
|
||||||
|
try require_numeric(&state, id, expression.left)
|
||||||
|
try require_numeric(&state, id, expression.right)
|
||||||
|
}
|
||||||
|
else: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
propagate(&state, queue)
|
||||||
|
|
||||||
# numeric edges are undirected constraints, not a traversal-time conversion.
|
# 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 or cells[i].assignment_value != assignment_values) continue
|
||||||
if (cells[i].role != .expression) 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: {
|
expression :: ast.binary_expr(tree, id)
|
||||||
expression :: ast.binary_expr(tree, id)
|
if (comparison(expression.operator)) {
|
||||||
if (comparison(expression.operator)) {
|
if (!bool_equality(&state, id)) try comparison_operands(&state, id)
|
||||||
if (!bool_equality(&state, id)) try comparison_operands(&state, id)
|
} else {
|
||||||
} else {
|
try numeric_operand(&state, id, expression.left)
|
||||||
try numeric_operand(&state, id, expression.left)
|
try numeric_operand(&state, id, expression.right)
|
||||||
try numeric_operand(&state, id, expression.right)
|
if (expression.operator == .divide) {
|
||||||
if (expression.operator == .divide) {
|
try impose(&state, id, .float, .integer_division, expression.span)
|
||||||
try impose(&state, id, .float, .integer_division, expression.span)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
else: {}
|
||||||
else: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (0..count) |i| {
|
|
||||||
id :: ast.NodeId(i)
|
|
||||||
if (cells[i].role == .declaration) {
|
|
||||||
declaration :: ast.statement_decl(tree, id)
|
|
||||||
if (declaration.type) |annotation| {
|
|
||||||
try demand(&state, declaration.initializer, types[usize(annotation)], .type_mismatch)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cells[i].role == .return_stmt) {
|
for (0..count) |i| {
|
||||||
if (ast.return_stmt(tree, id).value) |value| {
|
id :: ast.NodeId(i)
|
||||||
try demand(&state, value, types[usize(cells[i].return_annotation)], .return_type_mismatch)
|
if (!assignment_values and cells[i].role == .declaration) {
|
||||||
|
declaration :: ast.statement_decl(tree, id)
|
||||||
|
if (declaration.type) |annotation| {
|
||||||
|
try demand(&state, declaration.initializer, types[usize(annotation)], .type_mismatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!assignment_values and cells[i].role == .return_stmt) {
|
||||||
|
if (ast.return_stmt(tree, id).value) |value| {
|
||||||
|
try demand(&state, value, types[usize(cells[i].return_annotation)], .return_type_mismatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (assignment_values and cells[i].role == .assignment_stmt) {
|
||||||
|
assignment :: ast.assignment_stmt(tree, id)
|
||||||
|
if (mutable_target(&state, assignment.target)) {
|
||||||
|
try demand(&state, assignment.value, types[usize(assignment.target)], .type_mismatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cells[i].role != .expression or cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .call) continue
|
||||||
|
signature :: group_type(&state, ast.call_expr(tree, id).callee)
|
||||||
|
if (signature.function == ast.NodeId(maxval!(u32))) continue
|
||||||
|
parameter_count :: ast.function_param_count(tree, signature.function)
|
||||||
|
for (0..ast.call_arg_count(tree, id)) |j| {
|
||||||
|
if (j >= parameter_count) continue
|
||||||
|
parameter :: ast.function_param(tree, signature.function, j)
|
||||||
|
try demand(&state, ast.call_arg(tree, id, j), cells[usize(parameter)].type, .type_mismatch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cells[i].role != .expression or ast.expr_kind(tree, id) != .call) continue
|
|
||||||
signature :: group_type(&state, ast.call_expr(tree, id).callee)
|
|
||||||
if (signature.function == ast.NodeId(maxval!(u32))) continue
|
|
||||||
parameter_count :: ast.function_param_count(tree, signature.function)
|
|
||||||
for (0..ast.call_arg_count(tree, id)) |j| {
|
|
||||||
if (j >= parameter_count) continue
|
|
||||||
parameter :: ast.function_param(tree, signature.function, j)
|
|
||||||
try demand(&state, ast.call_arg(tree, id, j), cells[usize(parameter)].type, .type_mismatch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# default only after the complete program has supplied its numeric demands.
|
# 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
|
||||||
cells[i].type = Type{ kind = kind }
|
cells[i].type = Type{ kind = kind }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
for (0..count) |i| {
|
||||||
for (0..count) |i| {
|
id :: ast.NodeId(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
|
||||||
if (cells[i].role != .expression 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
|
if (group_type(&state, id).kind != .invalid) {
|
||||||
if (group_type(&state, id).kind != .invalid) {
|
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))))
|
}
|
||||||
|
}
|
||||||
|
for (0..count) |i| {
|
||||||
|
if (assignment_values or cells[i].role != .declaration or types[i].kind == .invalid) continue
|
||||||
|
index :: root(&state, i)
|
||||||
|
if (cells[index].type.kind != .none) continue
|
||||||
|
diagnostic :: try diagnostics.add(diagnostic_store, .cannot_infer_type, ast.statement_decl(tree, ast.NodeId(i)).name)
|
||||||
|
cells[index].type = invalid_type(diagnostic)
|
||||||
|
}
|
||||||
|
for (0..count) |i| {
|
||||||
|
if (cells[i].role == .none or cells[i].role == .body or cells[i].assignment_value != assignment_values or types[i].kind == .invalid) continue
|
||||||
|
types[i] = cells[root(&state, i)].type
|
||||||
}
|
}
|
||||||
}
|
|
||||||
for (0..count) |i| {
|
|
||||||
if (cells[i].role != .declaration or types[i].kind == .invalid) continue
|
|
||||||
index :: root(&state, i)
|
|
||||||
if (cells[index].type.kind != .none) continue
|
|
||||||
diagnostic :: try diagnostics.add(diagnostic_store, .cannot_infer_type, ast.statement_decl(tree, ast.NodeId(i)).name)
|
|
||||||
cells[index].type = invalid_type(diagnostic)
|
|
||||||
}
|
|
||||||
for (0..count) |i| {
|
|
||||||
if (cells[i].role == .none or cells[i].role == .body or types[i].kind == .invalid) continue
|
|
||||||
types[i] = cells[root(&state, i)].type
|
|
||||||
}
|
}
|
||||||
for (0..count) |i| try validate(&state, ast.NodeId(i))
|
for (0..count) |i| try validate(&state, ast.NodeId(i))
|
||||||
|
|
||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user