refactor(context): remove runtime checked objects tracking mechanism

- Removed runtimeCheckedObjects array from FunctionContext
- Eliminated requiresRuntimeObjectCheck method and related logic
- Updated native method call resolution to skip runtime checks
- Removed object validation before native method calls
- Cleaned up type compatibility checks for object expressions
- Removed property slot validation after unset operations
- Updated SSA analysis to handle object unset scenarios differently
- Modified translator to stop marking nullable objects for runtime checks
- Removed typeCheckAllowsNull helper method
- Updated tests to reflect new object handling behavior without runtime checks
pull/47/head
韩天峰 4 weeks ago
parent 70927ee68a
commit cb03570212
  1. 27
      phpunit/code/native-method-unset-keeps-optimization.php
  2. 14
      phpunit/src/NativePropertyTest.php
  3. 24
      phpunit/src/SsaAnalysisTest.php
  4. 10
      src/Context/FunctionContext.php
  5. 5
      src/Generator/TypeCheckGenerator.php
  6. 14
      src/Optimizer/SsaPropOptimizer.php
  7. 4
      src/Parser/MethodCallTrait.php
  8. 3
      src/Parser/PropertyAccessTrait.php
  9. 8
      src/Parser/TypeDetectionTrait.php
  10. 3
      src/Translator.php
  11. 4
      src/TypeSystem/NativeTypeCompatibilityTrait.php
  12. 27
      tests/compiler/basic/unset-typed-object-state.phpt

@ -0,0 +1,27 @@
<?php
use native_types;
class NativeMethodUnsetKeepsOptimization
{
public int $value = 7;
public function read(): int
{
return $this->value;
}
}
function nativeMethodUnsetKeepsOptimization(int $branch): int
{
$object = new NativeMethodUnsetKeepsOptimization();
if ($branch === 1) {
unset($object);
} elseif ($branch === 2) {
var_dump($object->value);
} else {
var_dump($object->value);
}
return $object->read();
}

