ERR/E1.4c: noreturn plumbing for divergence shapes
Type the divergence shapes as `noreturn` so a `catch` body that diverges (E1.5) unifies with the failable's success type. The plan's original "E1.4b", renumbered E1.4c (the SCC slice took the "E1.4b" label). - inferExprType: `return` / `raise` / `break` / `continue` -> .noreturn (removed `.return_stmt` from the statements-are-`.void` group) - if-else unification: a `.noreturn` branch yields the other branch's type; both diverging -> `.noreturn` - block-ending-in-divergence propagates `.noreturn` (existing block arm) - calls to `-> noreturn` already type via Function.ret (verified) - made inferExprType pub for the unit test Scope: the essential divergence shapes. Deferred `unreachable` (not a keyword in sx — a separate feature, no current consumer) and infinite-loop `noreturn` detection (rare). No observable consumer until E1.5's catch body, so validated by a unit test, not an example. Tests: unit test `E1.4c noreturn typing` in lower.test.zig (each shape -> noreturn; block propagation; if-else unification). Gates: zig build, zig build test, 262/262 examples (no new examples).
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user