feat(compiler): optimize fixed array returns as tuples

pull/20/head
韩天峰 1 month ago
parent 44fdc0b387
commit 4caed34458
  1. 18
      phpunit/code/multi-return-tuple.php
  2. 40
      phpunit/src/MultiReturnTest.php
  3. 19
      src/CompilerBase.php
  4. 12
      src/Entity/FunctionDef.php
  5. 46
      src/Parser/AssignOpTrait.php
  6. 6
      src/Parser/FunctionCallTrait.php
  7. 41
      src/Preprocessor.php
  8. 30
      src/Translator.php
  9. 22
      tests/compiler/array/multi-return-namespace.phpt
  10. 77
      tests/compiler/array/multi-return-tuple.phpt

@ -0,0 +1,18 @@
<?php
function phpunit_multi_values(): array
{
$first = 1;
$second = 'two';
return [$first, $second];
}
function phpunit_multi_consumer(): void
{
[$first, $second] = phpunit_multi_values();
$array = phpunit_multi_values();
}
function phpunit_multi_side_effect(): array
{
return [time(), 2];
}

@ -0,0 +1,40 @@
<?php
use PHPUnit\Framework\TestCase;
use TypePhp\CompilerTest;
final class MultiReturnTest extends TestCase
{
public function testGeneratesTupleFastPathAndArrayCompatibilityAdapter(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$file = __DIR__ . '/../code/multi-return-tuple.php';
$compiler->addFiles([$file]);
$compiler->prepareFile($file);
$cppFile = $compiler->convertFile($file);
$code = file_get_contents($cppFile);
$this->assertStringContainsString(
'std::tuple<php::Var, php::Var> 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,
);
}
}

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

@ -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')) . '>';
}
}

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

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

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

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

@ -0,0 +1,22 @@
--TEST--
Tuple multi-return fast path supports namespaced functions
--FILE--
<?php
namespace MultiReturnExample {
function values(): array
{
return [10, 'namespaced'];
}
}
namespace {
function main(): void
{
[$number, $text] = \MultiReturnExample\values();
var_dump($number, $text);
}
}
?>
--EXPECT--
int(10)
string(10) "namespaced"

@ -0,0 +1,77 @@
--TEST--
Fixed list returns use tuple fast path while preserving array semantics
--FILE--
<?php
function multi_values(): array
{
$a = 1;
$b = 'two';
$c = true;
return [$a, $b, $c];
}
function inferred_multi_values()
{
return [4, 'five'];
}
function multi_with_default(int $value = 7): array
{
return [$value, 'default'];
}
function main(): void
{
[$a, $b, $c] = multi_values();
var_dump($a, $b, $c);
$array = multi_values();
var_dump($array);
$function = 'multi_values';
var_dump($function());
[$x, $y] = multi_values();
var_dump($x, $y);
[$h, , $j] = multi_values();
var_dump($h, $j);
[$d, $e] = inferred_multi_values();
var_dump($d, $e);
[$f, $g] = multi_with_default();
[$k, $l] = multi_with_default(value: 8);
var_dump($f, $g, $k, $l);
}
?>
--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"
Loading…
Cancel
Save