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`
- `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
- 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
#### bitwise operations
+5
View File
@@ -955,6 +955,11 @@
- `noreturn` expressions coerce to any expected value type and terminate path analysis
- `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
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)
success := types.fallible_success(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 {
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) {
return types.widest(success, fallback)
}
return success if types.is_valid(success) else fallback
}
block_locals: [dynamic]Infer_Local
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)
infer_statements(checker, expr.body, &fallback_locals, local_types, pkg, file, demanded, &success, success)
return success
case .Struct_Literal:
value := types.INVALID
@@ -8139,26 +8137,37 @@ build_compound_expr :: proc(
block_handler := false
void_fallthrough := false
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 {
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)
} else {
block_handler = true
ctx := checker.current_build_ctx
if ctx == nil {
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)
}
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.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[:])
body = handler[:]
}
if capture != hir.INVALID_LOCAL {
resize(ctx.locals, capture_start)
}
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 {
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)
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)
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)
ct_pop_bindings(state, scope_start)
if !handler_ok {
+4 -4
View File
@@ -468,9 +468,8 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi
diagnostic=source.INVALID_DIAGNOSTIC,
})
} else {
if expr.integer != hir.CATCH_EXPRESSION {
capture := hir.as_local(expr.target)
if capture != hir.INVALID_LOCAL && int(capture) < len(state.func_locals) {
capture := hir.as_local(expr.target)
if capture != hir.INVALID_LOCAL && int(capture) < len(state.func_locals) {
error_type := state.func_locals[capture].type
error_value := append_instruction(state, ir.Instruction{
op=.Fallible_Error, span=expr.span, type=error_type,
@@ -489,7 +488,8 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi
target=ir.INVALID_REF, a=capture_slot, b=error_value,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
}
if expr.integer != hir.CATCH_EXPRESSION {
lower_statements(state, expr.body)
if expr.integer == hir.CATCH_VOID_FALLTHROUGH {
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)
if operator.kind == .Keyword_Catch {
left_expr := parser.module.exprs[left]
capture := token.Token{}
if _, pipe_ok := allow(parser, .Pipe); pipe_ok {
capture := current(parser)
capture = current(parser)
if capture.kind != .Identifier && capture.kind != .Underscore {
source.add(parser.diagnostics, capture.span, "expected a catch capture name")
} else {
@@ -1480,6 +1481,9 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
if _, close_ok := allow(parser, .Pipe); !close_ok {
source.add(parser.diagnostics, current(parser).span, "expected '|' after catch capture")
}
skip_newlines(parser)
}
if current(parser).kind == .Left_Brace {
body := parse_block(parser)
end := previous(parser)
left = add_expr(parser, ast.Expr{
@@ -1493,11 +1497,27 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
})
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_expr := parser.module.exprs[right]
left = add_expr(parser, ast.Expr{
kind=.Catch,
span=span_from(left_expr.span, right_expr.span),
name=capture.symbol,
left=left,
right=right,
diagnostic=source.INVALID_DIAGNOSTIC,
+66
View File
@@ -6097,6 +6097,72 @@ main func() i32 {
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)
fallible_void_fallthrough_compiles_at_runtime_and_comptime :: proc(t: ^testing.T) {
text := `Failure :: enum {
+14 -7
View File
@@ -33,6 +33,8 @@ module.exports = grammar({
[$.array_type, $.expression],
[$.array_type, $.array_literal],
[$.expression_statement, $.parenthesized_expression],
[$.assignment_statement, $.expression_statement],
[$.labeled_block, $.expression],
[$.block, $.tuple_literal, $.anonymous_record_literal],
[$.field_initializer, $.expression],
[$.capture_list, $.expression],
@@ -310,8 +312,8 @@ module.exports = grammar({
field('value', $._value),
),
break_statement: $ => seq('break', optional(seq(':', field('label', $.identifier)))),
continue_statement: $ => seq('continue', optional(seq(':', field('label', $.identifier)))),
break_statement: $ => prec.right(seq('break', optional(seq(':', field('label', $.identifier))))),
continue_statement: $ => prec.right(seq('continue', optional(seq(':', field('label', $.identifier))))),
defer_statement: $ => choice(
seq('defer', repeat($._newline), field('body', $.statement)),
@@ -468,7 +470,7 @@ module.exports = grammar({
binary_expression: $ => choice(
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.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))),
@@ -481,11 +483,16 @@ module.exports = grammar({
catch_expression: $ => prec.left(PREC.FALLBACK, seq(
field('value', $.expression),
'catch',
'|',
field('name', choice($.identifier, $.sink)),
'|',
optional(field('capture', $.error_capture)),
repeat($._newline),
field('body', $.block),
field('fallback', choice(
$.block,
$.if_statement,
$.while_statement,
$.for_statement,
$.match_statement,
$.expression,
)),
)),
unary_expression: $ => prec(PREC.PREFIX, seq(
+120 -97
View File
@@ -2072,72 +2072,80 @@
]
},
"break_statement": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "break"
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": ":"
},
{
"type": "FIELD",
"name": "label",
"content": {
"type": "SYMBOL",
"name": "identifier"
"type": "PREC_RIGHT",
"value": 0,
"content": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "break"
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": ":"
},
{
"type": "FIELD",
"name": "label",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
}
}
]
},
{
"type": "BLANK"
}
]
}
]
]
},
{
"type": "BLANK"
}
]
}
]
}
},
"continue_statement": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "continue"
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": ":"
},
{
"type": "FIELD",
"name": "label",
"content": {
"type": "SYMBOL",
"name": "identifier"
"type": "PREC_RIGHT",
"value": 0,
"content": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "continue"
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": ":"
},
{
"type": "FIELD",
"name": "label",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
}
}
]
},
{
"type": "BLANK"
}
]
}
]
]
},
{
"type": "BLANK"
}
]
}
]
}
},
"defer_statement": {
"type": "CHOICE",
@@ -3299,17 +3307,8 @@
"type": "FIELD",
"name": "operator",
"content": {
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": "orelse"
},
{
"type": "STRING",
"value": "catch"
}
]
"type": "STRING",
"value": "orelse"
}
},
{
@@ -3692,29 +3691,20 @@
"value": "catch"
},
{
"type": "STRING",
"value": "|"
},
{
"type": "FIELD",
"name": "name",
"content": {
"type": "CHOICE",
"members": [
{
"type": "CHOICE",
"members": [
{
"type": "FIELD",
"name": "capture",
"content": {
"type": "SYMBOL",
"name": "identifier"
},
{
"type": "SYMBOL",
"name": "sink"
"name": "error_capture"
}
]
}
},
{
"type": "STRING",
"value": "|"
},
{
"type": "BLANK"
}
]
},
{
"type": "REPEAT",
@@ -3725,10 +3715,35 @@
},
{
"type": "FIELD",
"name": "body",
"name": "fallback",
"content": {
"type": "SYMBOL",
"name": "block"
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"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",
"parenthesized_expression"
],
[
"assignment_statement",
"expression_statement"
],
[
"labeled_block",
"expression"
],
[
"block",
"tuple_literal",
+26 -14
View File
@@ -290,10 +290,6 @@
"type": ">>",
"named": false
},
{
"type": "catch",
"named": false
},
{
"type": "orelse",
"named": false
@@ -436,26 +432,42 @@
"type": "catch_expression",
"named": true,
"fields": {
"body": {
"capture": {
"multiple": false,
"required": false,
"types": [
{
"type": "error_capture",
"named": true
}
]
},
"fallback": {
"multiple": false,
"required": true,
"types": [
{
"type": "block",
"named": true
}
]
},
"name": {
"multiple": false,
"required": true,
"types": [
},
{
"type": "identifier",
"type": "expression",
"named": true
},
{
"type": "sink",
"type": "for_statement",
"named": true
},
{
"type": "if_statement",
"named": true
},
{
"type": "match_statement",
"named": true
},
{
"type": "while_statement",
"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))))))))))))))