diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 36da7790..0e7057da 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1812,12 +1812,85 @@ class CompilerBase implements PropertyAccessContext $this->fatalError($arg, 'Only string literals or `ClassName::class` constant are supported'); } + /** + * Resolve whether a call returns by reference. A null result means that + * dispatch is dynamic and must be checked at runtime. + */ + protected function resolveRefReturningCall(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?->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); + 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 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); + if ($class === 'self') { + $class = $this->getFullClassName(); + } elseif ($class === 'parent') { + if (!$this->classDef || !$this->classDef->extends) { + return false; + } + $class = $this->classDef->extends; + } elseif ($class === 'static') { + if (!$this->classDef) { + return null; + } + $class = $this->getFullClassName(); + } else { + $class = $this->getNamespacedClassName($class); + } + $method = $this->parseIdentifier($expr->name); + $function = $this->getNativeMethod($expr, $class, $method, false); + if ($function !== false) { + return $this->getFunction($function)->returnsByRef; + } + return null; + } + if ($expr instanceof Expr\StaticCall) { + return null; + } + return false; + } + protected function parseReturn(Node\Stmt\Return_ $v): string { if ($this->functionDef->returnsByRef) { if ($v->expr === null) { return 'return ' . Type::REF . '{};'; } + 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) && !$this->isStaticPropertyFetch($v->expr) @@ -1849,6 +1922,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/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 9668a1fc..b3275e44 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -774,6 +774,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/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 42cdc2f1..8b86e45c 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/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index 39f91829..51eec046 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -201,6 +201,29 @@ trait PropertyAccessTrait return $objectExpr . '.attrRef(' . $this->identifierToStr($expr->name) . ')'; } + protected function emitStaticPropertyFetchRef(Expr\StaticPropertyFetch $expr, NodeAbstract $errorNode): string + { + $resolution = $this->resolveNativeStaticPropertyFetch($expr); + if ($this->isIdExpr($expr->name)) { + $this->assertPropertySetVisibility($expr); + } + + if ($resolution !== null) { + $property = $this->identifierToStr($expr->name, literal: true); + if ($resolution->class !== null) { + $classPtr = $this->getClassEntryPtr($resolution->class); + return Symbol::getStaticPropertyRef() . '(' . $classPtr . ', ' . $property . ')'; + } + if ($resolution->expression !== null) { + // Dynamic target, e.g. `self` resolved through the called class inside a trait. + return Symbol::getStaticPropertyRef() . '(' . Symbol::getCalledCe() . ', ' . $property . ')'; + } + } + + // Fully dynamic path: `static` keyword, dynamic class name, or dynamic property name. + return $this->parseDynamicStaticPropertyFetch($expr, true); + } + protected function resolveNativeStaticPropertyFetch(Expr\StaticPropertyFetch $expr): ?StaticPropertyFetchResolution { @@ -326,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); @@ -337,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/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/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/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) 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 new file mode 100644 index 00000000..c0cbd823 --- /dev/null +++ b/tests/compiler/static/static-call-byref-arg.phpt @@ -0,0 +1,44 @@ +--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" +string(4) "test" +string(4) "test" 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" 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-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) 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)