A function may return multiple values via a bare-paren return signature: `-> (A, B)` / `-> (x: A, y: B)` / `-> (A, B, !)` (error always the last slot), and `-> ()` is `void`. This is DISTINCT from a `Tuple(…)` value — return-position only (a dedicated `ReturnTypeExpr` AST node resolving to a reused `.tuple` TypeId); a parameter / field / variable annotation `x: (A, B)` is rejected. A single-value `-> (T, !)` stays a plain failable (= `-> T !`). Returns use the bare comma form `return a, b` / `return x = a, y = b` (no `.( … )` literal). Consume by destructuring (`a, b := f()`) or single-bind + field access (`c := f(); c.sum`); a failable bound value holds only the value slots (the error stays on the `!` channel). Named return slots are in-scope assignable locals; with no explicit `return` the implicit return is synthesized from them. Path-sensitive definite-assignment enforces the must-set rule, and a slot may carry a default that exempts it. Validation rejects arity mismatches, out-of-slot-order named elements, a slot/parameter name collision, a comma list from a single-value function, and a multi-return signature used as a value type. Examples 0202-0213; readme + specs updated. issues/0197 files a pre-existing annotated-assignment type-check gap (`x: i32 = "hi"` segfaults) surfaced by the adversarial review.
6.2 KiB
CHECKPOINT-MULTIRET — bare-paren multi-value + named returns
Plan: current/PLAN-MULTIRET.md. Branch: feat/multi-return.
Last completed step
Phases 0–3 implemented (final suite + snapshot capture in progress). Examples
renumbered to the free types block 0202–0206 (0130/0131 already had duplicate
existing owners).
- Phase 0 — empty
()in the type path →void. (0202) - Phase 1 — multi-return SIGNATURES
(A, B)/(x: A, y: B)/(A, B, !)(≥2 value slots) parse to atuple_type_exprtaggedis_multi_return; a single-value(T, !)is a plain failable (=-> T !). Return resolver yields the reused tuple TypeId;resolveParamTyperejects a multi-return tuple (return-position-only). Consumed by destructuring. (0203, 0204) - Phase 2 — bare comma
return v1, v2(positional) /return x = v, y = w(named): the return parser builds the sametuple_literalthe.(…)form produces. Single positionalreturn vunchanged. (used throughout 0203–0205) - Phase 3 — NAMED-return slots are in-scope assignable LOCALS: bound as
zero-init allocas (
bindNamedReturnSlots), the implicit return is synthesized from them (synthesizeNamedReturn→ reuseslowerReturn), and the MUST-SET rule errors on an unset/undefaulted slot (bodyAssignsTo, path-insensitive MVP). Works with the failable error channel too. (0205 positive, 0206 negative)
Earlier foundation: parser collectGenericNames descends tuple/optional/function
nodes; generic.zig extractTypeParam handle the (value, !) tuple.
Current state (works, verified by probes)
() -> ()≡ void;-> (A, B)/-> (x: A, y: B)/-> (A, B, !)multi-return.return a, b/return x = a, y = bbare comma; named-return locals + implicit return + must-set diagnostic; failable named multi-return.(A, B)in a PARAM slot → loud diagnostic.- Representation:
TupleTypeExpr.is_multi_returnflag +Lowering.named_return_namesstate (reuses tuple ABI; no new TypeInfo variant; multi-return-ness derivable from the FnDecl AST).
Post-Phase-3 changes (this session)
- Representation refactored to a dedicated
ReturnTypeExprAST node (user preferred it over theTupleTypeExpr.is_multi_returnflag). Resolves to a reused.tupleTypeId via the sharedinternTupleLikehelper. Forced.return_type_exprarms onto the exhaustivenode.dataswitches (sema, semantic_diagnostics) — the coverage benefit. Param-position reject + the named-return-locals / must-set sites now key off.return_type_expr. - Destructure-only enforcement REVERSED (user): single-binding a multi-return
is ALLOWED.
c := f(); c.sumworks (the result is a tuple of the value slots). For a failable multi-return,c := f() catch …binds only the value slots — the error stays on the!channel (verified). ThecallIsMultiReturnreject was removed. Examples 0203/0204 updated to show single-bind + field access (output byte-identical, snapshots unchanged).
Phase 4 — named-return DEFAULTS (done, suite pending)
-> (sum: i32 = 0, good: bool): the parser parses = <expr> per slot into
ReturnTypeExpr.field_defaults; bindNamedReturnSlots seeds a defaulted slot
with its (lowered+coerced) default; a defaulted slot is EXEMPT from the must-set
rule. Also fixed hasFnBodyAfterArrow (the fn-def-vs-type-const lookahead) to
skip = + literal tokens in the return-type scan — otherwise a = made the decl
misread as a bodyless type-const ("expected ';'" at the body {). Lock: 0207.
Adversarial-review fixes (this session)
An adversarial review found 8 issues; fixed the soundness + silent-wrong ones:
- #1 (segfault on a conditionally-assigned non-scalar slot) → must-set is now
PATH-SENSITIVE definite-assignment (
definitelyAssigns, stmt.zig): a slot not assigned on every non-diverging path (and undefaulted) is a COMPILE ERROR, not a runtime garbage read.return/raisecount as divergence;ifneeds both branches;pushbodies count;matchneeds an else arm + all arms. - #2 (wrong-type default → segfault) →
bindNamedReturnSlotstype-checks a default via the coercion classifier (.none⇒ diagnostic). (NOTE: the same silent bitcast/segfault exists for ANY annotated assignmentx: i32 = "hi"— a broader PRE-EXISTING type-checking gap, not multi-return-specific.) - #3 / #8 (return arity garbage) + #4 (named elements ignored) →
validateMultiReturn(stmt.zig, called fromlowerReturn): rejects a bare value where ≥2 are required, wrong arity, a comma list from a single-value fn, and named elements out of slot order. (Reordering-by-name is a future nicety; for now a mismatch is a loud error, never silent-wrong.) - #5 (slot shadows param) → collision diagnostic in
bindNamedReturnSlots. - #7 (push/defer false must-set error) → subsumed by the DA rewrite (push bodies count; defer correctly does NOT, as it runs after the implicit return).
Known limitations / next
- #6 (design gap, NOT UB): a
ReturnTypeExpris still silently accepted in struct-field / var-annotation / generic-arg / closure-RETURN positions (resolves to a coherent tuple). Only the PARAM position is rejected. Rejecting the rest needs checks at several value-resolution sites; deferred (no soundness impact). - Reordering named return elements by name (vs requiring slot order) — future.
- PRE-EXISTING: annotated-assignment type mismatch (
x: i32 = "hi") segfaults — a general type-checking gap surfaced by the review; may warrant an issue. - Multi-return CLOSURE-TYPE values / lambda literals deferred (D3).
- Docs: readme.md / specs.md not yet updated for multi-return (docs-track rule).
Known issues
- (none yet)
Log
- Pivoted here from the Io-unification Phase 3 (true cancellation), which is
PAUSED at its blocker: capturing a failable closure into a nested closure loses
its failability (
worker() catch→ operand type 'unresolved'; repro.sx-tmp/pD.sx/pE.sx). That capture-typing gap is unrelated to multi-return and waits for a later session. The Io-Phase-3 stdlib edits (core/sched/io + example 1825) were REVERTED to keep the tree green; the multi-return-relevant compiler changes were kept. - Foundation landed + suite green; plan + checkpoint written.