feat: execute short-circuit boolean expressions

This commit is contained in:
2026-10-10 00:36:07 +02:00
parent 7551ce5f6d
commit 5f845b9520
15 changed files with 952 additions and 30 deletions
+68 -8
View File
@@ -24,6 +24,7 @@ State :: struct {
diagnostics @mut diagnostics.Store
module @mut ir.Module
storage []mut Storage
arguments arraylist.ArrayList(ir.ValueId)
function usize = 0
block ir.BlockId = ir.BlockId(0)
terminated bool = false
@@ -50,7 +51,9 @@ lower proc(
diagnostics = diagnostic_store,
module = &module,
storage = storage,
arguments = arraylist.init(ir.ValueId, allocator),
}
defer arraylist.deinit(&state.arguments)
# allocate every package identity before lowering any initializer or body.
for (0..ast.package_decl_count(tree)) |i| {
@@ -344,9 +347,10 @@ lower_while proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagn
state.block = guard
condition :: try lower_expression(state, loop.condition)
if (state.terminated) return
guard_end :: state.block
body :: try add_block(state)
exit :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(guard)].terminator = ir.Terminator{
state.module.functions.items[state.function].blocks.items[usize(guard_end)].terminator = ir.Terminator{
kind = .branch,
value = condition,
target = body,
@@ -413,6 +417,48 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d
}
}
@hide:file
lower_logical proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
expression :: ast.binary_expr(state.tree, id)
span :: ast.node_span(state.tree, id)
left :: try lower_expression(state, expression.left)
if (state.terminated) return ir.no_value
origin :: state.block
slot :: u32(state.module.functions.items[state.function].locals.items.len)
try arraylist.append(&state.module.functions.items[state.function].locals, ir.Type.bool)
right_block :: try add_block(state)
skip :: try add_block(state)
join :: try add_block(state)
conjunction :: expression.operator == .logical_and
target :: if (conjunction) right_block else skip
alternative :: if (conjunction) skip else right_block
state.module.functions.items[state.function].blocks.items[usize(origin)].terminator = ir.Terminator{
kind = .branch,
value = left,
target = target,
alternative = alternative,
span = ast.node_span(state.tree, expression.left),
}
state.block = skip
_ = try emit(state, ir.Instruction{ op = .store_local, type = .bool, left = left, slot = slot, span = span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
state.block = right_block
right :: try lower_expression(state, expression.right)
if (!state.terminated) {
_ = try emit(state, ir.Instruction{ op = .store_local, type = .bool, left = right, slot = slot, span = span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
}
# the skip edge reaches the join even when the demanded right edge traps.
state.block = join
state.terminated = false
if (trap_type(state, id)) return ir.no_value
return try emit(state, ir.Instruction{ op = .load_local, type = .bool, slot = slot, span = span })
}
@hide:file
lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
@@ -454,7 +500,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
}
.unary: {
expression :: ast.unary_expr(tree, id)
if (type == .int and ast.expr_kind(tree, expression.operand) == .literal_int) {
if (expression.operator == .negate and type == .int and ast.expr_kind(tree, expression.operand) == .literal_int) {
text :: ast.source_text(tree, ast.node_span(tree, expression.operand))
if (integer_magnitude(text, u64(maxval!(i64)) + 1)) |magnitude| {
if (magnitude == u64(maxval!(i64)) + 1) {
@@ -464,10 +510,17 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
}
operand :: try lower_expression(state, expression.operand)
if (state.terminated or trap_type(state, id)) return ir.no_value
return try emit(state, ir.Instruction{ op = .negate, type = type, left = operand, span = span })
op ir.Op :: match expression.operator {
.negate: .negate
.logical_not: .logical_not
}
return try emit(state, ir.Instruction{ op = op, type = type, left = operand, span = span })
}
.binary: {
expression :: ast.binary_expr(tree, id)
if (expression.operator == .logical_and or expression.operator == .logical_or) {
return try lower_logical(state, id)
}
left :: try lower_expression(state, expression.left)
if (state.terminated) return ir.no_value
right :: try lower_expression(state, expression.right)
@@ -483,23 +536,30 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
.less_equal: .less_equal
.greater: .greater
.greater_equal: .greater_equal
.logical_and, .logical_or: unreachable
}
return try emit(state, ir.Instruction{ op = op, type = type, left = left, right = right, span = span })
}
.call: {
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[usize(state.block)]
start :: block.arguments.items.len
# recursion borrows later scratch slots; no items pointer survives a child.
scratch_start :: state.arguments.items.len
defer state.arguments.items = state.arguments.items.ptr[..scratch_start]
count :: ast.call_arg_count(tree, id)
for (0..count) |_| try arraylist.append(&block.arguments, ir.no_value)
try arraylist.reserve(&state.arguments, scratch_start + count)
for (0..count) |_| try arraylist.append(&state.arguments, ir.no_value)
for (0..count) |i| {
argument :: try lower_expression(state, ast.call_arg(tree, id, i))
if (state.terminated) return ir.no_value
block.arguments.items[start + i] = argument
state.arguments.items[scratch_start + i] = argument
}
if (trap_type(state, id)) return ir.no_value
# branchful arguments end in a different block from the callee.
block := &state.module.functions.items[state.function].blocks.items[usize(state.block)]
start :: block.arguments.items.len
try arraylist.reserve(&block.arguments, start + count)
for (0..count) |i| try arraylist.append(&block.arguments, state.arguments.items[scratch_start + i])
return try emit(state, ir.Instruction{
op = .call,
type = type,
+272 -5
View File
@@ -422,6 +422,13 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
source ::
`text :: "owned\nstring"
`factor float :: 3.5
`enabled :: true or false and false
`compose proc(left bool, right bool) bool {
` return left and right
`}
`broken proc() bool {
` return compose(true and absent, false or true)
`}
`identity proc(value string) string {
` if false {
` return value
@@ -432,12 +439,12 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
`main proc() string {
` computed float :: factor * 2
` remaining := 2
` while remaining > 0 {
` while compose(remaining > 0 and enabled, compose(!false, compose(true or false, compose(true, compose(true, true))))) {
` pass :: identity(text)
` identity(pass)
` remaining = remaining - 1
` }
` if computed > 0 {
` if computed > 0 and (enabled or false) {
` saved :: identity(text)
` if computed < 10 {
` return saved
@@ -459,7 +466,7 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
defer typecheck.deinit(&checked)
succeeded := false
failures := usize(0)
for (usize(0)..usize(128)) |limit| {
for (usize(0)..usize(512)) |limit| {
state := AllocationState{ remaining = limit }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
module := lower(allocator, &tree, &resolution, &checked, &diagnostic_store) catch |error| {
@@ -856,8 +863,8 @@ loop_returns_do_not_guarantee_return_and_assignment_arithmetic_still_checks_over
bounded_pure_loops_do_not_allocate_per_iteration_and_release_runtime_storage test {
sources [2][]u8 :: [
"main proc() int {\n n := 0\n while n < 1 {\n local :: n\n n = local + 1\n }\n return n\n}\n",
"main proc() int {\n n := 0\n while n < 10000 {\n local :: n\n n = local + 1\n }\n return n\n}\n",
"main proc() int {\n n := 0\n while (n < 1 and !(n == 1)) or false {\n local :: n\n n = local + 1\n }\n return n\n}\n",
"main proc() int {\n n := 0\n while (n < 10000 and !(n == 10000)) or false {\n local :: n\n n = local + 1\n }\n return n\n}\n",
]
expected [2]i64 :: [1, 10000]
allocations := usize(0)
@@ -877,3 +884,263 @@ bounded_pure_loops_do_not_allocate_per_iteration_and_release_runtime_storage tes
else try testing.expect_equal(allocations, usize(64) - state.remaining)
}
}
boolean_truth_tables_nesting_negation_and_precedence_execute test {
sources [18][]u8 :: [
"main proc() bool { return false and false }\n",
"main proc() bool { return false and true }\n",
"main proc() bool { return true and false }\n",
"main proc() bool { return true and true }\n",
"main proc() bool { return false or false }\n",
"main proc() bool { return false or true }\n",
"main proc() bool { return true or false }\n",
"main proc() bool { return true or true }\n",
"main proc() bool { return !false }\n",
"main proc() bool { return !true }\n",
"main proc() bool { return !!true }\n",
"main proc() bool { return !!!true }\n",
"main proc() bool { return true or false and false }\n",
"main proc() bool { return (true or false) and false }\n",
"main proc() bool { return !(1 + 2 < 3) and 4 == 4 or false }\n",
"main proc() bool { return false or (true and (false or !false)) }\n",
"main proc() bool { return true and false or true and true }\n",
"main proc() bool { return !(-9223372036854775808 < 0) }\n",
]
expected [18]bool :: [
false, false, false, true, false, true, true, true,
true, false, true, false, true, false, true, true, true, false,
]
for (sources) |source, index| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect(outcome.trap == null)
try testing.expect_equal(ir.Type.bool, outcome.value.type)
try testing.expect_equal(expected[index], outcome.value.data.boolean)
}
}
branchful_calls_preserve_earlier_temps_nested_arguments_and_function_callees test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`integer proc(value int) int { return value }
`boolean proc(value bool) bool { return value }
`score proc(first int, left bool, right bool, last int) int {
` if left and right { return first + last }
` return 0
`}
`invoke :: score
`main proc() int {
` local :: invoke
` return local(integer(20), boolean(false or true), boolean(true and (false or true)), local(integer(0), false or boolean(true), true and !false, local(integer(5), true or false, true and true, integer(17))))
`}
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)
}
short_circuit_source_errors_remain_checked_but_skipped_in_every_consumer test {
sources [10][]u8 :: [
"main proc() bool { return false and absent or true }\n",
"main proc() bool { return true or 1 }\n",
"main proc() bool { return !(false and absent) }\n",
"main proc() bool { return (false and absent) == false }\n",
"flag :: true or absent\nmain proc() bool { return flag }\n",
"main proc() bool {\n flag :: true or absent\n return flag\n}\n",
"main proc() bool {\n flag := false\n flag = true or absent\n return flag\n}\n",
"identity proc(value bool) bool { return value }\nmain proc() bool { return identity(true or absent) }\n",
"main proc() bool {\n if true or absent { return true }\n return false\n}\n",
"main proc() bool {\n true or absent\n while false and absent {}\n return true\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)
count :: diagnostics.count(&diagnostic_store)
try testing.expect(count > 0)
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)
try testing.expect_equal(count, diagnostics.count(&diagnostic_store))
}
}
demanded_boolean_source_errors_keep_the_original_diagnostic_and_operand_span test {
sources [8][]u8 :: [
"main proc() bool { return true and absent }\n",
"main proc() bool { return false or absent }\n",
"main proc() bool { return true and 1 }\n",
"main proc() bool { return false or 1 }\n",
"main proc() bool { return !1 }\n",
"main proc() bool { return !absent }\n",
"main proc() bool { return absent and false }\n",
"main proc() bool { return absent or true }\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))?
code diagnostics.Code :: if (index >= 2 and index <= 4) .invalid_operand else .unknown_name
operand []u8 :: if (index >= 2 and index <= 4) "1" else "absent"
try testing.expect_equal(code, original.code)
try testing.expect(mem.eql(source[usize(original.span.start)..usize(original.span.end)], operand))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
}
}
branchful_call_arguments_evaluate_left_to_right_before_later_errors test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`both proc(left bool, right bool) bool { return left and right }
`main proc() bool {
` return both(false or (true and first_absent), true and second_absent)
`}
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
original :: diagnostics.get(&diagnostic_store, outcome.trap?)?
try testing.expect_equal(diagnostics.Code.unknown_name, original.code)
try testing.expect(mem.eql(source[usize(original.span.start)..usize(original.span.end)], "first_absent"))
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
}
short_circuit_protects_uninitialized_reads_global_cycles_and_overflow test {
sources [6][]u8 :: [
"main proc() bool {\n before :: false and later\n later :: true\n return !before\n}\n",
"main proc() bool {\n before :: true or later\n later :: false\n return before\n}\n",
"flag bool :: true or flag\nmain proc() bool { return flag }\n",
"left bool :: false and right\nright bool :: left\nmain proc() bool { return !left }\n",
"main proc() bool { return !(false and 9223372036854775807 + 1 == 0) }\n",
"main proc() bool { return true or -(-9223372036854775808) == 0 }\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(outcome.value.data.boolean)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
}
demanded_boolean_runtime_errors_keep_the_instruction_span test {
sources [4][]u8 :: [
"main proc() bool {\n before :: true and later\n later :: true\n return before\n}\n",
"flag bool :: false or flag\nmain proc() bool { return flag }\n",
"main proc() bool { return true and 9223372036854775807 + 1 == 0 }\n",
"main proc() bool {\n minimum :: -9223372036854775808\n return false or -minimum == 0\n}\n",
]
codes [4]diagnostics.Code :: [.read_before_initialization, .initialization_cycle, .integer_overflow, .integer_overflow]
spans [4][]u8 :: ["later", "flag", "9223372036854775807 + 1", "-minimum"]
for (sources) |source, index| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try expect_trap(outcome, &diagnostic_store, codes[index])
diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)?
try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], spans[index]))
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
}
}
composed_loop_guards_recompute_and_overwrite_synthetic_results test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`enabled :: false or true and !false
`positive proc(n int) bool { return n > 0 }
`main proc() int {
` n := 4
` total := 0
` while (enabled and positive(n)) or n > 4 {
` if (n == 4 or n == 2) and !(n == 3) {
` total = total + 21
` }
` n = n - 1
` }
` return total + n
`}
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)
}
lazy_branchful_global_initializers_and_calls_release_every_runtime_allocation test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`first :: true and later
`later :: false or !false
`both proc(left bool, right bool) bool { return left and right }
`main proc() bool {
` return both(first and first, both(false or later, !false and later))
`}
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
failures := usize(0)
succeeded := false
for (usize(0)..usize(64)) |limit| {
state := AllocationState{ remaining = limit }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
outcome :: interpreter.run(allocator, &module, &diagnostic_store) catch |error| {
try testing.expect(error == .out_of_memory)
failures += 1
try testing.expect_equal(usize(0), state.live)
continue
}
try testing.expect(outcome.trap == null)
try testing.expect(outcome.value.data.boolean)
try testing.expect_equal(usize(0), state.live)
succeeded = true
break
}
try testing.expect(succeeded)
try testing.expect(failures > 0)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
skipped_rhs_does_not_hide_a_demanded_outer_type_mismatch test {
sources [3][]u8 :: [
"main proc() int { return true or absent }\n",
"main proc() void {\n number := 0\n number = true or absent\n}\n",
"integer proc(value int) int { return value }\nmain proc() int { return integer(true or absent) }\n",
]
codes [3]diagnostics.Code :: [.return_type_mismatch, .type_mismatch, .type_mismatch]
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)
count :: diagnostics.count(&diagnostic_store)
try testing.expect_equal(2, count)
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
try expect_trap(outcome, &diagnostic_store, codes[index])
try testing.expect_equal(count, diagnostics.count(&diagnostic_store))
}
}