diff --git a/examples/229-value-failable-consume.sx b/examples/229-value-failable-consume.sx new file mode 100644 index 0000000..a5d4b23 --- /dev/null +++ b/examples/229-value-failable-consume.sx @@ -0,0 +1,61 @@ +// Consuming value-carrying failables with `try` and `catch` (ERR step E2.1b — +// the consumer side of the error-channel tuple ABI). `try f()` on a +// `-> (T, !E)` callee binds the value slot on success and propagates the error +// on failure (a pure-failable caller returns the tag; a value-carrying caller +// returns `{undef, tag}`). `f() catch e BODY` yields the value slot on success +// or the handler body's value on failure, merged through a block parameter. +// The producer side is `examples/228-value-failable.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; +} + +// value-carrying `try` in a value-carrying caller — propagates {undef, tag}. +inc :: (n: s32) -> (s32, !E) { + v := try parse(n); + return v + 1; +} + +// value-carrying `try` in a pure-failable caller — propagates the tag. +relay :: (n: s32) -> !E { + v := try parse(n); + if v < 0 { raise error.Bad; } + return; +} + +// value-carrying `catch`, bare-expression fallback. +safe :: (n: s32) -> s32 { + return parse(n) catch e 0; +} + +// value-carrying `catch`, match-body value. +classify :: (n: s32) -> s32 { + return parse(n) catch e == { + case .Bad: 1; + case .Empty: 2; + else: 3; + }; +} + +main :: () -> s32 { + r : s32 = 0; + a, ea := inc(5); // parse(5)=10 → v=10 → 11 + if ea == error.Bad { r = r + 100; } // false + r = r + a; // +11 + b, eb := inc(-1); // parse(-1)=Bad → propagate {undef, Bad} + if eb == error.Bad { r = r + 4; } // true → +4 + er := relay(3); // parse(3)=6 ok → relay ok + if er == error.Bad { r = r + 50; } // false + r = r + safe(7); // parse(7)=14 → +14 + r = r + safe(-1); // Bad → catch → 0 + r = r + classify(-1); // Bad → 1 + r = r + classify(0); // Empty → 2 + print("consume result: {}\n", r); // 11+4+14+0+1+2 = 32 + return r; +} diff --git a/examples/230-value-failable-reject.sx b/examples/230-value-failable-reject.sx new file mode 100644 index 0000000..579ca94 --- /dev/null +++ b/examples/230-value-failable-reject.sx @@ -0,0 +1,19 @@ +// Value-carrying `catch` rejection (ERR step E2.1b): when the failable LHS +// carries a value, a non-diverging catch handler must produce a value of the +// success type — a value-less (void) body is a type error (otherwise the +// success and error paths couldn't merge to one value). Diverge instead +// (`return` / `raise`) or yield a value. Positives: `examples/229-value-failable-consume.sx`. + +#import "modules/std.sx"; + +E :: error { Bad } + +parse :: (n: s32) -> (s32, !E) { + if n < 0 { raise error.Bad; } + return n; +} + +main :: () -> s32 { + x := parse(-1) catch e { print("oops\n"); }; // error: body yields no value + return x; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 11d6663..3528652 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -15263,16 +15263,12 @@ pub const Lowering = struct { return self.builder.constInt(0, .void); }; - // E1.4a scope guards — bail loudly on the shapes E2/E1.4b own. - if (op_ty != callee_set) { - // Value-carrying callee: `try` must bind the value slot(s), which - // needs the error-channel tuple ABI (E2). - return self.bailTry(span, "a value-carrying failable callee (`-> (T..., !)`)"); - } - if (caller_ret != caller_set) { - // Propagating from a value-carrying caller needs undef value slots - // alongside the error slot (E2's tuple ABI). - return self.bailTry(span, "a value-carrying failable function"); + // A value-carrying callee (`-> (T, !)`) returns a tuple `{v, err}`; a + // pure-failable callee (`-> !`) returns the bare error tag. Multi-value + // callees (`-> (T1, T2, !)`) need multi-slot extraction — deferred. + const callee_value_carrying = op_ty != callee_set; + if (callee_value_carrying and self.module.types.get(op_ty).tuple.fields.len != 2) { + return self.bailTry(span, "a multi-value failable callee (`-> (T1, T2, !)`)"); } // (3) Widening: the callee's escape set must be ⊆ the caller's named @@ -15293,28 +15289,64 @@ pub const Lowering = struct { } } - // (4) Lower: evaluate the call (→ the error tag, 0 = success), branch. - const err_val = self.lowerExpr(operand); + // (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 + // a value-carrying one). + const result = self.lowerExpr(operand); + const err_val = if (callee_value_carrying) + self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 1, .base_type = op_ty } }, callee_set) + else + result; const err_ty = self.builder.getRefType(err_val); - const zero = self.builder.constInt(0, err_ty); - const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = zero } }, .bool); + const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_ty) } }, .bool); const prop_bb = self.freshBlock("try.prop"); const ok_bb = self.freshBlock("try.ok"); self.builder.condBr(is_err, prop_bb, &.{}, ok_bb, &.{}); - // Propagation: run the function's defers, then return the error tag - // (widened to the caller's set — global-flat tag ids carry across). + // Propagation: run the function's defers, then return the caller's + // failure carrying this tag (pure caller → `ret(tag)`; value-carrying + // caller → `ret {undef..., tag}`). self.builder.switchToBlock(prop_bb); self.emitBlockDefers(self.func_defer_base); - const widened = if (err_ty != caller_set) self.coerceToType(err_val, err_ty, caller_set) else err_val; - self.builder.ret(widened, caller_set); + self.emitErrorReturn(caller_ret, caller_set, err_val); - // Success: continue. A pure-failable callee has no value slots. + // Success: a value-carrying callee yields its value slot; a + // pure-failable callee has no value (void). self.builder.switchToBlock(ok_bb); + if (callee_value_carrying) { + const succ_ty = self.module.types.get(op_ty).tuple.fields[0]; + return self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 0, .base_type = op_ty } }, succ_ty); + } return self.builder.constInt(0, .void); } + /// Return the enclosing function's failure carrying error tag `err`. A + /// pure-failable caller (`-> !`) returns the tag directly; a value-carrying + /// caller (`-> (T..., !)`) returns `{undef value slots..., tag}`. Honors + /// inline-comptime return targets. The caller emits defers first. + fn emitErrorReturn(self: *Lowering, caller_ret: TypeId, caller_set: TypeId, err: Ref) void { + const ety = self.builder.getRefType(err); + const coerced = if (ety != caller_set) self.coerceToType(err, ety, caller_set) else err; + if (caller_ret == caller_set) { + if (self.inline_return_target) |iri| { + self.builder.store(iri.slot, coerced); + self.builder.br(iri.done_bb, &.{}); + } else { + self.builder.ret(coerced, caller_set); + } + } else { + const fields = self.module.types.get(caller_ret).tuple.fields; + var undefs = std.ArrayList(Ref).empty; + defer undefs.deinit(self.alloc); + for (fields[0 .. fields.len - 1]) |vty| { + undefs.append(self.alloc, self.builder.constUndef(vty)) catch unreachable; + } + const tup = self.buildFailableTuple(caller_ret, undefs.items, coerced); + self.emitTupleRet(caller_ret, tup); + } + } + fn diagTryNotFailable(self: *Lowering, span: ast.Span) void { if (self.diagnostics) |diags| { diags.addFmt(.err, span, "`try` is only valid inside a failable function (a return type with `!` or `!Named`)", .{}); @@ -15338,46 +15370,90 @@ pub const Lowering = struct { } return self.builder.constInt(0, .void); }; - if (op_ty != err_set) { - if (self.diagnostics) |diags| { - diags.addFmt(.err, span, "`catch` on a value-carrying failable (`-> (T..., !)`) is not yet lowered — pending the error-channel tuple ABI (ERR E2)", .{}); - } + // Pure-failable LHS (`-> !`): no success value. Run the body on the + // error path; both paths fall through to a value-less merge. + if (op_ty == err_set) { + const err_val = self.lowerExpr(ce.operand); + 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 handle_bb = self.freshBlock("catch.handle"); + const merge_bb = self.freshBlock("catch.merge"); + self.builder.condBr(is_err, handle_bb, &.{}, merge_bb, &.{}); + self.builder.switchToBlock(handle_bb); + _ = self.runCatchBody(ce, err_val, err_set, null); + if (!self.currentBlockHasTerminator()) self.builder.br(merge_bb, &.{}); + self.builder.switchToBlock(merge_bb); return self.builder.constInt(0, .void); } - // Evaluate the operand → the error tag (0 = success), then branch. - const err_val = self.lowerExpr(ce.operand); - const err_ty = self.builder.getRefType(err_val); - const zero = self.builder.constInt(0, err_ty); - const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = zero } }, .bool); + // Value-carrying LHS (`-> (T, !)`): on success the catch yields the + // value slot; on error it yields the handler body's value. The paths + // merge through a block-parameter (phi). Multi-value is deferred. + const fields = self.module.types.get(op_ty).tuple.fields; + if (fields.len != 2) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, span, "`catch` 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(ce.operand); + const err_val = self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 1, .base_type = op_ty } }, err_set); + const succ_val = self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 0, .base_type = op_ty } }, succ_ty); + const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_set) } }, .bool); const handle_bb = self.freshBlock("catch.handle"); - const merge_bb = self.freshBlock("catch.merge"); - // On success (err == 0) jump straight to the merge; on error, handle. - self.builder.condBr(is_err, handle_bb, &.{}, merge_bb, &.{}); + const merge_bb = self.freshBlockWithParams("catch.merge", &.{succ_ty}); + // Success → merge with the value slot; error → run the handler. + self.builder.condBr(is_err, handle_bb, &.{}, merge_bb, &.{succ_val}); - // Handle: bind the tag (typed as the error set) and run the body in a - // child scope. The body diverges (terminates the block) or falls - // through to the merge. self.builder.switchToBlock(handle_bb); + const body_val = self.runCatchBody(ce, err_val, err_set, succ_ty); + if (!self.currentBlockHasTerminator()) { + // A non-diverging handler must produce a value of the success type. + // A value-less (void) body is a type error — diagnose and feed an + // undef placeholder so the merge phi stays well-typed (rather than + // coercing `void` into a bad ref). + 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) |diags| { + diags.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}); + } + + self.builder.switchToBlock(merge_bb); + return self.builder.blockParam(merge_bb, 0, succ_ty); + } + + /// 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 + /// null (pure-failable catch), runs the body for effect and returns null. + fn runCatchBody(self: *Lowering, ce: *const ast.CatchExpr, err_val: Ref, err_set: TypeId, want_ty: ?TypeId) ?Ref { var handle_scope = Scope.init(self.alloc, self.scope); const saved_scope = self.scope; self.scope = &handle_scope; + defer { + self.scope = saved_scope; + handle_scope.deinit(); + } if (ce.binding) |name| { handle_scope.put(name, .{ .ref = err_val, .ty = err_set, .is_alloca = false }); } - if (ce.body.data == .block) { - self.lowerBlock(ce.body); - } else { - _ = self.lowerExpr(ce.body); + if (want_ty == null) { + if (ce.body.data == .block) self.lowerBlock(ce.body) else _ = self.lowerExpr(ce.body); + return null; } - self.scope = saved_scope; - handle_scope.deinit(); - if (!self.currentBlockHasTerminator()) self.builder.br(merge_bb, &.{}); - - // Merge (success path + non-diverging handle). Pure-failable → void. - self.builder.switchToBlock(merge_bb); - return self.builder.constInt(0, .void); + const saved_fbv = self.force_block_value; + self.force_block_value = true; + defer self.force_block_value = saved_fbv; + return if (ce.body.data == .block) self.lowerBlockValue(ce.body) else self.lowerExpr(ce.body); } fn bailTry(self: *Lowering, span: ast.Span, comptime what: []const u8) Ref { diff --git a/tests/expected/229-value-failable-consume.exit b/tests/expected/229-value-failable-consume.exit new file mode 100644 index 0000000..f5c8955 --- /dev/null +++ b/tests/expected/229-value-failable-consume.exit @@ -0,0 +1 @@ +32 diff --git a/tests/expected/229-value-failable-consume.txt b/tests/expected/229-value-failable-consume.txt new file mode 100644 index 0000000..c4baab4 --- /dev/null +++ b/tests/expected/229-value-failable-consume.txt @@ -0,0 +1 @@ +consume result: 32 diff --git a/tests/expected/230-value-failable-reject.exit b/tests/expected/230-value-failable-reject.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/tests/expected/230-value-failable-reject.exit @@ -0,0 +1 @@ +1 diff --git a/tests/expected/230-value-failable-reject.txt b/tests/expected/230-value-failable-reject.txt new file mode 100644 index 0000000..20f1fa5 --- /dev/null +++ b/tests/expected/230-value-failable-reject.txt @@ -0,0 +1,5 @@ +error: `catch` body must produce a value of type 's32' (or diverge with `return` / `raise`) + --> /Users/agra/projects/sx/examples/230-value-failable-reject.sx:17:10 + | +17 | x := parse(-1) catch e { print("oops\n"); }; // error: body yields no value + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^