error inference in fallible functions

This commit is contained in:
2026-08-09 16:17:49 +02:00
parent 572ffe7d07
commit a7a32894f3
5 changed files with 217 additions and 37 deletions
+1
View File
@@ -45,6 +45,7 @@ roadmap and milestone history.
- void-payload tagged-union variants, anonymous struct payloads, contextual `.variant`, `.variant{payload}`, and `.variant{field = value}` construction
- native sum composition with `A | B` for unbacked enums and tagged unions, optionally grouped as `(A | B)`, using program-global `u16` variant ids
- fallible channel types `T ! E`, where `E` is a native enum, native struct, tagged union, or supported sum composition; `void ! E` functions complete successfully on fallthrough, and void-success `catch` handlers may fall through without `yield`
- bodyful hidden functions and root `main` may write `T!` to infer a specialization-local error channel from propagated `try` expressions and concretely typed error returns; inference composes only existing named error types, never synthesizes variants, and requires at least one inferred error
#### distinct types
+1
View File
@@ -249,6 +249,7 @@ Function :: struct {
params: []Param,
result: Type_Syntax,
error: Type_Syntax,
infer_error: bool,
body: []Stmt_Id,
link_name: string,
unsupported_reason: string,
+90 -27
View File
@@ -105,6 +105,7 @@ Build_Ctx :: struct {
pkg: ast.Package_Id,
file: ast.File_Id,
result: types.Type,
infer_error: bool,
local_types: []types.Type,
locals: ^[dynamic]Build_Local,
hir_locals: ^[dynamic]hir.Local,
@@ -253,7 +254,7 @@ Checker :: struct {
sink_symbol: symbol.Id,
type_symbol: symbol.Id,
current_result: types.Type,
inferred_test_error: ^types.Type,
inferred_error: ^types.Type,
current_build_ctx: ^Build_Ctx,
current_comptime_values: []Comptime_Value,
static_state: Ct_State,
@@ -1308,12 +1309,22 @@ function_channel_type :: proc(checker: ^Checker, function: ast.Function) -> type
if function.pkg == 0 && function.name == checker.main_symbol && result == types.INT {
result = types.I32
}
if function.infer_error {
return types.fallible(&checker.module.types, result, types.INVALID)
}
if types.is_valid(function.error) {
return types.fallible(&checker.module.types, result, type_from_syntax(checker, function.error, function.pkg, function.file))
}
return result
}
is_error_channel_type :: proc(checker: ^Checker, value: types.Type) -> bool {
resolved := types.resolve_alias(value, &checker.module.types)
return types.is_enum(resolved, &checker.module.types) ||
types.is_struct(resolved, &checker.module.types) ||
types.is_tagged_union(resolved, &checker.module.types)
}
is_runtime_type :: proc(checker: ^Checker, value: types.Type) -> bool {
return types.is_runtime_value(value, &checker.module.types)
}
@@ -1726,8 +1737,8 @@ configure_entry_point :: proc(checker: ^Checker) {
type_from_syntax(checker, function.result, function.pkg, function.file),
&checker.module.types,
)
if function.package_hidden && function.has_body && !function.c_abi && len(function.params) == 0 &&
!types.is_valid(function.error) && types.equal(result, io_type) {
if function.package_hidden && function.has_body && !function.c_abi && !function.infer_error &&
len(function.params) == 0 && !types.is_valid(function.error) && types.equal(result, io_type) {
provider = ast.function_id(function_id)
}
}
@@ -3335,7 +3346,7 @@ resolve_type_factory_call :: proc(checker: ^Checker, expr_id: ast.Expr_Id, pkg:
return types.INVALID
}
function := checker.ast_module.functions[template]
if !is_type_metatype_syntax(checker, function.result) || types.is_valid(function.error) {
if !is_type_metatype_syntax(checker, function.result) || function.infer_error || types.is_valid(function.error) {
source.addf(checker.diagnostics, expr.span, "function '%s' does not return a type", symbol_text(checker, expr.name))
return types.INVALID
}
@@ -3588,7 +3599,7 @@ function_value_signature :: proc(
if function_has_comptime_params(function) {
return nil, types.INVALID, false
}
if function.c_abi && types.is_valid(function.error) {
if function.c_abi && (function.infer_error || types.is_valid(function.error)) {
return nil, types.INVALID, false
}
if !function.c_abi && (!function.has_body || function.variadic) {
@@ -3855,7 +3866,7 @@ validate_external_globals :: proc(checker: ^Checker) {
}
runtime_write_declaration_matches :: proc(checker: ^Checker, function: ast.Function) -> bool {
if function.variadic || len(function.params) != 3 || types.is_valid(function.error) {
if function.variadic || len(function.params) != 3 || function.infer_error || types.is_valid(function.error) {
return false
}
store := &checker.module.types
@@ -3977,11 +3988,25 @@ validate_declarations :: proc(checker: ^Checker) {
)
}
}
if function.infer_error && !signature_poisoned {
root_main := function.pkg == 0 && function.name == checker.main_symbol
if function.c_abi {
checker.template_diagnostics[function_id] = source.add(
checker.diagnostics,
function.span,
"inferred error channels are not allowed on 'c_func'",
)
} else if !function.package_hidden && !root_main {
checker.template_diagnostics[function_id] = source.add(
checker.diagnostics,
function.span,
"inferred error channels are only allowed on hidden functions and root main",
)
}
}
if types.is_valid(function.error) && !signature_poisoned {
error_type := type_from_syntax(checker, function.error, function.pkg, function.file)
error_channel := types.is_enum(error_type, &checker.module.types) ||
types.is_struct(error_type, &checker.module.types) ||
types.is_tagged_union(error_type, &checker.module.types)
error_channel := is_error_channel_type(checker, error_type)
if function.c_abi {
checker.template_diagnostics[function_id] = source.add(
checker.diagnostics,
@@ -4780,10 +4805,16 @@ Infer_Frame :: struct {
reverse_operands: bool,
}
merge_inferred_test_error :: proc(checker: ^Checker, incoming: types.Type) {
current := checker.inferred_test_error
if current == nil || !types.is_valid(incoming) ||
types.can_sum_widen(incoming, current^, &checker.module.types) {
merge_inferred_error :: proc(checker: ^Checker, incoming: types.Type) {
current := checker.inferred_error
if current == nil || !types.is_valid(incoming) {
return
}
if !types.is_valid(current^) {
current^ = incoming
return
}
if types.can_sum_widen(incoming, current^, &checker.module.types) {
return
}
if merged, err := types.compose_sum(&checker.module.types, current^, incoming); err == .None {
@@ -5028,7 +5059,7 @@ infer_compound_expr :: proc(
case .Try:
left_expected := expected if checker.ast_module.exprs[expr.left].kind == .Call else types.INVALID
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types, left_expected)
merge_inferred_test_error(checker, types.fallible_error(value, store))
merge_inferred_error(checker, types.fallible_error(value, store))
return types.fallible_success(value, store) if types.kind(value, store) == .Fallible else types.INVALID
case .Catch:
left_expected := expected if checker.ast_module.exprs[expr.left].kind == .Call else types.INVALID
@@ -6103,6 +6134,12 @@ infer_statements :: proc(
}
if statement.expr != ast.INVALID_EXPR {
returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types, result_hint)
if checker.inferred_error != nil &&
is_error_channel_type(checker, returned) &&
!can_implicitly_convert_type(checker, returned, result_hint) {
merge_inferred_error(checker, returned)
continue
}
if is_runtime_type(checker, result_hint) {
_ = record_demand_shallow(checker, statement.expr, result_hint, locals^[:], local_types, pkg, file)
expr := checker.ast_module.exprs[statement.expr]
@@ -6372,23 +6409,31 @@ infer_spec_locals_and_result :: proc(
}
result := types.INVALID
test_error := function.error
previous_test_error := checker.inferred_test_error
checker.inferred_test_error = &test_error if function.test else nil
infer_statements(checker, function.body, &locals, local_types, function.pkg, function.file, demanded, &result, result_hint)
checker.inferred_test_error = previous_test_error
if function.test && !types.equal(function.error, test_error) {
checker.ast_module.functions[spec.template].error = test_error
success := types.fallible_success(checker.specs[id].result, &checker.module.types)
checker.specs[id].result = types.fallible(&checker.module.types, success, test_error)
inferred_error := function.error
if function.infer_error {
inferred_error = types.fallible_error(checker.specs[id].result, &checker.module.types)
}
previous_inferred_error := checker.inferred_error
checker.inferred_error = &inferred_error if function.test || function.infer_error else nil
infer_statements(checker, function.body, &locals, local_types, function.pkg, function.file, demanded, &result, result_hint)
checker.inferred_error = previous_inferred_error
if function.test && !types.equal(function.error, inferred_error) {
checker.ast_module.functions[spec.template].error = inferred_error
success := types.fallible_success(checker.specs[id].result, &checker.module.types)
checker.specs[id].result = types.fallible(&checker.module.types, success, inferred_error)
}
resolved_result := declared
if types.is_constraint(declared) {
// Narrow the inferred result to the constraint's family; an out-of-family
// result (e.g. returning a non-integer from an `int` function) yields
// INVALID and is rejected downstream.
return local_types, types.constraint_target(declared, result, &checker.module.types)
resolved_result = types.constraint_target(declared, result, &checker.module.types)
}
return local_types, declared
if function.infer_error {
checker.specs[id].result = types.fallible(&checker.module.types, resolved_result, inferred_error)
return local_types, checker.specs[id].result
}
return local_types, resolved_result
}
infer_spec_result :: proc(checker: ^Checker, id: Spec_Id, demanded: ^[dynamic]Spec_Id = nil) -> types.Type {
@@ -11465,7 +11510,16 @@ build_block :: proc(
success_has := types.sum_has_name(store, success, u32(expr_ast.name))
error_has := types.sum_has_name(store, error_type, u32(expr_ast.name))
if error_has && !success_has {
error_exit = true
if ctx.infer_error {
id := source.add(
checker.diagnostics,
expr_ast.span,
"inferred error returns require a concretely typed error value",
)
value = invalid_hir_expr(checker, expr_ast.span, id, ctx.result)
} else {
error_exit = true
}
} else if error_has && success_has {
id := source.add(checker.diagnostics, expr_ast.span, "ambiguous fallible return member")
value = invalid_hir_expr(checker, expr_ast.span, id, ctx.result)
@@ -13901,7 +13955,15 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
signature_diagnostic := source.INVALID_DIAGNOSTIC
unresolved_result := !types.is_void(spec.result) && !types.is_noreturn(spec.result) && !is_runtime_type(checker, spec.result)
if unresolved_result {
if types.is_comptime_only(spec.result, &checker.module.types) {
if function.infer_error &&
!types.is_valid(types.fallible_error(spec.result, &checker.module.types)) {
signature_diagnostic = source.addf(
checker.diagnostics,
function.span,
"could not infer a named error channel for '%s'; propagate one with 'try' or return a concretely typed error value",
symbol_text(checker, function.name),
)
} else if types.is_comptime_only(spec.result, &checker.module.types) {
signature_diagnostic = source.addf(
checker.diagnostics,
function.span,
@@ -14050,6 +14112,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
pkg = function.pkg,
file = function.file,
result = spec.result,
infer_error = function.infer_error,
local_types = local_types,
locals = &locals,
hir_locals = &hir_locals,
+23 -10
View File
@@ -403,8 +403,8 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax {
source.add(parser.diagnostics, current(parser).span, "expected ')' after function type parameters")
}
result := parse_type(parser)
if _, ok := allow(parser, .Bang); ok {
error_type := parse_error_type(parser)
error_type, _ := parse_function_error(parser, false)
if types.is_valid(error_type) {
if c_abi {
source.add(parser.diagnostics, current(parser).span, "c_func pointer types cannot be fallible")
} else {
@@ -530,6 +530,22 @@ parse_error_type :: proc(parser: ^Parser) -> ast.Type_Syntax {
return parse_type_pipe_tail(parser, left)
}
parse_function_error :: proc(parser: ^Parser, allow_inferred: bool) -> (ast.Type_Syntax, bool) {
bang, present := allow(parser, .Bang)
if !present {
return types.INVALID, false
}
kind := current(parser).kind
inferred := kind == .Left_Brace || kind == .Newline || kind == .Eof
if !inferred {
return parse_error_type(parser), false
}
if !allow_inferred {
source.add(parser.diagnostics, bang.span, "inferred error channels require a function body")
}
return types.INVALID, true
}
skip_parenthesized :: proc(parser: ^Parser) -> source.Span {
start := current(parser)
depth := 0
@@ -2600,10 +2616,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi, package_hidden
}
skip_newlines(parser)
result := parse_type(parser)
error_type := types.INVALID
if _, ok := allow(parser, .Bang); ok {
error_type = parse_error_type(parser)
}
error_type, infer_error := parse_function_error(parser, true)
end := previous(parser)
ended_by_newline := current(parser).kind == .Newline
if current(parser).kind == .Newline {
@@ -2626,6 +2639,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi, package_hidden
params=params,
result=result,
error=error_type,
infer_error=infer_error,
diagnostic=source.INVALID_DIAGNOSTIC,
})
return
@@ -2645,6 +2659,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi, package_hidden
params=params,
result=result,
error=error_type,
infer_error=infer_error,
body=body,
diagnostic=source.INVALID_DIAGNOSTIC,
})
@@ -2661,10 +2676,7 @@ parse_function_literal :: proc(parser: ^Parser) -> ast.Expr_Id {
}
skip_newlines(parser)
result := parse_type(parser)
error_type := types.INVALID
if _, ok := allow(parser, .Bang); ok {
error_type = parse_error_type(parser)
}
error_type, infer_error := parse_function_error(parser, true)
if current(parser).kind == .Newline {
skip_newlines(parser)
}
@@ -2687,6 +2699,7 @@ parse_function_literal :: proc(parser: ^Parser) -> ast.Expr_Id {
params=params,
result=result,
error=error_type,
infer_error=infer_error,
body=body,
diagnostic=source.INVALID_DIAGNOSTIC,
})
+102
View File
@@ -3167,6 +3167,50 @@ main func() int ! Error { return try value() }
Error :: enum { failed }
fail func() void ! Error { return .failed }
main func(_ process.Init) void ! Error { try fail() }
`,
exit_code=1,
},
{
name="inferred-main-try",
source=`Error :: enum { failed }
fail func() void ! Error { return .failed }
main func() void! { try fail() }
`,
exit_code=1,
},
{
name="inferred-main-typed-return",
source=`Error :: enum { failed }
main func() void! {
err Error := .failed
return err
}
`,
exit_code=1,
},
{
name="inferred-hidden-sum",
source=`A :: enum { a }
B :: enum { b }
fail_a func() void ! A { return .a }
fail_b func() void ! B { return .b }
hide dispatch func(selector i32) void! {
if selector == 1 {
try fail_a()
return
}
try fail_b()
}
main func() void! { try dispatch(2) }
`,
exit_code=1,
},
{
name="inferred-process-main",
source=`process :: import "@std/process"
Error :: enum { failed }
fail func() void ! Error { return .failed }
main func(_ process.Init) void! { try fail() }
`,
exit_code=1,
},
@@ -3202,6 +3246,64 @@ main func(_ process.Init) void ! Error { try fail() }
}
}
@(test)
inferred_error_channels_reject_unstable_or_untyped_contracts :: proc(t: ^testing.T) {
Case :: struct {
source: string,
message: string,
}
cases := [?]Case{
{
source=`Error :: enum { failed }
fail func() void ! Error { return .failed }
visible func() void! { try fail() }
main func() void {}
`,
message="inferred error channels are only allowed on hidden functions and root main",
},
{
source=`Error :: enum { failed }
fail func() void ! Error { return .failed }
hide untyped func(flag bool) void! {
if flag {
try fail()
return
}
return .failed
}
main func() void { untyped(false) catch |_| {} }
`,
message="inferred error returns require a concretely typed error value",
},
{
source=`hide empty func() void! {}
main func() void { empty() catch |_| {} }
`,
message="could not infer a named error channel for 'empty'",
},
}
for test_case in cases {
source_file := source.Source{path="test.bro", text=test_case.source}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, test_case.message)
}
testing.expect(t, found)
hir.destroy_module(&hir_module)
ast.destroy_module(&ast_module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
}
@(test)
milestone_33_injects_explicit_io_provider_and_runs_std_io :: proc(t: ^testing.T) {
sources := source.init_store()