Skip to content
Open
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
51 changes: 51 additions & 0 deletions phpunit/code/class-constant-expression-inline.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

class ClassConstantExpressionInline
{
public const CAP_A = 1;
public const CAP_B = 2;

public const ALL_MASK = self::CAP_A | self::CAP_B;

public function readLeaf(): int
{
return self::CAP_A;
}

public function readMask(): int
{
return self::ALL_MASK;
}
}

interface ClassConstantExpressionInlineFlags
{
public const FLAG_A = 1;
public const FLAG_B = 2;

public const COMBINED = self::FLAG_A | self::FLAG_B;
}

class ClassConstantExpressionInlineFlagUser implements ClassConstantExpressionInlineFlags
{
public function readCombined(): int
{
return self::COMBINED;
}

public function readCombinedDefault(int $value = self::COMBINED): int
{
return $value;
}
}

#[Native]
class ClassConstantExpressionInlineNativeMarker
{
public const LABEL = 'tmp_var_label';

public function label(): string
{
return self::LABEL;
}
}
100 changes: 100 additions & 0 deletions phpunit/src/ClassConstantExpressionInlineTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

use TypePhp\CompilerTest;

final class ClassConstantExpressionInlineTest extends \BaseTest
{
private ?CompilerTest $compiler = null;

public function testLeafConstantIsInlinedButExpressionConstantUsesZendLookup(): void
{
$code = $this->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);
}

public function testInterfaceConstantDefaultParameterEvaluatesAtCompileTime(): void
{
$this->compileFixture();

// The default-parameter (arginfo) path resolves the constant through
// getClassConstValue, which must stay independent of the method-body
// inline-eligibility gate: an expression-valued interface constant
// fetched on the implementing class must still evaluate to 3.
$default = $this->compiler->getClassConstValue(
new \PhpParser\Node\Expr\ClassConstFetch(
new \PhpParser\Node\Name('ClassConstantExpressionInlineFlagUser'),
'COMBINED',
),
'ClassConstantExpressionInlineFlagUser',
'COMBINED',
);

self::assertSame(3, $default);
}

public function testNativeLiteralContainingTmpVarNameIsInlinedWithNoLiteralStrings(): void
{
$code = $this->compileFixture(['noLiteralStrings' => true]);

// Native classes emit their methods as C++ member functions, so the
// body selector uses the qualified `Class::method` name.
$label = $this->methodBody(
$code,
'php_classconstantexpressioninlinenativemarker::label'
);
self::assertStringContainsString('ZEND_STRL("tmp_var_label")', $label);
self::assertDoesNotMatchRegularExpression('/php::(constant|classConstant)\(/', $label);
}

/**
* @param array{noLiteralStrings?: bool} $options
*/
private function compileFixture(array $options = []): string
{
global $translator;

$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
if (isset($options['noLiteralStrings'])) {
$property = new \ReflectionProperty(\TypePhp\CompilerBase::class, 'noLiteralStrings');
$property->setValue($compiler, $options['noLiteralStrings']);
}
$this->compiler = $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];
}
}
32 changes: 32 additions & 0 deletions src/CompilerBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -3375,6 +3375,20 @@ protected function findNativeClassConst(
if (!$interfaceConstDef->codegenFinalized) {
return false;
}
// Inline-eligibility only: a value that depends on
// materialize-temporaries (tmp_var_N, recorded structurally at
// lowering time) cannot be inlined into the fetching function
// because those locals only exist in arrayExpr's prelude
// (C2065 / C2593). Fall back to the Zend lookup below.
// Native classes have no zend_class_entry, so the Zend fallback
// is unsupported for them: keep their constants on the
// compile-time inline path.
if (!$this->isNativeObjectClass($class)
&& ($interfaceConstDef->arrayExpr !== ''
|| $interfaceConstDef->hasMaterializedTemporaries)
) {
return false;
}
$expr->setAttribute('nativeConst', $interfaceConstDef);
return $interfaceConstDef->value;
}
Expand All @@ -3400,6 +3414,24 @@ protected function findNativeClassConst(
if (!$constDef->codegenFinalized) {
return false;
}
// Inline-eligibility only: a constant initializer that parse
// lowered into materialize temporaries (tmp_var_N, recorded
// structurally at lowering time) 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.
// Native classes have no zend_class_entry, so the Zend fallback
// is unsupported for them: keep their constants on the
// compile-time inline path.
if (!$this->isNativeObjectClass($class)
&& ($constDef->arrayExpr !== '' || $constDef->hasMaterializedTemporaries)
) {
return false;
}
$expr->setAttribute('nativeConst', $constDef);
return $constDef->value;
}
Expand Down
9 changes: 9 additions & 0 deletions src/Entity/ConstantDef.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ class ConstantDef
public ?NodeAbstract $valueExpr = null;
/** True after the declaration AST has been lowered to C++ in convert. */
public bool $codegenFinalized = false;
/**
* True when lowering this constant's initializer materialized temporaries
* (tmp_var_N) into the local scope. Those temporaries only exist inside
* arrayExpr's prelude, so the value string must never be inlined into a
* fetching function. Set structurally at lowering time from the function
* context (never by inspecting the value text, which may legitimately
* contain substrings like "tmp_var_" inside string literals).
*/
public bool $hasMaterializedTemporaries = false;
/** Explicit declared type (e.g. `const int FOO`); null for inferred/untyped constants. */
public ?string $declaredType = null;

