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
韩天峰 2 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);
}
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
{
$this->exec(

@ -668,6 +668,30 @@ class SsaAnalysisTest extends TestCase
$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
{
$objVar = new Expr\Variable('obj');

@ -29,15 +29,6 @@ class FunctionContext
*/
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.
*
@ -95,7 +86,6 @@ class FunctionContext
$this->staticVars = [];
$this->arguments = [];
$this->objects = [];
$this->runtimeCheckedObjects = [];
$this->declaredObjects = [];
$this->stdArrays = [];
$this->stdContainers = [];

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

@ -19,8 +19,9 @@
* 8. First access is not inside a loop or nested block scope
* 9. Property is not readonly (including properties of a readonly class)
*
* Direct unset($o->prop) invalidates the property slot assumption. Any property
* observed in an unset operation must use dynamic attr() access.
* Direct unset($o->prop) invalidates that property slot; unset($o) invalidates
* 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;
@ -361,8 +362,8 @@ trait SsaPropOptimizer
* - mutate($o) or $o->method() — dynamic code may turn the property slot
* into a reference through the exposed object
*
* Direct unset($o->prop) invalidates the property slot and disables
* hoisting for that property.
* Direct unset($o->prop) disables hoisting for that property. unset($o)
* disables all property hoisting for the object.
*/
protected function hasDangerousPropOps(string $objName, array $stmts): bool
{
@ -412,6 +413,11 @@ trait SsaPropOptimizer
if ($propName !== null) {
$events[] = ['kind' => 'danger_always', 'prop' => $propName];
$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 {
$this->collectPropEvents($var, $objName, $events);
}

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

@ -774,9 +774,6 @@ trait PropertyAccessTrait
//
// Keep the declared class: unset() changes only the value
// 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;";
} else {
$lines[] = "{$name}.unset();";

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

@ -3603,9 +3603,6 @@ CODE;
$this->addArgument($argInfo->name, $argInfo->variadic ? Type::ARRAY : $argInfo->type);
if (!$argInfo->variadic and $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;
// 其他动态/外部库/any 场景保留 php::toObject() 作为运行时兜底。
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);
}
if ($this->isKnownConcreteObjectExpr($arg->value, $class)) {

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

Loading…
Cancel
Save