feat: execute conditionals through typed IR

Use typed singleton arrays for braceless arm blocks: &[statement] compiled but trapped under the Bro bootstrap.
This commit is contained in:
2026-10-09 23:43:44 +02:00
parent 0821b84bc3
commit 9932750e21
20 changed files with 1511 additions and 124 deletions
+11 -5
View File
@@ -33,11 +33,17 @@ the native bootstrap accepts either a no-argument `main` or `main proc(init proc
source loading uses the supplied `io.Io` for opening, reading, and closing files. `io.open` accepts a borrowed zero-terminated slice (`[;0]u8`), including `init.args.values[index]` without copying its storage. the bootstrap supports checked sentinel-slice construction with `pointer[..length;0]`. `io.read_all(allocator, reader, maximum)` reads to EOF into a caller-owned `ArrayList(u8)`; release it with `arraylist.deinit`. it frees partial storage on error and consumes one extra byte to distinguish an exact-size input from a limit violation. the driver maps `limit_exceeded` to `source_too_large` at its `u32` source-size limit.
a recursive example is in [programs/factorial.hon](programs/factorial.hon); `./build/honey run programs/factorial.hon` prints `120`.
the parser supports arithmetic expressions with nested parenthesized groups, such as `a + (b - c) * 2 / 3`. groups control precedence without adding AST nodes; inner expression spans exclude the grouping parentheses. parsing accepts integer division syntax, but type checking requires float operands for `/`.
function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. calls bind tighter than unary and binary arithmetic and can appear in initializers or as expression statements inside function bodies. argument lists allow a trailing comma; newlines remain statement boundaries. type checking validates callable values, argument counts and argument types.
name resolution (`@source/sema`) runs after parsing. it predeclares package names and function-body locals, allowing forward references and mutually recursive functions. parameters and locals share a function scope; signatures resolve names in the enclosing package scope. shadowing is forbidden, including reuse of builtin type names (`int`, `float`, `string`, `void`); sibling function scopes may independently reuse names.
`true` and `false` are boolean literals; `bool` is available for parameters, storage and results. `==`, `!=`, `<`, `<=`, `>` and `>=` compare compatible numeric operands and produce `bool`; `==` and `!=` also accept boolean operands. comparisons preserve contextual numeric inference without making their operands boolean. arithmetic binds tighter than relational comparisons, which bind tighter than equality; comparisons associate left to right. boolean arithmetic/ordering and string/function/void comparisons are invalid; there is no implicit numeric-to-boolean conversion.
`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.
`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.
@@ -45,10 +51,10 @@ type checking (`@source/sema/check`) runs after name resolution. `check.check` b
numeric typing follows contextual rules within Honey's current `int` and `float` families. integer literals can adopt either family; float literals remain float. numeric demands propagate through arithmetic and inferred bindings before undemanded integers default to `int`. conflicting demands are errors; fixed `int` values do not implicitly convert to `float`. `+`, `-`, `*` and unary `-` require compatible numeric operands; `/` requires and supplies float context. `3 / 2`, `3.0 / 2`, and `ratio float :: 3 / 2` are valid; division involving fixed `int` values requires explicit conversion.
type errors carry source spans and diagnostic identities; invalid operands propagate their existing identity without duplicate errors, while independent expressions and arguments 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; returns must match the declared result type, and nonvoid fallthrough is diagnosed. statements after a return are still 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 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.
`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 return or diagnostic-trap terminators. 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; 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; returned strings borrow the module. invocation frames are heap-backed and independent; 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. 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; returned strings borrow the module. 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.
recoverable source errors do not stop execution of unaffected code. unused invalid functions and globals retain diagnostic traps; reaching invalid code reports its original diagnostic identity. 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 and control-flow/assignment syntax remain unimplemented.
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.
+8
View File
@@ -0,0 +1,8 @@
factorial proc(n int) int {
if (n <= 1) return 1
return n * factorial(n - 1)
}
main proc() int {
return factorial(5)
}
+65 -7
View File
@@ -1,11 +1,14 @@
#! grammar:
#! * type ..... -> identifier
#! * assignment -> :: | :=
#! * literal .. -> int | float | string
#! * op ....... -> '+' | '-' | '*' | '/'
#! * literal .. -> int | float | string | 'true' | 'false'
#! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>='
#! * expr ..... -> identifier | literal | '(' expr ')' | '-' expr | expr op expr | expr '(' [expr {',' expr} [',']] ')'
#! * decl ..... -> identifier [type] assignment expr
#! * stmt ..... -> decl | expr | 'return' [expr]
#! * block .... -> '{' {stmt} '}'
#! * arm ...... -> block | stmt (single statements become canonical blocks)
#! * stmt ..... -> decl | expr | 'return' [expr] | 'if' expr arm ['else' arm]
#! braceless guards require parentheses unless the condition is a call; else binds to the nearest if.
import "@std/debug"
import "@std/mem"
@@ -22,13 +25,14 @@ NodeId :: distinct u32
no_node :: maxval!(NodeId)
LiteralKind :: enum {
int, float, string
int, float, string, bool
}
ExprKind :: enum {
literal_int
literal_float
literal_string
literal_bool
identifier
unary
binary
@@ -41,7 +45,9 @@ DeclKind :: enum {
}
StmtKind :: enum {
declaration, expression, return_stmt, invalid
declaration,
expression,
return_stmt, if_stmt, invalid
}
Mutability :: enum {
@@ -51,7 +57,11 @@ Mutability :: enum {
UnaryOperator :: enum { negate }
BinaryOperator :: enum {
add, subtract, multiply, divide
add, subtract,
multiply, divide
equal, not_equal,
less, less_equal,
greater, greater_equal
}
@hide:file
@@ -59,6 +69,7 @@ NodeKind :: enum {
literal_int,
literal_float,
literal_string,
literal_bool,
expr_identifier,
expr_unary,
expr_binary,
@@ -66,6 +77,7 @@ NodeKind :: enum {
stmt_expr,
stmt_decl,
stmt_return,
stmt_if,
func_decl,
param_decl,
block,
@@ -157,6 +169,13 @@ ReturnView :: struct {
value ?NodeId
}
IfView :: struct {
span Span
condition NodeId
then_block NodeId
else_block ?NodeId
}
InvalidView :: struct {
span Span
diagnostic DiagnosticId
@@ -206,6 +225,7 @@ add_literal proc(builder @mut Builder, kind LiteralKind, span Span) NodeId ! mem
.int: .literal_int
.float: .literal_float
.string: .literal_string
.bool: .literal_bool
}
return try add_node(builder, Node{ kind = node_kind }, span)
}
@@ -273,6 +293,22 @@ add_return_stmt proc(builder @mut Builder, span Span, value ?NodeId) NodeId ! me
}, span)
}
add_if_stmt proc(
builder @mut Builder,
span Span,
condition NodeId,
then_block NodeId,
else_block ?NodeId,
) NodeId ! mem.AllocError {
alternative :: if (else_block) |id| id else no_node
return try add_node(builder, Node{
kind = .stmt_if,
main = NodeData{ node_id = condition },
data0 = NodeData{ node_id = then_block },
data1 = NodeData{ node_id = alternative },
}, span)
}
add_statement_decl proc(
builder @mut Builder,
span Span,
@@ -408,6 +444,7 @@ stmt_kind proc(tree @Tree, id NodeId) StmtKind {
.stmt_decl: .declaration
.stmt_expr: .expression
.stmt_return: .return_stmt
.stmt_if: .if_stmt
.invalid_stmt: .invalid
else: unreachable
}
@@ -428,6 +465,17 @@ return_stmt proc(tree @Tree, id NodeId) ReturnView {
}
}
if_stmt proc(tree @Tree, id NodeId) IfView {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .stmt_if)
return IfView{
span = get_span(&tree.storage, id),
condition = node.main.node_id,
then_block = node.data0.node_id,
else_block = maybe_node_id(u32(node.data1.node_id)),
}
}
statement_decl proc(tree @Tree, id NodeId) StatementDeclView {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .stmt_decl)
@@ -491,10 +539,20 @@ block_statement proc(tree @Tree, id NodeId, index usize) NodeId {
}
expr_kind proc(tree @Tree, id NodeId) ExprKind {
return match get_node(&tree.storage, id).kind {
return expression_kind(get_node(&tree.storage, id).kind)
}
builder_expr_kind proc(builder @Builder, id NodeId) ExprKind {
return expression_kind(get_node(&builder.storage, id).kind)
}
@hide:file
expression_kind proc(kind NodeKind) ExprKind {
return match kind {
.literal_int: .literal_int
.literal_float: .literal_float
.literal_string: .literal_string
.literal_bool: .literal_bool
.expr_identifier: .identifier
.expr_unary: .unary
.expr_binary: .binary
+24 -6
View File
@@ -41,25 +41,43 @@ render_function proc(
try append_text(renderer, " function ")
try append_text(renderer, ast.source_text(tree, function.name))
try append_text(renderer, "\n")
for 0..ast.block_statement_count(tree, function.body) |index| {
statement :: ast.block_statement(tree, function.body, index)
try render_block(renderer, tree, function.body, 2)
}
@hide:file
render_block proc(renderer @mut Renderer, tree @ast.Tree, block ast.NodeId, depth usize) void ! mem.AllocError {
for 0..ast.block_statement_count(tree, block) |index| {
statement :: ast.block_statement(tree, block, index)
for 0..depth |_| try append_text(renderer, " ")
match ast.stmt_kind(tree, statement) {
.declaration: try render_statement(renderer, tree, statement, " ")
.declaration: try render_statement(renderer, tree, statement, "")
.expression: {
expression :: ast.statement_expr(tree, statement)
try append_text(renderer, " expression ")
try append_text(renderer, "expression ")
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, expression)))
try append_text(renderer, "\n")
}
.return_stmt: {
try append_text(renderer, " return")
try append_text(renderer, "return")
if (ast.return_stmt(tree, statement).value) |value| {
try append_text(renderer, " ")
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, value)))
}
try append_text(renderer, "\n")
}
.invalid: try append_text(renderer, " invalid statement\n")
.if_stmt: {
conditional :: ast.if_stmt(tree, statement)
try append_text(renderer, "if ")
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, conditional.condition)))
try append_text(renderer, "\n")
try render_block(renderer, tree, conditional.then_block, depth + 1)
if (conditional.else_block) |alternative| {
for 0..depth |_| try append_text(renderer, " ")
try append_text(renderer, "else\n")
try render_block(renderer, tree, alternative, depth + 1)
}
}
.invalid: try append_text(renderer, "invalid statement\n")
}
}
}
+3 -1
View File
@@ -27,6 +27,7 @@ Code :: enum {
cannot_infer_type,
missing_return,
return_type_mismatch,
condition_not_bool,
missing_entry,
invalid_entry,
literal_out_of_range,
@@ -85,13 +86,14 @@ message proc(code Code) []u8 {
.type_not_value: "type name cannot be used as a value"
.invalid_void_type: "void cannot be stored or used as a parameter type"
.type_mismatch: "incompatible value type"
.invalid_operand: "arithmetic requires compatible numeric operands"
.invalid_operand: "operator requires compatible operand types"
.integer_division: "division requires float operands"
.not_callable: "value is not callable"
.argument_count_mismatch: "argument count does not match the function signature"
.cannot_infer_type: "cannot infer a type without a type anchor"
.missing_return: "nonvoid function can finish without returning a value"
.return_type_mismatch: "return does not match the function result type"
.condition_not_bool: "if condition must have type bool"
.missing_entry: "program requires a main function"
.invalid_entry: "main must be a function with no parameters"
.literal_out_of_range: "numeric literal is outside its runtime type's range"
+45 -1
View File
@@ -26,6 +26,7 @@ GlobalState :: struct {
@hide:file
Frame :: struct {
function ir.FunctionId
block ir.BlockId
next_instruction usize = 0
destination ir.ValueId = ir.no_value
global ?usize = null
@@ -60,11 +61,21 @@ run proc(
while frames.items.len != 0 {
frame_index :: frames.items.len - 1
function :: &module.functions.items[usize(frames.items[frame_index].function)]
block :: &function.blocks.items[usize(function.entry)]
block :: &function.blocks.items[usize(frames.items[frame_index].block)]
instruction_index :: frames.items[frame_index].next_instruction
if instruction_index == block.instructions.items.len {
terminator :: block.terminator
if (terminator.kind == .trap) return Outcome{ trap = terminator.diagnostic }
if (terminator.kind == .jump or terminator.kind == .branch) {
target := terminator.target
if (terminator.kind == .branch) {
condition :: frames.items[frame_index].values[usize(terminator.value)]
if (!condition.data.boolean) target = terminator.alternative
}
frames.items[frame_index].block = target
frames.items[frame_index].next_instruction = 0
continue
}
value := ir.Value{}
if terminator.value != ir.no_value {
value = frames.items[frame_index].values[usize(terminator.value)]
@@ -169,6 +180,38 @@ run proc(
}
}
}
.equal, .not_equal, .less, .less_equal, .greater, .greater_equal: {
left :: frames.items[frame_index].values[usize(instruction.left)]
right :: frames.items[frame_index].values[usize(instruction.right)]
result := false
match left.type {
.int: result = match instruction.op {
.equal: left.data.integer == right.data.integer
.not_equal: left.data.integer != right.data.integer
.less: left.data.integer < right.data.integer
.less_equal: left.data.integer <= right.data.integer
.greater: left.data.integer > right.data.integer
.greater_equal: left.data.integer >= right.data.integer
else: unreachable
}
.float: result = match instruction.op {
.equal: left.data.real == right.data.real
.not_equal: left.data.real != right.data.real
.less: left.data.real < right.data.real
.less_equal: left.data.real <= right.data.real
.greater: left.data.real > right.data.real
.greater_equal: left.data.real >= right.data.real
else: unreachable
}
.bool: result = match instruction.op {
.equal: left.data.boolean == right.data.boolean
.not_equal: left.data.boolean != right.data.boolean
else: unreachable
}
else: unreachable
}
value = ir.Value{ type = .bool, data = ir.ValueData{ boolean = result } }
}
}
if instruction.destination != ir.no_value {
frames.items[frame_index].values[usize(instruction.destination)] = value
@@ -195,6 +238,7 @@ push_frame proc(
# typed IR assigns each temporary before use; only local slots need initialization flags.
try arraylist.append(frames, Frame{
function = function,
block = signature.entry,
destination = destination,
global = global,
locals = locals,
+195 -7
View File
@@ -391,8 +391,10 @@ recursive_invocations_use_heap_frames_and_free_on_allocation_failure test {
defer diagnostics.deinit(&store)
main :: try add_function(&module, mem.empty(ir.Type), 0, .void, 2)
module.entry = main
try emit(&module, main, function_value(0, main))
try emit(&module, main, ir.Instruction{ op = .call, type = .void, destination = ir.ValueId(1), left = ir.ValueId(0) })
recursive_block :: try add_block(&module, main)
module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = recursive_block }
try emit_at(&module, main, recursive_block, function_value(0, main))
try emit_at(&module, main, recursive_block, ir.Instruction{ op = .call, type = .void, destination = ir.ValueId(1), left = ir.ValueId(0) })
state := AllocationState{ remaining = 2048 }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
_ = run(allocator, &module, &store) catch |error| {
@@ -414,13 +416,17 @@ frames_and_globals_are_freed_at_every_allocation_boundary_and_trap test {
defer diagnostics.deinit(&store)
types [1]ir.Type :: [.int]
initializer :: try add_function(&module, types[..], 0, .int, 1)
try emit(&module, initializer, integer(0, 42))
return_value(&module, initializer, ir.ValueId(0))
init_block :: try add_block(&module, initializer)
module.functions.items[usize(initializer)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = init_block }
try emit_at(&module, initializer, init_block, integer(0, 42))
module.functions.items[usize(initializer)].blocks.items[usize(init_block)].terminator = ir.Terminator{ value = ir.ValueId(0) }
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = initializer, span = diagnostics.Span{ start = 0, end = 0 } })
main :: try add_function(&module, types[..], 0, .int, 1)
module.entry = main
try emit(&module, main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 })
return_value(&module, main, ir.ValueId(0))
work :: try add_block(&module, main)
module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = work }
try emit_at(&module, main, work, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 })
module.functions.items[usize(main)].blocks.items[usize(work)].terminator = ir.Terminator{ value = ir.ValueId(0) }
succeeded := false
failures usize := 0
for (usize(0)..usize(16)) |limit| {
@@ -443,7 +449,7 @@ frames_and_globals_are_freed_at_every_allocation_boundary_and_trap test {
try testing.expect(succeeded)
try testing.expect(failures > 0)
trap_id :: try diagnostics.add(&store, .invalid_escape, diagnostics.Span{ start = 0, end = 0 })
module.functions.items[usize(initializer)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
module.functions.items[usize(initializer)].blocks.items[usize(init_block)].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
state := AllocationState{ remaining = 64 }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
trapped :: try run(allocator, &module, &store)
@@ -477,3 +483,185 @@ runtime_diagnostic_allocation_failure_is_infrastructure_error_and_frees_frames t
}
return .expectation_failed
}
@hide:file
add_block proc(module @mut ir.Module, function ir.FunctionId) ir.BlockId ! mem.AllocError {
body := &module.functions.items[usize(function)]
id :: ir.BlockId(body.blocks.items.len)
try arraylist.append(&body.blocks, ir.init_block(module.allocator))
return id
}
@hide:file
emit_at proc(module @mut ir.Module, function ir.FunctionId, block ir.BlockId, instruction ir.Instruction) void ! mem.AllocError {
try arraylist.append(&module.functions.items[usize(function)].blocks.items[usize(block)].instructions, instruction)
}
branching_recursive_calls_and_lazy_initializers_resume_the_current_block 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]
count :: try add_function(&module, types[..], 1, .int, 9)
done :: try add_block(&module, count)
recurse :: try add_block(&module, count)
try emit(&module, count, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 })
try emit(&module, count, integer(1, 0))
try emit(&module, count, ir.Instruction{ op = .less_equal, type = .bool, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
module.functions.items[usize(count)].blocks.items[0].terminator = ir.Terminator{
kind = .branch, value = ir.ValueId(2), target = done, alternative = recurse,
}
module.functions.items[usize(count)].blocks.items[usize(done)].terminator = ir.Terminator{ value = ir.ValueId(1) }
try emit_at(&module, count, recurse, function_value(3, count))
try emit_at(&module, count, recurse, integer(4, 1))
try emit_at(&module, count, recurse, ir.Instruction{ op = .subtract, type = .int, destination = ir.ValueId(5), left = ir.ValueId(0), right = ir.ValueId(4) })
try arraylist.append(&module.functions.items[usize(count)].blocks.items[usize(recurse)].arguments, ir.ValueId(5))
try emit_at(&module, count, recurse, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(6), left = ir.ValueId(3), arguments_count = 1 })
try emit_at(&module, count, recurse, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(7), left = ir.ValueId(6), right = ir.ValueId(4) })
module.functions.items[usize(count)].blocks.items[usize(recurse)].terminator = ir.Terminator{ value = ir.ValueId(7) }
initializer :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3)
init_entry :: try add_block(&module, initializer)
init_exit :: try add_block(&module, initializer)
trap_id :: try diagnostics.add(&store, .invalid_escape, diagnostics.Span{ start = 0, end = 0 })
module.functions.items[usize(initializer)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
module.functions.items[usize(initializer)].entry = init_entry
try emit_at(&module, initializer, init_entry, function_value(0, count))
try emit_at(&module, initializer, init_entry, integer(1, 128))
try arraylist.append(&module.functions.items[usize(initializer)].blocks.items[usize(init_entry)].arguments, ir.ValueId(1))
try emit_at(&module, initializer, init_entry, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), arguments_count = 1 })
module.functions.items[usize(initializer)].blocks.items[usize(init_entry)].terminator = ir.Terminator{ kind = .jump, target = init_exit }
module.functions.items[usize(initializer)].blocks.items[usize(init_exit)].terminator = ir.Terminator{ value = ir.ValueId(2) }
try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = initializer, span = diagnostics.Span{ start = 0, end = 0 } })
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 5)
main_entry :: try add_block(&module, main)
main_exit :: try add_block(&module, main)
module.entry = main
module.functions.items[usize(main)].entry = main_entry
module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
try emit_at(&module, main, main_entry, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 })
try emit_at(&module, main, main_entry, function_value(1, count))
try emit_at(&module, main, main_entry, integer(2, 2))
try arraylist.append(&module.functions.items[usize(main)].blocks.items[usize(main_entry)].arguments, ir.ValueId(2))
try emit_at(&module, main, main_entry, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(3), left = ir.ValueId(1), arguments_count = 1 })
module.functions.items[usize(main)].blocks.items[usize(main_entry)].terminator = ir.Terminator{ kind = .jump, target = main_exit }
try emit_at(&module, main, main_exit, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(4), left = ir.ValueId(0), right = ir.ValueId(3) })
module.functions.items[usize(main)].blocks.items[usize(main_exit)].terminator = ir.Terminator{ value = ir.ValueId(4) }
state := AllocationState{ remaining = 2048 }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
outcome :: try run(allocator, &module, &store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(130), outcome.value.data.integer)
try testing.expect_equal(usize(0), state.live)
try testing.expect_equal(usize(1), diagnostics.count(&store))
# trapping after returning through many heap frames also releases every live frame.
module.functions.items[usize(main)].blocks.items[usize(main_exit)].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
trapped :: try run(allocator, &module, &store)
try testing.expect_equal(trap_id, trapped.trap?)
try testing.expect_equal(usize(0), state.live)
}
all_numeric_comparisons_dispatch_on_operand_type_not_bool_result_type test {
operations [6]ir.Op :: [.equal, .not_equal, .less, .less_equal, .greater, .greater_equal]
lefts [3]i64 :: [-2, 2, 3]
rights [3]i64 :: [3, 2, -2]
operand_types [2]ir.Type :: [.int, .float]
for (operand_types) |type| {
for (lefts) |left, pair| {
right :: rights[pair]
for (operations) |op| {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
main :: try add_function(&module, mem.empty(ir.Type), 0, .bool, 3)
module.entry = main
left_value := ir.Value{ type = type, data = ir.ValueData{ integer = left } }
right_value := ir.Value{ type = type, data = ir.ValueData{ integer = right } }
if (type == .float) {
left_value.data = ir.ValueData{ real = f64(left) }
right_value.data = ir.ValueData{ real = f64(right) }
}
try emit(&module, main, ir.Instruction{ op = .constant, type = type, destination = ir.ValueId(0), constant = left_value })
try emit(&module, main, ir.Instruction{ op = .constant, type = type, destination = ir.ValueId(1), constant = right_value })
try emit(&module, main, ir.Instruction{ op = op, type = .bool, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
return_value(&module, main, ir.ValueId(2))
expected :: match op {
.equal: left == right
.not_equal: left != right
.less: left < right
.less_equal: left <= right
.greater: left > right
.greater_equal: left >= right
else: unreachable
}
outcome :: try run(mem.c_allocator, &module, &store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.bool, outcome.value.type)
try testing.expect_equal(expected, outcome.value.data.boolean)
}
}
}
}
nan_comparisons_obey_ieee_rules_in_either_operand_position test {
operations [6]ir.Op :: [.equal, .not_equal, .less, .less_equal, .greater, .greater_equal]
for (operations) |op| {
for (usize(0)..usize(3)) |position| {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
main :: try add_function(&module, mem.empty(ir.Type), 0, .bool, 3)
module.entry = main
try emit(&module, main, ir.Instruction{
op = .constant, type = .float, destination = ir.ValueId(0),
constant = ir.Value{ type = .float, data = ir.ValueData{ real = 0 } },
})
try emit(&module, main, ir.Instruction{ op = .divide, type = .float, destination = ir.ValueId(1), left = ir.ValueId(0), right = ir.ValueId(0) })
left ir.ValueId :: if (position == 1) ir.ValueId(0) else ir.ValueId(1)
right ir.ValueId :: if (position == 0) ir.ValueId(0) else ir.ValueId(1)
try emit(&module, main, ir.Instruction{ op = op, type = .bool, destination = ir.ValueId(2), left = left, right = right })
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.bool, outcome.value.type)
try testing.expect_equal(op == .not_equal, outcome.value.data.boolean)
}
}
}
boolean_equality_and_branching_use_boolean_payloads test {
booleans [2]bool :: [false, true]
operations [2]ir.Op :: [.equal, .not_equal]
for (booleans) |left| {
for (booleans) |right| {
for (operations) |op| {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 5)
module.entry = main
yes :: try add_block(&module, main)
no :: try add_block(&module, main)
try emit(&module, main, ir.Instruction{ op = .constant, type = .bool, destination = ir.ValueId(0), constant = ir.Value{ type = .bool, data = ir.ValueData{ boolean = left } } })
try emit(&module, main, ir.Instruction{ op = .constant, type = .bool, destination = ir.ValueId(1), constant = ir.Value{ type = .bool, data = ir.ValueData{ boolean = right } } })
try emit(&module, main, ir.Instruction{ op = op, type = .bool, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) })
module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .branch, value = ir.ValueId(2), target = yes, alternative = no }
try emit_at(&module, main, yes, integer(3, 42))
module.functions.items[usize(main)].blocks.items[usize(yes)].terminator = ir.Terminator{ value = ir.ValueId(3) }
try emit_at(&module, main, no, integer(4, 7))
module.functions.items[usize(main)].blocks.items[usize(no)].terminator = ir.Terminator{ value = ir.ValueId(4) }
equal :: left == right
expected_true :: if (op == .equal) equal else !equal
expected i64 :: if (expected_true) 42 else 7
outcome :: try run(mem.c_allocator, &module, &store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(expected, outcome.value.data.integer)
}
}
}
}
+26 -2
View File
@@ -9,10 +9,20 @@ ValueId :: distinct u32
BlockId :: distinct u32
no_value :: maxval!(ValueId)
Type :: enum { invalid, int, float, string, void, function }
Type :: enum {
invalid,
int,
float,
bool,
string,
void,
function,
}
ValueData :: union {
integer i64
real f64
boolean bool
string []u8
function FunctionId
}
@@ -32,6 +42,12 @@ Op :: enum {
multiply
divide
call
equal
not_equal
less
less_equal
greater
greater_equal
}
Instruction :: struct {
@@ -47,10 +63,18 @@ Instruction :: struct {
arguments_count usize = 0
}
TerminatorKind :: enum { return_value, trap }
TerminatorKind :: enum {
return_value,
trap,
jump,
branch,
}
Terminator :: struct {
kind TerminatorKind = .return_value
value ValueId = no_value
target BlockId = BlockId(0)
alternative BlockId = BlockId(0)
diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 }
}
+22
View File
@@ -26,6 +26,8 @@ keywords strmap.StaticStringMap(TokenKind) :: strmap.init([
{ "for", .for },
{ "else", .else },
{ "while", .while },
{ "true", .true },
{ "false", .false },
])
#! owns token storage and retains its allocator; release with deinit.
@@ -203,8 +205,28 @@ scan proc(
continue
}
if (cursor + 1 < program.len and program[cursor + 1] == '=') {
comparison ?TokenKind :: match char {
'=': .equal_equal
'!': .not_equal
'<': .less_equal
'>': .greater_equal
else: null
}
if (comparison) |value| {
try add_token(state, Token{
span = make_span(cursor, cursor + 2),
kind = value,
})
cursor += 2
continue
}
}
kind ?TokenKind :: match char {
'=': .equal
'<': .less
'>': .greater
',': .comma
'(': .open_paren
')': .close_paren
+23
View File
@@ -30,3 +30,26 @@ reports_invalid_input_and_keeps_scanning test {
try testing.expect_equal(u32(3), state.tokens.items[1].span.start)
try testing.expect_equal(u32(7), state.tokens.items[1].span.end)
}
reserves_boolean_keywords_and_scans_comparisons test {
strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
state := init(mem.c_allocator)
defer deinit(&state)
source :: "true false bool trueish == != < <= > >="
try scan(&state, source, &diagnostic_store)
expected [11]TokenKind :: [
TokenKind.true, .false, .ident, .ident,
.equal_equal, .not_equal, .less, .less_equal, .greater, .greater_equal, .eof,
]
try testing.expect_equal(expected.len, state.tokens.items.len)
for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind)
try testing.expect_equal(u32(0), state.tokens.items[0].span.start)
try testing.expect_equal(u32(4), state.tokens.items[0].span.end)
try testing.expect_equal(u32(5), state.tokens.items[1].span.start)
try testing.expect_equal(u32(10), state.tokens.items[1].span.end)
try testing.expect_equal("bool", strpool.get_str(&strpool.strings, state.tokens.items[2].data.str_id)?)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
+1
View File
@@ -21,6 +21,7 @@ TokenKind :: enum {
if, else
for, while
proc, return
true, false
# literals
ident, string
+112 -13
View File
@@ -25,6 +25,7 @@ State :: struct {
module @mut ir.Module
storage []mut Storage
function usize = 0
block ir.BlockId = ir.BlockId(0)
terminated bool = false
}
@@ -89,6 +90,7 @@ lower_type proc(type typecheck.Type) ir.Type {
return match type.kind {
.int: .int
.float: .float
.bool: .bool
.string: .string
.void: .void
.function: .function
@@ -136,7 +138,7 @@ find_entry proc(state @mut State) void ! (mem.AllocError | diagnostics.CapacityE
@hide:file
trap proc(state @mut State, diagnostic diagnostics.DiagnosticId, span diagnostics.Span) void {
state.module.functions.items[state.function].blocks.items[0].terminator = ir.Terminator{
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .trap,
diagnostic = diagnostic,
span = span,
@@ -162,7 +164,7 @@ trap_binding proc(state @mut State, id ast.NodeId) bool {
@hide:file
finish proc(state @mut State, value ir.ValueId, span diagnostics.Span) void {
state.module.functions.items[state.function].blocks.items[0].terminator = ir.Terminator{
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
value = value,
span = span,
}
@@ -177,7 +179,7 @@ emit proc(state @mut State, instruction ir.Instruction) ir.ValueId ! mem.AllocEr
operation.destination = ir.ValueId(function.value_count)
function.value_count += 1
}
try arraylist.append(&function.blocks.items[0].instructions, operation)
try arraylist.append(&function.blocks.items[usize(state.block)].instructions, operation)
return operation.destination
}
@@ -190,6 +192,7 @@ constant proc(state @mut State, value ir.Value, span diagnostics.Span) ir.ValueI
lower_global proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
global :: state.module.globals.items[usize(state.storage[usize(id)].index)]
state.function = usize(global.initializer)
state.block = ir.BlockId(0)
state.terminated = false
state.module.functions.items[state.function].result = global.type
if (trap_binding(state, id)) return
@@ -205,6 +208,7 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di
tree :: state.tree
declaration :: ast.function_decl(tree, id)
state.function = usize(state.storage[usize(id)].index)
state.block = ir.BlockId(0)
state.terminated = false
function := &state.module.functions.items[state.function]
function.result = lower_type(typecheck.type_of(state.checked, declaration.return_type))
@@ -215,30 +219,112 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di
try arraylist.append(&function.parameters, type)
try arraylist.append(&function.locals, type)
}
for (0..ast.block_statement_count(tree, declaration.body)) |i| {
statement :: ast.block_statement(tree, declaration.body, i)
if (ast.stmt_kind(tree, statement) != .declaration) continue
state.storage[usize(statement)] = Storage{ kind = .local, index = u32(function.locals.items.len) }
try arraylist.append(&function.locals, lower_type(typecheck.type_of(state.checked, statement)))
}
try predeclare_locals(state, declaration.body)
# signature errors are entry traps; body errors are reached in statement order.
if (trap_binding(state, id) or trap_type(state, declaration.return_type)) return
for (0..ast.function_param_count(tree, id)) |i| {
if (trap_type(state, ast.function_param(tree, id, i))) return
}
if (trap_type(state, id)) return
for (0..ast.block_statement_count(tree, declaration.body)) |i| {
try lower_block(state, declaration.body)
if (state.terminated) return
try lower_statement(state, ast.block_statement(tree, declaration.body, i))
# aggregate arm errors are not demanded by fallthrough; only missing_return is.
body_type :: typecheck.type_of(state.checked, declaration.body)
if (function.result != .void and body_type.kind == .invalid) {
if (diagnostics.get(state.diagnostics, body_type.diagnostic)) |diagnostic| {
if (diagnostic.code == .missing_return) {
trap(state, body_type.diagnostic, ast.node_span(tree, declaration.body))
return
}
}
}
if (state.terminated or trap_type(state, declaration.body)) return
finish(state, ir.no_value, ast.node_span(tree, declaration.body))
}
@hide:file
predeclare_locals proc(state @mut State, block ast.NodeId) void ! mem.AllocError {
for (0..ast.block_statement_count(state.tree, block)) |i| {
statement :: ast.block_statement(state.tree, block, i)
match ast.stmt_kind(state.tree, statement) {
.declaration: {
function := &state.module.functions.items[state.function]
state.storage[usize(statement)] = Storage{ kind = .local, index = u32(function.locals.items.len) }
try arraylist.append(&function.locals, lower_type(typecheck.type_of(state.checked, statement)))
}
.if_stmt: {
conditional :: ast.if_stmt(state.tree, statement)
try predeclare_locals(state, conditional.then_block)
if (conditional.else_block) |alternative| try predeclare_locals(state, alternative)
}
else: {}
}
}
}
@hide:file
lower_block proc(state @mut State, block ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
for (0..ast.block_statement_count(state.tree, block)) |i| {
if (state.terminated) return
try lower_statement(state, ast.block_statement(state.tree, block, i))
}
}
@hide:file
add_block proc(state @mut State) ir.BlockId ! mem.AllocError {
function := &state.module.functions.items[state.function]
id :: ir.BlockId(function.blocks.items.len)
try arraylist.append(&function.blocks, ir.init_block(state.module.allocator))
return id
}
@hide:file
lower_if proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
conditional :: ast.if_stmt(state.tree, id)
condition :: try lower_expression(state, conditional.condition)
if (state.terminated) return
origin :: state.block
consequent :: try add_block(state)
alternative :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(origin)].terminator = ir.Terminator{
kind = .branch,
value = condition,
target = consequent,
alternative = alternative,
span = ast.node_span(state.tree, conditional.condition),
}
state.block = consequent
try lower_block(state, conditional.then_block)
consequent_end :: state.block
consequent_falls :: !state.terminated
state.block = alternative
state.terminated = false
if (conditional.else_block) |block| try lower_block(state, block)
alternative_end :: state.block
alternative_falls :: !state.terminated
if (!consequent_falls and !alternative_falls) {
state.terminated = true
return
}
join :: try add_block(state)
if (consequent_falls) {
state.module.functions.items[state.function].blocks.items[usize(consequent_end)].terminator = ir.Terminator{
kind = .jump, target = join, span = conditional.span,
}
}
if (alternative_falls) {
state.module.functions.items[state.function].blocks.items[usize(alternative_end)].terminator = ir.Terminator{
kind = .jump, target = join, span = conditional.span,
}
}
state.block = join
state.terminated = false
}
@hide:file
lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
match ast.stmt_kind(tree, id) {
.if_stmt: try lower_if(state, id)
.invalid: {
invalid :: ast.invalid(tree, id)
trap(state, invalid.diagnostic, invalid.span)
@@ -282,6 +368,13 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
if (trap_type(state, id)) return ir.no_value
return try lower_number(state, id)
}
.literal_bool: {
if (trap_type(state, id)) return ir.no_value
return try constant(state, ir.Value{
type = .bool,
data = ir.ValueData{ boolean = mem.eql(ast.source_text(tree, span), "true") },
}, span)
}
.literal_string: {
if (trap_type(state, id)) return ir.no_value
return try lower_string(state, id)
@@ -325,6 +418,12 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
.subtract: .subtract
.multiply: .multiply
.divide: .divide
.equal: .equal
.not_equal: .not_equal
.less: .less
.less_equal: .less_equal
.greater: .greater
.greater_equal: .greater_equal
}
return try emit(state, ir.Instruction{ op = op, type = type, left = left, right = right, span = span })
}
@@ -332,7 +431,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
callee :: try lower_expression(state, ast.call_expr(tree, id).callee)
if (state.terminated) return ir.no_value
# reserve this call's packed slice before nested calls append theirs.
block := &state.module.functions.items[state.function].blocks.items[0]
block := &state.module.functions.items[state.function].blocks.items[usize(state.block)]
start :: block.arguments.items.len
count :: ast.call_arg_count(tree, id)
for (0..count) |_| try arraylist.append(&block.arguments, ir.no_value)
+191
View File
@@ -410,11 +410,23 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
`text :: "owned\nstring"
`factor float :: 3.5
`identity proc(value string) string {
` if false {
` return value
` }
` local :: value
` return local
`}
`main proc() string {
` computed float :: factor * 2
` if computed > 0 {
` saved :: identity(text)
` if computed < 10 {
` return saved
` }
` } else {
` fallback :: "fallback"
` identity(fallback)
` }
` return identity(text)
`}
scan_state := lexer.init(mem.c_allocator)
@@ -450,3 +462,182 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
try testing.expect(succeeded)
try testing.expect(failures > 0)
}
recursive_factorial_and_conditional_returns_execute_after_frontend_cleanup test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`factorial proc(n int) int {
` if n <= 1 {
` return 1
` } else {
` return n * factorial(n - 1)
` }
`}
`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(i64(120), outcome.value.data.integer)
}
nested_branches_joins_calls_and_lazy_globals_preserve_execution_position test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`seed :: classify(2)
`enabled bool :: true
`identity proc(value int) int { return value }
`classify proc(n int) int {
` if n < 0 {
` return 3
` } else if n == 0 {
` return 5
` } else {
` if n > 1 {
` local :: identity(7)
` identity(local)
` } else {
` local :: identity(9)
` identity(local)
` }
` return identity(11)
` }
`}
`choose proc(flag bool) int {
` if flag {
` local :: identity(seed)
` identity(local)
` } else {
` local :: identity(13)
` identity(local)
` }
` return identity(seed) + classify(0) + classify(-1)
`}
`main proc() int {
` return choose(enabled) + choose(false) + classify(1) - 7
`}
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
}
bool_storage_results_and_numeric_comparisons_execute test {
sources [16][]u8 :: [
"main proc() bool { return 2 == 2 }\n",
"main proc() bool { return 2 != 3 }\n",
"main proc() bool { return -2 < 1 }\n",
"main proc() bool { return 2 <= 2 }\n",
"main proc() bool { return 3 > 2 }\n",
"main proc() bool { return 2 >= 2 }\n",
"main proc() bool { return 2 == 2.0 }\n",
"main proc() bool { return 2 != 3.0 }\n",
"main proc() bool { return 2 < 2.5 }\n",
"main proc() bool { return 2 <= 2.0 }\n",
"main proc() bool { return 3 > 2.5 }\n",
"main proc() bool { return 2 >= 2.0 }\n",
"main proc() bool { return true == true }\n",
"main proc() bool { return false != true }\n",
"flag bool :: false\nidentity proc(value bool) bool { return value }\nmain proc() bool {\n local bool :: identity(flag)\n return identity(local == false)\n}\n",
"main proc() bool {\n local :: 2\n return local < 2.5\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.bool, outcome.value.type)
try testing.expect(outcome.value.data.boolean)
}
}
conditional_errors_trap_only_when_demanded_and_preserve_original_identity test {
sources [7][]u8 :: [
"main proc() int {\n if true {\n return 42\n } else {\n return unknown\n }\n}\n",
"main proc() int {\n if false {\n return 42\n } else {\n return unknown\n }\n}\n",
"main proc() int {\n if true {\n return 42\n }\n}\n",
"main proc() int {\n if false {\n return 42\n }\n}\n",
"main proc() void {\n if false {\n unknown\n }\n}\n",
"main proc() int {\n if 1 {\n return 42\n } else {\n return 0\n }\n}\n",
"main proc() int {\n if unknown {\n return 42\n }\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)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
if (index == 0 or index == 2 or index == 4) {
try testing.expect(outcome.trap == null)
if (index == 4) try testing.expect_equal(ir.Type.void, outcome.value.type)
else try testing.expect_equal(i64(42), outcome.value.data.integer)
} else {
try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?)
code := diagnostics.Code.unknown_name
if (index == 3) code = .missing_return
if (index == 5) code = .condition_not_bool
try testing.expect_equal(code, original.code)
if (index == 5) try testing.expect(mem.eql(source[usize(original.span.start)..usize(original.span.end)], "1"))
}
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
}
}
missing_return_does_not_replace_reached_arm_error test {
sources [3][]u8 :: [
"main proc() int {\n if true {\n return unknown\n }\n}\n",
"main proc() int {\n if false {\n return unknown\n }\n}\n",
"main proc() int {\n if false {\n return unknown\n }\n return 42\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)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
if (index == 2) {
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
} else {
code diagnostics.Code :: if (index == 0) .unknown_name else .missing_return
try expect_trap(outcome, &diagnostic_store, code)
for (0..diagnostics.count(&diagnostic_store)) |i| {
diagnostic :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(i))?
if (diagnostic.code == code) try testing.expect_equal(diagnostics.DiagnosticId(i), outcome.trap?)
}
}
}
}
nested_locals_are_predeclared_before_any_arm_executes test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`main proc() int {
` if true {
` if true {
` before :: later
` later :: 42
` return before
` }
` }
` return 0
`}
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, .read_before_initialization)
}
+1
View File
@@ -144,6 +144,7 @@ print_result proc(output io.Writer, value ir.Value) void ! io.WriteError {
match value.type {
.int: try io.print("{}\n", output, {value.data.integer})
.float: try io.print("{d}\n", output, {value.data.real})
.bool: try io.print("{}\n", output, {value.data.boolean})
.string: try io.print("{s}\n", output, {value.data.string})
.void: {}
else: unreachable
+147 -10
View File
@@ -143,7 +143,7 @@ parse_block proc(state @mut State) ast.NodeId ! ParseError {
skip_newlines(state)
while !next_is(state, .close_curly) and !next_is(state, .eof) {
start :: state.next_token
statement :: parse_statement(state) catch |err| {
statement :: parse_statement(state, false) catch |err| {
match err {
.unexpected_token: {
recovered :: try recover_statement(state, start)
@@ -167,7 +167,7 @@ parse_block proc(state @mut State) ast.NodeId ! ParseError {
}
@hide:file
parse_statement proc(state @mut State) ast.NodeId ! ParseError {
parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
if (
next_is(state, .ident)
and (
@@ -179,8 +179,9 @@ parse_statement proc(state @mut State) ast.NodeId ! ParseError {
return try parse_statement_decl(state)
}
match current(state).kind {
.return: return try parse_return(state)
.ident, .int, .float, .string, .minus, .open_paren: {
.if: return try parse_if(state)
.return: return try parse_return(state, allow_else)
.ident, .int, .float, .string, .true, .false, .minus, .open_paren: {
expression :: try parse_expr(state)
return try ast.add_expression_stmt(&state.builder, expression)
}
@@ -189,13 +190,19 @@ parse_statement proc(state @mut State) ast.NodeId ! ParseError {
}
@hide:file
parse_return proc(state @mut State) ast.NodeId ! ParseError {
parse_return proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
keyword :: try consume(state, .return)
if (next_is(state, .newline) or next_is(state, .close_curly) or next_is(state, .eof)) {
if (
next_is(state, .newline) or next_is(state, .close_curly) or next_is(state, .eof)
or (allow_else and next_is(state, .else))
) {
return try ast.add_return_stmt(&state.builder, keyword.span, null)
}
value :: try parse_expr(state)
if (!next_is(state, .newline) and !next_is(state, .close_curly) and !next_is(state, .eof)) {
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
}
# include consumed grouping punctuation, which has no separate expression node.
@@ -207,6 +214,116 @@ parse_return proc(state @mut State) ast.NodeId ! ParseError {
)
}
@hide:file
parse_if proc(state @mut State) ast.NodeId ! ParseError {
keyword :: try consume(state, .if)
condition_start :: state.next_token
condition := parse_expr(state) catch |err| {
match err {
.unexpected_token: {}
else: return err
}
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
yield try ast.add_invalid_expr(&state.builder, current(state).span, diagnostic)
}
if (ast.builder_expr_kind(&state.builder, condition) == .invalid) {
# discard a malformed guard's tail, but leave its arm and enclosing block intact.
while (
!next_is(state, .open_curly) and !next_is(state, .close_curly)
and !next_is(state, .newline) and !next_is(state, .eof)
and !next_is(state, .else) and !next_is(state, .return) and !next_is(state, .if)
) {
state.next_token += 1
}
}
if (
!next_is(state, .open_curly)
and !parenthesized_guard(state, condition_start)
and ast.builder_expr_kind(&state.builder, condition) != .call
and ast.builder_expr_kind(&state.builder, condition) != .invalid
) {
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
condition = try ast.add_invalid_expr(
&state.builder,
ast.builder_span(&state.builder, condition),
diagnostic,
)
}
braced :: next_is(state, .open_curly)
then_block :: try parse_arm(state)
# only a braced arm permits its else on a later line.
after_then :: state.next_token
if (braced) skip_newlines(state)
alternative ?ast.NodeId := null
if next_is(state, .else) {
state.next_token += 1
alternative = try parse_arm(state)
} else {
state.next_token = after_then
}
end :: if (alternative) |block|
ast.builder_span(&state.builder, block).end
else
ast.builder_span(&state.builder, then_block).end
return try ast.add_if_stmt(
&state.builder,
ast.Span{ start = keyword.span.start, end = end },
condition,
then_block,
alternative,
)
}
@hide:file
parenthesized_guard proc(state @State, start usize) bool {
if (state.tokens[start].kind != .open_paren) return false
depth usize := 0
for start..state.next_token |index| {
kind :: state.tokens[index].kind
if (kind == .open_paren) depth += 1
if (kind == .close_paren) {
depth -= 1
if (depth == 0) return index + 1 == state.next_token
}
}
return false
}
@hide:file
parse_arm proc(state @mut State) ast.NodeId ! ParseError {
if next_is(state, .open_curly) return try parse_block(state)
start :: state.next_token
statement :: parse_statement(state, true) catch |err| {
match err {
.unexpected_token: {}
else: return err
}
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
span :: synchronize_arm(state, start)
yield try ast.add_invalid_stmt(&state.builder, span, diagnostic)
}
statements [1]ast.NodeId :: [statement]
return try ast.add_block(
&state.builder,
ast.builder_span(&state.builder, statement),
statements[..],
)
}
@hide:file
synchronize_arm proc(state @mut State, start usize) ast.Span {
first :: state.tokens[start].span
end := first.end
while (
!next_is(state, .eof) and !next_is(state, .newline)
and !next_is(state, .close_curly) and !next_is(state, .else)
) {
end = current(state).span.end
state.next_token += 1
}
return ast.Span{ start = first.start, end = end }
}
@hide:file
parse_statement_decl proc(state @mut State) ast.NodeId ! ParseError {
name :: try consume(state, .ident)
@@ -295,6 +412,10 @@ parse_call proc(state @mut State, callee ast.NodeId) ast.NodeId ! ParseError {
!next_is(state, .close_paren)
and !next_is(state, .newline)
and !next_is(state, .close_curly)
and !next_is(state, .open_curly)
and !next_is(state, .else)
and !next_is(state, .return)
and !next_is(state, .if)
and !next_is(state, .eof)
) {
argument :: try parse_expr(state)
@@ -347,6 +468,10 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
state.next_token += 1
return try ast.add_literal(&state.builder, .string, token.span)
}
.true, .false: {
state.next_token += 1
return try ast.add_literal(&state.builder, .bool, token.span)
}
.ident: {
state.next_token += 1
return try ast.add_identifier(&state.builder, token.span)
@@ -362,6 +487,10 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
and token.kind != .comma
and token.kind != .close_paren
and token.kind != .close_curly
and token.kind != .open_curly
and token.kind != .else
and token.kind != .return
and token.kind != .if
and token.kind != .eof
) {
state.next_token += 1
@@ -451,7 +580,7 @@ consume_either proc(state @mut State, kinds []lexer.TokenKind) lexer.Token ! Syn
@hide:file
prefix_binding_power proc(kind lexer.TokenKind) ?BindingPowerScalar {
return match kind {
.minus: BindingPowerScalar(30)
.minus: BindingPowerScalar(50)
else: null
}
}
@@ -459,8 +588,10 @@ prefix_binding_power proc(kind lexer.TokenKind) ?BindingPowerScalar {
@hide:file
infix_binding_power proc(kind lexer.TokenKind) ?BindingPower {
return match kind {
.plus, .minus: BindingPower{ left = BindingPowerScalar(10), right = BindingPowerScalar(11) }
.star, .slash: BindingPower{ left = BindingPowerScalar(20), right = BindingPowerScalar(21) }
.equal_equal, .not_equal: BindingPower{ left = BindingPowerScalar(10), right = BindingPowerScalar(11) }
.less, .less_equal, .greater, .greater_equal: BindingPower{ left = BindingPowerScalar(20), right = BindingPowerScalar(21) }
.plus, .minus: BindingPower{ left = BindingPowerScalar(30), right = BindingPowerScalar(31) }
.star, .slash: BindingPower{ left = BindingPowerScalar(40), right = BindingPowerScalar(41) }
else: null
}
}
@@ -472,6 +603,12 @@ binary_operator proc(kind lexer.TokenKind) ast.BinaryOperator {
.minus: .subtract
.star: .multiply
.slash: .divide
.equal_equal: .equal
.not_equal: .not_equal
.less: .less
.less_equal: .less_equal
.greater: .greater
.greater_equal: .greater_equal
else: unreachable
}
}
+180
View File
@@ -330,3 +330,183 @@ parses_return_boundaries_and_recovers_trailing_tokens test {
try testing.expect_equal("return", ast.source_text(&tree, last.span))
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
}
parses_boolean_comparison_precedence_and_associativity 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 ::
`value :: -f() * 2 + 3 < 4 <= 5 > 6 >= 7 == true != false
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)
different :: ast.binary_expr(&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 0)).initializer)
try testing.expect_equal(ast.BinaryOperator.not_equal, different.operator)
try testing.expect_equal(ast.ExprKind.literal_bool, ast.expr_kind(&tree, different.right))
try testing.expect_equal("false", ast.source_text(&tree, ast.node_span(&tree, different.right)))
equal :: ast.binary_expr(&tree, different.left)
try testing.expect_equal(ast.BinaryOperator.equal, equal.operator)
try testing.expect_equal(ast.ExprKind.literal_bool, ast.expr_kind(&tree, equal.right))
relation := equal.left
operators [4]ast.BinaryOperator :: [.greater_equal, .greater, .less_equal, .less]
for (operators) |operator| {
binary :: ast.binary_expr(&tree, relation)
try testing.expect_equal(operator, binary.operator)
relation = binary.left
}
addition :: ast.binary_expr(&tree, relation)
try testing.expect_equal(ast.BinaryOperator.add, addition.operator)
product :: ast.binary_expr(&tree, addition.left)
try testing.expect_equal(ast.BinaryOperator.multiply, product.operator)
negate :: ast.unary_expr(&tree, product.left)
try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, negate.operand))
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
parses_factorial_and_canonical_conditional_arms 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 {
` if (n <= 1) return 1 else return n * factorial(n - 1)
`}
`main proc() int {
` if true {
` local :: factorial(5)
` if (false) return 0 else return local
` }
` else if ready() return 2 else { return 3 }
`}
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)
factorial :: ast.function_decl(&tree, ast.package_decl(&tree, 0))
try testing.expect_equal(1, ast.block_statement_count(&tree, factorial.body))
guard :: ast.if_stmt(&tree, ast.block_statement(&tree, factorial.body, 0))
try testing.expect_equal("n <= 1", ast.source_text(&tree, ast.node_span(&tree, guard.condition)))
try testing.expect_equal(1, ast.block_statement_count(&tree, guard.then_block))
try testing.expect_equal("return 1", ast.source_text(&tree, ast.return_stmt(&tree, ast.block_statement(&tree, guard.then_block, 0)).span))
try testing.expect_equal(1, ast.block_statement_count(&tree, guard.else_block?))
try testing.expect_equal(ast.StmtKind.return_stmt, ast.stmt_kind(&tree, ast.block_statement(&tree, guard.else_block?, 0)))
main :: ast.function_decl(&tree, ast.package_decl(&tree, 1))
outer :: ast.if_stmt(&tree, ast.block_statement(&tree, main.body, 0))
try testing.expect_equal(2, ast.block_statement_count(&tree, outer.then_block))
try testing.expect_equal(ast.StmtKind.declaration, ast.stmt_kind(&tree, ast.block_statement(&tree, outer.then_block, 0)))
nested :: ast.if_stmt(&tree, ast.block_statement(&tree, outer.then_block, 1))
try testing.expect(nested.else_block != null)
try testing.expect_equal(1, ast.block_statement_count(&tree, outer.else_block?))
chain :: ast.if_stmt(&tree, ast.block_statement(&tree, outer.else_block?, 0))
try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, chain.condition))
try testing.expect(chain.else_block != null)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
attaches_dangling_else_to_nearest_if 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() int { if (true) if (false) return 1 else return 2 else return 3 }"
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)
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
outer :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 0))
inner :: ast.if_stmt(&tree, ast.block_statement(&tree, outer.then_block, 0))
inner_return :: ast.return_stmt(&tree, ast.block_statement(&tree, inner.else_block?, 0))
outer_return :: ast.return_stmt(&tree, ast.block_statement(&tree, outer.else_block?, 0))
try testing.expect_equal("2", ast.source_text(&tree, ast.node_span(&tree, inner_return.value?)))
try testing.expect_equal("3", ast.source_text(&tree, ast.node_span(&tree, outer_return.value?)))
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
recovers_conditional_conditions_and_arms_without_losing_siblings 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 { good() } else { other() }
` if (true) else return
` if broken( { kept() }
` after :: 7
` return 1 else return 2
` final :: 8
`}
`tail :: 9
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &diagnostic_store)
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(2, ast.package_decl_count(&tree))
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
try testing.expect_equal(6, ast.block_statement_count(&tree, body))
first :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 0))
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, first.condition))
condition_diagnostic :: diagnostics.get(&diagnostic_store, ast.invalid(&tree, first.condition).diagnostic)?
try testing.expect_equal("{", ast.source_text(&tree, condition_diagnostic.span))
try testing.expect_equal(1, ast.block_statement_count(&tree, first.then_block))
second :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 1))
bad_arm :: ast.block_statement(&tree, second.then_block, 0)
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, bad_arm))
arm_diagnostic :: diagnostics.get(&diagnostic_store, ast.invalid(&tree, bad_arm).diagnostic)?
try testing.expect_equal("else", ast.source_text(&tree, arm_diagnostic.span))
try testing.expect(second.else_block != null)
third :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 2))
try testing.expect_equal(1, ast.block_statement_count(&tree, third.then_block))
after :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 3))
try testing.expect_equal("after", ast.source_text(&tree, after.name))
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 4)))
final :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 5))
try testing.expect_equal("final", ast.source_text(&tree, final.name))
try testing.expect_equal(4, diagnostics.count(&diagnostic_store))
}
recovers_invalid_guard_syntax_and_missing_arm_at_enclosing_brace 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() int {
` if true return 1 else return 2
` if (true junk) { kept() } else { other() }
` sibling :: 3
` if (false) }
`tail :: 4
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &diagnostic_store)
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(2, ast.package_decl_count(&tree))
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
try testing.expect_equal(4, ast.block_statement_count(&tree, body))
first :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 0))
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, first.condition))
guard_error :: diagnostics.get(&diagnostic_store, ast.invalid(&tree, first.condition).diagnostic)?
try testing.expect_equal("return", ast.source_text(&tree, guard_error.span))
try testing.expect(first.else_block != null)
second :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 1))
try testing.expect_equal(1, ast.block_statement_count(&tree, second.then_block))
try testing.expect(second.else_block != null)
sibling :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 2))
try testing.expect_equal("sibling", ast.source_text(&tree, sibling.name))
last :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 3))
missing :: ast.block_statement(&tree, last.then_block, 0)
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, missing))
arm_error :: diagnostics.get(&diagnostic_store, ast.invalid(&tree, missing).diagnostic)?
try testing.expect_equal("}", ast.source_text(&tree, arm_error.span))
try testing.expect_equal(3, diagnostics.count(&diagnostic_store))
}
+119 -58
View File
@@ -5,7 +5,7 @@ import "@source/ast"
import "@source/diagnostics"
sema :: import "@source/sema"
TypeKind :: enum { none, invalid, int, float, string, void, function }
TypeKind :: enum { none, invalid, int, float, bool, string, void, function }
Type :: struct {
kind TypeKind = .none
@@ -27,7 +27,7 @@ Result :: struct { storage Storage }
no_index :: maxval!(usize)
@hide:file
Role :: enum { none, expression, declaration, parameter, function, annotation, invalid, body, return_stmt }
Role :: enum { none, expression, declaration, parameter, function, annotation, invalid, body, return_stmt, if_stmt }
@hide:file
Cell :: struct {
@@ -161,6 +161,7 @@ check proc(
.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)
}
@@ -178,12 +179,16 @@ check proc(
.unary: try numeric_operand(&state, id, ast.unary_expr(tree, id).operand)
.binary: {
expression :: ast.binary_expr(tree, id)
if (comparison(expression.operator)) {
if (!bool_equality(&state, id)) try comparison_operands(&state, id)
} else {
try numeric_operand(&state, id, expression.left)
try numeric_operand(&state, id, expression.right)
if (expression.operator == .divide) {
try impose(&state, id, .float, .integer_division, expression.span)
}
}
}
else: {}
}
}
@@ -246,6 +251,28 @@ check proc(
# errors travel only from dependencies to users. A rejected use never poisons
# an explicitly typed declaration, and body errors do not erase signatures.
propagate(&state, queue)
# validate conditions after operand errors have reached their users.
for (0..count) |i| {
if (cells[i].role != .if_stmt) continue
condition :: ast.if_stmt(tree, ast.NodeId(i)).condition
actual :: types[usize(condition)]
if (actual.kind != .invalid and actual.kind != .bool) {
try fail(&state, condition, .condition_not_bool, ast.node_span(tree, condition))
}
}
propagate(&state, queue)
for (0..ast.package_decl_count(tree)) |i| {
id :: ast.package_decl(tree, i)
if (ast.decl_kind(tree, id) != .function) continue
function :: ast.function_decl(tree, id)
result :: types[usize(function.return_type)]
if (result.kind == .void or result.kind == .invalid or types[usize(id)].kind == .invalid) continue
if (block_falls_through(&state, function.body)) {
# lowering must find this diagnostic even when another arm failed.
diagnostic :: try diagnostics.add(diagnostic_store, .missing_return, ast.node_span(tree, function.body))
types[usize(function.body)] = invalid_type(diagnostic)
}
}
return Result{ storage = Storage{ types = types, allocator = allocator } }
}
@@ -371,6 +398,7 @@ check_annotation proc(state @mut State, id ast.NodeId, allow_void bool) Type ! (
type.kind = match binding.data.builtin {
.int: .int
.float: .float
.bool: .bool
.string: .string
.void: .void
}
@@ -427,25 +455,44 @@ collect_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError |
state.cells[usize(id)].type = invalid_type(binding.data.diagnostic)
state.cells[usize(id)].type.function = id
}
state.cells[usize(function.body)].role = .body
for (0..ast.block_statement_count(tree, function.body)) |i| {
statement :: ast.block_statement(tree, function.body, i)
try collect_block(state, function.body, function.return_type)
}
@hide:file
collect_block proc(state @mut State, id, return_annotation ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
state.cells[usize(id)].role = .body
for (0..ast.block_statement_count(tree, id)) |i| {
statement :: ast.block_statement(tree, id, i)
match ast.stmt_kind(tree, statement) {
.declaration: try collect_declaration(state, statement)
.expression: try collect_expression(state, ast.statement_expr(tree, statement))
.return_stmt: {
state.cells[usize(statement)].role = .return_stmt
state.cells[usize(statement)].type = Type{ kind = .void }
state.cells[usize(statement)].return_annotation = function.return_type
state.cells[usize(statement)].return_annotation = return_annotation
if (ast.return_stmt(tree, statement).value) |value| {
try collect_expression(state, value)
try depend(state, value, statement)
}
}
.if_stmt: {
conditional :: ast.if_stmt(tree, statement)
state.cells[usize(statement)].role = .if_stmt
state.cells[usize(statement)].type = Type{ kind = .void }
try collect_expression(state, conditional.condition)
try depend(state, conditional.condition, statement)
try collect_block(state, conditional.then_block, return_annotation)
try depend(state, conditional.then_block, id)
if (conditional.else_block) |other| {
try collect_block(state, other, return_annotation)
try depend(state, other, id)
}
}
.invalid: collect_invalid(state, statement)
}
source :: if (ast.stmt_kind(tree, statement) == .expression) ast.statement_expr(tree, statement) else statement
try depend(state, source, function.body)
try depend(state, source, id)
}
}
@@ -460,6 +507,7 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
state.cells[usize(id)].anchored = true
}
.literal_float: state.cells[usize(id)].type = Type{ kind = .float }
.literal_bool: state.cells[usize(id)].type = Type{ kind = .bool }
.literal_string: state.cells[usize(id)].type = Type{ kind = .string }
.identifier: {
binding :: sema.binding(state.resolution, id)
@@ -476,8 +524,12 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
try depend(state, operand, id)
}
.binary: {
state.cells[usize(id)].numeric = true
expression :: ast.binary_expr(tree, id)
if (comparison(expression.operator)) {
state.cells[usize(id)].type = Type{ kind = .bool }
} else {
state.cells[usize(id)].numeric = true
}
try collect_expression(state, expression.left)
try collect_expression(state, expression.right)
try depend(state, expression.left, id)
@@ -502,6 +554,40 @@ numeric_kind proc(kind TypeKind) bool {
return kind == .int or kind == .float
}
@hide:file
comparison proc(operator ast.BinaryOperator) bool {
return (
operator == .equal or operator == .not_equal or operator == .less
or operator == .less_equal or operator == .greater or operator == .greater_equal
)
}
@hide:file
bool_equality proc(state @mut State, id ast.NodeId) bool {
expression :: ast.binary_expr(state.tree, id)
return (
(expression.operator == .equal or expression.operator == .not_equal)
and group_type(state, expression.left).kind == .bool
and group_type(state, expression.right).kind == .bool
)
}
@hide:file
comparison_operands proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
if (state.types[usize(id)].kind == .invalid) return
expression :: ast.binary_expr(state.tree, id)
left :: root(state, usize(expression.left))
right :: root(state, usize(expression.right))
# only operand groups receive numeric demands; the result stays bool.
if (state.cells[left].numeric or state.cells[left].type.kind == .none) {
state.cells[left].numeric = true
try numeric_constraint(state, expression.left, expression.right, id)
} else if (state.cells[right].numeric or state.cells[right].type.kind == .none) {
state.cells[right].numeric = true
try numeric_constraint(state, expression.right, expression.left, id)
}
}
@hide:file
impose proc(state @mut State, id ast.NodeId, kind TypeKind, code diagnostics.Code, span ast.Span) void ! (mem.AllocError | diagnostics.CapacityError) {
index :: root(state, usize(id))
@@ -516,6 +602,11 @@ impose proc(state @mut State, id ast.NodeId, kind TypeKind, code diagnostics.Cod
@hide:file
numeric_operand proc(state @mut State, expression, operand ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
try numeric_constraint(state, expression, operand, expression)
}
@hide:file
numeric_constraint proc(state @mut State, expression, operand, operation ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
index :: root(state, usize(operand))
type :: state.cells[index].type
if (state.types[usize(expression)].kind == .invalid or state.types[usize(operand)].kind == .invalid) return
@@ -525,13 +616,13 @@ numeric_operand proc(state @mut State, expression, operand ast.NodeId) void ! (m
operand_demand :: state.cells[index].demand
join(state, expression, operand)
if (expression_demand != .none) {
try impose(state, expression, expression_demand, .invalid_operand, ast.node_span(state.tree, expression))
try impose(state, expression, expression_demand, .invalid_operand, ast.node_span(state.tree, operation))
}
if (operand_demand != .none) {
try impose(state, expression, operand_demand, .invalid_operand, ast.node_span(state.tree, expression))
try impose(state, expression, operand_demand, .invalid_operand, ast.node_span(state.tree, operation))
}
} else if (numeric_kind(type.kind)) {
try impose(state, expression, type.kind, .invalid_operand, ast.node_span(state.tree, expression))
try impose(state, expression, type.kind, .invalid_operand, ast.node_span(state.tree, operation))
}
}
@@ -568,20 +659,6 @@ 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)])
}
.function: {
function :: ast.function_decl(tree, id)
result :: state.types[usize(function.return_type)]
if (result.kind == .void or result.kind == .invalid or state.types[usize(id)].kind == .invalid) return
for (0..ast.block_statement_count(tree, function.body)) |i| {
if (ast.stmt_kind(tree, ast.block_statement(tree, function.body, i)) == .return_stmt) return
}
if (syntactically_valid_body(tree, function.body)) {
diagnostic :: try diagnostics.add(state.diagnostics, .missing_return, ast.node_span(tree, function.body))
if (state.types[usize(function.body)].kind != .invalid) {
state.types[usize(function.body)] = invalid_type(diagnostic)
}
}
}
.return_stmt: {
statement :: ast.return_stmt(tree, id)
expected :: state.types[usize(state.cells[usize(id)].return_annotation)]
@@ -611,6 +688,8 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
left :: state.types[usize(expression.left)]
right :: state.types[usize(expression.right)]
if (left.kind == .invalid or right.kind == .invalid) return
if ((expression.operator == .equal or expression.operator == .not_equal)
and left.kind == .bool and right.kind == .bool) return
if (!numeric_kind(left.kind) or !numeric_kind(right.kind) or left.kind != right.kind) {
try fail(state, id, .invalid_operand, expression.span)
} else if (expression.operator == .divide and left.kind == .int) {
@@ -649,44 +728,26 @@ validate_call proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | dia
}
}
@hide:file
syntactically_valid_expression proc(tree @ast.Tree, id ast.NodeId) bool {
match ast.expr_kind(tree, id) {
.invalid: return false
.unary: return syntactically_valid_expression(tree, ast.unary_expr(tree, id).operand)
.binary: {
expression :: ast.binary_expr(tree, id)
return syntactically_valid_expression(tree, expression.left) and syntactically_valid_expression(tree, expression.right)
}
.call: {
if (!syntactically_valid_expression(tree, ast.call_expr(tree, id).callee)) return false
for (0..ast.call_arg_count(tree, id)) |i| {
if (!syntactically_valid_expression(tree, ast.call_arg(tree, id, i))) return false
block_falls_through proc(state @State, id ast.NodeId) bool {
tree :: state.tree
for (0..ast.block_statement_count(tree, id)) |i| {
statement :: ast.block_statement(tree, id, i)
kind :: ast.stmt_kind(tree, statement)
source :: if (kind == .expression) ast.statement_expr(tree, statement) else statement
if (state.types[usize(source)].kind == .invalid) return false
match kind {
.return_stmt, .invalid: return false
.if_stmt: {
conditional :: ast.if_stmt(tree, statement)
then_falls :: block_falls_through(state, conditional.then_block)
if (conditional.else_block) |other| {
if (!then_falls and !block_falls_through(state, other)) return false
}
}
else: {}
}
return true
}
@hide:file
syntactically_valid_body proc(tree @ast.Tree, id ast.NodeId) bool {
for (0..ast.block_statement_count(tree, id)) |i| {
statement :: ast.block_statement(tree, id, i)
match ast.stmt_kind(tree, statement) {
.invalid: return false
.expression: {
if (!syntactically_valid_expression(tree, ast.statement_expr(tree, statement))) return false
}
.return_stmt: {
if (ast.return_stmt(tree, statement).value) |value| {
if (!syntactically_valid_expression(tree, value)) return false
}
}
.declaration: {
if (!syntactically_valid_expression(tree, ast.statement_decl(tree, statement).initializer)) return false
}
}
}
return true
}
+237
View File
@@ -731,3 +731,240 @@ return_errors_preserve_identity_check_unreachable_code_and_suppress_missing_retu
try testing.expect_equal(2, code_count(&diagnostic_store, .unexpected_token))
try testing.expect_equal(8, diagnostics.count(&diagnostic_store))
}
checks_bool_comparisons_recursive_returns_and_nested_numeric_context 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 ::
`flag bool :: true
`same :: flag == false
`different :: true != flag
`less :: alias < 4.0
`less_equal :: seed <= 5
`greater :: 6 > seed
`greater_equal :: seed >= 1
`equal :: seed == 3
`not_equal :: seed != 2
`alias :: seed
`seed := 3
`identity proc(value bool) bool { return value }
`factorial proc(n int) int {
` if n <= 1 { return 1 }
` return n * factorial(n - 1)
`}
`fraction proc(value bool) float {
` if value {
` before :: later
` later := 3
` return -before / 2
` } else if false {
` later :: 4
` return later
` } else {
` return 5
` }
`}
`main proc() bool { return identity(flag) }
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 (0..usize(9)) |i| {
declaration :: ast.package_decl(&tree, i)
try expect_kind(&result, declaration, .bool)
try expect_kind(&result, ast.statement_decl(&tree, declaration).initializer, .bool)
if (i < 3) continue
comparison_view :: ast.binary_expr(&tree, ast.statement_decl(&tree, declaration).initializer)
try expect_kind(&result, comparison_view.left, .float)
try expect_kind(&result, comparison_view.right, .float)
}
for (usize(9)..usize(11)) |i| try expect_kind(&result, ast.package_decl(&tree, i), .float)
identity :: ast.package_decl(&tree, 11)
try expect_kind(&result, ast.param(&tree, ast.function_param(&tree, identity, 0)).type, .bool)
try expect_kind(&result, ast.function_decl(&tree, identity).return_type, .bool)
factorial :: ast.package_decl(&tree, 12)
factorial_view :: ast.function_decl(&tree, factorial)
try expect_kind(&result, factorial, .function)
conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, factorial_view.body, 0))
try expect_kind(&result, conditional.condition, .bool)
operands :: ast.binary_expr(&tree, conditional.condition)
try expect_kind(&result, operands.left, .int)
try expect_kind(&result, operands.right, .int)
fraction :: ast.function_decl(&tree, ast.package_decl(&tree, 13))
arms :: ast.if_stmt(&tree, ast.block_statement(&tree, fraction.body, 0))
for (0..usize(2)) |i| {
declaration :: ast.block_statement(&tree, arms.then_block, i)
try expect_kind(&result, declaration, .float)
try expect_kind(&result, ast.statement_decl(&tree, declaration).initializer, .float)
}
if (arms.else_block) |other| {
nested :: ast.if_stmt(&tree, ast.block_statement(&tree, other, 0))
try expect_kind(&result, ast.block_statement(&tree, nested.then_block, 0), .float)
} else {
return .expectation_failed
}
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
rejects_bool_numeric_mixing_and_preserves_invalid_condition_identity 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 ::
`sum :: true + false
`order :: true < false
`mixed :: true == 1
`strings :: "x" == "x"
`wrong_bool bool :: 1
`wrong_int int :: false
`main proc() void {
` if (1) { okay :: true }
` if "text" { okay :: false }
` if absent { okay :: true }
` if true + false { okay :: false }
` if true { return } else { -"bad" }
`}
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 (0..usize(4)) |i| {
expression :: ast.statement_decl(&tree, ast.package_decl(&tree, i)).initializer
_ = try expect_error(&result, &diagnostic_store, expression, .invalid_operand, ast.node_span(&tree, expression))
}
for (usize(4)..usize(6)) |i| {
expression :: ast.statement_decl(&tree, ast.package_decl(&tree, i)).initializer
_ = try expect_error(&result, &diagnostic_store, expression, .type_mismatch, ast.node_span(&tree, expression))
}
function :: ast.package_decl(&tree, 6)
body :: ast.function_decl(&tree, function).body
for (0..usize(2)) |i| {
statement :: ast.block_statement(&tree, body, i)
condition :: ast.if_stmt(&tree, statement).condition
diagnostic :: try expect_error(&result, &diagnostic_store, condition, .condition_not_bool, ast.node_span(&tree, condition))
try testing.expect_equal(diagnostic, type_of(&result, statement).diagnostic)
}
unknown :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 2)).condition
original :: sema.binding(&resolution, unknown).data.diagnostic
try testing.expect_equal(original, type_of(&result, unknown).diagnostic)
failed :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 3)).condition
_ = try expect_error(&result, &diagnostic_store, failed, .invalid_operand, ast.node_span(&tree, failed))
final_if :: ast.block_statement(&tree, body, 4)
try expect_kind(&result, final_if, .void)
final_view :: ast.if_stmt(&tree, final_if)
if (final_view.else_block) |other| {
error_expr :: ast.statement_expr(&tree, ast.block_statement(&tree, other, 0))
_ = try expect_error(&result, &diagnostic_store, error_expr, .invalid_operand, ast.node_span(&tree, error_expr))
} else {
return .expectation_failed
}
try expect_kind(&result, function, .function)
try testing.expect_equal(2, code_count(&diagnostic_store, .condition_not_bool))
try testing.expect_equal(6, code_count(&diagnostic_store, .invalid_operand))
try testing.expect_equal(2, code_count(&diagnostic_store, .type_mismatch))
try testing.expect_equal(1, code_count(&diagnostic_store, .unknown_name))
try testing.expect_equal(11, diagnostics.count(&diagnostic_store))
}
analyzes_return_paths_after_trap_propagation_without_poisoning_signatures 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 ::
`both proc(flag bool) int {
` if flag { return 1 } else { return 2 }
`}
`missing proc() int { if true { return 1 } }
`bad_arm proc(flag bool) int { if flag { -"bad" } }
`trap_and_return proc(flag bool) int {
` if flag { return 1 } else { bad int :: false }
`}
`invalid_condition proc() int { if 1 {} }
`invalid_expression proc() int { -"bad" }
`invalid_declaration proc() int { bad bool :: 1 }
`syntax_trap proc() int {
` return 1 stray
`}
`unreachable proc() int {
` return 1
` -"bad"
`}
`nested proc(a bool, b bool) int {
` if a { if b { return 1 } else { return 2 } } else { return 3 }
`}
`join proc(flag bool) int {
` if flag {} else { return 1 }
` return 2
`}
`answer :: bad_arm(false)
`wrong_return proc(flag bool) int {
` if flag { return false } else { return 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 (0..usize(11)) |i| try expect_kind(&result, ast.package_decl(&tree, i), .function)
for (usize(1)..usize(3)) |i| {
body :: ast.function_decl(&tree, ast.package_decl(&tree, i)).body
_ = try expect_error(&result, &diagnostic_store, body, .missing_return, ast.node_span(&tree, body))
}
bad_body :: ast.function_decl(&tree, ast.package_decl(&tree, 2)).body
conditional :: ast.block_statement(&tree, bad_body, 0)
try expect_kind(&result, conditional, .void)
arm :: ast.if_stmt(&tree, conditional).then_block
operand :: ast.statement_expr(&tree, ast.block_statement(&tree, arm, 0))
original :: try expect_error(&result, &diagnostic_store, operand, .invalid_operand, ast.node_span(&tree, operand))
try testing.expect_equal(original, type_of(&result, arm).diagnostic)
try testing.expect(type_of(&result, bad_body).diagnostic != original)
condition_body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body
condition :: ast.if_stmt(&tree, ast.block_statement(&tree, condition_body, 0)).condition
condition_diag :: try expect_error(&result, &diagnostic_store, condition, .condition_not_bool, ast.node_span(&tree, condition))
try testing.expect_equal(condition_diag, type_of(&result, condition_body).diagnostic)
recovered :: ast.function_decl(&tree, ast.package_decl(&tree, 7)).body
syntax :: ast.block_statement(&tree, recovered, 0)
try testing.expect_equal(ast.invalid(&tree, syntax).diagnostic, type_of(&result, recovered).diagnostic)
unreachable_body :: ast.function_decl(&tree, ast.package_decl(&tree, 8)).body
checked_after_return :: ast.statement_expr(&tree, ast.block_statement(&tree, unreachable_body, 1))
_ = try expect_error(&result, &diagnostic_store, checked_after_return, .invalid_operand, ast.node_span(&tree, checked_after_return))
try expect_kind(&result, ast.package_decl(&tree, 11), .int)
wrong_function :: ast.package_decl(&tree, 12)
try expect_kind(&result, wrong_function, .function)
wrong_body :: ast.function_decl(&tree, wrong_function).body
wrong_if :: ast.block_statement(&tree, wrong_body, 0)
try expect_kind(&result, wrong_if, .void)
wrong_arm :: ast.if_stmt(&tree, wrong_if).then_block
wrong_statement :: ast.block_statement(&tree, wrong_arm, 0)
if (ast.return_stmt(&tree, wrong_statement).value) |value| {
mismatch :: try expect_error(&result, &diagnostic_store, value, .return_type_mismatch, ast.node_span(&tree, value))
try testing.expect_equal(mismatch, type_of(&result, wrong_statement).diagnostic)
try testing.expect_equal(mismatch, type_of(&result, wrong_body).diagnostic)
} else {
return .expectation_failed
}
try testing.expect_equal(1, code_count(&diagnostic_store, .return_type_mismatch))
try testing.expect_equal(2, code_count(&diagnostic_store, .missing_return))
try testing.expect_equal(1, code_count(&diagnostic_store, .condition_not_bool))
}
+32 -13
View File
@@ -6,12 +6,13 @@ import "@std/strmap"
import "@source/ast"
import "@source/diagnostics"
BuiltinType :: enum { int, float, string, void }
BuiltinType :: enum { int, float, bool, string, void }
@hide:file
builtins strmap.StaticStringMap(BuiltinType) :: strmap.init([
{ "int", .int },
{ "float", .float },
{ "bool", .bool },
{ "string", .string },
{ "void", .void },
])
@@ -175,20 +176,38 @@ resolve_function proc(
parameter :: ast.function_param(tree, id, i)
try predeclare(state, &scope, parameter, ast.param(tree, parameter).name)
}
for (0..ast.block_statement_count(tree, function.body)) |i| {
statement :: ast.block_statement(tree, function.body, i)
if ast.stmt_kind(tree, statement) == .declaration {
try predeclare(state, &scope, statement, ast.statement_decl(tree, statement).name)
}
}
try resolve_block(state, &scope, function.body)
}
for (0..ast.block_statement_count(tree, function.body)) |i| {
statement :: ast.block_statement(tree, function.body, i)
@hide:file
resolve_arm proc(state @mut State, parent @Scope, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
scope := Scope{ names = hashmap.init(state.allocator), parent = parent }
defer hashmap.deinit(&scope.names)
try resolve_block(state, &scope, id)
}
@hide:file
resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
for (0..ast.block_statement_count(tree, id)) |i| {
statement :: ast.block_statement(tree, id, i)
if ast.stmt_kind(tree, statement) == .declaration {
try predeclare(state, scope, statement, ast.statement_decl(tree, statement).name)
}
}
for (0..ast.block_statement_count(tree, id)) |i| {
statement :: ast.block_statement(tree, id, i)
match ast.stmt_kind(tree, statement) {
.declaration: try resolve_statement_decl(state, &scope, statement)
.expression: try resolve_expr(state, &scope, ast.statement_expr(tree, statement))
.declaration: try resolve_statement_decl(state, scope, statement)
.expression: try resolve_expr(state, scope, ast.statement_expr(tree, statement))
.return_stmt: {
if (ast.return_stmt(tree, statement).value) |value| try resolve_expr(state, &scope, value)
if (ast.return_stmt(tree, statement).value) |value| try resolve_expr(state, scope, value)
}
.if_stmt: {
conditional :: ast.if_stmt(tree, statement)
try resolve_expr(state, scope, conditional.condition)
try resolve_arm(state, scope, conditional.then_block)
if (conditional.else_block) |other| try resolve_arm(state, scope, other)
}
.invalid: resolve_invalid(state, statement)
}
@@ -225,7 +244,7 @@ resolve_expr proc(
if (state.bindings[usize(id)].kind != .none) return
tree :: state.tree
match ast.expr_kind(tree, id) {
.literal_int, .literal_float, .literal_string: {}
.literal_int, .literal_float, .literal_bool, .literal_string: {}
.identifier: {
span :: ast.node_span(tree, id)
name :: ast.source_text(tree, span)
+68
View File
@@ -298,3 +298,71 @@ preserves_syntax_diagnostics_and_resolves_shared_unknown_types_once test {
try testing.expect_equal(later, binding(&resolution, valid_ref).data.declaration)
try testing.expect_equal(syntax_count + 3, diagnostics.count(&diagnostic_store))
}
resolves_conditional_arm_scopes_without_leaks_or_shadowing 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 ::
`choose proc(flag bool, value int) void {
` if flag {
` before :: later
` later :: value
` if before == value { nested :: later }
` } else {
` later :: value
` }
` later
` if later { later :: 1 }
` if flag {
` value :: 2
` bool :: false
` if flag { value }
` }
`}
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)
view :: ast.function_decl(&tree, function)
flag :: ast.function_param(&tree, function, 0)
value :: ast.function_param(&tree, function, 1)
try testing.expect_equal(BuiltinType.bool, binding(&resolution, ast.param(&tree, flag).type).data.builtin)
first :: ast.if_stmt(&tree, ast.block_statement(&tree, view.body, 0))
try testing.expect_equal(flag, binding(&resolution, first.condition).data.declaration)
before :: ast.block_statement(&tree, first.then_block, 0)
later :: ast.block_statement(&tree, first.then_block, 1)
try testing.expect_equal(later, binding(&resolution, ast.statement_decl(&tree, before).initializer).data.declaration)
try testing.expect_equal(value, binding(&resolution, ast.statement_decl(&tree, later).initializer).data.declaration)
nested :: ast.if_stmt(&tree, ast.block_statement(&tree, first.then_block, 2))
nested_decl :: ast.block_statement(&tree, nested.then_block, 0)
try testing.expect_equal(later, binding(&resolution, ast.statement_decl(&tree, nested_decl).initializer).data.declaration)
if (first.else_block) |other| {
sibling :: ast.block_statement(&tree, other, 0)
try testing.expect_equal(BindingKind.declaration, binding(&resolution, sibling).kind)
try testing.expect_equal(sibling, binding(&resolution, sibling).data.declaration)
} else {
return .expectation_failed
}
outside :: ast.statement_expr(&tree, ast.block_statement(&tree, view.body, 1))
try testing.expect_equal(BindingKind.invalid, binding(&resolution, outside).kind)
independent :: ast.if_stmt(&tree, ast.block_statement(&tree, view.body, 2))
try testing.expect_equal(BindingKind.invalid, binding(&resolution, independent.condition).kind)
try testing.expect_equal(BindingKind.declaration, binding(&resolution, ast.block_statement(&tree, independent.then_block, 0)).kind)
forbidden :: ast.if_stmt(&tree, ast.block_statement(&tree, view.body, 3))
shadow :: ast.block_statement(&tree, forbidden.then_block, 0)
builtin :: ast.block_statement(&tree, forbidden.then_block, 1)
try testing.expect_equal(BindingKind.invalid, binding(&resolution, shadow).kind)
try testing.expect_equal(BindingKind.invalid, binding(&resolution, builtin).kind)
poisoned :: ast.if_stmt(&tree, ast.block_statement(&tree, forbidden.then_block, 2))
use :: ast.statement_expr(&tree, ast.block_statement(&tree, poisoned.then_block, 0))
try testing.expect_equal(binding(&resolution, shadow).data.diagnostic, binding(&resolution, use).data.diagnostic)
try testing.expect_equal(4, diagnostics.count(&diagnostic_store))
}