From 6f634726bf56a4d278fdc31b6d1b3673962c3f76 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 14 Jul 2026 13:06:15 +0800 Subject: [PATCH] fix(ref): handle dynamic returns and typed static properties --- src/CompilerBase.php | 55 +++++++++++++------ src/Generator/Symbol.php | 5 ++ src/Parser/PropertyAccessTrait.php | 22 +++----- .../ref/dynamic-return-reference-chain.phpt | 52 ++++++++++++++++++ .../ref/static-return-reference-lsb.phpt | 39 +++++++++++++ .../static/static-call-byref-arg.phpt | 6 ++ .../static-prop-assign-ref-type-error.phpt | 25 +++++++++ 7 files changed, 173 insertions(+), 31 deletions(-) create mode 100644 tests/compiler/ref/dynamic-return-reference-chain.phpt create mode 100644 tests/compiler/ref/static-return-reference-lsb.phpt create mode 100644 tests/compiler/static/static-prop-assign-ref-type-error.phpt diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 6a18f004..348d230a 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1802,12 +1802,10 @@ class CompilerBase implements PropertyAccessContext } /** - * Returns true when the expression is a function/method/static call that - * itself returns by reference, so its result can be forwarded directly - * from a `return by reference` context. In PHP, `function &f() { return g(); }` - * is valid as long as `g()` also returns by reference. + * Resolve whether a call returns by reference. A null result means that + * dispatch is dynamic and must be checked at runtime. */ - protected function isRefReturningCall(Node $expr): bool + protected function resolveRefReturningCall(Node $expr): ?bool { if ($expr instanceof Expr\FuncCall && ($this->isNameExpr($expr->name) || $this->isFullNameExpr($expr->name))) { $name = $this->parseIdentifier($expr->name); @@ -1816,16 +1814,29 @@ class CompilerBase implements PropertyAccessContext return $this->getFunction($function)->returnsByRef; } $reflection = \TypePhp\Resolver\Reflection::getFunction(ltrim($this->getNamespacedFuncName($name), '\\')); - return $reflection !== null && $reflection->isInternal() && $reflection->returnsReference(); + return $reflection?->returnsReference(); + } + if ($expr instanceof Expr\FuncCall) { + return null; } if ($expr instanceof Expr\MethodCall && $this->isNamedMethod($expr->name) && $this->isVarExpr($expr->var)) { $object = $this->parseIdentifier($expr->var); $method = $this->parseIdentifier($expr->name); - $function = $this->findNativeMethod($expr, $object, $method); + if ($object === 'this_') { + $class = $this->getFullClassName(); + } elseif (isset($this->context->objects[$object])) { + $class = $this->context->stableObjects[$object] ?? $this->context->objects[$object]; + } else { + return null; + } + $function = $this->getNativeMethod($expr, $class, $method, false); if ($function !== false) { return $this->getFunction($function)->returnsByRef; } - return false; + return null; + } + if ($expr instanceof Expr\MethodCall) { + return null; } if ($expr instanceof Expr\StaticCall && ($this->isNameExpr($expr->class) || $this->isFullNameExpr($expr->class)) && $this->isIdExpr($expr->name)) { $class = $this->parseIdentifier($expr->class); @@ -1836,18 +1847,23 @@ class CompilerBase implements PropertyAccessContext return false; } $class = $this->classDef->extends; - } elseif ($class !== 'static') { + } elseif ($class === 'static') { + if (!$this->classDef) { + return null; + } + $class = $this->getFullClassName(); + } else { $class = $this->getNamespacedClassName($class); } - if ($class === 'static') { - return false; - } $method = $this->parseIdentifier($expr->name); - $function = $this->getNativeMethod($expr, $class, $method); + $function = $this->getNativeMethod($expr, $class, $method, false); if ($function !== false) { return $this->getFunction($function)->returnsByRef; } - return false; + return null; + } + if ($expr instanceof Expr\StaticCall) { + return null; } return false; } @@ -1858,9 +1874,11 @@ class CompilerBase implements PropertyAccessContext if ($v->expr === null) { return 'return ' . Type::REF . '{};'; } - // Forwarding a call that itself returns by reference is valid PHP. - if ($this->isRefReturningCall($v->expr)) { - return 'return ' . $this->parseExpr($v->expr) . ';'; + if ($v->expr instanceof CallLike) { + $returnsByRef = $this->resolveRefReturningCall($v->expr); + if ($returnsByRef !== false) { + return 'return php::toReferenceExact(' . $this->parseExpr($v->expr) . ');'; + } } if (!$this->isVarExpr($v->expr) && !$this->isPropertyFetch($v->expr) @@ -1893,6 +1911,9 @@ class CompilerBase implements PropertyAccessContext if ($this->isPropertyFetch($v->expr)) { return 'return ' . $this->emitDynamicPropertyFetchRef($v->expr, $v) . ';'; } + if ($this->isStaticPropertyFetch($v->expr)) { + return 'return ' . $this->emitStaticPropertyFetchRef($v->expr, $v) . ';'; + } return 'return ' . $this->parseChainedExpr($v->expr, self::OP_REFVAL) . ';'; } if ($v->expr === null) { diff --git a/src/Generator/Symbol.php b/src/Generator/Symbol.php index e80ae4d7..a3b0321a 100644 --- a/src/Generator/Symbol.php +++ b/src/Generator/Symbol.php @@ -17,6 +17,11 @@ class Symbol return 'php::getStaticProperty'; } + public static function getStaticPropertyRef(): string + { + return 'php::getStaticPropertyRef'; + } + public static function setStaticProperty(): string { return 'php::setStaticProperty'; diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index 4997c9a3..6bf89049 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -201,34 +201,27 @@ trait PropertyAccessTrait return $objectExpr . '.attrRef(' . $this->identifierToStr($expr->name) . ')'; } - /** - * Emit a reference (php::Ref) bound to a static property's underlying zval. - * - * `php::getStaticProperty(ce, offset)` returns a Variant that shares the - * static property's zval, so `.toReference()` yields a live reference whose - * writes propagate back to the static property. - */ protected function emitStaticPropertyFetchRef(Expr\StaticPropertyFetch $expr, NodeAbstract $errorNode): string { + $resolution = $this->resolveNativeStaticPropertyFetch($expr); if ($this->isIdExpr($expr->name)) { - $this->resolveNativeStaticPropertyFetch($expr); $this->assertPropertySetVisibility($expr); } - $resolution = $this->resolveNativeStaticPropertyFetch($expr); if ($resolution !== null) { + $property = $this->identifierToStr($expr->name, literal: true); if ($resolution->class !== null) { $classPtr = $this->getClassEntryPtr($resolution->class); - return Symbol::getStaticProperty() . '(' . $classPtr . ', ' . $resolution->expression . ').toReference()'; + return Symbol::getStaticPropertyRef() . '(' . $classPtr . ', ' . $property . ')'; } if ($resolution->expression !== null) { // Dynamic target, e.g. `self` resolved through the called class inside a trait. - return $resolution->expression . '.toReference()'; + return Symbol::getStaticPropertyRef() . '(' . Symbol::getCalledCe() . ', ' . $property . ')'; } } // Fully dynamic path: `static` keyword, dynamic class name, or dynamic property name. - return $this->parseDynamicStaticPropertyFetch($expr) . '.toReference()'; + return $this->parseDynamicStaticPropertyFetch($expr, true); } @@ -356,7 +349,7 @@ trait PropertyAccessTrait * object. Materialising both operands preserves PHP's left-to-right * evaluation order and avoids ambiguous C++ overload resolution for Var. */ - private function parseDynamicStaticPropertyFetch(Expr\StaticPropertyFetch $expr): string + private function parseDynamicStaticPropertyFetch(Expr\StaticPropertyFetch $expr, bool $reference = false): string { $classValue = $this->getDynamicStaticClassValue($expr->class); $propertyValue = $this->identifierToStr($expr->name, literal: true); @@ -367,7 +360,8 @@ trait PropertyAccessTrait $this->context->beforeStmtLines[] = $propertyVar . ' = ' . $propertyValue . ';'; $className = '(' . $classVar . '.isObject() ? php::fn::get_class(' . $classVar . ') : php::toString(' . $classVar . '))'; - return Symbol::getStaticProperty() . '(' . $className . ', php::toString(' . $propertyVar . '))'; + $helper = $reference ? Symbol::getStaticPropertyRef() : Symbol::getStaticProperty(); + return $helper . '(' . $className . ', php::toString(' . $propertyVar . '))'; } private function getDynamicStaticClassValue(NodeAbstract $class): string diff --git a/tests/compiler/ref/dynamic-return-reference-chain.phpt b/tests/compiler/ref/dynamic-return-reference-chain.phpt new file mode 100644 index 00000000..60015aae --- /dev/null +++ b/tests/compiler/ref/dynamic-return-reference-chain.phpt @@ -0,0 +1,52 @@ +--TEST-- +Reference-returning functions can forward dynamic and chained calls +--FILE-- +value; + } +} + +class DynamicRefFactory +{ + public function create(): DynamicRefBox + { + return new DynamicRefBox(); + } + + public function &forward(): mixed + { + return $this->create()->getValue(); + } +} + +function main(): void +{ + $dynamic = &dynamic_forward('dynamic_source'); + $dynamic = 10; + var_dump(dynamic_source()); + + $factory = new DynamicRefFactory(); + $chained = &$factory->forward(); + var_dump($chained); +} +?> +--EXPECT-- +int(10) +int(2) diff --git a/tests/compiler/ref/static-return-reference-lsb.phpt b/tests/compiler/ref/static-return-reference-lsb.phpt new file mode 100644 index 00000000..2a11dfec --- /dev/null +++ b/tests/compiler/ref/static-return-reference-lsb.phpt @@ -0,0 +1,39 @@ +--TEST-- +Reference-returning static method forwards a late-static-bound reference +--FILE-- + +--EXPECT-- +int(20) +int(1) diff --git a/tests/compiler/static/static-call-byref-arg.phpt b/tests/compiler/static/static-call-byref-arg.phpt index e0a975f4..c0cbd823 100644 --- a/tests/compiler/static/static-call-byref-arg.phpt +++ b/tests/compiler/static/static-call-byref-arg.phpt @@ -23,6 +23,10 @@ class TestClass extends P var_dump($v3); parent::test($v4); var_dump($v4); + static::test(value: $v5); + var_dump($v5); + parent::test(value: $v6); + var_dump($v6); } } @@ -36,3 +40,5 @@ string(4) "test" string(4) "test" string(4) "test" string(4) "test" +string(4) "test" +string(4) "test" diff --git a/tests/compiler/static/static-prop-assign-ref-type-error.phpt b/tests/compiler/static/static-prop-assign-ref-type-error.phpt new file mode 100644 index 00000000..d1ccdc30 --- /dev/null +++ b/tests/compiler/static/static-prop-assign-ref-type-error.phpt @@ -0,0 +1,25 @@ +--TEST-- +Reference to a typed static property preserves its type constraint +--FILE-- + +--EXPECT-- +TypeError +int(1)