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).
154 lines
10 KiB
Markdown
154 lines
10 KiB
Markdown
# 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 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 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` → 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.
|