From c4ce7008685ca051e9cae1a583242bcd5e64d04d Mon Sep 17 00:00:00 2001 From: Giandonn Date: Sat, 29 Aug 2026 20:08:20 -0300 Subject: [PATCH] fix(optimizer): class_exists() must not fold a trait name to true doFoldKnownClass folds class_exists() whenever the name is a literal the symbol table knows. That table also holds traits, so a trait name folded to true while PHP answers false: trait Helper {} class_exists('Helper'); // folded to true $name = 'Helper'; class_exists($name); // reaches php::fn::class_exists, answers false The same program therefore gives two different answers for the same trait, decided only by whether the argument is a literal. The runtime side is already right, and deliberately so: traits are compile-time AST templates in TypePHP, which is why tests/compiler/stdlib/class_exists.phpt expects trait_exists() to be false. Only the constant fold disagreed - with PHP and with the compiler's own runtime. A trait name now folds to false. Classes and enums keep folding to true, which matches PHP: an enum is a class, a trait is not. class_exists.phpt gains the literal and non-literal trait cases, and ClassExistsTraitFoldTest pins the fold decision in the generated C++. --- phpunit/code/class-exists-class-and-enum.php | 22 +++++++++ phpunit/code/class-exists-trait.php | 16 +++++++ phpunit/src/ClassExistsTraitFoldTest.php | 50 ++++++++++++++++++++ src/Optimizer/FuncCallOptimizer.php | 5 ++ tests/compiler/stdlib/class_exists.phpt | 8 ++++ 5 files changed, 101 insertions(+) create mode 100644 phpunit/code/class-exists-class-and-enum.php create mode 100644 phpunit/code/class-exists-trait.php create mode 100644 phpunit/src/ClassExistsTraitFoldTest.php diff --git a/phpunit/code/class-exists-class-and-enum.php b/phpunit/code/class-exists-class-and-enum.php new file mode 100644 index 00000000..234deb17 --- /dev/null +++ b/phpunit/code/class-exists-class-and-enum.php @@ -0,0 +1,22 @@ +compileToCpp('class-exists-trait.php'); + + // The trait is known at compile time, so the call is still folded - + // just to the answer PHP gives, which is false for a trait. + self::assertStringNotContainsString('php::fn::class_exists(', $cpp); + self::assertStringContainsString('= false;', $cpp); + } + + public function testClassAndEnumNamesStillFoldToTrue(): void + { + $cpp = $this->compileToCpp('class-exists-class-and-enum.php'); + + self::assertStringNotContainsString('php::fn::class_exists(', $cpp); + self::assertStringNotContainsString('= false;', $cpp); + } + + private function compileToCpp(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); + + return file_get_contents($compiler->convertFile($source)); + } +} diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index fe2ae690..423f0270 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -667,6 +667,11 @@ trait FuncCallOptimizer if (!$this->isScalarString($cn) || !$this->hasClass($cn->value)) { return false; } + // The class table also carries traits, but a trait is not a class to + // class_exists(): PHP answers false for it and true for an enum. + if ($this->getClassDef($cn->value)?->trait !== null) { + return 'false'; + } return $this->isNativeObjectClass($cn->value) ? 'false' : 'true'; } diff --git a/tests/compiler/stdlib/class_exists.phpt b/tests/compiler/stdlib/class_exists.phpt index bfab60a7..039466d1 100644 --- a/tests/compiler/stdlib/class_exists.phpt +++ b/tests/compiler/stdlib/class_exists.phpt @@ -16,6 +16,12 @@ function main() { var_dump(class_exists("NonexistentClass")); var_dump(class_exists("NonexistentClass", false)); + // A trait is not a class. The answer must not depend on whether the name + // is a literal the compiler can resolve at compile time. + var_dump(class_exists("MyTrait")); + $traitName = "MyTrait"; + var_dump(class_exists($traitName)); + // interface_exists var_dump(interface_exists("MyInterface")); var_dump(interface_exists("NonexistentInterface")); @@ -38,6 +44,8 @@ bool(true) bool(true) bool(false) bool(false) +bool(false) +bool(false) bool(true) bool(false) bool(false)