From 128bfa7e856644b81e436e6660d83c77a2d9f27c Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 10 Jul 2026 12:43:48 +0800 Subject: [PATCH] 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 --- src/CompilerBase.php | 31 +++++++++++++++ src/Entity/FunctionDef.php | 1 + src/Parser/AssignOpTrait.php | 16 ++++++++ src/Preprocessor.php | 5 +-- src/Translator.php | 12 ++++-- .../ref/function-return-reference-require.inc | 4 ++ tests/aot/ref/function-return-reference.phpt | 39 +++++++++++++++++++ 7 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 tests/aot/ref/function-return-reference-require.inc create mode 100644 tests/aot/ref/function-return-reference.phpt diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 6c7d6849..811bcad1 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -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); } diff --git a/src/Entity/FunctionDef.php b/src/Entity/FunctionDef.php index 255af225..5e9c14d2 100644 --- a/src/Entity/FunctionDef.php +++ b/src/Entity/FunctionDef.php @@ -26,6 +26,7 @@ class FunctionDef public bool $method = false; public bool $stub = false; public bool $returnTypeUndeclared = false; + public bool $returnsByRef = false; /** * @var string 必须是带有命名空间的完整类名 diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 1bada53d..d5cadb77 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -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); diff --git a/src/Preprocessor.php b/src/Preprocessor.php index c33a4480..0fc254cb 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -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); diff --git a/src/Translator.php b/src/Translator.php index d7ba6eda..af16c2ad 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -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. diff --git a/tests/aot/ref/function-return-reference-require.inc b/tests/aot/ref/function-return-reference-require.inc new file mode 100644 index 00000000..b8dd4afe --- /dev/null +++ b/tests/aot/ref/function-return-reference-require.inc @@ -0,0 +1,4 @@ + diff --git a/tests/aot/ref/function-return-reference.phpt b/tests/aot/ref/function-return-reference.phpt new file mode 100644 index 00000000..9655e5cc --- /dev/null +++ b/tests/aot/ref/function-return-reference.phpt @@ -0,0 +1,39 @@ +--TEST-- +function returning by reference preserves aliases +--FILE-- + +--EXPECT-- +int(42) +string(10) "kept alive" +string(9) "from eval" +string(12) "from require"