@@ -311,11 +311,15 @@ class ScopeOps
311311 return zv::Val::adopt (out);
312312 }
313313
314- /* Mirrors ScopeOps::mergeVariableHolders(). */
315- static zv::Val mergeVariableHolders (zv::TableRef ours, zv::TableRef theirs)
314+ /*
315+ * Mirrors ScopeOps::mergeVariableHolders(). differing (nullable) receives
316+ * a true marker for every key that is not one shared holder on both sides
317+ * — the twin's &$differingKeys out-parameter.
318+ */
319+ static zv::Val mergeVariableHolders (zv::TableRef ours, zv::TableRef theirs, HashTable *differing)
316320 {
317321 zv::Arr merged = zv::Arr::create (ours.size ());
318- if (UNEXPECTED (!mergeVariableHoldersInto (merged, ours, theirs))) {
322+ if (UNEXPECTED (!mergeVariableHoldersInto (merged, ours, theirs, differing ))) {
319323 return zv::Val ();
320324 }
321325 return zv::Val (std::move (merged));
@@ -394,7 +398,7 @@ class ScopeOps
394398
395399 /* mergedNative += filter(mergeVariableHolders(oursRemaining, theirsRemaining)) */
396400 {
397- zv::Val remainingMerged = mergeVariableHolders (zv::TableRef (oursNativeRemaining.table ()), zv::TableRef (theirsNativeRemaining.table ()));
401+ zv::Val remainingMerged = mergeVariableHolders (zv::TableRef (oursNativeRemaining.table ()), zv::TableRef (theirsNativeRemaining.table ()), NULL );
398402 if (UNEXPECTED (remainingMerged.isUndef ())) {
399403 return zv::Val ();
400404 }
@@ -461,7 +465,7 @@ class ScopeOps
461465 * per-guard caches; the input array is only duplicated once the first
462466 * conditional is actually appended.
463467 */
464- static zv::Val createConditionalExpressions (zv::TableRef conditional, zv::TableRef ours, zv::TableRef theirs, zv::TableRef merged)
468+ static zv::Val createConditionalExpressions (zv::TableRef conditional, zv::TableRef ours, zv::TableRef theirs, zv::TableRef merged, zv::TableRef differingKeys )
465469 {
466470 zend_class_entry *virtualNodeCe = pt_class (PT_CLASS_VIRTUAL_NODE );
467471 if (UNEXPECTED (virtualNodeCe == NULL )) {
@@ -472,16 +476,22 @@ class ScopeOps
472476 zv::ScratchTable typeGuards (8 );
473477
474478 /* guardsToExclude: subtype-absorbed their-branch variables are poor
475- * guards but stay valid conditional targets */
476- for (auto entry : theirs) {
477- zend_string *key = entry.stringKeyOrNull ();
478- zend_ulong idx = entry.indexKey ();
479+ * guards but stay valid conditional targets. Only the merge's differing
480+ * keys can qualify — iterate those (in their insertion order, like the
481+ * twin) instead of the whole holder maps. */
482+ for (auto diffEntry : differingKeys) {
483+ zend_string *key = diffEntry.stringKeyOrNull ();
484+ zend_ulong idx = diffEntry.indexKey ();
479485
486+ zval *theirSlot = pt_ht_find (theirs.table (), key, idx);
487+ if (theirSlot == NULL ) {
488+ continue ;
489+ }
480490 zval *mergedSlot = pt_ht_find (merged.table (), key, idx);
481491 if (mergedSlot == NULL ) {
482492 continue ;
483493 }
484- zv::Ref holder = entry. value ( ).deref ();
494+ zv::Ref holder = zv::Ref (theirSlot ).deref ();
485495 if (UNEXPECTED (!pt_check_holder (holder.raw ()))) {
486496 return zv::Val ();
487497 }
@@ -522,11 +532,15 @@ class ScopeOps
522532 }
523533
524534 /* typeGuards */
525- for (auto entry : ours) {
526- zend_string *key = entry.stringKeyOrNull ();
527- zend_ulong idx = entry.indexKey ();
528- zv::Ref holder = entry.value ().deref ();
535+ for (auto diffEntry : differingKeys) {
536+ zend_string *key = diffEntry.stringKeyOrNull ();
537+ zend_ulong idx = diffEntry.indexKey ();
529538
539+ zval *ourSlot = pt_ht_find (ours.table (), key, idx);
540+ if (ourSlot == NULL ) {
541+ continue ;
542+ }
543+ zv::Ref holder = zv::Ref (ourSlot).deref ();
530544 if (UNEXPECTED (!pt_check_holder (holder.raw ()))) {
531545 return zv::Val ();
532546 }
@@ -585,10 +599,15 @@ class ScopeOps
585599 zv::Arr result; /* stays UNDEF until the first append duplicates the input */
586600
587601 /* main loop: pair non-merged expressions with guards */
588- for (auto entry : ours) {
589- zend_string *key = entry.stringKeyOrNull ();
590- zend_ulong idx = entry.indexKey ();
591- zv::Ref holder = entry.value ().deref ();
602+ for (auto diffEntry : differingKeys) {
603+ zend_string *key = diffEntry.stringKeyOrNull ();
604+ zend_ulong idx = diffEntry.indexKey ();
605+
606+ zval *ourSlot = pt_ht_find (ours.table (), key, idx);
607+ if (ourSlot == NULL ) {
608+ continue ;
609+ }
610+ zv::Ref holder = zv::Ref (ourSlot).deref ();
592611
593612 if (instanceof_function (holderExpr (holder)->ce , virtualNodeCe)) {
594613 continue ;
@@ -711,14 +730,18 @@ class ScopeOps
711730 }
712731
713732 /* their-only expressions: record certainty-No conditionals per guard */
714- for (auto entry : merged ) {
715- zend_string *key = entry .stringKeyOrNull ();
716- zend_ulong idx = entry .indexKey ();
733+ for (auto diffEntry : differingKeys ) {
734+ zend_string *key = diffEntry .stringKeyOrNull ();
735+ zend_ulong idx = diffEntry .indexKey ();
717736
737+ zval *mergedSlot = pt_ht_find (merged.table (), key, idx);
738+ if (mergedSlot == NULL ) {
739+ continue ;
740+ }
718741 if (pt_ht_exists (ours.table (), key, idx)) {
719742 continue ;
720743 }
721- zv::Ref mergedHolder = entry. value ( ).deref ();
744+ zv::Ref mergedHolder = zv::Ref (mergedSlot ).deref ();
722745 if (UNEXPECTED (!pt_check_holder (mergedHolder.raw ()))) {
723746 return zv::Val ();
724747 }
@@ -1381,8 +1404,19 @@ class ScopeOps
13811404 return zv::Val::adopt (created);
13821405 }
13831406
1407+ /* $differing[$key] = true (marker insert, overwrites) */
1408+ static void markDiffering (HashTable *differing, zend_string *skey, zend_ulong idx)
1409+ {
1410+ if (differing == NULL ) {
1411+ return ;
1412+ }
1413+ zval trueZv;
1414+ ZVAL_TRUE (&trueZv);
1415+ pt_ht_update (differing, skey, idx, &trueZv);
1416+ }
1417+
13841418 /* The two loops of mergeVariableHolders(), filling a caller-owned table. */
1385- static bool mergeVariableHoldersInto (zv::Arr &merged, zv::TableRef ours, zv::TableRef theirs)
1419+ static bool mergeVariableHoldersInto (zv::Arr &merged, zv::TableRef ours, zv::TableRef theirs, HashTable *differing )
13861420 {
13871421 for (auto entry : ours) {
13881422 zend_string *key = entry.stringKeyOrNull ();
@@ -1402,13 +1436,15 @@ class ScopeOps
14021436 if (holder.asObject () == theirHolder.asObject ()) {
14031437 tableAddNewCopy (merged.table (), key, idx, holder);
14041438 } else {
1439+ markDiffering (differing, key, idx);
14051440 zval andHolder;
14061441 if (UNEXPECTED (!pt_holder_and (holder.raw (), theirHolder.raw (), &andHolder))) {
14071442 return false ;
14081443 }
14091444 tableAddNew (merged.table (), key, idx, zv::Val::adopt (andHolder));
14101445 }
14111446 } else {
1447+ markDiffering (differing, key, idx);
14121448 bool containsSuperGlobal = pt_expr_contains_superglobal (holderExpr (holder));
14131449 if (UNEXPECTED (EG (exception))) {
14141450 return false ;
@@ -1427,6 +1463,7 @@ class ScopeOps
14271463 if (pt_ht_exists (merged.table (), key, idx)) {
14281464 continue ;
14291465 }
1466+ markDiffering (differing, key, idx);
14301467 zv::Ref holder = entry.value ().deref ();
14311468 if (UNEXPECTED (!pt_check_holder (holder.raw ()))) {
14321469 return false ;
@@ -2060,13 +2097,27 @@ void pt_register_scope_ops()
20602097{
20612098 reg::Class cls (" PHPStanTurbo\\ ScopeOps" );
20622099
2063- cls.method (" mergeVariableHolders" , reg::PublicStatic, 2 , { reg::arrayArg (" ourVariableTypeHolders" ), reg::arrayArg (" theirVariableTypeHolders" ) }, [](INTERNAL_FUNCTION_PARAMETERS ) {
2100+ cls.method (" mergeVariableHolders" , reg::PublicStatic, 2 , { reg::arrayArg (" ourVariableTypeHolders" ), reg::arrayArg (" theirVariableTypeHolders" ), reg::any ( " differingKeys " , true ) }, [](INTERNAL_FUNCTION_PARAMETERS ) {
20642101 HashTable *ours, *theirs;
2065- ZEND_PARSE_PARAMETERS_START (2 , 2 )
2102+ zval *differing_zv = NULL ;
2103+ ZEND_PARSE_PARAMETERS_START (2 , 3 )
20662104 Z_PARAM_ARRAY_HT (ours)
20672105 Z_PARAM_ARRAY_HT (theirs)
2106+ Z_PARAM_OPTIONAL
2107+ Z_PARAM_ZVAL (differing_zv)
20682108 ZEND_PARSE_PARAMETERS_END ();
2069- zv::Val result = ScopeOps::mergeVariableHolders (zv::TableRef (ours), zv::TableRef (theirs));
2109+ HashTable *differing = NULL ;
2110+ if (differing_zv != NULL && Z_ISREF_P (differing_zv)) {
2111+ /* the twin declares `array &$differingKeys = []`; vivify like PHP
2112+ * would and write through the reference */
2113+ zval *inner = Z_REFVAL_P (differing_zv);
2114+ if (Z_TYPE_P (inner) != IS_ARRAY ) {
2115+ convert_to_array (inner);
2116+ }
2117+ SEPARATE_ARRAY (inner);
2118+ differing = Z_ARRVAL_P (inner);
2119+ }
2120+ zv::Val result = ScopeOps::mergeVariableHolders (zv::TableRef (ours), zv::TableRef (theirs), differing);
20702121 if (UNEXPECTED (result.isUndef ())) {
20712122 RETURN_THROWS ();
20722123 }
@@ -2184,15 +2235,16 @@ void pt_register_scope_ops()
21842235 result.intoReturnValue (return_value);
21852236 });
21862237
2187- cls.method (" createConditionalExpressions" , reg::PublicStatic, 4 , { reg::arrayArg (" conditionalExpressions" ), reg::arrayArg (" ourExpressionTypes" ), reg::arrayArg (" theirExpressionTypes" ), reg::arrayArg (" mergedExpressionTypes" ) }, [](INTERNAL_FUNCTION_PARAMETERS ) {
2188- HashTable *conditional, *ours, *theirs, *merged;
2189- ZEND_PARSE_PARAMETERS_START (4 , 4 )
2238+ cls.method (" createConditionalExpressions" , reg::PublicStatic, 5 , { reg::arrayArg (" conditionalExpressions" ), reg::arrayArg (" ourExpressionTypes" ), reg::arrayArg (" theirExpressionTypes" ), reg::arrayArg (" mergedExpressionTypes" ), reg::arrayArg ( " differingKeys " ) }, [](INTERNAL_FUNCTION_PARAMETERS ) {
2239+ HashTable *conditional, *ours, *theirs, *merged, *differing_keys ;
2240+ ZEND_PARSE_PARAMETERS_START (5 , 5 )
21902241 Z_PARAM_ARRAY_HT (conditional)
21912242 Z_PARAM_ARRAY_HT (ours)
21922243 Z_PARAM_ARRAY_HT (theirs)
21932244 Z_PARAM_ARRAY_HT (merged)
2245+ Z_PARAM_ARRAY_HT (differing_keys)
21942246 ZEND_PARSE_PARAMETERS_END ();
2195- zv::Val result = ScopeOps::createConditionalExpressions (zv::TableRef (conditional), zv::TableRef (ours), zv::TableRef (theirs), zv::TableRef (merged));
2247+ zv::Val result = ScopeOps::createConditionalExpressions (zv::TableRef (conditional), zv::TableRef (ours), zv::TableRef (theirs), zv::TableRef (merged), zv::TableRef (differing_keys) );
21962248 if (UNEXPECTED (result.isUndef ())) {
21972249 RETURN_THROWS ();
21982250 }
0 commit comments