test(closure): 添加闭包use值和引用捕获功能测试

- 新增closure-use-reference-capture.phpt测试文件验证闭包捕获行为
- 添加ClosureTest类实现use引用捕获编译测试
- 在CompilerBase中添加stringifyParsedExpr方法处理表达式字符串化
- 重构函数调用参数处理逻辑提取为独立方法确保数组和命名参数正确处理
- 修改for循环条件和迭代部分的表达式解析确保正确转换
- 添加use-reference-capture.php测试代码验证闭包引用捕获功能
pull/4/head
韩天峰 2 months ago
parent 248156b3b4
commit 903af8c477
  1. 22
      phpunit/code/closure/use-reference-capture.php
  2. 20
      phpunit/src/ClosureTest.php
  3. 107
      src/Php/CompilerBase.php
  4. 78
      tests/aot/closure/closure-use-reference-capture.phpt

@ -0,0 +1,22 @@
<?php
function main(): void
{
$arr = [1, 2];
$copy = function () use ($arr) {
$arr[] = 3;
return $arr;
};
$copy();
$ref = function () use (&$arr) {
$arr[] = 4;
return $arr;
};
$ref();
$value = 'old';
$returnCapturedRef = function () use (&$value) {
return $value;
};
$returnCapturedRef();
}

@ -0,0 +1,20 @@
<?php
use PhpAot\Php\CompilerTest;
class ClosureTest extends \PHPUnit\Framework\TestCase
{
public function testUseReferenceCaptureCompiles(): void
{
global $translator;
$testFile = ROOT_PATH . '/phpunit/code/closure/use-reference-capture.php';
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$compiler->convertFile($testFile);
$this->assertTrue(true);
}
}

