refactor(php): 重构PHP解析器的表达式处理和动态属性访问

- 将parseExpr替换为parseExprAsValue方法,统一处理void表达式转换为null
- 移除多处trimBrackets调用,简化表达式解析流程
- 添加emitDynamicPropertyFetchRead/Write/Unset等统一动态属性访问方法
- 重构PropertyWriteTarget类,将objectExpr和propertyExpr设为私有并添加getter方法
- 合并动态属性访问的目标路径和传统回退路径
- 修改测试框架中的exec方法为compile方法,更新相关测试用例
- 移除void表达式相关的错误检查,在运行时将其转换为null值
- 优化类型转换表达式的括号处理,移除不必要的trimBrackets调用
- 统一动态属性读取、写入、引用和取消设置的操作方法
pull/11/head
韩天峰 2 months ago
parent df5dd2a5f6
commit 6e5b186c91
  1. 5
      docs/REFACTORING_PLAN.md
  2. 8
      examples/void.php
  3. 22
      phpunit/bootstrap.php
  4. 6
      phpunit/code/internal-void-function-fully-qualified-assignment.php
  5. 6
      phpunit/code/internal-void-function-usleep-assignment.php
  6. 14
      phpunit/src/ClassTest.php
  7. 14
      phpunit/src/FunctionTest.php
  8. 9
      phpunit/src/Generator/UtilsTest.php
  9. 231
      src/Php/CompilerBase.php
  10. 9
      src/Php/Generator/Utils.php
  11. 58
      src/Php/Parser/AssignOpTrait.php
  12. 5
      src/Php/Parser/BinaryOpTrait.php
  13. 34
      src/Php/Parser/TypeConversionTrait.php
  14. 2
      src/Php/Parser/TypeDetectionTrait.php
  15. 22
      src/Php/Resolver/PropertyWriteTarget.php
  16. 39
      tests/aot/basic/void-expression-null.phpt

@ -297,8 +297,11 @@
- `PropertyWriteTarget` 已开始携带安全动态属性写入目标的 object/property 表达式;普通动态属性赋值、复合赋值、自增自减已优先通过 target 级 read/write helper 发射代码。
- 动态属性 `unset`、属性数组维度写入、引用参数/refval/引用赋值中的安全对象属性引用路径已开始复用 target 级 unset/ref helper。
- 对象属性引用表达式的 target/ref 生成已收敛到 `emitDynamicPropertyFetchRef()`;未使用的旧静态属性赋值入口已删除,静态属性赋值继续走统一 assignment target 路径。
- `PropertyWriteTarget` 的动态 object/property 字段已封装为 getter;属性数组维度写入已接入 target 级 append/update emitter。
- 已建立 `emitDynamicPropertyFetchRead/Write/Unset/AppendArray/UpdateArray()` 包装层,调用方只传入属性访问 AST 与可选 target,由 `CompilerBase` 统一选择 target 路径或旧 fallback 路径。
- 普通赋值、复合赋值、自增自减、unset、属性数组维度写入、引用赋值已去除 Parser trait 中对 dynamic target 的直接分支判断,改为复用统一 emitter 包装。
- 为避免改变复杂表达式求值顺序,当前仅对对象部分为变量的动态属性写入填充 target object/property 字段,复杂对象表达式仍保留旧路径。
- 当前步骤对有效代码保持生成逻辑兼容,但会让更多属性写入路径进入统一静态检查;后续继续收敛 dynamic/native property write emitter、compound assignment、inc/dec、unset 和 refval 路径。
- 当前步骤对有效代码保持生成逻辑兼容,但会让更多属性写入路径进入统一静态检查;后续继续收敛 static/native property write emitter 与 `??=` 属性写入结果生成
### 阶段 3:类型系统模块化

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
function main() {
$v = usleep(111);
var_dump($v);
var_dump(usleep(111) == true);
}

@ -9,20 +9,26 @@ require __DIR__ . '/../src/gen_stub.php';
class BaseTest extends TestCase
{
protected function compile(string $file): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$testFile = __DIR__ . '/code/' . $file;
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$compiler->convertFile($testFile);
$this->addToAssertionCount(1);
}
protected function exec(string $expected, string $file): void
{
try {
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$testFile = __DIR__ . '/code/' . $file;
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$compiler->convertFile($testFile);
$this->compile($file);
} catch (TestError $exception) {
$this->assertStringContainsString($expected, $exception->getMessage());
return;
}
$this->fail();
}
}
}

@ -0,0 +1,6 @@
<?php
function main(): void
{
$value = \usleep(1);
}

@ -0,0 +1,6 @@
<?php
function main(): void
{
$value = usleep(1);
}

@ -78,37 +78,37 @@ class ClassTest extends \BaseTest
public function testParentConstructorCannotBeUsedAsValue()
{
$this->exec('Cannot use void expression as assignment value', 'parent-constructor-used-as-value.php');
$this->compile('parent-constructor-used-as-value.php');
}
public function testParentConstructorCannotBeUsedAsArgument()
{
$this->exec('Cannot use void expression as function argument', 'parent-constructor-used-as-argument.php');
$this->compile('parent-constructor-used-as-argument.php');
}
public function testVoidExpressionCannotBeUsedAsBinaryOperand()
{
$this->exec('Cannot use void expression as binary operand', 'void-expression-binary-operand.php');
$this->compile('void-expression-binary-operand.php');
}
public function testVoidExpressionCannotBeUsedAsCondition()
{
$this->exec('Cannot use void expression as condition', 'void-expression-condition.php');
$this->compile('void-expression-condition.php');
}
public function testVoidExpressionCannotBeUsedAsTernaryBranch()
{
$this->exec('Cannot use void expression as ternary branch', 'void-expression-ternary-branch.php');
$this->compile('void-expression-ternary-branch.php');
}
public function testVoidExpressionCannotBeUsedAsArrayValue()
{
$this->exec('Cannot use void expression as array value', 'void-expression-array-value.php');
$this->compile('void-expression-array-value.php');
}
public function testVoidExpressionCannotBeUsedAsMatchArm()
{
$this->exec('Cannot use void expression as match arm', 'void-expression-match-arm.php');
$this->compile('void-expression-match-arm.php');
}
public function testDestructorCannotDeclareReturnType()

