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

154 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 positions~~
**DONE** (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"`) segfaults~~
**RESOLVED** 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 RESOLVED**
`monomorphizeFunction` 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 verifier~~ — **RESOLVED**
(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.