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
+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
+113 -14
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| {
if (state.terminated) return
try lower_statement(state, ast.block_statement(tree, declaration.body, i))
try lower_block(state, declaration.body)
if (state.terminated) return
# aggregate arm errors are not demanded by fallthrough; only missing_return is.
body_type :: typecheck.type_of(state.checked, declaration.body)
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))
}
+121 -60
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,10 +179,14 @@ check proc(
.unary: try numeric_operand(&state, id, ast.unary_expr(tree, id).operand)
.binary: {
expression :: ast.binary_expr(tree, id)
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)
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,43 +728,25 @@ 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
}
}
else: {}
}
return true
}
@hide:file
syntactically_valid_body proc(tree @ast.Tree, id ast.NodeId) bool {
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)
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
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
}
}
.declaration: {
if (!syntactically_valid_expression(tree, ast.statement_decl(tree, statement).initializer)) return false
}
else: {}
}
}
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))
}
+30 -11
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)
try resolve_block(state, &scope, function.body)
}
@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)
try predeclare(state, scope, statement, ast.statement_decl(tree, statement).name)
}
}
for (0..ast.block_statement_count(tree, function.body)) |i| {
statement :: ast.block_statement(tree, function.body, i)
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))
}