From 01ea7c40be973ea35de2f34b22964e8c29ae7ccc Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 11 Aug 2026 18:14:38 +0800 Subject: [PATCH] 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 --- src/Parser/BinaryOpTrait.php | 8 +- src/Parser/ClassConstantFetchTrait.php | 45 ++++++++++- .../class/dynamic-class-constant-name.phpt | 75 +++++++++++++++++++ .../temporary-call-argument-lifetime.phpt | 36 +++++++++ .../compiler/ref/nested-reference-return.phpt | 50 +++++++++++++ 5 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 tests/compiler/class/dynamic-class-constant-name.phpt create mode 100644 tests/compiler/object_ctor/temporary-call-argument-lifetime.phpt create mode 100644 tests/compiler/ref/nested-reference-return.phpt diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index a6722e0f..7f365f5a 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -535,11 +535,13 @@ trait BinaryOpTrait $tmpVar = $this->addTmpVar($type); $this->context->beforeStmtLines[] = $tmpVar . ' = ' . $value . ';'; $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 // expression temporary after the statement that consumes it. - // Keeping the value here would extend object lifetimes (notably - // WeakReference targets) until the native function returns. + // All zval-owning PHPX wrappers must be cleared here: an Object is + // 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();'; } return $tmpVar; diff --git a/src/Parser/ClassConstantFetchTrait.php b/src/Parser/ClassConstantFetchTrait.php index 850cff42..f1ea7c38 100644 --- a/src/Parser/ClassConstantFetchTrait.php +++ b/src/Parser/ClassConstantFetchTrait.php @@ -19,6 +19,10 @@ trait ClassConstantFetchTrait { $this->rejectPythonModuleClassConstantFetch($expr); + if (!$this->isIdExpr($expr->name)) { + return $this->parseDynamicClassConstNameFetch($expr); + } + if (!$this->isNameExpr($expr->class)) { return $this->parseDynamicClassConstFetch($expr); } @@ -97,9 +101,48 @@ trait ClassConstantFetchTrait 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 { - $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); $tmpVar = $this->addTmpVar(Type::VAR); $this->appendCapturedStmtLinesToContext($beforeStmts); diff --git a/tests/compiler/class/dynamic-class-constant-name.phpt b/tests/compiler/class/dynamic-class-constant-name.phpt new file mode 100644 index 00000000..91fe8b3f --- /dev/null +++ b/tests/compiler/class/dynamic-class-constant-name.phpt @@ -0,0 +1,75 @@ +--TEST-- +dynamic class constant names preserve PHP lookup and evaluation semantics +--FILE-- +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 diff --git a/tests/compiler/object_ctor/temporary-call-argument-lifetime.phpt b/tests/compiler/object_ctor/temporary-call-argument-lifetime.phpt new file mode 100644 index 00000000..fee87233 --- /dev/null +++ b/tests/compiler/object_ctor/temporary-call-argument-lifetime.phpt @@ -0,0 +1,36 @@ +--TEST-- +Owned call argument temporaries are released at the end of the call statement +--FILE-- + +--EXPECT-- +object=1 +array=2 diff --git a/tests/compiler/ref/nested-reference-return.phpt b/tests/compiler/ref/nested-reference-return.phpt new file mode 100644 index 00000000..f9985fd2 --- /dev/null +++ b/tests/compiler/ref/nested-reference-return.phpt @@ -0,0 +1,50 @@ +--TEST-- +Reference-returning functions preserve aliases to nested array elements and object properties +--FILE-- +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"