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:
168
src/ir/lower.zig
168
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");
|
||||
|
||||
Reference in New Issue
Block a user