Skip to content

Language: give the exact arg->producer link the last word (#23354) - #23424

Merged
PurHur merged 1 commit into
masterfrom
fix/arg-exact-link
Jul 26, 2026
Merged

Language: give the exact arg->producer link the last word (#23354)#23424
PurHur merged 1 commit into
masterfrom
fix/arg-exact-link

Conversation

@PurHur

@PurHur PurHur commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Completes #23354. Stacked on #23356 — review that one first.

#23356 fixed two of the sites that hand a hoisted call argument the trailing argument's producer. Tracing the rest of the corpus turned up four more copies of the same mistake, plus a second, unrelated defect underneath them.

Four more of the same

site what it does
resolveInlineArrayLiteralDimFetchCallArgSlot() returns children[$callIndex - 1] for any $argIndex — no ordinal check at all
resolvePrecedingExpressionPreludeCallArgSlot() hardcoded to 0 === $argIndex while reading the trailing producer — exactly backwards on a multi-argument call
resolvePrecedingArrayDimFetchCallArgSlot() when there are fewer hoisted fetches than arguments, aligns them to the first non-embedded arguments; hoisted fetches sit immediately before the call, so they belong to the last ones
inlineHoistedProducerForCallArgIndex() falls back to positional mapping

Rather than add a sixth positional heuristic, php-cfg's exact link now gets the last word before ARG_SEND is emitted. The hoisted argument temporary is a distinct Operand from the producer's ->result — which is why slotForOperand($arg) misses and all of these heuristics exist — but it records that producer as its sole writer, so args[$i]->ops[0] is right by construction rather than by shape. Restricted to dead inline temporaries with exactly one writer whose producer is a hoisted statement of this block before the call; everything else keeps the existing paths.

The defect underneath

f($x + 1, $r['k']) still printed K|K after all of those, because the value never reaches the call at all.

Block::getFrame() builds a fresh scope array per block. Named variables survive a CFG edge via findVariableInParentFrames() — but that resolves by name, and temporaries have none (deliberately, #3790). So every temporary computed before a block split reads back empty afterwards. ['k' => 'K'] triggers splitCfgBlockAfterStringKeyedArray(), and its continuation was the only one in the compiler that does not set inheritUndefinedLocals, which is what makes getFrame() inherit the parent frame's slots:

 $cont->inheritScopeFrom($block);
+$cont->inheritUndefinedLocals = true;   // temporaries cannot inherit by name

CFG merges, try/catch bodies and short-circuit arms all already set it.

Validation

Differential execution against Zend over the same 43 programs as #23356:

mismatching programs
master 24
#23356 8
this PR 0

Adds call_arg_producer_mixed.phpt, covering mixed producers, trailing property fetch, the split-stranded case, ternary arms, independent nested dim chains, and comparison producers. It fails on master (K| …) and on #23356 alone (K|K …); only this change makes it pass.

Compliance VMTest (7,549 cases) run base-vs-branch; results in the thread.

Results

Differential execution against Zend, rebased onto current master:

argcls (18) argsweep (25) total
master 13 22 35 / 43
this PR 18 25 43 / 43

Compliance VMTest, run on this branch and on master, compared by failing case name (the suite is not green on master — 407 pre-existing failures), sharded 24 ways with per-test TeamCity output:

comparable cases 7,387
regressions 0
fixed 23

Three cases first appeared as regressions and were each checked individually rather than assumed:

  • language/interface_abstract_static_call, types/dnf_return_type_error, stdlib/proc_get_status_basic — all three fail 5/5 on master as well when run in isolation, and re-running the shard that contains it flipped interface_abstract_static_call to failing on master too. They are order-dependent, not caused by this change. proc_get_status_basic is the clearest case: it appeared on the fixed list in the earlier Language: map each hoisted call arg to its own producer (#23354) #23356 comparison and on the regressed list here.

The 23 fixed cases are dominated by call-argument shapes — in_array/array_search with enum arguments (7 cases), array_udiff_enum, array_multisort_byref_call_flags, call_user_func_array_class_string_inline, sscanf_array_element_ref, date_sunrise_sunset_nested_strtotime, password_needs_rehash_nested_hash — which is what the exact-link mapping is expected to repair.

Follow-up to the argument-mapping fix. Four more sites resolved a hoisted call
argument from the statement immediately before the call — only ever the trailing
argument's producer — and applied it to an index it does not belong to:

  * resolveInlineArrayLiteralDimFetchCallArgSlot() returned children[$callIndex - 1]
    for any $argIndex, with no ordinal check at all.
  * resolvePrecedingExpressionPreludeCallArgSlot() is hardcoded to $argIndex === 0
    while reading the trailing producer — exactly backwards on a multi-arg call.
  * resolvePrecedingArrayDimFetchCallArgSlot() aligned fewer-fetches-than-args to
    the FIRST non-embedded arguments; hoisted fetches sit immediately before the
    call and so belong to the LAST ones.
  * inlineHoistedProducerForCallArgIndex() fell back to positional mapping.

Rather than add a sixth positional heuristic, php-cfg's exact link now has the last
word before ARG_SEND is emitted: the hoisted argument temporary is a distinct Operand
from the producer's ->result but records that producer as its sole writer, so
args[$i]->ops[0] is right by construction rather than by shape. Restricted to dead
inline temporaries with exactly one writer whose producer is a hoisted statement of
this block before the call; everything else keeps the existing paths.

Underneath sat a second defect. Block::getFrame() builds a fresh scope array per
block and findVariableInParentFrames() resolves by NAME, but temporaries have none
(deliberately, #3790) — so every temporary computed before a CFG block split read
back empty afterwards, which is why f($x + 1, $r['k']) stayed wrong even once the
mapping was right. Every other continuation block (CFG merges, try/catch bodies,
short-circuit arms) sets inheritUndefinedLocals so getFrame() inherits the parent
frame's slots; the continuation from splitCfgBlockAfterStringKeyedArray() did not.

Differential sweep against Zend over the same 43 programs: 24 mismatching on master,
8 after the first fix, 0 now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@PurHur

PurHur commented Jul 26, 2026

Copy link
Copy Markdown
Owner Author

Regression verification complete — all three candidates confirmed not caused by this change:

case isolation (5 runs each) shard-context rerun
language/interface_abstract_static_call master 0/5 pass, branch 0/5 pass flipped to failing on master
types/dnf_return_type_error master 0/5 pass, branch 0/5 pass fails on master too
stdlib/proc_get_status_basic master 0/5 pass, branch 0/5 pass

All three fail on master as well; their "pass" in the original sharded baseline was order-dependent. proc_get_status_basic is the clearest illustration — it appeared on the fixed list in the #23356 comparison and on the regressed list here, same test, opposite verdicts.

Net: 0 regressions, 23 fixed across 7,387 comparable cases.

A full JITTest comparison is also running, since compileCallArgSends feeds the AOT/JIT path and the VM suite alone would not cover it.

@PurHur

PurHur commented Jul 26, 2026

Copy link
Copy Markdown
Owner Author

Maintainer verify + merge (2026-07-26)

Host Zend 8.2.32 vs bin/vm.php:

master before this PR / post-merge
f($x+1, $r['k']) K|K 6|K
nested dim pair Array|CD AB|CD
comparison pair false|false true|false
call_arg_producer_mixed.phpt (missing/fail) EXPECT match

Merged as c87e96cb8; closed #23354.

(#10533 comments locked at 2500+) Gate note for this maintainer run: bootstrap-inventory --check red on file-list drift; bootstrap-selfhost-link was mid Zend gen-0 compiler_minimal compile (stale gen-0 fingerprint path). Filed behavioral IR gaps #23430#23433.

PurHur added a commit that referenced this pull request Jul 26, 2026
The compliance suite asserts against recorded expectations, so it only catches
what someone already thought to record. Nothing in the tree compared the
compiler's output against Zend's on arbitrary programs, which is why #23354 —
multi-argument calls handing every argument the trailing one's value — survived
with no diagnostic: f($x + 1, $x + 2) printed "12 12" and str_replace($p['from'],
$p['to'], 'xy!') returned "xy!", both running happily to completion.

24 of these 43 programs mismatched Zend on master when the corpus was written.
All are fixed (#23356, #23424); the corpus stays as the regression guard, and the
harness generalises to any directory of programs via --dir.

Cases are deliberately mundane — multi-argument calls by producer kind, mixed
producer kinds in one call, and the shapes the argument-resolution heuristics were
individually tuned against (var_export, usort, in_array, array_merge, sprintf,
by-ref, named args, spread, closures, static calls, constructor promotion).

Exit status is the mismatch count so it can gate a build.

Co-authored-by: PurHur <tedyyyyy@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@PurHur
PurHur deleted the fix/arg-exact-link branch July 26, 2026 15:04
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.

2 participants