diff --git a/examples/246-failable-or-chain.sx b/examples/246-failable-or-chain.sx new file mode 100644 index 0000000..5ad0faa --- /dev/null +++ b/examples/246-failable-or-chain.sx @@ -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 +} diff --git a/examples/247-failable-or-chain-propagate.sx b/examples/247-failable-or-chain-propagate.sx new file mode 100644 index 0000000..137f7e3 --- /dev/null +++ b/examples/247-failable-or-chain-propagate.sx @@ -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; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 51d4c84..a4eb6b4 100644 --- a/src/ir/lower.zig +++ b/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.(...)` 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 ────────── diff --git a/tests/expected/246-failable-or-chain.exit b/tests/expected/246-failable-or-chain.exit new file mode 100644 index 0000000..52bd8e4 --- /dev/null +++ b/tests/expected/246-failable-or-chain.exit @@ -0,0 +1 @@ +120 diff --git a/tests/expected/246-failable-or-chain.txt b/tests/expected/246-failable-or-chain.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/expected/246-failable-or-chain.txt @@ -0,0 +1 @@ + diff --git a/tests/expected/247-failable-or-chain-propagate.exit b/tests/expected/247-failable-or-chain-propagate.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/tests/expected/247-failable-or-chain-propagate.exit @@ -0,0 +1 @@ +1 diff --git a/tests/expected/247-failable-or-chain-propagate.txt b/tests/expected/247-failable-or-chain-propagate.txt new file mode 100644 index 0000000..96d057a --- /dev/null +++ b/tests/expected/247-failable-or-chain-propagate.txt @@ -0,0 +1,8 @@ +error: unhandled error reached main: error.A +error return trace (most recent call last): + frame 0: (raw 1) + frame 1: (raw 1) + frame 2: (raw 1) + frame 3: (raw 1) + frame 4: (raw 1) + frame 5: (raw 1)