feat(parser): support dynamic class constant name fetch expressions

- Extended BinaryOpTrait to handle temporary variable cleanup for STR, ARRAY, and OBJECT types
- Added parseDynamicClassConstNameFetch method to handle dynamic constant name resolution
- Implemented proper evaluation order for class target and dynamic constant name
- Added support for static, self, parent, and regular class references in dynamic context
- Created materializeDynamicClassConstOperand helper for operand processing
- Updated temporary variable management to clear zval-owning PHPX wrappers
- Added comprehensive tests for dynamic class constant name evaluation semantics
- Implemented proper object lifetime management for temporary arguments
- Added tests for reference-returning function alias preservation
- Fixed temporary call argument lifetime handling in object constructors
pull/48/head
韩天峰 2 weeks ago
parent 8aa8cdb065
commit 01ea7c40be
  1. 8
      src/Parser/BinaryOpTrait.php
  2. 45
      src/Parser/ClassConstantFetchTrait.php
  3. 75
      tests/compiler/class/dynamic-class-constant-name.phpt
  4. 36
      tests/compiler/object_ctor/temporary-call-argument-lifetime.phpt
  5. 50
      tests/compiler/ref/nested-reference-return.phpt

@ -535,11 +535,13 @@ trait BinaryOpTrait
$tmpVar = $this->addTmpVar($type); $tmpVar = $this->addTmpVar($type);
$this->context->beforeStmtLines[] = $tmpVar . ' = ' . $value . ';'; $this->context->beforeStmtLines[] = $tmpVar . ' = ' . $value . ';';
$this->appendCapturedStmtLinesToContext($afterStmts); $this->appendCapturedStmtLinesToContext($afterStmts);
if ($type === Type::VAR) { if (in_array($type, [Type::VAR, Type::STR, Type::ARRAY, Type::OBJECT], true)) {
// The declaration is function-scoped, but PHP releases an owned // The declaration is function-scoped, but PHP releases an owned
// expression temporary after the statement that consumes it. // expression temporary after the statement that consumes it.
// Keeping the value here would extend object lifetimes (notably // All zval-owning PHPX wrappers must be cleared here: an Object is
// WeakReference targets) until the native function returns. // directly observable through __destruct(), while an Array may own
// objects whose destruction would otherwise also be delayed until
// the native function returns.
$this->context->afterStmtLines[] = $tmpVar . '.unset();'; $this->context->afterStmtLines[] = $tmpVar . '.unset();';
} }
return $tmpVar; return $tmpVar;