Expand Down
11 changes: 11 additions & 0 deletions src/Preprocessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,11 @@ protected function finalizePreparedConstant(ConstantDef $constant, Node\Expr $ex
$this->resetFunction();
$constant->arrayExpr = '';
$constant->value = $this->parseIdentifier($expression);
// resetFunction() installed a fresh context, so the temporary counter
// reflects only this constant's lowering — a structural record of
// whether the value depends on prelude temporaries.
$constant->hasMaterializedTemporaries = $this->context->tmpVarIndex > 0
|| $this->context->beforeStmtLines !== [];
if ($this->context->beforeStmtLines) {
if ($this->context->localVars) {
$constant->arrayExpr .= $this->genScopeVarDecl();
Expand Down Expand Up @@ -2348,6 +2353,12 @@ private function parseClassLikeConstant(Node\Const_ $const, int $flags, string $
$constInfo->valueExpr = $const->value;
$constInfo->declaredType = $declaredType;
$constInfo->codegenFinalized = $this->compilerPhase === self::PHASE_CONVERT;
if ($constInfo->codegenFinalized) {
// Structural dependency record: computed from the lowering context
// (temporary counter / captured prelude), not from the value text.
$constInfo->hasMaterializedTemporaries = $this->context->tmpVarIndex > 0
|| $this->context->beforeStmtLines !== [];
}

if ($constInfo->codegenFinalized && $this->context->beforeStmtLines) {
$arrayExpr = '';
Expand Down
21 changes: 21 additions & 0 deletions src/Resolver/ClassConstantValueTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,27 @@ public function getClassConstValue(NodeAbstract $expr, string $_class, string $n
// `K::CONST === E::Case` false through every dynamic path.
return new EnumCaseRef($classDef->getNamespacedName(false), $name);
}
// PHP also resolves a fetch like `self::MASK` on an implementing
// class through the class's interfaces when neither the class nor
// any parent declares the constant. Constant DEFINITION lookup
// must not depend on findNativeClassConst's inline-eligibility
// gate (which legitimately returns false for expression-valued
// constants whose value string cannot be inlined), so the
// interface search runs independently of any codegen decision and
// evaluates the retained AST.
foreach ($this->getClassImplementedInterfaces($classDef) as $interfaceName) {
$constant = $this->findCompileTimeInterfaceConstant($interfaceName, $name);
if ($constant !== null) {
[$interface, $interfaceConstDef] = $constant;
return $this->evaluateCompileTimeClassConstant(
$expr,
$interface,
$interfaceConstDef,
$name,
$interface,
);
}
}
}
$this->fatalError($expr, "Class constant `{$class}::{$name}` not found");
}
Expand Down
Loading