refactor(compiler): 优化对象属性提升逻辑并增强危险操作检测

- 移除对this_对象属性unset操作的特殊警告处理
- 重构对象属性提升逻辑,支持更多类型的安全提升判断
- 添加unsafeObjectProps上下文跟踪以标记不可安全提升的属性
- 扩展危险属性操作检测功能,支持动态属性名和引用操作检测
- 优化循环变量分析器,增加对字符串函数的支持
- 添加多项单元测试验证属性提升和危险操作检测的准确性
- 改进代码生成,支持不同类型的属性值宏展开
pull/1/head
韩天峰 3 months ago
parent dbaa5a10e0
commit 21338f022f
  1. 181
      phpunit/src/SsaAnalysisTest.php
  2. 35
      src/Php/CompilerBase.php
  3. 4
      src/Php/Context/FunctionContext.php
  4. 3
      src/Php/Optimizer/LoopVarOptimizer.php
  5. 415
      src/Php/Optimizer/SsaPropOptimizer.php
  6. 2
      src/Php/Optimizer/SsaTypeOptimizer.php
  7. 26
      tests/aot/optimizations/objprop-hoist-array-var-ref.phpt
  8. 27
      tests/aot/optimizations/objprop-hoist-object-arg-escape.phpt
  9. 27
      tests/aot/optimizations/objprop-unset-this-typed.phpt

@ -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
// ========================================================================

@ -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'] . ')';

@ -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<string, string>
*/
@ -74,6 +77,7 @@ class FunctionContext
$this->objectProps = [];
$this->stableObjects = [];
$this->hoistedProps = [];
$this->unsafeObjectProps = [];
$this->staticPropRefs = [];
$this->ceWrappers = [];
$this->tmpVarIndex = 0;

@ -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

@ -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<string, bool> 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<int, array{kind: string, prop: string}> $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<int, array{kind: string, prop: string}> $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<int, array{kind: string, prop: string}> $events
* @return array<string, bool>
*/
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;

@ -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

@ -0,0 +1,26 @@
--TEST--
SSA object prop: hoist array property through indirect Var handle
--FILE--
<?php
use native_types;
class Foo {
public array $items = [];
}
function main(): void {
$foo = new Foo();
$foo->items[] = 'a';
$foo->items[] = 'b';
$foo->items[1] = 'c';
var_dump($foo->items);
}
?>
--EXPECT--
array(2) {
[0]=>
string(1) "a"
[1]=>
string(1) "c"
}

@ -0,0 +1,27 @@
--TEST--
SSA object prop: object argument escape prevents property hoisting
--FILE--
<?php
use native_types;
class Foo {
public int $a;
}
function make_ref(Foo $o): void {
$ref =& $o->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)

@ -0,0 +1,27 @@
--TEST--
SSA object prop: unset typed this property keeps PHP uninitialized semantics
--FILE--
<?php
use native_types;
class Foo {
public int $a = 7;
public function run(): void {
var_dump(isset($this->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)
Loading…
Cancel
Save