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'); $objVar = new Expr\Variable('obj');
$propFetch = new Expr\PropertyFetch($objVar, 'prop'); $propFetch = new Expr\PropertyFetch($objVar, 'prop');
$unset = new Stmt\Unset_([$propFetch]); $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]); $result = $this->invoke('hasDangerousPropOps', 'obj', [$unset, $read]);
$this->assertTrue($result, 'unset($obj->prop) should be detected'); $this->assertTrue($result, 'unset($obj->prop) before a later access should be detected');
} }
public function testHasDangerousPropOpsUnsetDifferentObj(): void public function testHasDangerousPropOpsUnsetDifferentObj(): void
@ -663,9 +667,23 @@ class SsaAnalysisTest extends TestCase
$refVar = new Expr\Variable('ref'); $refVar = new Expr\Variable('ref');
$assignRef = new Expr\AssignRef($refVar, $propFetch); $assignRef = new Expr\AssignRef($refVar, $propFetch);
$stmt = new Stmt\Expression($assignRef); $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]); $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 public function testHasDangerousPropOpsByRefArg(): void
@ -675,9 +693,13 @@ class SsaAnalysisTest extends TestCase
$arg = new Arg($propFetch, true); // byRef = true $arg = new Arg($propFetch, true); // byRef = true
$funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [$arg]); $funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [$arg]);
$stmt = new Stmt\Expression($funcCall); $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]); $result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt, $read]);
$this->assertTrue($result, 'func(&$obj->prop) should be detected'); $this->assertTrue($result, 'func(&$obj->prop) before a later access should be detected');
} }
public function testHasDangerousPropOpsRefval(): void public function testHasDangerousPropOpsRefval(): void
@ -689,9 +711,13 @@ class SsaAnalysisTest extends TestCase
$arg = new Arg($refvalCall); $arg = new Arg($refvalCall);
$funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [$arg]); $funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [$arg]);
$stmt = new Stmt\Expression($funcCall); $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]); $result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt, $read]);
$this->assertTrue($result, 'func(refval($obj->prop)) should be detected'); $this->assertTrue($result, 'func(refval($obj->prop)) before a later access should be detected');
} }
public function testHasDangerousPropOpsClean(): void public function testHasDangerousPropOpsClean(): void
@ -715,9 +741,13 @@ class SsaAnalysisTest extends TestCase
'elseifs' => [], 'elseifs' => [],
'else' => null, '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]); $result = $this->invoke('hasDangerousPropOps', 'obj', [$ifStmt, $read]);
$this->assertTrue($result, 'unset inside if should be detected'); $this->assertTrue($result, 'unset inside if before a later access should be detected');
} }
public function testHasDangerousPropOpsNestedRefvalInAssignment(): void public function testHasDangerousPropOpsNestedRefvalInAssignment(): void
@ -726,9 +756,13 @@ class SsaAnalysisTest extends TestCase
$refvalCall = new Expr\FuncCall(new Node\Name('refval'), [new Arg($propFetch)]); $refvalCall = new Expr\FuncCall(new Node\Name('refval'), [new Arg($propFetch)]);
$funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [new Arg($refvalCall)]); $funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [new Arg($refvalCall)]);
$stmt = new Stmt\Expression(new Expr\Assign(new Expr\Variable('result'), $funcCall)); $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]); $result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt, $read]);
$this->assertTrue($result, 'refval($obj->prop) nested in an assignment RHS should be detected'); $this->assertTrue($result, 'refval($obj->prop) nested in an assignment RHS before a later access should be detected');
} }
public function testHasDangerousPropOpsNestedByRefInReturn(): void public function testHasDangerousPropOpsNestedByRefInReturn(): void
@ -736,9 +770,111 @@ class SsaAnalysisTest extends TestCase
$propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop'); $propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), 'prop');
$funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [new Arg($propFetch, true)]); $funcCall = new Expr\FuncCall(new Node\Name('someFunc'), [new Arg($propFetch, true)]);
$stmt = new Stmt\Return_($funcCall); $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]); $result = $this->invoke('hasDangerousPropOps', 'obj', [$stmt, $read]);
$this->assertTrue($result, 'By-ref property argument nested in return should be detected'); $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')); $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 // LoopVarOptimizer: range-proven counters
// ======================================================================== // ========================================================================

