feat: support fallible results and scoped cleanup

This commit is contained in:
2026-10-26 12:37:15 +01:00
parent cd64afbb40
commit 9058135326
15 changed files with 1732 additions and 76 deletions
+158 -33
View File
@@ -30,6 +30,7 @@ State :: struct {
next_token usize
paren_depth usize = 0
allow_record_literals bool = true
allow_sum_types bool = true
match_arm_body bool = false
}
@@ -176,6 +177,7 @@ parse_func_decl proc(state @mut State) ast.NodeId ! ParseError {
parse_parameters proc(
state @mut State, params @mut arraylist.ArrayList(ast.NodeId),
) void ! ParseError {
skip_newlines(state)
names arraylist.ArrayList(lexer.Token) := arraylist.init(state.allocator)
defer arraylist.deinit(&names)
while !next_is(state, .close_paren) {
@@ -187,24 +189,42 @@ parse_parameters proc(
_ = try consume(state, .comma)
}
type_id :: try parse_type(state)
type_span :: ast.builder_span(state.builder, type_id)
type_end :: state.tokens[state.next_token - 1].span.end
for (names.items) |parameter_name| {
param_id :: try ast.add_param(
state.builder,
ast.Span{ start = parameter_name.span.start, end = type_span.end, source = parameter_name.span.source },
ast.Span{ start = parameter_name.span.start, end = type_end, source = parameter_name.span.source },
parameter_name.span,
parameter_name.data.str_id,
type_id,
)
try arraylist.append(params, param_id)
}
if next_is(state, .comma) _ = try consume(state, .comma)
skip_newlines(state)
if next_is(state, .comma) {
state.next_token += 1
skip_newlines(state)
}
}
}
@hide:file
parse_block proc(state @mut State) ast.NodeId ! ParseError {
open :: try consume(state, .open_curly)
previous_depth :: state.paren_depth
previous_record_literals :: state.allow_record_literals
previous_sum_types :: state.allow_sum_types
previous_match_arm_body :: state.match_arm_body
state.paren_depth = 0
state.allow_record_literals = true
state.allow_sum_types = true
state.match_arm_body = false
defer {
state.paren_depth = previous_depth
state.allow_record_literals = previous_record_literals
state.allow_sum_types = previous_sum_types
state.match_arm_body = previous_match_arm_body
}
statements arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
defer arraylist.deinit(&statements)
@@ -249,6 +269,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
or next_n_is(state, 1, .distinct)
or next_n_is(state, 1, .at)
or next_n_is(state, 1, .question)
or (next_n_is(state, 1, .open_paren) and pointer_binding_starts(state))
or (next_n_is(state, 1, .star) and pointer_binding_starts(state))
or (next_n_is(state, 1, .open_bracket) and array_starts_type(state))
or next_n_is(state, 1, .double_colon)
@@ -263,8 +284,10 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
.for: return try parse_for(state, allow_else)
.return: return try parse_return(state, allow_else)
.break, .continue: return try parse_loop_control(state, allow_else)
.ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren, .open_bracket, .open_curly, .struct,
.enum, .union, .distinct, .alias, .dot, .match, .ampersand, .at, .star, .question, .null: {
.defer, .errdefer: return try parse_cleanup(state, allow_else)
.open_curly: return try ast.add_block_stmt(state.builder, try parse_block(state))
.ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren, .open_bracket, .struct,
.enum, .union, .distinct, .alias, .dot, .match, .ampersand, .at, .star, .question, .null, .try: {
expression :: try parse_expr(state)
if next_is(state, .equal) return try parse_assignment(state, expression, allow_else)
return try ast.add_expression_stmt(state.builder, expression)
@@ -273,6 +296,30 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
}
}
@hide:file
parse_error_capture proc(state @mut State) ?ast.NodeId ! ParseError {
capture ?ast.NodeId := null
if (!next_is(state, .pipe)) return capture
state.next_token += 1
capture = try parse_capture_name(state)
_ = try consume(state, .pipe)
return capture
}
@hide:file
parse_cleanup proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
keyword :: current(state)
state.next_token += 1
error_only :: keyword.kind == .errdefer
capture ?ast.NodeId :: if (error_only) try parse_error_capture(state) else null
body :: try parse_arm(state, allow_else)
return try ast.add_cleanup_stmt(state.builder, ast.Span{
start = keyword.span.start,
end = ast.builder_span(state.builder, body).end,
source = keyword.span.source,
}, body, capture, error_only)
}
@hide:file
parse_return proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
keyword :: try consume(state, .return)
@@ -517,8 +564,14 @@ parse_for proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
@hide:file
parse_guard_expr proc(state @mut State) ast.NodeId ! ParseError {
previous :: state.allow_record_literals
previous_sum_types :: state.allow_sum_types
state.allow_record_literals = false
defer state.allow_record_literals = previous
# capture pipes stay boundaries even when an index/call permits record literals.
state.allow_sum_types = false
defer {
state.allow_record_literals = previous
state.allow_sum_types = previous_sum_types
}
return try parse_expr(state)
}
@@ -630,12 +683,49 @@ parse_statement_decl proc(state @mut State) ast.NodeId ! ParseError {
@hide:file
parse_type proc(state @mut State) ast.NodeId ! ParseError {
skip_operand_newlines(state)
start :: current(state).span
left := try parse_type_atom(state)
while true {
skip_continuation_newlines(state)
if (!next_is(state, .pipe)) break
state.next_token += 1
right :: try parse_type_atom(state)
left = try ast.add_binary_type(state.builder, ast.Span{
start = start.start,
end = state.tokens[state.next_token - 1].span.end,
source = start.source,
}, left, right, false)
}
if (next_is(state, .bang)) {
state.next_token += 1
right :: try parse_type(state)
left = try ast.add_binary_type(state.builder, ast.Span{
start = start.start,
end = state.tokens[state.next_token - 1].span.end,
source = start.source,
}, left, right, true)
}
return left
}
@hide:file
parse_type_atom proc(state @mut State) ast.NodeId ! ParseError {
skip_operand_newlines(state)
if (next_is(state, .open_paren)) {
state.next_token += 1
state.paren_depth += 1
defer state.paren_depth -= 1
child :: try parse_type(state)
_ = try consume(state, .close_paren)
return child
}
if (next_is(state, .question)) {
token :: current(state)
state.next_token += 1
child :: try parse_type(state)
child :: try parse_type_atom(state)
return try ast.add_optional_type(state.builder, ast.Span{
start = token.span.start, end = ast.builder_span(state.builder, child).end, source = token.span.source,
start = token.span.start, end = state.tokens[state.next_token - 1].span.end, source = token.span.source,
}, child)
}
if (next_is(state, .at) or next_is(state, .star)) {
@@ -653,7 +743,7 @@ parse_type proc(state @mut State) ast.NodeId ! ParseError {
result :: try parse_type(state)
span :: ast.Span{
start = token.span.start,
end = ast.builder_span(state.builder, result).end,
end = state.tokens[state.next_token - 1].span.end,
source = token.span.source,
}
if (mutable or token.kind == .star) {
@@ -662,10 +752,10 @@ parse_type proc(state @mut State) ast.NodeId ! ParseError {
}
return try ast.add_function_type(state.builder, span, result, params.items)
}
element :: try parse_type(state)
element :: try parse_type_atom(state)
return try ast.add_pointer_type(state.builder, ast.Span{
start = token.span.start,
end = ast.builder_span(state.builder, element).end,
end = state.tokens[state.next_token - 1].span.end,
source = token.span.source,
}, element, mutable, token.kind == .star)
}
@@ -680,9 +770,9 @@ parse_type proc(state @mut State) ast.NodeId ! ParseError {
_ = try consume(state, .close_bracket)
mutable :: next_is(state, .mut)
if (mutable) state.next_token += 1
element :: try parse_type(state)
element :: try parse_type_atom(state)
return try ast.add_slice_type(state.builder, ast.Span{
start = open.span.start, end = ast.builder_span(state.builder, element).end, source = open.span.source,
start = open.span.start, end = state.tokens[state.next_token - 1].span.end, source = open.span.source,
}, element, mutable, sentinel)
}
count ?ast.NodeId := null
@@ -698,12 +788,12 @@ parse_type proc(state @mut State) ast.NodeId ! ParseError {
_ = try consume(state, .close_bracket)
mutable :: next_is(state, .mut)
if (mutable) state.next_token += 1
element :: try parse_type(state)
element :: try parse_type_atom(state)
return try ast.add_array_type(
state.builder,
ast.Span{
start = open.span.start,
end = ast.builder_span(state.builder, element).end,
end = state.tokens[state.next_token - 1].span.end,
source = open.span.source,
},
count, element, mutable,
@@ -733,7 +823,7 @@ starts_type proc(kind lexer.TokenKind) bool {
return (
kind == .ident or kind == .open_bracket or kind == .struct
or kind == .enum or kind == .union or kind == .distinct or kind == .alias
or kind == .at or kind == .star or kind == .question
or kind == .at or kind == .star or kind == .question or kind == .open_paren
)
}
@@ -742,7 +832,7 @@ member_name proc(kind lexer.TokenKind) bool {
return match kind {
.ident, .if, .else, .for, .while, .proc, .return, .break, .continue,
.true, .false, .and, .or, .mut, .struct, .enum, .union, .distinct,
.alias, .match, .import, .test, .c_func, .null: true
.alias, .match, .import, .test, .c_func, .null, .try, .catch, .defer, .errdefer: true
else: false
}
}
@@ -961,7 +1051,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: true
.open_curly, .return, .break, .continue, .if, .while, .for, .defer, .errdefer: true
else: false
}
if (statement_body or named_binding_starts(state)) {
@@ -1144,21 +1234,21 @@ 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)
span :: ast.Span{
start = token.span.start,
end = operand_span.end,
source = token.span.source,
}
operator ast.UnaryOperator :: match token.kind {
.bang: .logical_not
.ampersand: .address_of
else: .negate
}
yield try ast.add_unary(
state.builder,
ast.Span{
start = token.span.start,
end = operand_span.end,
source = token.span.source,
},
operator,
operand,
)
prefixed :: if (token.kind == .try)
try ast.add_try_expr(state.builder, span, operand)
else
try ast.add_unary(state.builder, span, operator, operand)
yield prefixed
} else {
yield try parse_primary(state)
}
@@ -1178,7 +1268,11 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
left_grouped = false
continue
}
if (token.kind == .bang and next_n_is(state, 1, .open_paren)) {
# adjacent bang calls stay intrinsics; spaced 'T ! (E)' is a fallible type.
if (
token.kind == .bang and next_n_is(state, 1, .open_paren)
and state.tokens[state.next_token - 1].span.end == token.span.start
) {
state.next_token += 1
left = try parse_call(state, left, state.tokens[expression_start].span.start, true)
left_grouped = false
@@ -1218,6 +1312,34 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
left_grouped = false
continue
}
if (token.kind == .catch) {
if (min_bp > BindingPowerScalar(0)) break
state.next_token += 1
capture :: try parse_error_capture(state)
skip_operand_newlines(state)
handler_is_block :: next_is(state, .open_curly)
handler :: if (handler_is_block) try parse_block(state) else try parse_expr_bp(state, BindingPowerScalar(0))
left = try ast.add_catch_expr(state.builder, ast.Span{
start = state.tokens[expression_start].span.start,
end = state.tokens[state.next_token - 1].span.end,
source = token.span.source,
}, left, capture, handler, handler_is_block)
left_grouped = false
continue
}
if (token.kind == .bang or (token.kind == .pipe and state.allow_sum_types)) {
power :: if (token.kind == .bang) BindingPowerScalar(1) else BindingPowerScalar(2)
if (power < min_bp) break
state.next_token += 1
right :: try parse_expr_bp(state, BindingPowerScalar(u32(power) + 1))
left = try ast.add_binary_type(state.builder, ast.Span{
start = state.tokens[expression_start].span.start,
end = state.tokens[state.next_token - 1].span.end,
source = token.span.source,
}, left, right, token.kind == .bang)
left_grouped = false
continue
}
if (token.kind == .range or token.kind == .range_inclusive) {
if (min_bp > BindingPowerScalar(0)) break
left = try validate_range_bound(state, left, left_grouped)
@@ -1421,7 +1543,7 @@ invalid_array_literal proc(state @mut State, start, end u32) ast.NodeId ! ParseE
@hide:file
pointer_binding_starts proc(state @State) bool {
depth := usize(0)
for (state.next_token + 2)..(state.tokens.len) |i| {
for (state.next_token + 1)..(state.tokens.len) |i| {
kind :: state.tokens[i].kind
if (kind == .open_curly or kind == .open_bracket or kind == .open_paren) depth += 1
if (kind == .close_curly or kind == .close_bracket or kind == .close_paren) {
@@ -1444,6 +1566,7 @@ named_binding_starts proc(state @State) bool {
or next_n_is(state, 1, .ident) or next_n_is(state, 1, .proc)
or next_n_is(state, 1, .at)
or next_n_is(state, 1, .question)
or (next_n_is(state, 1, .open_paren) and pointer_binding_starts(state))
or (next_n_is(state, 1, .star) and pointer_binding_starts(state))
)
)
@@ -1538,7 +1661,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, .eof: true
.close_paren, .newline, .close_curly, .open_curly, .else, .return, .if, .while, .for, .defer, .errdefer, .eof: true
else: false
}
}
@@ -1560,6 +1683,7 @@ is_expression_recovery_boundary proc(kind lexer.TokenKind) bool {
or kind == .and
or kind == .or
or kind == .pipe
or kind == .catch
or is_expression_boundary(kind)
)
}
@@ -1588,6 +1712,7 @@ skip_continuation_newlines proc(state @mut State) void {
kind != .close_paren and kind != .comma and kind != .open_paren
and kind != .close_bracket and kind != .open_bracket
and kind != .range and kind != .range_inclusive
and kind != .pipe and kind != .bang and kind != .catch
and infix_binding_power(kind) == null
) state.next_token = start
}
@@ -1667,7 +1792,7 @@ recovery_closes_delimiters proc(state @State, paren_depth, square_depth usize) b
kind :: state.tokens[i].kind
match kind {
.open_curly, .close_curly, .eof, .if, .else, .while, .for,
.return, .break, .continue: return false
.return, .break, .continue, .defer, .errdefer: return false
.ident: {
if (i + 1 < state.tokens.len) {
next :: state.tokens[i + 1].kind
@@ -1739,7 +1864,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, .bang, .ampersand: BindingPowerScalar(50)
.minus, .bang, .ampersand, .try: BindingPowerScalar(50)
else: null
}
}