@ -1444,6 +1444,25 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
return [$value, $beforeStmts, $afterStmts];
}
protected function stringifyParsedExpr(mixed $expr): string
{
if (is_string($expr)) {
return $expr;
}
if (is_int($expr) || is_float($expr)) {
return (string) $expr;
}
if (is_object($expr)) {
if (method_exists($expr, 'toString')) {
return $expr->toString();
}
if (method_exists($expr, '__toString')) {
return $expr->__toString();
}
}
throw new \LogicException('Parsed expression must be stringable');
}
protected function appendCapturedStmtLines(string &$code, array $stmts): void
{
if ($stmts) {
@ -2744,6 +2763,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$list_expr = [];
foreach ($init as $expr) {
[$initExpr, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr);
$initExpr = $this->stringifyParsedExpr($initExpr);
$this->appendCapturedStmtLines($code, $beforeStmts);
$list_expr[] = $initExpr;
if ($afterStmts) {
@ -2754,12 +2774,15 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$code .= implode(";\n" . $this->getIndent(), $list_expr);
$list_cond = [];
$list_cond_expr = [];
$hasCondStmts = false;
foreach ($cond as $expr) {
$this->assertExprCanBeUsedAsCondition($expr, 'for condition');
[$condExpr, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr);
$condExpr = $this->stringifyParsedExpr($condExpr);
$hasCondStmts = $hasCondStmts || $beforeStmts || $afterStmts;
$list_cond[] = [$condExpr, $beforeStmts, $afterStmts];
$list_cond_expr[] = $condExpr;
}
$code .= $this->parseBeforeStmtLines() . PHP_EOL;
@ -2786,13 +2809,14 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$condCode .= $this->getIndent() . '}()';
$code .= $condCode;
} else {
$code .= implode(', ', array_map(static fn($item) => $item[0], $list_cond));
$code .= implode(', ', $list_cond_expr);
}
$code .= '; ';
$list_loop = [];
foreach ($loop as $expr) {
[$loopExpr, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr);
$loopExpr = $this->stringifyParsedExpr($loopExpr);
if ($beforeStmts || $afterStmts) {
$loopCode = '[&]() {';
$this->appendCapturedStmtLines($loopCode, $beforeStmts);
@ -3551,34 +3575,8 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$hasNamedArg = false;
$hasUnpack = false;
$ensureArrayArgs = function () use (&$arrayArgsVar, &$list_args): string {
if ($arrayArgsVar === null) {
$arrayArgsVar = $this->genTmpVarName();
$this->context->beforeStmtLines[] = self::TYPE_ARRAY . ' ' . $arrayArgsVar . '{' . implode(', ', $list_args) . '};';
$list_args = [];
}
return $arrayArgsVar;
};
$ensureNamedArgs = function () use (&$namedArgsVar): string {
if ($namedArgsVar === null) {
$namedArgsVar = $this->genTmpVarName();
$this->context->beforeStmtLines[] = self::TYPE_ARRAY . ' ' . $namedArgsVar . ';';
$this->context->afterStmtLines[] = $namedArgsVar . '.unset();';
}
return $namedArgsVar;
};
$addPositionalArg = function (string $value) use (&$arrayArgsVar, &$list_args): void {
if ($arrayArgsVar !== null) {
$this->context->beforeStmtLines[] = $arrayArgsVar . '.append(' . $value . ');';
} else {
$list_args[] = $value;
}
};
if ($forceArrayArgs) {
$ensureArrayArgs();
$this->ensureCallArrayArgs($arrayArgsVar, $list_args);
}
foreach ($args as $i => $arg) {
@ -3590,7 +3588,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$this->fatalError($arg, 'Cannot use argument unpacking after named arguments');
}
$hasUnpack = true;
$arrayArgs = $ensureArrayArgs();
$arrayArgs = $this->ensureCallArrayArgs($arrayArgsVar, $list_args);
$this->context->beforeStmtLines[] = $arrayArgs . '.merge(' . $this->parseArrayArg($arg) . ');';
continue;
}
@ -3608,10 +3606,10 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
? $this->parseReferenceCallArgValue($arg)
: $this->parseCallArgValue($arg);
if ($separateNamedArgs) {
$namedArgsArray = $ensureNamedArgs();
$namedArgsArray = $this->ensureCallNamedArgs($namedArgsVar);
$this->context->beforeStmtLines[] = $namedArgsArray . '.set(' . $this->getLiteralString($arg->name->name) . ', ' . $value . ');';
} else {
$arrayArgs = $ensureArrayArgs();
$arrayArgs = $this->ensureCallArrayArgs($arrayArgsVar, $list_args);
$this->context->beforeStmtLines[] = $arrayArgs . '.set(' . $this->getLiteralString($arg->name->name) . ', ' . $value . ');';
}
continue;
@ -3626,7 +3624,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
if ($this->isVarExpr($arg->value)) {
$name = $this->parseIdentifier($arg->value);
if ($byRef) {
$addPositionalArg($this->parseArgRefVar($arg, $name));
$this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args);
continue;
}
if (!$this->hasVar($name)) {
@ -3638,7 +3636,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$this->fatalError($arg, 'Undefined variable `$' . $obj . '`');
}
if ($byRef) {
$addPositionalArg($obj . '.attrRef(' . $this->identifierToStr($arg->value->name) . ')');
$this->addPositionalCallArg($obj . '.attrRef(' . $this->identifierToStr($arg->value->name) . ')', $arrayArgsVar, $list_args);
continue;
}
} elseif ($this->isArrayDimFetch($arg->value) and $this->isVarExpr($arg->value->var)) {
@ -3649,9 +3647,9 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
if ($byRef) {
$ref = $this->addTmpVar(self::TYPE_REF);
$this->context->beforeStmtLines[] = $ref . ' = ' . $globalVar . '.toReference();';
$addPositionalArg('&' . $ref);
$this->addPositionalCallArg('&' . $ref, $arrayArgsVar, $list_args);
} else {
$addPositionalArg($globalVar);
$this->addPositionalCallArg($globalVar, $arrayArgsVar, $list_args);
}
continue;
}
@ -3662,7 +3660,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
if ($arg->value->dim === null) {
$this->fatalError($arg, 'Array dimension must be a constant expression');
}
$addPositionalArg($array . '.itemRef(' . $this->identifierToStr($arg->value->dim) . ')');
$this->addPositionalCallArg($array . '.itemRef(' . $this->identifierToStr($arg->value->dim) . ')', $arrayArgsVar, $list_args);
continue;
}
} elseif ($this->isFuncCallExpr($arg->value)) {
@ -3675,12 +3673,12 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$name = $this->parseVariable($inner);
// 消除 refval() 函数调用,直接使用变量
$arg->value = $inner;
$addPositionalArg($this->parseArgRefVar($arg, $name));
$this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args);
continue;
}
$expr = $this->expandRefvalExpr($inner, $arg);
if ($expr !== null) {
$addPositionalArg($expr);
$this->addPositionalCallArg($expr, $arrayArgsVar, $list_args);
continue;
}
$this->fatalError($arg, 'The refval function only accepts a variable, array element, or object property');
@ -3693,12 +3691,12 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$tmpRef = $this->genTmpVarName();
$this->addLocalVar($tmpRef, self::TYPE_REF);
$this->context->beforeStmtLines[] = $tmpRef . ' = ' . $this->parseChainedExpr($arg->value, self::OP_REFVAL) . ';';
$addPositionalArg('&' . $tmpRef);
$this->addPositionalCallArg('&' . $tmpRef, $arrayArgsVar, $list_args);
continue;
}
}
$value = $this->parseCallArgValue($arg);
$addPositionalArg($value);
$this->addPositionalCallArg($value, $arrayArgsVar, $list_args);
}
if ($arrayArgsVar !== null) {
@ -3708,6 +3706,35 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
return $namedArgsVar !== null ? $callArgs . ', ' . $namedArgsVar . '.array()' : $callArgs;
}
protected function ensureCallArrayArgs(?string &$arrayArgsVar, array &$listArgs): string
{
if ($arrayArgsVar === null) {
$arrayArgsVar = $this->genTmpVarName();
$this->context->beforeStmtLines[] = self::TYPE_ARRAY . ' ' . $arrayArgsVar . '{' . implode(', ', $listArgs) . '};';
$listArgs = [];
}
return $arrayArgsVar;
}
protected function ensureCallNamedArgs(?string &$namedArgsVar): string
{
if ($namedArgsVar === null) {
$namedArgsVar = $this->genTmpVarName();
$this->context->beforeStmtLines[] = self::TYPE_ARRAY . ' ' . $namedArgsVar . ';';
$this->context->afterStmtLines[] = $namedArgsVar . '.unset();';
}
return $namedArgsVar;
}
protected function addPositionalCallArg(string $value, ?string $arrayArgsVar, array &$listArgs): void
{
if ($arrayArgsVar !== null) {
$this->context->beforeStmtLines[] = $arrayArgsVar . '.append(' . $value . ');';
} else {
$listArgs[] = $value;
}
}
protected function parseCallArgValue(Node\Arg $arg): string
{
$this->assertExprCanBeUsedAsValue($arg->value, 'function argument');

@ -0,0 +1,78 @@
--TEST--
Closure use value and reference capture
--FILE--
<?php
function main(): void
{
$arr = [1, 2];
$copy = function () use ($arr) {
$arr[] = 3;
return $arr;
};
$copyResult = $copy();
var_dump($arr);
var_dump($copyResult);
$ref = function () use (&$arr) {
$arr[] = 4;
return $arr;
};
$refResult = $ref();
var_dump($arr);
var_dump($refResult);
$value = 'old';
$returnCapturedRef = function () use (&$value) {
return $value;
};
var_dump($returnCapturedRef());
$value = 'new';
var_dump($returnCapturedRef());
$count = 0;
$inc = function () use (&$count) {
$count++;
return $count;
};
var_dump($inc());
var_dump($inc());
var_dump($count);
}
?>
--EXPECT--
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
}
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(4)
}
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(4)
}
string(3) "old"
string(3) "new"
int(1)
int(2)
int(2)
Loading…
Cancel
Save