diff --git a/examples/221-try.sx b/examples/221-try.sx new file mode 100644 index 0000000..02f9ec8 --- /dev/null +++ b/examples/221-try.sx @@ -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; +} diff --git a/examples/222-try-rejections.sx b/examples/222-try-rejections.sx new file mode 100644 index 0000000..15615d8 --- /dev/null +++ b/examples/222-try-rejections.sx @@ -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; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index e751a82..9affa08 100644 --- a/src/ir/lower.zig +++ b/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 => "+", diff --git a/tests/expected/221-try.exit b/tests/expected/221-try.exit new file mode 100644 index 0000000..7ed6ff8 --- /dev/null +++ b/tests/expected/221-try.exit @@ -0,0 +1 @@ +5 diff --git a/tests/expected/221-try.txt b/tests/expected/221-try.txt new file mode 100644 index 0000000..d8ef255 --- /dev/null +++ b/tests/expected/221-try.txt @@ -0,0 +1 @@ +try result: 5 diff --git a/tests/expected/222-try-rejections.exit b/tests/expected/222-try-rejections.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/tests/expected/222-try-rejections.exit @@ -0,0 +1 @@ +1 diff --git a/tests/expected/222-try-rejections.txt b/tests/expected/222-try-rejections.txt new file mode 100644 index 0000000..69174be --- /dev/null +++ b/tests/expected/222-try-rejections.txt @@ -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 + | ^^^^^^^^