From 4ad8c0a6fa7d3219c930df9191946b72b5d332ca Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 30 Jun 2026 12:52:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(php):=20=E5=AE=9E=E7=8E=B0=E8=A1=A8?= =?UTF-8?q?=E8=BE=BE=E5=BC=8F=E6=83=B0=E6=80=A7=E6=B1=82=E5=80=BC=E5=92=8C?= =?UTF-8?q?=E8=AF=AD=E5=8F=A5=E4=B8=8A=E4=B8=8B=E6=96=87=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在三元运算符解析中添加分支语句收集和处理逻辑 - 实现match表达式的惰性求值支持 - 添加条件表达式和函数调用参数的惰性求值机制 - 重构代码结构以支持语句前后的代码块插入 - 更新变量赋值和返回语句的生成逻辑 - 添加lambda包装器用于复杂表达式求值 - 优化临时变量管理和作用域控制 --- src/Php/CompilerBase.php | 147 ++++++++++++++++-- tests/aot/class/new-unpack-named.phpt | 29 ++++ tests/aot/coalesce/value-selection-lazy.phpt | 48 ++++++ tests/aot/control_flow/match-lazy-eval.phpt | 50 ++++++ tests/aot/nullsafe/nullsafe-args-lazy.phpt | 38 +++++ tests/aot/operator/ternary-lazy-eval.phpt | 44 ++++++ .../aot/parent_call/parent-unpack-named.phpt | 35 +++++ 7 files changed, 378 insertions(+), 13 deletions(-) create mode 100644 tests/aot/class/new-unpack-named.phpt create mode 100644 tests/aot/coalesce/value-selection-lazy.phpt create mode 100644 tests/aot/control_flow/match-lazy-eval.phpt create mode 100644 tests/aot/nullsafe/nullsafe-args-lazy.phpt create mode 100644 tests/aot/operator/ternary-lazy-eval.phpt create mode 100644 tests/aot/parent_call/parent-unpack-named.phpt diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index c70aa403..2f7eabac 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -3532,12 +3532,54 @@ class CompilerBase extends \PhpAot\Core\Translator return $this->parseValueSelection($expr, $expr->cond, $expr->else, self::OP_NOT_EMPTY); } $cond = $this->parseExpr($expr->cond); + $ifBeforeStmtCount = count($this->context->beforeStmtLines); + $ifAfterStmtCount = count($this->context->afterStmtLines); $if = $this->parseExpr($expr->if); + $ifBeforeStmts = array_slice($this->context->beforeStmtLines, $ifBeforeStmtCount); + $ifAfterStmts = array_slice($this->context->afterStmtLines, $ifAfterStmtCount); + $this->context->beforeStmtLines = array_slice($this->context->beforeStmtLines, 0, $ifBeforeStmtCount); + $this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $ifAfterStmtCount); + + $elseBeforeStmtCount = count($this->context->beforeStmtLines); + $elseAfterStmtCount = count($this->context->afterStmtLines); $else = $this->parseExpr($expr->else); - if ($this->detectTypeOfExpr($expr->if) !== $this->detectTypeOfExpr($expr->else)) { + $elseBeforeStmts = array_slice($this->context->beforeStmtLines, $elseBeforeStmtCount); + $elseAfterStmts = array_slice($this->context->afterStmtLines, $elseAfterStmtCount); + $this->context->beforeStmtLines = array_slice($this->context->beforeStmtLines, 0, $elseBeforeStmtCount); + $this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $elseAfterStmtCount); + + $hasBranchStmts = $ifBeforeStmts || $ifAfterStmts || $elseBeforeStmts || $elseAfterStmts; + $typeChanged = $this->detectTypeOfExpr($expr->if) !== $this->detectTypeOfExpr($expr->else); + if (!$hasBranchStmts && $typeChanged) { $if = 'php::Var(' . $if . ')'; $else = 'php::Var(' . $else . ')'; } + if ($hasBranchStmts) { + $appendStmtLines = function (string &$code, array $stmts): void { + if ($stmts) { + $code .= $this->getIndent() . implode(PHP_EOL . $this->getIndent(), $stmts); + } + }; + $appendReturn = function (string &$code, string $value, array $beforeStmts, array $afterStmts) use ($appendStmtLines): void { + $appendStmtLines($code, $beforeStmts); + if ($afterStmts) { + $tmpVar = $this->addTmpVar(self::TYPE_VAR); + $code .= $this->getIndent() . "{$tmpVar} = {$value};"; + $appendStmtLines($code, $afterStmts); + $code .= $this->getIndent() . 'return ' . $tmpVar . ';'; + } else { + $code .= $this->getIndent() . 'return php::Var(' . $value . ');'; + } + }; + $code = '[&]() -> ' . self::TYPE_VAR . '{'; + $code .= $this->getIndent() . 'if (' . $cond . ') {'; + $appendReturn($code, $if, $ifBeforeStmts, $ifAfterStmts); + $code .= $this->getIndent() . '} else {'; + $appendReturn($code, $else, $elseBeforeStmts, $elseAfterStmts); + $code .= $this->getIndent() . '}'; + $code .= $this->getIndent() . '}()'; + return $code; + } return '(' . $cond . ') ? (' . $if . ') : (' . $else . ')'; } @@ -3554,31 +3596,72 @@ class CompilerBase extends \PhpAot\Core\Translator $var = $tmpVar; } + $parseExprWithStmts = function (NodeAbstract $node): array { + $beforeStmtCount = count($this->context->beforeStmtLines); + $afterStmtCount = count($this->context->afterStmtLines); + $value = $this->parseExpr($node); + $beforeStmts = array_slice($this->context->beforeStmtLines, $beforeStmtCount); + $afterStmts = array_slice($this->context->afterStmtLines, $afterStmtCount); + $this->context->beforeStmtLines = array_slice($this->context->beforeStmtLines, 0, $beforeStmtCount); + $this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $afterStmtCount); + return [$value, $beforeStmts, $afterStmts]; + }; + + $appendStmtLines = function (string &$code, array $stmts): void { + if ($stmts) { + $code .= $this->getIndent() . implode(PHP_EOL . $this->getIndent(), $stmts); + } + }; + + $appendMatchReturn = function (string &$code, NodeAbstract $body) use ($parseExprWithStmts, $appendStmtLines): void { + [$value, $beforeStmts, $afterStmts] = $parseExprWithStmts($body); + $appendStmtLines($code, $beforeStmts); + if ($afterStmts) { + $tmpVar = $this->addTmpVar(self::TYPE_VAR); + $code .= $this->getIndent() . "{$tmpVar} = {$value};"; + $appendStmtLines($code, $afterStmts); + $code .= $this->getIndent() . 'return ' . $tmpVar . ';'; + } else { + $code .= $this->getIndent() . 'return ' . $value . ';'; + } + }; + $code = '[&]() -> ' . self::TYPE_VAR . '{'; $default = null; - foreach ($expr->arms as $i => $arm) { + foreach ($expr->arms as $arm) { if ($arm->conds === null) { $default = $arm->body; continue; } - $prefix = $i === 0 ? 'if' : 'else if'; - $condList = []; + $matched = $this->genTmpVarName(); + $code .= $this->getIndent() . 'bool ' . $matched . ' = false;'; foreach ($arm->conds as $cond) { if ($this->isMatchExpr($cond)) { $this->fatalError($arm, 'Match expression cannot be used as a condition'); } - $condList[] = 'php::same(' . $var . ', ' . $this->parseExpr($cond) . ')'; + [$condValue, $beforeStmts, $afterStmts] = $parseExprWithStmts($cond); + $code .= $this->getIndent() . 'if (!' . $matched . ') {'; + $appendStmtLines($code, $beforeStmts); + if ($afterStmts) { + $condTmpVar = $this->addTmpVar(self::TYPE_VAR); + $code .= $this->getIndent() . "{$condTmpVar} = {$condValue};"; + $appendStmtLines($code, $afterStmts); + $condValue = $condTmpVar; + } + $code .= $this->getIndent() . $matched . ' = php::same(' . $var . ', ' . $condValue . ');'; + $code .= $this->getIndent() . '}'; } - $code .= $prefix . '(' . implode(' || ', $condList) . ') {'; - $code .= 'return ' . $this->parseExpr($arm->body) . ';'; - $code .= '}'; + $code .= $this->getIndent() . 'if (' . $matched . ') {'; + $appendMatchReturn($code, $arm->body); + $code .= $this->getIndent() . '}'; } - $else = count($expr->arms) === 0 ? '' : 'else '; if ($default) { - $code .= $else . ' { return ' . $this->parseExpr($default) . '; }'; + $code .= $this->getIndent() . '{'; + $appendMatchReturn($code, $default); + $code .= $this->getIndent() . '}'; } else { - $code .= $else . ' { return php::throwException("UnhandledMatchError", "Unhandled match case"); }'; + $code .= $this->getIndent() . '{ return php::throwException("UnhandledMatchError", "Unhandled match case"); }'; } $code .= '}()'; @@ -3698,12 +3781,38 @@ class CompilerBase extends \PhpAot\Core\Translator $leftExpr = $chainOpResult; } + $rightBeforeStmtCount = count($this->context->beforeStmtLines); + $rightAfterStmtCount = count($this->context->afterStmtLines); $rightExpr = $this->parseIdentifier($right); + $rightBeforeStmts = array_slice($this->context->beforeStmtLines, $rightBeforeStmtCount); + $rightAfterStmts = array_slice($this->context->afterStmtLines, $rightAfterStmtCount); + $this->context->beforeStmtLines = array_slice($this->context->beforeStmtLines, 0, $rightBeforeStmtCount); + $this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $rightAfterStmtCount); $this->checkVarMustExist($right, $rightExpr); $tmpVar = $this->addTmpVar(self::TYPE_VAR); - $this->context->beforeStmtLines[] = '// Expr: ' . $this->printer->prettyPrintExpr($expr) . PHP_EOL . - $tmpVar . ' = ' . $condExpr . ' ? ' . $leftExpr . ' : ' . $rightExpr . ';'; + if ($rightBeforeStmts || $rightAfterStmts) { + $code = '// Expr: ' . $this->printer->prettyPrintExpr($expr) . PHP_EOL . + 'if (' . $condExpr . ') {' . PHP_EOL . + $this->getIndent() . $tmpVar . ' = ' . $leftExpr . ';' . PHP_EOL . + '} else {' . PHP_EOL; + if ($rightBeforeStmts) { + $code .= $this->getIndent() . implode(PHP_EOL . $this->getIndent(), $rightBeforeStmts) . PHP_EOL; + } + if ($rightAfterStmts) { + $rightTmpVar = $this->addTmpVar(self::TYPE_VAR); + $code .= $this->getIndent() . $rightTmpVar . ' = ' . $rightExpr . ';' . PHP_EOL; + $code .= $this->getIndent() . implode(PHP_EOL . $this->getIndent(), $rightAfterStmts) . PHP_EOL; + $code .= $this->getIndent() . $tmpVar . ' = ' . $rightTmpVar . ';' . PHP_EOL; + } else { + $code .= $this->getIndent() . $tmpVar . ' = ' . $rightExpr . ';' . PHP_EOL; + } + $code .= '}'; + $this->context->beforeStmtLines[] = $code; + } else { + $this->context->beforeStmtLines[] = '// Expr: ' . $this->printer->prettyPrintExpr($expr) . PHP_EOL . + $tmpVar . ' = ' . $condExpr . ' ? ' . $leftExpr . ' : ' . $rightExpr . ';'; + } $expr->setAttribute('replace', $tmpVar); return $tmpVar; @@ -6225,8 +6334,20 @@ class CompilerBase extends \PhpAot\Core\Translator if ($item[0] == 'property') { $code .= $this->getIndent() . "{$tmpVar} = {$object}.attr({$item[1]}, {$update});"; } else { + $beforeStmtCount = count($this->context->beforeStmtLines); + $afterStmtCount = count($this->context->afterStmtLines); $args = $this->parseCallArgs($item[2]); + $argBeforeStmts = array_slice($this->context->beforeStmtLines, $beforeStmtCount); + $argAfterStmts = array_slice($this->context->afterStmtLines, $afterStmtCount); + $this->context->beforeStmtLines = array_slice($this->context->beforeStmtLines, 0, $beforeStmtCount); + $this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $afterStmtCount); + if ($argBeforeStmts) { + $code .= $this->getIndent() . implode(PHP_EOL . $this->getIndent(), $argBeforeStmts); + } $code .= $this->getIndent() . "{$tmpVar} = {$object}.call({$item[1]}, {$args});"; + if ($argAfterStmts) { + $code .= $this->getIndent() . implode(PHP_EOL . $this->getIndent(), $argAfterStmts); + } } $object = $tmpVar; } diff --git a/tests/aot/class/new-unpack-named.phpt b/tests/aot/class/new-unpack-named.phpt new file mode 100644 index 00000000..a66e8f59 --- /dev/null +++ b/tests/aot/class/new-unpack-named.phpt @@ -0,0 +1,29 @@ +--TEST-- +new object with unpack before named arguments +--FILE-- +a . ':' . $this->b . "\n"; + } +} + +function main(): void +{ + $known = new PairValue(...[1], b: 2); + $known->show(); + + $class = PairValue::class; + $dynamic = new $class(...[3], b: 4); + $dynamic->show(); +} +?> +--EXPECT-- +1:2 +3:4 diff --git a/tests/aot/coalesce/value-selection-lazy.phpt b/tests/aot/coalesce/value-selection-lazy.phpt new file mode 100644 index 00000000..72f09f6e --- /dev/null +++ b/tests/aot/coalesce/value-selection-lazy.phpt @@ -0,0 +1,48 @@ +--TEST-- +Coalesce and shorthand ternary evaluate right side lazily +--FILE-- + +--EXPECT-- +string(4) "left" +args:coalesce-run +value:coalesce-run +body:coalesce-run:coalesce-run +string(25) "coalesce-run:coalesce-run" +string(6) "truthy" +args:shorthand-run +value:shorthand-run +body:shorthand-run:shorthand-run +string(27) "shorthand-run:shorthand-run" diff --git a/tests/aot/control_flow/match-lazy-eval.phpt b/tests/aot/control_flow/match-lazy-eval.phpt new file mode 100644 index 00000000..15142428 --- /dev/null +++ b/tests/aot/control_flow/match-lazy-eval.phpt @@ -0,0 +1,50 @@ +--TEST-- +Match expression evaluates conditions and bodies lazily +--FILE-- + build(...makeArgs('A'), value: makeValue('A')), + marker(name: 'b'), marker(name: 'skip') => build(...makeArgs('B'), value: makeValue('B')), + marker(name: 'c') => build(...makeArgs('C'), value: makeValue('C')), + default => build(...makeArgs('D'), value: makeValue('D')), + }; +} + +function main(): void +{ + var_dump(test_match('b')); +} +?> +--EXPECT-- +cond:a +cond:b +args:B +value:B +body:B:B +string(3) "B:B" diff --git a/tests/aot/nullsafe/nullsafe-args-lazy.phpt b/tests/aot/nullsafe/nullsafe-args-lazy.phpt new file mode 100644 index 00000000..59ada435 --- /dev/null +++ b/tests/aot/nullsafe/nullsafe-args-lazy.phpt @@ -0,0 +1,38 @@ +--TEST-- +Nullsafe method call does not evaluate arguments when receiver is null +--FILE-- +set(...makeArgs(), value: makeValue())); + + $recorder = new Recorder(); + var_dump($recorder?->set(...makeArgs(), value: makeValue())); +} +?> +--EXPECT-- +NULL +makeArgs +makeValue +string(4) "7:ok" diff --git a/tests/aot/operator/ternary-lazy-eval.phpt b/tests/aot/operator/ternary-lazy-eval.phpt new file mode 100644 index 00000000..1d7f288e --- /dev/null +++ b/tests/aot/operator/ternary-lazy-eval.phpt @@ -0,0 +1,44 @@ +--TEST-- +Ternary expression evaluates only selected branch +--FILE-- + +--EXPECT-- +args:T +value:T +body:T:T +string(3) "T:T" +args:F +value:F +body:F:F +string(3) "F:F" diff --git a/tests/aot/parent_call/parent-unpack-named.phpt b/tests/aot/parent_call/parent-unpack-named.phpt new file mode 100644 index 00000000..494a0ca6 --- /dev/null +++ b/tests/aot/parent_call/parent-unpack-named.phpt @@ -0,0 +1,35 @@ +--TEST-- +parent::method() with unpack before named arguments +--FILE-- +combine(9, 9) . "\n"; + echo $child->callParent() . "\n"; +} +?> +--EXPECT-- +child +1:2