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:
+113
-14
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user