ERR/E2.1a: value-carrying failable producer (return value + raise → tuple ABI)

The producer side of the error-channel tuple ABI for value-carrying `-> (T, !)`
functions. A failable that returns a value OR an error now lowers correctly;
the result is consumed via destructure (`v, err := f()`). Single-value
`-> (T, !)`; multi-value `-> (T1, T2, !)` and the value-carrying try/catch
consumers (E2.1b) follow.

- lowerReturn: a value-carrying failable's `return v;` assembles the success
  tuple `{v, 0}` (compiler appends the no-error slot) via lowerFailableSuccessReturn
  (tuple_init). Forwarding a full failable tuple (`return other_failable()` /
  explicit `return (v, e)`) returns as-is. Multi-value returns bail loudly (E2).
- lowerRaise: the value-carrying branch (previously a loud bail) now builds
  `{undef value slots..., tag}` (constUndef per value slot + the error tag) and
  returns it — any arity.
- helpers: buildFailableTuple (tuple_init from value refs + tag) + emitTupleRet
  (return honoring inline-comptime targets).

Value-carrying `try` / `catch` still bail (E2.1b). Tests:
examples/228-value-failable.sx (return value + both raises, consumed by
destructure; exit 60). Gates: zig build, zig build test, 266/266 examples.
This commit is contained in:
agra
2026-05-31 21:42:51 +03:00
parent 0bbff9d7fb
commit 17c19d5d30
4 changed files with 98 additions and 4 deletions

View File

@@ -1740,6 +1740,10 @@ pub const Lowering = struct {
if (ret_ty == .void) {
// Void function — just return void (the value expression was evaluated for side effects)
self.builder.retVoid();
} else if (!ret_ty.isBuiltin() and self.module.types.get(ret_ty) == .tuple and self.errorChannelOf(ret_ty) != null) {
// Value-carrying failable `-> (T..., !)`: the user returns the
// value part; the compiler appends the success error slot (0).
self.lowerFailableSuccessReturn(ref, ret_ty, rs.value.?.span);
} else {
// Coerce return value to match function return type (e.g., ?s32 → s32)
const val_ty = self.builder.getRefType(ref);
@@ -15154,11 +15158,64 @@ pub const Lowering = struct {
self.builder.ret(coerced, err_set);
}
} else {
// Value-carrying `-> (T..., !)`: needs undef value slots + the error
// slot, assembled per the error-channel tuple ABI (ERR E2.1/E2.2).
if (self.diagnostics) |diags| {
diags.addFmt(.err, span, "`raise` in a value-carrying failable function (`-> (T..., !)`) is not yet lowered — pending the error-channel tuple ABI (ERR E2); use a `-> !` / `-> !Named` signature for now", .{});
// Value-carrying `-> (T..., !)`: the error path leaves the value
// slots undefined and carries the tag in the error slot (ERR E2.1).
self.emitBlockDefers(self.func_defer_base);
const fields = self.module.types.get(ret_ty).tuple.fields;
var slots = std.ArrayList(Ref).empty;
defer slots.deinit(self.alloc);
for (fields[0 .. fields.len - 1]) |vty| {
slots.append(self.alloc, self.builder.constUndef(vty)) catch unreachable;
}
const tag_ty = self.builder.getRefType(tag_ref);
const coerced_tag = if (tag_ty != err_set) self.coerceToType(tag_ref, tag_ty, err_set) else tag_ref;
const tup = self.buildFailableTuple(ret_ty, slots.items, coerced_tag);
self.emitTupleRet(ret_ty, tup);
}
}
/// 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.
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];
const val_ty = self.builder.getRefType(ref);
if (val_ty == ret_ty) {
// The expression already IS the full failable tuple (forwarding).
self.emitTupleRet(ret_ty, ref);
return;
}
if (fields.len == 2) {
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", .{});
}
}
/// Build a failable return tuple `{value_refs..., tag}` typed `ret_ty`.
fn buildFailableTuple(self: *Lowering, ret_ty: TypeId, value_refs: []const Ref, tag: Ref) Ref {
var fields = std.ArrayList(Ref).empty;
defer fields.deinit(self.alloc);
fields.appendSlice(self.alloc, value_refs) catch unreachable;
fields.append(self.alloc, tag) catch unreachable;
return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, fields.items) catch unreachable } }, ret_ty);
}
/// 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 {
if (self.inline_return_target) |iri| {
self.builder.store(iri.slot, tup);
self.builder.br(iri.done_bb, &.{});
} else {
self.builder.ret(tup, ret_ty);
}
}