diff --git a/docs/INCOMPATIBLE_PHP_FEATURES.md b/docs/INCOMPATIBLE_PHP_FEATURES.md index b6745ce0..1163e256 100644 --- a/docs/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/INCOMPATIBLE_PHP_FEATURES.md @@ -16,7 +16,7 @@ - 不支持 `yield` / `yield from`。 - 不支持可变变量 `$$var`。 - 不支持 PHP 8.4 property hooks。 -- 不支持函数、方法、闭包或箭头函数按引用返回。 +- 不支持闭包或箭头函数按引用返回。 - `__construct()` 不允许返回值。 - 参数默认值不允许出现在必填参数之前(`PHP`允许,但会直接丢弃此默认参数)。 - 不支持引用可变参数 `&...$args`。 @@ -34,7 +34,7 @@ ## 调用与引用 - 闭包和箭头函数不支持引用参数。 -- 引用赋值的右侧必须是编译器可直接定位的变量、数组元素或对象属性;不支持从调用结果或复杂静态属性表达式建立引用。 +- 引用赋值不支持从复杂静态属性表达式建立引用。 - 动态调用、闭包调用等编译期无法确定参数签名的调用,不能自动转换引用参数;需要显式使用 `refval()` 或等价关键词方法 `toRef()`。 - `refval()` / `toRef()` 只接受变量、数组元素或对象属性。 - 带 unpack 且尾部追加 named arguments 的调用会退化为动态调用,不能使用 native call。 diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 811bcad1..a1922694 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1918,29 +1918,38 @@ class CompilerBase implements PropertyAccessContext if ($v->expr === null) { return 'return ' . self::TYPE_REF . '{};'; } - if (!$this->isVarExpr($v->expr)) { + if (!$this->isVarExpr($v->expr) + && !$this->isPropertyFetch($v->expr) + && !$this->isStaticPropertyFetch($v->expr) + && !$this->isArrayDimFetch($v->expr)) { $this->fatalError($v, 'A function returning by reference must return a variable'); } - $name = $this->parseIdentifier($v->expr); - if (!$this->hasVar($name)) { - $this->errorUndefinedVariable($v->expr); - } - if ($this->hasLocalVar($name) && $this->getVarType($name) !== self::TYPE_VAR && $this->getVarType($name) !== self::TYPE_REF) { - $isParameter = false; - foreach ($this->functionDef->argInfoList as $argInfo) { - if ($argInfo->name === $name) { - $isParameter = true; - break; - } + if ($this->isVarExpr($v->expr)) { + $name = $this->parseIdentifier($v->expr); + if (!$this->hasVar($name)) { + $this->errorUndefinedVariable($v->expr); } - if ($isParameter) { - $this->fatalError($v, 'A function returning by reference cannot return a native typed parameter'); + if ($this->hasLocalVar($name) && $this->getVarType($name) !== self::TYPE_VAR && $this->getVarType($name) !== self::TYPE_REF) { + $isParameter = false; + foreach ($this->functionDef->argInfoList as $argInfo) { + if ($argInfo->name === $name) { + $isParameter = true; + break; + } + } + if ($isParameter) { + $this->fatalError($v, 'A function returning by reference cannot return a native typed parameter'); + } + // The declaration is emitted after parsing the body, so a local can + // be promoted to Variant before C++ is generated. + $this->context->localVars[$name] = self::TYPE_VAR; } - // The declaration is emitted after parsing the body, so a local can - // be promoted to Variant before C++ is generated. - $this->context->localVars[$name] = self::TYPE_VAR; + return 'return ' . $name . '.toReference();'; + } + if ($this->isPropertyFetch($v->expr)) { + return 'return ' . $this->emitDynamicPropertyFetchRef($v->expr, $v) . ';'; } - return 'return ' . $name . '.toReference();'; + return 'return ' . $this->parseChainedExpr($v->expr, self::OP_REFVAL) . ';'; } if ($v->expr === null) { if ($this->functionDef->returnType === self::TYPE_VOID and !$this->context->inClosure) { diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index d5cadb77..7869a053 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -675,7 +675,49 @@ trait AssignOpTrait } $rightExpr = $tmpVar . ' = ' . $this->parseExpr($expr->expr); } elseif ($expr->expr instanceof Expr\FuncCall) { - $this->fatalError($expr, 'Cannot assign reference from a dynamic function call'); + $rightExpr = $tmpVar . ' = php::toReferenceExact(' . $this->parseExpr($expr->expr) . ')'; + } elseif ($expr->expr instanceof Expr\MethodCall) { + if (!$this->isNamedMethod($expr->expr->name) || !$this->isVarExpr($expr->expr->var)) { + $rightExpr = $tmpVar . ' = php::toReferenceExact(' . $this->parseExpr($expr->expr) . ')'; + } else { + $object = $this->parseIdentifier($expr->expr->var); + $method = $this->parseIdentifier($expr->expr->name); + $function = $this->findNativeMethod($expr->expr, $object, $method); + if (!$function) { + $rightExpr = $tmpVar . ' = php::toReferenceExact(' . $this->parseExpr($expr->expr) . ')'; + } else { + if (!$this->getFunction($function)->returnsByRef) { + $this->fatalError($expr, 'Cannot assign reference to a method that does not return by reference'); + } + $rightExpr = $tmpVar . ' = ' . $this->parseExpr($expr->expr); + } + } + } elseif ($expr->expr instanceof Expr\StaticCall) { + if (!$this->isNameExpr($expr->expr->class) || !$this->isIdExpr($expr->expr->name)) { + $rightExpr = $tmpVar . ' = php::toReferenceExact(' . $this->parseExpr($expr->expr) . ')'; + } else { + $class = $this->parseIdentifier($expr->expr->class); + if ($class === 'self') { + $class = $this->getFullClassName(); + } elseif ($class === 'parent') { + if (!$this->classDef || !$this->classDef->extends) { + $this->fatalError($expr, 'Cannot use "parent" outside a class or class does not extend any class'); + } + $class = $this->classDef->extends; + } elseif ($class !== 'static') { + $class = $this->getNamespacedClassName($class); + } + $method = $this->parseIdentifier($expr->expr->name); + $function = $class === 'static' ? false : $this->getNativeMethod($expr->expr, $class, $method); + if (!$function) { + $rightExpr = $tmpVar . ' = php::toReferenceExact(' . $this->parseExpr($expr->expr) . ')'; + } else { + if (!$this->getFunction($function)->returnsByRef) { + $this->fatalError($expr, 'Cannot assign reference to a static method that does not return by reference'); + } + $rightExpr = $tmpVar . ' = ' . $this->parseExpr($expr->expr); + } + } } elseif ($this->isPropertyFetch($expr->expr)) { $left = $this->parseIdentifier($expr->var); $rightExpr = $tmpVar . ' = ' . $this->emitDynamicPropertyFetchRef($expr->expr, $expr); diff --git a/tests/aot/ref/function-return-reference.phpt b/tests/aot/ref/function-return-reference.phpt index 9655e5cc..3836d2fa 100644 --- a/tests/aot/ref/function-return-reference.phpt +++ b/tests/aot/ref/function-return-reference.phpt @@ -7,6 +7,11 @@ function &value_ref() global $value; return $value; } +function value_copy() +{ + global $value; + return $value; +} function main() { global $value; @@ -24,6 +29,18 @@ function main() require __DIR__ . '/function-return-reference-require.inc'; var_dump(value_ref()); + + $callback = 'value_ref'; + $dynamicAlias =& $callback(); + $dynamicAlias = 'from dynamic callback'; + var_dump(value_ref()); + + $callback = 'value_copy'; + try { + $badAlias =& $callback(); + } catch (TypeError $e) { + echo "dynamic callback TypeError\n"; + } } function &local_ref() @@ -37,3 +54,5 @@ int(42) string(10) "kept alive" string(9) "from eval" string(12) "from require" +string(21) "from dynamic callback" +dynamic callback TypeError diff --git a/tests/aot/ref/method-return-reference-require.inc b/tests/aot/ref/method-return-reference-require.inc new file mode 100644 index 00000000..e624ac78 --- /dev/null +++ b/tests/aot/ref/method-return-reference-require.inc @@ -0,0 +1,6 @@ +valueRef(); +$requireMethodAlias = 'method require'; +$requireStaticAlias =& RefBox::staticRef(); +$requireStaticAlias = 'static require'; +?> diff --git a/tests/aot/ref/method-return-reference.phpt b/tests/aot/ref/method-return-reference.phpt new file mode 100644 index 00000000..f6a45e8b --- /dev/null +++ b/tests/aot/ref/method-return-reference.phpt @@ -0,0 +1,87 @@ +--TEST-- +methods returning by reference preserve aliases +--FILE-- +value; + } + + public static function &staticRef() + { + return self::$staticValue; + } + + public function valueCopy() + { + return $this->value; + } + + public static function staticCopy() + { + return self::$staticValue; + } +} + +function main() +{ + $box = new RefBox(); + + $methodAlias =& $box->valueRef(); + $methodAlias = 42; + var_dump($box->valueRef()); + + $staticAlias =& RefBox::staticRef(); + $staticAlias = 77; + var_dump(RefBox::staticRef()); + + $GLOBALS['eval_box'] = $box; + eval('$evalMethodAlias =& $GLOBALS["eval_box"]->valueRef(); $evalMethodAlias = "method eval"; $evalStaticAlias =& RefBox::staticRef(); $evalStaticAlias = "static eval";'); + var_dump($box->valueRef()); + var_dump(RefBox::staticRef()); + + require __DIR__ . '/method-return-reference-require.inc'; + var_dump($box->valueRef()); + var_dump(RefBox::staticRef()); + + $method = 'valueRef'; + $dynamicMethodAlias =& $box->$method(); + $dynamicMethodAlias = 'method dynamic'; + var_dump($box->valueRef()); + + $staticMethod = 'staticRef'; + $dynamicStaticAlias =& RefBox::$staticMethod(); + $dynamicStaticAlias = 'static dynamic'; + var_dump(RefBox::staticRef()); + + $method = 'valueCopy'; + try { + $badMethodAlias =& $box->$method(); + } catch (TypeError $e) { + echo "dynamic method TypeError\n"; + } + + $staticMethod = 'staticCopy'; + try { + $badStaticAlias =& RefBox::$staticMethod(); + } catch (TypeError $e) { + echo "dynamic static TypeError\n"; + } +} +?> +--EXPECT-- +int(42) +int(77) +string(11) "method eval" +string(11) "static eval" +string(14) "method require" +string(14) "static require" +string(14) "method dynamic" +string(14) "static dynamic" +dynamic method TypeError +dynamic static TypeError