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
+19 -4
View File
@@ -185,7 +185,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
.if: return try parse_if(state)
.while: return try parse_while(state, allow_else)
.return: return try parse_return(state, allow_else)
.ident, .int, .float, .string, .true, .false, .minus, .open_paren: {
.ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren: {
expression :: try parse_expr(state)
return try ast.add_expression_stmt(&state.builder, expression)
}
@@ -418,10 +418,11 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
state.next_token += 1
operand :: try parse_expr_bp(state, right_bp)
operand_span :: ast.builder_span(&state.builder, operand)
operator ast.UnaryOperator :: if (token.kind == .bang) .logical_not else .negate
yield try ast.add_unary(
&state.builder,
ast.Span{ start = token.span.start, end = operand_span.end },
.negate,
operator,
operand,
)
} else {
@@ -525,7 +526,7 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
else
try diagnostics.add(state.diagnostics, .unexpected_token, token.span)
if token.kind != .comma and !is_expression_boundary(token.kind) {
if !is_expression_recovery_boundary(token.kind) {
state.next_token += 1
}
@@ -542,6 +543,16 @@ is_expression_boundary proc(kind lexer.TokenKind) bool {
}
}
@hide:file
is_expression_recovery_boundary proc(kind lexer.TokenKind) bool {
return (
kind == .comma
or kind == .and
or kind == .or
or is_expression_boundary(kind)
)
}
@hide:file
recover_decl proc(
state @mut State,
@@ -625,7 +636,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(50)
.minus, .bang: BindingPowerScalar(50)
else: null
}
}
@@ -633,6 +644,8 @@ prefix_binding_power proc(kind lexer.TokenKind) ?BindingPowerScalar {
@hide:file
infix_binding_power proc(kind lexer.TokenKind) ?BindingPower {
return match kind {
.or: BindingPower{ left = BindingPowerScalar(2), right = BindingPowerScalar(3) }
.and: BindingPower{ left = BindingPowerScalar(4), right = BindingPowerScalar(5) }
.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) }
@@ -644,6 +657,8 @@ infix_binding_power proc(kind lexer.TokenKind) ?BindingPower {
@hide:file
binary_operator proc(kind lexer.TokenKind) ast.BinaryOperator {
return match kind {
.and: .logical_and
.or: .logical_or
.plus: .add
.minus: .subtract
.star: .multiply
+160
View File
@@ -739,3 +739,163 @@ recovers_nested_braces_in_malformed_loop_body_and_assignment test {
tail :: ast.statement_decl(&tree, ast.package_decl(&tree, 1))
try testing.expect_equal("tail", ast.source_text(&tree, tail.name))
}
parses_logical_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 :: !probe() == a + b * c < d and e and f or g or h\n"
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(0, diagnostics.count(&diagnostic_store))
value :: ast.statement_decl(&tree, ast.package_decl(&tree, 0))
outer_or :: ast.binary_expr(&tree, value.initializer)
try testing.expect_equal(ast.BinaryOperator.logical_or, outer_or.operator)
try testing.expect_equal("h", ast.source_text(&tree, ast.node_span(&tree, outer_or.right)))
inner_or :: ast.binary_expr(&tree, outer_or.left)
try testing.expect_equal(ast.BinaryOperator.logical_or, inner_or.operator)
outer_and :: ast.binary_expr(&tree, inner_or.left)
try testing.expect_equal(ast.BinaryOperator.logical_and, outer_and.operator)
try testing.expect_equal("f", ast.source_text(&tree, ast.node_span(&tree, outer_and.right)))
inner_and :: ast.binary_expr(&tree, outer_and.left)
try testing.expect_equal(ast.BinaryOperator.logical_and, inner_and.operator)
equality :: ast.binary_expr(&tree, inner_and.left)
try testing.expect_equal(ast.BinaryOperator.equal, equality.operator)
logical_not :: ast.unary_expr(&tree, equality.left)
try testing.expect_equal(ast.UnaryOperator.logical_not, logical_not.operator)
try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, logical_not.operand))
try testing.expect_equal("!probe()", ast.source_text(&tree, logical_not.span))
comparison :: ast.binary_expr(&tree, equality.right)
try testing.expect_equal(ast.BinaryOperator.less, comparison.operator)
addition :: ast.binary_expr(&tree, comparison.left)
try testing.expect_equal(ast.BinaryOperator.add, addition.operator)
multiply :: ast.binary_expr(&tree, addition.right)
try testing.expect_equal(ast.BinaryOperator.multiply, multiply.operator)
}
parses_right_associative_mixed_prefix_operators 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 :: !-!probe()\nother :: -!false\n"
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(0, diagnostics.count(&diagnostic_store))
value :: ast.statement_decl(&tree, ast.package_decl(&tree, 0))
first :: ast.unary_expr(&tree, value.initializer)
second :: ast.unary_expr(&tree, first.operand)
third :: ast.unary_expr(&tree, second.operand)
try testing.expect_equal(ast.UnaryOperator.logical_not, first.operator)
try testing.expect_equal(ast.UnaryOperator.negate, second.operator)
try testing.expect_equal(ast.UnaryOperator.logical_not, third.operator)
try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, third.operand))
try testing.expect_equal("!-!probe()", ast.source_text(&tree, first.span))
try testing.expect_equal("-!probe()", ast.source_text(&tree, second.span))
other :: ast.statement_decl(&tree, ast.package_decl(&tree, 1))
negate :: ast.unary_expr(&tree, other.initializer)
try testing.expect_equal(ast.UnaryOperator.negate, negate.operator)
try testing.expect_equal(ast.UnaryOperator.logical_not, ast.unary_expr(&tree, negate.operand).operator)
}
parses_logical_expressions_in_all_expression_positions 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 ::
`global :: false and absent
`main proc() bool {
` value := true or absent
` value = !value and ready()
` !value
` if !value or ready() { value = false }
` while value and !done() { value = false }
` return choose(value or false, nested(!value and true))
`}
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(0, diagnostics.count(&diagnostic_store))
global :: ast.statement_decl(&tree, ast.package_decl(&tree, 0))
global_and :: ast.binary_expr(&tree, global.initializer)
try testing.expect_equal(ast.BinaryOperator.logical_and, global_and.operator)
try testing.expect_equal("absent", ast.source_text(&tree, ast.node_span(&tree, global_and.right)))
function :: ast.function_decl(&tree, ast.package_decl(&tree, 1))
try testing.expect_equal(6, ast.block_statement_count(&tree, function.body))
local :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 0))
try testing.expect_equal(ast.BinaryOperator.logical_or, ast.binary_expr(&tree, local.initializer).operator)
assignment :: ast.assignment_stmt(&tree, ast.block_statement(&tree, function.body, 1))
try testing.expect_equal(ast.BinaryOperator.logical_and, ast.binary_expr(&tree, assignment.value).operator)
expression :: ast.statement_expr(&tree, ast.block_statement(&tree, function.body, 2))
try testing.expect_equal(ast.UnaryOperator.logical_not, ast.unary_expr(&tree, expression).operator)
conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, function.body, 3))
try testing.expect_equal(ast.BinaryOperator.logical_or, ast.binary_expr(&tree, conditional.condition).operator)
loop :: ast.while_stmt(&tree, ast.block_statement(&tree, function.body, 4))
try testing.expect_equal(ast.BinaryOperator.logical_and, ast.binary_expr(&tree, loop.condition).operator)
returned :: ast.return_stmt(&tree, ast.block_statement(&tree, function.body, 5)).value?
try testing.expect_equal(2, ast.call_arg_count(&tree, returned))
first_argument :: ast.call_arg(&tree, returned, 0)
try testing.expect_equal(ast.BinaryOperator.logical_or, ast.binary_expr(&tree, first_argument).operator)
nested :: ast.call_arg(&tree, returned, 1)
nested_argument :: ast.call_arg(&tree, nested, 0)
try testing.expect_equal(ast.BinaryOperator.logical_and, ast.binary_expr(&tree, nested_argument).operator)
}
recovers_missing_logical_operands_and_preserves_enclosing_nodes 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() bool {
` first :: true and
` second :: true and or false
` third :: choose(!, false or)
` if ! { return false }
` return true or }
`after :: true
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(6, diagnostics.count(&diagnostic_store))
try testing.expect_equal(2, ast.package_decl_count(&tree))
function :: ast.function_decl(&tree, ast.package_decl(&tree, 0))
try testing.expect_equal(5, ast.block_statement_count(&tree, function.body))
first :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 0))
first_and :: ast.binary_expr(&tree, first.initializer)
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, first_and.right))
second :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 1))
second_or :: ast.binary_expr(&tree, second.initializer)
try testing.expect_equal(ast.BinaryOperator.logical_or, second_or.operator)
second_and :: ast.binary_expr(&tree, second_or.left)
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, second_and.right))
try testing.expect_equal(ast.ExprKind.literal_bool, ast.expr_kind(&tree, second_or.right))
third :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 2))
try testing.expect_equal(2, ast.call_arg_count(&tree, third.initializer))
missing_prefix :: ast.unary_expr(&tree, ast.call_arg(&tree, third.initializer, 0))
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, missing_prefix.operand))
missing_rhs :: ast.binary_expr(&tree, ast.call_arg(&tree, third.initializer, 1))
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, missing_rhs.right))
conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, function.body, 3))
try testing.expect_equal(1, ast.block_statement_count(&tree, conditional.then_block))
returned :: ast.return_stmt(&tree, ast.block_statement(&tree, function.body, 4)).value?
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, ast.binary_expr(&tree, returned).right))
after :: ast.statement_decl(&tree, ast.package_decl(&tree, 1))
try testing.expect_equal("after", ast.source_text(&tree, after.name))
}