ERR/E2: multi-value failables -> (T1, ..., !)

Generalize the single-value `-> (T, !)` error-channel ABI to any value
arity. Retire the five `fields.len == 2` bails (lowerFailableSuccessReturn,
lowerTry, lowerCatch, lowerFailableOr, and the inferExprType try/catch/or
arms); lowerRaise + emitErrorReturn already looped over N value slots.

New helpers centralize "value-part = every slot but the last (error) one":
failableSuccessType (lone value type, or a value-tuple), extractSuccessValue,
extractErrorSlot.

Fix one latent bug the feature surfaced: coerceToType had no tuple->tuple
arm, so a value-tuple flowing into a differently-typed success slot (e.g.
(s64,s64) catch body into (s32,s32)) fell through unchanged. Add element-wise
coercion. No lowerTupleLiteral change is needed: a `return (a, b)` literal
against a 3-field failable target already gets target_fields=null via the
arity mismatch, so it types as a plain value-tuple that
lowerFailableSuccessReturn consumes.

examples/235-multi-value-failable.sx exercises producer return/raise,
destructure (binding every slot incl. the error tag), multi-value try
(success + propagation), catch (bare-expr tuple body), and or-tuple
terminator. Match-body tuple arms are left out: `(` after `case PAT:` is
parsed as a payload capture (a pre-existing, multi-value-unrelated parser
bug). Gates: zig build, zig build test, 273/273 examples.
This commit is contained in:
agra
2026-05-31 23:32:16 +03:00
parent 57d8e327cd
commit ae330365b4
4 changed files with 173 additions and 61 deletions

View File

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

View File

