diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 3195b15..567e892 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -12712,12 +12712,88 @@ build_value_loop :: proc( 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), // 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 // `while true` cannot fall through because the language has no `break` statement. -// Recursion into the branch slices handles nested ifs and `else if` chains. -all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool { +// Exhaustive equality guards over one immutable enum local also terminate collectively. +all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id, locals: []hir.Local = nil) -> bool { for id in stmts { statement := module.statements[id] #partial switch statement.kind { @@ -12725,8 +12801,8 @@ all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool { return true case .If: if statement.else_body != nil && - all_paths_return(module, statement.then_body) && - all_paths_return(module, statement.else_body) { + all_paths_return(module, statement.then_body, locals) && + all_paths_return(module, statement.else_body, locals) { return true } 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: @@ -12967,7 +13043,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { block := build_block(&ctx, function.body) checker.current_result = previous_result 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 { append(&body, block_stmt) } diff --git a/compiler_tests.odin b/compiler_tests.odin index d3e5368..7d27a3b 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -9519,6 +9519,42 @@ main func() void { 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) conditional_unwrap_compiles_and_runs :: proc(t: ^testing.T) { 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 } +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 { _ = ordered 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.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 } `