refactor: expose checked function fallthrough obligations
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user