error inference in fallible functions
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user