@ -79,12 +79,22 @@ class FunctionTest extends \BaseTest
public function testInternalVoidFunctionCannotBeAssigned()
{
$this->exec('Cannot use void expression as assignment value', 'internal-void-function-assignment.php');
$this->compile('internal-void-function-assignment.php');
}
public function testReflectedInternalVoidFunctionCannotBeAssigned()
{
$this->compile('internal-void-function-usleep-assignment.php');
}
public function testFullyQualifiedInternalVoidFunctionCannotBeAssigned()
{
$this->compile('internal-void-function-fully-qualified-assignment.php');
}
public function testInternalVoidFunctionCannotBeUsedAsBinaryOperand()
{
$this->exec('Cannot use void expression as binary operand', 'internal-void-function-binary-operand.php');
$this->compile('internal-void-function-binary-operand.php');
}
}

@ -308,13 +308,4 @@ class UtilsTest extends TestCase
$this->assertFalse($this->invokeMethod('isClosedExpr', 'bar(1, 2)', 'foo'));
}
// ========================================================================
// trimBrackets
// ========================================================================
public function testTrimBrackets(): void
{
$this->assertEquals('a + b', $this->invokeMethod('trimBrackets', '(a + b)'));
$this->assertEquals('not wrapped', $this->invokeMethod('trimBrackets', 'not wrapped'));
}
}

@ -934,16 +934,33 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
protected function assertExprCanBeUsedAsValue(NodeAbstract $expr, string $context = 'value'): void
{
if ($this->detectTypeOfExpr($expr) === self::TYPE_VOID) {
$this->fatalError($expr, 'Cannot use void expression as ' . $context);
}
// PHP permits using a void/never call as an expression; the expression
// result is null after the call side effect has run.
}
protected function assertExprCanBeUsedAsCondition(NodeAbstract $expr, string $context = 'condition'): void
{
if ($this->detectTypeOfExpr($expr) === self::TYPE_VOID) {
$this->fatalError($expr, 'Cannot use void expression as ' . $context);
// Conditions are value contexts in PHP. A void/never expression is
// evaluated for side effects and then coerced from null.
}
protected function isVoidValueExpr(NodeAbstract $expr): bool
{
return $this->detectTypeOfExpr($expr) === self::TYPE_VOID;
}
protected function wrapVoidExprAsNull(NodeAbstract $expr, string $exprCode): string
{
if (!$this->isVoidValueExpr($expr)) {
return $exprCode;
}
return '((void) (' . $exprCode . '), ' . self::VALUE_NULL . ')';
}
protected function parseExprAsValue(NodeAbstract $expr): string
{
return $this->wrapVoidExprAsNull($expr, $this->parseExpr($expr));
}
public function getNamespacedClassName(string $class, string $currentNamespace = ''): string
@ -1378,9 +1395,9 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
if (!$this->isVarExpr($expr->var)) {
$this->fatalError($expr, 'When an assignment expression serves as an rvalue, it must be an assignment of a variable');
}
return $this->parseExpr($expr);
return $this->parseExprAsValue($expr);
default:
return $this->parseExpr($expr);
return $this->parseExprAsValue($expr);
}
}
@ -1436,7 +1453,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
{
$beforeStmtCount = count($this->context->beforeStmtLines);
$afterStmtCount = count($this->context->afterStmtLines);
$value = $this->parseExpr($expr);
$value = $this->parseExprAsValue($expr);
$beforeStmts = array_slice($this->context->beforeStmtLines, $beforeStmtCount);
$afterStmts = array_slice($this->context->afterStmtLines, $afterStmtCount);
$this->context->beforeStmtLines = array_slice($this->context->beforeStmtLines, 0, $beforeStmtCount);
@ -1631,7 +1648,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$this->fatalError($expr, 'Cannot echo assign expression');
} else {
$type = $this->detectTypeOfExpr($expr);
$parsed = $this->convertExprToStringByType($this->parseExpr($expr), $type);
$parsed = $this->convertExprToStringByType($this->parseExprAsValue($expr), $type);
$lines[] = 'php::echo(' . $parsed . ');';
}
}
@ -1793,10 +1810,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
if ($this->isCurrentConstructor() && !$this->context->inClosure) {
$this->fatalError($v, 'Method `' . $this->getCurrentMethodDisplayName() . '()` cannot return a value');
}
if ($type === self::TYPE_VOID) {
$this->fatalError($v, 'Cannot return void expression');
}
$expr = $this->parseExpr($v->expr);
$expr = $this->parseExprAsValue($v->expr);
$returnType = $this->getReturnType();
// 匿名函数的返回值一定是 var
@ -2896,24 +2910,11 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$target = $this->preparePropertyWriteTarget($var);
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, self::TYPE_VAR);
if ($target !== null && $target->isDynamicObjectProperty()) {
if ($isPre) {
$this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->emitDynamicPropertyTargetRead($target) . " {$op} 1; " . $this->emitDynamicPropertyTargetWrite($target, $tmpVar) . ';';
} else {
$this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->emitDynamicPropertyTargetRead($target) . ';';
$this->context->afterStmtLines[] = $this->emitDynamicPropertyTargetWrite($target, "{$tmpVar} {$op} 1") . ';';
}
return $tmpVar;
}
$obj = $this->parseIdentifier($var->var);
$propName = $this->identifierToStr($var->name, literal: true);
if ($isPre) {
$this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->emitDynamicPropertyRead($obj, $propName) . " {$op} 1; " . $this->emitDynamicPropertyWrite($obj, $propName, $tmpVar) . ';';
$this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->emitDynamicPropertyFetchRead($var, $target) . " {$op} 1; " . $this->emitDynamicPropertyFetchWrite($var, $tmpVar, $target) . ';';
} else {
$this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->emitDynamicPropertyRead($obj, $propName) . ';';
$this->context->afterStmtLines[] = $this->emitDynamicPropertyWrite($obj, $propName, "{$tmpVar} {$op} 1") . ';';
$this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->emitDynamicPropertyFetchRead($var, $target) . ';';
$this->context->afterStmtLines[] = $this->emitDynamicPropertyFetchWrite($var, "{$tmpVar} {$op} 1", $target) . ';';
}
return $tmpVar;
@ -3049,7 +3050,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$this->context->inAssignExpr = false;
$dim = $this->parseIdentifier($node->dim);
$this->context->inAssignExpr = $oriInAssignExpr;
return $var . '.item(' . $this->trimBrackets($dim) . ', ' . $this->escapeBool($write) . ')';
return $var . '.item(' . $dim . ', ' . $this->escapeBool($write) . ')';
}
}
@ -4221,7 +4222,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
protected function parseBooleanNot(Expr\BooleanNot $expr): string
{
$this->assertExprCanBeUsedAsCondition($expr->expr, 'boolean operand');
return '!(' . $this->parseExpr($expr->expr) . ')';
return '!(' . $this->parseExprAsValue($expr->expr) . ')';
}
protected function parseWhile(Node\Stmt\While_ $v): string
@ -4254,7 +4255,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
protected function parsePrint(Expr\Print_ $expr): string
{
$this->assertExprCanBeUsedAsValue($expr->expr, 'print operand');
return 'php::print(' . $this->parseExpr($expr->expr) . ')';
return 'php::print(' . $this->parseExprAsValue($expr->expr) . ')';
}
protected function parseDo(Node\Stmt\Do_ $v): string
@ -4430,32 +4431,33 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
protected function parseClone(Expr\Clone_ $expr): string
{
$this->assertExprCanBeUsedAsValue($expr->expr, 'clone operand');
return 'php::clone(' . $this->parseExpr($expr->expr) . ')';
return 'php::clone(' . $this->parseExprAsValue($expr->expr) . ')';
}
protected function parseInstanceof(Expr\Instanceof_ $expr): string
{
$this->assertExprCanBeUsedAsValue($expr->expr, 'instanceof operand');
$value = $this->parseExprAsValue($expr->expr);
if ($this->isNameExpr($expr->class)) {
$className = $this->getNamespacedClassName($this->parseIdentifier($expr->class));
$className = $this->getClassEntryPtr($className);
return 'php::instanceOf(' . $this->parseExpr($expr->expr) . ', ' . $className . ')';
return 'php::instanceOf(' . $value . ', ' . $className . ')';
} else {
return 'php::instanceOf(' . $this->parseExpr($expr->expr) . ', ' . $this->identifierToStr($expr->class) . ')';
return 'php::instanceOf(' . $value . ', ' . $this->identifierToStr($expr->class) . ')';
}
}
protected function parseCastInt(Expr\Cast\Int_ $node): string
{
$this->assertExprCanBeUsedAsValue($node->expr, 'cast operand');
return $this->convertIntExpr($this->parseExpr($node->expr));
return $this->convertIntExpr($this->parseExprAsValue($node->expr));
}
protected function parseCastString(Expr\Cast\String_ $node): string
{
$this->assertExprCanBeUsedAsValue($node->expr, 'cast operand');
return $this->convertExprToStringByType(
$this->parseExpr($node->expr),
$this->parseExprAsValue($node->expr),
$this->detectTypeOfExpr($node->expr)
);
}
@ -4463,13 +4465,13 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
protected function parseCastBool(Expr\Cast\Bool_ $node): string
{
$this->assertExprCanBeUsedAsValue($node->expr, 'cast operand');
return $this->convertBoolExpr($this->parseExpr($node->expr));
return $this->convertBoolExpr($this->parseExprAsValue($node->expr));
}
protected function parseCastObject(Expr\Cast\Object_ $node): string
{
$this->assertExprCanBeUsedAsValue($node->expr, 'cast operand');
return $this->convertObjectExpr($this->parseExpr($node->expr));
return $this->convertObjectExpr($this->parseExprAsValue($node->expr));
}
protected function parseConstFetch(Expr\ConstFetch $expr, bool $scalar = false): string
@ -4539,15 +4541,15 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$type = $this->detectTypeOfExpr($expr->expr);
$this->assertExprCanBeUsedAsValue($expr->expr, 'unary operand');
if ($type === self::TYPE_BIGFLOAT) {
return 'php::BigFloat::neg(' . $this->parseExpr($expr->expr) . ')';
return 'php::BigFloat::neg(' . $this->parseExprAsValue($expr->expr) . ')';
}
if ($type === self::TYPE_BIGINT) {
return 'php::BigInt::neg(' . $this->parseExpr($expr->expr) . ')';
return 'php::BigInt::neg(' . $this->parseExprAsValue($expr->expr) . ')';
}
if ($type === self::TYPE_DECIMAL) {
return 'php::Decimal::neg(' . $this->parseExpr($expr->expr) . ')';
return 'php::Decimal::neg(' . $this->parseExprAsValue($expr->expr) . ')';
}
$code = $this->parseExpr($expr->expr);
$code = $this->parseExprAsValue($expr->expr);
return '-' . $code;
}
@ -4555,7 +4557,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
protected function parseUnaryPlus(Expr\UnaryPlus $expr): string
{
$this->assertExprCanBeUsedAsValue($expr->expr, 'unary operand');
return $this->parseExpr($expr->expr);
return $this->parseExprAsValue($expr->expr);
}
protected function parseInterpolatedString(Node\Scalar\InterpolatedString $expr): string
@ -4937,9 +4939,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
}
} elseif ($this->isPropertyFetch($var)) {
$propertyWriteTarget = $this->preparePropertyWriteTarget($var);
$object = $propertyWriteTarget !== null && $propertyWriteTarget->isDynamicObjectProperty()
? $propertyWriteTarget->objectExpr
: $this->parseIdentifier($var->var);
$object = $this->getDynamicPropertyFetchObjectExpr($var, $propertyWriteTarget);
$restoreDefault = null;
if ($this->isIdExpr($var->name)) {
$propertyId = $this->getPropertyIdentifier($var, $var->var, $var->name);
@ -4962,11 +4962,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
}
}
if ($restoreDefault === null) {
if ($propertyWriteTarget !== null && $propertyWriteTarget->isDynamicObjectProperty()) {
$lines[] = $this->emitDynamicPropertyTargetUnset($propertyWriteTarget) . ';';
} else {
$lines[] = $object . '.unsetProperty(' . $this->identifierToStr($var->name, literal: true) . ');';
}
$lines[] = $this->emitDynamicPropertyFetchUnset($var, $propertyWriteTarget) . ';';
}
} elseif ($this->isStaticPropertyFetch($var)) {
$this->fatalError($var, 'Attempt to unset static property ' . $this->parseIdentifier($var->class) . '::$' . $this->parseIdentifier($var->name));
@ -5577,28 +5573,108 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
{
$this->assertDynamicPropertyTarget($target);
return $this->emitDynamicPropertyRead($target->objectExpr, $target->propertyExpr);
return $this->emitDynamicPropertyRead($target->getDynamicObjectExpr(), $target->getDynamicPropertyExpr());
}
protected function emitDynamicPropertyTargetWrite(PropertyWriteTarget $target, string $value): string
{
$this->assertDynamicPropertyTarget($target);
return $this->emitDynamicPropertyWrite($target->objectExpr, $target->propertyExpr, $value);
return $this->emitDynamicPropertyWrite($target->getDynamicObjectExpr(), $target->getDynamicPropertyExpr(), $value);
}
protected function emitDynamicPropertyTargetUnset(PropertyWriteTarget $target): string
{
$this->assertDynamicPropertyTarget($target);
return $target->objectExpr . '.unsetProperty(' . $target->propertyExpr . ')';
return $target->getDynamicObjectExpr() . '.unsetProperty(' . $target->getDynamicPropertyExpr() . ')';
}
protected function emitDynamicPropertyTargetRef(PropertyWriteTarget $target): string
{
$this->assertDynamicPropertyTarget($target);
return $target->objectExpr . '.attrRef(' . $target->propertyExpr . ')';
return $target->getDynamicObjectExpr() . '.attrRef(' . $target->getDynamicPropertyExpr() . ')';
}
protected function emitDynamicPropertyTargetAppendArray(PropertyWriteTarget $target, string $value): string
{
$this->assertDynamicPropertyTarget($target);
return $target->getDynamicObjectExpr() . '.appendArrayProperty(' . $target->getDynamicPropertyExpr() . ', ' . $value . ')';
}
protected function emitDynamicPropertyTargetUpdateArray(PropertyWriteTarget $target, string $dim, string $value): string
{
$this->assertDynamicPropertyTarget($target);
return $target->getDynamicObjectExpr() . '.updateArrayProperty(' . $target->getDynamicPropertyExpr() . ', ' . $dim . ', ' . $value . ')';
}
protected function canEmitDynamicPropertyTarget(?PropertyWriteTarget $target): bool
{
return $target !== null && $target->isDynamicObjectProperty();
}
protected function emitDynamicPropertyFetchRead(Expr\PropertyFetch $expr, ?PropertyWriteTarget $target = null): string
{
if ($this->canEmitDynamicPropertyTarget($target)) {
return $this->emitDynamicPropertyTargetRead($target);
}
return $this->emitDynamicPropertyRead(
$this->parseIdentifier($expr->var),
$this->identifierToStr($expr->name, literal: true)
);
}
protected function emitDynamicPropertyFetchWrite(Expr\PropertyFetch $expr, string $value, ?PropertyWriteTarget $target = null): string
{
if ($this->canEmitDynamicPropertyTarget($target)) {
return $this->emitDynamicPropertyTargetWrite($target, $value);
}
return $this->emitDynamicPropertyWrite(
$this->parseIdentifier($expr->var),
$this->identifierToStr($expr->name, literal: true),
$value
);
}
protected function getDynamicPropertyFetchObjectExpr(Expr\PropertyFetch $expr, ?PropertyWriteTarget $target = null): string
{
if ($this->canEmitDynamicPropertyTarget($target)) {
return $target->getDynamicObjectExpr();
}
return $this->parseIdentifier($expr->var);
}
protected function emitDynamicPropertyFetchUnset(Expr\PropertyFetch $expr, ?PropertyWriteTarget $target = null): string
{
if ($this->canEmitDynamicPropertyTarget($target)) {
return $this->emitDynamicPropertyTargetUnset($target);
}
return $this->parseIdentifier($expr->var) . '.unsetProperty(' . $this->identifierToStr($expr->name, literal: true) . ')';
}
protected function emitDynamicPropertyFetchAppendArray(Expr\PropertyFetch $expr, string $value, ?PropertyWriteTarget $target = null): string
{
if ($this->canEmitDynamicPropertyTarget($target)) {
return $this->emitDynamicPropertyTargetAppendArray($target, $value);
}
return $this->parseIdentifier($expr->var) . '.appendArrayProperty(' . $this->identifierToStr($expr->name) . ', ' . $value . ')';
}
protected function emitDynamicPropertyFetchUpdateArray(Expr\PropertyFetch $expr, string $dim, string $value, ?PropertyWriteTarget $target = null): string
{
if ($this->canEmitDynamicPropertyTarget($target)) {
return $this->emitDynamicPropertyTargetUpdateArray($target, $dim, $value);
}
return $this->parseIdentifier($expr->var) . '.updateArrayProperty(' . $this->identifierToStr($expr->name) . ', ' . $dim . ', ' . $value . ')';
}
protected function assertDynamicPropertyTarget(PropertyWriteTarget $target): void
@ -5611,15 +5687,22 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
protected function emitDynamicPropertyFetchRef(Expr\PropertyFetch $expr, NodeAbstract $errorNode): string
{
$target = $this->preparePropertyWriteTarget($expr);
$objectExpr = $target !== null && $target->isDynamicObjectProperty()
? $target->objectExpr
: $this->parseIdentifier($expr->var);
if ($this->canEmitDynamicPropertyTarget($target)) {
$objectExpr = $target->getDynamicObjectExpr();
if (!$this->hasVar($objectExpr)) {
$this->fatalError($errorNode, 'Undefined variable `$' . $objectExpr . '`');
}
return $this->emitDynamicPropertyTargetRef($target);
}
if (!$this->isVarExpr($expr->var)) {
return $this->parseExpr($expr->var) . '.attrRef(' . $this->identifierToStr($expr->name) . ')';
}
$objectExpr = $this->parseIdentifier($expr->var);
if (!$this->hasVar($objectExpr)) {
$this->fatalError($errorNode, 'Undefined variable `$' . $objectExpr . '`');
}
if ($target !== null && $target->isDynamicObjectProperty()) {
return $this->emitDynamicPropertyTargetRef($target);
}
return $objectExpr . '.attrRef(' . $this->identifierToStr($expr->name) . ')';
}
@ -5703,7 +5786,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
protected function parseCastArray(Expr\Cast\Array_ $expr): string
{
$this->assertExprCanBeUsedAsValue($expr->expr, 'cast operand');
return $this->convertArrayExpr($this->parseExpr($expr->expr));
return $this->convertArrayExpr($this->parseExprAsValue($expr->expr));
}
protected function hasGlobalVar(string $name): bool
@ -5729,12 +5812,12 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
protected function detectFuncCallReturnType(string $name): string
{
if ($this->isInternalFunction($name)) {
$returnType = Reflection::getFunctionReturnType($name);
if ($returnType) {
return $this->getTypeFromZendType($returnType);
}
$name = ltrim($name, '\\');
$returnType = Reflection::getFunctionReturnType($name);
if ($returnType !== null) {
return $this->getTypeFromZendType($returnType);
}
return self::TYPE_VAR;
}
@ -5787,7 +5870,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$methodName = $expr->name->toString();
$receiverType = $this->isVarExpr($expr->var) ? $this->getVarType($object) : $this->detectTypeOfExpr($expr->var);
if ($receiverType === self::TYPE_VOID) {
$this->fatalError($expr->var, 'Cannot call method on void');
$receiverType = self::TYPE_VAR;
}
// to* builtins
if (isset(self::KEYWORD_METHOD_MAP[$methodName])) {
@ -5841,7 +5924,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
if (!$this->isVarExpr($expr->var) and $this->isNamedMethod($expr->name)) {
$type = $this->detectTypeOfExpr($expr->var);
if ($type === self::TYPE_VOID) {
$this->fatalError($expr->var, 'Cannot call method on void');
$type = self::TYPE_VAR;
}
if ($type !== self::TYPE_VAR && !$this->checkArgType($type, self::TYPE_OBJECT)) {
$methodName = $expr->name->toString();
@ -6825,7 +6908,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$this->fatalError($expr, 'Cannot construct BigInt from float, use string or int instead');
}
if ($argType === self::TYPE_INT) {
return 'php::toBigInt(' . $this->trimBrackets($valueExpr) . ')';
return 'php::toBigInt(' . $valueExpr . ')';
}
return 'php::BigInt::newInstance(' . $valueExpr . ')';
}
@ -6840,16 +6923,16 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$this->fatalError($expr, 'Cannot construct Decimal from float variable, use string or int instead');
}
if ($argType === self::TYPE_INT) {
return 'php::toDecimal(' . $this->trimBrackets($valueExpr) . ')';
return 'php::toDecimal(' . $valueExpr . ')';
}
return 'php::Decimal::newInstance(' . $valueExpr . ')';
}
if ($type === self::TYPE_BIGFLOAT) {
if ($argType === self::TYPE_INT) {
return 'php::toBigFloat(' . $this->trimBrackets($valueExpr) . ')';
return 'php::toBigFloat(' . $valueExpr . ')';
}
if ($argType === self::TYPE_FLOAT) {
return 'php::toBigFloat(' . $this->trimBrackets($valueExpr) . ')';
return 'php::toBigFloat(' . $valueExpr . ')';
}
return 'php::BigFloat::newInstance(' . $valueExpr . ')';
}

@ -180,15 +180,6 @@ trait Utils
return str_replace('_', '', $rawValue);
}
protected function trimBrackets(string $str): string
{
if ($this->isClosedExpr($str, '')) {
return substr($str, 1, -1);
}
return $str;
}
protected function getNamespaceOfClass(string $class): string
{
$lastPos = strrpos($class, '\\');

@ -31,7 +31,7 @@ trait AssignOpTrait
$this->addLocalVar($array, self::TYPE_ARRAY);
}
$value = $this->trimBrackets($this->parseExpr($right));
$value = $this->parseExprAsValue($right);
$tmp = $this->genTmpVarName();
$this->addLocalVar($tmp, self::TYPE_VAR);
@ -39,7 +39,7 @@ trait AssignOpTrait
if ($left->dim === null) {
return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.offsetSet(" . self::VALUE_NULL . ", {$tmp})" . '), ' . $tmp . ')';
}
$dim = $this->trimBrackets($this->parseIdentifier($left->dim));
$dim = $this->parseIdentifier($left->dim);
return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.offsetSet({$dim}, {$tmp})" . '), ' . $tmp . ')';
}
@ -50,7 +50,7 @@ trait AssignOpTrait
$this->assertCanAssignPropertyWrite($target, $right);
}
$rightExpr = $this->trimBrackets($this->parseExpr($right));
$rightExpr = $this->parseExprAsValue($right);
if ($target !== null) {
$rightExpr = $this->wrapPropertyWriteTypeCheck($target, $right, $rightExpr);
} else {
@ -60,13 +60,7 @@ trait AssignOpTrait
$tmp = $this->genTmpVarName();
$this->addLocalVar($tmp, self::TYPE_VAR);
// Comma expression: store RHS → execute side effect → evaluate to stored value
if ($target !== null && $target->isDynamicObjectProperty()) {
return '((' . $tmp . ' = ' . $rightExpr . ', ' . $this->emitDynamicPropertyTargetWrite($target, $tmp) . '), ' . $tmp . ')';
}
$array = $this->parseIdentifier($left->var);
$propName = $this->identifierToStr($left->name, literal: true);
return '((' . $tmp . ' = ' . $rightExpr . ', ' . $this->emitDynamicPropertyWrite($array, $propName, $tmp) . '), ' . $tmp . ')';
return '((' . $tmp . ' = ' . $rightExpr . ', ' . $this->emitDynamicPropertyFetchWrite($left, $tmp, $target) . '), ' . $tmp . ')';
}
protected function parseRightAssociativeAssign(NodeAbstract $left, Expr\Assign $right): string
@ -158,7 +152,7 @@ trait AssignOpTrait
}
$finalVarType = $type = $this->detectTypeOfExpr($right);
if ($type === self::TYPE_VOID) {
$this->fatalError($right, 'Cannot use void expression as assignment value');
$finalVarType = $type = self::TYPE_VAR;
}
if ($this->isVarExpr($left)) {
@ -308,7 +302,7 @@ trait AssignOpTrait
protected function parseAssignRightExpr(Expr $right): string
{
$rightExpr = $this->parseExpr($right);
$rightExpr = $this->parseExprAsValue($right);
if ($this->isVarExpr($right)) {
$rightVar = $this->parseIdentifier($right);
if ($this->isStdContainer($rightVar)) {
@ -404,13 +398,7 @@ trait AssignOpTrait
$binaryOp = $this->removeAssignOp($op);
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, self::TYPE_VAR);
if ($propertyWriteTarget !== null && $propertyWriteTarget->isDynamicObjectProperty()) {
$readProperty = $this->emitDynamicPropertyTargetRead($propertyWriteTarget);
} else {
$obj = $this->parseIdentifier($node->var->var);
$propName = $this->identifierToStr($node->var->name, literal: true);
$readProperty = $this->emitDynamicPropertyRead($obj, $propName);
}
$readProperty = $this->emitDynamicPropertyFetchRead($node->var, $propertyWriteTarget);
if ($this->isAssignOpConcat($op)) {
$this->context->beforeStmtLines[] = "{$tmpVar} = php::concat({$readProperty}, {$expr});";
} elseif ($this->isAssignOpPow($op)) {
@ -418,11 +406,7 @@ trait AssignOpTrait
} else {
$this->context->beforeStmtLines[] = "{$tmpVar} = {$readProperty} {$binaryOp} ({$expr});";
}
if ($propertyWriteTarget !== null && $propertyWriteTarget->isDynamicObjectProperty()) {
$this->context->afterStmtLines[] = $this->emitDynamicPropertyTargetWrite($propertyWriteTarget, $tmpVar) . ';';
} else {
$this->context->afterStmtLines[] = $this->emitDynamicPropertyWrite($obj, $propName, $tmpVar) . ';';
}
$this->context->afterStmtLines[] = $this->emitDynamicPropertyFetchWrite($node->var, $tmpVar, $propertyWriteTarget) . ';';
return $tmpVar;
}
@ -515,7 +499,7 @@ trait AssignOpTrait
$id = $this->parseIdentifier($array);
$this->context->inAssignExpr = $oriInAssignExpr;
return $id . '.offsetSet(' . $this->trimBrackets($dim) . ', ' . $this->trimBrackets($var) . ')';
return $id . '.offsetSet(' . $dim . ', ' . $var . ')';
}
protected function parseAssignOpShiftLeft(Expr\AssignOp\ShiftLeft $node): string
@ -562,14 +546,7 @@ trait AssignOpTrait
$rightExpr = $tmpVar . ' = ' . $this->parseIdentifier($expr->expr) . '.toReference()';
} elseif ($this->isPropertyFetch($expr->expr)) {
$left = $this->parseIdentifier($expr->var);
$propertyWriteTarget = $this->preparePropertyWriteTarget($expr->expr);
if ($propertyWriteTarget !== null && $propertyWriteTarget->isDynamicObjectProperty()) {
$rightExpr = $tmpVar . ' = ' . $this->emitDynamicPropertyTargetRef($propertyWriteTarget);
} else {
$object = $this->parseExpr($expr->expr->var);
$prop = $this->identifierToStr($expr->expr->name);
$rightExpr = $tmpVar . ' = ' . $object . '.attrRef(' . $prop . ')';
}
$rightExpr = $tmpVar . ' = ' . $this->emitDynamicPropertyFetchRef($expr->expr, $expr);
} elseif ($this->isArrayDimFetch($expr->expr)) {
$left = $this->parseIdentifier($expr->var);
$array = $this->parseIdentifier($expr->expr->var);
@ -588,25 +565,18 @@ trait AssignOpTrait
protected function parseAssignPropertyArrayDim(NodeAbstract $left, NodeAbstract $right): string
{
$propertyWriteTarget = $this->preparePropertyWriteTarget($left->var);
if ($propertyWriteTarget !== null && $propertyWriteTarget->isDynamicObjectProperty()) {
$obj = $propertyWriteTarget->objectExpr;
$propName = $propertyWriteTarget->propertyExpr;
} else {
$obj = $this->parseIdentifier($left->var->var);
$propName = $this->identifierToStr($left->var->name);
}
$code = '';
$value = $this->trimBrackets($this->parseExpr($right));
$value = $this->parseExprAsValue($right);
$tmp = $this->genTmpVarName();
$this->addLocalVar($tmp, self::TYPE_VAR);
if ($left->dim === null) {
return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$obj}.appendArrayProperty({$propName}, {$tmp})" . '), ' . $tmp . ')';
return $code . '((' . $tmp . ' = ' . $value . ', ' . $this->emitDynamicPropertyFetchAppendArray($left->var, $tmp, $propertyWriteTarget) . '), ' . $tmp . ')';
}
$dim = $this->trimBrackets($this->parseIdentifier($left->dim));
$dim = $this->parseIdentifier($left->dim);
return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$obj}.updateArrayProperty({$propName}, {$dim}, {$tmp})" . '), ' . $tmp . ')';
return $code . '((' . $tmp . ' = ' . $value . ', ' . $this->emitDynamicPropertyFetchUpdateArray($left->var, $dim, $tmp, $propertyWriteTarget) . '), ' . $tmp . ')';
}
protected function parseAssignOpCoalesce(Expr\AssignOp\Coalesce $expr): string

@ -259,10 +259,7 @@ trait BinaryOpTrait
$argList = [];
foreach ($items as $item) {
$type = $this->detectTypeOfExpr($item);
if ($type === self::TYPE_VOID) {
$this->fatalError($expr, 'Cannot concat void');
}
$argList[] = $this->convertExprToStringByType($this->parseExpr($item), $type);
$argList[] = $this->convertExprToStringByType($this->parseExprAsValue($item), $type);
}
return Symbol::concat() . '(' . Symbol::argList() . '{' . implode(', ', $argList) . '})';

@ -30,7 +30,7 @@ trait TypeConversionTrait
protected function convertIntExpr(string $expr): string
{
if (!$this->isClosedExpr($expr, 'php::toInt')) {
return 'php::toInt(' . $this->trimBrackets($expr) . ')';
return 'php::toInt(' . $expr . ')';
}
return $expr;
@ -39,7 +39,7 @@ trait TypeConversionTrait
protected function convertFloatExpr(string $expr): string
{
if (!$this->isClosedExpr($expr, 'php::toFloat')) {
return 'php::toFloat(' . $this->trimBrackets($expr) . ')';
return 'php::toFloat(' . $expr . ')';
}
return $expr;
@ -59,13 +59,13 @@ trait TypeConversionTrait
if ($node instanceof Node\Scalar\String_) {
return 'php::toDecimal(' . $this->getLiteralString($node->value) . ')';
}
return 'php::toDecimal(php::toString(' . $this->trimBrackets($expr) . '))';
return 'php::toDecimal(php::toString(' . $expr . '))';
}
if ($fromType === self::TYPE_INT) {
return 'php::toDecimal(php::toString(' . $this->trimBrackets($expr) . '))';
return 'php::toDecimal(php::toString(' . $expr . '))';
}
if ($fromType === self::TYPE_BIGINT) {
return 'php::toDecimal(php::BigInt::toString(' . $this->trimBrackets($expr) . '))';
return 'php::toDecimal(php::BigInt::toString(' . $expr . '))';
}
return $expr;
}
@ -73,13 +73,13 @@ trait TypeConversionTrait
protected function convertBigIntExpr(string $expr, string $fromType = ''): string
{
if ($fromType === self::TYPE_INT) {
return 'php::toBigInt(' . $this->trimBrackets($expr) . ')';
return 'php::toBigInt(' . $expr . ')';
}
if ($fromType === self::TYPE_FLOAT) {
$this->error('Cannot convert float to BigInt, use string or int instead');
}
if ($fromType === self::TYPE_STR) {
return 'php::toBigInt(php::toString(' . $this->trimBrackets($expr) . '))';
return 'php::toBigInt(php::toString(' . $expr . '))';
}
return $expr;
}
@ -87,19 +87,19 @@ trait TypeConversionTrait
protected function convertBigFloatExpr(string $expr, string $fromType = ''): string
{
if ($fromType === self::TYPE_INT) {
return 'php::toBigFloat(' . $this->trimBrackets($expr) . ')';
return 'php::toBigFloat(' . $expr . ')';
}
if ($fromType === self::TYPE_FLOAT) {
return 'php::toBigFloat(' . $this->trimBrackets($expr) . ')';
return 'php::toBigFloat(' . $expr . ')';
}
if ($fromType === self::TYPE_STR) {
return 'php::toBigFloat(php::toString(' . $this->trimBrackets($expr) . '))';
return 'php::toBigFloat(php::toString(' . $expr . '))';
}
if ($fromType === self::TYPE_BIGINT) {
return 'php::BigFloat::newInstance(php::BigInt::toString(' . $this->trimBrackets($expr) . '))';
return 'php::BigFloat::newInstance(php::BigInt::toString(' . $expr . '))';
}
if ($fromType === self::TYPE_DECIMAL) {
return 'php::BigFloat::newInstance(php::Decimal::toString(' . $this->trimBrackets($expr) . '))';
return 'php::BigFloat::newInstance(php::Decimal::toString(' . $expr . '))';
}
return $expr;
}
@ -107,7 +107,7 @@ trait TypeConversionTrait
protected function convertStringExpr(string $expr): string
{
if (!$this->isClosedExpr($expr, 'php::toString')) {
return 'php::toString(' . $this->trimBrackets($expr) . ')';
return 'php::toString(' . $expr . ')';
}
return $expr;
@ -117,9 +117,9 @@ trait TypeConversionTrait
{
if (!$this->isClosedExpr($expr, 'php::toObject')) {
if ($class === '') {
return 'php::toObject(' . $this->trimBrackets($expr) . ')';
return 'php::toObject(' . $expr . ')';
}
return 'php::toObject(' . $this->trimBrackets($expr) . ', ' . $class . ')';
return 'php::toObject(' . $expr . ', ' . $class . ')';
}
return $expr;
@ -128,7 +128,7 @@ trait TypeConversionTrait
protected function convertArrayExpr(string $expr): string
{
if (!$this->isClosedExpr($expr, 'php::toArray')) {
return 'php::toArray(' . $this->trimBrackets($expr) . ')';
return 'php::toArray(' . $expr . ')';
}
return $expr;
@ -137,7 +137,7 @@ trait TypeConversionTrait
protected function convertBoolExpr(string $expr): string
{
if (!$this->isClosedExpr($expr, 'php::toBool')) {
return 'php::toBool(' . $this->trimBrackets($expr) . ')';
return 'php::toBool(' . $expr . ')';
}
return $expr;

@ -86,6 +86,8 @@ trait TypeDetectionTrait
protected function isInternalFunction(string $name): bool
{
$name = ltrim($name, '\\');
return array_key_exists($name, $this->internalFunctions);
}

@ -15,8 +15,8 @@ final readonly class PropertyWriteTarget
public function __construct(
public NodeAbstract $node,
public string $label,
public ?string $objectExpr = null,
public ?string $propertyExpr = null,
private ?string $objectExpr = null,
private ?string $propertyExpr = null,
) {
}
@ -24,4 +24,22 @@ final readonly class PropertyWriteTarget
{
return $this->objectExpr !== null && $this->propertyExpr !== null;
}
public function getDynamicObjectExpr(): string
{
if (!$this->isDynamicObjectProperty()) {
throw new \LogicException('Property write target is not a dynamic object property');
}
return $this->objectExpr;
}
public function getDynamicPropertyExpr(): string
{
if (!$this->isDynamicObjectProperty()) {
throw new \LogicException('Property write target is not a dynamic object property');
}
return $this->propertyExpr;
}
}

@ -0,0 +1,39 @@
--TEST--
void and never expressions produce null in value contexts
--FILE--
<?php
function sink($value): void
{
var_dump($value);
}
function main(): void
{
$a = usleep(1);
var_dump($a);
$b = \usleep(1);
var_dump($b);
$flag = true && usleep(1);
var_dump($flag);
$items = [usleep(1)];
var_dump($items);
sink(usleep(1));
var_dump("prefix" . usleep(1) . "suffix");
}
?>
--EXPECT--
NULL
NULL
bool(false)
array(1) {
[0]=>
NULL
}
NULL
string(12) "prefixsuffix"
Loading…
Cancel
Save