feat(parser): add support for method and static call reference assignments

- Implement handling of MethodCall expressions in assignment operations with references
- Add StaticCall expression support for reference assignments with proper name validation
- Introduce native method lookup and reference return validation for method calls
- Add static method reference assignment with class name resolution and parent/self handling
- Update function return by reference to support property fetches and chained expressions
- Modify documentation to reflect updated reference assignment capabilities
- Add comprehensive tests for method and static call reference return scenarios
pull/16/head
韩天峰 2 months ago
parent 128bfa7e85
commit 2503c79f03
  1. 4
      docs/INCOMPATIBLE_PHP_FEATURES.md
  2. 45
      src/CompilerBase.php
  3. 44
      src/Parser/AssignOpTrait.php
  4. 19
      tests/aot/ref/function-return-reference.phpt
  5. 6
      tests/aot/ref/method-return-reference-require.inc
  6. 87
      tests/aot/ref/method-return-reference.phpt

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

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

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

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

@ -0,0 +1,6 @@
<?php
$requireMethodAlias =& $GLOBALS['eval_box']->valueRef();
$requireMethodAlias = 'method require';
$requireStaticAlias =& RefBox::staticRef();
$requireStaticAlias = 'static require';
?>

@ -0,0 +1,87 @@
--TEST--
methods returning by reference preserve aliases
--FILE--
<?php
class RefBox
{
public $value = 1;
public static $staticValue = 10;
public function &valueRef()
{
return $this->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
Loading…
Cancel
Save