From 29fcf0648203fbb4ad9fa1a4a1706c1a66ff4bbd Mon Sep 17 00:00:00 2001 From: Yurun Date: Mon, 13 Jul 2026 15:35:25 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix(compiler):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=9D=99=E6=80=81=E5=B1=9E=E6=80=A7=E5=BC=95=E7=94=A8=E8=B5=8B?= =?UTF-8?q?=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Parser/AssignOpTrait.php | 3 ++ src/Parser/PropertyAccessTrait.php | 30 ++++++++++++++ ...c-prop-assign-ref-late-static-binding.phpt | 34 +++++++++++++++ .../static/static-prop-assign-ref-native.phpt | 41 +++++++++++++++++++ .../static/static-prop-assign-ref-parent.phpt | 36 ++++++++++++++++ .../static/static-prop-assign-ref.phpt | 40 ++++++++++++++++++ 6 files changed, 184 insertions(+) create mode 100644 tests/compiler/static/static-prop-assign-ref-late-static-binding.phpt create mode 100644 tests/compiler/static/static-prop-assign-ref-native.phpt create mode 100644 tests/compiler/static/static-prop-assign-ref-parent.phpt create mode 100644 tests/compiler/static/static-prop-assign-ref.phpt diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index d006e6bb..c92fad04 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -758,6 +758,9 @@ trait AssignOpTrait } elseif ($this->isPropertyFetch($expr->expr)) { $left = $this->parseIdentifier($expr->var); $rightExpr = $tmpVar . ' = ' . $this->emitDynamicPropertyFetchRef($expr->expr, $expr); + } elseif ($this->isStaticPropertyFetch($expr->expr)) { + $left = $this->parseIdentifier($expr->var); + $rightExpr = $tmpVar . ' = ' . $this->emitStaticPropertyFetchRef($expr->expr, $expr); } elseif ($this->isArrayDimFetch($expr->expr)) { $left = $this->parseIdentifier($expr->var); $array = $this->parseWritableIdentifier($expr->expr->var); diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index 6bb55ef9..4997c9a3 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -201,6 +201,36 @@ 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 + { + if ($this->isIdExpr($expr->name)) { + $this->resolveNativeStaticPropertyFetch($expr); + $this->assertPropertySetVisibility($expr); + } + + $resolution = $this->resolveNativeStaticPropertyFetch($expr); + if ($resolution !== null) { + if ($resolution->class !== null) { + $classPtr = $this->getClassEntryPtr($resolution->class); + return Symbol::getStaticProperty() . '(' . $classPtr . ', ' . $resolution->expression . ').toReference()'; + } + if ($resolution->expression !== null) { + // Dynamic target, e.g. `self` resolved through the called class inside a trait. + return $resolution->expression . '.toReference()'; + } + } + + // Fully dynamic path: `static` keyword, dynamic class name, or dynamic property name. + return $this->parseDynamicStaticPropertyFetch($expr) . '.toReference()'; + } + protected function resolveNativeStaticPropertyFetch(Expr\StaticPropertyFetch $expr): ?StaticPropertyFetchResolution { diff --git a/tests/compiler/static/static-prop-assign-ref-late-static-binding.phpt b/tests/compiler/static/static-prop-assign-ref-late-static-binding.phpt new file mode 100644 index 00000000..c2053e27 --- /dev/null +++ b/tests/compiler/static/static-prop-assign-ref-late-static-binding.phpt @@ -0,0 +1,34 @@ +--TEST-- +Assign by reference to late-static-bound property resolves to called class +--FILE-- + +--EXPECT-- +int(1) +int(99) +int(1) diff --git a/tests/compiler/static/static-prop-assign-ref-native.phpt b/tests/compiler/static/static-prop-assign-ref-native.phpt new file mode 100644 index 00000000..fb454e18 --- /dev/null +++ b/tests/compiler/static/static-prop-assign-ref-native.phpt @@ -0,0 +1,41 @@ +--TEST-- +Assign by reference to native typed static property (self / static / class name) +--FILE-- + +--EXPECT-- +int(123) +int(456) +int(456) +int(789) +int(789) +int(1000) diff --git a/tests/compiler/static/static-prop-assign-ref-parent.phpt b/tests/compiler/static/static-prop-assign-ref-parent.phpt new file mode 100644 index 00000000..c75c0c45 --- /dev/null +++ b/tests/compiler/static/static-prop-assign-ref-parent.phpt @@ -0,0 +1,36 @@ +--TEST-- +Assign by reference to parent static property (parent::$value) +--FILE-- + +--EXPECT-- +int(1) +int(1) +int(999) +int(999) +int(999) diff --git a/tests/compiler/static/static-prop-assign-ref.phpt b/tests/compiler/static/static-prop-assign-ref.phpt new file mode 100644 index 00000000..f31f5dba --- /dev/null +++ b/tests/compiler/static/static-prop-assign-ref.phpt @@ -0,0 +1,40 @@ +--TEST-- +Assign by reference to static property (self / static / class name) +--FILE-- + +--EXPECT-- +int(123) +int(456) +int(456) +int(789) +int(789) +int(1000) From 02e16794b828fe5fd016650cb2053f44a7508eac Mon Sep 17 00:00:00 2001 From: Yurun Date: Mon, 13 Jul 2026 16:52:40 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix(compiler):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=9D=99=E6=80=81=E6=96=B9=E6=B3=95=E8=B0=83=E7=94=A8=E6=8C=89?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E5=8F=82=E6=95=B0=E4=BC=A0=E5=80=BC=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Parser/MethodCallTrait.php | 16 ++++++-- .../static/static-call-byref-arg.phpt | 38 +++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 tests/compiler/static/static-call-byref-arg.phpt diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index adf60e11..6d4fb329 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -252,6 +252,7 @@ trait MethodCallTrait $this->guardAbstractMethod($parentClass, $method, $expr); $methodPtr = $this->getMethodPtr($parentClass, $method); } else { + $method = ''; // parent:: is bound to the lexical parent class, not the runtime // object's parent. Look the method up on that class, then invoke it // through this_ so Zend receives the current call scope. @@ -261,7 +262,8 @@ trait MethodCallTrait if (empty($expr->args)) { return 'this_.call(' . $methodPtr . ')'; } - return 'this_.call(' . $methodPtr . ', ' . $this->parseCallArgs($expr->args) . ')'; + // 传入方法名与父类名,以便在按引用参数检测时解析方法签名 + return 'this_.call(' . $methodPtr . ', ' . $this->parseCallArgs($expr->args, $method, $parentClass) . ')'; } @@ -453,6 +455,8 @@ trait MethodCallTrait { $self = false; $callScope = []; + $rtFunc = ''; + $rtClass = ''; $class = $this->parseIdentifier($expr->class); // parent::$method() still has a lexical parent class even when the @@ -475,13 +479,17 @@ trait MethodCallTrait } $placeHolder = $fn; } elseif ($this->isNameExpr($expr->class) and $class === 'static') { + $method = $this->parseIdentifier($expr->name); $methodPtr = $this->identifierToStr($expr->name, literal: true); $fn = Symbol::getCalledCe() . ', php::getMethod(' . Symbol::getCalledCe() . ', ' . $methodPtr . ')'; $this->context->beforeStmtLines[] = $this->formatCppLineComment( 'Static Method Call: ', - 'static::' . $this->parseIdentifier($expr->name) . '()' + 'static::' . $method . '()' ); $placeHolder = $this->genArray([Symbol::getCalledClass(), $methodPtr]); + // 用于在按引用参数检测时解析方法签名(late static binding 在当前类层级中解析) + $rtFunc = $method; + $rtClass = $this->getNamespacedClassName($this->class); } elseif ($this->isNameExpr($expr->class)) { if ($class === 'self') { $class = $this->class; @@ -493,6 +501,8 @@ trait MethodCallTrait _do_call: $method = $this->parseIdentifier($expr->name); + $rtFunc = $method; + $rtClass = $class; $dynamicCall = false; $this->context->beforeStmtLines[] = $this->formatCppLineComment( 'Static Method Call: ', @@ -556,7 +566,7 @@ trait MethodCallTrait return $call . '(' . $fn . ')'; } try { - return $this->genRuntimeFunctionCall($fn, $expr->args); + return $this->genRuntimeFunctionCall($fn, $expr->args, $rtFunc, $rtClass); } catch (PlaceHolder) { return $this->genPlaceHolder($placeHolder); } diff --git a/tests/compiler/static/static-call-byref-arg.phpt b/tests/compiler/static/static-call-byref-arg.phpt new file mode 100644 index 00000000..e0a975f4 --- /dev/null +++ b/tests/compiler/static/static-call-byref-arg.phpt @@ -0,0 +1,38 @@ +--TEST-- +Static method calls (self / static / class name / parent) with by-reference args +--FILE-- + +--EXPECT-- +string(4) "test" +string(4) "test" +string(4) "test" +string(4) "test" From da93efcb692b58ef3b80934bf812c49dfa64ba10 Mon Sep 17 00:00:00 2001 From: Yurun Date: Mon, 13 Jul 2026 20:19:02 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix(compiler):=20=E5=A4=84=E7=90=86?= =?UTF-8?q?=E6=9C=AA=E5=AE=9A=E4=B9=89=E5=8F=98=E9=87=8F=E8=A2=AB=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E8=B5=8B=E5=80=BC=E5=90=8E=E8=B7=A8=E7=B1=BB=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=8F=82=E6=95=B0=E5=8C=B9=E9=85=8D=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E4=B8=8D=E6=AD=A3=E7=A1=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/TypeSystem/CompositeTypeCheckerTrait.php | 6 ++- .../static-call-byref-undefined-var.phpt | 38 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/compiler/static/static-call-byref-undefined-var.phpt diff --git a/src/TypeSystem/CompositeTypeCheckerTrait.php b/src/TypeSystem/CompositeTypeCheckerTrait.php index 8ce63cdd..77762b4d 100644 --- a/src/TypeSystem/CompositeTypeCheckerTrait.php +++ b/src/TypeSystem/CompositeTypeCheckerTrait.php @@ -35,7 +35,11 @@ trait CompositeTypeCheckerTrait // TYPE_VAR means that the expression is dynamic or its result cannot // be represented by the current scalar type system. It must retain the // runtime type check. - if ($this->detectTypeOfExpr($value) === Type::VAR && !$this->isNullExpr($value)) { + // A reference (TYPE_REF) is a Variant reference whose concrete type is + // only known at runtime (e.g. an undefined variable auto-created by a + // by-reference argument), so it is treated the same way. + $valueType = $this->detectTypeOfExpr($value); + if (($valueType === Type::VAR || $valueType === Type::REF) && !$this->isNullExpr($value)) { return self::COMPOSITE_TYPE_UNKNOWN; } diff --git a/tests/compiler/static/static-call-byref-undefined-var.phpt b/tests/compiler/static/static-call-byref-undefined-var.phpt new file mode 100644 index 00000000..e51f81d5 --- /dev/null +++ b/tests/compiler/static/static-call-byref-undefined-var.phpt @@ -0,0 +1,38 @@ +--TEST-- +Static method call passing an undefined variable by reference (late static binding) +--FILE-- + +--EXPECT-- +string(4) "test" +string(4) "test" From fe99467ea48fddc658f97fda3824b4e173d1f755 Mon Sep 17 00:00:00 2001 From: Yurun Date: Mon, 13 Jul 2026 20:43:01 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(compiler):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=E5=BC=95=E7=94=A8=E8=BD=AC=E5=8F=91=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/CompilerBase.php | 55 +++++++++++++++++++ .../ref/function-return-reference-chain.phpt | 34 ++++++++++++ .../ref/method-return-reference-chain.phpt | 33 +++++++++++ .../ref/static-return-reference-chain.phpt | 33 +++++++++++ 4 files changed, 155 insertions(+) create mode 100644 tests/compiler/ref/function-return-reference-chain.phpt create mode 100644 tests/compiler/ref/method-return-reference-chain.phpt create mode 100644 tests/compiler/ref/static-return-reference-chain.phpt diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 75b21849..6a18f004 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1801,12 +1801,67 @@ class CompilerBase implements PropertyAccessContext $this->fatalError($arg, 'Only string literals or `ClassName::class` constant are supported'); } + /** + * 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. + */ + protected function isRefReturningCall(Node $expr): bool + { + if ($expr instanceof Expr\FuncCall && ($this->isNameExpr($expr->name) || $this->isFullNameExpr($expr->name))) { + $name = $this->parseIdentifier($expr->name); + $function = $this->findNativeFunction($name); + if ($function !== false) { + return $this->getFunction($function)->returnsByRef; + } + $reflection = \TypePhp\Resolver\Reflection::getFunction(ltrim($this->getNamespacedFuncName($name), '\\')); + return $reflection !== null && $reflection->isInternal() && $reflection->returnsReference(); + } + 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 ($function !== false) { + return $this->getFunction($function)->returnsByRef; + } + return false; + } + if ($expr instanceof Expr\StaticCall && ($this->isNameExpr($expr->class) || $this->isFullNameExpr($expr->class)) && $this->isIdExpr($expr->name)) { + $class = $this->parseIdentifier($expr->class); + if ($class === 'self') { + $class = $this->getFullClassName(); + } elseif ($class === 'parent') { + if (!$this->classDef || !$this->classDef->extends) { + return false; + } + $class = $this->classDef->extends; + } elseif ($class !== 'static') { + $class = $this->getNamespacedClassName($class); + } + if ($class === 'static') { + return false; + } + $method = $this->parseIdentifier($expr->name); + $function = $this->getNativeMethod($expr, $class, $method); + if ($function !== false) { + return $this->getFunction($function)->returnsByRef; + } + return false; + } + return false; + } + protected function parseReturn(Node\Stmt\Return_ $v): string { if ($this->functionDef->returnsByRef) { 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 (!$this->isVarExpr($v->expr) && !$this->isPropertyFetch($v->expr) && !$this->isStaticPropertyFetch($v->expr) diff --git a/tests/compiler/ref/function-return-reference-chain.phpt b/tests/compiler/ref/function-return-reference-chain.phpt new file mode 100644 index 00000000..edf4464f --- /dev/null +++ b/tests/compiler/ref/function-return-reference-chain.phpt @@ -0,0 +1,34 @@ +--TEST-- +Function returning by reference can forward another by-reference call +--FILE-- + +--EXPECT-- +int(1) +int(2) +int(1) diff --git a/tests/compiler/ref/method-return-reference-chain.phpt b/tests/compiler/ref/method-return-reference-chain.phpt new file mode 100644 index 00000000..36285141 --- /dev/null +++ b/tests/compiler/ref/method-return-reference-chain.phpt @@ -0,0 +1,33 @@ +--TEST-- +Method returning by reference can forward another by-reference method call +--FILE-- +value; + } + + public function &getRefValue() + { + return $this->getValue(); + } +} + +function main() +{ + $test = new Test; + var_dump($test->getRefValue()); + $ref = &$test->getRefValue(); + $ref = 2; + var_dump($test->getValue()); +} + +// main(); +?> +--EXPECT-- +int(1) +int(2) diff --git a/tests/compiler/ref/static-return-reference-chain.phpt b/tests/compiler/ref/static-return-reference-chain.phpt new file mode 100644 index 00000000..749be438 --- /dev/null +++ b/tests/compiler/ref/static-return-reference-chain.phpt @@ -0,0 +1,33 @@ +--TEST-- +Static method returning by reference can forward another by-reference static call +--FILE-- + +--EXPECT-- +int(1) +int(11) From 6f634726bf56a4d278fdc31b6d1b3673962c3f76 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 14 Jul 2026 13:06:15 +0800 Subject: [PATCH 5/5] 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)