From ab1854beb00fd99d206d3d38efc2a82fbb052cbc Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Thu, 20 Aug 2026 13:27:49 +0800 Subject: [PATCH] refactor(parser): update local variable initializer hoisting logic - Rename canFoldLocalLiteralIntoDeclaration to canFoldLocalInitializerIntoDeclaration - Replace isDeclarationLiteral with isHoistSafeDeclarationInitializer - Add support for hoisting compile-time class constants in addition to literals - Implement isHoistSafeClassConstFetch to determine safe class constant hoisting - Add isHoistSafeConstFetch to validate constant fetch safety for hoisting - Update documentation to reflect hoist-safe value requirements - Add tests for native scalar literals, compile-time constants and class constants - Move internal constant value generation to separate method - Refine constant resolution logic with namespace fallback handling - Add validation for runtime constant dependencies in class constants --- ...class-constant-declaration-initializer.php | 42 +++++++ ...local-constant-declaration-initializer.php | 36 ++++++ ...literal-declaration-initializer-native.php | 15 +++ phpunit/src/LocalVariableInitializerTest.php | 84 ++++++++++++++ src/Parser/AssignOpTrait.php | 36 +++--- src/Parser/ClassConstantFetchTrait.php | 77 +++++++++++++ src/Parser/ConstantExpressionTrait.php | 103 +++++++++++++----- ...lass-constant-declaration-initializer.phpt | 44 ++++++++ ...ocal-constant-declaration-initializer.phpt | 36 ++++++ ...local-literal-declaration-initializer.phpt | 19 ++-- 10 files changed, 435 insertions(+), 57 deletions(-) create mode 100644 phpunit/code/local-class-constant-declaration-initializer.php create mode 100644 phpunit/code/local-constant-declaration-initializer.php create mode 100644 phpunit/code/local-literal-declaration-initializer-native.php create mode 100644 tests/compiler/basic/local-class-constant-declaration-initializer.phpt create mode 100644 tests/compiler/basic/local-constant-declaration-initializer.phpt diff --git a/phpunit/code/local-class-constant-declaration-initializer.php b/phpunit/code/local-class-constant-declaration-initializer.php new file mode 100644 index 00000000..bb9e6568 --- /dev/null +++ b/phpunit/code/local-class-constant-declaration-initializer.php @@ -0,0 +1,42 @@ +addFiles([$source]); + $compiler->prepareFile($source); + $generated = $compiler->convertFile($source); + $code = file_get_contents($generated); + + self::assertIsString($code); + self::assertStringContainsString('php::Int integer = php::toInt(42L);', $code); + self::assertStringContainsString('php::Int negative = php::toInt(-7L);', $code); + self::assertStringContainsString('php::Float floating = php::toFloat(1.25);', $code); + self::assertStringContainsString('php::Bool boolean = php::toBool(true);', $code); + self::assertMatchesRegularExpression('/php::Str string = _literal_strings\[\d+\];/', $code); + self::assertStringContainsString('php::Var nullValue = php::null;', $code); + } + + public function testOnlyCompileTimeConstantsInitializeHoistedDeclarations(): void + { + global $translator; + + $compiler = CompilerTest::create(ROOT_PATH); + $translator = $compiler; + $source = ROOT_PATH . '/phpunit/code/local-constant-declaration-initializer.php'; + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + $generated = $compiler->convertFile($source); + $code = file_get_contents($generated); + + self::assertIsString($code); + self::assertStringContainsString( + 'php::Var imported = _const_var_LocalConstantInitializer__Provider__LIMIT;', + $code, + ); + self::assertStringContainsString( + 'php::Var qualified = _const_var_LocalConstantInitializer__Provider__LABEL;', + $code, + ); + self::assertStringContainsString( + 'php::Var namespaced = _const_var_LocalConstantInitializer__Consumer__ENABLED;', + $code, + ); + self::assertStringContainsString('php::Var internal = ZEND_LONG_MAX;', $code); + + self::assertStringContainsString('php::Var runtime;', $code); + self::assertStringContainsString('runtime = php::constant(', $code); + self::assertStringContainsString('php::Var namespaceFallback;', $code); + self::assertStringContainsString('namespaceFallback = php::constant(', $code); + } + + public function testOnlyCompileTimeClassConstantsInitializeHoistedDeclarations(): void + { + global $translator; + + $compiler = CompilerTest::create(ROOT_PATH); + $translator = $compiler; + $source = ROOT_PATH . '/phpunit/code/local-class-constant-declaration-initializer.php'; + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + $generated = $compiler->convertFile($source); + $code = file_get_contents($generated); + + self::assertIsString($code); + self::assertStringContainsString('php::Var selfValue = _literal_strings[', $code); + self::assertStringContainsString('php::Var parentValue = 128L;', $code); + self::assertStringContainsString('php::Var concreteValue = _literal_strings[', $code); + self::assertStringContainsString('php::Var selfClass = _literal_strings[', $code); + self::assertStringContainsString('php::Var parentClass = _literal_strings[', $code); + self::assertStringContainsString('php::Var unknownClass = _literal_strings[', $code); + + self::assertStringContainsString('php::Var lateStatic;', $code); + self::assertStringContainsString('lateStatic = php::constant(php_get_called_ce(this_)', $code); + self::assertStringContainsString('php::Var external = "', $code); + self::assertStringNotContainsString("php::Var external;\n", $code); + self::assertStringContainsString('php::Var runtimeClassConstant;', $code); + self::assertStringContainsString('runtimeClassConstant = php::constant(', $code); + self::assertStringContainsString('php::Var dynamicClass;', $code); + self::assertStringContainsString('dynamicClass = php::constant(', $code); + } } diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 220b6371..79f0273b 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -155,17 +155,17 @@ trait AssignOpTrait return $this->parseAssignFinally( $left, $right, - $this->canFoldLocalLiteralIntoDeclaration($v), + $this->canFoldLocalInitializerIntoDeclaration($v), ); } /** - * A pure literal assigned to a new function-top-level local can initialize - * the already-hoisted C++ declaration directly. Keep arrays and compound - * constant expressions on the ordinary path: parsing them may create - * temporaries whose declaration order must remain explicit. + * A hoist-safe value assigned to a new function-top-level local can + * initialize the already-hoisted C++ declaration directly. Keep runtime + * constants, arrays and compound expressions on the ordinary path: their + * evaluation order or generated temporaries must remain at the source site. */ - private function canFoldLocalLiteralIntoDeclaration(Expr\Assign $assign): bool + private function canFoldLocalInitializerIntoDeclaration(Expr\Assign $assign): bool { if (!$assign->getAttribute(self::ATTR_STATEMENT_EXPRESSION, false) || $this->context->scopeLevel !== 1 @@ -178,22 +178,24 @@ trait AssignOpTrait if ($name === 'this_' || $this->hasVar($name)) { return false; } - return $this->isDeclarationLiteral($assign->expr); + return $this->isHoistSafeDeclarationInitializer($assign->expr); } - private function isDeclarationLiteral(Expr $expr): bool + private function isHoistSafeDeclarationInitializer(Expr $expr): bool { - if ($expr instanceof Node\Scalar\Int_ + $literal = $expr instanceof Node\Scalar\Int_ || $expr instanceof Node\Scalar\Float_ || $expr instanceof Node\Scalar\String_ - ) { - return true; - } - if ($expr instanceof Expr\ConstFetch) { - return in_array(strtolower($expr->name->toString()), ['true', 'false', 'null'], true); - } - return ($expr instanceof Expr\UnaryPlus || $expr instanceof Expr\UnaryMinus) - && ($expr->expr instanceof Node\Scalar\Int_ || $expr->expr instanceof Node\Scalar\Float_); + || ($expr instanceof Expr\ConstFetch && $this->isHoistSafeConstFetch($expr)) + || ($expr instanceof Expr\ClassConstFetch && $this->isHoistSafeClassConstFetch($expr)) + || (($expr instanceof Expr\UnaryPlus || $expr instanceof Expr\UnaryMinus) + && ($expr->expr instanceof Node\Scalar\Int_ || $expr->expr instanceof Node\Scalar\Float_)); + + return $literal && in_array( + $this->detectTypeOfExpr($expr), + [Type::INT, Type::FLOAT, Type::BOOL, Type::STR, Type::VAR], + true, + ); } private function parseAssignToMultiReturn(Expr\List_ $left, Expr $right): ?string diff --git a/src/Parser/ClassConstantFetchTrait.php b/src/Parser/ClassConstantFetchTrait.php index 194dfb9b..521035f3 100644 --- a/src/Parser/ClassConstantFetchTrait.php +++ b/src/Parser/ClassConstantFetchTrait.php @@ -12,9 +12,82 @@ use TypePhp\Type; use PhpParser\Node\Expr; use PhpParser\NodeAbstract; use TypePhp\Generator\Symbol; +use TypePhp\Resolver\Reflection; trait ClassConstantFetchTrait { + /** + * Return true when a class constant fetch can be evaluated at the hoisted + * local declaration without changing PHP execution order. Only a literal + * ::class name, constants owned by a statically known TypePHP class and + * public scalar constants from an internal class/interface are eligible. + * ZendVM lookup, late static binding and dynamic operands stay at their + * original source position. + */ + protected function isHoistSafeClassConstFetch(Expr\ClassConstFetch $expr): bool + { + if (!$this->isNameExpr($expr->class) || !$this->isIdExpr($expr->name)) { + return false; + } + if ($this->resolvePythonModule($expr->class) !== null) { + return false; + } + + $class = $this->parseIdentifier($expr->class); + if ($class === 'static') { + return false; + } + if ($class === 'self' || $class === 'this_') { + if (!$this->classDef) { + return false; + } + $class = '\\' . $this->getFullClassName(); + } elseif ($class === 'parent') { + if (!$this->classDef || !$this->classDef->extends) { + return false; + } + $class = '\\' . $this->classDef->extends; + } + + $class = $this->getNamespacedClassName($class); + $const = $this->parseIdentifier($expr->name); + if (strcasecmp($const, 'class') === 0) { + return true; + } + if ($this->hasClass($class)) { + if ($this->getClass($class)->enum) { + return false; + } + $nativeConst = $this->findNativeClassConst($expr, $class, $const); + return $nativeConst !== false + && !$this->classConstantValueRequiresRuntimeCall($nativeConst); + } + + return $this->getInternalScalarClassConstant($class, $const) !== null; + } + + private function classConstantValueRequiresRuntimeCall(string $value): bool + { + return preg_match('/\b[A-Za-z_][A-Za-z0-9_:]*\s*\(/', $value) === 1; + } + + /** @return array{mixed}|null */ + private function getInternalScalarClassConstant(string $class, string $const): ?array + { + if (!$this->isInternalClass($class) && !$this->isInternalInterface($class)) { + return null; + } + + $reflection = Reflection::getClass($class); + $constant = $reflection?->getReflectionConstant($const); + if ($constant === false || $constant === null || !$constant->isPublic()) { + return null; + } + + $value = $constant->getValue(); + return is_scalar($value) ? [$value] : null; + } + protected function parseClassConstFetch(Expr\ClassConstFetch $expr): string { if (!$this->isNameExpr($expr->class)) { @@ -89,6 +162,10 @@ trait ClassConstantFetchTrait return $nativeConst; } } + $internalConst = $this->getInternalScalarClassConstant($class, $const); + if ($internalConst !== null) { + return $this->genInternalScalarConstantValue($internalConst[0]); + } $ce = $this->getClassEntryPtr($class); return Symbol::constant() . '(' . $ce . ', ' . $this->getLiteralString($const) . ')'; } diff --git a/src/Parser/ConstantExpressionTrait.php b/src/Parser/ConstantExpressionTrait.php index 8acc2458..3d71265d 100644 --- a/src/Parser/ConstantExpressionTrait.php +++ b/src/Parser/ConstantExpressionTrait.php @@ -46,32 +46,15 @@ trait ConstantExpressionTrait return Symbol::constant() . '(' . $ce . ', ' . $this->getLiteralString($ns[1]) . ')'; } - if (isset($this->useConstants[$name])) { - $name = $this->useConstants[$name]; - } elseif ($expr->name->isUnqualified()) { - if ($this->namespace) { - $namespacedName = $this->namespace . '\\' . $name; - if ($this->hasConstant($namespacedName)) { - return $this->getConstant($namespacedName); - } - - // PHP resolves an unqualified constant in a namespace at - // runtime: first Namespace\NAME, then the global NAME. AOT - // cannot select only the namespaced spelling because a - // define() call may execute before this fetch. - return Symbol::constant() . '(' - . $this->getLiteralString($namespacedName) - . ', php::ConstantLookup::UnqualifiedInNamespace)'; - } - // A class import with the same alias does not affect a bare - // constant fetch. Only `use const` participates here. - } elseif ($expr->name instanceof Node\Name\FullyQualified) { - // parseIdentifier() has already removed the leading slash. - } else { - $fullName = $this->getNamespacedClassName($name); - if ($fullName) { - $name = $fullName; - } + [$name, $runtimeNamespaceFallback] = $this->resolveConstantFetchName($expr, $name); + if ($runtimeNamespaceFallback) { + // PHP resolves an unqualified constant in a namespace at + // runtime: first Namespace\NAME, then the global NAME. AOT + // cannot select only the namespaced spelling because a + // define() call may execute before this fetch. + return Symbol::constant() . '(' + . $this->getLiteralString($name) + . ', php::ConstantLookup::UnqualifiedInNamespace)'; } if ($this->hasConstant($name)) { @@ -92,6 +75,66 @@ trait ConstantExpressionTrait return Symbol::constant() . '("' . $this->escapeString($name) . '")'; } + /** + * Resolve a regular constant name and report whether PHP namespace + * fallback must remain deferred until the source expression executes. + * + * @return array{string, bool} + */ + private function resolveConstantFetchName(Expr\ConstFetch $expr, string $name): array + { + if (isset($this->useConstants[$name])) { + return [$this->useConstants[$name], false]; + } + if ($expr->name->isUnqualified()) { + if ($this->namespace) { + $namespacedName = $this->namespace . '\\' . $name; + return [$namespacedName, !$this->hasConstant($namespacedName)]; + } + + // A class import with the same alias does not affect a bare + // constant fetch. Only `use const` participates here. + return [$name, false]; + } + if ($expr->name instanceof Node\Name\FullyQualified) { + // parseIdentifier() has already removed the leading slash. + return [$name, false]; + } + + return [$this->getNamespacedClassName($name), false]; + } + + /** + * Return true only when reading this constant can be moved to the hoisted + * local declaration at function entry. Runtime define() constants and + * namespace fallback must stay at their original source position. + */ + protected function isHoistSafeConstFetch(Expr\ConstFetch $expr): bool + { + if ($this->resolvePythonModuleMember($expr->name) !== null + || ($expr->name->getType() !== 'Name' && !$expr->name instanceof Node\Name\FullyQualified) + ) { + return false; + } + + $name = ltrim($this->parseIdentifier($expr->name), '\\'); + if (in_array(strtolower($name), ['null', 'true', 'false'], true)) { + return true; + } + if (str_contains($name, '::')) { + return false; + } + + [$name, $runtimeNamespaceFallback] = $this->resolveConstantFetchName($expr, $name); + if ($runtimeNamespaceFallback) { + return false; + } + + return $this->hasConstant($name) + || $name === 'PHP_EOL' + || $this->isInternalScalarConstant($name); + } + protected function parseMagicConst(MagicConst $expr): string { $class = $this->classDef?->getNamespacedName(false) @@ -177,7 +220,11 @@ trait ConstantExpressionTrait protected function getInternalScalarConstantValue(string $name): string { - $value = $this->internalConstants[$name]; + return $this->genInternalScalarConstantValue($this->internalConstants[$name]); + } + + protected function genInternalScalarConstantValue(mixed $value): string + { if (is_int($value)) { return $this->genIntegerLiteral($value); } @@ -191,7 +238,7 @@ trait ConstantExpressionTrait return $this->genCValue($value); } if (is_bool($value)) { - return $value ? 1 : 0; + return $value ? 'true' : 'false'; } if (is_string($value)) { return $this->genCharPtr($value, true); diff --git a/tests/compiler/basic/local-class-constant-declaration-initializer.phpt b/tests/compiler/basic/local-class-constant-declaration-initializer.phpt new file mode 100644 index 00000000..fe788af7 --- /dev/null +++ b/tests/compiler/basic/local-class-constant-declaration-initializer.phpt @@ -0,0 +1,44 @@ +--TEST-- +Only compile-time class constants initialize hoisted local declarations +--FILE-- +read()); +} +?> +--EXPECT-- +array(5) { + [0]=> + string(7) "typephp" + [1]=> + int(128) + [2]=> + string(24) "LocalClassConstantValues" + [3]=> + string(7) "typephp" + [4]=> + string(13) "Y-m-d\TH:i:sP" +} diff --git a/tests/compiler/basic/local-constant-declaration-initializer.phpt b/tests/compiler/basic/local-constant-declaration-initializer.phpt new file mode 100644 index 00000000..e4af2f27 --- /dev/null +++ b/tests/compiler/basic/local-constant-declaration-initializer.phpt @@ -0,0 +1,36 @@ +--TEST-- +Only compile-time constants initialize hoisted local declarations +--FILE-- + +--EXPECT-- +array(3) { + [0]=> + int(42) + [1]=> + bool(true) + [2]=> + int(99) +} diff --git a/tests/compiler/basic/local-literal-declaration-initializer.phpt b/tests/compiler/basic/local-literal-declaration-initializer.phpt index e67b25cb..9b9ab80c 100644 --- a/tests/compiler/basic/local-literal-declaration-initializer.phpt +++ b/tests/compiler/basic/local-literal-declaration-initializer.phpt @@ -3,19 +3,14 @@ Top-level literal local assignments preserve PHP values --FILE-- --EXPECT-- array(6) {