diff --git a/phpunit/code/native-method-unset-keeps-optimization.php b/phpunit/code/native-method-unset-keeps-optimization.php new file mode 100644 index 00000000..5229a247 --- /dev/null +++ b/phpunit/code/native-method-unset-keeps-optimization.php @@ -0,0 +1,27 @@ +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(); +} diff --git a/phpunit/src/NativePropertyTest.php b/phpunit/src/NativePropertyTest.php index ef6fd24a..49b0b842 100644 --- a/phpunit/src/NativePropertyTest.php +++ b/phpunit/src/NativePropertyTest.php @@ -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( diff --git a/phpunit/src/SsaAnalysisTest.php b/phpunit/src/SsaAnalysisTest.php index 6fb77bb4..51e8e7bf 100644 --- a/phpunit/src/SsaAnalysisTest.php +++ b/phpunit/src/SsaAnalysisTest.php @@ -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'); diff --git a/src/Context/FunctionContext.php b/src/Context/FunctionContext.php index 063ba191..3b979f67 100644 --- a/src/Context/FunctionContext.php +++ b/src/Context/FunctionContext.php @@ -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 - */ - 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 = []; diff --git a/src/Generator/TypeCheckGenerator.php b/src/Generator/TypeCheckGenerator.php index 8520cc32..fc8ea15b 100644 --- a/src/Generator/TypeCheckGenerator.php +++ b/src/Generator/TypeCheckGenerator.php @@ -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) { diff --git a/src/Optimizer/SsaPropOptimizer.php b/src/Optimizer/SsaPropOptimizer.php index 0137ea74..789ffcf3 100644 --- a/src/Optimizer/SsaPropOptimizer.php +++ b/src/Optimizer/SsaPropOptimizer.php @@ -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); } diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index d1c69b76..08ca8dbe 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -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(); diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index 8566592c..c2f5c1fc 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -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();"; diff --git a/src/Parser/TypeDetectionTrait.php b/src/Parser/TypeDetectionTrait.php index 83d97ba2..0e3762ca 100644 --- a/src/Parser/TypeDetectionTrait.php +++ b/src/Parser/TypeDetectionTrait.php @@ -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 diff --git a/src/Translator.php b/src/Translator.php index eddc4318..e485cc1d 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -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; - } } } diff --git a/src/TypeSystem/NativeTypeCompatibilityTrait.php b/src/TypeSystem/NativeTypeCompatibilityTrait.php index 28e43ed0..4fd9c467 100644 --- a/src/TypeSystem/NativeTypeCompatibilityTrait.php +++ b/src/TypeSystem/NativeTypeCompatibilityTrait.php @@ -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)) { diff --git a/tests/compiler/basic/unset-typed-object-state.phpt b/tests/compiler/basic/unset-typed-object-state.phpt index dfe23f7d..e42a4371 100644 --- a/tests/compiler/basic/unset-typed-object-state.phpt +++ b/tests/compiler/basic/unset-typed-object-state.phpt @@ -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-- 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"