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
master
韩天峰 6 days ago
parent d33073c021
commit ab1854beb0
  1. 42
      phpunit/code/local-class-constant-declaration-initializer.php
  2. 36
      phpunit/code/local-constant-declaration-initializer.php
  3. 15
      phpunit/code/local-literal-declaration-initializer-native.php
  4. 84
      phpunit/src/LocalVariableInitializerTest.php
  5. 36
      src/Parser/AssignOpTrait.php
  6. 77
      src/Parser/ClassConstantFetchTrait.php
  7. 103
      src/Parser/ConstantExpressionTrait.php
  8. 44
      tests/compiler/basic/local-class-constant-declaration-initializer.phpt
  9. 36
      tests/compiler/basic/local-constant-declaration-initializer.phpt
  10. 19
      tests/compiler/basic/local-literal-declaration-initializer.phpt

@ -0,0 +1,42 @@
<?php
namespace LocalClassConstantInitializer;
class BaseValues
{
public const LIMIT = 128;
}
class Values extends BaseValues
{
public const LABEL = 'typephp';
public function initialize(): void
{
$selfValue = self::LABEL;
$parentValue = parent::LIMIT;
$concreteValue = Values::LABEL;
$selfClass = self::class;
$parentClass = parent::class;
$unknownClass = MissingClass::class;
$lateStatic = static::LABEL;
$external = \DateTimeInterface::ATOM;
$runtimeClassConstant = RuntimeProvider::VALUE;
$class = Values::class;
$dynamicClass = $class::LABEL;
var_dump(
$selfValue,
$parentValue,
$concreteValue,
$selfClass,
$parentClass,
$unknownClass,
$lateStatic,
$external,
$runtimeClassConstant,
$dynamicClass,
);
}
}

@ -0,0 +1,36 @@
<?php
namespace LocalConstantInitializer\Provider {
const LIMIT = 64;
const LABEL = 'known';
}
namespace LocalConstantInitializer\Consumer {
use const LocalConstantInitializer\Provider\LIMIT as IMPORTED_LIMIT;
const ENABLED = true;
function localConstantDeclarationInitializer(): void
{
$imported = IMPORTED_LIMIT;
$qualified = \LocalConstantInitializer\Provider\LABEL;
$namespaced = ENABLED;
$internal = \PHP_INT_MAX;
define('LocalConstantInitializer\\Consumer\\RUNTIME_VALUE', 99);
$runtime = RUNTIME_VALUE;
// An unqualified internal constant in a namespace can be shadowed by
// a namespaced define() before this statement executes.
$namespaceFallback = PHP_VERSION_ID;
var_dump(
$imported,
$qualified,
$namespaced,
$internal,
$runtime,
$namespaceFallback,
);
}
}

@ -0,0 +1,15 @@
<?php
use native_types;
function localLiteralDeclarationInitializerNative(): void
{
$integer = 42;
$negative = -7;
$floating = 1.25;
$boolean = true;
$string = 'hello';
$nullValue = null;
var_dump($integer, $negative, $floating, $boolean, $string, $nullValue);
}

@ -35,4 +35,88 @@ final class LocalVariableInitializerTest extends \BaseTest
); );
self::assertStringNotContainsString('integer = 42L;', $afterDeclaration); self::assertStringNotContainsString('integer = 42L;', $afterDeclaration);
} }
public function testNativeScalarLiteralsInitializeNativeDeclarations(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/local-literal-declaration-initializer-native.php';
$compiler->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);
}
} }

