From 2c11772931ef4f91e0b635d1698dd98ce3c854df Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sun, 25 Oct 2026 01:59:57 +0200 Subject: [PATCH] refactor: expose checked function fallthrough obligations --- README.md | 2 ++ source/lower/lower.hon | 12 ++------ source/sema/check/check.hon | 35 ++++++++++++++++----- source/sema/check/check.test.hon | 53 ++++++++++++++++++++++++-------- 4 files changed, 74 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 2a972a8..426d15e 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,8 @@ resolver scopes use `@std/hashmap.AutoHashMap(K, V)`, which shares `init`, `put` type checking (`@source/sema/check`) runs after name resolution. `check.check` borrows the AST and binding table and returns owned per-node types and interned array descriptors; query with `check.type_of` and `check.array_type`, and release with `check.deinit`. declarations infer their initializer types, including forward references and function aliases; function types identify the declaration supplying the callable signature. annotations support builtin types and fixed arrays; type names are not ordinary values. `void` is valid as a function result, not as a parameter or stored value. +function fallthrough obligations are separate from body types. `check.fallthrough_diagnostic(&result, function)` accepts a function declaration's `NodeId` and returns its original missing-return diagnostic ID, or `null` when no obligation exists. nested body errors retain their original identities. lowering consumes this fact without interpreting diagnostic codes and places its trap only on a surviving fallthrough path; `check.deinit` releases the obligation storage. + the checker keeps union-find parent links and group sizes in a compact array separate from per-node inference metadata. root walks and path compression access only that link storage; both temporary arrays are released before `check.check` returns. numeric typing follows contextual rules within Honey's current `int` and `float` families. integer literals can adopt a supported numeric family only when their value is exactly representable; float literals remain float and opt into ordinary IEEE rounding. numeric demands propagate through arithmetic and inferred bindings before undemanded integers default to `int`. conflicting demands are errors; fixed `int`/`i64`, `usize`, and `float`/`f64` values do not implicitly convert between families. `+`, `-`, `*` and unary `-` require compatible numeric operands; `/` requires and supplies float context. `3 / 2`, `3.0 / 2`, and `ratio float :: 3 / 2` are valid. `value f64 :: 9007199254740993` is diagnosed, including when float context arrives through aliases, division, or array elements; `9007199254740993.0` explicitly permits rounding. exactly representable integers beyond the signed/unsigned integer ranges remain valid in float context. explicit numeric casts are not yet implemented. diff --git a/source/lower/lower.hon b/source/lower/lower.hon index 717ebd4..ea19a22 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -300,15 +300,9 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di if (trap_type(state, id)) return try lower_block(state, declaration.body, false) if (state.terminated) return - # aggregate arm errors are not demanded by fallthrough; only missing_return is. - body_type :: typecheck.type_of(state.checked, declaration.body) - if (function.result != .void and body_type.kind == .invalid) { - if (diagnostics.get(state.diagnostics, body_type.diagnostic)) |diagnostic| { - if (diagnostic.code == .missing_return) { - trap(state, body_type.diagnostic, ast.node_span(tree, declaration.body)) - return - } - } + if (typecheck.fallthrough_diagnostic(state.checked, id)) |diagnostic| { + trap(state, diagnostic, ast.node_span(tree, declaration.body)) + return } finish(state, ir.no_value, ast.node_span(tree, declaration.body)) } diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index 31e0721..296200a 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -1,6 +1,7 @@ import "@std/debug" import "@std/mem" import "@std/arraylist" +import "@std/hashmap" import "@source/ast" import "@source/diagnostics" sema :: import "@source/sema" @@ -40,11 +41,12 @@ Type :: struct { Storage :: struct { types []mut Type arrays arraylist.ArrayList(ArrayType) + fallthroughs hashmap.AutoHashMap(ast.NodeId, diagnostics.DiagnosticId) allocator mem.Allocator } -#! owns node-indexed types and interned array descriptors; release with deinit. -#! function IDs refer to the caller-owned AST, not checker-owned signatures. +#! owns node-indexed types, interned array descriptors, and function fallthrough obligations; release with deinit. +#! function IDs refer to the caller-owned AST; diagnostic IDs refer to the caller-owned store. Result :: struct { storage Storage } @hide:file @@ -118,7 +120,7 @@ State :: struct { } #! borrows the AST, resolution, and diagnostic store only during checking. -#! temporary constraints are freed on every exit; Result owns types and array descriptors. +#! temporary constraints are freed on every exit; Result owns types, array descriptors, and fallthrough obligations. check proc( allocator mem.Allocator, tree @ast.Tree, @@ -360,6 +362,8 @@ check proc( } } propagate(&state, queue) + fallthroughs hashmap.AutoHashMap(ast.NodeId, diagnostics.DiagnosticId) := hashmap.init(allocator) + errdefer hashmap.deinit(&fallthroughs) for (0..ast.package_decl_count(tree)) |i| { id :: ast.package_decl(tree, i) if (ast.decl_kind(tree, id) != .function) continue @@ -367,12 +371,22 @@ check proc( result :: types[usize(function.return_type)] if (result.kind == .void or result.kind == .invalid or types[usize(id)].kind == .invalid) continue if (block_falls_through(&state, function.body)) { - # lowering must find this diagnostic even when another arm failed. + # fallthrough is independent of errors in paths that may not execute. diagnostic :: try diagnostics.add(diagnostic_store, .missing_return, ast.node_span(tree, function.body)) - types[usize(function.body)] = invalid_type(diagnostic) + hashmap.put(&fallthroughs, id, diagnostic) catch |err| { + match err { + .key_exists: unreachable + else: return err + } + } } } - return Result{ storage = Storage{ types = types, arrays = state.arrays, allocator = allocator } } + return Result{ storage = Storage{ + types = types, + arrays = state.arrays, + fallthroughs = fallthroughs, + allocator = allocator, + } } } #! returns a type without retaining a reference to checker temporaries. @@ -381,6 +395,12 @@ type_of proc(result @Result, id ast.NodeId) Type { return result.storage.types[usize(id)] } +#! returns the original diagnostic for a function's nonvoid fallthrough, independent of body errors. +#! null means the function has no fallthrough obligation; lowering still owns trap placement. +fallthrough_diagnostic proc(result @Result, function ast.NodeId) ?diagnostics.DiagnosticId { + return hashmap.get(&result.storage.fallthroughs, function) +} + array_type_count proc(result @Result) usize { return result.storage.arrays.items.len } @@ -390,10 +410,11 @@ array_type proc(result @Result, id ArrayId) ArrayType { return result.storage.arrays.items[usize(id)] } -#! releases type and descriptor storage using the allocator retained by check. +#! releases type, descriptor, and fallthrough storage using the allocator retained by check. deinit proc(result @mut Result) void { mem.free(result.storage.allocator, result.storage.types) arraylist.deinit(&result.storage.arrays) + hashmap.deinit(&result.storage.fallthroughs) } @hide:file diff --git a/source/sema/check/check.test.hon b/source/sema/check/check.test.hon index bbc7d5a..36c78ee 100644 --- a/source/sema/check/check.test.hon +++ b/source/sema/check/check.test.hon @@ -34,6 +34,24 @@ expect_error proc( return actual.diagnostic } +@hide:file +expect_fallthrough proc( + result @Result, + diagnostic_store @diagnostics.Store, + function ast.NodeId, + span ast.Span, +) diagnostics.DiagnosticId ! testing.Error { + if (fallthrough_diagnostic(result, function)) |id| { + if (diagnostics.get(diagnostic_store, id)) |diagnostic| { + try testing.expect_equal(diagnostics.Code.missing_return, diagnostic.code) + try testing.expect_equal(span.start, diagnostic.span.start) + try testing.expect_equal(span.end, diagnostic.span.end) + return id + } + } + return .expectation_failed +} + @hide:file code_count proc(diagnostic_store @diagnostics.Store, code diagnostics.Code) usize { count := usize(0) @@ -376,14 +394,16 @@ reports_nonvoid_fallthrough_without_poisoning_signatures_and_checks_unused_bodie try expect_kind(&result, function, .function) try testing.expect_equal(function, type_of(&result, function).function) try expect_kind(&result, view.return_type, returns[index]) - _ = try expect_error(&result, &diagnostic_store, view.body, .missing_return, ast.node_span(&tree, view.body)) + _ = try expect_fallthrough(&result, &diagnostic_store, function, ast.node_span(&tree, view.body)) } broken :: ast.package_decl(&tree, 4) try expect_kind(&result, broken, .function) + try testing.expect_equal(null, fallthrough_diagnostic(&result, broken)) broken_body :: ast.function_decl(&tree, broken).body bad_operand :: ast.statement_expr(&tree, ast.block_statement(&tree, broken_body, 0)) _ = try expect_error(&result, &diagnostic_store, bad_operand, .invalid_operand, ast.node_span(&tree, bad_operand)) main_body :: ast.function_decl(&tree, ast.package_decl(&tree, 5)).body + try testing.expect_equal(null, fallthrough_diagnostic(&result, ast.package_decl(&tree, 5))) call :: ast.statement_expr(&tree, ast.block_statement(&tree, main_body, 0)) try expect_kind(&result, call, .int) try testing.expect_equal(2, code_count(&diagnostic_store, .missing_return)) @@ -721,8 +741,9 @@ return_errors_preserve_identity_check_unreachable_code_and_suppress_missing_retu try expect_kind(&result, ast.block_statement(&tree, unreachable_body, 0), .void) bad :: ast.statement_expr(&tree, ast.block_statement(&tree, unreachable_body, 1)) _ = try expect_error(&result, &diagnostic_store, bad, .invalid_operand, ast.node_span(&tree, bad)) - missing_body :: ast.function_decl(&tree, ast.package_decl(&tree, 6)).body - _ = try expect_error(&result, &diagnostic_store, missing_body, .missing_return, ast.node_span(&tree, missing_body)) + missing :: ast.package_decl(&tree, 6) + missing_body :: ast.function_decl(&tree, missing).body + _ = try expect_fallthrough(&result, &diagnostic_store, missing, ast.node_span(&tree, missing_body)) recovered_body :: ast.function_decl(&tree, ast.package_decl(&tree, 7)).body try expect_kind(&result, ast.block_statement(&tree, recovered_body, 1), .int) try testing.expect_equal(3, code_count(&diagnostic_store, .return_type_mismatch)) @@ -928,8 +949,9 @@ analyzes_return_paths_after_trap_propagation_without_poisoning_signatures test { for (0..usize(11)) |i| try expect_kind(&result, ast.package_decl(&tree, i), .function) for (usize(1)..usize(3)) |i| { - body :: ast.function_decl(&tree, ast.package_decl(&tree, i)).body - _ = try expect_error(&result, &diagnostic_store, body, .missing_return, ast.node_span(&tree, body)) + function :: ast.package_decl(&tree, i) + body :: ast.function_decl(&tree, function).body + _ = try expect_fallthrough(&result, &diagnostic_store, function, ast.node_span(&tree, body)) } bad_body :: ast.function_decl(&tree, ast.package_decl(&tree, 2)).body conditional :: ast.block_statement(&tree, bad_body, 0) @@ -938,7 +960,7 @@ analyzes_return_paths_after_trap_propagation_without_poisoning_signatures test { operand :: ast.statement_expr(&tree, ast.block_statement(&tree, arm, 0)) original :: try expect_error(&result, &diagnostic_store, operand, .invalid_operand, ast.node_span(&tree, operand)) try testing.expect_equal(original, type_of(&result, arm).diagnostic) - try testing.expect(type_of(&result, bad_body).diagnostic != original) + try testing.expect(fallthrough_diagnostic(&result, ast.package_decl(&tree, 2))? != original) condition_body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body condition :: ast.if_stmt(&tree, ast.block_statement(&tree, condition_body, 0)).condition condition_diag :: try expect_error(&result, &diagnostic_store, condition, .condition_not_bool, ast.node_span(&tree, condition)) @@ -964,6 +986,11 @@ analyzes_return_paths_after_trap_propagation_without_poisoning_signatures test { } else { return .expectation_failed } + for 0..ast.package_decl_count(&tree) |i| { + function :: ast.package_decl(&tree, i) + if (i == 1 or i == 2 or ast.decl_kind(&tree, function) != .function) continue + try testing.expect_equal(null, fallthrough_diagnostic(&result, function)) + } try testing.expect_equal(1, code_count(&diagnostic_store, .return_type_mismatch)) try testing.expect_equal(2, code_count(&diagnostic_store, .missing_return)) try testing.expect_equal(1, code_count(&diagnostic_store, .condition_not_bool)) @@ -1212,8 +1239,9 @@ while_checks_nested_bodies_without_claiming_a_zero_iteration_return test { defer deinit(&result) for usize(0)..usize(2) |index| { - body :: ast.function_decl(&tree, ast.package_decl(&tree, index)).body - _ = try expect_error(&result, &diagnostic_store, body, .missing_return, ast.node_span(&tree, body)) + function :: ast.package_decl(&tree, index) + body :: ast.function_decl(&tree, function).body + _ = try expect_fallthrough(&result, &diagnostic_store, function, ast.node_span(&tree, body)) try expect_kind(&result, ast.block_statement(&tree, body, 0), .void) } continues_body :: ast.function_decl(&tree, ast.package_decl(&tree, 2)).body @@ -1526,7 +1554,7 @@ short_circuit_skip_paths_do_not_guarantee_a_return test { function :: ast.package_decl(&tree, index) try expect_kind(&result, function, .function) body :: ast.function_decl(&tree, function).body - _ = try expect_error(&result, &diagnostic_store, body, .missing_return, ast.node_span(&tree, body)) + _ = try expect_fallthrough(&result, &diagnostic_store, function, ast.node_span(&tree, body)) } expression_body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body expression :: ast.statement_expr(&tree, ast.block_statement(&tree, expression_body, 0)) @@ -1670,8 +1698,9 @@ loop_controls_do_not_guarantee_function_returns test { defer deinit(&result) for (usize(0)..usize(3)) |index| { - body :: ast.function_decl(&tree, ast.package_decl(&tree, index)).body - _ = try expect_error(&result, &diagnostic_store, body, .missing_return, ast.node_span(&tree, body)) + function :: ast.package_decl(&tree, index) + body :: ast.function_decl(&tree, function).body + _ = try expect_fallthrough(&result, &diagnostic_store, function, ast.node_span(&tree, body)) try expect_kind(&result, ast.block_statement(&tree, body, 0), .void) } closed_body :: ast.function_decl(&tree, ast.package_decl(&tree, 3)).body @@ -2037,7 +2066,7 @@ range_loop_return_flow_and_controls_are_lexical_and_body_errors_are_isolated tes function :: ast.package_decl(&tree, index) body :: ast.function_decl(&tree, function).body try expect_kind(&result, function, .function) - _ = try expect_error(&result, &diagnostic_store, body, .missing_return, ast.node_span(&tree, body)) + _ = try expect_fallthrough(&result, &diagnostic_store, function, ast.node_span(&tree, body)) try expect_kind(&result, ast.block_statement(&tree, body, 0), .void) } closed_body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body