From 9f433af7246f29dc95445ef01f4dbf2699197dc6 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sat, 18 Jul 2026 00:30:57 +0200 Subject: [PATCH] preserve application and error inference in test builds --- LANGUAGE.md | 3 ++- compiler/ast/ast.odin | 1 + compiler/checker/checker.odin | 29 +++++++++++++++++++++++++++++ compiler/testing.odin | 19 ++++++++++++++++--- compiler_tests.odin | 25 +++++++++++++++++++++++-- 5 files changed, 71 insertions(+), 6 deletions(-) diff --git a/LANGUAGE.md b/LANGUAGE.md index 977b41d..1939810 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -18,7 +18,8 @@ roadmap and milestone history. - `hide` makes any named top-level declaration file-local; declarations are public by default, leading underscores are ordinary identifier characters, and imports are always file-local - relative `.h` imports as synthetic C header package namespaces -- native `name test { ... }` declarations with implicit fallible-void results, plus anonymous +- native `name test { ... }` declarations with fallible-void results inferred from `testing.Error` + and errors propagated by `try`, plus anonymous transitive `test import "..."` discovery used only by test builds - root `main` validation with trap executable recovery for missing or unusable entry points diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index d748859..ba87f2f 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -227,6 +227,7 @@ Function :: struct { c_abi: bool, imported: bool, generated: bool, + analysis_root: bool, test: bool, file_hidden: bool, has_body: bool, diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 991b7a5..d518559 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -229,6 +229,7 @@ Checker :: struct { sink_symbol: symbol.Id, type_symbol: symbol.Id, current_result: types.Type, + inferred_test_error: ^types.Type, current_build_ctx: ^Build_Ctx, current_comptime_values: []Comptime_Value, static_state: Ct_State, @@ -4129,6 +4130,17 @@ Infer_Frame :: struct { template: ast.Function_Id, } +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) { + return + } + if merged, err := types.compose_sum(&checker.module.types, current^, incoming); err == .None { + current^ = merged + } +} + infer_nested_expr :: proc( checker: ^Checker, expr_id: ast.Expr_Id, @@ -4313,6 +4325,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)) 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 @@ -5488,7 +5501,16 @@ 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) + } 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 @@ -5948,6 +5970,11 @@ infer_all :: proc(checker: ^Checker) { } } + for function, index in checker.ast_module.functions { + if function.analysis_root { + ensure_spec(checker, ast.function_id(index), nil) + } + } main_template := find_template(checker, checker.main_symbol, 0) if main_template != ast.INVALID_FUNCTION { ensure_spec(checker, main_template, nil) @@ -6035,7 +6062,9 @@ infer_all :: proc(checker: ^Checker) { for index := 0; index < len(checker.specs); index += 1 { id := spec_id(index) + before := checker.specs[id].result inferred := infer_spec_result(checker, id) + changed = !types.equal(before, checker.specs[id].result) || changed changed = merge_inferred_type(&checker.module.types, &checker.specs[id].result, inferred) || changed } if len(checker.specs) != spec_count { diff --git a/compiler/testing.odin b/compiler/testing.odin index fa257e8..9c76d15 100644 --- a/compiler/testing.odin +++ b/compiler/testing.odin @@ -165,20 +165,28 @@ append_runner :: proc( diagnostics: ^source.Diagnostics, symbols: ^symbol.Table, testing_pkg: ast.Package_Id, + testing_error: types.Type, tests: []Test_Entry, ) { builder := strings.builder_make(module.allocator) defer strings.builder_destroy(&builder) + for entry, index in tests { + test := module.functions[entry.function] + alias := fmt.tprintf("__brolang_test_%d", index) + fmt.sbprintf(&builder, "hide __brolang_test_adapter_%d func() void ! __brolang_testing.Error ", index) + strings.write_string(&builder, "{\n\t") + fmt.sbprintf(&builder, "%s.%s() catch |_| ", alias, symbol.resolve(symbols, test.name)) + strings.write_string(&builder, "{\n\t\treturn .expectation_failed\n\t}\n}\n\n") + } strings.write_string(&builder, "main func() i32 {\n\tfailed i32 = 0\n") for entry, index in tests { test_id := entry.function test := module.functions[test_id] pkg := module.packages[test.pkg] - alias := fmt.tprintf("__brolang_test_%d", index) name := fmt.tprintf("%s.%s", filepath.base(pkg.path), symbol.resolve(symbols, test.name)) strings.write_string(&builder, "\tif (!__brolang_testing.run(\"") write_brolang_string(&builder, name) - fmt.sbprintf(&builder, "\", %s.%s)) ", alias, symbol.resolve(symbols, test.name)) + fmt.sbprintf(&builder, "\", __brolang_test_adapter_%d)) ", index) strings.write_string(&builder, "{\n\t\tfailed += 1\n\t}\n") } fmt.sbprintf(&builder, "\t__brolang_testing.summary(%d - failed, failed)\n", len(tests)) @@ -190,7 +198,11 @@ append_runner :: proc( append(&module.files, ast.File{source=source_id, pkg=0}) stream := lexer.lex(&sources.items[source_id], diagnostics, symbols, module.allocator) defer delete(stream.items) + function_start := len(module.functions) parser.parse_into(&stream, &sources.items[source_id], diagnostics, module, 0, file_id) + for index in 0..