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:
35
examples/228-value-failable.sx
Normal file
35
examples/228-value-failable.sx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Value-carrying failable functions (ERR step E2.1a — the producer side of the
|
||||||
|
// error-channel tuple ABI). A `-> (T, !E)` function returns EITHER a value OR
|
||||||
|
// an error: `return v;` yields the success tuple `{v, 0}` (the compiler appends
|
||||||
|
// the no-error slot), and `raise error.X` yields `{undef, tag}` (value slot
|
||||||
|
// undefined, error slot = the tag). Today the result is consumed by
|
||||||
|
// destructuring `v, err := f()` (which extracts both slots); the value-carrying
|
||||||
|
// `try` / `catch` consumers land in E2.1b.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
E :: error { Bad, Empty }
|
||||||
|
|
||||||
|
parse :: (n: s32) -> (s32, !E) {
|
||||||
|
if n < 0 { raise error.Bad; }
|
||||||
|
if n == 0 { raise error.Empty; }
|
||||||
|
return n * 10; // success → {n*10, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
r : s32 = 0;
|
||||||
|
|
||||||
|
v1, e1 := parse(5); // success → v1 = 50, e1 = no error
|
||||||
|
if e1 == error.Bad { r = r + 1000; } // false
|
||||||
|
r = r + v1; // +50
|
||||||
|
|
||||||
|
v2, e2 := parse(-1); // Bad
|
||||||
|
if e2 == error.Bad { r = r + 7; } // true → +7
|
||||||
|
if e2 == error.Empty { r = r + 200; } // false
|
||||||
|
|
||||||
|
v3, e3 := parse(0); // Empty
|
||||||
|
if e3 == error.Empty { r = r + 3; } // true → +3
|
||||||
|
|
||||||
|
print("value-failable result: {}\n", r); // 50 + 7 + 3 = 60
|
||||||
|
return r;
|
||||||
|
}
|
||||||
@@ -1740,6 +1740,10 @@ pub const Lowering = struct {
|
|||||||
if (ret_ty == .void) {
|
if (ret_ty == .void) {
|
||||||
// Void function — just return void (the value expression was evaluated for side effects)
|
// Void function — just return void (the value expression was evaluated for side effects)
|
||||||
self.builder.retVoid();
|
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 {
|
} else {
|
||||||
// Coerce return value to match function return type (e.g., ?s32 → s32)
|
// Coerce return value to match function return type (e.g., ?s32 → s32)
|
||||||
const val_ty = self.builder.getRefType(ref);
|
const val_ty = self.builder.getRefType(ref);
|
||||||
@@ -15154,11 +15158,64 @@ pub const Lowering = struct {
|
|||||||
self.builder.ret(coerced, err_set);
|
self.builder.ret(coerced, err_set);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Value-carrying `-> (T..., !)`: needs undef value slots + the error
|
// Value-carrying `-> (T..., !)`: the error path leaves the value
|
||||||
// slot, assembled per the error-channel tuple ABI (ERR E2.1/E2.2).
|
// slots undefined and carries the tag in the error slot (ERR E2.1).
|
||||||
if (self.diagnostics) |diags| {
|
self.emitBlockDefers(self.func_defer_base);
|
||||||
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", .{});
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
tests/expected/228-value-failable.exit
Normal file
1
tests/expected/228-value-failable.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
60
|
||||||
1
tests/expected/228-value-failable.txt
Normal file
1
tests/expected/228-value-failable.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
value-failable result: 60
|
||||||
Reference in New Issue
Block a user