diff --git a/phpunit/code/multi-return-tuple.php b/phpunit/code/multi-return-tuple.php new file mode 100644 index 00000000..e2eb4c87 --- /dev/null +++ b/phpunit/code/multi-return-tuple.php @@ -0,0 +1,18 @@ +addFiles([$file]); + $compiler->prepareFile($file); + $cppFile = $compiler->convertFile($file); + $code = file_get_contents($cppFile); + + $this->assertStringContainsString( + 'std::tuple typephp::detail::multi_return::php_phpunit_multi_values()', + $code, + ); + $this->assertStringContainsString( + 'std::tie(first, second) = typephp::detail::multi_return::php_phpunit_multi_values()', + $code, + ); + $this->assertStringContainsString( + 'php::Array php_phpunit_multi_values()', + $code, + ); + $this->assertStringContainsString( + 'array = php_phpunit_multi_values()', + $code, + ); + $this->assertStringNotContainsString( + 'typephp::detail::multi_return::php_phpunit_multi_side_effect', + $code, + ); + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 0e7057da..037d62e4 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -143,6 +143,8 @@ class CompilerBase implements PropertyAccessContext protected const int COMPOSITE_TYPE_MATCH = 1; protected const string ATTR_ARRAY_DIM_FETCH_UPDATE = 'aotArrayDimFetchUpdate'; protected const string ATTR_PROPERTY_FETCH_UPDATE = 'aotPropertyFetchUpdate'; + protected const string ATTR_STATEMENT_EXPRESSION = 'aotStatementExpression'; + protected const string ATTR_MULTI_RETURN_IMPL = 'aotMultiReturnImpl'; /** * Keyword methods (to* builtins) with mandated return types. @@ -195,6 +197,7 @@ class CompilerBase implements PropertyAccessContext public const string NAMESPACE_SEPARATOR = '__'; public const string PREFIX = 'php_'; + protected const string MULTI_RETURN_NAMESPACE = 'typephp::detail::multi_return'; public const string OP_ISSET = 'isset'; public const string OP_EMPTY = 'empty'; public const string OP_NOT_EMPTY = 'notEmpty'; @@ -1497,6 +1500,7 @@ class CompilerBase implements PropertyAccessContext $lines[] = $this->getComment($v, $class); switch ($class) { case 'Stmt_Expression': + $v->expr->setAttribute(self::ATTR_STATEMENT_EXPRESSION, true); if ($this->inGeneratorBody && $v->expr instanceof Expr\Yield_) { $result = $this->parseYieldStmt($v->expr); } elseif ($this->inGeneratorBody && $v->expr instanceof Expr\YieldFrom) { @@ -1956,6 +1960,16 @@ class CompilerBase implements PropertyAccessContext return 'return ' . self::VALUE_NULL . ';'; } } + if (!$this->context->inClosure && $this->functionDef->hasMultiReturn()) { + if (!$v->expr instanceof Expr\Array_) { + throw new \LogicException('Optimized multi-return function must return a fixed array literal'); + } + $values = []; + foreach ($v->expr->items as $item) { + $values[] = Type::VAR . '(' . $this->parseExprAsValue($item->value) . ')'; + } + return 'return ' . $this->functionDef->getMultiReturnCppType() . '{' . implode(', ', $values) . '};'; + } // 实际函数的返回值 $type = $this->detectTypeOfExpr($v->expr); if ($this->isCurrentConstructor() && !$this->context->inClosure) { @@ -2032,6 +2046,11 @@ class CompilerBase implements PropertyAccessContext return $code; } + protected function getMultiReturnImplName(string $nativeName): string + { + return self::MULTI_RETURN_NAMESPACE . '::' . self::PREFIX . $nativeName; + } + protected function genClosureCheckedReturn(string $exprCode): string { [$code, $tmpVar] = $this->genClosureCheckedReturnAssignment($exprCode); diff --git a/src/Entity/FunctionDef.php b/src/Entity/FunctionDef.php index 132cc5e8..b4ec6058 100644 --- a/src/Entity/FunctionDef.php +++ b/src/Entity/FunctionDef.php @@ -28,6 +28,8 @@ class FunctionDef public bool $returnTypeUndeclared = false; public bool $returnsByRef = false; public bool $generator = false; + /** Number of fixed positional values returned through the internal tuple fast path. */ + public int $multiReturnCount = 0; /** Source file containing this function definition. */ public string $sourceFile = ''; /** First source line of this function definition. */ @@ -63,4 +65,14 @@ class FunctionDef { return $this->argInfoList && $this->argInfoList[count($this->argInfoList) - 1]->variadic; } + + public function hasMultiReturn(): bool + { + return $this->multiReturnCount > 0; + } + + public function getMultiReturnCppType(): string + { + return 'std::tuple<' . implode(', ', array_fill(0, $this->multiReturnCount, 'php::Var')) . '>'; + } } diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index b3275e44..c8885a17 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -101,9 +101,55 @@ trait AssignOpTrait if ($this->isAssignExpr($right)) { return $this->parseRightAssociativeAssign($left, $right); } + if ($left instanceof Expr\List_ && $v->getAttribute(self::ATTR_STATEMENT_EXPRESSION, false)) { + $optimized = $this->parseAssignToMultiReturn($left, $right); + if ($optimized !== null) { + return $optimized; + } + } return $this->parseAssignFinally($left, $right); } + private function parseAssignToMultiReturn(Expr\List_ $left, Expr $right): ?string + { + if (!$right instanceof Expr\FuncCall + || (!$this->isNameExpr($right->name) && !$this->isFullNameExpr($right->name))) { + return null; + } + + $nativeFunc = $this->findNativeFunction($this->parseIdentifier($right->name)); + if ($nativeFunc === false) { + return null; + } + $functionDef = $this->getFunction($nativeFunc); + if (!$functionDef->hasMultiReturn() + || $functionDef->multiReturnCount !== count($left->items) + || $this->shouldUseDynamicCallForNativeArgs($nativeFunc, $right->args)) { + return null; + } + + $variables = []; + foreach ($left->items as $item) { + if (!$item instanceof ArrayItem || $item->key !== null || $item->unpack || $item->byRef + || !$this->isVarExpr($item->value) || !is_string($item->value->name)) { + return null; + } + $name = $this->parseWritableIdentifier($item->value); + if ($this->hasVar($name) && $this->getVarType($name) !== Type::VAR) { + return null; + } + $variables[] = $name; + } + + foreach ($variables as $name) { + if (!$this->hasVar($name)) { + $this->addLocalVar($name, Type::VAR); + } + } + $right->setAttribute(self::ATTR_MULTI_RETURN_IMPL, true); + return 'std::tie(' . implode(', ', $variables) . ') = ' . $this->parseFuncCall($right); + } + protected function parseAssignToList(Expr $left, Expr $right): string { $items = $left->items; diff --git a/src/Parser/FunctionCallTrait.php b/src/Parser/FunctionCallTrait.php index f5c2f476..ebf7d5cd 100644 --- a/src/Parser/FunctionCallTrait.php +++ b/src/Parser/FunctionCallTrait.php @@ -102,7 +102,10 @@ trait FunctionCallTrait return $this->genRuntimeFunctionCall($this->getFuncPtr($functionDef->getNamespacedName()), $expr->args, $name); } try { - return self::PREFIX . $nativeFn . '(' . $this->parseNativeCallArgs($expr->args, $nativeFn) . ')'; + $callee = $expr->getAttribute(self::ATTR_MULTI_RETURN_IMPL, false) + ? $this->getMultiReturnImplName($nativeFn) + : self::PREFIX . $nativeFn; + return $callee . '(' . $this->parseNativeCallArgs($expr->args, $nativeFn) . ')'; } catch (PlaceHolder) { return $this->genPlaceHolder($this->identifierToStr($expr->name)); } @@ -133,4 +136,3 @@ trait FunctionCallTrait } } } - diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 524b8e63..0273d2cd 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -459,6 +459,13 @@ class Preprocessor extends CompilerBase } } + if (!$this->method && $this->canOptimizeMultiReturn($v, $functionDef)) { + $functionDef->multiReturnCount = count($v->stmts[array_key_last($v->stmts)]->expr->items); + // The fixed tuple is an internal ABI detail. PHP and ordinary native + // callers continue to observe an array return value. + $functionDef->returnType = Type::ARRAY; + } + $this->parseParams($v->params, $functionDef); // main 函数,返回值必须为 void 类型,参数必须为空或者 argc, argv 两个参数 @@ -482,6 +489,40 @@ class Preprocessor extends CompilerBase return $functionDef; } + private function canOptimizeMultiReturn(Node\Stmt\Function_|Node\Stmt\ClassMethod $function, FunctionDef $functionDef): bool + { + if ($functionDef->stub || $functionDef->generator || $functionDef->returnsByRef + || ($functionDef->returnType !== Type::ARRAY && !$functionDef->returnTypeUndeclared) + || !$function->stmts) { + return false; + } + + $return = $function->stmts[array_key_last($function->stmts)] ?? null; + if (!$return instanceof Node\Stmt\Return_ || !$return->expr instanceof Node\Expr\Array_ + || count($return->expr->items) < 2) { + return false; + } + + $returns = (new NodeFinder())->findInstanceOf($function->stmts, Node\Stmt\Return_::class); + if (count($returns) !== 1) { + return false; + } + + foreach ($return->expr->items as $item) { + if ($item === null || $item->key !== null || $item->unpack || $item->byRef) { + return false; + } + $value = $item->value; + if (($value instanceof Node\Expr\Variable && is_string($value->name)) + || $value instanceof Node\Scalar + || $value instanceof Node\Expr\ConstFetch) { + continue; + } + return false; + } + return true; + } + protected function prepareFunction(Node\Stmt\ClassMethod|Node\Stmt\Function_ $v): void { $this->resetFunction(); diff --git a/src/Translator.php b/src/Translator.php index 4362ba8a..1ecc9a71 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1524,7 +1524,6 @@ CODE; $code .= $this->genDefaultArgumentHelpers(); foreach ($this->symbols->functions() as $name => $func) { - $code .= 'extern ' . ($func->returnsByRef ? Type::REF : $func->returnType) . ' ' . self::PREFIX . $name . '('; $list = []; if ($func->method) { $list[] = Type::OBJECT . ' &this_'; @@ -1543,8 +1542,13 @@ CODE; $list[] = $arg; } } - $code .= implode(', ', $list); - $code .= ');' . PHP_EOL; + $params = implode(', ', $list); + $code .= 'extern ' . ($func->returnsByRef ? Type::REF : $func->returnType) . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL; + if ($func->hasMultiReturn()) { + $code .= 'namespace ' . self::MULTI_RETURN_NAMESPACE . ' {' . PHP_EOL; + $code .= 'extern ' . $func->getMultiReturnCppType() . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL; + $code .= '}' . PHP_EOL; + } } $code .= PHP_EOL; @@ -2945,8 +2949,12 @@ CODE; $stmts = $this->genReturnCode(); } - $cppReturnType = $this->functionDef->returnsByRef ? Type::REF : $this->getReturnType(); - $functionDeclCode = $cppReturnType . ' ' . self::PREFIX . $name . '('; + $multiReturn = $this->functionDef->hasMultiReturn(); + $cppReturnType = $multiReturn + ? $this->functionDef->getMultiReturnCppType() + : ($this->functionDef->returnsByRef ? Type::REF : $this->getReturnType()); + $nativeName = self::PREFIX . $name; + $functionDeclCode = $cppReturnType . ' ' . ($multiReturn ? $this->getMultiReturnImplName($name) : $nativeName) . '('; if ($this->class) { $functionDeclCode .= Type::OBJECT . ' &this_'; if ($this->functionDef->params) { @@ -2992,6 +3000,18 @@ CODE; $code .= $stmts; $code .= "}\n"; + if ($multiReturn) { + $forwardArgs = implode(', ', array_map( + static fn($argInfo) => $argInfo->name, + $this->functionDef->argInfoList, + )); + $code .= Type::ARRAY . ' ' . $nativeName . '(' . $this->functionDef->params . ') {' . PHP_EOL; + $this->indentLevel++; + $code .= $this->getIndent() . 'return ' . Type::ARRAY . '(' . $this->getMultiReturnImplName($name) . '(' . $forwardArgs . '));' . PHP_EOL; + $this->indentLevel--; + $code .= '}' . PHP_EOL; + } + $this->resetFunction(); return $code; diff --git a/tests/compiler/array/multi-return-namespace.phpt b/tests/compiler/array/multi-return-namespace.phpt new file mode 100644 index 00000000..0b4bb40f --- /dev/null +++ b/tests/compiler/array/multi-return-namespace.phpt @@ -0,0 +1,22 @@ +--TEST-- +Tuple multi-return fast path supports namespaced functions +--FILE-- + +--EXPECT-- +int(10) +string(10) "namespaced" diff --git a/tests/compiler/array/multi-return-tuple.phpt b/tests/compiler/array/multi-return-tuple.phpt new file mode 100644 index 00000000..a15f2b8d --- /dev/null +++ b/tests/compiler/array/multi-return-tuple.phpt @@ -0,0 +1,77 @@ +--TEST-- +Fixed list returns use tuple fast path while preserving array semantics +--FILE-- + +--EXPECT-- +int(1) +string(3) "two" +bool(true) +array(3) { + [0]=> + int(1) + [1]=> + string(3) "two" + [2]=> + bool(true) +} +array(3) { + [0]=> + int(1) + [1]=> + string(3) "two" + [2]=> + bool(true) +} +int(1) +string(3) "two" +int(1) +bool(true) +int(4) +string(4) "five" +int(7) +string(7) "default" +int(8) +string(7) "default"