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:
32
examples/221-try.sx
Normal file
32
examples/221-try.sx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// First runnable `try` (ERR step E1.4a). The STANDALONE form: a failable
|
||||||
|
// expression whose failure propagates to the enclosing function's error
|
||||||
|
// return (Zig-style). `outer` calls `try inner(n)` — on `inner`'s failure
|
||||||
|
// `outer` returns that error; on success it continues. Both are pure
|
||||||
|
// failable (`-> !E`). The error-channel tuple ABI for value-carrying
|
||||||
|
// `-> (T, !)` and `try` in an `or` chain land in ERR E1.4b/E2.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
E :: error { Bad, Worse }
|
||||||
|
|
||||||
|
inner :: (n: s32) -> !E {
|
||||||
|
if n < 0 { raise error.Bad; }
|
||||||
|
return; // success — no error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propagates inner's error (standalone `try`, target = function return).
|
||||||
|
outer :: (n: s32) -> !E {
|
||||||
|
try inner(n);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
bad := outer(-1); // inner raises Bad -> outer propagates
|
||||||
|
good := outer(7); // inner succeeds -> outer succeeds
|
||||||
|
r : s32 = 0;
|
||||||
|
if bad == error.Bad { r = r + 5; } // true -> +5
|
||||||
|
if good == error.Bad { r = r + 1; } // false (success = no error)
|
||||||
|
if bad == error.Worse { r = r + 2; } // false (propagated Bad)
|
||||||
|
print("try result: {}\n", r); // -> 5
|
||||||
|
return r;
|
||||||
|
}
|
||||||
41
examples/222-try-rejections.sx
Normal file
41
examples/222-try-rejections.sx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// `try` rejections (ERR step E1.4a):
|
||||||
|
// - `try` is only valid inside a failable function,
|
||||||
|
// - the operand must be failable (the sole failable-operand check —
|
||||||
|
// the parser imposes none),
|
||||||
|
// - propagating a `try` whose callee's error set is not a subset of the
|
||||||
|
// caller's named set is rejected (widening at a function-propagation site).
|
||||||
|
// The positive case lives in `examples/221-try.sx`.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
A :: error { Xa }
|
||||||
|
B :: error { Yb }
|
||||||
|
|
||||||
|
ga :: () -> !A { return; }
|
||||||
|
gb :: () -> !B { return; }
|
||||||
|
plain :: () -> s32 { return 0; }
|
||||||
|
|
||||||
|
// `try` in a non-failable function.
|
||||||
|
bad_ctx :: () -> s32 {
|
||||||
|
try ga(); // error: `try` outside a failable function
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `try` on a non-failable operand.
|
||||||
|
bad_operand :: () -> !A {
|
||||||
|
try plain(); // error: operand has type s32 (not failable)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callee's set (B = {Yb}) is not a subset of the caller's set (A = {Xa}).
|
||||||
|
widen :: () -> !A {
|
||||||
|
try gb(); // error: Yb not in caller's error set A
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
a := bad_ctx(); // force bad_ctx to lower
|
||||||
|
b := bad_operand(); // force bad_operand to lower
|
||||||
|
c := widen(); // force widen to lower
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
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);
|
break :blk self.emitError(te.name, node.span);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.try_expr => |te| self.lowerTry(te.operand, node.span),
|
||||||
else => self.emitError("unknown_expr", 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
|
// Short-circuit: `a or b` → if a then true else b
|
||||||
if (bop.op == .or_op) {
|
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 lhs = self.lowerExpr(bop.lhs);
|
||||||
const rhs_bb = self.freshBlock("or.rhs");
|
const rhs_bb = self.freshBlock("or.rhs");
|
||||||
const merge_bb = self.freshBlockWithParams("or.merge", &.{.bool});
|
const merge_bb = self.freshBlockWithParams("or.merge", &.{.bool});
|
||||||
@@ -13529,6 +13541,18 @@ pub const Lowering = struct {
|
|||||||
},
|
},
|
||||||
else => .unresolved,
|
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_expr => |ie| {
|
||||||
// If-else: infer from then branch
|
// If-else: infer from then branch
|
||||||
if (ie.else_branch != null) {
|
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 {
|
fn binOpSymbol(op: ast.BinaryOp.Op) []const u8 {
|
||||||
return switch (op) {
|
return switch (op) {
|
||||||
.add => "+",
|
.add => "+",
|
||||||
|
|||||||
1
tests/expected/221-try.exit
Normal file
1
tests/expected/221-try.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
5
|
||||||
1
tests/expected/221-try.txt
Normal file
1
tests/expected/221-try.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
try result: 5
|
||||||
1
tests/expected/222-try-rejections.exit
Normal file
1
tests/expected/222-try-rejections.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
17
tests/expected/222-try-rejections.txt
Normal file
17
tests/expected/222-try-rejections.txt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
error: `try` is only valid inside a failable function (a return type with `!` or `!Named`)
|
||||||
|
--> /Users/agra/projects/sx/examples/222-try-rejections.sx:20:5
|
||||||
|
|
|
||||||
|
20 | try ga(); // error: `try` outside a failable function
|
||||||
|
| ^^^^^^^^
|
||||||
|
|
||||||
|
error: `try` requires a failable expression; operand has type 's32'
|
||||||
|
--> /Users/agra/projects/sx/examples/222-try-rejections.sx:26:5
|
||||||
|
|
|
||||||
|
26 | try plain(); // error: operand has type s32 (not failable)
|
||||||
|
| ^^^^^^^^^^^
|
||||||
|
|
||||||
|
error: error tag 'error.Yb' is not in caller's error set 'A'
|
||||||
|
--> /Users/agra/projects/sx/examples/222-try-rejections.sx:32:5
|
||||||
|
|
|
||||||
|
32 | try gb(); // error: Yb not in caller's error set A
|
||||||
|
| ^^^^^^^^
|
||||||
Reference in New Issue
Block a user