@ -155,17 +155,17 @@ trait AssignOpTrait
return $this->parseAssignFinally( return $this->parseAssignFinally(
$left, $left,
$right, $right,
$this->canFoldLocalLiteralIntoDeclaration($v), $this->canFoldLocalInitializerIntoDeclaration($v),
); );
} }
/** /**
* A pure literal assigned to a new function-top-level local can initialize * A hoist-safe value assigned to a new function-top-level local can
* the already-hoisted C++ declaration directly. Keep arrays and compound * initialize the already-hoisted C++ declaration directly. Keep runtime
* constant expressions on the ordinary path: parsing them may create * constants, arrays and compound expressions on the ordinary path: their
* temporaries whose declaration order must remain explicit. * 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) if (!$assign->getAttribute(self::ATTR_STATEMENT_EXPRESSION, false)
|| $this->context->scopeLevel !== 1 || $this->context->scopeLevel !== 1
@ -178,22 +178,24 @@ trait AssignOpTrait
if ($name === 'this_' || $this->hasVar($name)) { if ($name === 'this_' || $this->hasVar($name)) {
return false; 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\Float_
|| $expr instanceof Node\Scalar\String_ || $expr instanceof Node\Scalar\String_
) { || ($expr instanceof Expr\ConstFetch && $this->isHoistSafeConstFetch($expr))
return true; || ($expr instanceof Expr\ClassConstFetch && $this->isHoistSafeClassConstFetch($expr))
} || (($expr instanceof Expr\UnaryPlus || $expr instanceof Expr\UnaryMinus)
if ($expr instanceof Expr\ConstFetch) { && ($expr->expr instanceof Node\Scalar\Int_ || $expr->expr instanceof Node\Scalar\Float_));
return in_array(strtolower($expr->name->toString()), ['true', 'false', 'null'], true);
} return $literal && in_array(
return ($expr instanceof Expr\UnaryPlus || $expr instanceof Expr\UnaryMinus) $this->detectTypeOfExpr($expr),
&& ($expr->expr instanceof Node\Scalar\Int_ || $expr->expr instanceof Node\Scalar\Float_); [Type::INT, Type::FLOAT, Type::BOOL, Type::STR, Type::VAR],
true,
);
} }
private function parseAssignToMultiReturn(Expr\List_ $left, Expr $right): ?string private function parseAssignToMultiReturn(Expr\List_ $left, Expr $right): ?string

@ -12,9 +12,82 @@ use TypePhp\Type;
use PhpParser\Node\Expr; use PhpParser\Node\Expr;
use PhpParser\NodeAbstract; use PhpParser\NodeAbstract;
use TypePhp\Generator\Symbol; use TypePhp\Generator\Symbol;
use TypePhp\Resolver\Reflection;
trait ClassConstantFetchTrait 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 protected function parseClassConstFetch(Expr\ClassConstFetch $expr): string
{ {
if (!$this->isNameExpr($expr->class)) { if (!$this->isNameExpr($expr->class)) {
@ -89,6 +162,10 @@ trait ClassConstantFetchTrait
return $nativeConst; return $nativeConst;
} }
} }
$internalConst = $this->getInternalScalarClassConstant($class, $const);
if ($internalConst !== null) {
return $this->genInternalScalarConstantValue($internalConst[0]);
}
$ce = $this->getClassEntryPtr($class); $ce = $this->getClassEntryPtr($class);
return Symbol::constant() . '(' . $ce . ', ' . $this->getLiteralString($const) . ')'; return Symbol::constant() . '(' . $ce . ', ' . $this->getLiteralString($const) . ')';
} }

@ -46,32 +46,15 @@ trait ConstantExpressionTrait
return Symbol::constant() . '(' . $ce . ', ' . $this->getLiteralString($ns[1]) . ')'; return Symbol::constant() . '(' . $ce . ', ' . $this->getLiteralString($ns[1]) . ')';
} }
if (isset($this->useConstants[$name])) { [$name, $runtimeNamespaceFallback] = $this->resolveConstantFetchName($expr, $name);
$name = $this->useConstants[$name]; if ($runtimeNamespaceFallback) {
} elseif ($expr->name->isUnqualified()) { // PHP resolves an unqualified constant in a namespace at
if ($this->namespace) { // runtime: first Namespace\NAME, then the global NAME. AOT
$namespacedName = $this->namespace . '\\' . $name; // cannot select only the namespaced spelling because a
if ($this->hasConstant($namespacedName)) { // define() call may execute before this fetch.
return $this->getConstant($namespacedName); return Symbol::constant() . '('
} . $this->getLiteralString($name)
. ', php::ConstantLookup::UnqualifiedInNamespace)';
// 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;
}
} }
if ($this->hasConstant($name)) { if ($this->hasConstant($name)) {
@ -92,6 +75,66 @@ trait ConstantExpressionTrait
return Symbol::constant() . '("' . $this->escapeString($name) . '")'; 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 protected function parseMagicConst(MagicConst $expr): string
{ {
$class = $this->classDef?->getNamespacedName(false) $class = $this->classDef?->getNamespacedName(false)
@ -177,7 +220,11 @@ trait ConstantExpressionTrait
protected function getInternalScalarConstantValue(string $name): string 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)) { if (is_int($value)) {
return $this->genIntegerLiteral($value); return $this->genIntegerLiteral($value);
} }
@ -191,7 +238,7 @@ trait ConstantExpressionTrait
return $this->genCValue($value); return $this->genCValue($value);
} }
if (is_bool($value)) { if (is_bool($value)) {
return $value ? 1 : 0; return $value ? 'true' : 'false';
} }
if (is_string($value)) { if (is_string($value)) {
return $this->genCharPtr($value, true); return $this->genCharPtr($value, true);

@ -0,0 +1,44 @@
--TEST--
Only compile-time class constants initialize hoisted local declarations
--FILE--
<?php
class LocalClassConstantBase
{
public const LIMIT = 128;
}
class LocalClassConstantValues extends LocalClassConstantBase
{
public const LABEL = 'typephp';
public function read(): array
{
$selfValue = self::LABEL;
$parentValue = parent::LIMIT;
$className = self::class;
$lateStatic = static::LABEL;
$external = \DateTimeInterface::ATOM;
return [$selfValue, $parentValue, $className, $lateStatic, $external];
}
}
function main(): void
{
var_dump((new LocalClassConstantValues())->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"
}

@ -0,0 +1,36 @@
--TEST--
Only compile-time constants initialize hoisted local declarations
--FILE--
<?php
namespace LocalConstantInitializer {
const VALUE = 42;
function readValues(): array
{
$compiled = VALUE;
$internal = \PHP_INT_MAX;
define('LocalConstantInitializer\\RUNTIME_VALUE', 99);
$runtime = RUNTIME_VALUE;
return [$compiled, $internal === PHP_INT_MAX, $runtime];
}
}
namespace {
function main(): void
{
var_dump(\LocalConstantInitializer\readValues());
}
}
?>
--EXPECT--
array(3) {
[0]=>
int(42)
[1]=>
bool(true)
[2]=>
int(99)
}

@ -3,19 +3,14 @@ Top-level literal local assignments preserve PHP values
--FILE-- --FILE--
<?php <?php
function values(): array $integer = 42;
{ $negative = -7;
$integer = 42; $floating = 1.25;
$negative = -7; $boolean = true;
$floating = 1.25; $string = 'hello';
$boolean = true; $nullValue = null;
$string = 'hello';
$nullValue = null;
return [$integer, $negative, $floating, $boolean, $string, $nullValue]; var_dump([$integer, $negative, $floating, $boolean, $string, $nullValue]);
}
var_dump(values());
?> ?>
--EXPECT-- --EXPECT--
array(6) { array(6) {

Loading…
Cancel
Save