unify catch fallback value sources

This commit is contained in:
2026-07-22 00:37:48 +02:00
parent 17508ff751
commit 5f343ad2d3
12 changed files with 1048397 additions and 324314 deletions
+1 -1
View File
@@ -102,7 +102,7 @@ fields. `_` is not a keyword member name.
- bare void `return`, same-line `return value`, value blocks, value `if` with implicit single-expression branches, value loops, value `match`, and strictly value-producing `yield value` / `yield :label value` - bare void `return`, same-line `return value`, value blocks, value `if` with implicit single-expression branches, value loops, value `match`, and strictly value-producing `yield value` / `yield :label value`
- `match` statements/expressions over enums, tagged unions, and scalars, including exhaustiveness checks, payload captures, pointer payload captures, multi-pattern arms, and scalar range patterns - `match` statements/expressions over enums, tagged unions, and scalars, including exhaustiveness checks, payload captures, pointer payload captures, multi-pattern arms, and scalar range patterns
- a final `expand |value|:` enum arm or `expand |payload[, tag]|:` tagged-union arm generates one specialized arm for each variant not covered earlier; enum values and optional tags are comptime-known, while union payloads keep their concrete variant type - a final `expand |value|:` enum arm or `expand |payload[, tag]|:` tagged-union arm generates one specialized arm for each variant not covered earlier; enum values and optional tags are comptime-known, while union payloads keep their concrete variant type
- fallible `try`, fallback `catch`, and `catch |e| { ... }` handler blocks - fallible `try` and uniform `catch [|e|] value_source` fallbacks; captures work with ordinary expressions, value blocks, and value-producing `if` / loops / `match`
- direct `return match ...` and `yield match ...` value-control-flow operands - direct `return match ...` and `yield match ...` value-control-flow operands
#### bitwise operations #### bitwise operations
+5
View File
@@ -955,6 +955,11 @@
- `noreturn` expressions coerce to any expected value type and terminate path analysis - `noreturn` expressions coerce to any expected value type and terminate path analysis
- `unreachable` always traps at runtime and reports an error during comptime evaluation - `unreachable` always traps at runtime and reports an error during comptime evaluation
49. unify catch fallback value sources (implemented)
- `expr catch [|error|] value_source` uses the same ordinary expression, value block, and
value-producing control-flow forms with or without an error capture
- void fallthrough and diverging `noreturn` fallbacks remain valid
## A word on unchecked casts ## A word on unchecked casts
For casts that bypass safety checks, Honey provides builtin functions: For casts that bypass safety checks, Honey provides builtin functions:
+28 -19
View File
@@ -4737,23 +4737,21 @@ infer_compound_expr :: proc(
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types, left_expected) value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types, left_expected)
success := types.fallible_success(value, store) success := types.fallible_success(value, store)
error_type := types.fallible_error(value, store) error_type := types.fallible_error(value, store)
fallback_locals: [dynamic]Infer_Local
fallback_locals.allocator = checker.allocator
defer delete(fallback_locals)
append(&fallback_locals, ..locals)
if symbol.is_valid(expr.name) && expr.name != checker.sink_symbol && types.is_valid(error_type) {
append(&fallback_locals, Infer_Local{name=expr.name, type=error_type, declared=error_type, statement=ast.INVALID_STMT})
}
if expr.right != ast.INVALID_EXPR { if expr.right != ast.INVALID_EXPR {
fallback := infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types) fallback := infer_nested_expr(checker, expr.right, fallback_locals[:], pkg, file, demanded, local_types)
if types.is_valid(success) && types.is_valid(fallback) && !types.equal(success, fallback) { if types.is_valid(success) && types.is_valid(fallback) && !types.equal(success, fallback) {
return types.widest(success, fallback) return types.widest(success, fallback)
} }
return success if types.is_valid(success) else fallback return success if types.is_valid(success) else fallback
} }
block_locals: [dynamic]Infer_Local infer_statements(checker, expr.body, &fallback_locals, local_types, pkg, file, demanded, &success, success)
block_locals.allocator = checker.allocator
defer delete(block_locals)
append(&block_locals, ..locals)
capture_start := len(block_locals)
if symbol.is_valid(expr.name) && expr.name != checker.sink_symbol && types.is_valid(error_type) {
append(&block_locals, Infer_Local{name=expr.name, type=error_type, declared=error_type, statement=ast.INVALID_STMT})
}
infer_statements(checker, expr.body, &block_locals, local_types, pkg, file, demanded, &success, success)
resize(&block_locals, capture_start)
return success return success
case .Struct_Literal: case .Struct_Literal:
value := types.INVALID value := types.INVALID
@@ -8139,26 +8137,37 @@ build_compound_expr :: proc(
block_handler := false block_handler := false
void_fallthrough := false void_fallthrough := false
fallback := hir.INVALID_EXPR fallback := hir.INVALID_EXPR
ctx := checker.current_build_ctx
capture_start := 0
if symbol.is_valid(expr.name) && expr.name != checker.sink_symbol {
if ctx == nil {
id := source.add(checker.diagnostics, expr.span, "a captured catch fallback is only valid in a function body")
return invalid_hir_expr(checker, expr.span, id, success)
}
capture_start = len(ctx.locals^)
error_type := types.fallible_error(channel_type, store)
capture = append_build_local(ctx, expr.name, error_type, false, expr.span)
}
if expr.right != ast.INVALID_EXPR { if expr.right != ast.INVALID_EXPR {
fallback = build_nested_expr(checker, expr.right, locals, global_reads, calls, success, pkg, file) fallback_locals := locals
if capture != hir.INVALID_LOCAL {
fallback_locals = ctx.locals^[:]
}
fallback = build_nested_expr(checker, expr.right, fallback_locals, global_reads, calls, success, pkg, file)
fallback = coerce_expr(checker, fallback, success, checker.module.exprs[fallback].span) fallback = coerce_expr(checker, fallback, success, checker.module.exprs[fallback].span)
} else { } else {
block_handler = true block_handler = true
ctx := checker.current_build_ctx
if ctx == nil { if ctx == nil {
id := source.add(checker.diagnostics, expr.span, "catch block form is only valid in a function body") id := source.add(checker.diagnostics, expr.span, "catch block form is only valid in a function body")
return invalid_hir_expr(checker, expr.span, id, success) return invalid_hir_expr(checker, expr.span, id, success)
} }
capture_start := len(ctx.locals^)
error_type := types.fallible_error(channel_type, store)
if symbol.is_valid(expr.name) && expr.name != checker.sink_symbol {
capture = append_build_local(ctx, expr.name, error_type, false, expr.span)
}
handler: [dynamic]hir.Stmt_Id handler: [dynamic]hir.Stmt_Id
handler.allocator = checker.allocator handler.allocator = checker.allocator
fallback, _ = build_value_source(ctx, &handler, expr.body, success, expr.span, allow_exit=true) fallback, _ = build_value_source(ctx, &handler, expr.body, success, expr.span, value_control_flow=expr.integer != 0, allow_exit=true)
void_fallthrough = fallback == hir.INVALID_EXPR && !all_paths_exit(&checker.module, handler[:]) void_fallthrough = fallback == hir.INVALID_EXPR && !all_paths_exit(&checker.module, handler[:])
body = handler[:] body = handler[:]
}
if capture != hir.INVALID_LOCAL {
resize(ctx.locals, capture_start) resize(ctx.locals, capture_start)
} }
catch_mode := hir.CATCH_EXPRESSION catch_mode := hir.CATCH_EXPRESSION
+5 -3
View File
@@ -3763,14 +3763,16 @@ ct_eval_catch_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Typ
if value.active == 0 { if value.active == 0 {
return payload, ct_flow(.Normal), true return payload, ct_flow(.Normal), true
} }
if expr.right != ast.INVALID_EXPR {
return ct_eval_expr(state, expr.right, success, depth+1)
}
scope_start := len(state.bindings) scope_start := len(state.bindings)
if symbol.is_valid(expr.name) && expr.name != checker.sink_symbol && payload != INVALID_CT_VALUE { if symbol.is_valid(expr.name) && expr.name != checker.sink_symbol && payload != INVALID_CT_VALUE {
error_type := types.fallible_error(value.type, &checker.module.types) error_type := types.fallible_error(value.type, &checker.module.types)
ct_bind_value(state, expr.name, error_type, payload, false) ct_bind_value(state, expr.name, error_type, payload, false)
} }
if expr.right != ast.INVALID_EXPR {
result, result_flow, result_ok := ct_eval_expr(state, expr.right, success, depth+1)
ct_pop_bindings(state, scope_start)
return result, result_flow, result_ok
}
handler, handler_ok := ct_exec_statements(state, expr.body, true, depth+1) handler, handler_ok := ct_exec_statements(state, expr.body, true, depth+1)
ct_pop_bindings(state, scope_start) ct_pop_bindings(state, scope_start)
if !handler_ok { if !handler_ok {
+1 -1
View File
@@ -468,7 +468,6 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi
diagnostic=source.INVALID_DIAGNOSTIC, diagnostic=source.INVALID_DIAGNOSTIC,
}) })
} else { } else {
if expr.integer != hir.CATCH_EXPRESSION {
capture := hir.as_local(expr.target) capture := hir.as_local(expr.target)
if capture != hir.INVALID_LOCAL && int(capture) < len(state.func_locals) { if capture != hir.INVALID_LOCAL && int(capture) < len(state.func_locals) {
error_type := state.func_locals[capture].type error_type := state.func_locals[capture].type
@@ -490,6 +489,7 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi
diagnostic=source.INVALID_DIAGNOSTIC, diagnostic=source.INVALID_DIAGNOSTIC,
}) })
} }
if expr.integer != hir.CATCH_EXPRESSION {
lower_statements(state, expr.body) lower_statements(state, expr.body)
if expr.integer == hir.CATCH_VOID_FALLTHROUGH { if expr.integer == hir.CATCH_VOID_FALLTHROUGH {
append_instruction(state, ir.Instruction{ append_instruction(state, ir.Instruction{
+21 -1
View File
@@ -1470,8 +1470,9 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
skip_newlines(parser) skip_newlines(parser)
if operator.kind == .Keyword_Catch { if operator.kind == .Keyword_Catch {
left_expr := parser.module.exprs[left] left_expr := parser.module.exprs[left]
capture := token.Token{}
if _, pipe_ok := allow(parser, .Pipe); pipe_ok { if _, pipe_ok := allow(parser, .Pipe); pipe_ok {
capture := current(parser) capture = current(parser)
if capture.kind != .Identifier && capture.kind != .Underscore { if capture.kind != .Identifier && capture.kind != .Underscore {
source.add(parser.diagnostics, capture.span, "expected a catch capture name") source.add(parser.diagnostics, capture.span, "expected a catch capture name")
} else { } else {
@@ -1480,6 +1481,9 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
if _, close_ok := allow(parser, .Pipe); !close_ok { if _, close_ok := allow(parser, .Pipe); !close_ok {
source.add(parser.diagnostics, current(parser).span, "expected '|' after catch capture") source.add(parser.diagnostics, current(parser).span, "expected '|' after catch capture")
} }
skip_newlines(parser)
}
if current(parser).kind == .Left_Brace {
body := parse_block(parser) body := parse_block(parser)
end := previous(parser) end := previous(parser)
left = add_expr(parser, ast.Expr{ left = add_expr(parser, ast.Expr{
@@ -1493,11 +1497,27 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
}) })
continue continue
} }
if cf, is_cf := parse_value_control_flow(parser); is_cf {
body := make([]ast.Stmt_Id, 1, parser.module.allocator)
body[0] = cf
left = add_expr(parser, ast.Expr{
kind=.Catch,
span=span_from(left_expr.span, parser.module.statements[cf].span),
name=capture.symbol,
left=left,
right=ast.INVALID_EXPR,
body=body,
integer=1,
diagnostic=source.INVALID_DIAGNOSTIC,
})
continue
}
right := parse_expression_bp(parser, right_power, nesting+1) right := parse_expression_bp(parser, right_power, nesting+1)
right_expr := parser.module.exprs[right] right_expr := parser.module.exprs[right]
left = add_expr(parser, ast.Expr{ left = add_expr(parser, ast.Expr{
kind=.Catch, kind=.Catch,
span=span_from(left_expr.span, right_expr.span), span=span_from(left_expr.span, right_expr.span),
name=capture.symbol,
left=left, left=left,
right=right, right=right,
diagnostic=source.INVALID_DIAGNOSTIC, diagnostic=source.INVALID_DIAGNOSTIC,
+66
View File
@@ -6097,6 +6097,72 @@ main func() i32 {
testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, state.exit_code, 0)
} }
@(test)
catch_fallback_value_sources_compile_and_run :: proc(t: ^testing.T) {
text := `Failure :: enum {
bad
worse
}
value func(fail bool, worse bool) i32 ! Failure {
if fail {
if worse { return .worse }
return .bad
}
return 10
}
void_value func(fail bool) void ! Failure {
if fail { return .bad }
}
capture_score func(err Failure) i32 {
return match err {
.bad: 1
.worse: 2
}
}
consume func(err Failure) void { _ = err }
comptime_recovery func() i32 {
return value(true, false) catch |err| capture_score(err)
}
main func() i32 {
a :: value(true, false) catch 3
b :: value(true, false) catch |err| capture_score(err)
c :: value(true, true) catch |err| match err {
.bad: 4
.worse: 5
}
d :: value(true, false) catch if true {
yield 6
} else {
yield 7
}
e :: value(true, false) catch {
yield 8
}
f :: value(true, false) catch |err| {
_ = err
yield 9
}
g :: value(false, false) catch unreachable
h :: value(false, false) catch |_| unreachable
void_value(true) catch |err| consume(err)
void_value(false) catch {}
ct i32 :: $comptime_recovery()
return a + b + c + d + e + f + g + h + ct - 53
}
`
directory := "/tmp/brolang-test-catch-value-sources"
main_path := "/tmp/brolang-test-catch-value-sources/main.bro"
output := "/tmp/brolang-test-catch-value-sources-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test) @(test)
fallible_void_fallthrough_compiles_at_runtime_and_comptime :: proc(t: ^testing.T) { fallible_void_fallthrough_compiles_at_runtime_and_comptime :: proc(t: ^testing.T) {
text := `Failure :: enum { text := `Failure :: enum {
+14 -7
View File
@@ -33,6 +33,8 @@ module.exports = grammar({
[$.array_type, $.expression], [$.array_type, $.expression],
[$.array_type, $.array_literal], [$.array_type, $.array_literal],
[$.expression_statement, $.parenthesized_expression], [$.expression_statement, $.parenthesized_expression],
[$.assignment_statement, $.expression_statement],
[$.labeled_block, $.expression],
[$.block, $.tuple_literal, $.anonymous_record_literal], [$.block, $.tuple_literal, $.anonymous_record_literal],
[$.field_initializer, $.expression], [$.field_initializer, $.expression],
[$.capture_list, $.expression], [$.capture_list, $.expression],
@@ -310,8 +312,8 @@ module.exports = grammar({
field('value', $._value), field('value', $._value),
), ),
break_statement: $ => seq('break', optional(seq(':', field('label', $.identifier)))), break_statement: $ => prec.right(seq('break', optional(seq(':', field('label', $.identifier))))),
continue_statement: $ => seq('continue', optional(seq(':', field('label', $.identifier)))), continue_statement: $ => prec.right(seq('continue', optional(seq(':', field('label', $.identifier))))),
defer_statement: $ => choice( defer_statement: $ => choice(
seq('defer', repeat($._newline), field('body', $.statement)), seq('defer', repeat($._newline), field('body', $.statement)),
@@ -468,7 +470,7 @@ module.exports = grammar({
binary_expression: $ => choice( binary_expression: $ => choice(
prec.left(PREC.RANGE, seq(field('left', $.expression), field('operator', choice('..', '..=')), repeat($._newline), field('right', $.expression))), prec.left(PREC.RANGE, seq(field('left', $.expression), field('operator', choice('..', '..=')), repeat($._newline), field('right', $.expression))),
prec.left(PREC.FALLBACK, seq(field('left', $.expression), field('operator', choice('orelse', 'catch')), repeat($._newline), field('right', $.expression))), prec.left(PREC.FALLBACK, seq(field('left', $.expression), field('operator', 'orelse'), repeat($._newline), field('right', $.expression))),
prec.left(PREC.OR, seq(field('left', $.expression), 'or', repeat($._newline), field('right', $.expression))), prec.left(PREC.OR, seq(field('left', $.expression), 'or', repeat($._newline), field('right', $.expression))),
prec.left(PREC.AND, seq(field('left', $.expression), 'and', repeat($._newline), field('right', $.expression))), prec.left(PREC.AND, seq(field('left', $.expression), 'and', repeat($._newline), field('right', $.expression))),
prec.left(PREC.COMPARE, seq(field('left', $.expression), field('operator', choice('==', '!=', '<', '<=', '>', '>=')), repeat($._newline), field('right', $.expression))), prec.left(PREC.COMPARE, seq(field('left', $.expression), field('operator', choice('==', '!=', '<', '<=', '>', '>=')), repeat($._newline), field('right', $.expression))),
@@ -481,11 +483,16 @@ module.exports = grammar({
catch_expression: $ => prec.left(PREC.FALLBACK, seq( catch_expression: $ => prec.left(PREC.FALLBACK, seq(
field('value', $.expression), field('value', $.expression),
'catch', 'catch',
'|', optional(field('capture', $.error_capture)),
field('name', choice($.identifier, $.sink)),
'|',
repeat($._newline), repeat($._newline),
field('body', $.block), field('fallback', choice(
$.block,
$.if_statement,
$.while_statement,
$.for_statement,
$.match_statement,
$.expression,
)),
)), )),
unary_expression: $ => prec(PREC.PREFIX, seq( unary_expression: $ => prec(PREC.PREFIX, seq(
+48 -25
View File
@@ -2072,6 +2072,9 @@
] ]
}, },
"break_statement": { "break_statement": {
"type": "PREC_RIGHT",
"value": 0,
"content": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [
{ {
@@ -2104,8 +2107,12 @@
] ]
} }
] ]
}
}, },
"continue_statement": { "continue_statement": {
"type": "PREC_RIGHT",
"value": 0,
"content": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [
{ {
@@ -2138,6 +2145,7 @@
] ]
} }
] ]
}
}, },
"defer_statement": { "defer_statement": {
"type": "CHOICE", "type": "CHOICE",
@@ -3299,17 +3307,8 @@
"type": "FIELD", "type": "FIELD",
"name": "operator", "name": "operator",
"content": { "content": {
"type": "CHOICE",
"members": [
{
"type": "STRING", "type": "STRING",
"value": "orelse" "value": "orelse"
},
{
"type": "STRING",
"value": "catch"
}
]
} }
}, },
{ {
@@ -3692,29 +3691,20 @@
"value": "catch" "value": "catch"
}, },
{ {
"type": "STRING",
"value": "|"
},
{
"type": "FIELD",
"name": "name",
"content": {
"type": "CHOICE", "type": "CHOICE",
"members": [ "members": [
{ {
"type": "FIELD",
"name": "capture",
"content": {
"type": "SYMBOL", "type": "SYMBOL",
"name": "identifier" "name": "error_capture"
}
}, },
{ {
"type": "SYMBOL", "type": "BLANK"
"name": "sink"
} }
] ]
}
},
{
"type": "STRING",
"value": "|"
}, },
{ {
"type": "REPEAT", "type": "REPEAT",
@@ -3725,10 +3715,35 @@
}, },
{ {
"type": "FIELD", "type": "FIELD",
"name": "body", "name": "fallback",
"content": { "content": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL", "type": "SYMBOL",
"name": "block" "name": "block"
},
{
"type": "SYMBOL",
"name": "if_statement"
},
{
"type": "SYMBOL",
"name": "while_statement"
},
{
"type": "SYMBOL",
"name": "for_statement"
},
{
"type": "SYMBOL",
"name": "match_statement"
},
{
"type": "SYMBOL",
"name": "expression"
}
]
} }
} }
] ]
@@ -5481,6 +5496,14 @@
"expression_statement", "expression_statement",
"parenthesized_expression" "parenthesized_expression"
], ],
[
"assignment_statement",
"expression_statement"
],
[
"labeled_block",
"expression"
],
[ [
"block", "block",
"tuple_literal", "tuple_literal",
+25 -13
View File
@@ -290,10 +290,6 @@
"type": ">>", "type": ">>",
"named": false "named": false
}, },
{
"type": "catch",
"named": false
},
{ {
"type": "orelse", "type": "orelse",
"named": false "named": false
@@ -436,26 +432,42 @@
"type": "catch_expression", "type": "catch_expression",
"named": true, "named": true,
"fields": { "fields": {
"body": { "capture": {
"multiple": false,
"required": false,
"types": [
{
"type": "error_capture",
"named": true
}
]
},
"fallback": {
"multiple": false, "multiple": false,
"required": true, "required": true,
"types": [ "types": [
{ {
"type": "block", "type": "block",
"named": true "named": true
}
]
}, },
"name": {
"multiple": false,
"required": true,
"types": [
{ {
"type": "identifier", "type": "expression",
"named": true "named": true
}, },
{ {
"type": "sink", "type": "for_statement",
"named": true
},
{
"type": "if_statement",
"named": true
},
{
"type": "match_statement",
"named": true
},
{
"type": "while_statement",
"named": true "named": true
} }
] ]
+1047997 -324168
View File
File diff suppressed because it is too large Load Diff
+110
View File
@@ -0,0 +1,110 @@
==================
Catch value sources
==================
recover func() void {
a :: fail() catch |err| handle(err)
b :: fail() catch |err| match err {
.bad: 1
else: 2
}
c :: fail() catch { yield 3 }
d :: fail() catch if true { yield 4 } else { yield 5 }
}
---
(source_file
(function_declaration
(identifier)
(parameter_list)
(type
(builtin_type))
(block
(statement
(constant_declaration
(identifier)
(expression
(catch_expression
(expression
(call_expression
(expression
(identifier))
(argument_list)))
(error_capture
(identifier))
(expression
(call_expression
(expression
(identifier))
(argument_list
(expression
(identifier)))))))))
(statement
(constant_declaration
(identifier)
(expression
(catch_expression
(expression
(call_expression
(expression
(identifier))
(argument_list)))
(error_capture
(identifier))
(match_statement
(expression
(identifier))
(match_arm
(expression
(enum_literal
(identifier)))
(statement
(expression_statement
(expression
(integer)))))
(match_arm
(statement
(expression_statement
(expression
(integer))))))))))
(statement
(constant_declaration
(identifier)
(expression
(catch_expression
(expression
(call_expression
(expression
(identifier))
(argument_list)))
(block
(statement
(yield_statement
(expression
(integer)))))))))
(statement
(constant_declaration
(identifier)
(expression
(catch_expression
(expression
(call_expression
(expression
(identifier))
(argument_list)))
(if_statement
(expression
(boolean))
(statement
(block
(statement
(yield_statement
(expression
(integer))))))
(statement
(block
(statement
(yield_statement
(expression
(integer))))))))))))))