From 903af8c477db4a2a426e41384636e9eeb48be9e5 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Wed, 1 Jul 2026 12:12:52 +0800 Subject: [PATCH] =?UTF-8?q?test(closure):=20=E6=B7=BB=E5=8A=A0=E9=97=AD?= =?UTF-8?q?=E5=8C=85use=E5=80=BC=E5=92=8C=E5=BC=95=E7=94=A8=E6=8D=95?= =?UTF-8?q?=E8=8E=B7=E5=8A=9F=E8=83=BD=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增closure-use-reference-capture.phpt测试文件验证闭包捕获行为 - 添加ClosureTest类实现use引用捕获编译测试 - 在CompilerBase中添加stringifyParsedExpr方法处理表达式字符串化 - 重构函数调用参数处理逻辑提取为独立方法确保数组和命名参数正确处理 - 修改for循环条件和迭代部分的表达式解析确保正确转换 - 添加use-reference-capture.php测试代码验证闭包引用捕获功能 --- .../code/closure/use-reference-capture.php | 22 ++++ phpunit/src/ClosureTest.php | 20 ++++ src/Php/CompilerBase.php | 107 +++++++++++------- .../closure-use-reference-capture.phpt | 78 +++++++++++++ 4 files changed, 187 insertions(+), 40 deletions(-) create mode 100644 phpunit/code/closure/use-reference-capture.php create mode 100644 phpunit/src/ClosureTest.php create mode 100644 tests/aot/closure/closure-use-reference-capture.phpt diff --git a/phpunit/code/closure/use-reference-capture.php b/phpunit/code/closure/use-reference-capture.php new file mode 100644 index 00000000..899330f5 --- /dev/null +++ b/phpunit/code/closure/use-reference-capture.php @@ -0,0 +1,22 @@ +addFiles([$testFile]); + $compiler->prepareFile($testFile); + $compiler->convertFile($testFile); + + $this->assertTrue(true); + } +} diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 4732941e..ae2ca1b5 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -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'); diff --git a/tests/aot/closure/closure-use-reference-capture.phpt b/tests/aot/closure/closure-use-reference-capture.phpt new file mode 100644 index 00000000..ef78af8d --- /dev/null +++ b/tests/aot/closure/closure-use-reference-capture.phpt @@ -0,0 +1,78 @@ +--TEST-- +Closure use value and reference capture +--FILE-- + +--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)