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:
agra
2026-05-31 20:33:13 +03:00
parent d2cba4e460
commit 696a749bd5
2 changed files with 77 additions and 7 deletions

View File

@@ -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,