feat(compiler): 添加空合并赋值运算符支持并优化变量检查

- 实现了 ??= 运算符的解析和编译功能
- 添加了变量存在性检查的改进实现
- 增加了左值有效性验证确保赋值操作合法
- 更新了测试用例覆盖空合并赋值的各种场景
- 修复了 stray code 检查的错误定位问题
- 在编译器基类中增加了 isset 和 empty 操作常量定义
pull/1/head
韩天峰 7 months ago
parent f2e7b70043
commit e0a4e2f977
  1. 2
      .gitignore
  2. 52
      src/Php/CompilerBase.php
  3. 6
      src/Php/Preprocessor.php
  4. 15
      tests/aot/assign_coalesce_001.phpt
  5. 119
      tests/zend/coalesce/assign_coalesce_001.phpt
  6. 28
      tests/zend/use_function/basic.phpt

2
.gitignore vendored

@ -6,6 +6,6 @@
/build
/vendor
/tmp
/examples/wordpress
/projects/wordpress
/.php-cs-fixer.cache
*.o

@ -59,6 +59,8 @@ class CompilerBase extends \PhpAot\Core\Translator
public const string NAMESPACE_SEPARATOR = '__';
public const string PREFIX = 'php_';
public const string OP_ISSET = 'isset';
public const string OP_EMPTY = 'empty';
protected string $phpxDir = '~/workspace/projects/phpx';
protected string $lang = 'PHP';
protected string $cppCompiler = 'g++';
@ -311,6 +313,8 @@ class CompilerBase extends \PhpAot\Core\Translator
return $this->parseAssignOpBitwiseXor($expr);
case 'Expr_AssignOp_Pow':
return $this->parseAssignOpPow($expr);
case 'Expr_AssignOp_Coalesce':
return $this->parseAssignOpCoalesce($expr);
case 'Expr_BinaryOp_Mul':
return $this->parseBinaryOpMul($expr);
case 'Expr_BinaryOp_Concat':
@ -1713,6 +1717,11 @@ class CompilerBase extends \PhpAot\Core\Translator
$this->error("{$msg} in {$this->file}:{$node->getStartLine()}");
}
protected function errorUndefinedVariable(Variable $node): never
{
$this->fatalError($node, "The variable `{$node->name}` is undefined");
}
protected function dump(NodeAbstract $v): void
{
if ($this->debugLine == $v->getStartLine()) {
@ -1734,7 +1743,7 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($write) {
$this->addLocalVar($var, self::TYPE_ARRAY);
} else {
$this->fatalError($node->var, "The variable `{$node->var->name}` is undefined");
$this->errorUndefinedVariable($node->var);
}
} else {
$type = $this->getVarType($var);
@ -1835,9 +1844,9 @@ class CompilerBase extends \PhpAot\Core\Translator
return false;
}
protected function foundStrayCode(): never
protected function foundStrayCode(Node $node): never
{
$this->error("All execution code must be within a function; there is no allowance for stray code.");
$this->fatalError($node, "All execution code must be within a function, found stray code");
}
protected function parseFuncCall(Node\Expr\FuncCall $expr, bool $silent = false): string
@ -2825,11 +2834,24 @@ class CompilerBase extends \PhpAot\Core\Translator
return $this->parseVarCheckExpr($expr->expr, 'empty');
}
/**
* 左值只能为变量、数组、对象属性、对象静态属性
* @param NodeAbstract $expr
* @return void
*/
protected function checkLeftValue(NodeAbstract $expr): void
{
if (!$this->isVarExpr($expr) && !$this->isArrayDimFetch($expr) && !$this->isPropertyFetch($expr) && !$this->isStaticPropertyFetch($expr)) {
$this->fatalError($expr, 'The left value of assignment operation can only be variable, array item, object property, class static property');
}
}
protected function parseVarCheckExpr(NodeAbstract $expr, string $op): string
{
if ($this->isVarExpr($expr)) {
if ($op === 'isset') {
return $this->hasVar($this->parseIdentifier($expr)) ? 'true' : 'false';
$var = $this->parseIdentifier($expr);
return $this->hasVar($var) ? 'php::exists(' . $var . ')' : 'false';
}
return 'php::' . $op . '(' . $this->parseExpr($expr) . ')';
}
@ -3466,4 +3488,26 @@ class CompilerBase extends \PhpAot\Core\Translator
return 'php::newClosure(' . $tmpVar . ', { ' . implode(', ', $useVars) . ' })';
}
protected function parseAssignOpCoalesce(Node\Expr\AssignOp\Coalesce $expr): string
{
$this->checkLeftValue($expr->var);
$isset = $this->parseVarCheckExpr($expr->var, self::OP_ISSET);
$inAssignExpr = $this->inAssignExpr;
$this->inAssignExpr = true;
$var = $this->parseIdentifier($expr->var);
$this->inAssignExpr = $inAssignExpr;
$right = $this->parseExpr($expr->expr);
if ($this->isVarExpr($expr->expr) and !$this->hasVar($right)) {
$this->errorUndefinedVariable($expr->expr);
}
if ($this->isVarExpr($expr->var) and !$this->hasVar($var)) {
$this->addLocalVar($var, $this->detectExprType($expr->expr));
}
return 'if (!' . $isset . ') {' . PHP_EOL .
$this->getIndent() . $var . ' = ' . $right . ';' . PHP_EOL .
'}' . PHP_EOL;
}
}

