diff --git a/LANGUAGE.md b/LANGUAGE.md index eee5560..92849e0 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -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 diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index 908124a..5b11e62 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -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, diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index df039a7..fe2a899 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -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, diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index b9c694a..3808791 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -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, }) diff --git a/compiler_tests.odin b/compiler_tests.odin index 04b4bf9..cac7fcb 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -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()