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:
agra
2026-06-01 10:31:43 +03:00
parent e898effb4b
commit 1d6e26f011
7 changed files with 357 additions and 62 deletions

View 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
}

View 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;
}

View File

@@ -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 ──────────

View File

@@ -0,0 +1 @@
120

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
1

View 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)