@ -106,7 +106,7 @@ class Preprocessor extends CompilerBase
$this->parseConstDef($v);
break;
case 'Stmt_Expression':
$this->foundStrayCode();
$this->foundStrayCode($v);
default:
$this->fatalError($v, 'Unsupported statement: ' . $type);
}
@ -144,7 +144,7 @@ class Preprocessor extends CompilerBase
case 'Stmt_Const':
break;
case 'Stmt_Expression':
$this->foundStrayCode();
$this->foundStrayCode($v2);
default:
abort($v2);
}
@ -178,7 +178,7 @@ class Preprocessor extends CompilerBase
$code .= $this->prepareFunction($v) . PHP_EOL;
break;
case 'Stmt_Expression':
$this->foundStrayCode();
$this->foundStrayCode($v);
default:
abort($v);
}

@ -0,0 +1,15 @@
--TEST--
assign coalesce
--FILE--
<?php
$a = 123;
$a ??= 456;
var_dump($a);
$b = null;
$b ??= 'foo';
var_dump($b);
?>
--EXPECT--
int(123)
string(3) "foo"

@ -0,0 +1,119 @@
--TEST--
Coalesce assign (??=): Basic behavior
--FILE--
<?php
// Identity function used to track single-evaluation
function id($arg) {
return $arg;
}
class Test {
public static $foo;
public static $bar;
}
function main() {
// Refcounted values
$foo = "fo";
$foo .= "o";
$bar = "ba";
$bar .= "r";
echo "Simple variables:\n";
$a = 123;
$a ??= 456;
var_dump($a);
$b = null;
$b ??= $foo;
var_dump($b);
$c = $foo;
$c ??= $bar;
var_dump($c);
$d ??= $foo;
var_dump($c);
echo "\nArrays:\n";
$ary = [];
$ary["foo"] ??= 123;
$ary[$foo] ??= $bar;
$ary[$bar] ??= $foo;
var_dump($ary);
echo "\nArrays (identity):\n";
$ary = [];
$ary[id($foo)] ??= 123;
$ary[id($foo)] ??= $bar;
$ary[id($bar)] ??= $foo;
var_dump($ary);
echo "\nObjects:\n";
$obj = new stdClass;
$obj->foo ??= 123;
$obj->$foo ??= $bar;
$obj->$bar ??= $foo;
var_dump($obj);
$obj = new stdClass;
$obj->{id($foo)} ??= 123;
$obj->{id($foo)} ??= $bar;
$obj->{id($bar)} ??= $foo;
var_dump($obj);
echo "\nStatic props:\n";
Test::$foo ??= 123;
Test::$$foo ??= $bar;
Test::$$bar ??= $foo;
var_dump(Test::$foo, Test::$bar);
Test::$foo = null;
Test::$bar = null;
Test::${id($foo)} ??= 123;
Test::${id($foo)} ??= $bar;
Test::${id($bar)} ??= $foo;
var_dump(Test::$foo, Test::$bar);
}
?>
--EXPECT--
Simple variables:
int(123)
string(3) "foo"
string(3) "foo"
string(3) "foo"
Arrays:
array(2) {
["foo"]=>
int(123)
["bar"]=>
string(3) "foo"
}
Arrays (identity):
array(2) {
["foo"]=>
int(123)
["bar"]=>
string(3) "foo"
}
Objects:
object(stdClass)#1 (2) {
["foo"]=>
int(123)
["bar"]=>
string(3) "foo"
}
object(stdClass)#2 (2) {
["foo"]=>
int(123)
["bar"]=>
string(3) "foo"
}
Static props:
int(123)
string(3) "foo"
int(123)
string(3) "foo"

@ -0,0 +1,28 @@
--TEST--
import namespaced function
--FILE--
<?php
namespace foo\bar {
function baz() {
return 'foo.bar.baz';
}
function qux() {
return baz();
}
}
namespace {
use function foo\bar\baz, foo\bar\qux;
function main() {
var_dump(baz());
var_dump(qux());
echo "Done\n";
}
}
?>
--EXPECT--
string(11) "foo.bar.baz"
string(11) "foo.bar.baz"
Done
Loading…
Cancel
Save