From 729488e7021c9c36d152247531404ff5114b1ac6 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sat, 8 Aug 2026 22:49:31 +0200 Subject: [PATCH] structs as errors in fallibles --- LANGUAGE.md | 2 +- compiler/checker/checker.odin | 9 ++++--- compiler/checker/comptime.odin | 13 ++++++++++ compiler/llvm/llvm.odin | 16 ++++++++++++ compiler_tests.odin | 45 ++++++++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 5 deletions(-) diff --git a/LANGUAGE.md b/LANGUAGE.md index 143e166..348283b 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -44,7 +44,7 @@ roadmap and milestone history. - named native struct fields may declare defaults with `field T = expression`; keyed literals use defaults for omitted fields and explicit initializers override them - 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/tagged union or supported sum composition; `void ! E` functions complete successfully on fallthrough, and void-success `catch` handlers may fall through without `yield` +- 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` #### distinct types diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 0cf7742..711004b 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -3976,19 +3976,20 @@ validate_declarations :: proc(checker: ^Checker) { } if types.is_valid(function.error) && !signature_poisoned { error_type := type_from_syntax(checker, function.error, function.pkg, function.file) - error_sum := types.is_enum(error_type, &checker.module.types) || - types.is_tagged_union(error_type, &checker.module.types) + 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) if function.c_abi { checker.template_diagnostics[function_id] = source.add( checker.diagnostics, function.span, "fallible functions must use 'func', not 'c_func'", ) - } else if !error_sum { + } else if !error_channel { checker.template_diagnostics[function_id] = source.add( checker.diagnostics, function.span, - "fallible function error type must be a native enum or tagged union", + "fallible function error type must be a native enum, struct, or tagged union", ) } } diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index 96864e3..0f0e2b4 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -4487,6 +4487,19 @@ ct_return_value :: proc(state: ^Ct_State, expr_id: ast.Expr_Id, span: source.Spa if expr.kind == .Enum_Literal && types.sum_has_name(&checker.module.types, error_type, u32(expr.name)) { error_path = true expected = error_type + } else if expr.kind == .Struct_Literal { + target_pkg, available := expr_package(checker, expr, state.pkg, state.file, true) + named := types.find_named( + &checker.module.types, + u32(target_pkg), + u32(expr.name), + file=u32(expr_lookup_file(expr, state.file)), + ) if available else types.INVALID + named = types.resolve_alias(named, &checker.module.types) + if can_implicitly_convert_type(checker, named, error_type) { + error_path = true + expected = error_type + } } else if expr.kind == .Name && !symbol.is_valid(expr.qualifier) { if index, found := ct_find_binding_index(state, expr.name); found { actual := state.bindings[index].type diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index 3e9556b..4c5182f 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -1050,6 +1050,16 @@ emit_instruction_stream :: proc( fmt.sbprintf(&emitter.builder, " %%fallible_payload%d = getelementptr i8, ptr %%fallible_slot%d, i64 %d\n", instruction_index, instruction_index, payload_offset) fmt.sbprintf(&emitter.builder, " call void @llvm.memcpy.p0.p0.i64(ptr %%fallible_payload%d, ptr %%fallible_error_payload%d, i64 %d, i1 false)\n", instruction_index, instruction_index, error_payload_size) } + } else if types.is_struct(error_type, &emitter.module.types) { + if !valid_value(instructions, instruction.args[0], error_type, &emitter.module.types) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid fallible struct error operand") + continue + } + fmt.sbprintf(&emitter.builder, " store i16 1, ptr %%fallible_slot%d\n", instruction_index) + fmt.sbprintf(&emitter.builder, " %%fallible_payload%d = getelementptr i8, ptr %%fallible_slot%d, i64 %d\n", instruction_index, instruction_index, payload_offset) + fmt.sbprintf(&emitter.builder, " store %s ", llvm_type(error_type, &emitter.module.types)) + write_operand(&emitter.builder, instructions, instruction.args[0], error_type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, ", ptr %%fallible_payload%d\n", instruction_index) } fmt.sbprintf(&emitter.builder, " %%v%d = load %s, ptr %%fallible_slot%d\n", instruction_index, type_name, instruction_index) continue @@ -1389,6 +1399,12 @@ emit_instruction_stream :: proc( fmt.sbprintf(&emitter.builder, " %%v%d = load %s, ptr %%fallible_error_slot%d\n", instruction_index, type_name, instruction_index) continue } + if types.is_struct(error_type, &emitter.module.types) { + source_offset := types.fallible_payload_offset(channel_type, &emitter.module.types, emitter.module.target) + fmt.sbprintf(&emitter.builder, " %%fallible_error_source%d = getelementptr i8, ptr %%v%d, i64 %d\n", instruction_index, instruction.a, source_offset) + fmt.sbprintf(&emitter.builder, " %%v%d = load %s, ptr %%fallible_error_source%d\n", instruction_index, llvm_type(error_type, &emitter.module.types), instruction_index) + continue + } emit_recovery_value(emitter, instruction_index, instruction, "unsupported fallible error type") case .Store: if !valid_address(instructions, instruction.a, instruction.type, &emitter.module.types) || diff --git a/compiler_tests.odin b/compiler_tests.odin index c28d21a..de349b0 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -6953,6 +6953,51 @@ main func() i32 { testing.expect_value(t, state.exit_code, 0) } +@(test) +struct_error_channels_compile_and_run_at_runtime_and_comptime :: proc(t: ^testing.T) { + text := `ScanError :: struct { + code i32 + start usize + end usize +} +scan func(fail bool) i32 ! ScanError { + if fail { + return ScanError { code = 5, start = 7, end = 11 } + } + return 41 +} +scan_local func() i32 ! ScanError { + err ScanError := ScanError { code = 3, start = 13, end = 17 } + return err +} +forward func(fail bool) i32 ! ScanError { + return try scan(fail) +} +score_comptime func() i32 { + return scan(true) catch |err| err.code + i32(err.start) + i32(err.end) +} +main func() i32 { + direct :: scan(true) catch |err| err.code + i32(err.start) + i32(err.end) + local :: scan_local() catch |err| err.code + i32(err.start) + i32(err.end) + forwarded :: forward(true) catch |err| err.code + i32(err.start) + i32(err.end) + success :: forward(false) catch 0 + comptime_score i32 :: $score_comptime() + return direct + local + forwarded + success + comptime_score - 143 +} +` + directory := "/tmp/brolang-test-struct-error-channel" + main_path := "/tmp/brolang-test-struct-error-channel/main.bro" + output := "/tmp/brolang-test-struct-error-channel-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) matched_error_residuals_return_at_runtime_and_comptime :: proc(t: ^testing.T) { text := `KeyError :: enum { key_exists }