refactor: expose checked function fallthrough obligations

This commit is contained in:
2026-10-25 01:59:57 +02:00
parent 2761c77182
commit 2c11772931
4 changed files with 74 additions and 28 deletions
+3 -9
View File
@@ -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))
}
+28 -7
View File
@@ -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
+41 -12
View File
@@ -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