@ -3431,14 +3431,6 @@ class CompilerBase extends \PhpAot\Core\Translator
} elseif ($this->isPropertyFetch($var)) { } elseif ($this->isPropertyFetch($var)) {
$object = $this->parseIdentifier($var->var); $object = $this->parseIdentifier($var->var);
$lines[] = $object . '.unsetProperty(' . $this->identifierToStr($var->name, literal: true) . ');'; $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)) { } elseif ($this->isStaticPropertyFetch($var)) {
$this->fatalError($var, 'Attempt to unset static property ' . $this->parseIdentifier($var->class) . '::$' . $this->parseIdentifier($var->name)); $this->fatalError($var, 'Attempt to unset static property ' . $this->parseIdentifier($var->class) . '::$' . $this->parseIdentifier($var->name));
} elseif ($this->isVarExpr($var)) { } elseif ($this->isVarExpr($var)) {
@ -3496,24 +3488,26 @@ class CompilerBase extends \PhpAot\Core\Translator
* @var PropertyDef $def * @var PropertyDef $def
*/ */
$def = $expr->getAttribute('nativePropertyDef'); $def = $expr->getAttribute('nativePropertyDef');
if ($def->type === self::TYPE_INT or $def->type === self::TYPE_FLOAT) { $propName = $this->parseIdentifier($property);
$propVar = $this->getObjectPropVarName($objectVar, $this->parseIdentifier($property)); $propVar = $this->getObjectPropVarName($objectVar, $propName);
if ($objectVar === 'this_') { if ($objectVar === 'this_') {
if ($this->canHoistObjectProp($objectVar, $propName)) {
if (!$this->hasObjectPropVar($propVar)) { if (!$this->hasObjectPropVar($propVar)) {
$info = $this->getHoistedObjectPropInfo($def->type);
$this->context->objectProps[$propVar] = [ $this->context->objectProps[$propVar] = [
'type' => $def->type, 'type' => $info['type'],
'getter' => $getProperty, 'getter' => $getProperty,
'kind' => $info['kind'],
]; ];
} }
$expr->setAttribute('nativePropertyVar', $propVar); $expr->setAttribute('nativePropertyVar', $propVar);
return $propVar; return $propVar;
} }
} elseif ($this->canHoistStableObjectProp($objectVar, $propName)) {
// SSA-stable object: lazily create reference at first access point // SSA-stable object: lazily create reference at first access point
if ($this->isStableObject($objectVar)) { $result = $this->hoistStableObjectProp($objectVar, $propName, $id, $def->type);
$result = $this->hoistStableObjectProp($objectVar, $this->parseIdentifier($property), $id, $def->type); $expr->setAttribute('nativePropertyVar', $result);
$expr->setAttribute('nativePropertyVar', $result); return $result;
return $result;
}
} }
} }
return $getProperty; return $getProperty;
@ -5094,7 +5088,12 @@ class CompilerBase extends \PhpAot\Core\Translator
$code .= $this->getIndent() . self::TYPE_VAR . ' &' . $name . ' = ' . $this->escapeGlobalVar($name) . ';' . PHP_EOL; $code .= $this->getIndent() . self::TYPE_VAR . ' &' . $name . ' = ' . $this->escapeGlobalVar($name) . ';' . PHP_EOL;
} }
foreach ($this->context->objectProps as $name => $info) { 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) { foreach ($this->context->staticPropRefs as $name => $info) {
$getter = Symbol::getStaticProperty() . '(' . $info['classPtr'] . ', ' . $info['offsetExpr'] . ')'; $getter = Symbol::getStaticProperty() . '(' . $info['classPtr'] . ', ' . $info['offsetExpr'] . ')';

@ -21,6 +21,9 @@ class FunctionContext
/** Map of hoisted property refs: objName => [propName => true] (SsaPropOptimizer). */ /** Map of hoisted property refs: objName => [propName => true] (SsaPropOptimizer). */
public array $hoistedProps = []; public array $hoistedProps = [];
/** Map of properties that must not be hoisted: objName => [propName|'*' => true] (SsaPropOptimizer). */
public array $unsafeObjectProps = [];
/** /**
* @var array<string, string> * @var array<string, string>
*/ */
@ -74,6 +77,7 @@ class FunctionContext
$this->objectProps = []; $this->objectProps = [];
$this->stableObjects = []; $this->stableObjects = [];
$this->hoistedProps = []; $this->hoistedProps = [];
$this->unsafeObjectProps = [];
$this->staticPropRefs = []; $this->staticPropRefs = [];
$this->ceWrappers = []; $this->ceWrappers = [];
$this->tmpVarIndex = 0; $this->tmpVarIndex = 0;

@ -22,6 +22,9 @@ trait LoopVarOptimizer
'strlen' => true, 'strlen' => true,
'count' => true, 'count' => true,
'sizeof' => true, 'sizeof' => true,
'mb_strlen' => true,
'substr_count' => true,
'mb_substr_count' => true,
]; ];
protected function optimizeLoopVars(): void protected function optimizeLoopVars(): void

