Type-checking gaps (segfault/corruption → compile errors): - 0197: reject a store into an annotated slot whose value has no modeled coercion AND a different byte width (a 16-byte string into a 4-byte i32 overran the slot and segfaulted). New checkAssignable / noneReinterpretIsUnsafe (coerce.zig, width via the LLVM-accurate typeSizeBytes) wired into every store site: var/const-decl, single + multi assignment (identifier/field/index/ element/deref), named-return defaults. Same-width reinterpretations (*T→[*]T, i64→isize, fn-ref) and explicit xx/cast stay allowed; cascades suppressed via externalErrorsExist. Examples 1205, 1206. - 0198: an implicit `Any → T` unbox is now a compile error (it blindly reinterpreted the boxed payload — silent garbage for a wrong scalar, a segfault for an aggregate). xx and compiler-generated match/pack unboxes are unaffected. Example 1207. - 0199: `Any == <concrete>` (one operand Any) aborted the LLVM verifier — the comparison arm now fires when either operand is Any, boxing the concrete side first. Example 0654. Multi-return deferrals (PLAN-MULTIRET #6 + named-order + D3 + generic): - Reorder named return elements by name instead of requiring slot order; error on unknown/duplicate/missing (value-only AND full-failable-tuple forms). Examples 0210, 0214. - Reject a bare-paren (A, B) multi-return signature in generic-arg position (return-position-only). Example 0215. - Multi-return closure types / lambda literals work via the reused tuple machinery (destructure, single-bind+field, lambda arg). Example 0216. - Generic multi-return: positional works (0217); 0200: the named-slot implicit-return form now works for generic free fns + struct methods — monomorphizeFunction now calls bindNamedReturnSlots. Example 0218. readme.md documents the annotated-store coercion rule; CHECKPOINT-MULTIRET.md updated. Full corpus green (850/0).
10 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):— DONE (2026-06-27): generic-type-arg position now rejected (ReturnTypeExprsilently accepted in non-return positionsrejectMultiReturnValueTypeat bothinstantiateGenericStructarg-resolution sites, generic.zig). Param / field / variable already rejected. Type-aliasT :: (A,B)is value-parsed → already rejected. Closure-RETURN(A,B)is a legitimate return position → see D3 below (works as a multi-return closure). Lock: 0215 (negative generic-arg).Reordering named return elements by name (vs requiring slot order)— DONE (2026-06-27):reorderNamedReturn(stmt.zig) permutes a fully-named multi-return list to slot order by name (value-only AND full-failable-tuple forms); errors on unknown / duplicate / missing-slot names; positional & mixed lists pass through unchanged.validateMultiReturn's old slot-order check was removed. Adversarial review caught a silent mis-permute in the full-failable- tuple named form (now reordered/validated, not positionally dropped). Lock: 0210 (positive reorder, incl. failable) + 0214 (negative: unknown / duplicate).PRE-EXISTING: annotated-assignment type mismatch (— RESOLVED as issue 0197 (2026-06-27): width-mismatch guard (x: i32 = "hi") segfaultscheckAssignable/noneReinterpretIsUnsafe, coerce.zig) at every annotated-slot store site; the named-return-default guard now shares it. Locked byexamples/diagnostics/1205+1206.Multi-return CLOSURE-TYPE values / lambda literals deferred (D3).— RESOLVED (2026-06-27): they ALREADY WORK via the reused tuple machinery. AClosure() -> (A, B)value's call result destructures (a, b := cb()), single-binds + field-accesses (c := cb(); c.0), and a() => { return v1, v2; }lambda literal satisfies a multi-return closure param — verified identical to the function-decl surface. NOClosureInfo.multi_returnmarker needed (the destructure-only rule was reversed, so there's nothing extra to enforce). Lock: 0216.- Generic multi-return (Task 2d): DONE. POSITIONAL works —
(a: $T, b: $U) -> (T, U)(inferred) and($T: Type, …) -> (T, U)(explicit); lock 0217. NAMED-slot implicit-return form now works too (issue 0200 RESOLVED —monomorphizeFunctionnow callsbindNamedReturnSlots; covers free fns + generic struct methods, defaults, failable); lock 0218. - Docs: readme.md / specs.md not yet updated for multi-return (docs-track rule).
Known issues
issue 0198: implicit— RESOLVED (2026-06-27): implicitAny → Tunbox unchecked (segfault / silent garbage)Any → Tis now a compile error (coerceMode.unbox_anyarm, mode == .implicit);xx+ match dispatch unaffected. Locked byexamples/diagnostics/1207.issue 0199:— RESOLVED (2026-06-27): theAny == <concrete>aborts the LLVM verifierAny-shaped==/!=arm (expr.zig) now fires when EITHER operand is.any, boxing the concrete side first. Lock 0654.issue 0200: NAMED generic multi-return implicit-return "produces no value"— RESOLVED (2026-06-27):monomorphizeFunctionnow callsbindNamedReturnSlots(it previously bound params but skipped named-return slots). Covers generic free fns + struct methods, defaults, failable. Lock 0218.
Log
- 2026-06-27 session (handover: issue 0197 → finish multi-return → Io Phase 3):
- issue 0197 RESOLVED — width-mismatch guard at every annotated-slot store
site (var/const-decl, single + multi assignment for identifier/field/index/
element/deref, named-return defaults). Examples 1205 + 1206. Adversarial review
caught & fixed: a bare-fn-ref false-positive (size-discriminator via
typeSizeBytes, not the wrong fn-ref typing) and an aggregate-overrun false-negative (sx-paddedsizeOf→ LLVM-accuratetypeSizeBytes); cascade suppression viaexternalErrorsExist(guard tallies its own diagnostics). - issue 0198 RESOLVED — implicit
Any → Tunbox is now a compile error (reviewer-confirmed sound). Example 1207. issue 0199 FILED (Any==concrete LLVM-verify abort, loud, open). - multi-return Task 2 DONE (2a reorder 0210/0214; 2b reject in generic-arg 0215; 2c D3 closures already work 0216; 2d positional generic works 0217 + named-generic gap filed as 0200). Multi-return feature surface complete.
- REMAINING (next session): Task 3 Io-unification Phase 3 (the capture-typing blocker below + true cancellation — needs fresh context + both macOS & aarch64-linux validation per PLAN-IO-UNIFY.md). (0198/0199/0200 all resolved this session; no open multi-return/type-check issues remain.)
- issue 0197 RESOLVED — width-mismatch guard at every annotated-slot store
site (var/const-decl, single + multi assignment for identifier/field/index/
element/deref, named-return defaults). Examples 1205 + 1206. Adversarial review
caught & fixed: a bare-fn-ref false-positive (size-discriminator via
- 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.