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
+44 -8
View File
@@ -199,10 +199,13 @@ check proc(
id :: ast.NodeId(i)
if (cells[i].role != .expression or cells[i].assignment_value != assignment_values) continue
match ast.expr_kind(tree, id) {
.unary: try require_numeric(&state, id, ast.unary_expr(tree, id).operand)
.unary: {
expression :: ast.unary_expr(tree, id)
if (expression.operator == .negate) try require_numeric(&state, id, expression.operand)
}
.binary: {
expression :: ast.binary_expr(tree, id)
if (bool_equality(&state, id)) continue
if (logical(expression.operator) or bool_equality(&state, id)) continue
try require_numeric(&state, id, expression.left)
try require_numeric(&state, id, expression.right)
}
@@ -216,9 +219,13 @@ check proc(
id :: ast.NodeId(i)
if (cells[i].role != .expression or cells[i].assignment_value != assignment_values) continue
match ast.expr_kind(tree, id) {
.unary: try numeric_operand(&state, id, ast.unary_expr(tree, id).operand)
.unary: {
expression :: ast.unary_expr(tree, id)
if (expression.operator == .negate) try numeric_operand(&state, id, expression.operand)
}
.binary: {
expression :: ast.binary_expr(tree, id)
if (logical(expression.operator)) continue
if (comparison(expression.operator)) {
if (!bool_equality(&state, id)) try comparison_operands(&state, id)
} else {
@@ -599,14 +606,19 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
}
}
.unary: {
state.cells[usize(id)].numeric = true
operand :: ast.unary_expr(tree, id).operand
expression :: ast.unary_expr(tree, id)
if (expression.operator == .logical_not) {
state.cells[usize(id)].type = Type{ kind = .bool }
} else {
state.cells[usize(id)].numeric = true
}
operand :: expression.operand
try collect_expression(state, operand)
try depend(state, operand, id)
}
.binary: {
expression :: ast.binary_expr(tree, id)
if (comparison(expression.operator)) {
if (comparison(expression.operator) or logical(expression.operator)) {
state.cells[usize(id)].type = Type{ kind = .bool }
} else {
state.cells[usize(id)].numeric = true
@@ -614,7 +626,8 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
try collect_expression(state, expression.left)
try collect_expression(state, expression.right)
try depend(state, expression.left, id)
try depend(state, expression.right, id)
# the RHS may be skipped, so its errors do not erase the bool result.
if (!logical(expression.operator)) try depend(state, expression.right, id)
}
.call: {
callee :: ast.call_expr(tree, id).callee
@@ -643,6 +656,11 @@ comparison proc(operator ast.BinaryOperator) bool {
)
}
@hide:file
logical proc(operator ast.BinaryOperator) bool {
return operator == .logical_and or operator == .logical_or
}
@hide:file
bool_equality proc(state @mut State, id ast.NodeId) bool {
expression :: ast.binary_expr(state.tree, id)
@@ -722,6 +740,14 @@ fail proc(state @mut State, id ast.NodeId, code diagnostics.Code, span ast.Span)
state.types[usize(id)] = invalid_type(try diagnostics.add(state.diagnostics, code, span))
}
@hide:file
require_bool proc(state @mut State, operand ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
type :: state.types[usize(operand)]
if (type.kind != .invalid and type.kind != .bool) {
try fail(state, operand, .invalid_operand, ast.node_span(state.tree, operand))
}
}
@hide:file
mutable_target proc(state @State, id ast.NodeId) bool {
binding :: sema.binding(state.resolution, id)
@@ -793,13 +819,23 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
.expression: {
match ast.expr_kind(tree, id) {
.unary: {
operand :: state.types[usize(ast.unary_expr(tree, id).operand)]
expression :: ast.unary_expr(tree, id)
if (expression.operator == .logical_not) {
try require_bool(state, expression.operand)
return
}
operand :: state.types[usize(expression.operand)]
if (operand.kind != .invalid and !numeric_kind(operand.kind)) {
try fail(state, id, .invalid_operand, ast.node_span(tree, id))
}
}
.binary: {
expression :: ast.binary_expr(tree, id)
if (logical(expression.operator)) {
try require_bool(state, expression.left)
try require_bool(state, expression.right)
return
}
left :: state.types[usize(expression.left)]
right :: state.types[usize(expression.right)]
if (left.kind == .invalid or right.kind == .invalid) return
+299
View File
@@ -1249,3 +1249,302 @@ while_checks_nested_bodies_without_claiming_a_zero_iteration_return test {
try testing.expect_equal(1, code_count(&diagnostic_store, .condition_not_bool))
try testing.expect_equal(3, code_count(&diagnostic_store, .invalid_operand))
}
logical_types_preserve_comparison_context_and_assignment_isolation 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 ::
`seed :: 3
`forward :: seed
`composed :: !(forward < 4.0) and true or false
`equal :: !(true and false) == (true or false)
`nested :: !!!true
`identity proc(value bool) bool { return value }
`main proc() bool {
` count := 1
` flag := true
` flag = !(count < 2) and identity(true) or false
` return 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 (usize(0)..usize(2)) |index| try expect_kind(&result, ast.package_decl(&tree, index), .float)
for usize(2)..usize(5) |index| {
declaration :: ast.package_decl(&tree, index)
try expect_kind(&result, declaration, .bool)
try expect_kind(&result, ast.statement_decl(&tree, declaration).initializer, .bool)
}
composed :: ast.binary_expr(&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 2)).initializer)
try testing.expect_equal(ast.BinaryOperator.logical_or, composed.operator)
conjunction :: ast.binary_expr(&tree, composed.left)
try testing.expect_equal(ast.BinaryOperator.logical_and, conjunction.operator)
inverted :: ast.unary_expr(&tree, conjunction.left)
try expect_kind(&result, conjunction.left, .bool)
comparison_view :: ast.binary_expr(&tree, inverted.operand)
try expect_kind(&result, comparison_view.left, .float)
try expect_kind(&result, comparison_view.right, .float)
nested := ast.statement_decl(&tree, ast.package_decl(&tree, 4)).initializer
for 0..3 |_| {
try expect_kind(&result, nested, .bool)
view :: ast.unary_expr(&tree, nested)
try testing.expect_equal(ast.UnaryOperator.logical_not, view.operator)
nested = view.operand
}
try expect_kind(&result, nested, .bool)
body :: ast.function_decl(&tree, ast.package_decl(&tree, 6)).body
count :: ast.block_statement(&tree, body, 0)
try expect_kind(&result, count, .int)
assignment :: ast.assignment_stmt(&tree, ast.block_statement(&tree, body, 2))
try expect_kind(&result, assignment.value, .bool)
disjunction :: ast.binary_expr(&tree, assignment.value)
assignment_conjunction :: ast.binary_expr(&tree, disjunction.left)
assignment_not :: ast.unary_expr(&tree, assignment_conjunction.left)
assignment_comparison :: ast.binary_expr(&tree, assignment_not.operand)
try expect_kind(&result, assignment_comparison.left, .int)
try expect_kind(&result, assignment_comparison.right, .int)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
logical_operand_errors_are_independent_and_skipped_rhs_preserves_bool 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 ::
`number :: 1
`number_alias :: number
`skipped_unknown :: false and absent
`skipped_kind :: true or number
`both_bad :: 1 and "wrong"
`demanded_unknown :: missing or true
`demanded_not :: !number_alias
`nested :: !(false and absent_nested) == true
`independent :: left_missing and 1
`identity proc(value bool) bool { return value }
`skips proc() bool {
` local :: true or 1
` value := false
` value = false and absent_assignment
` identity(true or absent_argument)
` if true or 1 { value = true }
` while false and 1 { value = false }
` return false and absent_return
`}
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 usize(0)..usize(2) |index| {
declaration :: ast.package_decl(&tree, index)
try expect_kind(&result, declaration, .int)
try expect_kind(&result, ast.statement_decl(&tree, declaration).initializer, .int)
}
skipped_indices [3]usize :: [2, 3, 7]
for skipped_indices |index| {
declaration :: ast.package_decl(&tree, index)
try expect_kind(&result, declaration, .bool)
try expect_kind(&result, ast.statement_decl(&tree, declaration).initializer, .bool)
}
unknown :: ast.binary_expr(&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 2)).initializer)
try testing.expect_equal(sema.binding(&resolution, unknown.right).data.diagnostic, type_of(&result, unknown.right).diagnostic)
_ = try expect_error(&result, &diagnostic_store, unknown.right, .unknown_name, ast.node_span(&tree, unknown.right))
wrong :: ast.binary_expr(&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 3)).initializer)
_ = try expect_error(&result, &diagnostic_store, wrong.right, .invalid_operand, ast.node_span(&tree, wrong.right))
both_declaration :: ast.package_decl(&tree, 4)
both_id :: ast.statement_decl(&tree, both_declaration).initializer
both :: ast.binary_expr(&tree, both_id)
left_error :: try expect_error(&result, &diagnostic_store, both.left, .invalid_operand, ast.node_span(&tree, both.left))
right_error :: try expect_error(&result, &diagnostic_store, both.right, .invalid_operand, ast.node_span(&tree, both.right))
try testing.expect(left_error != right_error)
try testing.expect_equal(left_error, type_of(&result, both_id).diagnostic)
try testing.expect_equal(left_error, type_of(&result, both_declaration).diagnostic)
demanded_id :: ast.statement_decl(&tree, ast.package_decl(&tree, 5)).initializer
demanded :: ast.binary_expr(&tree, demanded_id)
try testing.expect_equal(sema.binding(&resolution, demanded.left).data.diagnostic, type_of(&result, demanded_id).diagnostic)
not_id :: ast.statement_decl(&tree, ast.package_decl(&tree, 6)).initializer
not_view :: ast.unary_expr(&tree, not_id)
not_error :: try expect_error(&result, &diagnostic_store, not_view.operand, .invalid_operand, ast.node_span(&tree, not_view.operand))
try testing.expect_equal(not_error, type_of(&result, not_id).diagnostic)
independent_id :: ast.statement_decl(&tree, ast.package_decl(&tree, 8)).initializer
independent :: ast.binary_expr(&tree, independent_id)
try testing.expect_equal(sema.binding(&resolution, independent.left).data.diagnostic, type_of(&result, independent_id).diagnostic)
_ = try expect_error(&result, &diagnostic_store, independent.right, .invalid_operand, ast.node_span(&tree, independent.right))
body :: ast.function_decl(&tree, ast.package_decl(&tree, 10)).body
try expect_kind(&result, body, .none)
try expect_kind(&result, ast.block_statement(&tree, body, 0), .bool)
assignment_id :: ast.block_statement(&tree, body, 2)
assignment :: ast.assignment_stmt(&tree, assignment_id)
try expect_kind(&result, assignment_id, .void)
try expect_kind(&result, assignment.value, .bool)
call :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 3))
try expect_kind(&result, call, .bool)
try expect_kind(&result, ast.call_arg(&tree, call, 0), .bool)
conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 4))
loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 5))
try expect_kind(&result, conditional.condition, .bool)
try expect_kind(&result, loop.condition, .bool)
returned :: ast.block_statement(&tree, body, 6)
try expect_kind(&result, returned, .void)
if (ast.return_stmt(&tree, returned).value) |value| {
try expect_kind(&result, value, .bool)
} else {
return .expectation_failed
}
try testing.expect_equal(7, code_count(&diagnostic_store, .unknown_name))
try testing.expect_equal(8, code_count(&diagnostic_store, .invalid_operand))
try testing.expect_equal(15, diagnostics.count(&diagnostic_store))
}
logical_external_mismatches_and_always_demanded_not_preserve_errors 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 ::
`accept_bool proc(value bool) bool { return value }
`accept_int proc(value int) bool { return true }
`noop proc() void {}
`wrong_decl int :: true or 1
`wrong_return proc() int { return false and absent_return }
`wrong_argument proc() bool { return accept_int(false and absent_argument) }
`wrong_assignment proc() void {
` count := 1
` count = true or 1
`}
`unary_bad proc() void {
` !1.0
` !"text"
` !accept_bool
` !noop()
`}
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)
declaration :: ast.package_decl(&tree, 3)
initializer :: ast.statement_decl(&tree, declaration).initializer
declaration_error :: try expect_error(&result, &diagnostic_store, initializer, .type_mismatch, ast.node_span(&tree, initializer))
try testing.expect_equal(declaration_error, type_of(&result, declaration).diagnostic)
return_body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body
return_id :: ast.block_statement(&tree, return_body, 0)
if (ast.return_stmt(&tree, return_id).value) |value| {
return_error :: try expect_error(&result, &diagnostic_store, value, .return_type_mismatch, ast.node_span(&tree, value))
try testing.expect_equal(return_error, type_of(&result, return_id).diagnostic)
operand :: ast.binary_expr(&tree, value).right
try testing.expect_equal(sema.binding(&resolution, operand).data.diagnostic, type_of(&result, operand).diagnostic)
try testing.expect(return_error != type_of(&result, operand).diagnostic)
} else {
return .expectation_failed
}
argument_body :: ast.function_decl(&tree, ast.package_decl(&tree, 5)).body
argument_return :: ast.block_statement(&tree, argument_body, 0)
if (ast.return_stmt(&tree, argument_return).value) |call| {
argument :: ast.call_arg(&tree, call, 0)
argument_error :: try expect_error(&result, &diagnostic_store, argument, .type_mismatch, ast.node_span(&tree, argument))
try testing.expect_equal(argument_error, type_of(&result, call).diagnostic)
try testing.expect_equal(argument_error, type_of(&result, argument_return).diagnostic)
} else {
return .expectation_failed
}
assignment_body :: ast.function_decl(&tree, ast.package_decl(&tree, 6)).body
assignment_id :: ast.block_statement(&tree, assignment_body, 1)
assignment :: ast.assignment_stmt(&tree, assignment_id)
assignment_error :: try expect_error(&result, &diagnostic_store, assignment.value, .type_mismatch, ast.node_span(&tree, assignment.value))
try testing.expect_equal(assignment_error, type_of(&result, assignment_id).diagnostic)
try expect_kind(&result, assignment.target, .int)
try expect_kind(&result, ast.block_statement(&tree, assignment_body, 0), .int)
unary_body :: ast.function_decl(&tree, ast.package_decl(&tree, 7)).body
for usize(0)..usize(4) |index| {
expression :: ast.statement_expr(&tree, ast.block_statement(&tree, unary_body, index))
operand :: ast.unary_expr(&tree, expression).operand
original :: try expect_error(&result, &diagnostic_store, operand, .invalid_operand, ast.node_span(&tree, operand))
try testing.expect_equal(original, type_of(&result, expression).diagnostic)
}
for (usize(0)..usize(3)) |index| try expect_kind(&result, ast.package_decl(&tree, index), .function)
try testing.expect_equal(6, code_count(&diagnostic_store, .invalid_operand))
try testing.expect_equal(2, code_count(&diagnostic_store, .unknown_name))
try testing.expect_equal(3, code_count(&diagnostic_store, .type_mismatch))
try testing.expect_equal(1, code_count(&diagnostic_store, .return_type_mismatch))
try testing.expect_equal(12, diagnostics.count(&diagnostic_store))
}
short_circuit_skip_paths_do_not_guarantee_a_return 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 ::
`expression proc() int { false and missing_expression }
`declaration proc() int { local :: true or 1 }
`assignment proc() int {
` value := false
` value = false and missing_assignment
`}
`call proc() int { take(true or 1) }
`conditional proc() int { if false and missing_condition { return 1 } }
`loop proc() int { while true or missing_guard { return 1 } }
`loop_body proc() int { while true { true or 1 } }
`arms proc(flag bool) int {
` if flag { false and missing_arm } else { return 1 }
`}
`demanded proc() int { missing_left or true }
`successful proc() bool { return true or missing_return }
`take proc(value bool) void {}
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 usize(0)..usize(8) |index| {
function :: ast.package_decl(&tree, index)
try expect_kind(&result, function, .function)
body :: ast.function_decl(&tree, function).body
_ = try expect_error(&result, &diagnostic_store, body, .missing_return, ast.node_span(&tree, body))
}
expression_body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
expression :: ast.statement_expr(&tree, ast.block_statement(&tree, expression_body, 0))
try expect_kind(&result, expression, .bool)
loop_body :: ast.function_decl(&tree, ast.package_decl(&tree, 6)).body
loop_id :: ast.block_statement(&tree, loop_body, 0)
try expect_kind(&result, loop_id, .void)
demanded_body :: ast.function_decl(&tree, ast.package_decl(&tree, 8)).body
demanded :: ast.statement_expr(&tree, ast.block_statement(&tree, demanded_body, 0))
left :: ast.binary_expr(&tree, demanded).left
original :: sema.binding(&resolution, left).data.diagnostic
try testing.expect_equal(original, type_of(&result, demanded).diagnostic)
try testing.expect_equal(original, type_of(&result, demanded_body).diagnostic)
successful_body :: ast.function_decl(&tree, ast.package_decl(&tree, 9)).body
try expect_kind(&result, successful_body, .none)
try expect_kind(&result, ast.block_statement(&tree, successful_body, 0), .void)
try testing.expect_equal(8, code_count(&diagnostic_store, .missing_return))
try testing.expect_equal(7, code_count(&diagnostic_store, .unknown_name))
try testing.expect_equal(3, code_count(&diagnostic_store, .invalid_operand))
try testing.expect_equal(18, diagnostics.count(&diagnostic_store))
}