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
+147 -10
View File
@@ -143,7 +143,7 @@ parse_block proc(state @mut State) ast.NodeId ! ParseError {
skip_newlines(state)
while !next_is(state, .close_curly) and !next_is(state, .eof) {
start :: state.next_token
statement :: parse_statement(state) catch |err| {
statement :: parse_statement(state, false) catch |err| {
match err {
.unexpected_token: {
recovered :: try recover_statement(state, start)
@@ -167,7 +167,7 @@ parse_block proc(state @mut State) ast.NodeId ! ParseError {
}
@hide:file
parse_statement proc(state @mut State) ast.NodeId ! ParseError {
parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
if (
next_is(state, .ident)
and (
@@ -179,8 +179,9 @@ parse_statement proc(state @mut State) ast.NodeId ! ParseError {
return try parse_statement_decl(state)
}
match current(state).kind {
.return: return try parse_return(state)
.ident, .int, .float, .string, .minus, .open_paren: {
.if: return try parse_if(state)
.return: return try parse_return(state, allow_else)
.ident, .int, .float, .string, .true, .false, .minus, .open_paren: {
expression :: try parse_expr(state)
return try ast.add_expression_stmt(&state.builder, expression)
}
@@ -189,13 +190,19 @@ parse_statement proc(state @mut State) ast.NodeId ! ParseError {
}
@hide:file
parse_return proc(state @mut State) ast.NodeId ! ParseError {
parse_return proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
keyword :: try consume(state, .return)
if (next_is(state, .newline) or next_is(state, .close_curly) or next_is(state, .eof)) {
if (
next_is(state, .newline) or next_is(state, .close_curly) or next_is(state, .eof)
or (allow_else and next_is(state, .else))
) {
return try ast.add_return_stmt(&state.builder, keyword.span, null)
}
value :: try parse_expr(state)
if (!next_is(state, .newline) and !next_is(state, .close_curly) and !next_is(state, .eof)) {
if (
!next_is(state, .newline) and !next_is(state, .close_curly) and !next_is(state, .eof)
and !(allow_else and next_is(state, .else))
) {
return .unexpected_token
}
# include consumed grouping punctuation, which has no separate expression node.
@@ -207,6 +214,116 @@ parse_return proc(state @mut State) ast.NodeId ! ParseError {
)
}
@hide:file
parse_if proc(state @mut State) ast.NodeId ! ParseError {
keyword :: try consume(state, .if)
condition_start :: state.next_token
condition := parse_expr(state) catch |err| {
match err {
.unexpected_token: {}
else: return err
}
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
yield try ast.add_invalid_expr(&state.builder, current(state).span, diagnostic)
}
if (ast.builder_expr_kind(&state.builder, condition) == .invalid) {
# discard a malformed guard's tail, but leave its arm and enclosing block intact.
while (
!next_is(state, .open_curly) and !next_is(state, .close_curly)
and !next_is(state, .newline) and !next_is(state, .eof)
and !next_is(state, .else) and !next_is(state, .return) and !next_is(state, .if)
) {
state.next_token += 1
}
}
if (
!next_is(state, .open_curly)
and !parenthesized_guard(state, condition_start)
and ast.builder_expr_kind(&state.builder, condition) != .call
and ast.builder_expr_kind(&state.builder, condition) != .invalid
) {
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
condition = try ast.add_invalid_expr(
&state.builder,
ast.builder_span(&state.builder, condition),
diagnostic,
)
}
braced :: next_is(state, .open_curly)
then_block :: try parse_arm(state)
# only a braced arm permits its else on a later line.
after_then :: state.next_token
if (braced) skip_newlines(state)
alternative ?ast.NodeId := null
if next_is(state, .else) {
state.next_token += 1
alternative = try parse_arm(state)
} else {
state.next_token = after_then
}
end :: if (alternative) |block|
ast.builder_span(&state.builder, block).end
else
ast.builder_span(&state.builder, then_block).end
return try ast.add_if_stmt(
&state.builder,
ast.Span{ start = keyword.span.start, end = end },
condition,
then_block,
alternative,
)
}
@hide:file
parenthesized_guard proc(state @State, start usize) bool {
if (state.tokens[start].kind != .open_paren) return false
depth usize := 0
for start..state.next_token |index| {
kind :: state.tokens[index].kind
if (kind == .open_paren) depth += 1
if (kind == .close_paren) {
depth -= 1
if (depth == 0) return index + 1 == state.next_token
}
}
return false
}
@hide:file
parse_arm proc(state @mut State) ast.NodeId ! ParseError {
if next_is(state, .open_curly) return try parse_block(state)
start :: state.next_token
statement :: parse_statement(state, true) catch |err| {
match err {
.unexpected_token: {}
else: return err
}
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
span :: synchronize_arm(state, start)
yield try ast.add_invalid_stmt(&state.builder, span, diagnostic)
}
statements [1]ast.NodeId :: [statement]
return try ast.add_block(
&state.builder,
ast.builder_span(&state.builder, statement),
statements[..],
)
}
@hide:file
synchronize_arm proc(state @mut State, start usize) ast.Span {
first :: state.tokens[start].span
end := first.end
while (
!next_is(state, .eof) and !next_is(state, .newline)
and !next_is(state, .close_curly) and !next_is(state, .else)
) {
end = current(state).span.end
state.next_token += 1
}
return ast.Span{ start = first.start, end = end }
}
@hide:file
parse_statement_decl proc(state @mut State) ast.NodeId ! ParseError {
name :: try consume(state, .ident)
@@ -295,6 +412,10 @@ parse_call proc(state @mut State, callee ast.NodeId) ast.NodeId ! ParseError {
!next_is(state, .close_paren)
and !next_is(state, .newline)
and !next_is(state, .close_curly)
and !next_is(state, .open_curly)
and !next_is(state, .else)
and !next_is(state, .return)
and !next_is(state, .if)
and !next_is(state, .eof)
) {
argument :: try parse_expr(state)
@@ -347,6 +468,10 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
state.next_token += 1
return try ast.add_literal(&state.builder, .string, token.span)
}
.true, .false: {
state.next_token += 1
return try ast.add_literal(&state.builder, .bool, token.span)
}
.ident: {
state.next_token += 1
return try ast.add_identifier(&state.builder, token.span)
@@ -362,6 +487,10 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
and token.kind != .comma
and token.kind != .close_paren
and token.kind != .close_curly
and token.kind != .open_curly
and token.kind != .else
and token.kind != .return
and token.kind != .if
and token.kind != .eof
) {
state.next_token += 1
@@ -451,7 +580,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(30)
.minus: BindingPowerScalar(50)
else: null
}
}
@@ -459,8 +588,10 @@ prefix_binding_power proc(kind lexer.TokenKind) ?BindingPowerScalar {
@hide:file
infix_binding_power proc(kind lexer.TokenKind) ?BindingPower {
return match kind {
.plus, .minus: BindingPower{ left = BindingPowerScalar(10), right = BindingPowerScalar(11) }
.star, .slash: BindingPower{ left = BindingPowerScalar(20), right = BindingPowerScalar(21) }
.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) }
.star, .slash: BindingPower{ left = BindingPowerScalar(40), right = BindingPowerScalar(41) }
else: null
}
}
@@ -472,6 +603,12 @@ binary_operator proc(kind lexer.TokenKind) ast.BinaryOperator {
.minus: .subtract
.star: .multiply
.slash: .divide
.equal_equal: .equal
.not_equal: .not_equal
.less: .less
.less_equal: .less_equal
.greater: .greater
.greater_equal: .greater_equal
else: unreachable
}
}
+180
View File
@@ -330,3 +330,183 @@ parses_return_boundaries_and_recovers_trailing_tokens test {
try testing.expect_equal("return", ast.source_text(&tree, last.span))
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
}
parses_boolean_comparison_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 :: -f() * 2 + 3 < 4 <= 5 > 6 >= 7 == true != false
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)
different :: ast.binary_expr(&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 0)).initializer)
try testing.expect_equal(ast.BinaryOperator.not_equal, different.operator)
try testing.expect_equal(ast.ExprKind.literal_bool, ast.expr_kind(&tree, different.right))
try testing.expect_equal("false", ast.source_text(&tree, ast.node_span(&tree, different.right)))
equal :: ast.binary_expr(&tree, different.left)
try testing.expect_equal(ast.BinaryOperator.equal, equal.operator)
try testing.expect_equal(ast.ExprKind.literal_bool, ast.expr_kind(&tree, equal.right))
relation := equal.left
operators [4]ast.BinaryOperator :: [.greater_equal, .greater, .less_equal, .less]
for (operators) |operator| {
binary :: ast.binary_expr(&tree, relation)
try testing.expect_equal(operator, binary.operator)
relation = binary.left
}
addition :: ast.binary_expr(&tree, relation)
try testing.expect_equal(ast.BinaryOperator.add, addition.operator)
product :: ast.binary_expr(&tree, addition.left)
try testing.expect_equal(ast.BinaryOperator.multiply, product.operator)
negate :: ast.unary_expr(&tree, product.left)
try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, negate.operand))
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
parses_factorial_and_canonical_conditional_arms 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 ::
`factorial proc(n int) int {
` if (n <= 1) return 1 else return n * factorial(n - 1)
`}
`main proc() int {
` if true {
` local :: factorial(5)
` if (false) return 0 else return local
` }
` else if ready() return 2 else { return 3 }
`}
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)
factorial :: ast.function_decl(&tree, ast.package_decl(&tree, 0))
try testing.expect_equal(1, ast.block_statement_count(&tree, factorial.body))
guard :: ast.if_stmt(&tree, ast.block_statement(&tree, factorial.body, 0))
try testing.expect_equal("n <= 1", ast.source_text(&tree, ast.node_span(&tree, guard.condition)))
try testing.expect_equal(1, ast.block_statement_count(&tree, guard.then_block))
try testing.expect_equal("return 1", ast.source_text(&tree, ast.return_stmt(&tree, ast.block_statement(&tree, guard.then_block, 0)).span))
try testing.expect_equal(1, ast.block_statement_count(&tree, guard.else_block?))
try testing.expect_equal(ast.StmtKind.return_stmt, ast.stmt_kind(&tree, ast.block_statement(&tree, guard.else_block?, 0)))
main :: ast.function_decl(&tree, ast.package_decl(&tree, 1))
outer :: ast.if_stmt(&tree, ast.block_statement(&tree, main.body, 0))
try testing.expect_equal(2, ast.block_statement_count(&tree, outer.then_block))
try testing.expect_equal(ast.StmtKind.declaration, ast.stmt_kind(&tree, ast.block_statement(&tree, outer.then_block, 0)))
nested :: ast.if_stmt(&tree, ast.block_statement(&tree, outer.then_block, 1))
try testing.expect(nested.else_block != null)
try testing.expect_equal(1, ast.block_statement_count(&tree, outer.else_block?))
chain :: ast.if_stmt(&tree, ast.block_statement(&tree, outer.else_block?, 0))
try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, chain.condition))
try testing.expect(chain.else_block != null)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
attaches_dangling_else_to_nearest_if 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() int { if (true) if (false) return 1 else return 2 else return 3 }"
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)
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
outer :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 0))
inner :: ast.if_stmt(&tree, ast.block_statement(&tree, outer.then_block, 0))
inner_return :: ast.return_stmt(&tree, ast.block_statement(&tree, inner.else_block?, 0))
outer_return :: ast.return_stmt(&tree, ast.block_statement(&tree, outer.else_block?, 0))
try testing.expect_equal("2", ast.source_text(&tree, ast.node_span(&tree, inner_return.value?)))
try testing.expect_equal("3", ast.source_text(&tree, ast.node_span(&tree, outer_return.value?)))
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
}
recovers_conditional_conditions_and_arms_without_losing_siblings 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() void {
` if { good() } else { other() }
` if (true) else return
` if broken( { kept() }
` after :: 7
` return 1 else return 2
` final :: 8
`}
`tail :: 9
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(2, ast.package_decl_count(&tree))
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
try testing.expect_equal(6, ast.block_statement_count(&tree, body))
first :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 0))
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, first.condition))
condition_diagnostic :: diagnostics.get(&diagnostic_store, ast.invalid(&tree, first.condition).diagnostic)?
try testing.expect_equal("{", ast.source_text(&tree, condition_diagnostic.span))
try testing.expect_equal(1, ast.block_statement_count(&tree, first.then_block))
second :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 1))
bad_arm :: ast.block_statement(&tree, second.then_block, 0)
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, bad_arm))
arm_diagnostic :: diagnostics.get(&diagnostic_store, ast.invalid(&tree, bad_arm).diagnostic)?
try testing.expect_equal("else", ast.source_text(&tree, arm_diagnostic.span))
try testing.expect(second.else_block != null)
third :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 2))
try testing.expect_equal(1, ast.block_statement_count(&tree, third.then_block))
after :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 3))
try testing.expect_equal("after", ast.source_text(&tree, after.name))
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 4)))
final :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 5))
try testing.expect_equal("final", ast.source_text(&tree, final.name))
try testing.expect_equal(4, diagnostics.count(&diagnostic_store))
}
recovers_invalid_guard_syntax_and_missing_arm_at_enclosing_brace 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() int {
` if true return 1 else return 2
` if (true junk) { kept() } else { other() }
` sibling :: 3
` if (false) }
`tail :: 4
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(2, ast.package_decl_count(&tree))
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
try testing.expect_equal(4, ast.block_statement_count(&tree, body))
first :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 0))
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, first.condition))
guard_error :: diagnostics.get(&diagnostic_store, ast.invalid(&tree, first.condition).diagnostic)?
try testing.expect_equal("return", ast.source_text(&tree, guard_error.span))
try testing.expect(first.else_block != null)
second :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 1))
try testing.expect_equal(1, ast.block_statement_count(&tree, second.then_block))
try testing.expect(second.else_block != null)
sibling :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 2))
try testing.expect_equal("sibling", ast.source_text(&tree, sibling.name))
last :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 3))
missing :: ast.block_statement(&tree, last.then_block, 0)
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, missing))
arm_error :: diagnostics.get(&diagnostic_store, ast.invalid(&tree, missing).diagnostic)?
try testing.expect_equal("}", ast.source_text(&tree, arm_error.span))
try testing.expect_equal(3, diagnostics.count(&diagnostic_store))
}