feat: support yielding value blocks

This commit is contained in:
2026-10-29 22:54:19 +01:00
parent 9838b253ee
commit 670ba8f756
16 changed files with 1582 additions and 107 deletions
+68 -3
View File
@@ -321,6 +321,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
.while: return try parse_while(state, allow_else)
.for, .inline: return try parse_for(state, allow_else)
.return: return try parse_return(state, allow_else)
.yield: return try parse_yield(state, allow_else)
.break, .continue: return try parse_loop_control(state, allow_else)
.defer, .errdefer: return try parse_cleanup(state, allow_else)
.open_curly: return try ast.add_block_stmt(state.builder, try parse_block(state))
@@ -385,6 +386,31 @@ parse_return proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
)
}
@hide:file
parse_yield proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
keyword :: try consume(state, .yield)
value :: if (
next_is(state, .newline) or next_is(state, .close_curly) or next_is(state, .eof)
or (state.match_arm_body and next_is(state, .comma))
or (allow_else and next_is(state, .else))
) {
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, keyword.span)
yield try ast.add_invalid_expr(state.builder, keyword.span, diagnostic)
} else {
yield try parse_expr(state)
}
if (
!next_is(state, .newline) and !next_is(state, .close_curly) and !next_is(state, .eof)
and !(allow_else and next_is(state, .else))
and !(state.match_arm_body and next_is(state, .comma))
) return .unexpected_token
return try ast.add_yield_stmt(state.builder, ast.Span{
start = keyword.span.start,
end = state.tokens[state.next_token - 1].span.end,
source = keyword.span.source,
}, value)
}
@hide:file
parse_loop_control proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
keyword :: current(state)
@@ -925,7 +951,7 @@ starts_type proc(kind lexer.TokenKind) bool {
@hide:file
member_name proc(kind lexer.TokenKind) bool {
return match kind {
.ident, .if, .else, .for, .inline, .while, .proc, .return, .break, .continue,
.ident, .if, .else, .for, .inline, .while, .proc, .return, .yield, .break, .continue,
.true, .false, .and, .or, .mut, .struct, .c_struct, .opaque, .enum, .union, .distinct,
.alias, .match, .import, .test, .c_func, .null, .undefined, .try, .catch, .defer, .errdefer: true
else: false
@@ -1158,7 +1184,7 @@ parse_match_arm proc(state @mut State) ast.NodeId ! ParseError {
state.match_arm_body = true
defer state.match_arm_body = previous
statement_body :: match current(state).kind {
.open_curly, .return, .break, .continue, .if, .while, .for, .inline, .defer, .errdefer: true
.open_curly, .return, .yield, .break, .continue, .if, .while, .for, .inline, .defer, .errdefer: true
else: false
}
body ast.NodeId := undefined
@@ -1701,6 +1727,41 @@ named_binding_starts proc(state @State) bool {
)
}
@hide:file
brace_starts_block proc(state @State) bool {
first := state.next_token + 1
while (first < state.tokens.len and state.tokens[first].kind == .newline) first += 1
# empty records and braces without statement markers retain record syntax.
if (first >= state.tokens.len or state.tokens[first].kind == .close_curly) return false
depth := usize(0)
for (state.next_token)..(state.tokens.len) |index| {
kind :: state.tokens[index].kind
if (kind == .eof) return false
if (depth > 0
and (state.tokens[first].kind == .if or state.tokens[first].kind == .open_curly)
and (kind == .yield or kind == .return or kind == .defer or kind == .errdefer)
and state.tokens[index - 1].kind != .dot
and !(index + 1 < state.tokens.len and state.tokens[index + 1].kind == .equal)) return true
if (depth == 1) {
if (kind == .double_colon or kind == .colon_equal) return true
previous :: if (index > first) state.tokens[index - 1].kind else lexer.TokenKind.open_curly
if (previous != .dot and !(index + 1 < state.tokens.len and state.tokens[index + 1].kind == .equal)) {
match kind {
.yield, .return, .break, .continue, .while, .for, .inline, .defer, .errdefer: return true
else: {}
}
}
}
if (kind == .open_curly or kind == .open_paren or kind == .open_bracket) depth += 1
if (kind == .close_curly or kind == .close_paren or kind == .close_bracket) {
if (depth == 0) return false
depth -= 1
if (depth == 0) return false
}
}
return false
}
@hide:file
parse_primary proc(state @mut State) ast.NodeId ! ParseError {
token :: current(state)
@@ -1736,6 +1797,10 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
}, member_id)
}
.open_curly: {
if (state.allow_record_literals and brace_starts_block(state)) {
body :: try parse_block(state)
return try ast.add_block_expr(state.builder, ast.builder_span(state.builder, body), body)
}
if (state.allow_record_literals) return try parse_record_literal(state, null, token.span.start)
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, token.span)
return try ast.add_invalid_expr(state.builder, token.span, diagnostic)
@@ -1808,7 +1873,7 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
@hide:file
is_expression_boundary proc(kind lexer.TokenKind) bool {
return match kind {
.close_paren, .newline, .close_curly, .open_curly, .else, .return, .if, .while, .for, .inline, .defer, .errdefer, .eof: true
.close_paren, .newline, .close_curly, .open_curly, .else, .return, .yield, .if, .while, .for, .inline, .defer, .errdefer, .eof: true
else: false
}
}
+202
View File
@@ -2040,3 +2040,205 @@ keeps_match_default_arm_after_newline_single_statement_if test {
try testing.expect(branch.else_block == null)
try testing.expect(ast.match_arm_view(&tree, ast.match_arm(&tree, expression, 1)).catch_all)
}
parses_value_blocks_and_clones_yield_children test {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`run proc(flag bool) int {
` result :: {
` local :: 3
` { if (flag) yield local }
` yield { yield 4 }
` }
` return if (flag) { yield result } else { yield 5 }
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
builder := ast.init_builder(mem.c_allocator, source)
defer ast.deinit_builder(&builder)
root :: try parse_source(&builder, scan_state.tokens.items, &diagnostic_store)
original :: ast.view(&builder, root)
mapping :: try mem.alloc(ast.NodeId, mem.c_allocator, ast.node_count(&original))
defer mem.free(mem.c_allocator, mapping)
mem.set(mapping, maxval!(ast.NodeId))
cloned :: try ast.clone_subtree(&builder, root, mapping)
tree :: ast.view(&builder, root)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
roots [2]ast.NodeId :: [root, cloned]
for (roots) |package| {
function :: ast.function_decl(&tree, ast.child(&tree, package, 0))
value :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 0)).initializer
try testing.expect_equal(ast.ExprKind.block_expr, ast.expr_kind(&tree, value))
try testing.expect_equal(ast.NodeClass.expression, ast.node_class(&tree, value))
block :: ast.block_expr(&tree, value)
try testing.expect_equal(3, ast.block_statement_count(&tree, block))
ordinary :: ast.statement_block(&tree, ast.block_statement(&tree, block, 1))
branch :: ast.if_stmt(&tree, ast.block_statement(&tree, ordinary, 0))
first :: ast.block_statement(&tree, branch.then_block, 0)
try testing.expect_equal(ast.StmtKind.yield_stmt, ast.stmt_kind(&tree, first))
try testing.expect_equal(ast.NodeClass.statement, ast.node_class(&tree, first))
try testing.expect_equal("yield local", ast.source_text(&tree, ast.yield_stmt(&tree, first).span))
last :: ast.yield_stmt(&tree, ast.block_statement(&tree, block, 2))
try testing.expect_equal(ast.ExprKind.block_expr, ast.expr_kind(&tree, last.value?))
nested :: ast.block_statement(&tree, ast.block_expr(&tree, last.value?), 0)
try testing.expect_equal(ast.ExprKind.literal_int, ast.expr_kind(&tree, ast.yield_stmt(&tree, nested).value?))
conditional :: ast.return_stmt(&tree, ast.block_statement(&tree, function.body, 1)).value?
for (0..ast.match_arm_count(&tree, conditional)) |index| {
arm :: ast.match_arm_view(&tree, ast.match_arm(&tree, conditional, index))
try testing.expect(arm.expression)
try testing.expect_equal(ast.ExprKind.block_expr, ast.expr_kind(&tree, arm.body))
}
}
first_function :: ast.function_decl(&tree, ast.child(&tree, root, 0))
first_value :: ast.statement_decl(&tree, ast.block_statement(&tree, first_function.body, 0)).initializer
copied_value :: mapping[usize(first_value)]
try testing.expect(copied_value != first_value)
try testing.expect_equal(mapping[usize(ast.block_expr(&tree, first_value))], ast.block_expr(&tree, copied_value))
}
parses_value_blocks_starting_with_named_assignments test {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`main proc() i16 {
` value ?i16 :: 42
` return if value |payload| { payload = 7
` yield payload
` } else 0
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &strings, &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))
function :: ast.function_decl(&tree, ast.package_decl(&tree, 0))
conditional :: ast.return_stmt(&tree, ast.block_statement(&tree, function.body, 1)).value?
arm :: ast.match_arm_view(&tree, ast.match_arm(&tree, conditional, 0))
try testing.expect_equal(ast.ExprKind.block_expr, ast.expr_kind(&tree, arm.body))
block :: ast.block_expr(&tree, arm.body)
try testing.expect_equal(2, ast.block_statement_count(&tree, block))
first :: ast.block_statement(&tree, block, 0)
try testing.expect_equal(ast.StmtKind.assignment_stmt, ast.stmt_kind(&tree, first))
assignment :: ast.assignment_stmt(&tree, first)
try testing.expect_equal("payload", ast.source_text(&tree, ast.node_span(&tree, assignment.target)))
try testing.expect_equal("7", ast.source_text(&tree, ast.node_span(&tree, assignment.value)))
last :: ast.block_statement(&tree, block, 1)
try testing.expect_equal(ast.StmtKind.yield_stmt, ast.stmt_kind(&tree, last))
try testing.expect_equal("payload", ast.source_text(&tree, ast.node_span(&tree, ast.yield_stmt(&tree, last).value?)))
}
preserves_record_literals_alongside_value_blocks test {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`empty :: {}
`single :: {1}
`pair :: {1, 2}
`keyed :: { value = { yield 3 } }
`keyword :: { yield = 4 }
`choose :: if (true) { value = 7 } else { value = 8 }
`conditional :: {if (true) 1 else 2}
`block :: { if (true) yield 9 }
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &strings, &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))
try testing.expect_equal(8, ast.package_decl_count(&tree))
for (usize(0)..usize(5)) |index| {
value :: ast.statement_decl(&tree, ast.package_decl(&tree, index)).initializer
try testing.expect_equal(ast.ExprKind.record_literal, ast.expr_kind(&tree, value))
}
choose :: ast.statement_decl(&tree, ast.package_decl(&tree, 5)).initializer
for (usize(0)..usize(2)) |index| {
arm :: ast.match_arm_view(&tree, ast.match_arm(&tree, choose, index))
try testing.expect_equal(ast.ExprKind.record_literal, ast.expr_kind(&tree, arm.body))
}
conditional :: ast.statement_decl(&tree, ast.package_decl(&tree, 6)).initializer
try testing.expect_equal(ast.ExprKind.record_literal, ast.expr_kind(&tree, conditional))
block :: ast.statement_decl(&tree, ast.package_decl(&tree, 7)).initializer
try testing.expect_equal(ast.ExprKind.block_expr, ast.expr_kind(&tree, block))
}
requires_yield_values_and_recovers_following_statements test {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`run proc() int {
` value :: {
` yield
` after :: 2
` if (true) yield else yield 3
` yield 4 5
` yield after
` }
` return value
`}
`tail :: 7
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(3, diagnostics.count(&diagnostic_store))
try testing.expect_equal(2, ast.package_decl_count(&tree))
function :: ast.function_decl(&tree, ast.package_decl(&tree, 0))
value :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 0)).initializer
block :: ast.block_expr(&tree, value)
try testing.expect_equal(5, ast.block_statement_count(&tree, block))
missing :: ast.yield_stmt(&tree, ast.block_statement(&tree, block, 0))
try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, missing.value?))
try testing.expect_equal(ast.StmtKind.declaration, ast.stmt_kind(&tree, ast.block_statement(&tree, block, 1)))
try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, block, 3)))
last :: ast.yield_stmt(&tree, ast.block_statement(&tree, block, 4))
try testing.expect_equal("after", ast.source_text(&tree, ast.node_span(&tree, last.value?)))
}
keeps_yielding_match_and_catch_bodies_as_raw_blocks test {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`run proc() int {
` selected :: match 1 { 1: { yield 2 }, else: yield 3 }
` recovered :: selected catch |err| { yield err }
` return use({ yield recovered }, $({ yield 4 }))
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &strings, &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))
function :: ast.function_decl(&tree, ast.package_decl(&tree, 0))
selected :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 0)).initializer
for (usize(0)..usize(2)) |index| {
arm :: ast.match_arm_view(&tree, ast.match_arm(&tree, selected, index))
try testing.expect(!arm.expression)
try testing.expect_equal(ast.NodeClass.block, ast.node_class(&tree, arm.body))
try testing.expect_equal(ast.StmtKind.yield_stmt, ast.stmt_kind(&tree, ast.block_statement(&tree, arm.body, 0)))
}
recovered :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 1)).initializer
handler :: ast.catch_expr(&tree, recovered)
try testing.expect(handler.handler_is_block)
try testing.expect_equal(ast.NodeClass.block, ast.node_class(&tree, handler.handler))
try testing.expect_equal(ast.StmtKind.yield_stmt, ast.stmt_kind(&tree, ast.block_statement(&tree, handler.handler, 0)))
call :: ast.return_stmt(&tree, ast.block_statement(&tree, function.body, 2)).value?
try testing.expect_equal(ast.ExprKind.block_expr, ast.expr_kind(&tree, ast.call_arg(&tree, call, 0)))
constant :: ast.call_arg(&tree, call, 1)
try testing.expect_equal(ast.ExprKind.comptime_expr, ast.expr_kind(&tree, constant))
try testing.expect_equal(ast.ExprKind.block_expr, ast.expr_kind(&tree, ast.comptime_operand(&tree, constant)))
}