feat(parser): add support for functions returning by reference

- Add returnsByRef property to FunctionDef entity
- Remove restriction on reference return types in Preprocessor
- Implement proper reference return handling in CompilerBase
- Add validation for reference assignment from function calls
- Update Translator to generate correct C++ code for reference returns
- Add compatibility checks for method override with reference returns
- Include test case for function returning by reference preserving aliases
pull/16/head
韩天峰 2 months ago
parent 86b4d64d7d
commit 128bfa7e85
  1. 31
      src/CompilerBase.php
  2. 1
      src/Entity/FunctionDef.php
  3. 16
      src/Parser/AssignOpTrait.php
  4. 5
      src/Preprocessor.php
  5. 12
      src/Translator.php
  6. 4
      tests/aot/ref/function-return-reference-require.inc
  7. 39
      tests/aot/ref/function-return-reference.phpt

@ -1914,6 +1914,34 @@ class CompilerBase implements PropertyAccessContext
protected function parseReturn(Node\Stmt\Return_ $v): string
{
if ($this->functionDef->returnsByRef) {
if ($v->expr === null) {
return 'return ' . self::TYPE_REF . '{};';
}
if (!$this->isVarExpr($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 ($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;
}
return 'return ' . $name . '.toReference();';
}
if ($v->expr === null) {
if ($this->functionDef->returnType === self::TYPE_VOID and !$this->context->inClosure) {
return 'return;';
@ -7874,6 +7902,9 @@ class CompilerBase implements PropertyAccessContext
protected function genReturnCode(): string
{
if ($this->functionDef->returnsByRef) {
return $this->getIndent() . 'return ' . self::TYPE_REF . '{};';
}
if ($this->shouldCheckClosureReturnType()) {
return $this->genClosureCheckedReturn(self::VALUE_NULL);
}

@ -26,6 +26,7 @@ class FunctionDef
public bool $method = false;
public bool $stub = false;
public bool $returnTypeUndeclared = false;
public bool $returnsByRef = false;
/**
* @var string 必须是带有命名空间的完整类名

@ -660,6 +660,22 @@ trait AssignOpTrait
if ($this->isVarExpr($expr->expr)) {
$rightExpr = $tmpVar . ' = ' . $this->parseIdentifier($expr->expr) . '.toReference()';
} elseif ($expr->expr instanceof Expr\FuncCall && $this->isNameExpr($expr->expr->name)) {
$name = $this->parseIdentifier($expr->expr->name);
$function = $this->findNativeFunction($name);
if ($function) {
if (!$this->getFunction($function)->returnsByRef) {
$this->fatalError($expr, 'Cannot assign reference to a function that does not return by reference');
}
} else {
$reflection = \TypePhp\Reflection::getFunction(ltrim($this->getNamespacedFuncName($name), '\\'));
if ($reflection === null || !$reflection->isInternal() || !$reflection->returnsReference()) {
$this->fatalError($expr, 'Cannot assign reference to a function that does not return by reference');
}
}
$rightExpr = $tmpVar . ' = ' . $this->parseExpr($expr->expr);
} elseif ($expr->expr instanceof Expr\FuncCall) {
$this->fatalError($expr, 'Cannot assign reference from a dynamic function call');
} elseif ($this->isPropertyFetch($expr->expr)) {
$left = $this->parseIdentifier($expr->var);
$rightExpr = $tmpVar . ' = ' . $this->emitDynamicPropertyFetchRef($expr->expr, $expr);

@ -419,10 +419,6 @@ class Preprocessor extends CompilerBase
$this->fatalError($v, 'The return type of the function `' . $name . '` must be specified');
}
}
// 返回值不能是引用类型
if ($v->byRef) {
$this->fatalError($v, 'The return type of the function `' . $v->name . '` cannot be a reference type');
}
if ($this->method and $v->returnType !== null) {
$methodName = $this->class . '::' . $this->method;
if (in_array($this->method, ['__construct', '__destruct'], true)) {
@ -446,6 +442,7 @@ class Preprocessor extends CompilerBase
$functionDef->returnClass = $class;
$functionDef->stub = $this->stubFile;
$functionDef->returnTypeUndeclared = $v->returnType === null;
$functionDef->returnsByRef = $v->byRef;
if ($v->returnType instanceof NullableType || $v->returnType instanceof UnionType || $v->returnType instanceof IntersectionType) {
$typeInfo = $this->buildTypeCheckFromNode($v->returnType);

@ -1831,7 +1831,7 @@ CODE;
$code .= $this->genDefaultArgumentHelpers();
foreach ($this->functions as $name => $func) {
$code .= 'extern ' . $func->returnType . ' ' . self::PREFIX . $name . '(';
$code .= 'extern ' . ($func->returnsByRef ? self::TYPE_REF : $func->returnType) . ' ' . self::PREFIX . $name . '(';
$list = [];
if ($func->method) {
$list[] = self::TYPE_OBJECT . ' &this_';
@ -3232,7 +3232,9 @@ CODE;
if ($functionDef->returnType !== self::TYPE_VOID) {
$cppCode .= $this->getIndent() . 'auto retval = ' . $fn . '(' . $callParams . ');' . PHP_EOL;
$cppCode .= $this->getIndent() . 'php::move(retval, return_value);' . PHP_EOL;
$cppCode .= $this->getIndent() . 'php::deref(return_value);' . PHP_EOL;
if (!$functionDef->returnsByRef) {
$cppCode .= $this->getIndent() . 'php::deref(return_value);' . PHP_EOL;
}
} else {
$cppCode .= $this->getIndent() . $fn . '(' . $callParams . ');' . PHP_EOL;
}
@ -3448,7 +3450,8 @@ CODE;
$stmts = $this->genReturnCode();
}
$functionDeclCode = $this->getReturnType() . ' ' . self::PREFIX . $name . '(';
$cppReturnType = $this->functionDef->returnsByRef ? self::TYPE_REF : $this->getReturnType();
$functionDeclCode = $cppReturnType . ' ' . self::PREFIX . $name . '(';
if ($this->class) {
$functionDeclCode .= self::TYPE_OBJECT . ' &this_';
if ($this->functionDef->params) {
@ -3579,6 +3582,9 @@ CODE;
if (!$this->isReturnTypeOverrideCompatible($childFuncDef, $parentFuncDef)) {
$this->fatalMethodOverrideIncompatible($v, $className, $methodName, $parentClass);
}
if ($childFuncDef->returnsByRef !== $parentFuncDef->returnsByRef) {
$this->fatalMethodOverrideIncompatible($v, $className, $methodName, $parentClass);
}
// Child methods may add optional trailing parameters, but they cannot
// require more arguments than the parent contract.

@ -0,0 +1,4 @@
<?php
$requireAlias =& value_ref();
$requireAlias = 'from require';
?>

@ -0,0 +1,39 @@
--TEST--
function returning by reference preserves aliases
--FILE--
<?php
function &value_ref()
{
global $value;
return $value;
}
function main()
{
global $value;
$value = 1;
$alias =& value_ref();
$alias = 42;
var_dump(value_ref());
$localAlias =& local_ref();
$localAlias = 'kept alive';
var_dump($localAlias);
eval('$evalAlias =& value_ref(); $evalAlias = "from eval";');
var_dump(value_ref());
require __DIR__ . '/function-return-reference-require.inc';
var_dump(value_ref());
}
function &local_ref()
{
$value = 1;
return $value;
}
?>
--EXPECT--
int(42)
string(10) "kept alive"
string(9) "from eval"
string(12) "from require"
Loading…
Cancel
Save