@@ -13645,8 +13645,7 @@ pub const Lowering = struct {
const lt = self.inferExprType(bop.lhs); const lt = self.inferExprType(bop.lhs);
if (self.errorChannelOf(lt)) |ch| { if (self.errorChannelOf(lt)) |ch| {
if (lt == ch) break :blk .unresolved; // pure-failable (rejected at lowering) if (lt == ch) break :blk .unresolved; // pure-failable (rejected at lowering)
const f = self.module.types.get(lt).tuple.fields; break :blk self.failableSuccessType(lt);
break :blk if (f.len == 2) f[0] else .unresolved;
} }
break :blk .bool; break :blk .bool;
}, },
@@ -13665,15 +13664,13 @@ pub const Lowering = struct {
}, },
// `try X` evaluates to X's success type (the value part). A // `try X` evaluates to X's success type (the value part). A
// pure-failable operand (`-> !` / `-> !Named`, whose type IS the // pure-failable operand (`-> !` / `-> !Named`, whose type IS the
// error set) has no value → `void`; a value-carrying `-> (T, !)` // error set) has no value → `void`; a value-carrying `-> (T..., !)`
// operand yields its value part (single field, or the tuple). // operand yields its value part (the lone value, or a value-tuple).
.try_expr => |te| blk: { .try_expr => |te| blk: {
const op_ty = self.inferExprType(te.operand); const op_ty = self.inferExprType(te.operand);
const channel = self.errorChannelOf(op_ty) orelse break :blk .unresolved; const channel = self.errorChannelOf(op_ty) orelse break :blk .unresolved;
if (op_ty == channel) break :blk .void; if (op_ty == channel) break :blk .void;
const info = self.module.types.get(op_ty); break :blk self.failableSuccessType(op_ty);
if (info == .tuple and info.tuple.fields.len == 2) break :blk info.tuple.fields[0];
break :blk op_ty;
}, },
// `expr catch ...` strips the error channel → the success type // `expr catch ...` strips the error channel → the success type
// (void for a pure-failable LHS; the value part for value-carrying). // (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 op_ty = self.inferExprType(ce.operand);
const channel = self.errorChannelOf(op_ty) orelse break :blk .unresolved; const channel = self.errorChannelOf(op_ty) orelse break :blk .unresolved;
if (op_ty == channel) break :blk .void; if (op_ty == channel) break :blk .void;
const info = self.module.types.get(op_ty); break :blk self.failableSuccessType(op_ty);
if (info == .tuple and info.tuple.fields.len == 2) break :blk info.tuple.fields[0];
break :blk op_ty;
}, },
.if_expr => |ie| { .if_expr => |ie| {
// If-else types as its branches' unified type. A `noreturn` // 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); 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) // Optional → Concrete unwrapping (flow-sensitive narrowing coercion)
if (!src_ty.isBuiltin()) { if (!src_ty.isBuiltin()) {
const src_info = self.module.types.get(src_ty); 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 /// Return a value-carrying failable function's success tuple
/// `{value(s)..., 0}` from `ref` (the user-returned value part). Forwarding /// `{value(s)..., 0}` from `ref` (the user-returned value part). Forwarding
/// a full failable tuple (`return other_failable()` / explicit `return /// a full failable tuple (`return other_failable()` / explicit `return
/// (v, e)`) returns it as-is. ERR E2.1a covers the single-value `-> (T, !)` /// (v, e)`) returns it as-is. Single-value `-> (T, !)` takes `ref` as the
/// shape; multi-value `-> (T1, T2, !)` is deferred. /// 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 { fn lowerFailableSuccessReturn(self: *Lowering, ref: Ref, ret_ty: TypeId, span: ast.Span) void {
const fields = self.module.types.get(ret_ty).tuple.fields; const fields = self.module.types.get(ret_ty).tuple.fields;
const err_ty = fields[fields.len - 1]; const err_ty = fields[fields.len - 1];
@@ -15262,15 +15276,31 @@ pub const Lowering = struct {
self.emitTupleRet(ret_ty, ref); self.emitTupleRet(ret_ty, ref);
return; 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 cv = self.coerceToType(ref, val_ty, fields[0]);
const tup = self.buildFailableTuple(ret_ty, &.{cv}, self.builder.constInt(0, err_ty)); const tup = self.buildFailableTuple(ret_ty, &.{cv}, self.builder.constInt(0, err_ty));
self.emitTupleRet(ret_ty, tup); self.emitTupleRet(ret_ty, tup);
return; return;
} }
if (self.diagnostics) |diags| { // Multi-value: `ref` must be a value-tuple `(T1, ..., Tn)`. Extract
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", .{}); // 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`. /// 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); 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 /// Emit a return of an already-assembled tuple, honoring inline-comptime
/// return targets (store + branch) vs a real function return. /// return targets (store + branch) vs a real function return.
fn emitTupleRet(self: *Lowering, ret_ty: TypeId, tup: Ref) void { fn emitTupleRet(self: *Lowering, ret_ty: TypeId, tup: Ref) void {
@@ -15337,13 +15406,10 @@ pub const Lowering = struct {
return self.builder.constInt(0, .void); return self.builder.constInt(0, .void);
}; };
// A value-carrying callee (`-> (T, !)`) returns a tuple `{v, err}`; a // A value-carrying callee (`-> (T..., !)`) returns a tuple
// pure-failable callee (`-> !`) returns the bare error tag. Multi-value // `{v..., err}`; a pure-failable callee (`-> !`) returns the bare
// callees (`-> (T1, T2, !)`) need multi-slot extraction — deferred. // error tag.
const callee_value_carrying = op_ty != callee_set; 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 // (3) Widening: the callee's escape set must be ⊆ the caller's named
// set. For an inferred caller (`!`) the absorption happens in the // set. For an inferred caller (`!`) the absorption happens in the
@@ -15368,7 +15434,7 @@ pub const Lowering = struct {
// a value-carrying one). // a value-carrying one).
const result = self.lowerExpr(operand); const result = self.lowerExpr(operand);
const err_val = if (callee_value_carrying) 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 else
result; result;
const err_ty = self.builder.getRefType(err_val); 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.emitErrorCleanup(self.func_defer_base, err_val);
self.emitErrorReturn(caller_ret, caller_set, err_val); self.emitErrorReturn(caller_ret, caller_set, err_val);
// Success: a value-carrying callee yields its value slot; a // Success: a value-carrying callee yields its value part (the lone
// pure-failable callee has no value (void). // value, or a value-tuple); a pure-failable callee has no value (void).
self.builder.switchToBlock(ok_bb); self.builder.switchToBlock(ok_bb);
if (callee_value_carrying) { if (callee_value_carrying) {
const succ_ty = self.module.types.get(op_ty).tuple.fields[0]; const succ_ty = self.failableSuccessType(op_ty);
return self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 0, .base_type = op_ty } }, succ_ty); return self.extractSuccessValue(result, op_ty, succ_ty);
} }
return self.builder.constInt(0, .void); return self.builder.constInt(0, .void);
} }
@@ -15460,20 +15526,14 @@ pub const Lowering = struct {
return self.builder.constInt(0, .void); return self.builder.constInt(0, .void);
} }
// Value-carrying LHS (`-> (T, !)`): on success the catch yields the // Value-carrying LHS (`-> (T..., !)`): on success the catch yields the
// value slot; on error it yields the handler body's value. The paths // value part (the lone value, or a value-tuple); on error it yields
// merge through a block-parameter (phi). Multi-value is deferred. // the handler body's value. The paths merge through a block-parameter
const fields = self.module.types.get(op_ty).tuple.fields; // (phi).
if (fields.len != 2) { const succ_ty = self.failableSuccessType(op_ty);
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 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 err_val = self.extractErrorSlot(result, op_ty, err_set);
const succ_val = self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 0, .base_type = op_ty } }, succ_ty); 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 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 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); 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 /// `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 /// form). On LHS success the result is its value part (the lone value, or a
/// is discarded and the result is `rhs` (a plain value of the success /// value-tuple); on failure the LHS error is discarded and the result is
/// type), so the whole expression is non-failable. Single-value /// `rhs` (a plain value of the success type), so the whole expression is
/// value-carrying LHS only. The CHAIN form (`... or try ...` / a failable /// non-failable. The CHAIN form (`... or try ...` / a failable RHS) needs
/// RHS) needs the fallback-target routing deferred from E1.4 — bail. /// the fallback-target routing deferred from E1.4 — bail.
fn lowerFailableOr(self: *Lowering, bop: *const ast.BinaryOp) Ref { fn lowerFailableOr(self: *Lowering, bop: *const ast.BinaryOp) Ref {
const span = bop.lhs.span; const span = bop.lhs.span;
@@ -15568,18 +15621,11 @@ pub const Lowering = struct {
} }
return self.builder.constInt(0, .void); return self.builder.constInt(0, .void);
} }
const fields = self.module.types.get(lhs_ty).tuple.fields; const succ_ty = self.failableSuccessType(lhs_ty);
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 result = self.lowerExpr(bop.lhs); 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 err_val = self.extractErrorSlot(result, lhs_ty, err_set);
const succ_val = self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 0, .base_type = lhs_ty } }, succ_ty); 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 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"); const fail_bb = self.freshBlock("or.fail");

View File

@@ -0,0 +1 @@
164

View File

@@ -0,0 +1 @@
multi-value result: 164