Skip to content

fix(arrays): keep sparse integer keys from zero-filling packed arrays - #2263

Open
nahime0 wants to merge 4 commits into
mainfrom
fix/task-9fc655fe
Open

nahime0 wants to merge 4 commits into
mainfrom
fix/task-9fc655fe

Conversation

@nahime0

@nahime0 nahime0 commented Nov 6, 2026

Copy link
Copy Markdown
Member

Summary

An integer key that skips a slot (k > count($a) or k < 0) must give PHP's hash shape. elephc instead zero-filled the gap in packed storage in several places:

  • untyped array properties: class H { public $items = []; } with $h->items[1] = 6 on a fresh object printed [0,6] instead of {"1":6} (found while fixing A ?: on truthy nullable-int parameter with a null default SIGSEGVs #1944);
  • non-empty typed locals written with a key the checker cannot bound ($a = [1]; $a[$k] = $n; printed [1,0,0,6]);
  • array<int> locals passed to user functions, where the fix must not box every slot.

What changes

  • Properties: an untyped array property written with an integer key that is not provably 0 is typed as hash storage, and appends on a promoted hash are supported. Static properties share the same rule through updated_array_property_assign_type.
  • Non-empty locals: a write under an unbounded integer key records its site, and lowering promotes the packed value to a hash before the write (ArrayToHash + HashSet). The literal-key rule from perf(core): close the native and builtin speed gap #2090 (nonempty_array_key_forces_hash_storage) is kept; the broad unproven-key clause it added is replaced by the per-site logic below, so packed storage survives where the key really stays in range.
  • Packed-or-hash locals (src/types/checker/promotable_arrays.rs): a candidate array<int> local (no references, no unset, no by-reference or dynamic calls, and so on) keeps typed raw slots. Its element operations dispatch on the heap kind (one header byte). It is promoted to a hash in place only when a key really skips a slot at run time, and whole-value reads see the kind-aware array<mixed> view. ?int values and null entries keep PHP's null.
  • Typed clones across calls (src/generics/promotable_clones.rs): a direct call that passes a promotable local to an undeclared by-value parameter gets a clone that declares that parameter array<int>, so the array crosses the call raw instead of being boxed into the mixed ABI. Clone bodies carry their own source identity, so span-keyed checker tables stay apart and warnings are not duplicated.
  • Allocation-free counter keys (src/codegen/stack_numeric.rs, src/ir_passes/peephole/tagged_box.rs):
    • int|float counter keys such as $j + 1 and $j-- stay in frame-local numeric cells when used as array or hash keys. The key materializer reads the cell inline.
    • Ordering comparisons build the runtime triple in registers for inline scalars, instead of boxing them.
    • Inline ?int boxes that only feed an ordering comparison, or nothing at all, are removed.

All five targets get the same codegen (aarch64 and x86_64 emitters), with asm tests that run on every target.

Performance

Insertion sort over 6000 elements keyed by $j + 1 and $j--:

main this branch PHP 8.5
counter keys 1275 ms about 185 ms 166 ms
int keys 400 ms 107 ms

Tests

  • tests/codegen/arrays/sparse_int_keys.rs and tests/codegen/objects/property_access/sparse_int_keys.rs: PHP shapes for property and local writes, packed-or-hash promotion, clones, null entries, leak checks.
  • tests/codegen/optimizer/stack_numeric/counter_keys.rs: allocation counts that do not grow with iterations, PHP semantics of nullable ordering, float and negative counter keys, asm checks on all five targets.
  • src/ir_lower/tests/sparse_key_writes.rs and src/ir_passes/tests/tagged_box_test.rs: lowering and peephole unit tests.

Docs: docs/php/arrays.md, docs/internals/the-codegen.md, docs/internals/memory-model.md.

Sparse integer writes on untyped array properties and locals now promote
to a hash instead of filling the hole. Locals whose keys are only proven
at runtime become packed-or-hash arrays, typed clones carry them across
direct calls, and int|float counter keys stay in frame cells so the
promoted paths do not allocate per iteration.
Range analysis now narrows `$j = $i - 1` to a raw isub whose acquire
boxes it into the int|float counter slot. Treat that acquire as a numeric
box so the seed reuses the frame cell instead of allocating per entry.
… them once

A float variable key past the end of a non-promotable packed local now
promotes it to a hash instead of zero-filling. A boxed float key on a
promotable local no longer repeats its precision deprecation on the hash
write after the range probe.
@github-actions github-actions Bot added area:codegen Touches target-aware assembly or backend lowering. area:eir Touches EIR definitions, lowering, validation, or passes. area:types Touches type checking, inference, or compatibility. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:l Large pull request. type:fix Corrects broken or incompatible behavior. labels Nov 6, 2026
…static slots

A counter-key read of an array<int> local takes the ?int view only when
the local can hold a promotable array (written by element, or recorded
promotable); an unwritten local reads as mixed again, so buffer indexes
and int reassignments compile. The skipping-key hash promotion applies to
instance properties only, since a static slot's ??= stores boxed values.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:codegen Touches target-aware assembly or backend lowering. area:eir Touches EIR definitions, lowering, validation, or passes. area:types Touches type checking, inference, or compatibility. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:l Large pull request. type:fix Corrects broken or incompatible behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant