diff --git a/docs/REFACTORING_PLAN.md b/docs/REFACTORING_PLAN.md index 7ba7a70a..78fbd490 100644 --- a/docs/REFACTORING_PLAN.md +++ b/docs/REFACTORING_PLAN.md @@ -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:类型系统模块化 diff --git a/examples/void.php b/examples/void.php new file mode 100644 index 00000000..9384b8dd --- /dev/null +++ b/examples/void.php @@ -0,0 +1,8 @@ +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(); } -} \ No newline at end of file +} diff --git a/phpunit/code/internal-void-function-fully-qualified-assignment.php b/phpunit/code/internal-void-function-fully-qualified-assignment.php new file mode 100644 index 00000000..66f2a883 --- /dev/null +++ b/phpunit/code/internal-void-function-fully-qualified-assignment.php @@ -0,0 +1,6 @@ +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() diff --git a/phpunit/src/FunctionTest.php b/phpunit/src/FunctionTest.php index e547d5cb..c52d6e9a 100644 --- a/phpunit/src/FunctionTest.php +++ b/phpunit/src/FunctionTest.php @@ -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'); } } diff --git a/phpunit/src/Generator/UtilsTest.php b/phpunit/src/Generator/UtilsTest.php index 0018497b..8acbd90b 100644 --- a/phpunit/src/Generator/UtilsTest.php +++ b/phpunit/src/Generator/UtilsTest.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')); - } } diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index c216b715..93ce84ab 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -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 . ')'; } diff --git a/src/Php/Generator/Utils.php b/src/Php/Generator/Utils.php index 85fd3fa7..45d3fb17 100644 --- a/src/Php/Generator/Utils.php +++ b/src/Php/Generator/Utils.php @@ -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, '\\'); diff --git a/src/Php/Parser/AssignOpTrait.php b/src/Php/Parser/AssignOpTrait.php index 021d655e..b23b0614 100644 --- a/src/Php/Parser/AssignOpTrait.php +++ b/src/Php/Parser/AssignOpTrait.php @@ -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 diff --git a/src/Php/Parser/BinaryOpTrait.php b/src/Php/Parser/BinaryOpTrait.php index 5637f3b5..f0c281f0 100644 --- a/src/Php/Parser/BinaryOpTrait.php +++ b/src/Php/Parser/BinaryOpTrait.php @@ -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) . '})'; diff --git a/src/Php/Parser/TypeConversionTrait.php b/src/Php/Parser/TypeConversionTrait.php index cec3d0b8..a571d3df 100644 --- a/src/Php/Parser/TypeConversionTrait.php +++ b/src/Php/Parser/TypeConversionTrait.php @@ -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; diff --git a/src/Php/Parser/TypeDetectionTrait.php b/src/Php/Parser/TypeDetectionTrait.php index 715b8ce3..eba7cef7 100644 --- a/src/Php/Parser/TypeDetectionTrait.php +++ b/src/Php/Parser/TypeDetectionTrait.php @@ -86,6 +86,8 @@ trait TypeDetectionTrait protected function isInternalFunction(string $name): bool { + $name = ltrim($name, '\\'); + return array_key_exists($name, $this->internalFunctions); } diff --git a/src/Php/Resolver/PropertyWriteTarget.php b/src/Php/Resolver/PropertyWriteTarget.php index 4bb8fbb3..158f7808 100644 --- a/src/Php/Resolver/PropertyWriteTarget.php +++ b/src/Php/Resolver/PropertyWriteTarget.php @@ -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; + } } diff --git a/tests/aot/basic/void-expression-null.phpt b/tests/aot/basic/void-expression-null.phpt new file mode 100644 index 00000000..37d93dad --- /dev/null +++ b/tests/aot/basic/void-expression-null.phpt @@ -0,0 +1,39 @@ +--TEST-- +void and never expressions produce null in value contexts +--FILE-- + +--EXPECT-- +NULL +NULL +bool(false) +array(1) { + [0]=> + NULL +} +NULL +string(12) "prefixsuffix"