Skip to content

fix(ir): write nested array assignments through the parent cell (#529)#553

Merged
nahime0 merged 4 commits into
illegalstudio:mainfrom
mirchaemanuel:fix/529-nested-mixed-array-write
Jul 18, 2026
Merged

fix(ir): write nested array assignments through the parent cell (#529)#553
nahime0 merged 4 commits into
illegalstudio:mainfrom
mirchaemanuel:fix/529-nested-mixed-array-write

Conversation

@mirchaemanuel

@mirchaemanuel mirchaemanuel commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

fix(ir): write nested array assignments through the parent cell (#529)

Fixes #529.

Symptom

$a = [['x', 'y0'], ['x', 'y1'], ['x', 'y2'], 7];
$a[2][1] = 'patched';
echo $a[2][1]; // prints y2 — the write is silently lost, +2 leaked blocks

The nested write into an Array(Mixed) element — the only nested-write shape the checker accepts for plain arrays — was silently dropped, with a small heap leak per write.

Root cause (two stacked defects)

StmtKind::NestedArrayAssign lowered the full target as a read and then replaced the resulting Mixed cell in place (two-operand Op::RuntimeCall → the mixed-cell replacement path):

  1. Lost write: __rt_mixed_array_get returns a detached fresh box whenever the slot storage is not already a boxed Mixed cell (string/int/float slots of a concrete homogeneous inner array — mixed_array_get.rs boxes those through __rt_mixed_from_value). The in-place cell replacement then mutated a temporary that nothing stores back: the write was lost and the replacement payload leaked (the 2 leaked blocks: fresh cell + persisted string).
  2. Leak on the "working" shape: when the inner slot was a boxed Mixed cell (heterogeneous inner array), the get returns the stored cell retained (__rt_incref); the write landed, but the retained cell was never released — one block leaked per write.

Fix

Split off the innermost key in lower_nested_array_assign instead of reading the full target: the parent chain is lowered as before, and the write goes through the three-operand Op::RuntimeCall__rt_mixed_array_set for Mixed parents, offsetSet dispatch for ArrayAccess objects. That helper mutates the container aliased by the parent cell and handles every slot representation (including COW split and representation conversion, publishing the new pointer back into the parent cell). Owned intermediate boxes of deeper chains are released after the write; string key/value operands follow the same release_persisted_string_operand rules as the existing $mixed[$k] = $v path.

For ArrayAccess object parents this also means the write now goes through offsetSet (PHP semantics) instead of a cell replacement on the offsetGet result.

Tests

tests/codegen/arrays/nested_mixed_write.rs, 7 regression tests (all failing before the fix, all heap-clean after):

  • string-slot inner array (the issue repro), int-slot inner array
  • boxed-cell inner array (write landed before but leaked)
  • associative inner container (overwrite + brand-new key)
  • compound nested assignment ($a[0][1] .= '!')
  • three-level chain through boxed intermediates
  • nested write surviving a function-return boundary

Residual limitation (pre-existing, documented in the test header)

A 3+ level chain whose intermediate level is a concrete homogeneous array (raw-pointer slots, e.g. [[['x', 1]], 7]) still loses the write: the intermediate read boxes a fresh retained cell, the retain makes the leaf look shared, and the write path's __rt_array_to_mixed COW-splits it. Fixing that shape needs a fetch-for-write read helper that promotes intermediate slots to boxed cells in place — happy to file it as a follow-up issue.

QA

  • Issue repro now prints patched with HEAP DEBUG: leak summary: clean.
  • Targeted suites green locally (macos-aarch64): arrays 328, foreach 118, spl 247, optimizer 254, nested 140, json 444, zval 72, mixed 252, ir_backend_smoke_test 255 — 0 failures.

https://claude.ai/code/session_01QyzntRke1Cgxgq1ueuz4X9

…galstudio#529)

Nested assignments ($a[$i][$j] = ...) lowered the FULL target as a read
and replaced the resulting Mixed cell in place. __rt_mixed_array_get
returns a detached fresh box whenever the slot storage is not already a
boxed Mixed cell (string/int/float slots of a concrete inner array), so
the replacement mutated a temporary: the write was silently lost and the
replacement payload leaked. When the slot WAS a boxed cell the write
landed, but the retained cell returned by the read was never released.

Lower the statement by splitting off the innermost key instead: the
parent chain is read as before, and the write goes through the
three-operand RuntimeCall (__rt_mixed_array_set for Mixed parents,
offsetSet for ArrayAccess objects), which mutates the container aliased
by the parent cell for every slot representation. Owned intermediate
boxes of deeper chains are released after the write.

Claude-Session: https://claude.ai/code/session_01QyzntRke1Cgxgq1ueuz4X9

nahime0 commented Jul 17, 2026

Copy link
Copy Markdown
Member

Follow-up #555 now tracks the missing-intermediate case: a write such as $a[7][1] = 'patched' needs fetch-for-write/autovivification so the newly created inner container is published back into the outer slot.

That is distinct from #529/#553's existing-parent fix, so it does not need to broaden this PR. The issue also records the related 3+ level concrete-homogeneous-intermediate limitation documented here, since both likely need the same in-place promotion/fetch-for-write infrastructure.

@nahime0
nahime0 merged commit c5038d2 into illegalstudio:main Jul 18, 2026
113 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:eir Touches EIR definitions, lowering, validation, or passes. size:s Small pull request. type:fix Corrects broken or incompatible behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nested write into an Array(Mixed) element ($a[$i][$j] = ...) is silently lost: the parent keeps the stale inner array

2 participants