From 0b0b00a0506059b3f5f4c64b43d15152a0bf8b7d Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Mon, 13 Jul 2026 23:34:14 +0200 Subject: [PATCH] bug fixes --- LANGUAGE.md | 2 +- compiler/checker/checker.odin | 34 ++++++++++++----- compiler/checker/comptime.odin | 9 +++++ compiler/hir/hir.odin | 10 +++-- compiler/lower/lower.odin | 9 ++++- compiler_tests.odin | 67 ++++++++++++++++++++++++++++++++-- 6 files changed, 114 insertions(+), 17 deletions(-) diff --git a/LANGUAGE.md b/LANGUAGE.md index d3254bf..de6f0ab 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -37,7 +37,7 @@ roadmap and milestone history. - source-order native structs, opaque nominal records with `Name :: opaque`, complete `c_struct { ... }`, keyed record literals, native untagged unions, and native tagged unions `union(Enum)` / `union(enum)` - 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, using program-global `u16` variant ids -- fallible channel types `T ! E`, where `E` is a native enum/tagged union or supported sum composition +- 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` #### native record constraint fields diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 392fb86..1a69799 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -5913,6 +5913,7 @@ build_compound_expr :: proc( body: []hir.Stmt_Id capture := hir.INVALID_LOCAL block_handler := false + void_fallthrough := false fallback := hir.INVALID_EXPR if expr.right != ast.INVALID_EXPR { fallback = build_nested_expr(checker, expr.right, locals, global_reads, calls, success, pkg, file) @@ -5932,14 +5933,19 @@ build_compound_expr :: proc( handler: [dynamic]hir.Stmt_Id handler.allocator = checker.allocator fallback, _ = build_value_source(ctx, &handler, expr.body, success, expr.span, allow_exit=true) + void_fallthrough = fallback == hir.INVALID_EXPR && !all_paths_exit(&checker.module, handler[:]) body = handler[:] resize(ctx.locals, capture_start) } + catch_mode := hir.CATCH_EXPRESSION + if block_handler { + catch_mode = hir.CATCH_VOID_FALLTHROUGH if void_fallthrough else hir.CATCH_BLOCK + } return add_hir_expr(checker, hir.Expr{ kind=.Catch, span=expr.span, type=success, - integer=1 if block_handler else 0, + integer=catch_mode, left=channel, right=fallback, body=body, @@ -8236,10 +8242,11 @@ build_block :: proc( } // build_value_block builds a `{ ... yield v }` value block whose final statement -// must be a `yield`. Catch handlers may instead exit on every path, in which case -// `allow_exit` leaves the fallback expression invalid. Otherwise it builds the leading -// statements inline, evaluates the yield in their scope, then captures the value before -// running defers. `expected` is the binding's type (INVALID for an untyped `::`). +// must be a `yield`. Catch handlers may instead exit on every path or complete with +// void, in which case `allow_exit` leaves the fallback expression invalid. Otherwise +// it builds the leading statements inline, evaluates the yield in their scope, then +// captures the value before running defers. `expected` is the binding's type (INVALID +// for an untyped `::`). build_value_block :: proc( ctx: ^Build_Ctx, body: ^[dynamic]hir.Stmt_Id, @@ -8257,7 +8264,7 @@ build_value_block :: proc( for s in inner { append(body, s) } - if allow_exit && all_paths_exit(&checker.module, inner) { + if allow_exit && (types.is_void(expected) || all_paths_exit(&checker.module, inner)) { delete(inner, checker.allocator) return hir.INVALID_EXPR, expected } @@ -9785,7 +9792,16 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { } delete(block, checker.allocator) - if !types.is_void(spec.result) && !returns { + fallible_void := types.kind(spec.result, &checker.module.types) == .Fallible && + types.is_void(types.fallible_success(spec.result, &checker.module.types)) + if !returns && fallible_void { + value := fallible_aggregate(checker, function.span, spec.result, hir.INVALID_EXPR, false) + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind=.Return, span=function.span, expr=value, local=hir.INVALID_LOCAL, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } else if !types.is_void(spec.result) && !returns { id := source.addf( checker.diagnostics, function.span, @@ -9944,8 +9960,8 @@ build_globals :: proc(checker: ^Checker) { } expr := build_expr(checker, global.expr, nil, &dependencies, &calls, expected, global.pkg, global.file) global_type := checker.global_types[global_index] - if is_runtime_type(checker, declared) { - expr = coerce_expr(checker, expr, declared, global.span) + if is_runtime_type(checker, checker.global_types[global_index]) { + expr = coerce_expr(checker, expr, checker.global_types[global_index], global.span) global_type = checker.module.exprs[expr].type } else if is_runtime_type(checker, checker.module.exprs[expr].type) { global_type = checker.module.exprs[expr].type diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index efb2437..151383d 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -2196,6 +2196,11 @@ ct_eval_template_call :: proc( result := ct_add_value(state, Ct_Value{kind=.Void, type=types.VOID}) return result, ct_flow(.Normal), true } + if flow.kind == .Normal && types.kind(result_type, &checker.module.types) == .Fallible && + types.is_void(types.fallible_success(result_type, &checker.module.types)) { + result := ct_make_fallible(state, result_type, INVALID_CT_VALUE, false) + return result, ct_flow(.Normal), true + } return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, span, "comptime function '%s' did not return a value", symbol_text(checker, function.name)) } result := flow.value @@ -2304,6 +2309,10 @@ ct_eval_catch_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Typ } return handler.value, ct_flow(.Normal), true } + if handler.kind == .Normal && types.is_void(success) { + value := ct_add_value(state, Ct_Value{kind=.Void, type=types.VOID}) + return value, ct_flow(.Normal), true + } return INVALID_CT_VALUE, handler, ct_fail(state, .Not_Comptime, expr.span, "catch block must yield a value") } diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index f5800fc..097ab82 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -132,6 +132,10 @@ Expr_Kind :: enum u8 { Call, } +CATCH_EXPRESSION :: i64(0) +CATCH_BLOCK :: i64(1) +CATCH_VOID_FALLTHROUGH :: i64(2) + Expr :: struct { span: source.Span, type: types.Type, @@ -140,9 +144,9 @@ Expr :: struct { // encoded as Expr_Id because it otherwise has no args. args: []Expr_Id, // `Catch` block handlers use `body` for the handler statements and `target` - // for the optional captured error local. A missing `right` means the handler - // exits on every path and therefore has no fallback value. `Try` uses `body` - // for active error-exit cleanup. + // for the optional captured error local. `integer` selects the catch mode; + // a missing `right` means the block exits or completes with void. `Try` uses + // `body` for active error-exit cleanup. body: []Stmt_Id, target: Ref, left: Expr_Id, diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 6333e10..23f346c 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -468,7 +468,7 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi diagnostic=source.INVALID_DIAGNOSTIC, }) } else { - if expr.integer != 0 { + if expr.integer != hir.CATCH_EXPRESSION { capture := hir.as_local(expr.target) if capture != hir.INVALID_LOCAL && int(capture) < len(state.func_locals) { error_type := state.func_locals[capture].type @@ -491,6 +491,13 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi }) } lower_statements(state, expr.body) + if expr.integer == hir.CATCH_VOID_FALLTHROUGH { + append_instruction(state, ir.Instruction{ + op=.Br, span=expr.span, type=types.VOID, integer=merge_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } } if expr.right != hir.INVALID_EXPR { fallback := lower_nested_expr(state, expr.right) diff --git a/compiler_tests.odin b/compiler_tests.odin index bb869cd..fd27ff3 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2585,9 +2585,8 @@ main func() i32 { testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(hir_module.functions), 3) - for global in hir_module.globals { - testing.expect(t, types.equal(global.type, types.I8)) - } + testing.expect(t, types.equal(hir_module.globals[0].type, types.I32)) + testing.expect(t, types.equal(hir_module.globals[1].type, types.I8)) } @(test) @@ -4249,6 +4248,50 @@ main func() i32 { testing.expect_value(t, state.exit_code, 0) } +@(test) +fallible_void_fallthrough_compiles_at_runtime_and_comptime :: proc(t: ^testing.T) { + text := `Failure :: enum { + bad +} +succeed func() void ! Failure {} +fail func() void ! Failure { return .bad } +maybe func(should_fail bool) void ! Failure { + if should_fail { return .bad } +} +recover func(exit bool) i32 { + fail() catch |_| { + if exit { return 7 } + } + return 0 +} +comptime_scenario func() i32 { + succeed() catch |_| {} + fail() catch |_| {} + return 0 +} +main func() i32 { + comptime_result i32 :: $comptime_scenario() + handled bool = false + fail() catch |_| { handled = true } + if !handled { return 3 } + succeed() catch |_| { return 1 } + maybe(false) catch |_| { return 2 } + return comptime_result + recover(false) + recover(true) - 7 +} +` + directory := "/tmp/brolang-test-fallible-void-fallthrough" + main_path := "/tmp/brolang-test-fallible-void-fallthrough/main.bro" + output := "/tmp/brolang-test-fallible-void-fallthrough-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) conversion_diagnostics_render_source_types :: proc(t: ^testing.T) { text := `Allocator :: struct { @@ -6739,6 +6782,24 @@ valid_runtime_global_initializes_before_main :: proc(t: ^testing.T) { testing.expect_value(t, state.exit_code, 0) } +@(test) +inferred_string_global_decays_to_demanded_slice :: proc(t: ^testing.T) { + text := "consume func(value []u8) usize { return value.len }\n" + + "program ::\n\t`abc\n" + + "main func() i32 {\n\tif consume(program) == 3 { return 0 }\n\treturn 1\n}\n" + directory := "/tmp/brolang-test-inferred-string-global" + main_path := "/tmp/brolang-test-inferred-string-global/main.bro" + output := "/tmp/brolang-test-inferred-string-global-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) mutable_runtime_globals_compile_and_run :: proc(t: ^testing.T) { output := "/tmp/brolang-test-mutable-global"