diff --git a/phpunit/code/php-int-max-fold-global.php b/phpunit/code/php-int-max-fold-global.php new file mode 100644 index 00000000..5c4f8a27 --- /dev/null +++ b/phpunit/code/php-int-max-fold-global.php @@ -0,0 +1,11 @@ +compileFixture('php-int-max-fold-namespace.php'); + + // The unqualified fetch reads the namespaced constant at runtime. + self::assertStringContainsString('_const_var_FoldNs__PHP_INT_MAX', $code); + // The fully qualified fetch still folds to the overflowed float. + self::assertStringContainsString('9.2233720368547758e+18', $code); + } + + public function testLowercaseNameIsARuntimeConstantLookup(): void + { + $code = $this->compileFixture('php-int-max-fold-global.php'); + + // php_int_max is undefined in PHP; it must stay a runtime lookup + // that raises the undefined-constant Error, never fold. + self::assertStringContainsString('php::constant(', $code); + // The exact-case global fetch keeps folding. + self::assertStringContainsString('9.2233720368547758e+18', $code); + } + + private function compileFixture(string $file): string + { + global $translator; + + $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); + $translator = $compiler; + $source = TYPEPHP_ROOT_PATH . '/phpunit/code/' . $file; + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + $generated = $compiler->convertFile($source); + $code = file_get_contents($generated); + + self::assertIsString($code); + return $code; + } +} diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index 8ba4e9aa..e2c35d76 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -393,10 +393,13 @@ protected function constantNumericValue(NodeAbstract $expr, bool $nativeSemantic return $value === null ? null : -$value; } if ($expr instanceof Node\Expr\ConstFetch) { - $name = strtolower($expr->name->toString()); + // PHP constants are case-sensitive and unqualified names resolve + // through the namespace first, so only a fetch that provably + // names the global constant may fold to its value. + $name = $this->resolveGlobalFoldableConstantName($expr); return match ($name) { - 'php_int_max' => PHP_INT_MAX, - 'php_int_min' => PHP_INT_MIN, + 'PHP_INT_MAX' => PHP_INT_MAX, + 'PHP_INT_MIN' => PHP_INT_MIN, default => null, }; } @@ -441,6 +444,34 @@ protected function constantNumericValue(NodeAbstract $expr, bool $nativeSemantic }; } + /** + * Resolve a constant fetch to the global constant name it provably + * denotes, or null when the fetch may refer to something else. + * + * A `use const` alias resolves to its target. A fully qualified name is + * already global. An unqualified name inside a namespace participates in + * PHP's runtime fallback (Namespace\NAME can be defined before the fetch + * executes), so it never provably names the global constant. A qualified + * relative name resolves inside a namespace/import and is never global. + */ + protected function resolveGlobalFoldableConstantName(Node\Expr\ConstFetch $expr): ?string + { + $name = ltrim($expr->name->toString(), '\\'); + if (isset($this->useConstants[$name])) { + return ltrim($this->useConstants[$name], '\\'); + } + if ($expr->name instanceof Node\Name\FullyQualified) { + return $name; + } + if (!$expr->name->isUnqualified()) { + return null; + } + if ($this->namespace) { + return null; + } + return $name; + } + protected function constantDivisionValue(int|float $left, int|float $right, bool $nativeSemantics): int|float|null { if ($right == 0) { diff --git a/tests/compiler/const/php-int-max-namespace-shadow.phpt b/tests/compiler/const/php-int-max-namespace-shadow.phpt new file mode 100644 index 00000000..d25063de --- /dev/null +++ b/tests/compiler/const/php-int-max-namespace-shadow.phpt @@ -0,0 +1,32 @@ +--TEST-- +Namespaced PHP_INT_MAX shadows the global constant in unqualified fetches +--FILE-- + +--EXPECT-- +int(6) +float(9.223372036854776E+18) +float(9.223372036854776E+18)