ERR/E2.4b: failable or chains
`lhs or rhs` with failable operands now lowers as a full short-circuit chain (was a loud bail). Each failing attempt routes to the next operand; the chain resolves when an operand succeeds or a value terminator absorbs; total failure propagates to the function — or, when the chain is the operand of a `catch`, to the handler. All in ir/lower.zig. - Dispatch (lowerBinaryOp .or_op): structural `orIsFailableChain` (an operand is a `try`, error-channel-typed, or a nested failable `or` chain) instead of the type-only `exprIsFailable(lhs)`, which missed nested chains (a try-chain's value type is non-failable T). - inferExprType .or_op: a failable chain reports its success type via `orChainSuccessType` (was `.bool`). - lowerFailableOr rewritten: flatten the left-assoc chain, lower operands left-to-right. Non-final failure → push frame + fall to next operand block (no function exit, so onfail doesn't fire). Success → clear trace + merge. Final failure → push frame + route to a `catch` target (chain_fail_target field) if set, else propagate (cleanup + error return). Value terminator → clear + merge the terminator value. Subsumes the E2.4a path. Widening factored into `checkEscapeWidening`, checked only at a propagating final operand. - Catch-over-chain: lowerCatchOverChain sets chain_fail_target so the chain's total failure reaches the handler (binds the final tag, may inspect the trace, clears on non-diverging exit). Verified JIT + AOT: 2-/3-operand chains, bare chain + value terminator, void chains, all-fail propagation (exit 1 + trace), catch-over-chain, trace clear-on-absorb, onfail gating. examples/246-failable-or-chain.sx (exit 120), 247-failable-or-chain-propagate.sx (exit 1 + trace).
This commit is contained in:
39
examples/246-failable-or-chain.sx
Normal file
39
examples/246-failable-or-chain.sx
Normal file
@@ -0,0 +1,39 @@
|
||||
// Failable `or` chains (ERR step E2.4b). `lhs or rhs` with failable operands
|
||||
// is a left-to-right, short-circuit chain: each failing attempt routes to the
|
||||
// next operand; the chain resolves when an operand succeeds (or a value
|
||||
// terminator absorbs). `try` marks an operand whose failure is visible routing
|
||||
// (path-marker rule); a bare failable operand is allowed when a downstream
|
||||
// terminator absorbs it. A `catch` over a parenthesized chain redirects the
|
||||
// chain's total failure to the handler instead of the function. Absorbed
|
||||
// failures clear the trace buffer; `onfail` does not fire for a failure that
|
||||
// never leaves its block. This run takes only absorbed paths → exit 120.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
E :: error { A, B };
|
||||
|
||||
fa :: (n: s32) -> (s32, !E) {
|
||||
if n == 0 { raise error.A; }
|
||||
if n < 0 { raise error.B; }
|
||||
return n;
|
||||
}
|
||||
|
||||
fv :: (n: s32) -> !E { // void (pure) failable
|
||||
if n == 0 { raise error.A; }
|
||||
return;
|
||||
}
|
||||
|
||||
main :: () -> (s32, !E) {
|
||||
onfail print("onfail fired (BUG)\n"); // must NOT fire — every chain below absorbs
|
||||
|
||||
r : s32 = 0;
|
||||
r = r + (try fa(0) or try fa(7)); // a fails → b succeeds → 7
|
||||
r = r + (try fa(0) or try fa(0) or try fa(3)); // first two fail → third → +3 = 10
|
||||
r = r + (fa(0) or fa(0) or 96); // bare chain + value terminator → +96 = 106
|
||||
r = r + ((try fa(0) or try fa(0)) catch e 5); // both fail → catch handler → +5 = 111
|
||||
r = r + ((try fa(0) or try fa(9)) catch e 0); // second succeeds → catch skipped → +9 = 120
|
||||
|
||||
try fv(0) or try fv(1); // void chain: first fails → second succeeds
|
||||
|
||||
return r; // success → exit 120; onfail skipped
|
||||
}
|
||||
21
examples/247-failable-or-chain-propagate.sx
Normal file
21
examples/247-failable-or-chain-propagate.sx
Normal file
@@ -0,0 +1,21 @@
|
||||
// Failable `or` chain propagation (ERR step E2.4b). When every operand of a
|
||||
// `try … or try …` chain fails and there is no value terminator, the final
|
||||
// failure propagates to the enclosing function — here `main`, so the E4.2
|
||||
// entry-point wrapper prints the unhandled-error header + return trace to
|
||||
// stderr and exits 1. Each failed attempt contributes its `raise` frame plus
|
||||
// the chain-attempt frame, so three all-failing attempts leave six frames
|
||||
// (locations are placeholders until DWARF / E3.0). Expected exit code: 1.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
E :: error { A };
|
||||
|
||||
fa :: (n: s32) -> (s32, !E) {
|
||||
if n == 0 { raise error.A; }
|
||||
return n;
|
||||
}
|
||||
|
||||
main :: () -> (s32, !E) {
|
||||
v := try fa(0) or try fa(0) or try fa(0); // all fail → propagate to main
|
||||
return v;
|
||||
}
|
||||
348
src/ir/lower.zig
348
src/ir/lower.zig
@@ -127,6 +127,7 @@ pub const Lowering = struct {
|
||||
trace_push_fid: ?FuncId = null, // extern `sx_trace_push` (ERR E3.1, from library/vendors/sx_trace_runtime/sx_trace.c)
|
||||
trace_clear_fid: ?FuncId = null, // extern `sx_trace_clear`
|
||||
needs_trace_runtime: bool = false, // set when lowering emits a trace push/clear; signals Compilation to auto-link sx_trace.c
|
||||
chain_fail_target: ?ChainFailTarget = null, // ERR E2.4: when set, a failable `or` chain routes its TOTAL failure here (an absorbing consumer like `catch`) instead of propagating to the function
|
||||
foreign_class_map: std.StringHashMap(*const ast.ForeignClassDecl) = std.StringHashMap(*const ast.ForeignClassDecl).init(std.heap.page_allocator), // sx alias → ForeignClassDecl (jni_class / objc_class / swift_class / ... — registered in scan pass)
|
||||
current_foreign_class: ?*const ast.ForeignClassDecl = null, // set while lowering a `#jni_main` (or any sx-defined `#jni_class`) bodied method — `super.method(args)` dispatch resolves the parent class against this fcd's `#extends`
|
||||
current_foreign_method: ?ast.ForeignMethodDecl = null, // the specific method whose body is being lowered; `super.<same_name>(...)` reuses its signature
|
||||
@@ -259,6 +260,12 @@ pub const Lowering = struct {
|
||||
|
||||
const InlineReturnInfo = struct { slot: Ref, ret_ty: TypeId, done_bb: BlockId };
|
||||
|
||||
/// ERR E2.4 — where a failable `or` chain's TOTAL failure routes when the
|
||||
/// chain is the operand of an absorbing consumer (`catch`). `bb` is a block
|
||||
/// with a single parameter typed `set` (the error tag); the chain branches
|
||||
/// there with its final error instead of propagating to the function.
|
||||
const ChainFailTarget = struct { bb: BlockId, set: TypeId };
|
||||
|
||||
/// Pack-variadic impl entry — `impl Proto(Args...) for Closure(Prefix..., ..$pack) -> $ret`.
|
||||
/// Matches any concrete closure source whose first `prefix_len` param types
|
||||
/// equal `source_pack_ty`'s fixed prefix; the tail binds to `pack_var_name`
|
||||
@@ -2677,9 +2684,11 @@ pub const Lowering = struct {
|
||||
}
|
||||
// Short-circuit: `a or b` → if a then true else b
|
||||
if (bop.op == .or_op) {
|
||||
// Failable LHS → the error-handling `or` (value-terminator / chain),
|
||||
// not the optional/boolean unwrap below.
|
||||
if (self.exprIsFailable(bop.lhs)) {
|
||||
// A failable `or` (value-terminator or chain) routes to the error-
|
||||
// handling lowering, not the optional/boolean unwrap below. Detected
|
||||
// structurally (a `try`-chain's value type is non-failable `T`, so a
|
||||
// type-only `exprIsFailable(lhs)` would miss nested chains).
|
||||
if (self.orIsFailableChain(bop)) {
|
||||
return self.lowerFailableOr(bop);
|
||||
}
|
||||
const lhs = self.lowerExpr(bop.lhs);
|
||||
@@ -13805,14 +13814,12 @@ pub const Lowering = struct {
|
||||
.null_literal => .void,
|
||||
.binary_op => |bop| switch (bop.op) {
|
||||
.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)
|
||||
break :blk self.failableSuccessType(lt);
|
||||
}
|
||||
// A failable `or` (value-terminator or chain) yields the
|
||||
// chain's success type (the error is absorbed/propagated);
|
||||
// a non-failable `or` is boolean / optional-unwrap → bool.
|
||||
// Detected structurally — a `try`-chain's operands type as
|
||||
// non-failable `T`, so a type-only check would miss it.
|
||||
if (self.orIsFailableChain(&bop)) break :blk self.orChainSuccessType(&bop);
|
||||
break :blk .bool;
|
||||
},
|
||||
.eq, .neq, .lt, .lte, .gt, .gte, .and_op, .in_op => .bool,
|
||||
@@ -15585,20 +15592,7 @@ pub const Lowering = struct {
|
||||
// (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)) {
|
||||
if (self.isInferredErrorSet(callee_set)) {
|
||||
// Bare-`!` callee: its escape set is the SCC-converged set
|
||||
// (the empty placeholder TypeId carries no tags), so check the
|
||||
// converged tags from the whole-program pass.
|
||||
if (callTargetName(operand)) |nm| {
|
||||
if (self.inferred_error_sets.get(nm)) |tags| {
|
||||
self.diagTagsNotInSet(tags, caller_set, span);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.checkErrorSetSubset(callee_set, caller_set, span);
|
||||
}
|
||||
}
|
||||
self.checkEscapeWidening(operand, callee_set, caller_set, span);
|
||||
|
||||
// (4) Lower: evaluate the operand, then branch on its error tag (which
|
||||
// is the bare result for a pure callee, or the last tuple slot for
|
||||
@@ -15677,6 +15671,16 @@ pub const Lowering = struct {
|
||||
/// value-producing body unifying with the success tuple) needs the
|
||||
/// error-channel tuple ABI and lands in E2 — bail loudly here.
|
||||
fn lowerCatch(self: *Lowering, ce: *const ast.CatchExpr, span: ast.Span) Ref {
|
||||
// A failable `or` chain operand (`(try a or try b) catch e …`) routes
|
||||
// its total failure to the catch handler — not the function — via the
|
||||
// chain-fail target (ERR E2.4). A chain's value type is non-failable
|
||||
// `T`, so it wouldn't pass the `errorChannelOf` check below.
|
||||
if (ce.operand.data == .binary_op and ce.operand.data.binary_op.op == .or_op and
|
||||
self.orIsFailableChain(&ce.operand.data.binary_op))
|
||||
{
|
||||
return self.lowerCatchOverChain(ce, span);
|
||||
}
|
||||
|
||||
const op_ty = self.inferExprType(ce.operand);
|
||||
const err_set = self.errorChannelOf(op_ty) orelse {
|
||||
if (self.diagnostics) |diags| {
|
||||
@@ -15751,6 +15755,73 @@ pub const Lowering = struct {
|
||||
return self.builder.blockParam(merge_bb, 0, succ_ty);
|
||||
}
|
||||
|
||||
/// `(failable or-chain) catch [e] BODY` (ERR E2.4). The chain's operands
|
||||
/// route per the chain rules; its TOTAL failure (the final operand failing)
|
||||
/// is redirected to the catch handler via `chain_fail_target` rather than
|
||||
/// propagating to the function. `e` binds the final error tag; the handler's
|
||||
/// value (or divergence) joins the chain's success value at the merge.
|
||||
fn lowerCatchOverChain(self: *Lowering, ce: *const ast.CatchExpr, span: ast.Span) Ref {
|
||||
const chain = &ce.operand.data.binary_op;
|
||||
|
||||
// The error tag reaching the handler is the final operand's (left-assoc
|
||||
// chain → the top-level rhs). A value-terminator last operand means the
|
||||
// chain can't fail — nothing for `catch` to absorb.
|
||||
const last = unwrapTryNode(chain.rhs);
|
||||
const last_ty = self.inferExprType(last);
|
||||
const err_set = self.errorChannelOf(last_ty) orelse {
|
||||
if (self.diagnostics) |d| d.addFmt(.err, span, "`catch` here is redundant — the `or` chain already absorbs every failure via its value terminator", .{});
|
||||
return self.builder.constInt(0, .void);
|
||||
};
|
||||
|
||||
const succ_ty = self.orChainSuccessType(chain);
|
||||
const has_value = succ_ty != .void;
|
||||
|
||||
const handle_bb = self.freshBlockWithParams("catch.handle", &.{err_set});
|
||||
const merge_bb = if (has_value)
|
||||
self.freshBlockWithParams("catch.merge", &.{succ_ty})
|
||||
else
|
||||
self.freshBlock("catch.merge");
|
||||
|
||||
// Lower the chain with its total failure routed to the handler.
|
||||
const saved = self.chain_fail_target;
|
||||
self.chain_fail_target = .{ .bb = handle_bb, .set = err_set };
|
||||
const chain_val = self.lowerExpr(ce.operand);
|
||||
self.chain_fail_target = saved;
|
||||
// Chain success → merge with its value (the buffer was already cleared
|
||||
// at the succeeding operand inside the chain).
|
||||
if (has_value) {
|
||||
const cv = self.coerceToType(chain_val, self.builder.getRefType(chain_val), succ_ty);
|
||||
self.builder.br(merge_bb, &.{cv});
|
||||
} else {
|
||||
self.builder.br(merge_bb, &.{});
|
||||
}
|
||||
|
||||
// Handler: bind the final tag, run the body. The buffer still holds the
|
||||
// chain's frames (handler may inspect them); absorb on non-diverging exit.
|
||||
self.builder.switchToBlock(handle_bb);
|
||||
const tag = self.builder.blockParam(handle_bb, 0, err_set);
|
||||
const body_val = self.runCatchBody(ce, tag, err_set, if (has_value) succ_ty else null);
|
||||
if (!self.currentBlockHasTerminator()) {
|
||||
self.emitTraceClear();
|
||||
if (has_value) {
|
||||
const bv: Ref = blk: {
|
||||
if (body_val) |v| {
|
||||
const vty = self.builder.getRefType(v);
|
||||
if (vty != .void) break :blk self.coerceToType(v, vty, succ_ty);
|
||||
}
|
||||
if (self.diagnostics) |d| d.addFmt(.err, span, "`catch` body must produce a value of type '{s}' (or diverge with `return` / `raise`)", .{self.formatTypeName(succ_ty)});
|
||||
break :blk self.builder.constUndef(succ_ty);
|
||||
};
|
||||
self.builder.br(merge_bb, &.{bv});
|
||||
} else {
|
||||
self.builder.br(merge_bb, &.{});
|
||||
}
|
||||
}
|
||||
|
||||
self.builder.switchToBlock(merge_bb);
|
||||
return if (has_value) self.builder.blockParam(merge_bb, 0, succ_ty) else self.builder.constInt(0, .void);
|
||||
}
|
||||
|
||||
/// Lower a `catch` body in a child scope that binds the error tag to the
|
||||
/// catch binding (if any). When `want_ty` is non-null (value-carrying
|
||||
/// catch), returns the body's value (or null if the body diverged); when
|
||||
@@ -15782,56 +15853,209 @@ pub const Lowering = struct {
|
||||
/// `rhs` (a plain value of the success type), so the whole expression is
|
||||
/// non-failable. The CHAIN form (`... or try ...` / a failable RHS) needs
|
||||
/// the fallback-target routing deferred from E1.4 — bail.
|
||||
/// Widening at an escape (function-propagation) site: the escaping set must
|
||||
/// be ⊆ the caller's named set. An inferred caller (`!`) absorbs everything
|
||||
/// via the whole-program SCC (E1.4b) — no check. A bare-`!` callee carries
|
||||
/// no tags on its placeholder TypeId, so check its SCC-converged set.
|
||||
/// Shared by `try` propagation and a failable `or` chain's final operand.
|
||||
fn checkEscapeWidening(self: *Lowering, callee_node: *const Node, callee_set: TypeId, caller_set: TypeId, span: ast.Span) void {
|
||||
if (self.isInferredErrorSet(caller_set)) return;
|
||||
if (self.isInferredErrorSet(callee_set)) {
|
||||
if (callTargetName(callee_node)) |nm| {
|
||||
if (self.inferred_error_sets.get(nm)) |tags| {
|
||||
self.diagTagsNotInSet(tags, caller_set, span);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.checkErrorSetSubset(callee_set, caller_set, span);
|
||||
}
|
||||
}
|
||||
|
||||
/// Structural test: is this `or` a *failable* construct (value-terminator or
|
||||
/// chain), rather than a boolean / optional-unwrap `or`? True when either
|
||||
/// operand is failable-like — a `try`, an error-channel-typed expression, or
|
||||
/// itself a nested failable `or` chain. Kept separate from `inferExprType`:
|
||||
/// a `try`-chain's *value* type is its success type `T` (non-failable), so
|
||||
/// the chain-ness is structural, not type-derived.
|
||||
fn orIsFailableChain(self: *Lowering, bop: *const ast.BinaryOp) bool {
|
||||
return self.operandIsFailableLike(bop.lhs) or self.operandIsFailableLike(bop.rhs);
|
||||
}
|
||||
|
||||
fn operandIsFailableLike(self: *Lowering, node: *const Node) bool {
|
||||
if (node.data == .try_expr) return true;
|
||||
if (node.data == .binary_op and node.data.binary_op.op == .or_op) {
|
||||
return self.orIsFailableChain(&node.data.binary_op);
|
||||
}
|
||||
return self.errorChannelOf(self.inferExprType(node)) != null;
|
||||
}
|
||||
|
||||
/// The success (value) type of a failable `or` chain: descend to the
|
||||
/// leftmost operand, unwrap any `try`, and take its failable success type
|
||||
/// (`void` for a pure-`-> !` chain). All operands share this type.
|
||||
fn orChainSuccessType(self: *Lowering, bop: *const ast.BinaryOp) TypeId {
|
||||
var lhs = bop.lhs;
|
||||
while (lhs.data == .binary_op and lhs.data.binary_op.op == .or_op and
|
||||
self.orIsFailableChain(&lhs.data.binary_op))
|
||||
{
|
||||
lhs = lhs.data.binary_op.lhs;
|
||||
}
|
||||
const ft = self.inferExprType(unwrapTryNode(lhs));
|
||||
const fset = self.errorChannelOf(ft) orelse return .unresolved;
|
||||
return if (ft == fset) .void else self.failableSuccessType(ft);
|
||||
}
|
||||
|
||||
/// `try X` → `X` (the underlying failable); any other node unchanged. In an
|
||||
/// `or` chain the `try` marker's routing IS the chain, so the chain lowers
|
||||
/// the underlying failable directly rather than re-entering `lowerTry`.
|
||||
fn unwrapTryNode(node: *const Node) *const Node {
|
||||
return if (node.data == .try_expr) node.data.try_expr.operand else node;
|
||||
}
|
||||
|
||||
/// Flatten a left-associative failable `or` chain into its operands,
|
||||
/// left-to-right. `a or b or c` parses as `(a or b) or c`; this collects
|
||||
/// `[a, b, c]`. Walks the left spine only while it stays a failable
|
||||
/// `or` chain (a parenthesized non-chain `or` on the left stops the walk).
|
||||
fn flattenOrChain(self: *Lowering, bop: *const ast.BinaryOp, list: *std.ArrayList(*const Node)) void {
|
||||
if (bop.lhs.data == .binary_op and bop.lhs.data.binary_op.op == .or_op and
|
||||
self.orIsFailableChain(&bop.lhs.data.binary_op))
|
||||
{
|
||||
self.flattenOrChain(&bop.lhs.data.binary_op, list);
|
||||
} else {
|
||||
list.append(self.alloc, bop.lhs) catch unreachable;
|
||||
}
|
||||
list.append(self.alloc, bop.rhs) catch unreachable;
|
||||
}
|
||||
|
||||
/// Lower a failable `or` (ERR E2.4): a value-terminator (`lhs or value`) or
|
||||
/// a chain (`try a or try b or …`, possibly with a trailing value
|
||||
/// terminator). Left-to-right, short-circuit: each failable operand's
|
||||
/// failure routes to the next operand; the final operand either absorbs
|
||||
/// (value terminator) or propagates to the enclosing function. Each failed
|
||||
/// attempt pushes a trace frame; an absorbing resolution (any operand
|
||||
/// succeeding, or the value terminator) clears the buffer; total failure
|
||||
/// preserves the frames for the caller.
|
||||
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`", .{});
|
||||
}
|
||||
var operands = std.ArrayList(*const Node).empty;
|
||||
defer operands.deinit(self.alloc);
|
||||
self.flattenOrChain(bop, &operands);
|
||||
const last_idx = operands.items.len - 1;
|
||||
const last_is_value = !self.operandIsFailableLike(operands.items[last_idx]);
|
||||
|
||||
// The chain's total-failure routing. An absorbing consumer (`catch`)
|
||||
// sets this so the final operand's failure reaches the handler; cleared
|
||||
// while lowering operands so a nested operand doesn't inherit it.
|
||||
const fail_target = self.chain_fail_target;
|
||||
self.chain_fail_target = null;
|
||||
defer self.chain_fail_target = fail_target;
|
||||
|
||||
// Success type from the first operand (a failable; unwrap any `try`).
|
||||
const first_ty = self.inferExprType(unwrapTryNode(operands.items[0]));
|
||||
const first_set = self.errorChannelOf(first_ty) orelse {
|
||||
if (self.diagnostics) |d| d.addFmt(.err, span, "the left operand of a failable `or` must be failable; got '{s}'", .{self.formatTypeName(first_ty)});
|
||||
return self.builder.constInt(0, .void);
|
||||
};
|
||||
const has_value = first_ty != first_set;
|
||||
const succ_ty = if (has_value) self.failableSuccessType(first_ty) else TypeId.void;
|
||||
|
||||
// Pure-failable LHS (`-> !`) with a value terminator: nothing to fall
|
||||
// back to.
|
||||
if (!has_value and last_is_value) {
|
||||
if (self.diagnostics) |d| d.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 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", .{});
|
||||
// Caller failability — only needed when the chain can propagate to the
|
||||
// function (final operand is failable AND no absorbing consumer target).
|
||||
var caller_ret: TypeId = .void;
|
||||
var caller_set: TypeId = .void;
|
||||
if (!last_is_value and fail_target == null) {
|
||||
const cret = self.effectiveReturnType();
|
||||
const cset = if (cret) |r| self.errorChannelOf(r) else null;
|
||||
if (cset == null) {
|
||||
if (self.diagnostics) |d| d.addFmt(.err, span, "a failable `or` chain propagates on total failure, so it is only valid inside a failable function — add a value terminator (`… or value`) or wrap with `catch`", .{});
|
||||
return self.builder.constInt(0, .void);
|
||||
}
|
||||
return self.builder.constInt(0, .void);
|
||||
caller_ret = cret.?;
|
||||
caller_set = cset.?;
|
||||
}
|
||||
const succ_ty = self.failableSuccessType(lhs_ty);
|
||||
|
||||
const result = self.lowerExpr(bop.lhs);
|
||||
const err_val = self.extractErrorSlot(result, lhs_ty, err_set);
|
||||
const succ_val = self.extractSuccessValue(result, lhs_ty, succ_ty);
|
||||
const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_set) } }, .bool);
|
||||
const merge_bb = if (has_value)
|
||||
self.freshBlockWithParams("orc.merge", &.{succ_ty})
|
||||
else
|
||||
self.freshBlock("orc.merge");
|
||||
|
||||
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});
|
||||
for (operands.items, 0..) |operand, i| {
|
||||
const is_last = i == last_idx;
|
||||
|
||||
self.builder.switchToBlock(fail_bb);
|
||||
// The `or value` terminator absorbs the LHS's failure: clear the trace
|
||||
// buffer before producing the fallback (ERR E3.2).
|
||||
self.emitTraceClear();
|
||||
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});
|
||||
if (is_last and last_is_value) {
|
||||
// Value terminator: absorbs every prior failure.
|
||||
self.emitTraceClear();
|
||||
const saved = self.target_type;
|
||||
self.target_type = succ_ty;
|
||||
const v = self.lowerExpr(operand);
|
||||
self.target_type = saved;
|
||||
const vc = self.coerceToType(v, self.builder.getRefType(v), succ_ty);
|
||||
self.builder.br(merge_bb, &.{vc});
|
||||
break;
|
||||
}
|
||||
|
||||
// Failable operand (`try X` marker or a bare failable). Lower the
|
||||
// underlying failable; the `try` marker's routing IS the chain.
|
||||
const underlying = unwrapTryNode(operand);
|
||||
const op_ty = self.inferExprType(underlying);
|
||||
const op_set = self.errorChannelOf(op_ty) orelse {
|
||||
if (self.diagnostics) |d| d.addFmt(.err, operand.span, "operand of a failable `or` chain must be failable; got '{s}'", .{self.formatTypeName(op_ty)});
|
||||
return self.builder.constInt(0, .void);
|
||||
};
|
||||
const op_value_carrying = op_ty != op_set;
|
||||
|
||||
// Widening applies only when the final failure escapes to the
|
||||
// function (no absorbing consumer); a `catch` target absorbs it.
|
||||
if (is_last and fail_target == null) self.checkEscapeWidening(underlying, op_set, caller_set, operand.span);
|
||||
|
||||
const result = self.lowerExpr(underlying);
|
||||
const err_val = if (op_value_carrying) self.extractErrorSlot(result, op_ty, op_set) else result;
|
||||
const err_ty = self.builder.getRefType(err_val);
|
||||
const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_ty) } }, .bool);
|
||||
|
||||
const ok_bb = self.freshBlock("orc.ok");
|
||||
const fail_bb = self.freshBlock(if (is_last) "orc.prop" else "orc.next");
|
||||
self.builder.condBr(is_err, fail_bb, &.{}, ok_bb, &.{});
|
||||
|
||||
// Success: the chain resolved here — clear the buffer, merge value.
|
||||
self.builder.switchToBlock(ok_bb);
|
||||
self.emitTraceClear();
|
||||
if (has_value) {
|
||||
const sv = self.extractSuccessValue(result, op_ty, succ_ty);
|
||||
const svc = self.coerceToType(sv, self.builder.getRefType(sv), succ_ty);
|
||||
self.builder.br(merge_bb, &.{svc});
|
||||
} else {
|
||||
self.builder.br(merge_bb, &.{});
|
||||
}
|
||||
|
||||
// Failure: push a trace frame, then either route to the next
|
||||
// operand (same block — no function exit, so `onfail` does not
|
||||
// fire) or, for the final operand, resolve the total failure: to an
|
||||
// absorbing consumer (`catch`) if one set a target, else propagate
|
||||
// to the caller.
|
||||
self.builder.switchToBlock(fail_bb);
|
||||
self.emitTracePush(self.placeholderTraceFrame());
|
||||
if (is_last) {
|
||||
if (fail_target) |t| {
|
||||
const ec = self.coerceToType(err_val, self.builder.getRefType(err_val), t.set);
|
||||
self.builder.br(t.bb, &.{ec});
|
||||
} else {
|
||||
self.emitErrorCleanup(self.func_defer_base, err_val);
|
||||
self.emitErrorReturn(caller_ret, caller_set, err_val);
|
||||
}
|
||||
}
|
||||
// else: fall through — the next operand is lowered in fail_bb.
|
||||
}
|
||||
|
||||
self.builder.switchToBlock(merge_bb);
|
||||
return self.builder.blockParam(merge_bb, 0, succ_ty);
|
||||
return if (has_value) self.builder.blockParam(merge_bb, 0, succ_ty) else self.builder.constInt(0, .void);
|
||||
}
|
||||
|
||||
// ── ERR E1.4b: whole-program inferred-error-set convergence ──────────
|
||||
|
||||
1
tests/expected/246-failable-or-chain.exit
Normal file
1
tests/expected/246-failable-or-chain.exit
Normal file
@@ -0,0 +1 @@
|
||||
120
|
||||
1
tests/expected/246-failable-or-chain.txt
Normal file
1
tests/expected/246-failable-or-chain.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
tests/expected/247-failable-or-chain-propagate.exit
Normal file
1
tests/expected/247-failable-or-chain-propagate.exit
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
8
tests/expected/247-failable-or-chain-propagate.txt
Normal file
8
tests/expected/247-failable-or-chain-propagate.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
error: unhandled error reached main: error.A
|
||||
error return trace (most recent call last):
|
||||
frame 0: <location pending DWARF> (raw 1)
|
||||
frame 1: <location pending DWARF> (raw 1)
|
||||
frame 2: <location pending DWARF> (raw 1)
|
||||
frame 3: <location pending DWARF> (raw 1)
|
||||
frame 4: <location pending DWARF> (raw 1)
|
||||
frame 5: <location pending DWARF> (raw 1)
|
||||
Reference in New Issue
Block a user