# PLAN-MULTIRET — bare-paren multi-value returns + named returns ## Why sx already has multi-value returns, but only in a verbose spelling: `-> Tuple(A, B)` / `-> Tuple(x: A, y: B)` types and `return .(a, b)` / `return .(x = a, y = b)` tuple-literal returns. Destructuring (`a, b := f()`), named/positional field access (`r.x` / `r.0`), and value-carrying failables (`Tuple(A, B) !E`) all work on top of the existing `.tuple` TypeId. The user wants the ergonomic, canonical surface: ```sx a :: () -> () { } // () ≡ void two :: () -> (i32, bool) { return 42, true; } // bare-paren type + bare comma return b :: (f1: i32, f2: i32) -> (sum: i32, good: bool) { // named returns are in-scope locals good = true; sum = f1 + f2; // implicit return: all named slots set } b2 :: (f1: i32, f2: i32) -> (sum: i32, good: bool) { return f1 + f2, f2 > 42; // bare comma return still works } read :: () -> (i32, bool, !) { ... } // error channel ALWAYS the last slot ``` Rules (from the user): - **`() -> ()` ≡ `() -> void`.** - **A multi-return signature is NOT a tuple — it just REUSES the tuple machinery.** `-> (i32, bool)` / `-> (x: i32, y: bool)` mean "this function returns multiple values", a DISTINCT thing from `-> Tuple(i32, bool)` (which returns one tuple value). The bare-paren form is valid ONLY as a function/closure RETURN signature — `x: (A, B)` (a variable/param/field annotation) stays REJECTED; `Tuple(…)` is the spelling for an actual tuple value type. - **Consumption — destructure OR single-bind (REVISED 2026-06-27).** A multi-return result may be DESTRUCTURED (`s, g := b2()`) OR bound to a single name and reached by field (`c := b2(); c.sum` / `c.0`). The earlier destructure-only rule (single-bind = error) was REVERSED by the user — single binding is allowed; the bound value behaves like a tuple of the value slots. - **Failable: the error stays SEPARATE.** For `-> (sum, good, !)`, a bound value (`c := f() catch …` / `try`) holds ONLY the value slots — the error rides the `!` channel and is NEVER part of `c` (no `c.err`). This falls out of the existing failable machinery (catch/try strip the error before binding). - **Failable: the error channel is always the LAST slot** (`(A, B, !)`). - **Bare comma return**: `return v1, v2;` maps positionally to the return slots — no `.(…)` tuple literal needed. - **Named returns are assignable locals.** With no explicit `return`, an implicit return at end-of-body synthesizes the result from the named locals. **A named return that is neither assigned on the path nor given a default is a COMPILE ERROR.** A named slot may carry a default (`(sum: i32 = 0, good: bool)`); a defaulted slot needn't be assigned. ## Representation (how "not a tuple, reuse machinery" is realized) — AS BUILT - A dedicated AST node **`ReturnTypeExpr`** (`field_types` + optional `field_names`, same shape as a tuple) is produced by the parser for a bare-paren result list with **≥2 value slots** (`(A, B)`, `(x: A, y: B)`, `(A, B, !)`). A single-value `(T, !)` stays a `tuple_type_expr` (a plain failable, `= -> T !`). An EMPTY `()` parses to the `void` type. - It resolves (type_resolver `internTupleLike`, shared with `tuple_type_expr`) to a reused `.tuple` TypeId — full ABI / failable / destructure / field-access machinery reuse. Its distinct MEANING lives in the AST node, not the TypeId. - Position gating: the node is valid only in a return slot. `resolveParamType` rejects a `ReturnTypeExpr` parameter annotation ("multi-return is return-only; use Tuple(…)"). Being a distinct node, its mere appearance in a value-type position is categorically an error (no flag to check) — exhaustive `switch`es over `node.data` were forced to add a `.return_type_expr` arm (coverage). - Consumption: destructure (`a, b := f()`) or single-bind + field access (`c := f(); c.sum`). No single source of truth needed at call sites — the result is just a tuple value. - SCOPE: multi-return on `name :: (...) -> (…) { }` function declarations first. Multi-return CLOSURE-TYPE values (`cb: Closure() -> (A, B)`) and lambda literals are a later phase. ## What already exists (re-use, do NOT rebuild) - `tuple_type_expr` → `.tuple` TypeId with optional `names` (type_resolver.zig `resolveCompound`). - Named + positional tuple field access `r.x` / `r.0` (expr.zig `lowerFieldAccessOnType`). - Destructuring `a, b := f()` (`DestructureDecl`, stmt.zig). - Value-carrying failable assembly `(T1, …, !)` (error.zig `lowerFailableSuccessReturn` / `emitTupleRet`) — error in the last slot. - `return .(a, b)` / `return .(x = a, y = b)` tuple-literal returns (stmt.zig `lowerReturn`). - Generic inference through a failable/tuple closure return (this session's parser `collectGenericNames` + generic.zig `extractTypeParam` tuple arms). ## Foundation already landed (uncommitted, suite-green) - **parser.zig** — `collectGenericNames` descends tuple/optional/function nodes (so `Closure() -> $R !` binds `$R`); the bare-paren result-list path builds a failable `tuple_type_expr` when it ends in `!` (`(A, B, !)` parses). - **generic.zig** — `extractTypeParam` / `matchTypeParam[Static]` handle the `(value, !)` tuple so `$R` infers from a closure ARG's failable return. ## Phases (each: implement → lock with an example → `zig build test` green) 0. **`() -> ()` = void (parser).** Isolated, unambiguous. An empty `()` in the paren type path resolves to `void`. Lock: `a :: () -> () { }`. 1. **Multi-return signatures `-> (A, B)` / `-> (x: A, y: B)` / `-> (A, B, !)` (parser + AST + resolution).** Add the `multi_return_type` AST node; the parser produces it for a bare-paren result list (return position). The return resolver lowers it to a `.tuple` TypeId and sets `Function.multi_return`; the general resolver rejects it (return-position only). Returns still use the existing `return .(…)` literal in this phase (bare comma is Phase 2). Consumption is destructuring `a, b := f()` (existing machinery). Lock: positional + named + failable multi-return examples, each destructured. 2. **Destructure-only enforcement + bare comma `return v1, v2` (parser + lowering).** (a) Reject using a multi-return call as a single value (`r := f()`, an arg, an operand) — read `Function.multi_return` at the binding/use site; only destructuring is allowed. (b) Extend the return statement to parse a comma-separated value list and lower it to the same multi-slot return the `.(…)` literal produces (error slot stays implicit for failables). Single-value `return v` unchanged. Lock: `-> (i64, bool) { return 7, true; }`, a failable variant, and a negative example (`r := f()` → diagnostic). 3. **Named-return locals + must-set rule (sema/lowering).** For a named return `-> (x: A, y: B)`, bind each name as an in-scope assignable local (alloca). On a path that reaches end-of-body with NO explicit `return`, synthesize the implicit return from the named locals. Diagnose loudly if any named slot is neither assigned on that path nor defaulted (no silent zero-fill). Explicit `return v1, v2` / `return .(…)` still override. Lock: the `b :: (...) -> (sum, good) { good = true; sum = ... }` example + a negative example (unset slot → diagnostic). 4. **Named-return defaults `(sum: i32 = 0, good: bool)`.** A slot with a default is exempt from the must-set rule; the default fills it at the implicit (or partial explicit) return. Lock: an example mixing a defaulted + a required slot. ## Open decisions (Decisions Log) - **D1 — multi-return is NOT a tuple; return-position-only.** *Chosen* (user directive). Realized via a distinct **`ReturnTypeExpr` AST node** (the user preferred a dedicated node over a `TupleTypeExpr.is_multi_return` flag — it makes "not a tuple" true at the AST level and makes position-gating categorical) that resolves to a reused `.tuple` TypeId. A new `.tuple`-like TypeInfo variant was rejected — it would ripple through every exhaustive type switch for no ABI benefit. **Destructure-only was REVERSED** (see Rules): single-binding a multi-return result is allowed (field access on the value slots); the failable error stays on the separate `!` channel. - **D2 (Phase 3) — storage for named-return locals.** Lean: an alloca per named slot bound in the function scope under its name; the implicit return reads them into the result tuple. Revisit if the must-set analysis wants SSA-style definite-assignment instead of an alloca + per-path check. - **D3 — multi-return closure-type values / lambda literals.** Deferred past the function-decl phases (needs a `ClosureInfo.multi_return` flag). Phases 0–4 cover named function declarations only. ## Validation (every phase) - `zig build && zig build test` green (full corpus). - New `examples//…` locked with snapshots; review the diff for `.ir` churn only where expected (the prelude type table is untouched by this stream, so churn should be minimal/none). - Adversarial review of each phase before it lands. ## Category for examples Multi-return is a core type/return feature — use the `types` block (`01xx`), next free numbers, unless a better fit emerges.