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

@@ -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");