@ -41,7 +41,18 @@ trait SsaPropOptimizer
protected function optimizeObjectProps(): void protected function optimizeObjectProps(): void
{ {
$ssa = $this->context->ssaBuilder; $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; return;
} }
@ -72,8 +83,9 @@ trait SsaPropOptimizer
continue; continue;
} }
if ($this->hasDangerousPropOps($objName, $ssa->getStmts())) { $unsafeProps = $this->collectDangerousPropOps($objName, $ssa->getStmts());
continue; if ($unsafeProps) {
$this->context->unsafeObjectProps[$objName] = $unsafeProps;
} }
$this->context->stableObjects[$objName] = $className; $this->context->stableObjects[$objName] = $className;
@ -253,149 +265,223 @@ trait SsaPropOptimizer
*/ */
protected function hasDangerousPropOps(string $objName, array $stmts): bool 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) { foreach ($stmts as $stmt) {
if ($this->scanDangerousPropOp($stmt, $objName)) { $this->collectPropEvents($stmt, $objName, $events);
return true;
}
} }
return false; return $this->unsafePropsFromEvents($events);
} }
protected function scanDangerousPropOp($stmt, string $objName): bool protected function scanDangerousPropOp($stmt, string $objName): bool
{ {
if (!$stmt instanceof Node) { $events = [];
return false; $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, if ($node instanceof Node\Stmt\Unset_) {
// but untyped dynamic properties could be. Check anyway. foreach ($node->vars as $var) {
if ($stmt instanceof Node\Stmt\Unset_) { $propName = $this->getPropNameOfObj($var, $objName);
foreach ($stmt->vars as $var) { if ($propName !== null) {
if ($this->isPropOfObj($var, $objName)) { $events[] = ['kind' => 'danger', 'prop' => $propName];
return true; $this->collectPropEventsInDynamicParts($var, $objName, $events);
} else {
$this->collectPropEvents($var, $objName, $events);
} }
} }
return;
} }
// $ref = &$o->prop — reference capture of property if ($node instanceof Expr\AssignRef) {
if ($stmt instanceof Node\Stmt\Expression && $stmt->expr instanceof Expr\AssignRef) { $leftProp = $this->getPropNameOfObj($node->var, $objName);
if ($this->isPropOfObj($stmt->expr->expr, $objName)) { if ($leftProp !== null) {
return true; // $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) { $rightProp = $this->getPropNameOfObj($node->expr, $objName);
if ($this->exprHasDangerousPropOp($stmt->expr, $objName)) { if ($rightProp !== null) {
return true; // $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_ if ($node instanceof Expr\FuncCall
|| $stmt instanceof Node\Stmt\While_ && $node->name instanceof Node\Name
|| $stmt instanceof Node\Stmt\Do_) && $node->name->toLowerString() === 'refval'
&& $stmt->cond instanceof Node && !empty($node->args)) {
&& $this->exprHasDangerousPropOp($stmt->cond, $objName)) { $propName = $this->getPropNameOfObj($node->args[0]->value, $objName);
return true; if ($propName !== null) {
$events[] = ['kind' => 'danger', 'prop' => $propName];
$this->collectPropEventsInDynamicParts($node->args[0]->value, $objName, $events);
return;
}
} }
if ($stmt instanceof Node\Stmt\For_) { if ($node instanceof Expr\FuncCall || $node instanceof Expr\MethodCall
foreach ([$stmt->init, $stmt->cond, $stmt->loop] as $exprList) { || $node instanceof Expr\StaticCall || $node instanceof Expr\NullsafeMethodCall) {
foreach ($exprList as $expr) { if ($node instanceof Expr\StaticCall && $node->class instanceof Expr) {
if ($expr instanceof Node && $this->exprHasDangerousPropOp($expr, $objName)) { $this->collectPropEvents($node->class, $objName, $events);
return true; }
} 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 ($node instanceof Expr\Assign) {
if ($this->exprHasDangerousPropOp($stmt->expr, $objName)) { $this->collectPropEvents($node->expr, $objName, $events);
return true; $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 ($node instanceof Expr\AssignOp || $node instanceof Expr\PreInc || $node instanceof Expr\PreDec
if ($this->exprHasDangerousPropOp($stmt->cond, $objName)) { || $node instanceof Expr\PostInc || $node instanceof Expr\PostDec) {
return true; $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 ($node instanceof Expr\Closure) {
if ($this->exprHasDangerousPropOp($stmt->expr, $objName)) { foreach ($node->uses as $use) {
return true; if ($this->isVarNamed($use->var, $objName)) {
$events[] = ['kind' => 'danger', 'prop' => '*'];
}
} }
} }
if ($stmt instanceof Node\Stmt\Echo_) { $propName = $this->getPropNameOfObj($node, $objName);
foreach ($stmt->exprs as $expr) { if ($propName !== null && $propName !== '*') {
if ($expr instanceof Node && $this->exprHasDangerousPropOp($expr, $objName)) { $events[] = ['kind' => 'access', 'prop' => $propName];
return true; }
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) { if (!$node instanceof Expr\PropertyFetch) {
return false; return;
} }
if ($node->name instanceof Node) {
if ($expr instanceof Expr\AssignRef && $this->isPropOfObj($expr->expr, $objName)) { $this->collectPropEvents($node->name, $objName, $events);
return true;
} }
}
if ($expr instanceof Expr\FuncCall /**
&& $expr->name instanceof Node\Name * @param array<int, array{kind: string, prop: string}> $events
&& $expr->name->toLowerString() === 'refval' * @return array<string, bool>
&& !empty($expr->args) */
&& $this->isPropOfObj($expr->args[0]->value, $objName)) { protected function unsafePropsFromEvents(array $events): array
return true; {
} $liveProps = [];
$unsafeProps = [];
if ($expr instanceof Expr\FuncCall || $expr instanceof Expr\MethodCall for ($i = count($events) - 1; $i >= 0; $i--) {
|| $expr instanceof Expr\StaticCall || $expr instanceof Expr\NullsafeMethodCall) { $event = $events[$i];
foreach ($expr->args as $arg) { $propName = $event['prop'];
if ($arg->byRef && $this->isPropOfObj($arg->value, $objName)) {
return true; if ($event['kind'] === 'access') {
} $liveProps[$propName] = true;
if ($this->exprHasDangerousPropOp($arg->value, $objName)) { continue;
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;
} }
return false;
}
foreach (['left', 'right', 'expr', 'var', 'cond', 'if', 'else', 'dim', 'value'] as $prop) { if ($event['kind'] === 'danger_always') {
if (isset($expr->$prop) && $expr->$prop instanceof Node) { $unsafeProps[$propName] = true;
if ($this->exprHasDangerousPropOp($expr->$prop, $objName)) { continue;
return true;
}
} }
}
foreach (['args', 'exprs', 'items'] as $prop) { if ($propName === '*') {
if (isset($expr->$prop) && is_array($expr->$prop)) { foreach ($liveProps as $liveProp => $_) {
foreach ($expr->$prop as $item) { $unsafeProps[$liveProp] = true;
if ($item instanceof Node && $this->exprHasDangerousPropOp($item, $objName)) {
return 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 protected function isPropOfObj($node, string $objName): bool
{ {
return $node instanceof Expr\PropertyFetch return $this->getPropNameOfObj($node, $objName) !== null;
&& $node->var instanceof Expr\Variable
&& is_string($node->var->name)
&& $node->var->name === $objName;
} }
protected function recurseDangerousPropOp($stmt, string $objName): bool protected function getPropNameOfObj($node, string $objName): ?string
{ {
if ($stmt instanceof Node\Stmt\If_) { if (!$node instanceof Expr\PropertyFetch
if ($this->hasDangerousPropOps($objName, $stmt->stmts)) return true; || !$node->var instanceof Expr\Variable
foreach ($stmt->elseifs as $elseif) { || !is_string($node->var->name)
if ($this->hasDangerousPropOps($objName, $elseif->stmts)) return true; || !$this->isVarNamed($node->var, $objName)) {
} return null;
if ($stmt->else && $this->hasDangerousPropOps($objName, $stmt->else->stmts)) return true;
} }
if ($stmt instanceof Node\Stmt\While_ || $stmt instanceof Node\Stmt\Do_) { if ($node->name instanceof Node\Identifier) {
if ($this->hasDangerousPropOps($objName, $stmt->stmts)) return true; return $node->name->toString();
} }
if ($stmt instanceof Node\Stmt\For_ || $stmt instanceof Node\Stmt\Foreach_) { if (is_string($node->name)) {
if ($this->hasDangerousPropOps($objName, $stmt->stmts)) return true; return $node->name;
} }
if ($stmt instanceof Node\Stmt\TryCatch) { return '*';
if ($this->hasDangerousPropOps($objName, $stmt->stmts)) return true; }
foreach ($stmt->catches as $catch) {
if ($this->hasDangerousPropOps($objName, $catch->stmts)) return true; protected function exprMayExposeObject($node, string $objName): bool
} {
if ($stmt->finally && $this->hasDangerousPropOps($objName, $stmt->finally->stmts)) return true; if (!$node instanceof Node) {
return false;
}
if ($this->isVarNamed($node, $objName)) {
return true;
} }
if ($stmt instanceof Node\Stmt\Switch_) { if ($node instanceof Expr\PropertyFetch
foreach ($stmt->cases as $case) { && $node->var instanceof Expr\Variable
if ($this->hasDangerousPropOps($objName, $case->stmts)) return true; && 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; return false;
} }
@ -453,6 +577,49 @@ trait SsaPropOptimizer
return isset($this->context->stableObjects[$objName]); 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. * Generate the property reference declaration for a stable object.
* Emits via beforeStmtLines so the reference is declared before the * Emits via beforeStmtLines so the reference is declared before the
@ -474,8 +641,12 @@ trait SsaPropOptimizer
} }
$refGetter = $objName . '.attr(' . $id . ', true)'; $refGetter = $objName . '.attr(' . $id . ', true)';
$zvalMacro = ($cType === 'php::Float') ? 'Z_DVAL_P' : 'Z_LVAL_P'; $zvalMacro = $this->getZvalValueMacroForPropType($cType);
$this->context->beforeStmtLines[] = $cType . ' &' . $propVar . ' = ' . $zvalMacro . '(' . $refGetter . '.unwrap_ptr());'; 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; $this->context->hoistedProps[$objName][$propName] = true;
return $propVar; return $propVar;

@ -580,7 +580,7 @@ trait SsaTypeOptimizer
{ {
return $node instanceof Node\Expr\Variable return $node instanceof Node\Expr\Variable
&& is_string($node->name) && is_string($node->name)
&& $node->name === $varName; && ($node->name === $varName || $this->escapeVarName($node->name) === $varName);
} }
protected function hasDangerousFloatOps(string $varName, array $stmts): bool 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