@ -19,6 +19,10 @@ trait ClassConstantFetchTrait
{ {
$this->rejectPythonModuleClassConstantFetch($expr); $this->rejectPythonModuleClassConstantFetch($expr);
if (!$this->isIdExpr($expr->name)) {
return $this->parseDynamicClassConstNameFetch($expr);
}
if (!$this->isNameExpr($expr->class)) { if (!$this->isNameExpr($expr->class)) {
return $this->parseDynamicClassConstFetch($expr); return $this->parseDynamicClassConstFetch($expr);
} }
@ -97,9 +101,48 @@ trait ClassConstantFetchTrait
return Symbol::constant() . '(php::concat({' . $className . ', "::", ' . $this->getLiteralString($const) . '}))'; return Symbol::constant() . '(php::concat({' . $className . ', "::", ' . $this->getLiteralString($const) . '}))';
} }
protected function parseDynamicClassConstNameFetch(Expr\ClassConstFetch $expr): string
{
$scope = $this->methodDef && $this->classDef
? $this->getClassEntryPtr($this->getFullClassName())
: 'nullptr';
if (!$this->isNameExpr($expr->class)) {
// PHP evaluates the class target before the dynamic constant name.
$target = $this->materializeDynamicClassConstOperand($expr->class, 'class constant target');
$name = $this->materializeDynamicClassConstOperand($expr->name, 'class constant name');
return 'php::classConstant(' . $target . ', ' . $name . ', ' . $scope . ')';
}
$class = $this->parseIdentifier($expr->class);
if ($class === 'static') {
if (!$this->methodDef) {
$this->fatalError($expr, "The 'static' keyword can only be used as the class name in class methods");
}
$ce = Symbol::getCalledCe();
} elseif ($class === 'self' or $class === 'this_') {
$ce = $this->getClassEntryPtr($this->getFullClassName());
} elseif ($class === 'parent') {
if (!$this->classDef || !$this->classDef->extends) {
$this->fatalError($expr, 'Cannot use "parent" outside a class or class does not extend any class');
}
$ce = $this->getClassEntryPtr($this->classDef->extends);
} else {
$ce = $this->getClassEntryPtr($this->getNamespacedClassName($class));
}
$name = $this->materializeDynamicClassConstOperand($expr->name, 'class constant name');
return 'php::classConstant(' . $ce . ', ' . $name . ', ' . $scope . ')';
}
protected function materializeDynamicClassConstTarget(NodeAbstract $expr): string protected function materializeDynamicClassConstTarget(NodeAbstract $expr): string
{ {
$this->assertExprCanBeUsedAsValue($expr, 'class constant target'); return $this->materializeDynamicClassConstOperand($expr, 'class constant target');
}
protected function materializeDynamicClassConstOperand(NodeAbstract $expr, string $description): string
{
$this->assertExprCanBeUsedAsValue($expr, $description);
[$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr); [$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr);
$tmpVar = $this->addTmpVar(Type::VAR); $tmpVar = $this->addTmpVar(Type::VAR);
$this->appendCapturedStmtLinesToContext($beforeStmts); $this->appendCapturedStmtLinesToContext($beforeStmts);

@ -0,0 +1,75 @@
--TEST--
dynamic class constant names preserve PHP lookup and evaluation semantics
--FILE--
<?php
class DynamicConstantName
{
public const ANSWER = 42;
private const SECRET = 'secret';
public static function secret(string $name): string
{
return self::{$name};
}
}
enum DynamicConstantCase
{
case READY;
}
function dynamic_constant_target(): string
{
echo "target\n";
return DynamicConstantName::class;
}
function dynamic_constant_name(): string
{
echo "name\n";
return 'ANSWER';
}
function main(): void
{
$name = 'ANSWER';
var_dump(DynamicConstantName::{$name});
var_dump(dynamic_constant_target()::{dynamic_constant_name()});
$className = 'class';
var_dump(DynamicConstantName::{$className});
$className = 'CLASS';
var_dump(DynamicConstantName::{$className});
$case = 'READY';
var_dump(DynamicConstantCase::{$case} === DynamicConstantCase::READY);
$secret = 'SECRET';
var_dump(DynamicConstantName::secret($secret));
try {
DynamicConstantName::{$secret};
} catch (Error $error) {
echo $error->getMessage(), "\n";
}
$invalid = 1;
try {
DynamicConstantName::{$invalid};
} catch (TypeError $error) {
echo $error->getMessage(), "\n";
}
}
?>
--EXPECT--
int(42)
target
name
int(42)
string(19) "DynamicConstantName"
string(19) "DynamicConstantName"
bool(true)
string(6) "secret"
Cannot access private constant DynamicConstantName::SECRET
Cannot use value of type int as class constant name

@ -0,0 +1,36 @@
--TEST--
Owned call argument temporaries are released at the end of the call statement
--FILE--
<?php
final class TemporaryArgumentLifetimeProbe
{
public static int $destroyed = 0;
public function __destruct()
{
self::$destroyed++;
}
}
function consume_temporary_object(TemporaryArgumentLifetimeProbe $value): void
{
}
function consume_temporary_array(array $values): void
{
}
function main(): void
{
TemporaryArgumentLifetimeProbe::$destroyed = 0;
consume_temporary_object(new TemporaryArgumentLifetimeProbe());
echo 'object=', TemporaryArgumentLifetimeProbe::$destroyed, "\n";
consume_temporary_array([new TemporaryArgumentLifetimeProbe()]);
echo 'array=', TemporaryArgumentLifetimeProbe::$destroyed, "\n";
}
?>
--EXPECT--
object=1
array=2

@ -0,0 +1,50 @@
--TEST--
Reference-returning functions preserve aliases to nested array elements and object properties
--FILE--
<?php
final class NestedReferenceBox
{
public string $value = 'object-before';
}
function &array_element_ref(array &$values): mixed
{
return $values['item'];
}
function &nested_array_element_ref(array &$values): mixed
{
return $values['outer']['inner'];
}
function &object_property_ref(NestedReferenceBox $box): mixed
{
return $box->value;
}
function main(): void
{
$values = [
'item' => 'before',
'outer' => ['inner' => 'nested-before'],
];
$item =& array_element_ref($values);
$item = 'after';
var_dump($values['item']);
$inner =& nested_array_element_ref($values);
$inner = 'nested-after';
var_dump($values['outer']['inner']);
$box = new NestedReferenceBox();
$property =& object_property_ref($box);
$property = 'object-after';
var_dump($box->value);
}
?>
--EXPECT--
string(5) "after"
string(12) "nested-after"
string(12) "object-after"
Loading…
Cancel
Save