diff --git a/phpunit/src/SsaAnalysisTest.php b/phpunit/src/SsaAnalysisTest.php index d0601c9d..9601f330 100644 --- a/phpunit/src/SsaAnalysisTest.php +++ b/phpunit/src/SsaAnalysisTest.php @@ -641,9 +641,13 @@ class SsaAnalysisTest extends TestCase $objVar = new Expr\Variable('obj'); $propFetch = new Expr\PropertyFetch($objVar, 'prop'); $unset = new Stmt\Unset_([$propFetch]); + $read = new Stmt\Expression(new Expr\Assign( + new Expr\Variable('value'), + new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop') + )); - $result = $this->invoke('hasDangerousPropOps', 'obj', [$unset]); - $this->assertTrue($result, 'unset($obj->prop) should be detected'); + $result = $this->invoke('hasDangerousPropOps', 'obj', [$unset, $read]); + $this->assertTrue($result, 'unset($obj->prop) before a later access should be detected'); } public function testHasDangerousPropOpsUnsetDifferentObj(): void @@ -663,9 +667,23 @@ class SsaAnalysisTest extends TestCase $refVar = new Expr\Variable('ref'); $assignRef = new Expr\AssignRef($refVar, $propFetch); $stmt = new Stmt\Expression($assignRef); + $read = new Stmt\Expression(new Expr\Assign( + new Expr\Variable('value'), + new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop') + )); + + $result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt, $read]); + $this->assertTrue($result, '&$obj->prop before a later access should be detected'); + } + + public function testHasDangerousPropOpsAssignRefToProperty(): void + { + $propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop'); + $assignRef = new Expr\AssignRef($propFetch, new Expr\Variable('ref')); + $stmt = new Stmt\Expression($assignRef); $result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt]); - $this->assertTrue($result, '&$obj->prop should be detected'); + $this->assertTrue($result, '$obj->prop =& $ref should be detected'); } public function testHasDangerousPropOpsByRefArg(): void @@ -675,9 +693,13 @@ class SsaAnalysisTest extends TestCase $arg = new Arg($propFetch, true); // byRef = true $funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [$arg]); $stmt = new Stmt\Expression($funcCall); + $read = new Stmt\Expression(new Expr\Assign( + new Expr\Variable('value'), + new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop') + )); - $result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt]); - $this->assertTrue($result, 'func(&$obj->prop) should be detected'); + $result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt, $read]); + $this->assertTrue($result, 'func(&$obj->prop) before a later access should be detected'); } public function testHasDangerousPropOpsRefval(): void @@ -689,9 +711,13 @@ class SsaAnalysisTest extends TestCase $arg = new Arg($refvalCall); $funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [$arg]); $stmt = new Stmt\Expression($funcCall); + $read = new Stmt\Expression(new Expr\Assign( + new Expr\Variable('value'), + new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop') + )); - $result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt]); - $this->assertTrue($result, 'func(refval($obj->prop)) should be detected'); + $result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt, $read]); + $this->assertTrue($result, 'func(refval($obj->prop)) before a later access should be detected'); } public function testHasDangerousPropOpsClean(): void @@ -715,9 +741,13 @@ class SsaAnalysisTest extends TestCase 'elseifs' => [], 'else' => null, ]); + $read = new Stmt\Expression(new Expr\Assign( + new Expr\Variable('value'), + new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop') + )); - $result = $this->invoke('hasDangerousPropOps', 'obj', [$ifStmt]); - $this->assertTrue($result, 'unset inside if should be detected'); + $result = $this->invoke('hasDangerousPropOps', 'obj', [$ifStmt, $read]); + $this->assertTrue($result, 'unset inside if before a later access should be detected'); } public function testHasDangerousPropOpsNestedRefvalInAssignment(): void @@ -726,9 +756,13 @@ class SsaAnalysisTest extends TestCase $refvalCall = new Expr\FuncCall(new Node\Name('refval'), [new Arg($propFetch)]); $funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [new Arg($refvalCall)]); $stmt = new Stmt\Expression(new Expr\Assign(new Expr\Variable('result'), $funcCall)); + $read = new Stmt\Expression(new Expr\Assign( + new Expr\Variable('value'), + new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop') + )); - $result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt]); - $this->assertTrue($result, 'refval($obj->prop) nested in an assignment RHS should be detected'); + $result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt, $read]); + $this->assertTrue($result, 'refval($obj->prop) nested in an assignment RHS before a later access should be detected'); } public function testHasDangerousPropOpsNestedByRefInReturn(): void @@ -736,9 +770,111 @@ class SsaAnalysisTest extends TestCase $propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop'); $funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [new Arg($propFetch, true)]); $stmt = new Stmt\Return_($funcCall); + $read = new Stmt\Expression(new Expr\Assign( + new Expr\Variable('value'), + new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop') + )); - $result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt]); - $this->assertTrue($result, 'By-ref property argument nested in return should be detected'); + $result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt, $read]); + $this->assertTrue($result, 'By-ref property argument nested in return before a later access should be detected'); + } + + public function testCollectDangerousPropOpsTracksPropertyNames(): void + { + $propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), 'b'); + $assignRef = new Expr\AssignRef(new Expr\Variable('ref'), $propFetch); + $stmt = new Stmt\Expression($assignRef); + $read = new Stmt\Expression(new Expr\Assign( + new Expr\Variable('value'), + new Expr\PropertyFetch(new Expr\Variable('obj'), 'b') + )); + + $result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]); + $this->assertSame(['b' => true], $result); + } + + public function testCollectDangerousPropOpsDynamicPropertyWildcard(): void + { + $propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), new Expr\Variable('prop')); + $unset = new Stmt\Unset_([$propFetch]); + $read = new Stmt\Expression(new Expr\Assign( + new Expr\Variable('value'), + new Expr\PropertyFetch(new Expr\Variable('obj'), 'a') + )); + + $result = $this->invoke('collectDangerousPropOps', 'obj', [$unset, $read]); + $this->assertSame(['a' => true], $result); + } + + public function testCollectDangerousPropOpsObjectArgumentWildcard(): void + { + $funcCall = new Expr\FuncCall(new Node\Name('mutate'), [new Arg(new Expr\Variable('obj'))]); + $stmt = new Stmt\Expression($funcCall); + $read = new Stmt\Expression(new Expr\Assign( + new Expr\Variable('value'), + new Expr\PropertyFetch(new Expr\Variable('obj'), 'a') + )); + + $result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]); + $this->assertSame(['a' => true], $result); + } + + public function testCollectDangerousPropOpsObjectAliasWildcard(): void + { + $assign = new Expr\Assign(new Expr\Variable('alias'), new Expr\Variable('obj')); + $stmt = new Stmt\Expression($assign); + $read = new Stmt\Expression(new Expr\Assign( + new Expr\Variable('value'), + new Expr\PropertyFetch(new Expr\Variable('obj'), 'a') + )); + + $result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]); + $this->assertSame(['a' => true], $result); + } + + public function testCollectDangerousPropOpsPropertyReadDoesNotExposeObject(): void + { + $propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), 'a'); + $assign = new Expr\Assign(new Expr\Variable('value'), $propFetch); + $stmt = new Stmt\Expression($assign); + + $result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt]); + $this->assertSame([], $result); + } + + public function testCollectDangerousPropOpsUnsetAfterLastAccessIsSafe(): void + { + $read = new Stmt\Expression(new Expr\Assign( + new Expr\Variable('value'), + new Expr\PropertyFetch(new Expr\Variable('obj'), 'a') + )); + $unset = new Stmt\Unset_([new Expr\PropertyFetch(new Expr\Variable('obj'), 'a')]); + + $result = $this->invoke('collectDangerousPropOps', 'obj', [$read, $unset]); + $this->assertSame([], $result); + } + + public function testCollectDangerousPropOpsAssignRefToPropertyIsAlwaysUnsafe(): void + { + $propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), 'a'); + $assignRef = new Expr\AssignRef($propFetch, new Expr\Variable('ref')); + $stmt = new Stmt\Expression($assignRef); + + $result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt]); + $this->assertSame(['a' => true], $result); + } + + public function testCollectDangerousPropOpsObjectArgumentAfterLastAccessIsSafe(): void + { + $read = new Stmt\Expression(new Expr\Assign( + new Expr\Variable('value'), + new Expr\PropertyFetch(new Expr\Variable('obj'), 'a') + )); + $funcCall = new Expr\FuncCall(new Node\Name('mutate'), [new Arg(new Expr\Variable('obj'))]); + $stmt = new Stmt\Expression($funcCall); + + $result = $this->invoke('collectDangerousPropOps', 'obj', [$read, $stmt]); + $this->assertSame([], $result); } // ======================================================================== @@ -818,6 +954,25 @@ class SsaAnalysisTest extends TestCase $this->assertFalse($this->compiler->isStableObject('unknown')); } + public function testCanHoistStableObjectPropAllowsCleanProperty(): void + { + $this->invoke('resetFunction'); + $this->setContextProperty('stableObjects', ['obj' => 'App\\MyClass']); + $this->setContextProperty('unsafeObjectProps', ['obj' => ['b' => true]]); + + $this->assertTrue($this->compiler->canHoistStableObjectProp('obj', 'a')); + $this->assertFalse($this->compiler->canHoistStableObjectProp('obj', 'b')); + } + + public function testCanHoistStableObjectPropRejectsWildcard(): void + { + $this->invoke('resetFunction'); + $this->setContextProperty('stableObjects', ['obj' => 'App\\MyClass']); + $this->setContextProperty('unsafeObjectProps', ['obj' => ['*' => true]]); + + $this->assertFalse($this->compiler->canHoistStableObjectProp('obj', 'a')); + } + // ======================================================================== // LoopVarOptimizer: range-proven counters // ======================================================================== diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index a1b4b80f..c2b1d12c 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -3431,14 +3431,6 @@ class CompilerBase extends \PhpAot\Core\Translator } elseif ($this->isPropertyFetch($var)) { $object = $this->parseIdentifier($var->var); $lines[] = $object . '.unsetProperty(' . $this->identifierToStr($var->name, literal: true) . ');'; - if ($object === 'this_' and $this->isIdExpr($var->name)) { - $propName = $this->parseIdentifier($var->name); - if ($this->hasObjectPropVar($this->getObjectPropVarName($object, $propName))) { - $this->warning($var, "Object property `{$propName}` of native types cannot be unset"); - $lines = []; - $lines[] = $this->getObjectPropVarName($object, $propName) . ' = 0;'; - } - } } elseif ($this->isStaticPropertyFetch($var)) { $this->fatalError($var, 'Attempt to unset static property ' . $this->parseIdentifier($var->class) . '::$' . $this->parseIdentifier($var->name)); } elseif ($this->isVarExpr($var)) { @@ -3496,24 +3488,26 @@ class CompilerBase extends \PhpAot\Core\Translator * @var PropertyDef $def */ $def = $expr->getAttribute('nativePropertyDef'); - if ($def->type === self::TYPE_INT or $def->type === self::TYPE_FLOAT) { - $propVar = $this->getObjectPropVarName($objectVar, $this->parseIdentifier($property)); - if ($objectVar === 'this_') { + $propName = $this->parseIdentifier($property); + $propVar = $this->getObjectPropVarName($objectVar, $propName); + if ($objectVar === 'this_') { + if ($this->canHoistObjectProp($objectVar, $propName)) { if (!$this->hasObjectPropVar($propVar)) { + $info = $this->getHoistedObjectPropInfo($def->type); $this->context->objectProps[$propVar] = [ - 'type' => $def->type, + 'type' => $info['type'], 'getter' => $getProperty, + 'kind' => $info['kind'], ]; } $expr->setAttribute('nativePropertyVar', $propVar); return $propVar; } + } elseif ($this->canHoistStableObjectProp($objectVar, $propName)) { // SSA-stable object: lazily create reference at first access point - if ($this->isStableObject($objectVar)) { - $result = $this->hoistStableObjectProp($objectVar, $this->parseIdentifier($property), $id, $def->type); - $expr->setAttribute('nativePropertyVar', $result); - return $result; - } + $result = $this->hoistStableObjectProp($objectVar, $propName, $id, $def->type); + $expr->setAttribute('nativePropertyVar', $result); + return $result; } } return $getProperty; @@ -5094,7 +5088,12 @@ class CompilerBase extends \PhpAot\Core\Translator $code .= $this->getIndent() . self::TYPE_VAR . ' &' . $name . ' = ' . $this->escapeGlobalVar($name) . ';' . PHP_EOL; } foreach ($this->context->objectProps as $name => $info) { - $code .= $this->getIndent() . $info['type'] . ' &' . $name . ' = Z_LVAL_P(' . $info['getter'] . '.unwrap_ptr());' . PHP_EOL; + if (($info['kind'] ?? 'zval') === 'var') { + $code .= $this->getIndent() . self::TYPE_VAR . ' ' . $name . ' = ' . $info['getter'] . ';' . PHP_EOL; + } else { + $zvalMacro = ($info['type'] === self::TYPE_FLOAT) ? 'Z_DVAL_P' : 'Z_LVAL_P'; + $code .= $this->getIndent() . $info['type'] . ' &' . $name . ' = ' . $zvalMacro . '(' . $info['getter'] . '.unwrap_ptr());' . PHP_EOL; + } } foreach ($this->context->staticPropRefs as $name => $info) { $getter = Symbol::getStaticProperty() . '(' . $info['classPtr'] . ', ' . $info['offsetExpr'] . ')'; diff --git a/src/Php/Context/FunctionContext.php b/src/Php/Context/FunctionContext.php index 2091d977..66ac809b 100644 --- a/src/Php/Context/FunctionContext.php +++ b/src/Php/Context/FunctionContext.php @@ -21,6 +21,9 @@ class FunctionContext /** Map of hoisted property refs: objName => [propName => true] (SsaPropOptimizer). */ public array $hoistedProps = []; + /** Map of properties that must not be hoisted: objName => [propName|'*' => true] (SsaPropOptimizer). */ + public array $unsafeObjectProps = []; + /** * @var array */ @@ -74,6 +77,7 @@ class FunctionContext $this->objectProps = []; $this->stableObjects = []; $this->hoistedProps = []; + $this->unsafeObjectProps = []; $this->staticPropRefs = []; $this->ceWrappers = []; $this->tmpVarIndex = 0; diff --git a/src/Php/Optimizer/LoopVarOptimizer.php b/src/Php/Optimizer/LoopVarOptimizer.php index 9b993e01..04d4acb1 100644 --- a/src/Php/Optimizer/LoopVarOptimizer.php +++ b/src/Php/Optimizer/LoopVarOptimizer.php @@ -22,6 +22,9 @@ trait LoopVarOptimizer 'strlen' => true, 'count' => true, 'sizeof' => true, + 'mb_strlen' => true, + 'substr_count' => true, + 'mb_substr_count' => true, ]; protected function optimizeLoopVars(): void diff --git a/src/Php/Optimizer/SsaPropOptimizer.php b/src/Php/Optimizer/SsaPropOptimizer.php index e333cfa7..192319b4 100644 --- a/src/Php/Optimizer/SsaPropOptimizer.php +++ b/src/Php/Optimizer/SsaPropOptimizer.php @@ -41,7 +41,18 @@ trait SsaPropOptimizer protected function optimizeObjectProps(): void { $ssa = $this->context->ssaBuilder; - if (!$ssa || empty($ssa->ssaVars) || !$this->nativeTypes) { + if (!$ssa || !$this->nativeTypes) { + return; + } + + if ($this->class) { + $unsafeProps = $this->collectDangerousPropOps('this_', $ssa->getStmts()); + if ($unsafeProps) { + $this->context->unsafeObjectProps['this_'] = $unsafeProps; + } + } + + if (empty($ssa->ssaVars)) { return; } @@ -72,8 +83,9 @@ trait SsaPropOptimizer continue; } - if ($this->hasDangerousPropOps($objName, $ssa->getStmts())) { - continue; + $unsafeProps = $this->collectDangerousPropOps($objName, $ssa->getStmts()); + if ($unsafeProps) { + $this->context->unsafeObjectProps[$objName] = $unsafeProps; } $this->context->stableObjects[$objName] = $className; @@ -253,149 +265,223 @@ trait SsaPropOptimizer */ protected function hasDangerousPropOps(string $objName, array $stmts): bool { + return $this->collectDangerousPropOps($objName, $stmts) !== []; + } + + /** + * @return array property name map; '*' means any property may be invalidated. + */ + protected function collectDangerousPropOps(string $objName, array $stmts): array + { + $events = []; foreach ($stmts as $stmt) { - if ($this->scanDangerousPropOp($stmt, $objName)) { - return true; - } + $this->collectPropEvents($stmt, $objName, $events); } - return false; + return $this->unsafePropsFromEvents($events); } protected function scanDangerousPropOp($stmt, string $objName): bool { - if (!$stmt instanceof Node) { - return false; + $events = []; + $this->collectPropEvents($stmt, $objName, $events); + return $this->unsafePropsFromEvents($events) !== []; + } + + /** + * @param array $events + */ + protected function collectPropEvents($node, string $objName, array &$events): void + { + if (!$node instanceof Node) { + return; } - // unset($o->prop) — typed property can't be unset in PHP 8, - // but untyped dynamic properties could be. Check anyway. - if ($stmt instanceof Node\Stmt\Unset_) { - foreach ($stmt->vars as $var) { - if ($this->isPropOfObj($var, $objName)) { - return true; + if ($node instanceof Node\Stmt\Unset_) { + foreach ($node->vars as $var) { + $propName = $this->getPropNameOfObj($var, $objName); + if ($propName !== null) { + $events[] = ['kind' => 'danger', 'prop' => $propName]; + $this->collectPropEventsInDynamicParts($var, $objName, $events); + } else { + $this->collectPropEvents($var, $objName, $events); } } + return; } - // $ref = &$o->prop — reference capture of property - if ($stmt instanceof Node\Stmt\Expression && $stmt->expr instanceof Expr\AssignRef) { - if ($this->isPropOfObj($stmt->expr->expr, $objName)) { - return true; + if ($node instanceof Expr\AssignRef) { + $leftProp = $this->getPropNameOfObj($node->var, $objName); + if ($leftProp !== null) { + // $o->prop =& $ref would parse the left property as a normal + // assignment target, so it must never use a hoisted property var. + $events[] = ['kind' => 'danger_always', 'prop' => $leftProp]; + $this->collectPropEventsInDynamicParts($node->var, $objName, $events); + } else { + $this->collectPropEvents($node->var, $objName, $events); } - } - if ($stmt instanceof Node\Stmt\Expression) { - if ($this->exprHasDangerousPropOp($stmt->expr, $objName)) { - return true; + $rightProp = $this->getPropNameOfObj($node->expr, $objName); + if ($rightProp !== null) { + // $ref = &$o->prop changes the slot to a reference. Earlier + // optimized accesses remain safe only if the property is not + // touched again afterwards. + $events[] = ['kind' => 'danger', 'prop' => $rightProp]; + $this->collectPropEventsInDynamicParts($node->expr, $objName, $events); + } else { + $this->collectPropEvents($node->expr, $objName, $events); } + return; } - if (($stmt instanceof Node\Stmt\If_ - || $stmt instanceof Node\Stmt\While_ - || $stmt instanceof Node\Stmt\Do_) - && $stmt->cond instanceof Node - && $this->exprHasDangerousPropOp($stmt->cond, $objName)) { - return true; + if ($node instanceof Expr\FuncCall + && $node->name instanceof Node\Name + && $node->name->toLowerString() === 'refval' + && !empty($node->args)) { + $propName = $this->getPropNameOfObj($node->args[0]->value, $objName); + if ($propName !== null) { + $events[] = ['kind' => 'danger', 'prop' => $propName]; + $this->collectPropEventsInDynamicParts($node->args[0]->value, $objName, $events); + return; + } } - if ($stmt instanceof Node\Stmt\For_) { - foreach ([$stmt->init, $stmt->cond, $stmt->loop] as $exprList) { - foreach ($exprList as $expr) { - if ($expr instanceof Node && $this->exprHasDangerousPropOp($expr, $objName)) { - return true; - } + if ($node instanceof Expr\FuncCall || $node instanceof Expr\MethodCall + || $node instanceof Expr\StaticCall || $node instanceof Expr\NullsafeMethodCall) { + if ($node instanceof Expr\StaticCall && $node->class instanceof Expr) { + $this->collectPropEvents($node->class, $objName, $events); + } + if ($node instanceof Expr\MethodCall || $node instanceof Expr\NullsafeMethodCall) { + $this->collectPropEvents($node->var, $objName, $events); + } + foreach ($node->args as $arg) { + $propName = $arg->byRef ? $this->getPropNameOfObj($arg->value, $objName) : null; + if ($propName !== null) { + $events[] = ['kind' => 'danger', 'prop' => $propName]; + $this->collectPropEventsInDynamicParts($arg->value, $objName, $events); + } else { + $this->collectPropEvents($arg->value, $objName, $events); + } + } + if (($node instanceof Expr\MethodCall || $node instanceof Expr\NullsafeMethodCall) + && $this->isVarNamed($node->var, $objName)) { + $events[] = ['kind' => 'danger', 'prop' => '*']; + } + foreach ($node->args as $arg) { + if ($this->exprMayExposeObject($arg->value, $objName)) { + $events[] = ['kind' => 'danger', 'prop' => '*']; } } + return; } - if ($stmt instanceof Node\Stmt\Foreach_ && $stmt->expr instanceof Node) { - if ($this->exprHasDangerousPropOp($stmt->expr, $objName)) { - return true; + if ($node instanceof Expr\Assign) { + $this->collectPropEvents($node->expr, $objName, $events); + $this->collectPropEvents($node->var, $objName, $events); + if ($this->isDynamicPropWriteOfObj($node->var, $objName)) { + $events[] = ['kind' => 'danger', 'prop' => '*']; + } + if ($this->exprMayExposeObject($node->expr, $objName)) { + $events[] = ['kind' => 'danger', 'prop' => '*']; } + return; } - if ($stmt instanceof Node\Stmt\Switch_ && $stmt->cond instanceof Node) { - if ($this->exprHasDangerousPropOp($stmt->cond, $objName)) { - return true; + if ($node instanceof Expr\AssignOp || $node instanceof Expr\PreInc || $node instanceof Expr\PreDec + || $node instanceof Expr\PostInc || $node instanceof Expr\PostDec) { + $target = $node instanceof Expr\AssignOp ? $node->var : $node->var; + if ($node instanceof Expr\AssignOp) { + $this->collectPropEvents($node->expr, $objName, $events); } + $this->collectPropEvents($target, $objName, $events); + if ($this->isDynamicPropWriteOfObj($target, $objName)) { + $events[] = ['kind' => 'danger', 'prop' => '*']; + } + return; } - if ($stmt instanceof Node\Stmt\Return_ && $stmt->expr instanceof Node) { - if ($this->exprHasDangerousPropOp($stmt->expr, $objName)) { - return true; + if ($node instanceof Expr\Closure) { + foreach ($node->uses as $use) { + if ($this->isVarNamed($use->var, $objName)) { + $events[] = ['kind' => 'danger', 'prop' => '*']; + } } } - if ($stmt instanceof Node\Stmt\Echo_) { - foreach ($stmt->exprs as $expr) { - if ($expr instanceof Node && $this->exprHasDangerousPropOp($expr, $objName)) { - return true; + $propName = $this->getPropNameOfObj($node, $objName); + if ($propName !== null && $propName !== '*') { + $events[] = ['kind' => 'access', 'prop' => $propName]; + } + + foreach ($node->getSubNodeNames() as $subNodeName) { + $subNode = $node->$subNodeName; + if ($subNode instanceof Node) { + $this->collectPropEvents($subNode, $objName, $events); + } elseif (is_array($subNode)) { + foreach ($subNode as $item) { + if ($item instanceof Node) { + $this->collectPropEvents($item, $objName, $events); + } } } } - - // Recurse into compound statements - return $this->recurseDangerousPropOp($stmt, $objName); } - protected function exprHasDangerousPropOp($expr, string $objName): bool + /** + * Dynamic property name expressions can contain normal property reads: + * unset($o->{$other->name}) should still record $other->name if relevant. + * + * @param array $events + */ + protected function collectPropEventsInDynamicParts($node, string $objName, array &$events): void { - if (!$expr instanceof Node) { - return false; + if (!$node instanceof Expr\PropertyFetch) { + return; } - - if ($expr instanceof Expr\AssignRef && $this->isPropOfObj($expr->expr, $objName)) { - return true; + if ($node->name instanceof Node) { + $this->collectPropEvents($node->name, $objName, $events); } + } - if ($expr instanceof Expr\FuncCall - && $expr->name instanceof Node\Name - && $expr->name->toLowerString() === 'refval' - && !empty($expr->args) - && $this->isPropOfObj($expr->args[0]->value, $objName)) { - return true; - } + /** + * @param array $events + * @return array + */ + protected function unsafePropsFromEvents(array $events): array + { + $liveProps = []; + $unsafeProps = []; - if ($expr instanceof Expr\FuncCall || $expr instanceof Expr\MethodCall - || $expr instanceof Expr\StaticCall || $expr instanceof Expr\NullsafeMethodCall) { - foreach ($expr->args as $arg) { - if ($arg->byRef && $this->isPropOfObj($arg->value, $objName)) { - return true; - } - if ($this->exprHasDangerousPropOp($arg->value, $objName)) { - return true; - } - } - if (($expr instanceof Expr\MethodCall || $expr instanceof Expr\NullsafeMethodCall) - && $this->exprHasDangerousPropOp($expr->var, $objName)) { - return true; - } - if ($expr instanceof Expr\StaticCall && $expr->class instanceof Expr - && $this->exprHasDangerousPropOp($expr->class, $objName)) { - return true; + for ($i = count($events) - 1; $i >= 0; $i--) { + $event = $events[$i]; + $propName = $event['prop']; + + if ($event['kind'] === 'access') { + $liveProps[$propName] = true; + continue; } - return false; - } - foreach (['left', 'right', 'expr', 'var', 'cond', 'if', 'else', 'dim', 'value'] as $prop) { - if (isset($expr->$prop) && $expr->$prop instanceof Node) { - if ($this->exprHasDangerousPropOp($expr->$prop, $objName)) { - return true; - } + if ($event['kind'] === 'danger_always') { + $unsafeProps[$propName] = true; + continue; } - } - foreach (['args', 'exprs', 'items'] as $prop) { - if (isset($expr->$prop) && is_array($expr->$prop)) { - foreach ($expr->$prop as $item) { - if ($item instanceof Node && $this->exprHasDangerousPropOp($item, $objName)) { - return true; - } + if ($propName === '*') { + foreach ($liveProps as $liveProp => $_) { + $unsafeProps[$liveProp] = true; } + } elseif (isset($liveProps[$propName])) { + $unsafeProps[$propName] = true; } } - return false; + return $unsafeProps; + } + + protected function exprHasDangerousPropOp($expr, string $objName): bool + { + $events = []; + $this->collectPropEvents($expr, $objName, $events); + return $this->unsafePropsFromEvents($events) !== []; } /** @@ -403,43 +489,81 @@ trait SsaPropOptimizer */ protected function isPropOfObj($node, string $objName): bool { - return $node instanceof Expr\PropertyFetch - && $node->var instanceof Expr\Variable - && is_string($node->var->name) - && $node->var->name === $objName; + return $this->getPropNameOfObj($node, $objName) !== null; } - protected function recurseDangerousPropOp($stmt, string $objName): bool + protected function getPropNameOfObj($node, string $objName): ?string { - if ($stmt instanceof Node\Stmt\If_) { - if ($this->hasDangerousPropOps($objName, $stmt->stmts)) return true; - foreach ($stmt->elseifs as $elseif) { - if ($this->hasDangerousPropOps($objName, $elseif->stmts)) return true; - } - if ($stmt->else && $this->hasDangerousPropOps($objName, $stmt->else->stmts)) return true; + if (!$node instanceof Expr\PropertyFetch + || !$node->var instanceof Expr\Variable + || !is_string($node->var->name) + || !$this->isVarNamed($node->var, $objName)) { + return null; } - if ($stmt instanceof Node\Stmt\While_ || $stmt instanceof Node\Stmt\Do_) { - if ($this->hasDangerousPropOps($objName, $stmt->stmts)) return true; + if ($node->name instanceof Node\Identifier) { + return $node->name->toString(); } - if ($stmt instanceof Node\Stmt\For_ || $stmt instanceof Node\Stmt\Foreach_) { - if ($this->hasDangerousPropOps($objName, $stmt->stmts)) return true; + if (is_string($node->name)) { + return $node->name; } - if ($stmt instanceof Node\Stmt\TryCatch) { - if ($this->hasDangerousPropOps($objName, $stmt->stmts)) return true; - foreach ($stmt->catches as $catch) { - if ($this->hasDangerousPropOps($objName, $catch->stmts)) return true; - } - if ($stmt->finally && $this->hasDangerousPropOps($objName, $stmt->finally->stmts)) return true; + return '*'; + } + + protected function exprMayExposeObject($node, string $objName): bool + { + if (!$node instanceof Node) { + return false; + } + + if ($this->isVarNamed($node, $objName)) { + return true; } - if ($stmt instanceof Node\Stmt\Switch_) { - foreach ($stmt->cases as $case) { - if ($this->hasDangerousPropOps($objName, $case->stmts)) return true; + if ($node instanceof Expr\PropertyFetch + && $node->var instanceof Expr\Variable + && is_string($node->var->name) + && $this->isVarNamed($node->var, $objName)) { + return false; + } + + if ($node instanceof Expr\BinaryOp || $node instanceof Expr\BooleanNot + || $node instanceof Expr\Cast || $node instanceof Expr\UnaryMinus + || $node instanceof Expr\UnaryPlus) { + return false; + } + + foreach ($node->getSubNodeNames() as $subNodeName) { + $subNode = $node->$subNodeName; + if ($subNode instanceof Node) { + if ($this->exprMayExposeObject($subNode, $objName)) { + return true; + } + } elseif (is_array($subNode)) { + foreach ($subNode as $item) { + if ($this->exprMayExposeObject($item, $objName)) { + return true; + } + } } } + return false; + } + + protected function isDynamicPropWriteOfObj($node, string $objName): bool + { + if ($node instanceof Expr\PropertyFetch + && $node->var instanceof Expr\Variable + && is_string($node->var->name) + && $this->isVarNamed($node->var, $objName)) { + return $this->getPropNameOfObj($node, $objName) === '*'; + } + + if ($node instanceof Expr\ArrayDimFetch && $node->var instanceof Node) { + return $this->isDynamicPropWriteOfObj($node->var, $objName); + } return false; } @@ -453,6 +577,49 @@ trait SsaPropOptimizer return isset($this->context->stableObjects[$objName]); } + public function canHoistStableObjectProp(string $objName, string $propName): bool + { + if (!$this->isStableObject($objName)) { + return false; + } + return $this->canHoistObjectPropBySafety($objName, $propName); + } + + public function canHoistObjectProp(string $objName, string $propName): bool + { + if ($objName !== 'this_' && !$this->isStableObject($objName)) { + return false; + } + return $this->canHoistObjectPropBySafety($objName, $propName); + } + + protected function canHoistObjectPropBySafety(string $objName, string $propName): bool + { + $unsafeProps = $this->context->unsafeObjectProps[$objName] ?? []; + return !isset($unsafeProps['*']) && !isset($unsafeProps[$propName]); + } + + /** + * @return array{type: string, kind: string} + */ + protected function getHoistedObjectPropInfo(string $declaredType): array + { + if ($declaredType === self::TYPE_INT || $declaredType === self::TYPE_FLOAT) { + return ['type' => $declaredType, 'kind' => 'zval']; + } + + return ['type' => self::TYPE_VAR, 'kind' => 'var']; + } + + protected function getZvalValueMacroForPropType(string $type): ?string + { + return match ($type) { + self::TYPE_INT => 'Z_LVAL_P', + self::TYPE_FLOAT => 'Z_DVAL_P', + default => null, + }; + } + /** * Generate the property reference declaration for a stable object. * Emits via beforeStmtLines so the reference is declared before the @@ -474,8 +641,12 @@ trait SsaPropOptimizer } $refGetter = $objName . '.attr(' . $id . ', true)'; - $zvalMacro = ($cType === 'php::Float') ? 'Z_DVAL_P' : 'Z_LVAL_P'; - $this->context->beforeStmtLines[] = $cType . ' &' . $propVar . ' = ' . $zvalMacro . '(' . $refGetter . '.unwrap_ptr());'; + $zvalMacro = $this->getZvalValueMacroForPropType($cType); + if ($zvalMacro !== null) { + $this->context->beforeStmtLines[] = $cType . ' &' . $propVar . ' = ' . $zvalMacro . '(' . $refGetter . '.unwrap_ptr());'; + } else { + $this->context->beforeStmtLines[] = self::TYPE_VAR . ' ' . $propVar . ' = ' . $refGetter . ';'; + } $this->context->hoistedProps[$objName][$propName] = true; return $propVar; diff --git a/src/Php/Optimizer/SsaTypeOptimizer.php b/src/Php/Optimizer/SsaTypeOptimizer.php index bcea8107..f247c823 100644 --- a/src/Php/Optimizer/SsaTypeOptimizer.php +++ b/src/Php/Optimizer/SsaTypeOptimizer.php @@ -580,7 +580,7 @@ trait SsaTypeOptimizer { return $node instanceof Node\Expr\Variable && is_string($node->name) - && $node->name === $varName; + && ($node->name === $varName || $this->escapeVarName($node->name) === $varName); } protected function hasDangerousFloatOps(string $varName, array $stmts): bool diff --git a/tests/aot/optimizations/objprop-hoist-array-var-ref.phpt b/tests/aot/optimizations/objprop-hoist-array-var-ref.phpt new file mode 100644 index 00000000..9388a125 --- /dev/null +++ b/tests/aot/optimizations/objprop-hoist-array-var-ref.phpt @@ -0,0 +1,26 @@ +--TEST-- +SSA object prop: hoist array property through indirect Var handle +--FILE-- +items[] = 'a'; + $foo->items[] = 'b'; + $foo->items[1] = 'c'; + + var_dump($foo->items); +} +?> +--EXPECT-- +array(2) { + [0]=> + string(1) "a" + [1]=> + string(1) "c" +} diff --git a/tests/aot/optimizations/objprop-hoist-object-arg-escape.phpt b/tests/aot/optimizations/objprop-hoist-object-arg-escape.phpt new file mode 100644 index 00000000..b57026c4 --- /dev/null +++ b/tests/aot/optimizations/objprop-hoist-object-arg-escape.phpt @@ -0,0 +1,27 @@ +--TEST-- +SSA object prop: object argument escape prevents property hoisting +--FILE-- +a; + $ref = 99; +} + +function main(): void { + $o = new Foo(); + $o->a = 1; + + make_ref($o); + $o->a += 1; + + var_dump($o->a); +} +?> +--EXPECT-- +int(100) diff --git a/tests/aot/optimizations/objprop-unset-this-typed.phpt b/tests/aot/optimizations/objprop-unset-this-typed.phpt new file mode 100644 index 00000000..02ead36c --- /dev/null +++ b/tests/aot/optimizations/objprop-unset-this-typed.phpt @@ -0,0 +1,27 @@ +--TEST-- +SSA object prop: unset typed this property keeps PHP uninitialized semantics +--FILE-- +a)); + unset($this->a); + var_dump(isset($this->a)); + $this->a = 11; + var_dump($this->a); + } +} + +function main(): void { + $foo = new Foo(); + $foo->run(); +} +?> +--EXPECT-- +bool(true) +bool(false) +int(11)