From 50e55150805d896328e41a4aed5303ff1bac397f Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 31 May 2026 22:16:28 +0300 Subject: [PATCH] ERR/E2.4a: failable `or` value-terminator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `lhs or value` where `lhs` is a value-carrying failable (`-> (T, !E)`): on success the result is the LHS value, on failure the LHS error is discarded and the result is the terminator value — the whole expression is non-failable (T). Unblocked by the value ABI (E2.1); needs no fallback-routing (it's a 2-operand, non-chained `or`). - lowerBinaryOp `.or_op`: a failable LHS now routes to lowerFailableOr instead of the E1.4a loud bail; non-failable `or` (boolean / optional-unwrap) unchanged. - lowerFailableOr: chain form (a `try`-marked LHS, whose own type is its success value, or a failable RHS) bails → E2.4b (fallback routing). Pure failable `or value` rejected ("no success value to fall back to — use catch"). Value-carrying: tuple_get the value/error, condBr, merge the LHS value (success) or the terminator (failure) through a block-param phi. Multi-value bails (E2). - inferExprType `.or_op`: a failable `or value` types as the LHS success type (was always `.bool`); non-failable `or` still `.bool`. Tests: examples/231-failable-or.sx (success + Bad + Empty terminators; exit 116), examples/232-failable-or-reject.sx (pure-failable `or value` rejected; exit 1). Gates: zig build, zig build test, 270/270 examples. --- examples/231-failable-or.sx | 25 ++++++ examples/232-failable-or-reject.sx | 19 +++++ src/ir/lower.zig | 88 +++++++++++++++++++--- tests/expected/231-failable-or.exit | 1 + tests/expected/231-failable-or.txt | 1 + tests/expected/232-failable-or-reject.exit | 1 + tests/expected/232-failable-or-reject.txt | 5 ++ 7 files changed, 130 insertions(+), 10 deletions(-) create mode 100644 examples/231-failable-or.sx create mode 100644 examples/232-failable-or-reject.sx create mode 100644 tests/expected/231-failable-or.exit create mode 100644 tests/expected/231-failable-or.txt create mode 100644 tests/expected/232-failable-or-reject.exit create mode 100644 tests/expected/232-failable-or-reject.txt diff --git a/examples/231-failable-or.sx b/examples/231-failable-or.sx new file mode 100644 index 0000000..1b25f96 --- /dev/null +++ b/examples/231-failable-or.sx @@ -0,0 +1,25 @@ +// Failable `or` value-terminator (ERR step E2.4a). `lhs or value` where `lhs` +// is a value-carrying failable (`-> (T, !E)`): on success the result is the +// LHS value; on failure the LHS error is discarded and the result is the +// terminator value. The whole expression is non-failable (type T). The chain +// form (`try a or try b`) needs fallback-target routing and lands in E2.4b. +// Rejections: `examples/232-failable-or-reject.sx`. + +#import "modules/std.sx"; + +E :: error { Bad, Empty } + +parse :: (n: s32) -> (s32, !E) { + if n < 0 { raise error.Bad; } + if n == 0 { raise error.Empty; } + return n * 2; +} + +main :: () -> s32 { + a := parse(5) or 0; // success → 10 + b := parse(-1) or 99; // Bad → 99 (terminator) + c := parse(0) or 7; // Empty → 7 (terminator) + r := a + b + c; // 10 + 99 + 7 = 116 + print("or result: {}\n", r); + return r; +} diff --git a/examples/232-failable-or-reject.sx b/examples/232-failable-or-reject.sx new file mode 100644 index 0000000..3455eb3 --- /dev/null +++ b/examples/232-failable-or-reject.sx @@ -0,0 +1,19 @@ +// Failable `or` rejection (ERR step E2.4a): the value-terminator form +// (`lhs or value`) requires a value-carrying failable LHS — a pure failable +// (`-> !`) has no success value to fall back to, so `or value` is rejected +// (use `catch` to absorb the error). The positive cases live in +// `examples/231-failable-or.sx`. + +#import "modules/std.sx"; + +E :: error { Bad } + +must :: (n: s32) -> !E { + if n < 0 { raise error.Bad; } + return; +} + +main :: () -> s32 { + x := must(-1) or 0; // error: `-> !` has no success value to fall back to + return 0; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 3528652..95890ce 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -2607,16 +2607,10 @@ 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. + // Failable LHS → the error-handling `or` (value-terminator / chain), + // not the optional/boolean unwrap 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 E2.4; for now use a single `try` or a `catch`", .{}); - } - return self.builder.constInt(0, .void); + return self.lowerFailableOr(bop); } const lhs = self.lowerExpr(bop.lhs); const rhs_bb = self.freshBlock("or.rhs"); @@ -13576,7 +13570,19 @@ pub const Lowering = struct { .bool_literal => .bool, .null_literal => .void, .binary_op => |bop| switch (bop.op) { - .eq, .neq, .lt, .lte, .gt, .gte, .and_op, .or_op, .in_op => .bool, + .or_op => blk: { + // A failable `or value` yields the LHS's success type (the + // error is discarded); a non-failable `or` is boolean / + // optional-unwrap → bool. + const lt = self.inferExprType(bop.lhs); + if (self.errorChannelOf(lt)) |ch| { + if (lt == ch) break :blk .unresolved; // pure-failable (rejected at lowering) + const f = self.module.types.get(lt).tuple.fields; + break :blk if (f.len == 2) f[0] else .unresolved; + } + break :blk .bool; + }, + .eq, .neq, .lt, .lte, .gt, .gte, .and_op, .in_op => .bool, else => self.inferExprType(bop.lhs), }, .unary_op => |uop| switch (uop.op) { @@ -15463,6 +15469,68 @@ pub const Lowering = struct { return self.builder.constInt(0, .void); } + /// `lhs or rhs` with a failable LHS (ERR step E2.4a — the value-terminator + /// form). On LHS success the result is its value; on failure the LHS error + /// is discarded and the result is `rhs` (a plain value of the success + /// type), so the whole expression is non-failable. Single-value + /// value-carrying LHS only. The CHAIN form (`... or try ...` / a failable + /// RHS) needs the fallback-target routing deferred from E1.4 — bail. + fn lowerFailableOr(self: *Lowering, bop: *const ast.BinaryOp) Ref { + const span = bop.lhs.span; + + // Chain form — a `try`-marked LHS (whose own type is its success value, + // not a failable) or a failable RHS — routes per the fallback target; + // deferred to E2.4b. The value-terminator form has a BARE failable LHS + // and a plain-value RHS. + if (bop.lhs.data == .try_expr or self.exprIsFailable(bop.rhs)) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, span, "a failable `or` chain (`… or try …`) is not yet lowered — pending the fallback-target routing (ERR E2.4b); use a value terminator (`… or value`) or `catch`", .{}); + } + return self.builder.constInt(0, .void); + } + + const lhs_ty = self.inferExprType(bop.lhs); + const err_set = self.errorChannelOf(lhs_ty) orelse return self.builder.constInt(0, .void); + + // Value-terminator. A pure-failable LHS (`-> !`) has no success value to + // fall back to. + if (lhs_ty == err_set) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, span, "`or value` requires a value-carrying failable (`-> (T, !)`) — a `-> !` has no success value to fall back to; use `catch` to absorb the error", .{}); + } + return self.builder.constInt(0, .void); + } + const fields = self.module.types.get(lhs_ty).tuple.fields; + if (fields.len != 2) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, span, "`or value` on a multi-value failable (`-> (T1, T2, !)`) is not yet lowered — pending the multi-value error-channel ABI (ERR E2)", .{}); + } + return self.builder.constInt(0, .void); + } + const succ_ty = fields[0]; + + const result = self.lowerExpr(bop.lhs); + const err_val = self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 1, .base_type = lhs_ty } }, err_set); + const succ_val = self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 0, .base_type = lhs_ty } }, succ_ty); + const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_set) } }, .bool); + + const fail_bb = self.freshBlock("or.fail"); + const merge_bb = self.freshBlockWithParams("or.merge", &.{succ_ty}); + // Success → merge with the LHS value; failure → evaluate the terminator. + self.builder.condBr(is_err, fail_bb, &.{}, merge_bb, &.{succ_val}); + + self.builder.switchToBlock(fail_bb); + const saved_target = self.target_type; + self.target_type = succ_ty; + const rhs_val = self.lowerExpr(bop.rhs); + self.target_type = saved_target; + const rhs_c = self.coerceToType(rhs_val, self.builder.getRefType(rhs_val), succ_ty); + self.builder.br(merge_bb, &.{rhs_c}); + + self.builder.switchToBlock(merge_bb); + return self.builder.blockParam(merge_bb, 0, succ_ty); + } + // ── ERR E1.4b: whole-program inferred-error-set convergence ────────── /// The bare callee name of a call expression (`g(...)` → "g"), or null if diff --git a/tests/expected/231-failable-or.exit b/tests/expected/231-failable-or.exit new file mode 100644 index 0000000..4699eb3 --- /dev/null +++ b/tests/expected/231-failable-or.exit @@ -0,0 +1 @@ +116 diff --git a/tests/expected/231-failable-or.txt b/tests/expected/231-failable-or.txt new file mode 100644 index 0000000..019d22c --- /dev/null +++ b/tests/expected/231-failable-or.txt @@ -0,0 +1 @@ +or result: 116 diff --git a/tests/expected/232-failable-or-reject.exit b/tests/expected/232-failable-or-reject.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/tests/expected/232-failable-or-reject.exit @@ -0,0 +1 @@ +1 diff --git a/tests/expected/232-failable-or-reject.txt b/tests/expected/232-failable-or-reject.txt new file mode 100644 index 0000000..028c149 --- /dev/null +++ b/tests/expected/232-failable-or-reject.txt @@ -0,0 +1,5 @@ +error: `or value` requires a value-carrying failable (`-> (T, !)`) — a `-> !` has no success value to fall back to; use `catch` to absorb the error + --> /Users/agra/projects/sx/examples/232-failable-or-reject.sx:17:10 + | +17 | x := must(-1) or 0; // error: `-> !` has no success value to fall back to + | ^^^^^^^^