@@ -1084,7 +1084,7 @@ class AssignHandler
10841084 void construct (zval *argv)
10851085 {
10861086 zv::ObjRef object (self);
1087- for (uint32_t i = 0 ; i < 20 ; i++) {
1087+ for (uint32_t i = 0 ; i < 21 ; i++) {
10881088 object.propAtWrite (i, zv::Val::copyOf (zv::Ref (&argv[i])));
10891089 }
10901090 }
@@ -2159,10 +2159,35 @@ class AssignHandler
21592159 AH_VALB (assignNode, newVariableAssignNode (var, assignedExpr.raw ()));
21602160 AH_OKB (nsrCallNodeCallback (nsr, nodeCallback, assignNode.raw (), scopeBeforeAssignEval.raw (), storage));
21612161 }
2162+ zv::Val remappedConditionalExpressions = ahEmptyArray ();
2163+ // only the concat-assign's own write remaps - an enclosing plain
2164+ // assignment of the same variable (`$s = $s .= 'x'`) sees the
2165+ // already-remapped holders and would remap them a second time
2166+ if (Z_TYPE_P (AH_TARGET (target, isAssignOp)) == IS_TRUE && ahIs (assignedExpr.raw (), PT_CLASS_ASSIGN_OP_CONCAT )) {
2167+ zend_string *concatVarName = ahStringVariableName (AH_PROP (assignedExpr.raw (), var));
2168+ if (concatVarName != NULL && zend_string_equals (concatVarName, varName)) {
2169+ AH_VALB (scopeConditionalExpressions, pt_mutating_scope_get_conditional_expressions (Z_OBJ_P (scope.raw ())));
2170+ if (Z_TYPE_P (scopeConditionalExpressions.raw ()) == IS_ARRAY && zend_hash_num_elements (Z_ARRVAL_P (scopeConditionalExpressions.raw ())) > 0 ) {
2171+ AH_VALB (appendedType, pt_expression_result_get_type (valueResult));
2172+ zv::Str varExprString = zv::Str::adopt (zend_string_concat2 (PT_LC (" $" ), ZSTR_VAL (varName), ZSTR_LEN (varName)));
2173+ AH_SETB (remappedConditionalExpressions, remapConditionalExpressionsThroughConcatAssign (scopeConditionalExpressions.raw (), varExprString.get (), appendedType.raw ()));
2174+ }
2175+ }
2176+ }
21622177
21632178 AH_VALB (nativeScope, pt_mutating_scope_do_not_treat_phpdoc_types_as_certain (Z_OBJ_P (scope.raw ())));
21642179 AH_VALB (nativeType, readAssignedValueType (nsr, stored, assignedExpr.raw (), nativeScope.raw ()));
21652180 AH_SETB (scope, pt_mutating_scope_assign_variable (Z_OBJ_P (scope.raw ()), varName, type.raw (), nativeType.raw (), ahTrinary (PT_TRI_YES )));
2181+ for (auto entry : zv::TableRef (Z_ARRVAL_P (remappedConditionalExpressions.raw ()))) {
2182+ zval *holders = entry.value ().raw ();
2183+ zend_string *exprString = entry.stringKeyOrNull ();
2184+ if (exprString != NULL ) {
2185+ AH_SETB (scope, pt_mutating_scope_add_conditional_expressions (Z_OBJ_P (scope.raw ()), exprString, Z_ARRVAL_P (holders)));
2186+ } else {
2187+ zv::Str key = zv::Str::adopt (zend_long_to_str ((zend_long) entry.indexKey ()));
2188+ AH_SETB (scope, pt_mutating_scope_add_conditional_expressions (Z_OBJ_P (scope.raw ()), key.get (), Z_ARRVAL_P (holders)));
2189+ }
2190+ }
21662191 for (auto entry : zv::TableRef (Z_ARRVAL_P (conditionalExpressions.raw ()))) {
21672192 zval *holders = entry.value ().raw ();
21682193 zend_string *exprString = entry.stringKeyOrNull ();
@@ -2485,6 +2510,127 @@ class AssignHandler
24852510 return true ;
24862511 }
24872512
2513+ static bool strContains (zend_string *haystack, zend_string *needle)
2514+ {
2515+ return zend_memnstr (ZSTR_VAL (haystack), ZSTR_VAL (needle), ZSTR_LEN (needle), ZSTR_VAL (haystack) + ZSTR_LEN (haystack)) != NULL ;
2516+ }
2517+
2518+ /* the single constant string a type is exactly (count(getConstantStrings())
2519+ * === 1 and equals() it), UNDEF = pending exception, PHP null = none */
2520+ static zv::Val singleConstantString (zval *type)
2521+ {
2522+ AH_VAL (constantStrings, ahCall (type, PT_LC (" getconstantstrings" ), 0 , NULL ));
2523+ if (Z_TYPE_P (constantStrings.raw ()) != IS_ARRAY || zend_hash_num_elements (Z_ARRVAL_P (constantStrings.raw ())) != 1 ) return zv::Val::null ();
2524+ zval *constantString = zend_hash_index_find (Z_ARRVAL_P (constantStrings.raw ()), 0 );
2525+ if (UNEXPECTED (constantString == NULL )) {
2526+ pt_throw_should_not_happen ();
2527+ return zv::Val ();
2528+ }
2529+ ZVAL_DEREF (constantString);
2530+ bool equal;
2531+ AH_OK (ahEquals (type, constantString, equal));
2532+ if (!equal) return zv::Val::null ();
2533+ return zv::Val::copyOf (zv::Ref (constantString));
2534+ }
2535+
2536+ /* $holder->getConditionExpressionTypeHolders() / ->getTypeHolder() of a ConditionalExpressionHolder */
2537+ static zv::Val conditionalHolderPart (zval *holder, bool conditions)
2538+ {
2539+ if (EXPECTED (Z_TYPE_P (holder) == IS_OBJECT && Z_OBJCE_P (holder) == pt_ce_cond_expr_holder)) {
2540+ return zv::Val::copyOf (zv::ObjRef (Z_OBJ_P (holder)).propAt (conditions ? PT_CEH_PROP_CONDS : PT_CEH_PROP_TYPEHOLDER ).deref ());
2541+ }
2542+ return conditions ? ahCall (holder, PT_LC (" getconditionexpressiontypeholders" ), 0 , NULL ) : ahCall (holder, PT_LC (" gettypeholder" ), 0 , NULL );
2543+ }
2544+
2545+ /* (twin remapConditionalExpressionsThroughConcatAssign) `$var .= <single
2546+ * constant string>` keeps the conditional-expression holders about $var
2547+ * alive by remapping them through the append instead of losing them to
2548+ * the write's invalidation; see the PHP twin for the rules. */
2549+ zv::Val remapConditionalExpressionsThroughConcatAssign (zval *conditionalExpressions, zend_string *varExprString, zval *appendedType)
2550+ {
2551+ AH_VAL (appendedConstantString, singleConstantString (appendedType));
2552+ if (appendedConstantString.isNull ()) return ahEmptyArray ();
2553+
2554+ zv::Val remapped = ahEmptyArray ();
2555+ for (auto targetEntry : zv::TableRef (Z_ARRVAL_P (conditionalExpressions))) {
2556+ zv::Str targetExprString = targetEntry.stringKeyOrNull () != NULL ? zv::Str::copyOf (targetEntry.stringKey ()) : zv::Str::adopt (zend_long_to_str ((zend_long) targetEntry.indexKey ()));
2557+ bool targetIsVar = zend_string_equals (targetExprString.get (), varExprString);
2558+ if (!targetIsVar && strContains (targetExprString.get (), varExprString)) {
2559+ // a composite target containing the variable ($var[0], f($var), ...)
2560+ continue ;
2561+ }
2562+
2563+ zval *holders = targetEntry.value ().deref ().raw ();
2564+ if (UNEXPECTED (Z_TYPE_P (holders) != IS_ARRAY )) continue ;
2565+ for (auto holderEntry : zv::TableRef (Z_ARRVAL_P (holders))) {
2566+ zval *holder = holderEntry.value ().deref ().raw ();
2567+ bool holderTouchesVar = targetIsVar;
2568+ bool remappable = true ;
2569+ zv::Val newConditions = ahEmptyArray ();
2570+ AH_VAL (conditions, conditionalHolderPart (holder, true ));
2571+ if (UNEXPECTED (Z_TYPE_P (conditions.raw ()) != IS_ARRAY )) continue ;
2572+ for (auto conditionEntry : zv::TableRef (Z_ARRVAL_P (conditions.raw ()))) {
2573+ zv::Str conditionExprString = conditionEntry.stringKeyOrNull () != NULL ? zv::Str::copyOf (conditionEntry.stringKey ()) : zv::Str::adopt (zend_long_to_str ((zend_long) conditionEntry.indexKey ()));
2574+ zval *conditionHolder = conditionEntry.value ().deref ().raw ();
2575+ if (zend_string_equals (conditionExprString.get (), varExprString)) {
2576+ holderTouchesVar = true ;
2577+ if (pt_holder_certainty_value (Z_OBJ_P (conditionHolder)) != PT_TRI_YES ) {
2578+ remappable = false ;
2579+ break ;
2580+ }
2581+ zval *conditionType = OBJ_PROP_NUM (Z_OBJ_P (conditionHolder), PT_ETH_PROP_TYPE );
2582+ ZVAL_DEREF (conditionType);
2583+ AH_VAL (conditionConstantString, singleConstantString (conditionType));
2584+ if (conditionConstantString.isNull ()) {
2585+ remappable = false ;
2586+ break ;
2587+ }
2588+ zval appendArg;
2589+ ZVAL_COPY_VALUE (&appendArg, appendedConstantString.raw ());
2590+ AH_VAL (appended, ahCall (conditionConstantString.raw (), PT_LC (" append" ), 1 , &appendArg));
2591+ zval *conditionExpr = OBJ_PROP_NUM (Z_OBJ_P (conditionHolder), PT_ETH_PROP_EXPR );
2592+ ZVAL_DEREF (conditionExpr);
2593+ AH_VAL (newConditionHolder, newExpressionTypeHolder (conditionExpr, appended.raw (), PT_TRI_YES ));
2594+ ahSetKey (newConditions, conditionExprString.get (), newConditionHolder.raw ());
2595+ continue ;
2596+ }
2597+
2598+ if (strContains (conditionExprString.get (), varExprString)) {
2599+ remappable = false ;
2600+ break ;
2601+ }
2602+
2603+ ahSetKey (newConditions, conditionExprString.get (), conditionHolder);
2604+ }
2605+ if (!remappable || !holderTouchesVar) continue ;
2606+
2607+ AH_VAL (typeHolder, conditionalHolderPart (holder, false ));
2608+ if (targetIsVar) {
2609+ if (pt_holder_certainty_value (Z_OBJ_P (typeHolder.raw ())) != PT_TRI_YES ) {
2610+ // the append leaves the variable defined on every path -
2611+ // an undefined/maybe consequence cannot be carried over
2612+ continue ;
2613+ }
2614+ zval *holderType = OBJ_PROP_NUM (Z_OBJ_P (typeHolder.raw ()), PT_ETH_PROP_TYPE );
2615+ ZVAL_DEREF (holderType);
2616+ zv::Args concatArgs{holderType, appendedType};
2617+ AH_VAL (concatType, ahCall (prop (slots::initializerExprTypeResolver), PT_LC (" resolveconcattype" ), 2 , concatArgs));
2618+ if (Z_TYPE_P (concatType.raw ()) == IS_OBJECT && instanceof_function (Z_OBJCE_P (concatType.raw ()), pt_ce_error_type)) continue ;
2619+ zval *holderExpr = OBJ_PROP_NUM (Z_OBJ_P (typeHolder.raw ()), PT_ETH_PROP_EXPR );
2620+ ZVAL_DEREF (holderExpr);
2621+ AH_SET (typeHolder, newExpressionTypeHolder (holderExpr, concatType.raw (), PT_TRI_YES ));
2622+ }
2623+
2624+ AH_VAL (newHolder, newConditionalExpressionHolder (newConditions.raw (), typeHolder.raw ()));
2625+ zv::Str newHolderKey = zv::Str::adopt (conditionalHolderKey (newHolder.raw ()));
2626+ AH_OK (!newHolderKey.isNull ());
2627+ nestedSet (remapped, targetExprString.get (), newHolderKey.get (), newHolder.raw ());
2628+ }
2629+ }
2630+
2631+ return remapped;
2632+ }
2633+
24882634 /* Records "if the assigned variable has $variableType, the target variable is
24892635 * certainly defined (with its branch-scope type)" holders for variables whose
24902636 * certainty is Yes in the given branch continuation scope of the RHS (the
@@ -4960,7 +5106,7 @@ PT_MINIT_REGISTRATION(pt_register_assign_handler)
49605106 cls.method (sigs::__construct, [](INTERNAL_FUNCTION_PARAMETERS ) {
49615107 zval *argv;
49625108 uint32_t argc;
4963- ZEND_PARSE_PARAMETERS_START (20 , 20 )
5109+ ZEND_PARSE_PARAMETERS_START (21 , 21 )
49645110 Z_PARAM_VARIADIC (' +' , argv, argc)
49655111 ZEND_PARSE_PARAMETERS_END ();
49665112 for (uint32_t i = 0 ; i < argc; i++) {
0 commit comments