From 17c19d5d30c66ea64b86e9af06729b272078c9f2 Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 31 May 2026 21:42:51 +0300 Subject: [PATCH] =?UTF-8?q?ERR/E2.1a:=20value-carrying=20failable=20produc?= =?UTF-8?q?er=20(return=20value=20+=20raise=20=E2=86=92=20tuple=20ABI)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- examples/228-value-failable.sx | 35 ++++++++++++++ src/ir/lower.zig | 65 ++++++++++++++++++++++++-- tests/expected/228-value-failable.exit | 1 + tests/expected/228-value-failable.txt | 1 + 4 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 examples/228-value-failable.sx create mode 100644 tests/expected/228-value-failable.exit create mode 100644 tests/expected/228-value-failable.txt diff --git a/examples/228-value-failable.sx b/examples/228-value-failable.sx new file mode 100644 index 0000000..f410fea --- /dev/null +++ b/examples/228-value-failable.sx @@ -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; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 4916658..11d6663 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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); } } diff --git a/tests/expected/228-value-failable.exit b/tests/expected/228-value-failable.exit new file mode 100644 index 0000000..abdfb05 --- /dev/null +++ b/tests/expected/228-value-failable.exit @@ -0,0 +1 @@ +60 diff --git a/tests/expected/228-value-failable.txt b/tests/expected/228-value-failable.txt new file mode 100644 index 0000000..5db3777 --- /dev/null +++ b/tests/expected/228-value-failable.txt @@ -0,0 +1 @@ +value-failable result: 60