fix(types,ir): loop-grown arrays widen to mixed before the body is processed (#452)#475
Conversation
…ocessed Fixes illegalstudio#452. Both the type checker and EIR lowering process loop bodies in a single pass, so with $vals[] = 1; $vals[] = 2.0; in a loop the earlier push site was typed/lowered against the pre-promotion element type (array<never>/ array<int>) and emitted a raw scalar push. On iterations >= 2 the back edge brings the promoted array<mixed> around, so the raw push wrote an unboxed scalar into boxed-cell storage; reading that element dereferenced the scalar as a Mixed cell pointer (SIGSEGV), or silently corrupted values and leaked in the string/float flavour. A shared prescan (types::checker::loop_widening) collects the body's $name[] = value pushes recursively and reports locals whose element type joins to mixed. The checker widens those locals in the TypeEnv before checking each loop body; EIR lowering widens its local_types and materializes the promotion once before the loop (in-place ArrayToMixed + storeback), so every push site boxes its value. Same-type pushes and never->T growth keep their current lowering (no action unless the join is mixed). The foreach prescan binds the loop value/key variables to their real element types so a push of the loop variable does not over-widen. Regression tests in tests/codegen/array_basics.rs (element read, foreach sum, int+string flavour, float-first ordering). (cherry picked from commit c3b449b)
c3b449b to
c7831bd
Compare
…d defaults CI caught a false positive: the prescan treated any pushed variable unknown at loop entry as Mixed evidence, spuriously widening same-typed rebuild loops. The compiler-synthesized MultipleIterator::detachIterator body rebuilds an array<Iterator> through a loop-defined variable ($candidate = $this->iterators[$i]; $newIterators[] = $candidate;), and the widened array<mixed> then failed the typed-property storeback — every program calling detachIterator stopped compiling. Two layers: - loop_widening: a pushed variable now joins its loop-entry type with self-evident literal assignments found inside the body ($x = 1; $a[] = $x still widens); a variable whose only in-loop sources are non-literal yields no evidence and is excluded from the join (poison entries guard partial evidence). Unknown never defaults to Mixed. - ir_lower lookup closure: returned Some(Mixed) for undeclared names via the local_type fallback, so the checker skipped but ir_lower still widened; it now returns None for names without a declared slot, mirroring the checker's env.get. Regression tests: the MultipleIterator detach shape compiles and runs, and the literal-through-variable heterogeneous push still widens (array_basics.rs). Full sweep green including the spl suite (1803 codegen + 993 error tests).
|
CI fix pushed ( What was wrong: the widening prescan treated any pushed variable unknown at loop entry as The fix: unknown never defaults to Regression tests added for both directions (the detach shape compiles and runs; the literal-through-variable heterogeneous push still widens). Full sweep green including the spl suite this time (1803 codegen + 993 error tests). |
Summary
Fixes #452. Growing an array inside a loop across heterogeneous push sites (
$vals[] = 1; $vals[] = 2.0;) corrupted the heap: reading the affected element produced garbage or a SIGSEGV, and the string/float flavour silently corrupted values and leaked.Root cause
Both the type checker and EIR lowering process loop bodies in a single pass, so each push site is typed and lowered against the types in force the first time through:
$vals[] = 1) is lowered againstarray<never>→ a raw scalararray_push;$vals[] = 2.0) widens the element type and emits thearray_to_mixedpromotion.The loop back edge brings the promoted
array<mixed>around, so from iteration 2 the stale-typed site A writes an unboxed i64 into boxed-cell storage. Any value-read of that element dereferences the raw scalar as a Mixed cell pointer → SIGSEGV (a raw string payload happens to survive reads, which is why the string flavour corrupted silently instead). Diagnosed via a deterministic bisection: the minimal repro is 2 iterations +echo $vals[2]— noforeachneeded.Fix
A shared prescan (
src/types/checker/loop_widening.rs) walks a loop body's statements recursively, collects its$name[] = valuepush sites, and reports the locals whose indexed-array element type joins tomixedacross all sites (neveradopts the other side, equal types stay, anything else widens). Then:array<mixed>in theTypeEnvbefore checking each loop body (while/do-while/for/foreach; forforeachafter the value/key bindings so a push of the loop variable joins with its real element type);local_typesthe same way and materializes the promotion once before the loop (in-placeArrayToMixed+ storeback), so every push site inside the body boxes its value.Same-type pushes and
never → Tgrowth are untouched: on those paths the generated IR is byte-identical tomain(verified) — the widening only fires on the representation-changing join tomixed. As a bonus, loops that previously re-promoted per iteration now hoist a single conversion out of the loop.Verification
tests/codegen/array_basics.rs: element read, foreach sum, int+string flavour, float-first ordering, push underif,while-loop variant (the walker's recursion branches).while/do-while/nested loops, push underif/try, foreach value-var push) all match PHP with a clean--heap-debug; iterate-and-push snapshot semantics, conditionalbreakbefore the promoting site, COW copies between iterations, andcontinueskipping a site all verified against PHP.Found during review (pre-existing, not this change)
forupdate clause — filed separately with a repro.