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
+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)
}