ERR/E1.4a: standalone try sema + pure-failable propagation + named widening
`try f()` (standalone form) now propagates a failable callee's error to the enclosing failable function. E1.4 was split: E1.4a = standalone try (failure target = function-propagation); E1.4b = fallback-target routing + failable-`or` + whole-program SCC for inferred sets + empty-inferred warning. - lowerExpr: `.try_expr` -> lowerTry - lowerTry: (1) try legal only inside a failable fn; (2) the sole failable-operand check (errorChannelOf(inferExprType(operand))); (3) named-caller widening (checkErrorSetSubset at the propagation site); (4) pure-failable lowering — condBr on tag != 0: propagate (run defers + ret the widened tag) vs continue on success - inferExprType: `.try_expr` arm (success type: void for pure-failable) - lowerBinaryOp .or_op: bail loudly on a failable LHS (exprIsFailable); the optional-`or` path is unchanged for non-failable LHS - value-carrying callee/caller `try` bail loudly (pending E2's tuple ABI) Tests: examples/221-try.sx (positive propagation, exit 5), examples/222-try-rejections.sx (3 stable rejections: outside-failable, non-failable operand, named-widening miss; exit 1). Gates: zig build, zig build test, 260/260 examples.
This commit is contained in:
116
src/ir/lower.zig
116
src/ir/lower.zig
@@ -2558,6 +2558,7 @@ pub const Lowering = struct {
|
||||
break :blk self.emitError(te.name, node.span);
|
||||
},
|
||||
|
||||
.try_expr => |te| self.lowerTry(te.operand, node.span),
|
||||
else => self.emitError("unknown_expr", node.span),
|
||||
};
|
||||
}
|
||||
@@ -2589,6 +2590,17 @@ pub const Lowering = struct {
|
||||
}
|
||||
// Short-circuit: `a or b` → if a then true else b
|
||||
if (bop.op == .or_op) {
|
||||
// Failable `or` (chain / value-terminator) routes per the fallback
|
||||
// target and unions error sets — that, plus the whole-program SCC,
|
||||
// lands in E1.4b/E2.4. Detect a failable LHS (a `try`, or any
|
||||
// expression carrying an error channel) and bail loudly rather than
|
||||
// mis-lower it through the optional-unwrap path below.
|
||||
if (self.exprIsFailable(bop.lhs)) {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, bop.lhs.span, "failable `or` (chain / value terminator) is not yet lowered — pending ERR E1.4b; for now use a single `try` or a `catch`", .{});
|
||||
}
|
||||
return self.builder.constInt(0, .void);
|
||||
}
|
||||
const lhs = self.lowerExpr(bop.lhs);
|
||||
const rhs_bb = self.freshBlock("or.rhs");
|
||||
const merge_bb = self.freshBlockWithParams("or.merge", &.{.bool});
|
||||
@@ -13529,6 +13541,18 @@ pub const Lowering = struct {
|
||||
},
|
||||
else => .unresolved,
|
||||
},
|
||||
// `try X` evaluates to X's success type (the value part). A
|
||||
// pure-failable operand (`-> !` / `-> !Named`, whose type IS the
|
||||
// error set) has no value → `void`; a value-carrying `-> (T, !)`
|
||||
// operand yields its value part (single field, or the tuple).
|
||||
.try_expr => |te| blk: {
|
||||
const op_ty = self.inferExprType(te.operand);
|
||||
const channel = self.errorChannelOf(op_ty) orelse break :blk .unresolved;
|
||||
if (op_ty == channel) break :blk .void;
|
||||
const info = self.module.types.get(op_ty);
|
||||
if (info == .tuple and info.tuple.fields.len == 2) break :blk info.tuple.fields[0];
|
||||
break :blk op_ty;
|
||||
},
|
||||
.if_expr => |ie| {
|
||||
// If-else: infer from then branch
|
||||
if (ie.else_branch != null) {
|
||||
@@ -15068,6 +15092,98 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// True if `node`'s value is failable — a `try` (the result is its
|
||||
/// operand's success value, but the expression itself routes an error) or
|
||||
/// any expression whose type carries an error channel (a bare failable
|
||||
/// call). Used to detect failable `or` chains (deferred to E1.4b).
|
||||
fn exprIsFailable(self: *Lowering, node: *const Node) bool {
|
||||
if (node.data == .try_expr) return true;
|
||||
return self.errorChannelOf(self.inferExprType(node)) != null;
|
||||
}
|
||||
|
||||
/// `try X` — a fallible attempt (ERR step E1.4a: the STANDALONE form, whose
|
||||
/// failure target is function-propagation). Evaluates X; on failure, runs
|
||||
/// the function's defers and returns the error to the caller; on success,
|
||||
/// continues with X's value. E1.4a lowers the pure-failable shape (callee
|
||||
/// `-> !` / `-> !Named`, caller likewise pure-failable). Value-carrying
|
||||
/// callees, propagation from a value-carrying caller, and `try` inside an
|
||||
/// `or` chain need the error-channel tuple ABI / fallback routing — those
|
||||
/// land in E1.4b/E2, so we bail loudly here.
|
||||
fn lowerTry(self: *Lowering, operand: *const Node, span: ast.Span) Ref {
|
||||
// (1) `try` is legal only inside a failable function.
|
||||
const caller_ret = self.effectiveReturnType() orelse {
|
||||
self.diagTryNotFailable(span);
|
||||
return self.builder.constInt(0, .void);
|
||||
};
|
||||
const caller_set = self.errorChannelOf(caller_ret) orelse {
|
||||
self.diagTryNotFailable(span);
|
||||
return self.builder.constInt(0, .void);
|
||||
};
|
||||
|
||||
// (2) The operand must be failable. This is the sole failable-operand
|
||||
// check (the parser imposes none — see E0.2).
|
||||
const op_ty = self.inferExprType(operand);
|
||||
const callee_set = self.errorChannelOf(op_ty) orelse {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, span, "`try` requires a failable expression; operand has type '{s}'", .{self.formatTypeName(op_ty)});
|
||||
}
|
||||
return self.builder.constInt(0, .void);
|
||||
};
|
||||
|
||||
// E1.4a scope guards — bail loudly on the shapes E2/E1.4b own.
|
||||
if (op_ty != callee_set) {
|
||||
// Value-carrying callee: `try` must bind the value slot(s), which
|
||||
// needs the error-channel tuple ABI (E2).
|
||||
return self.bailTry(span, "a value-carrying failable callee (`-> (T..., !)`)");
|
||||
}
|
||||
if (caller_ret != caller_set) {
|
||||
// Propagating from a value-carrying caller needs undef value slots
|
||||
// alongside the error slot (E2's tuple ABI).
|
||||
return self.bailTry(span, "a value-carrying failable function");
|
||||
}
|
||||
|
||||
// (3) Widening: the callee's escape set must be ⊆ the caller's named
|
||||
// set. For an inferred caller (`!`) the absorption happens in the
|
||||
// whole-program SCC (E1.4b) — no check here.
|
||||
if (!self.isInferredErrorSet(caller_set)) {
|
||||
self.checkErrorSetSubset(callee_set, caller_set, span);
|
||||
}
|
||||
|
||||
// (4) Lower: evaluate the call (→ the error tag, 0 = success), branch.
|
||||
const err_val = self.lowerExpr(operand);
|
||||
const err_ty = self.builder.getRefType(err_val);
|
||||
const zero = self.builder.constInt(0, err_ty);
|
||||
const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = zero } }, .bool);
|
||||
|
||||
const prop_bb = self.freshBlock("try.prop");
|
||||
const ok_bb = self.freshBlock("try.ok");
|
||||
self.builder.condBr(is_err, prop_bb, &.{}, ok_bb, &.{});
|
||||
|
||||
// Propagation: run the function's defers, then return the error tag
|
||||
// (widened to the caller's set — global-flat tag ids carry across).
|
||||
self.builder.switchToBlock(prop_bb);
|
||||
self.emitBlockDefers(self.func_defer_base);
|
||||
const widened = if (err_ty != caller_set) self.coerceToType(err_val, err_ty, caller_set) else err_val;
|
||||
self.builder.ret(widened, caller_set);
|
||||
|
||||
// Success: continue. A pure-failable callee has no value slots.
|
||||
self.builder.switchToBlock(ok_bb);
|
||||
return self.builder.constInt(0, .void);
|
||||
}
|
||||
|
||||
fn diagTryNotFailable(self: *Lowering, span: ast.Span) void {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, span, "`try` is only valid inside a failable function (a return type with `!` or `!Named`)", .{});
|
||||
}
|
||||
}
|
||||
|
||||
fn bailTry(self: *Lowering, span: ast.Span, comptime what: []const u8) Ref {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, span, "`try` with " ++ what ++ " is not yet lowered — pending the error-channel tuple ABI / fallback routing (ERR E1.4b/E2)", .{});
|
||||
}
|
||||
return self.builder.constInt(0, .void);
|
||||
}
|
||||
|
||||
fn binOpSymbol(op: ast.BinaryOp.Op) []const u8 {
|
||||
return switch (op) {
|
||||
.add => "+",
|
||||
|
||||
Reference in New Issue
Block a user