diff --git a/examples/235-multi-value-failable.sx b/examples/235-multi-value-failable.sx new file mode 100644 index 0000000..ab7de90 --- /dev/null +++ b/examples/235-multi-value-failable.sx @@ -0,0 +1,64 @@ +// Multi-value value-carrying failables (ERR — the multi-value error-channel +// ABI). A `-> (T1, T2, !E)` function returns EITHER a value-tuple OR an error: +// `return (a, b)` yields the success tuple `{a, b, 0}` (the compiler appends the +// no-error slot) and `raise error.X` yields `{undef, undef, tag}`. Every consumer +// generalizes from the single-value shape: a destructure binds every slot +// INCLUDING the error (dropping it is the spec'd discard error — bind it and +// inspect); `try` binds the value-tuple on success and propagates `{undef..., tag}` +// on failure; `catch` / `or` absorb the failure and merge the value-tuple or the +// handler/terminator value. Single-value `-> (T, !E)` is examples/228-231. + +#import "modules/std.sx"; + +E :: error { Bad, Empty } + +parse :: (n: s32) -> (s32, s32, !E) { + if n < 0 { raise error.Bad; } + if n == 0 { raise error.Empty; } + return (n * 2, n + 1); // success → {n*2, n+1, 0} +} + +// Multi-value `try` in a multi-value caller — propagates {undef, undef, tag}. +inc :: (n: s32) -> (s32, s32, !E) { + v, b := try parse(n); + return (v + 1, b + 1); +} + +// Multi-value `catch`, bare-expression tuple fallback (absorbs the failure). +safe :: (n: s32) -> s32 { + v, b := parse(n) catch e (40, 50); + return v + b; +} + +// Multi-value `or (tuple)` value-terminator (absorbs the failure). +ortest :: (n: s32) -> s32 { + v, b := parse(n) or (7, 8); + return v + b; +} + +main :: () -> s32 { + r : s32 = 0; + + // Destructure binds EVERY slot including the error tag (e1 / e2 / e3) — + // the error is treated, never dropped. + v1, b1, e1 := parse(5); // success → (10, 6, no-error) + if e1 == error.Bad { r = r + 1000; } // false + r = r + v1 + b1; // +16 + + v2, b2, e2 := parse(-1); // Bad → {undef, undef, Bad} + if e2 == error.Bad { r = r + 4; } // +4 + + a, c, ea := inc(5); // parse(5)=(10,6) → (11, 7, no-error) + if ea == error.Bad { r = r + 2000; } // false + r = r + a + c; // +18 + + a2, c2, e3 := inc(-1); // try parse(-1)=Bad → propagate {undef, undef, Bad} + if e3 == error.Bad { r = r + 5; } // +5 + + r = r + safe(5); // (10, 6) → 16 + r = r + safe(-1); // Bad → catch → (40, 50) → 90 + r = r + ortest(0); // Empty → or → (7, 8) → 15 + + print("multi-value result: {}\n", r); // 16+4+18+5+16+90+15 = 164 + return r; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 3a9d667..0e8855f 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -13645,8 +13645,7 @@ pub const Lowering = struct { const lt = self.inferExprType(bop.lhs); if (self.errorChannelOf(lt)) |ch| { if (lt == ch) break :blk .unresolved; // pure-failable (rejected at lowering) - const f = self.module.types.get(lt).tuple.fields; - break :blk if (f.len == 2) f[0] else .unresolved; + break :blk self.failableSuccessType(lt); } break :blk .bool; }, @@ -13665,15 +13664,13 @@ pub const Lowering = struct { }, // `try X` evaluates to X's success type (the value part). A // pure-failable operand (`-> !` / `-> !Named`, whose type IS the - // error set) has no value → `void`; a value-carrying `-> (T, !)` - // operand yields its value part (single field, or the tuple). + // error set) has no value → `void`; a value-carrying `-> (T..., !)` + // operand yields its value part (the lone value, or a value-tuple). .try_expr => |te| blk: { const op_ty = self.inferExprType(te.operand); const channel = self.errorChannelOf(op_ty) orelse break :blk .unresolved; if (op_ty == channel) break :blk .void; - const info = self.module.types.get(op_ty); - if (info == .tuple and info.tuple.fields.len == 2) break :blk info.tuple.fields[0]; - break :blk op_ty; + break :blk self.failableSuccessType(op_ty); }, // `expr catch ...` strips the error channel → the success type // (void for a pure-failable LHS; the value part for value-carrying). @@ -13681,9 +13678,7 @@ pub const Lowering = struct { const op_ty = self.inferExprType(ce.operand); const channel = self.errorChannelOf(op_ty) orelse break :blk .unresolved; if (op_ty == channel) break :blk .void; - const info = self.module.types.get(op_ty); - if (info == .tuple and info.tuple.fields.len == 2) break :blk info.tuple.fields[0]; - break :blk op_ty; + break :blk self.failableSuccessType(op_ty); }, .if_expr => |ie| { // If-else types as its branches' unified type. A `noreturn` @@ -14876,6 +14871,24 @@ pub const Lowering = struct { return self.builder.boxAny(val, src_ty); } + // Tuple → Tuple element-wise coercion (e.g. a `(s64, s64)` literal + // flowing into a `(s32, s32)` slot — the multi-value failable success + // tuple). Same arity, at least one differing field (src_ty == dst_ty + // already returned above): extract each slot, coerce it, rebuild. + if (!src_ty.isBuiltin() and !dst_ty.isBuiltin()) { + const si = self.module.types.get(src_ty); + const di = self.module.types.get(dst_ty); + if (si == .tuple and di == .tuple and si.tuple.fields.len == di.tuple.fields.len) { + var elems = std.ArrayList(Ref).empty; + defer elems.deinit(self.alloc); + for (si.tuple.fields, di.tuple.fields, 0..) |sf, df, i| { + const fv = self.builder.emit(.{ .tuple_get = .{ .base = val, .field_index = @intCast(i), .base_type = src_ty } }, sf); + elems.append(self.alloc, self.coerceToType(fv, sf, df)) catch unreachable; + } + return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, elems.items) catch unreachable } }, dst_ty); + } + } + // Optional → Concrete unwrapping (flow-sensitive narrowing coercion) if (!src_ty.isBuiltin()) { const src_info = self.module.types.get(src_ty); @@ -15251,8 +15264,9 @@ pub const Lowering = struct { /// Return a value-carrying failable function's success tuple /// `{value(s)..., 0}` from `ref` (the user-returned value part). Forwarding /// a full failable tuple (`return other_failable()` / explicit `return - /// (v, e)`) returns it as-is. ERR E2.1a covers the single-value `-> (T, !)` - /// shape; multi-value `-> (T1, T2, !)` is deferred. + /// (v, e)`) returns it as-is. Single-value `-> (T, !)` takes `ref` as the + /// lone value; multi-value `-> (T1, ..., !)` takes `ref` as a value-tuple + /// `(T1, ...)` and re-assembles its slots alongside the success error slot. fn lowerFailableSuccessReturn(self: *Lowering, ref: Ref, ret_ty: TypeId, span: ast.Span) void { const fields = self.module.types.get(ret_ty).tuple.fields; const err_ty = fields[fields.len - 1]; @@ -15262,15 +15276,31 @@ pub const Lowering = struct { self.emitTupleRet(ret_ty, ref); return; } - if (fields.len == 2) { + const n_vals = fields.len - 1; + if (n_vals == 1) { const cv = self.coerceToType(ref, val_ty, fields[0]); const tup = self.buildFailableTuple(ret_ty, &.{cv}, self.builder.constInt(0, err_ty)); self.emitTupleRet(ret_ty, tup); return; } - if (self.diagnostics) |diags| { - diags.addFmt(.err, span, "returning a value from a multi-value failable function (`-> (T1, T2, !)`) is not yet lowered — pending the multi-value error-channel ABI (ERR E2); single-value `-> (T, !)` works", .{}); + // Multi-value: `ref` must be a value-tuple `(T1, ..., Tn)`. Extract + // each value slot, coerce to the declared field type, and re-assemble + // with the success error slot (0). + if (val_ty.isBuiltin() or self.module.types.get(val_ty) != .tuple or self.module.types.get(val_ty).tuple.fields.len != n_vals) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, span, "a multi-value failable function (`-> (T1, ..., !)`) must `return` a {d}-tuple of its value types", .{n_vals}); + } + return; } + const vfields = self.module.types.get(val_ty).tuple.fields; + var vals = std.ArrayList(Ref).empty; + defer vals.deinit(self.alloc); + for (0..n_vals) |i| { + const fv = self.builder.emit(.{ .tuple_get = .{ .base = ref, .field_index = @intCast(i), .base_type = val_ty } }, vfields[i]); + vals.append(self.alloc, self.coerceToType(fv, vfields[i], fields[i])) catch unreachable; + } + const tup = self.buildFailableTuple(ret_ty, vals.items, self.builder.constInt(0, err_ty)); + self.emitTupleRet(ret_ty, tup); } /// Build a failable return tuple `{value_refs..., tag}` typed `ret_ty`. @@ -15282,6 +15312,45 @@ pub const Lowering = struct { return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, fields.items) catch unreachable } }, ret_ty); } + /// The success (value-part) type of a value-carrying failable tuple + /// `op_ty` (`-> (T..., !)`): the lone value type for a single-value + /// failable, or a synthesized value-tuple `(T1, ..., Tn)` (error slot + /// dropped) for a multi-value one. Callers must pass a value-carrying + /// tuple — a pure `-> !`'s success type is `void`, handled separately. + fn failableSuccessType(self: *Lowering, op_ty: TypeId) TypeId { + const fields = self.module.types.get(op_ty).tuple.fields; + const n_vals = fields.len - 1; + if (n_vals == 1) return fields[0]; + return self.module.types.intern(.{ .tuple = .{ + .fields = self.alloc.dupe(TypeId, fields[0..n_vals]) catch unreachable, + .names = null, + } }); + } + + /// Extract the success value from an evaluated value-carrying failable + /// tuple `result` (type `op_ty`): the lone value slot for single-value, + /// or an assembled value-tuple (typed `succ_ty`) for multi-value. + fn extractSuccessValue(self: *Lowering, result: Ref, op_ty: TypeId, succ_ty: TypeId) Ref { + const fields = self.module.types.get(op_ty).tuple.fields; + const n_vals = fields.len - 1; + if (n_vals == 1) { + return self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 0, .base_type = op_ty } }, fields[0]); + } + var vals = std.ArrayList(Ref).empty; + defer vals.deinit(self.alloc); + for (0..n_vals) |i| { + vals.append(self.alloc, self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = @intCast(i), .base_type = op_ty } }, fields[i])) catch unreachable; + } + return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, vals.items) catch unreachable } }, succ_ty); + } + + /// Extract the error slot (always the last field) of an evaluated + /// value-carrying failable tuple `result`, typed as `err_set`. + fn extractErrorSlot(self: *Lowering, result: Ref, op_ty: TypeId, err_set: TypeId) Ref { + const fields = self.module.types.get(op_ty).tuple.fields; + return self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = @intCast(fields.len - 1), .base_type = op_ty } }, err_set); + } + /// Emit a return of an already-assembled tuple, honoring inline-comptime /// return targets (store + branch) vs a real function return. fn emitTupleRet(self: *Lowering, ret_ty: TypeId, tup: Ref) void { @@ -15337,13 +15406,10 @@ pub const Lowering = struct { return self.builder.constInt(0, .void); }; - // 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. + // A value-carrying callee (`-> (T..., !)`) returns a tuple + // `{v..., err}`; a pure-failable callee (`-> !`) returns the bare + // error tag. 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 // set. For an inferred caller (`!`) the absorption happens in the @@ -15368,7 +15434,7 @@ pub const Lowering = struct { // 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) + self.extractErrorSlot(result, op_ty, callee_set) else result; const err_ty = self.builder.getRefType(err_val); @@ -15385,12 +15451,12 @@ pub const Lowering = struct { self.emitErrorCleanup(self.func_defer_base, err_val); self.emitErrorReturn(caller_ret, caller_set, err_val); - // Success: a value-carrying callee yields its value slot; a - // pure-failable callee has no value (void). + // Success: a value-carrying callee yields its value part (the lone + // value, or a value-tuple); 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); + const succ_ty = self.failableSuccessType(op_ty); + return self.extractSuccessValue(result, op_ty, succ_ty); } return self.builder.constInt(0, .void); } @@ -15460,20 +15526,14 @@ pub const Lowering = struct { return self.builder.constInt(0, .void); } - // 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]; + // Value-carrying LHS (`-> (T..., !)`): on success the catch yields the + // value part (the lone value, or a value-tuple); on error it yields + // the handler body's value. The paths merge through a block-parameter + // (phi). + const succ_ty = self.failableSuccessType(op_ty); 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 err_val = self.extractErrorSlot(result, op_ty, err_set); + const succ_val = self.extractSuccessValue(result, 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"); @@ -15530,19 +15590,12 @@ pub const Lowering = struct { 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 { - if (self.diagnostics) |diags| { - diags.addFmt(.err, span, "`try` with " ++ what ++ " is not yet lowered — pending the error-channel tuple ABI (ERR E2)", .{}); - } - return self.builder.constInt(0, .void); - } - /// `lhs or rhs` with a failable LHS (ERR step E2.4a — the value-terminator - /// form). On LHS success the result is its value; on failure the LHS error - /// is discarded and the result is `rhs` (a plain value of the success - /// type), so the whole expression is non-failable. Single-value - /// value-carrying LHS only. The CHAIN form (`... or try ...` / a failable - /// RHS) needs the fallback-target routing deferred from E1.4 — bail. + /// form). On LHS success the result is its value part (the lone value, or a + /// value-tuple); on failure the LHS error is discarded and the result is + /// `rhs` (a plain value of the success type), so the whole expression is + /// non-failable. The CHAIN form (`... or try ...` / a failable RHS) needs + /// the fallback-target routing deferred from E1.4 — bail. fn lowerFailableOr(self: *Lowering, bop: *const ast.BinaryOp) Ref { const span = bop.lhs.span; @@ -15568,18 +15621,11 @@ pub const Lowering = struct { } return self.builder.constInt(0, .void); } - const fields = self.module.types.get(lhs_ty).tuple.fields; - if (fields.len != 2) { - if (self.diagnostics) |diags| { - diags.addFmt(.err, span, "`or value` on a multi-value failable (`-> (T1, T2, !)`) is not yet lowered — pending the multi-value error-channel ABI (ERR E2)", .{}); - } - return self.builder.constInt(0, .void); - } - const succ_ty = fields[0]; + const succ_ty = self.failableSuccessType(lhs_ty); const result = self.lowerExpr(bop.lhs); - const err_val = self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 1, .base_type = lhs_ty } }, err_set); - const succ_val = self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 0, .base_type = lhs_ty } }, succ_ty); + const 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 fail_bb = self.freshBlock("or.fail"); diff --git a/tests/expected/235-multi-value-failable.exit b/tests/expected/235-multi-value-failable.exit new file mode 100644 index 0000000..4e9bdff --- /dev/null +++ b/tests/expected/235-multi-value-failable.exit @@ -0,0 +1 @@ +164 diff --git a/tests/expected/235-multi-value-failable.txt b/tests/expected/235-multi-value-failable.txt new file mode 100644 index 0000000..26a704e --- /dev/null +++ b/tests/expected/235-multi-value-failable.txt @@ -0,0 +1 @@ +multi-value result: 164