From 73f63607e1c5c86ae86fb77790167c13b3f48209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8E=9F=E7=82=B9?= <467490186@qq.com> Date: Tue, 20 Oct 2026 21:04:11 +0800 Subject: [PATCH] fix(compiler): fall back to Zend lookup for expression-valued class constants findNativeClassConst inlines a constant's value string directly into the fetching expression. When the initializer is an expression, parse lowers it into materialize temporaries (tmp_var_N) whose declarations only exist in arrayExpr's prelude, which is emitted together with array constants. The inlined value therefore references locals the fetching function does not have: `const ALL = A | B` fetched inside a method body compiles to `((php::Var(tmp_var_0)) | (php::Var(tmp_var_1)))`, a self-referencing temporary plus an undeclared one (C2065 / C2593). Return false when the value string carries a materialize temporary or the constant has an array expression, so the existing Zend class-constant lookup is used instead. That lookup reads the already evaluated literal registered for the constant and is cached in the convert phase, the same shape as the existing forward-reference fallback. Literal constants keep being inlined. Interface-owned constants resolved through an implementing class take the same guard, since they flow through the identical inline path. Add a PHPUnit codegen fixture with a literal constant and an expression constant in the same class, asserting the leaf constant is still inlined while the expression constant goes through the Zend lookup and carries no inlined temporary. An interface constant fetched on an implementing class is covered by the same assertions. Validation: the new test fails on the parent commit and passes with the guard; the full suite keeps the pre-existing failures of master with an identical failing test list on both Windows and Linux, and an AOT build of the interface fixture links and prints the expected result. --- .../code/class-constant-expression-inline.php | 35 ++++++++++++ .../src/ClassConstantExpressionInlineTest.php | 56 +++++++++++++++++++ src/CompilerBase.php | 18 ++++++ 3 files changed, 109 insertions(+) create mode 100644 phpunit/code/class-constant-expression-inline.php create mode 100644 phpunit/src/ClassConstantExpressionInlineTest.php diff --git a/phpunit/code/class-constant-expression-inline.php b/phpunit/code/class-constant-expression-inline.php new file mode 100644 index 00000000..cf33a609 --- /dev/null +++ b/phpunit/code/class-constant-expression-inline.php @@ -0,0 +1,35 @@ +compileFixture(); + + $leaf = $this->methodBody($code, 'php_classconstantexpressioninline__readleaf'); + self::assertDoesNotMatchRegularExpression('/php::(constant|classConstant)\(/', $leaf); + self::assertMatchesRegularExpression('/= \(1L+\);/', $leaf); + + $mask = $this->methodBody($code, 'php_classconstantexpressioninline__readmask'); + self::assertStringContainsString('php::constant(', $mask); + self::assertDoesNotMatchRegularExpression('/php::Var\(tmp_var_\d+\)/', $mask); + } + + public function testInterfaceConstantFetchOnImplementingClassUsesZendLookup(): void + { + $code = $this->compileFixture(); + + $combined = $this->methodBody( + $code, + 'php_classconstantexpressioninlineflaguser__readcombined' + ); + self::assertStringContainsString('php::constant(', $combined); + self::assertDoesNotMatchRegularExpression('/php::Var\(tmp_var_\d+\)/', $combined); + } + + private function compileFixture(): string + { + global $translator; + + $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); + $translator = $compiler; + $source = TYPEPHP_ROOT_PATH . '/phpunit/code/class-constant-expression-inline.php'; + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + $generated = $compiler->convertFile($source); + $code = file_get_contents($generated); + + self::assertIsString($code); + return $code; + } + + private function methodBody(string $code, string $function): string + { + $pattern = '/' . preg_quote($function, '/') . '\([^)]*\) \{[\s\S]*?\n\}/'; + preg_match($pattern, $code, $matches); + + self::assertSame(1, count($matches), "Missing body for {$function}"); + return $matches[0]; + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 6d8bd986..111ddae6 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -3375,6 +3375,13 @@ protected function findNativeClassConst( if (!$interfaceConstDef->codegenFinalized) { return false; } + // Same materialize-temporary guard as the class-constant + // branch below: inlining a tmp_var_N initializer would + // reference locals that do not exist in the fetching function + // (C2065 / C2593). + if ($interfaceConstDef->arrayExpr !== '' || str_contains($interfaceConstDef->value, 'tmp_var_')) { + return false; + } $expr->setAttribute('nativeConst', $interfaceConstDef); return $interfaceConstDef->value; } @@ -3400,6 +3407,17 @@ protected function findNativeClassConst( if (!$constDef->codegenFinalized) { return false; } + // A constant initializer that parse lowered into materialize + // temporaries (tmp_var_N) cannot be inlined: those temporaries are + // declared and assigned only in arrayExpr's prelude, which is + // emitted together with array constants. Inlining the value string + // into the fetching function references locals that do not exist + // there (C2065 / C2593). Fall back to the Zend class-constant + // lookup instead: it reads the already-evaluated literal that was + // registered for the constant and is cached in the convert phase. + if ($constDef->arrayExpr !== '' || str_contains($constDef->value, 'tmp_var_')) { + return false; + } $expr->setAttribute('nativeConst', $constDef); return $constDef->value; }