feat(php): 实现表达式惰性求值和语句上下文处理

- 在三元运算符解析中添加分支语句收集和处理逻辑
- 实现match表达式的惰性求值支持
- 添加条件表达式和函数调用参数的惰性求值机制
- 重构代码结构以支持语句前后的代码块插入
- 更新变量赋值和返回语句的生成逻辑
- 添加lambda包装器用于复杂表达式求值
- 优化临时变量管理和作用域控制
pull/5/head
韩天峰 2 months ago
parent 00f79ecdc1
commit 4ad8c0a6fa
  1. 147
      src/Php/CompilerBase.php
  2. 29
      tests/aot/class/new-unpack-named.phpt
  3. 48
      tests/aot/coalesce/value-selection-lazy.phpt
  4. 50
      tests/aot/control_flow/match-lazy-eval.phpt
  5. 38
      tests/aot/nullsafe/nullsafe-args-lazy.phpt
  6. 44
      tests/aot/operator/ternary-lazy-eval.phpt
  7. 35
      tests/aot/parent_call/parent-unpack-named.phpt

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

@ -0,0 +1,29 @@
--TEST--
new object with unpack before named arguments
--FILE--
<?php
class PairValue
{
public function __construct(public int $a, public int $b)
{
}
public function show(): void
{
echo $this->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

@ -0,0 +1,48 @@
--TEST--
Coalesce and shorthand ternary evaluate right side lazily
--FILE--
<?php
function makeArgs(string $name): array
{
echo "args:$name\n";
return [$name];
}
function makeValue(string $name): string
{
echo "value:$name\n";
return $name;
}
function build(string $id, string $value): string
{
echo "body:$id:$value\n";
return $id . ':' . $value;
}
function main(): void
{
$present = 'left';
var_dump($present ?? build(...makeArgs('coalesce-skip'), value: makeValue('coalesce-skip')));
$missing = null;
var_dump($missing ?? build(...makeArgs('coalesce-run'), value: makeValue('coalesce-run')));
$truthy = 'truthy';
var_dump($truthy ?: build(...makeArgs('shorthand-skip'), value: makeValue('shorthand-skip')));
$empty = '';
var_dump($empty ?: build(...makeArgs('shorthand-run'), value: makeValue('shorthand-run')));
}
?>
--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"

@ -0,0 +1,50 @@
--TEST--
Match expression evaluates conditions and bodies lazily
--FILE--
<?php
function marker(string $name): string
{
echo "cond:$name\n";
return $name;
}
function makeArgs(string $name): array
{
echo "args:$name\n";
return [$name];
}
function makeValue(string $name): string
{
echo "value:$name\n";
return $name;
}
function build(string $id, string $value): string
{
echo "body:$id:$value\n";
return $id . ':' . $value;
}
function test_match(string $value): string
{
return match ($value) {
marker(name: 'a') => 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"

@ -0,0 +1,38 @@
--TEST--
Nullsafe method call does not evaluate arguments when receiver is null
--FILE--
<?php
class Recorder
{
public function set(int $id, string $value): string
{
return $id . ':' . $value;
}
}
function makeArgs(): array
{
echo "makeArgs\n";
return [7];
}
function makeValue(): string
{
echo "makeValue\n";
return "ok";
}
function main(): void
{
$null = null;
var_dump($null?->set(...makeArgs(), value: makeValue()));
$recorder = new Recorder();
var_dump($recorder?->set(...makeArgs(), value: makeValue()));
}
?>
--EXPECT--
NULL
makeArgs
makeValue
string(4) "7:ok"

@ -0,0 +1,44 @@
--TEST--
Ternary expression evaluates only selected branch
--FILE--
<?php
function makeArgs(string $name): array
{
echo "args:$name\n";
return [$name];
}
function makeValue(string $name): string
{
echo "value:$name\n";
return $name;
}
function build(string $id, string $value): string
{
echo "body:$id:$value\n";
return $id . ':' . $value;
}
function choose(bool $flag): string
{
return $flag
? build(...makeArgs('T'), value: makeValue('T'))
: build(...makeArgs('F'), value: makeValue('F'));
}
function main(): void
{
var_dump(choose(true));
var_dump(choose(false));
}
?>
--EXPECT--
args:T
value:T
body:T:T
string(3) "T:T"
args:F
value:F
body:F:F
string(3) "F:F"

@ -0,0 +1,35 @@
--TEST--
parent::method() with unpack before named arguments
--FILE--
<?php
class ParentPair
{
public function combine(int $a, int $b): string
{
return $a . ':' . $b;
}
}
class ChildPair extends ParentPair
{
public function combine(int $a, int $b): string
{
return 'child';
}
public function callParent(): string
{
return parent::combine(...[1], b: 2);
}
}
function main(): void
{
$child = new ChildPair();
echo $child->combine(9, 9) . "\n";
echo $child->callParent() . "\n";
}
?>
--EXPECT--
child
1:2
Loading…
Cancel
Save