ERR/E2.4a: failable or value-terminator
`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.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user