feat(arrays): array_slice() on an associative receiver - #1042
Conversation
|
5bdc728 to
3aa9127
Compare
Every associative receiver was refused at compile time -- string-keyed and
integer-keyed alike, with and without `preserve_keys` -- behind two different
messages:
array_slice(["x" => 1, "y" => 2, "z" => 3], 1, 2, true)
unsupported EIR backend feature: array_slice preserve_keys for PHP type
AssocArray { key: Str, value: Int }
array_slice(["x" => 1, "y" => 2, "z" => 3], 1, 2)
unsupported EIR backend feature: array_slice for PHP type
AssocArray { key: Str, value: Int }
The indexed receiver was fully supported in BOTH modes, so the gap was the
receiver being a hash, not the element type.
## The helper
`$offset` and `$length` count POSITIONS in insertion order, not keys, which a hash
cannot answer without a walk. `__rt_hash_slice` therefore walks the source through
`__rt_hash_iter_next` and counts, exactly as `__rt_hash_flip` and
`__rt_hash_clone_shallow` do. The window NORMALIZATION is the existing
`emit_slice_bounds` prologue, unchanged: it reads the source length from the first
header word, and a hash stores its entry count there just as an indexed array
stores its length.
One helper covers both `preserve_keys` modes, because they differ by a single
per-entry decision and nothing else. `preserve_keys` is NOT "keep all keys" versus
"drop all keys" -- php-src only ever renumbers INTEGER keys, and a string key
survives either way:
array_slice(["x"=>1,"y"=>2,"z"=>3], 1, 2) ["y"=>2, "z"=>3]
array_slice(["x"=>1,"y"=>2,"z"=>3], 1, 2, true) ["y"=>2, "z"=>3]
array_slice([5=>1, 9=>2, 12=>3], 1, 2) [0=>2, 1=>3]
array_slice([5=>1, 9=>2, 12=>3], 1, 2, true) [9=>2, 12=>3]
A mixed-key source shows the rule directly: `[5=>"a", "k"=>"b", 9=>"c"]` slices to
`[0=>"a", "k"=>"b", 1=>"c"]` -- only the integer entries move.
The result's key type is therefore the SOURCE's key type in both modes, which is
already what the checker records for an associative source ("narrowing a hash
preserves its keys"). No checker change was needed.
OWNERSHIP mirrors `__rt_hash_clone_shallow`, because the window holds the same
values the source does: string keys and refcounted values are retained, string
values are re-persisted through `__rt_str_persist`, scalars are copied inline, and
a renumbered integer key needs no ownership at all.
## Verified
Twenty-four shapes against host PHP 8.5.10, all byte-identical: string keys,
integer keys, mixed keys, both modes; an omitted length, a negative offset, a
negative length, an offset past the end, an offset before the start, a zero length,
an over-long length, an empty source; string, float, bool and nested-array values;
the source left untouched; and the indexed receiver still taking its own path in
both modes.
The routing was confirmed load-bearing by disabling it: both tests fail with the
original `unsupported EIR backend feature: array_slice for PHP type AssocArray` .
`--gc-stats` reports `allocs=5600 frees=5600` over 200 iterations of five slice
shapes, and a `leak summary: clean` heap-debug test pins the retains and releases
the copy performs.
`cargo test --test codegen_tests -- codegen::arrays` passes in full (568), as do
the 1681 lib tests and 1534 error tests.
## iOS
`test_hash_slice_is_emitted_for_every_supported_target` emits the runtime for all
five supported targets -- `macos-aarch64`, `ios-arm64`, `ios-sim-arm64`,
`linux-aarch64`, `linux-x86_64` -- and asserts each body reaches
`__rt_hash_iter_next`, `__rt_hash_new` and `__rt_hash_insert_owned`. Those three
are what make it a hash slice rather than a clone: the iterator is what gives
positions their meaning, and the allocator is what makes the result a table of its
own. The executable shards cover three of the five; the two iOS targets are covered
by emission tests like this, which is what the policy asks for.
## Docs and example
`docs/php/arrays.md` states the real rule on the `array_slice()` row, with both
integer-key outcomes spelled out. `docs/internals/the-runtime.md` gains the
`__rt_hash_slice` row next to the splice helpers. `examples/assoc-arrays/main.php`
slices a roster by position and shows a mixed-key array where only the integer
entries renumber; the whole example is byte-identical to host PHP 8.5.10.
## Scope
`array_chunk()` on an associative receiver is the other half of #683 and is NOT in
this commit. The two do not share an implementation: `array_chunk`'s
`preserve_keys = false` drops string keys too and restarts numbering inside each
chunk, so the per-entry rule is different, and the result is a nested container
rather than a window. Filed separately so neither review has to carry the other.
Part of #683
Claude-Session: https://claude.ai/code/session_01KSAAWPyNBq6dP2b5puN3wr
3aa9127 to
caa41a1
Compare
Grok review (READ-ONLY)Verdict: sound with nits — merge yes, with follow-ups. Closes the AOT gap for What this PR changesRouting in
What looks fine
Follow-ups opened
|
Keep associative array_slice() from #1042 and take main's array_push/unset-by-ref updates in the shared example and docs. Co-authored-by: Vincenzo Petrucci <nahime0@users.noreply.github.com>
Part of #683 — the
array_slice()half.array_chunk()is filed separately; see Scope below.Every associative receiver was refused at compile time — string-keyed and integer-keyed alike, with and without
preserve_keys— behind two different messages:The indexed receiver was fully supported in both modes, so the gap was the receiver being a hash, not the element type.
The helper
$offsetand$lengthcount positions in insertion order, not keys, which a hash cannot answer without a walk.__rt_hash_slicetherefore walks the source through__rt_hash_iter_nextand counts, exactly as__rt_hash_flipand__rt_hash_clone_shallowdo. The window normalization is the existingemit_slice_boundsprologue, unchanged: it reads the source length from the first header word, and a hash stores its entry count there just as an indexed array stores its length.One helper covers both
preserve_keysmodes, because they differ by a single per-entry decision and nothing else.preserve_keysis not "keep all keys" versus "drop all keys" — php-src only ever renumbers integer keys, and a string key survives either way:array_slice(["x"=>1,"y"=>2,"z"=>3], 1, 2)["y"=>2, "z"=>3]array_slice(["x"=>1,"y"=>2,"z"=>3], 1, 2, true)["y"=>2, "z"=>3]array_slice([5=>1, 9=>2, 12=>3], 1, 2)[0=>2, 1=>3]array_slice([5=>1, 9=>2, 12=>3], 1, 2, true)[9=>2, 12=>3]A mixed-key source shows the rule directly:
[5=>"a", "k"=>"b", 9=>"c"]slices to[0=>"a", "k"=>"b", 1=>"c"]— only the integer entries move.The result's key type is therefore the source's key type in both modes, which is already what the checker records for an associative source ("narrowing a hash preserves its keys"). No checker change was needed.
Ownership mirrors
__rt_hash_clone_shallow, because the window holds the same values the source does: string keys and refcounted values are retained, string values are re-persisted through__rt_str_persist, scalars are copied inline, and a renumbered integer key needs no ownership at all.Verified
Twenty-four shapes against host PHP 8.5.10, all byte-identical: string keys, integer keys, mixed keys, both modes; an omitted length, a negative offset, a negative length, an offset past the end, an offset before the start, a zero length, an over-long length, an empty source; string, float, bool and nested-array values; the source left untouched; and the indexed receiver still taking its own path in both modes.
The routing was confirmed load-bearing by disabling it: both tests fail with the original
unsupported EIR backend feature: array_slice for PHP type AssocArray.--gc-statsreportsallocs=5600 frees=5600over 200 iterations of five slice shapes, and aleak summary: cleanheap-debug test pins the retains and releases the copy performs.cargo test --test codegen_tests -- codegen::arrayspasses in full (568), as do the 1681 lib tests and 1534 error tests.iOS
test_hash_slice_is_emitted_for_every_supported_targetemits the runtime for all five supported targets —macos-aarch64,ios-arm64,ios-sim-arm64,linux-aarch64,linux-x86_64— and asserts each body reaches__rt_hash_iter_next,__rt_hash_newand__rt_hash_insert_owned. Those three are what make it a hash slice rather than a clone: the iterator is what gives positions their meaning, and the allocator is what makes the result a table of its own. The executable shards cover three of the five; the two iOS targets are covered by emission tests like this, which is what the policy asks for.Docs and example
docs/php/arrays.mdstates the real rule on thearray_slice()row, with both integer-key outcomes spelled out.docs/internals/the-runtime.mdgains the__rt_hash_slicerow next to the splice helpers.examples/assoc-arrays/main.phpslices a roster by position and shows a mixed-key array where only the integer entries renumber; the whole example is byte-identical to host PHP 8.5.10.Scope
array_chunk()on an associative receiver is the other half of #683 and is not in this PR. The two do not share an implementation:array_chunk'spreserve_keys = falsedrops string keys too and restarts numbering inside each chunk, so the per-entry rule is different, and the result is a nested container rather than a window. Splitting it keeps each review to one helper rather than making this one carry a second ~350-line dual-architecture emitter it shares nothing with.🤖 Generated with Claude Code
https://claude.ai/code/session_01KSAAWPyNBq6dP2b5puN3wr