finite-domain enum return analysis

This commit is contained in:
2026-07-20 15:12:20 +02:00
parent 10abba54a5
commit deab47e75e
2 changed files with 134 additions and 6 deletions
+82 -6
View File
@@ -12712,12 +12712,88 @@ build_value_loop :: proc(
return value, target.slot_type return value, target.slot_type
} }
Return_Enum_Guard :: struct {
local: hir.Local_Id,
type: types.Type,
value: i64,
}
return_enum_guard :: proc(
module: ^hir.Module,
statement: hir.Stmt,
locals: []hir.Local,
) -> (Return_Enum_Guard, bool) {
if statement.kind != .If || statement.else_body != nil ||
statement.expr == hir.INVALID_EXPR || int(statement.expr) >= len(module.exprs) {
return {}, false
}
condition := module.exprs[statement.expr]
if condition.kind != .Eq || condition.left == hir.INVALID_EXPR || condition.right == hir.INVALID_EXPR ||
int(condition.left) >= len(module.exprs) || int(condition.right) >= len(module.exprs) {
return {}, false
}
left := module.exprs[condition.left]
right := module.exprs[condition.right]
if left.kind == .Integer {
left, right = right, left
}
if left.kind != .Local || right.kind != .Integer ||
!types.equal(left.type, right.type) || !types.is_enum(left.type, &module.types) {
return {}, false
}
local := hir.as_local(left.target)
if local == hir.INVALID_LOCAL || int(local) >= len(locals) || locals[local].mutable {
return {}, false
}
if !all_paths_return(module, statement.then_body, locals) {
return {}, false
}
return Return_Enum_Guard{local=local, type=left.type, value=right.integer}, true
}
enum_guards_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id, locals: []hir.Local) -> bool {
guards: [dynamic]Return_Enum_Guard
guards.allocator = module.allocator
defer delete(guards)
for id in stmts {
if guard, ok := return_enum_guard(module, module.statements[id], locals); ok {
append(&guards, guard)
}
}
for guard, index in guards {
seen := false
for previous in guards[:index] {
seen = seen || previous.local == guard.local
}
if seen {
continue
}
members := types.enum_members_for(&module.types, guard.type)
if len(members) == 0 {
continue
}
complete := true
for member in members {
value := i64(member.value) if member.value < 0 else transmute(i64)u64(member.value)
covered := false
for candidate in guards {
covered = covered || candidate.local == guard.local && candidate.value == value
}
complete = complete && covered
}
if complete {
return true
}
}
return false
}
// Reports whether every control-flow path through `stmts` terminates (returns or traps), // Reports whether every control-flow path through `stmts` terminates (returns or traps),
// so the end of the block is unreachable. A `.Return` or `.Trap` terminates outright; an // so the end of the block is unreachable. A `.Return` or `.Trap` terminates outright; an
// `.If` terminates only when it has an `else` and both arms terminate. A literal // `.If` terminates only when it has an `else` and both arms terminate. A literal
// `while true` cannot fall through because the language has no `break` statement. // `while true` cannot fall through because the language has no `break` statement.
// Recursion into the branch slices handles nested ifs and `else if` chains. // Exhaustive equality guards over one immutable enum local also terminate collectively.
all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool { all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id, locals: []hir.Local = nil) -> bool {
for id in stmts { for id in stmts {
statement := module.statements[id] statement := module.statements[id]
#partial switch statement.kind { #partial switch statement.kind {
@@ -12725,8 +12801,8 @@ all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
return true return true
case .If: case .If:
if statement.else_body != nil && if statement.else_body != nil &&
all_paths_return(module, statement.then_body) && all_paths_return(module, statement.then_body, locals) &&
all_paths_return(module, statement.else_body) { all_paths_return(module, statement.else_body, locals) {
return true return true
} }
case .While: case .While:
@@ -12741,7 +12817,7 @@ all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
} }
} }
} }
return false return enum_guards_return(module, stmts, locals)
} }
// Reports whether `stmts` contains a `break` that targets the enclosing loop: // Reports whether `stmts` contains a `break` that targets the enclosing loop:
@@ -12967,7 +13043,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
block := build_block(&ctx, function.body) block := build_block(&ctx, function.body)
checker.current_result = previous_result checker.current_result = previous_result
checker.current_build_ctx = previous_ctx checker.current_build_ctx = previous_ctx
returns := all_paths_return(&checker.module, block) returns := all_paths_return(&checker.module, block, hir_locals[:])
for block_stmt in block { for block_stmt in block {
append(&body, block_stmt) append(&body, block_stmt)
} }
+52
View File
@@ -9519,6 +9519,42 @@ main func() void {
testing.expect(t, !found) testing.expect(t, !found)
} }
@(test)
exhaustive_enum_guard_sequences_return :: proc(t: ^testing.T) {
text := `E :: enum { a, b }
complete func(value E) i32 {
if (value == .a) { return 1 }
if (value == .b) { return 2 }
}
incomplete func(value E) i32 {
if (value == .a) { return 1 }
}
main func() void {
_ = complete(.a)
_ = incomplete(.a)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
missing_returns := 0
for diagnostic in diagnostics.items {
if strings.contains(diagnostic.message, "does not return a value") {
missing_returns += 1
}
}
testing.expect_value(t, missing_returns, 1)
}
@(test) @(test)
conditional_unwrap_compiles_and_runs :: proc(t: ^testing.T) { conditional_unwrap_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-conditional-unwrap" output := "/tmp/brolang-test-conditional-unwrap"
@@ -13844,6 +13880,18 @@ init func($E, $V type, values meta.EnumFieldStruct(E, ?V, some!(none))) Map(E, V
return map return map
} }
get func($E, $V type, map @Map(E, V), key E) ?V {
match typeinfo!(E) {
.enum |info|: expand for info.fields |field, index| {
if key == field!(E, field.name) {
if map.present[index] { return map.values[index] }
return none
}
}
else: compile_error!("EnumMap key must be an enum")
}
}
main func() i32 { main func() i32 {
_ = ordered _ = ordered
inferred :: {x = 40, name = "bro"} inferred :: {x = 40, name = "bro"}
@@ -13889,6 +13937,10 @@ main func() i32 {
}) })
if !map.present[0] or !map.present[1] or map.present[2] { return 9 } if !map.present[0] or !map.present[1] or map.present[2] { return 9 }
if map.values[0].len != 10 or map.values[1].len != 7 { return 18 } if map.values[0].len != 10 or map.values[1].len != 7 { return 18 }
if get(TokenKind, []u8, &map, TokenKind.ident) |value| {
if value.len != 10 { return 19 }
} else { return 20 }
if get(TokenKind, []u8, &map, TokenKind.eof) |_| { return 21 }
return 0 return 0
} }
` `