fix(ref): handle dynamic returns and typed static properties

pull/17/head
韩天峰 1 month ago
parent fe99467ea4
commit 6f634726bf
  1. 55
      src/CompilerBase.php
  2. 5
      src/Generator/Symbol.php
  3. 22
      src/Parser/PropertyAccessTrait.php
  4. 52
      tests/compiler/ref/dynamic-return-reference-chain.phpt
  5. 39
      tests/compiler/ref/static-return-reference-lsb.phpt
  6. 6
      tests/compiler/static/static-call-byref-arg.phpt
  7. 25
      tests/compiler/static/static-prop-assign-ref-type-error.phpt

@ -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) {

@ -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';

@ -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

@ -0,0 +1,52 @@
--TEST--
Reference-returning functions can forward dynamic and chained calls
--FILE--
<?php
function &dynamic_source(): mixed
{
static $value = 1;
return $value;
}
function &dynamic_forward(string $function): mixed
{
return $function();
}
class DynamicRefBox
{
public int $value = 2;
public function &getValue(): mixed
{
return $this->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)

@ -0,0 +1,39 @@
--TEST--
Reference-returning static method forwards a late-static-bound reference
--FILE--
<?php
class RefLsbBase
{
protected static int $value = 1;
protected static function &valueRef(): mixed
{
return static::$value;
}
public static function &forward(): mixed
{
return static::valueRef();
}
public static function value(): int
{
return static::$value;
}
}
class RefLsbChild extends RefLsbBase
{
protected static int $value = 2;
}
function main(): void
{
$value = &RefLsbChild::forward();
$value = 20;
var_dump(RefLsbChild::value(), RefLsbBase::value());
}
?>
--EXPECT--
int(20)
int(1)

@ -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"

@ -0,0 +1,25 @@
--TEST--
Reference to a typed static property preserves its type constraint
--FILE--
<?php
use native_types;
class TypedStaticReference
{
public static int $value = 1;
}
function main(): void
{
$reference = &TypedStaticReference::$value;
try {
$reference = 'invalid';
} catch (TypeError $error) {
echo "TypeError\n";
}
var_dump(TypedStaticReference::$value);
}
?>
--EXPECT--
TypeError
int(1)
Loading…
Cancel
Save