@ -113,6 +113,20 @@ class NativePropertyTest extends \BaseTest
$this->assertStringContainsString('this_.attr(', $code); $this->assertStringContainsString('this_.attr(', $code);
} }
public function testUnsetObjectDisablesPropertySlotsButKeepsNativeMethodCall(): void
{
try {
$outputFile = $this->compileNativeProperty('native-method-unset-keeps-optimization.php');
} catch (TestError $e) {
$this->fail($e->getMessage());
}
$code = file_get_contents($outputFile);
$this->assertStringNotContainsString('_object_prop_object__value', $code);
$this->assertStringContainsString('object.attr(', $code);
$this->assertStringContainsString('php_nativemethodunsetkeepsoptimization__read(object)', $code);
}
public function testNativePropertyStaticScalarTypeMismatchFailsAtCompileTime(): void public function testNativePropertyStaticScalarTypeMismatchFailsAtCompileTime(): void
{ {
$this->exec( $this->exec(

@ -668,6 +668,30 @@ class SsaAnalysisTest extends TestCase
$this->assertFalse($result, 'unset on different object should not match'); $this->assertFalse($result, 'unset on different object should not match');
} }
public function testHasDangerousPropOpsUnsetObject(): void
{
$unset = new Stmt\Unset_([new Expr\Variable('obj')]);
$result = $this->invoke('collectDangerousPropOps', 'obj', [$unset]);
$this->assertSame(['*' => true], $result, 'unset($obj) must disable every property slot');
}
public function testUnsetObjectInElseifDisablesSlotsForAllBranches(): void
{
$if = new Stmt\If_(new Expr\Variable('first'), [
'stmts' => [new Stmt\Expression(new Expr\PropertyFetch(new Expr\Variable('obj'), 'a'))],
'elseifs' => [new Stmt\ElseIf_(new Expr\Variable('second'), [
new Stmt\Unset_([new Expr\Variable('obj')]),
])],
'else' => new Stmt\Else_([
new Stmt\Expression(new Expr\PropertyFetch(new Expr\Variable('obj'), 'b')),
]),
]);
$result = $this->invoke('collectDangerousPropOps', 'obj', [$if]);
$this->assertSame(['*' => true], $result);
}
public function testHasDangerousPropOpsAssignRef(): void public function testHasDangerousPropOpsAssignRef(): void
{ {
$objVar = new Expr\Variable('obj'); $objVar = new Expr\Variable('obj');

@ -29,15 +29,6 @@ class FunctionContext
*/ */
public array $objects = []; public array $objects = [];
/**
* Object variables that require a runtime value check before use. This
* includes nullable declarations and typed objects cleared by unset().
* Their class constraint remains in objects/declaredObjects.
*
* @var array<string, bool>
*/
public array $runtimeCheckedObjects = [];
/** /**
* Declared object constraints that are not used for native-call dispatch. * Declared object constraints that are not used for native-call dispatch.
* *
@ -95,7 +86,6 @@ class FunctionContext
$this->staticVars = []; $this->staticVars = [];
$this->arguments = []; $this->arguments = [];
$this->objects = []; $this->objects = [];
$this->runtimeCheckedObjects = [];
$this->declaredObjects = []; $this->declaredObjects = [];
$this->stdArrays = []; $this->stdArrays = [];
$this->stdContainers = []; $this->stdContainers = [];

@ -273,11 +273,6 @@ trait TypeCheckGenerator
&& !$this->compositeTypeContainsKind($typeCheck, 'isInt'); && !$this->compositeTypeContainsKind($typeCheck, 'isInt');
} }
protected function typeCheckAllowsNull(array $typeCheck): bool
{
return $this->compositeTypeContainsKind($typeCheck, 'isNull');
}
private function compositeTypeContainsKind(array $typeCheck, string $kind): bool private function compositeTypeContainsKind(array $typeCheck, string $kind): bool
{ {
foreach ($typeCheck as $entry) { foreach ($typeCheck as $entry) {

@ -19,8 +19,9 @@
* 8. First access is not inside a loop or nested block scope * 8. First access is not inside a loop or nested block scope
* 9. Property is not readonly (including properties of a readonly class) * 9. Property is not readonly (including properties of a readonly class)
* *
* Direct unset($o->prop) invalidates the property slot assumption. Any property * Direct unset($o->prop) invalidates that property slot; unset($o) invalidates
* observed in an unset operation must use dynamic attr() access. * every property slot associated with the object. These properties must use
* dynamic attr() access for the whole function, independent of branch order.
*/ */
namespace TypePhp\Optimizer; namespace TypePhp\Optimizer;
@ -361,8 +362,8 @@ trait SsaPropOptimizer
* - mutate($o) or $o->method() — dynamic code may turn the property slot * - mutate($o) or $o->method() — dynamic code may turn the property slot
* into a reference through the exposed object * into a reference through the exposed object
* *
* Direct unset($o->prop) invalidates the property slot and disables * Direct unset($o->prop) disables hoisting for that property. unset($o)
* hoisting for that property. * disables all property hoisting for the object.
*/ */
protected function hasDangerousPropOps(string $objName, array $stmts): bool protected function hasDangerousPropOps(string $objName, array $stmts): bool
{ {
@ -412,6 +413,11 @@ trait SsaPropOptimizer
if ($propName !== null) { if ($propName !== null) {
$events[] = ['kind' => 'danger_always', 'prop' => $propName]; $events[] = ['kind' => 'danger_always', 'prop' => $propName];
$this->collectPropEventsInDynamicParts($var, $objName, $events); $this->collectPropEventsInDynamicParts($var, $objName, $events);
} elseif ($this->isVarNamedAny($var, $objName)) {
// Object lifetime is no longer continuous. A reference to
// any property slot could outlive the object in one branch,
// so disable slot hoisting for every branch of the function.
$events[] = ['kind' => 'danger_always', 'prop' => '*'];
} else { } else {
$this->collectPropEvents($var, $objName, $events); $this->collectPropEvents($var, $objName, $events);
} }

@ -120,10 +120,6 @@ trait MethodCallTrait
protected function findNativeMethod(CallLike $expr, string $object, string $method): string|false protected function findNativeMethod(CallLike $expr, string $object, string $method): string|false
{ {
if ($object !== 'this_' && $this->requiresRuntimeObjectCheck($object)) {
return false;
}
$classDef = null; $classDef = null;
if ($object === 'this_') { if ($object === 'this_') {
$class = $this->getFullClassName(); $class = $this->getFullClassName();

@ -774,9 +774,6 @@ trait PropertyAccessTrait
// //
// Keep the declared class: unset() changes only the value // Keep the declared class: unset() changes only the value
// state and does not make null or another class assignable. // state and does not make null or another class assignable.
// Native operations must validate the current value before
// using the retained class declaration.
$this->context->runtimeCheckedObjects[$name] = true;
$lines[] = "{$name} = php::null;"; $lines[] = "{$name} = php::null;";
} else { } else {
$lines[] = "{$name}.unset();"; $lines[] = "{$name}.unset();";

@ -19,13 +19,7 @@ trait TypeDetectionTrait
{ {
public function isTypedObject(string $object): bool public function isTypedObject(string $object): bool
{ {
return !isset($this->context->runtimeCheckedObjects[$object]) return isset($this->context->objects[$object]) || isset($this->context->stableObjects[$object]);
&& (isset($this->context->objects[$object]) || isset($this->context->stableObjects[$object]));
}
protected function requiresRuntimeObjectCheck(string $object): bool
{
return isset($this->context->runtimeCheckedObjects[$object]);
} }
protected function isSuperGlobal(string $var): bool protected function isSuperGlobal(string $var): bool

@ -3603,9 +3603,6 @@ CODE;
$this->addArgument($argInfo->name, $argInfo->variadic ? Type::ARRAY : $argInfo->type); $this->addArgument($argInfo->name, $argInfo->variadic ? Type::ARRAY : $argInfo->type);
if (!$argInfo->variadic and $argInfo->declaredClass) { if (!$argInfo->variadic and $argInfo->declaredClass) {
$this->addObject($argInfo->name, $argInfo->declaredClass); $this->addObject($argInfo->name, $argInfo->declaredClass);
if ($argInfo->nullable || $this->typeCheckAllowsNull($argInfo->typeCheck ?? [])) {
$this->context->runtimeCheckedObjects[$argInfo->name] = true;
}
} }
} }

@ -223,10 +223,6 @@ trait NativeTypeCompatibilityTrait
// 如果无法证明,但右值是已知 concrete object,说明一定不兼容,直接编译期 fatal; // 如果无法证明,但右值是已知 concrete object,说明一定不兼容,直接编译期 fatal;
// 其他动态/外部库/any 场景保留 php::toObject() 作为运行时兜底。 // 其他动态/外部库/any 场景保留 php::toObject() 作为运行时兜底。
if ($this->isObjectClassStaticallyAssignableTo($class, $declaredClass)) { if ($this->isObjectClassStaticallyAssignableTo($class, $declaredClass)) {
if ($this->isVarExpr($arg->value)
&& $this->requiresRuntimeObjectCheck($this->parseIdentifier($arg->value))) {
return $this->convertObjectExpr($expr, $this->getClassEntryPtr($declaredClass));
}
return $type === Type::OBJECT ? $expr : $this->convertObjectExpr($expr); return $type === Type::OBJECT ? $expr : $this->convertObjectExpr($expr);
} }
if ($this->isKnownConcreteObjectExpr($arg->value, $class)) { if ($this->isKnownConcreteObjectExpr($arg->value, $class)) {

@ -1,13 +1,20 @@
--TEST-- --TEST--
unset typed object invalidates native-call assumptions and reads as null unset typed object reads as null and accepts a valid reassignment
--FILE-- --FILE--
<?php <?php
class UnsetTypedObjectValue class UnsetTypedObjectValue
{ {
public int $number = 1;
public function value(): string public function value(): string
{ {
return 'value'; return 'value';
} }
public function readProperty(): int
{
return $this->number;
}
} }
function makeUnsetTypedObjectValue(): UnsetTypedObjectValue function makeUnsetTypedObjectValue(): UnsetTypedObjectValue
@ -15,12 +22,6 @@ function makeUnsetTypedObjectValue(): UnsetTypedObjectValue
return new UnsetTypedObjectValue(); return new UnsetTypedObjectValue();
} }
function acceptUnsetTypedObjectValue(UnsetTypedObjectValue $value): string
{
echo "entered\n";
return $value->value();
}
function main() function main()
{ {
$value = makeUnsetTypedObjectValue(); $value = makeUnsetTypedObjectValue();
@ -31,19 +32,12 @@ function main()
var_dump(isset($value)); var_dump(isset($value));
try { try {
acceptUnsetTypedObjectValue(@$value); @$value->readProperty();
} catch (Throwable $error) {
echo $error::class, "\n";
}
try {
@$value->value();
} catch (Throwable $error) { } catch (Throwable $error) {
echo $error::class, "\n"; echo $error::class, "\n";
} }
$value = makeUnsetTypedObjectValue(); $value = makeUnsetTypedObjectValue();
var_dump(acceptUnsetTypedObjectValue($value));
var_dump($value->value()); var_dump($value->value());
} }
?> ?>
@ -51,8 +45,5 @@ function main()
bool(true) bool(true)
bool(false) bool(false)
bool(false) bool(false)
TypeError
Error Error
entered
string(5) "value"
string(5) "value" string(5) "value"

Loading…
Cancel
Save