Files
sx/current/CHECKPOINT-MULTIRET.md
agra b322dcfe61 fix: type-safe stores + Any unbox/eq; finish multi-return deferrals
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).
2026-06-27 17:28:27 +03:00

10 KiB
Raw Blame History

CHECKPOINT-MULTIRET — bare-paren multi-value + named returns

Plan: current/PLAN-MULTIRET.md. Branch: feat/multi-return.

Last completed step

Phases 03 implemented (final suite + snapshot capture in progress). Examples renumbered to the free types block 02020206 (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 a tuple_type_expr tagged is_multi_return; a single-value (T, !) is a plain failable (= -> T !). Return resolver yields the reused tuple TypeId; resolveParamType rejects 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 same tuple_literal the .(…) form produces. Single positional return v unchanged. (used throughout 02030205)
  • Phase 3 — NAMED-return slots are in-scope assignable LOCALS: bound as zero-init allocas (bindNamedReturnSlots), the implicit return is synthesized from them (synthesizeNamedReturn → reuses lowerReturn), 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 = b bare 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_return flag + Lowering.named_return_names state (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 ReturnTypeExpr AST node (user preferred it over the TupleTypeExpr.is_multi_return flag). Resolves to a reused .tuple TypeId via the shared internTupleLike helper. Forced .return_type_expr arms onto the exhaustive node.data switches (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.sum works (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). The callIsMultiReturn reject 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/raise count as divergence; if needs both branches; push bodies count; match needs an else arm + all arms.
  • #2 (wrong-type default → segfault)bindNamedReturnSlots type-checks a default via the coercion classifier (.none ⇒ diagnostic). (NOTE: the same silent bitcast/segfault exists for ANY annotated assignment x: 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 from lowerReturn): 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): ReturnTypeExpr silently accepted in non-return positionsDONE (2026-06-27): generic-type-arg position now rejected (rejectMultiReturnValueType at both instantiateGenericStruct arg-resolution sites, generic.zig). Param / field / variable already rejected. Type-alias T :: (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 (x: i32 = "hi") segfaultsRESOLVED as issue 0197 (2026-06-27): width-mismatch guard (checkAssignable / noneReinterpretIsUnsafe, coerce.zig) at every annotated-slot store site; the named-return-default guard now shares it. Locked by examples/diagnostics/1205 + 1206.
  • Multi-return CLOSURE-TYPE values / lambda literals deferred (D3).RESOLVED (2026-06-27): they ALREADY WORK via the reused tuple machinery. A Closure() -> (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. NO ClosureInfo.multi_return marker 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 RESOLVEDmonomorphizeFunction now calls bindNamedReturnSlots; 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 Any → T unbox unchecked (segfault / silent garbage)RESOLVED (2026-06-27): implicit Any → T is now a compile error (coerceMode .unbox_any arm, mode == .implicit); xx + match dispatch unaffected. Locked by examples/diagnostics/1207.
  • issue 0199: Any == <concrete> aborts the LLVM verifierRESOLVED (2026-06-27): the Any-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): monomorphizeFunction now calls bindNamedReturnSlots (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-padded sizeOf → LLVM-accurate typeSizeBytes); cascade suppression via externalErrorsExist (guard tallies its own diagnostics).
    • issue 0198 RESOLVED — implicit Any → T unbox 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.)
  • 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.