From 65d3710a61463d44070a9f34f690eb434890b90e Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 21 Aug 2026 12:38:15 +0800 Subject: [PATCH] fix(parser): handle PHP array overflow in compound assignments and improve method name parsing - Change temporary variable type to Variant for PHP arrays to prevent undefined C++ behavior - Add methodNameToStr helper that treats 'self' and 'static' as ordinary member names - Replace identifierToStr with methodNameToStr for method calls to respect lexical rules - Update nullsafe access trait to use proper method name parsing - Add test case for array element compound arithmetic overflow handling - Add test case for nullsafe chain preserving typed method returns --- src/CompilerBase.php | 9 +++++ src/Parser/AssignOpTrait.php | 6 ++- src/Parser/MethodCallTrait.php | 12 +++--- src/Parser/NullsafeAccessTrait.php | 4 +- tests/compiler/array/compound-overflow.phpt | 23 ++++++++++++ .../nullsafe/nullsafe-chain-typed-return.phpt | 37 +++++++++++++++++++ 6 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 tests/compiler/array/compound-overflow.phpt create mode 100644 tests/compiler/nullsafe/nullsafe-chain-typed-return.phpt diff --git a/src/CompilerBase.php b/src/CompilerBase.php index d28da198..99ba6d8d 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -4631,6 +4631,15 @@ class CompilerBase implements PropertyAccessContext return $this->identifierToStr($node, $require, $literal); } + /** + * Method identifiers share the same lexical rules as property names: + * `self` and `static` are ordinary member names here, not class keywords. + */ + protected function methodNameToStr(NodeAbstract $node, bool $require = true, bool $literal = false): string + { + return $this->propertyNameToStr($node, $require, $literal); + } + protected function requireVar($node, string $var): void { if (!$this->hasVar($var)) { diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 5f955a50..da1b7905 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -897,7 +897,11 @@ trait AssignOpTrait $type = $this->detectVarType($node->var); $rightType = $this->detectTypeOfExpr($node->expr); $tmpVar = $this->genTmpVarName(); - $this->addLocalVar($tmpVar, $rightType); + // PHP arrays are dynamically typed even when SSA can currently + // infer an element as int. Keep the compound result in Variant so + // Zend arithmetic promotes overflowing integers to float instead + // of evaluating a signed C++ expression with undefined behavior. + $this->addLocalVar($tmpVar, Type::VAR); $dim = $this->parseIdentifier($node->var->dim); $readVar = $this->parseArrayDimFetchRead($node->var); $binaryOp = $this->removeAssignOp($op); diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 42e65e97..2644a017 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -322,7 +322,7 @@ trait MethodCallTrait // object's parent. Resolve the method there; the receiver below is // selected from the current static/instance context. $methodPtr = 'php::getMethod(' . $this->getClassEntryPtr($parentClass) . ', ' - . $this->identifierToStr($expr->name) . ')'; + . $this->methodNameToStr($expr->name) . ')'; // A dynamic parent call made from a static method cannot have an // object receiver. Zend validates the resolved method at runtime. $staticCall = (bool) ($this->methodDef->flags & Modifiers::STATIC); @@ -437,7 +437,7 @@ trait MethodCallTrait } $magicMethod = false; - $method = $this->identifierToStr($expr->name, literal: true); + $method = $this->methodNameToStr($expr->name, literal: true); $pythonFacadeCall = $this->parsePythonNativeFacadeMethodCall($expr, $object); if ($pythonFacadeCall !== null) { @@ -795,9 +795,9 @@ trait MethodCallTrait goto _do_call; } if ($this->getVarType($var) == Type::OBJECT) { - $fn = 'php::concat({' . $var . '.getClassName(), "::", ' . $this->identifierToStr($expr->name) . '})'; + $fn = 'php::concat({' . $var . '.getClassName(), "::", ' . $this->methodNameToStr($expr->name) . '})'; } else { - $fn = 'php::concat({' . $this->identifierToStr($expr->class) . ', "::", ' . $this->identifierToStr($expr->name) . '})'; + $fn = 'php::concat({' . $this->identifierToStr($expr->class) . ', "::", ' . $this->methodNameToStr($expr->name) . '})'; } $placeHolder = $fn; } elseif ($this->isNameExpr($expr->class) and $class === 'static') { @@ -808,7 +808,7 @@ trait MethodCallTrait ); } $method = $this->parseIdentifier($expr->name); - $methodPtr = $this->identifierToStr($expr->name, literal: true); + $methodPtr = $this->methodNameToStr($expr->name, literal: true); $fn = Symbol::getCalledCe() . ', php::getMethod(' . Symbol::getCalledCe() . ', ' . $methodPtr . ')'; if ($this->debug) { $this->context->beforeStmtLines[] = $this->formatCppLineComment( @@ -882,7 +882,7 @@ trait MethodCallTrait $fn = $this->getLiteralString($class . '::' . $method); $placeHolder = $this->genArray($callScope); } else { - $fn = 'php::concat({' . $this->identifierToStr($expr->class) . ', "::", ' . $this->identifierToStr($expr->name) . '})'; + $fn = 'php::concat({' . $this->identifierToStr($expr->class) . ', "::", ' . $this->methodNameToStr($expr->name) . '})'; $placeHolder = $fn; } diff --git a/src/Parser/NullsafeAccessTrait.php b/src/Parser/NullsafeAccessTrait.php index 426aae61..194f7424 100644 --- a/src/Parser/NullsafeAccessTrait.php +++ b/src/Parser/NullsafeAccessTrait.php @@ -54,13 +54,13 @@ trait NullsafeAccessTrait $list[] = ['property', $this->propertyNameToStr($expr->name, literal: true), $expr, true]; $expr = $expr->var; } elseif ($expr instanceof Expr\NullsafeMethodCall) { - $list[] = ['method', $this->identifierToStr($expr->name, literal: true), $expr->args, true, $expr]; + $list[] = ['method', $this->methodNameToStr($expr->name, literal: true), $expr->args, true, $expr]; $expr = $expr->var; } elseif ($expr instanceof Expr\PropertyFetch) { $list[] = ['property', $this->propertyNameToStr($expr->name, literal: true), $expr, false]; $expr = $expr->var; } elseif ($expr instanceof Expr\MethodCall) { - $list[] = ['method', $this->identifierToStr($expr->name, literal: true), $expr->args, false, $expr]; + $list[] = ['method', $this->methodNameToStr($expr->name, literal: true), $expr->args, false, $expr]; $expr = $expr->var; } else { if ($this->isVarExpr($expr)) { diff --git a/tests/compiler/array/compound-overflow.phpt b/tests/compiler/array/compound-overflow.phpt new file mode 100644 index 00000000..70c41575 --- /dev/null +++ b/tests/compiler/array/compound-overflow.phpt @@ -0,0 +1,23 @@ +--TEST-- +Array element compound arithmetic promotes overflowing integers to float +--FILE-- + $maximum, 'multiply' => $large]; + + $values['add'] += 1; + $values['multiply'] *= 2; + + var_dump($values['add'], $values['multiply']); + echo gettype($values['add']), ',', gettype($values['multiply']), "\n"; +} +?> +--EXPECT-- +float(9.223372036854776E+18) +float(9.223372036854776E+18) +double,double diff --git a/tests/compiler/nullsafe/nullsafe-chain-typed-return.phpt b/tests/compiler/nullsafe/nullsafe-chain-typed-return.phpt new file mode 100644 index 00000000..6c990e8a --- /dev/null +++ b/tests/compiler/nullsafe/nullsafe-chain-typed-return.phpt @@ -0,0 +1,37 @@ +--TEST-- +Nullsafe chain preserves typed method returns and member names +--FILE-- +leaf = new NullsafeTypedReturnLeaf(); + } + + public function self(): NullsafeTypedReturnNode + { + return $this; + } +} + +function main(): void +{ + $target = new NullsafeTypedReturnNode(); + $weak = WeakReference::create($target); + echo $target->self()->leaf->value, "\n"; + echo $weak->get()?->self()?->leaf->value, "\n"; +} +?> +--EXPECT-- +forward +forward