diff --git a/src/ir/lower.test.zig b/src/ir/lower.test.zig index cf8f1fd..a09c264 100644 --- a/src/ir/lower.test.zig +++ b/src/ir/lower.test.zig @@ -710,3 +710,61 @@ test "E1.4b converge inferred error sets: empty -> warning, raising -> converged try std.testing.expect(stub_warned); try std.testing.expect(!raiser_warned); } + +test "E1.4c noreturn typing: divergence shapes + if-else unification + block propagation" { + const alloc = std.testing.allocator; + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var lowering = Lowering.init(&module); + + const mk = struct { + fn node(a: std.mem.Allocator, data: ast.Node.Data) *Node { + const n = a.create(Node) catch unreachable; + n.* = .{ .span = .{ .start = 0, .end = 0 }, .data = data }; + return n; + } + }; + + // return; / break; / continue; / raise error.X → noreturn + const ret = mk.node(alloc, .{ .return_stmt = .{ .value = null } }); + defer alloc.destroy(ret); + const brk = mk.node(alloc, .{ .break_expr = {} }); + defer alloc.destroy(brk); + const cont = mk.node(alloc, .{ .continue_expr = {} }); + defer alloc.destroy(cont); + const err_id = mk.node(alloc, .{ .identifier = .{ .name = "error" } }); + defer alloc.destroy(err_id); + const fa = mk.node(alloc, .{ .field_access = .{ .object = err_id, .field = "X" } }); + defer alloc.destroy(fa); + const raise = mk.node(alloc, .{ .raise_stmt = .{ .tag = fa } }); + defer alloc.destroy(raise); + + try std.testing.expectEqual(TypeId.noreturn, lowering.inferExprType(ret)); + try std.testing.expectEqual(TypeId.noreturn, lowering.inferExprType(brk)); + try std.testing.expectEqual(TypeId.noreturn, lowering.inferExprType(cont)); + try std.testing.expectEqual(TypeId.noreturn, lowering.inferExprType(raise)); + + // Block whose last statement diverges → noreturn. + const five = mk.node(alloc, .{ .int_literal = .{ .value = 5 } }); + defer alloc.destroy(five); + const blk_stmts: []const *Node = &.{ five, ret }; + const blk = mk.node(alloc, .{ .block = .{ .stmts = blk_stmts } }); + defer alloc.destroy(blk); + try std.testing.expectEqual(TypeId.noreturn, lowering.inferExprType(blk)); + + // if-else with one diverging branch unifies to the other branch's type; + // both diverging → noreturn. + const lit = mk.node(alloc, .{ .int_literal = .{ .value = 1 } }); + defer alloc.destroy(lit); + const then_div = mk.node(alloc, .{ .if_expr = .{ .condition = lit, .then_branch = ret, .else_branch = lit, .is_inline = false } }); + defer alloc.destroy(then_div); + try std.testing.expectEqual(TypeId.s64, lowering.inferExprType(then_div)); // then diverges → else (s64) + + const else_div = mk.node(alloc, .{ .if_expr = .{ .condition = lit, .then_branch = lit, .else_branch = ret, .is_inline = false } }); + defer alloc.destroy(else_div); + try std.testing.expectEqual(TypeId.s64, lowering.inferExprType(else_div)); // then is s64 + + const both_div = mk.node(alloc, .{ .if_expr = .{ .condition = lit, .then_branch = ret, .else_branch = brk, .is_inline = false } }); + defer alloc.destroy(both_div); + try std.testing.expectEqual(TypeId.noreturn, lowering.inferExprType(both_div)); +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 2823a75..b90d6ac 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -13532,7 +13532,7 @@ pub const Lowering = struct { // ── Helpers ───────────────────────────────────────────────────── /// Infer the type of an expression from its AST node (used for untyped var decls). - fn inferExprType(self: *Lowering, node: *const Node) TypeId { + pub fn inferExprType(self: *Lowering, node: *const Node) TypeId { return switch (node.data) { .string_literal => .string, .int_literal => .s64, @@ -13566,14 +13566,25 @@ pub const Lowering = struct { break :blk op_ty; }, .if_expr => |ie| { - // If-else: infer from then branch - if (ie.else_branch != null) { - return self.inferExprType(ie.then_branch); + // If-else types as its branches' unified type. A `noreturn` + // branch (one that diverges — `return` / `raise` / `break` / + // `continue`) unifies away, so the expression takes the other + // branch's type; both diverging → `noreturn` (ERR E1.4c). + if (ie.else_branch) |eb| { + const then_ty = self.inferExprType(ie.then_branch); + if (then_ty == .noreturn) return self.inferExprType(eb); + return then_ty; } return .void; }, + // Divergence shapes type as `noreturn` — they transfer control and + // produce no value at their site. A block whose last statement is + // one of these propagates `noreturn` (block arm below), which lets + // a `catch` body that ends in `return` / `raise` unify with the + // success type (ERR E1.4c / E1.5). + .return_stmt, .raise_stmt, .break_expr, .continue_expr => .noreturn, .block => |blk| { - // Block type is the type of the last expression + // Block type is the type of the last expression / statement. if (blk.stmts.len > 0) { return self.inferExprType(blk.stmts[blk.stmts.len - 1]); } @@ -13976,8 +13987,9 @@ pub const Lowering = struct { } break :blk self.inferExprType(nc.rhs); }, - // Statements don't produce values - .assignment, .var_decl, .const_decl, .fn_decl, .return_stmt, + // Statements don't produce values (`.return_stmt` is handled above + // as `.noreturn` — it diverges rather than yielding `void`). + .assignment, .var_decl, .const_decl, .fn_decl, .defer_stmt, .push_stmt, .multi_assign, .destructure_decl, => .void, else => .unresolved,