Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions phpunit/code/php-int-max-fold-global.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

function lowercaseIsRuntime()
{
return php_int_max + 1;
}

function uppercaseFolds(): float
{
return PHP_INT_MAX + 1;
}
15 changes: 15 additions & 0 deletions phpunit/code/php-int-max-fold-namespace.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace FoldNs;

const PHP_INT_MAX = 5;

function shadowedFold(): int
{
return PHP_INT_MAX + 1;
}

function globalFold(): float
{
return \PHP_INT_MAX + 1;
}
50 changes: 50 additions & 0 deletions phpunit/src/PhpIntMaxFoldTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

use TypePhp\CompilerTest;

/**
* The PHP_INT_MAX/PHP_INT_MIN constant folder must resolve the constant name
* like PHP does: case-sensitively, and only to the real global constant. An
* unqualified fetch inside a namespace resolves to Namespace\PHP_INT_MAX
* first, and a lowercase php_int_max is an undefined constant, not the
* global value.
*/
final class PhpIntMaxFoldTest extends \BaseTest
{
public function testNamespacedConstantShadowsGlobalAndIsNotFolded(): void
{
$code = $this->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;
}
}
37 changes: 34 additions & 3 deletions src/Parser/BinaryOpTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
Expand Down Expand Up @@ -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) {
Expand Down
32 changes: 32 additions & 0 deletions tests/compiler/const/php-int-max-namespace-shadow.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
--TEST--
Namespaced PHP_INT_MAX shadows the global constant in unqualified fetches
--FILE--
<?php

namespace N {
const PHP_INT_MAX = 5;

function shadowed(): int
{
return PHP_INT_MAX + 1;
}

function globalValue(): float
{
return \PHP_INT_MAX + 1;
}
}

namespace {
function main(): void
{
var_dump(\N\shadowed());
var_dump(\N\globalValue());
var_dump(PHP_INT_MAX + 1);
}
}
?>
--EXPECT--
int(6)
float(9.223372036854776E+18)
float(9.223372036854776E+18)
Loading