Skip to content

Do not keep the results of literal array keys and values - #6781

Open
ondrejmirtes wants to merge 2 commits into
2.3.xfrom
literal-array-memory
Open

ondrejmirtes wants to merge 2 commits into
2.3.xfrom
literal-array-memory

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Follow-up to #6778 for the memory side of phpstan/phpstan#15452.

A statement keeps every ExpressionResult it produced in the storage until it ends, and ArrayHandler keeps its items' results until the array's own type is resolved. In a large array literal that is two results per item, although a literal's type is lexical.

  • ArrayHandler keeps only the types of its literal (String_/Int_/Float_) keys and values, and drops their results once the item's callback has fired. getCallableDependencies() and collectAbsorbedItems() read the kept types.
  • MutatingScope::resolveType() prices a literal directly. Without that, every later ask about a dropped literal (e.g. DuplicateKeysInLiteralArraysRule asking about each key) went through on-demand processing, costing ~15% time on the reproducer.
  • AssignHandler reads the keys' results of an assigned array to alias its by-ref items, so an array with a by-ref item anywhere keeps them, and the aliasing walk now skips arrays without one (it had nothing to do there). hasArrayReference() moved to ArrayHandler::hasByRefItem() so both sides share one definition. nsrt/array-literal-by-ref-literal-keys.php mixes plain nested arrays with by-ref items and fails with an internal error if either side of that rule is removed.
  • Ported to the turbo extension (ArrayHandler, AssignHandler, ExpressionResultStorage, MutatingScope).

Reporter's reproducer (https://github.com/alfredbez/phpstan-literal-array-memory-reproducer):

2.3.x this PR
20,000 entries, turbo off 109 MB, 0.58 s 87 MB, 0.56 s
40,000 entries, turbo off 233 MB, 1.13 s 187 MB, 1.14 s
40,000 entries, turbo on 244 MB, 0.43 s 199 MB, 0.42 s

Of the remaining 87 MB at 20,000 entries, 53 MB is php-parser's AST of the 700 KB file.

No time change on ordinary code: src/Type self-analysis, interleaved A/B pairs on user CPU: +0.28% turbo off (n=4, sd 0.85), +0.42% turbo on (n=10, t=0.38).

Verified: full suite with and without turbo, walk-trace identical, smoke, side-by-side, signature parity, clang-tidy on the changed sources.

🤖 Generated with Claude Code

https://claude.ai/code/session_017pydsEaJ8dAiZ22Nt1Yqf4

ondrejmirtes and others added 2 commits October 22, 2026 22:29
A statement keeps every ExpressionResult it produced in the storage until it
ends, and ArrayHandler keeps its items' results until the array's own type is
resolved. In a large array literal that is two results per item, although a
literal's type is lexical.

ArrayHandler now keeps only the types of its literal keys and values and drops
their results once the item's callback has fired. Asks about a literal from
then on are priced directly by MutatingScope::resolveType(), which also spares
them the on-demand processing a missing result would otherwise cost.

AssignHandler reads the keys' results of an assigned array to alias its by-ref
items, so an array with a by-ref item anywhere keeps them, and the aliasing
walk now skips arrays without one - it had nothing to do in them anyway.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017pydsEaJ8dAiZ22Nt1Yqf4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant