From ea08344c929ba8cfc123cb5e90c7c8bade1ab588 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 14 Aug 2026 16:35:03 +0800 Subject: [PATCH] feat(parser): implement nullsafe operator temporary variable lifecycle management - Add ownedTmpVars array to track temporary variables in nullsafe operations - Register object temporary variables when creating nullsafe access chains - Track intermediate temporary variables during nullsafe traversal - Add cleanup logic to release C++ temporaries at PHP statement boundaries - Generate unset calls for tracked temporaries in reverse order - Create comprehensive test case for nullsafe receiver temporary lifetime - Verify weak reference target release after nullsafe statement completion --- src/Parser/NullsafeAccessTrait.php | 8 +++ .../nullsafe/receiver-temporary-lifetime.phpt | 55 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 tests/compiler/nullsafe/receiver-temporary-lifetime.phpt diff --git a/src/Parser/NullsafeAccessTrait.php b/src/Parser/NullsafeAccessTrait.php index efa203fa..5ba0e215 100644 --- a/src/Parser/NullsafeAccessTrait.php +++ b/src/Parser/NullsafeAccessTrait.php @@ -39,6 +39,7 @@ trait NullsafeAccessTrait ): string { $list = []; + $ownedTmpVars = []; $comment = $this->formatCppLineComment('Nullsafe Operator: ', $this->printer->prettyPrint([$expr])); while (1) { @@ -66,6 +67,7 @@ trait NullsafeAccessTrait } } $object = $this->addTmpVar(Type::OBJECT); + $ownedTmpVars[] = $object; $this->context->beforeStmtLines[] = $this->getIndent() . $object . ' = ' . $this->parseIdentifier($expr) . ';'; break; } @@ -80,6 +82,7 @@ trait NullsafeAccessTrait foreach ($list as $key => $item) { $tmpVar = $this->addTmpVar($key !== $last ? Type::OBJECT : Type::VAR); + $ownedTmpVars[] = $tmpVar; if ($item[3]) { $code .= "if ({$object}.isNull()) { return " . self::VALUE_NULL . '; }'; } @@ -117,6 +120,11 @@ trait NullsafeAccessTrait } $code .= $this->getIndent() . "return {$object}; };"; $this->context->beforeStmtLines[] = $code; + + // C++ temporaries are function-scoped; release their zvals at the PHP statement boundary. + foreach (array_reverse($ownedTmpVars) as $tmpVar) { + $this->context->afterStmtLines[] = $tmpVar . '.unset();'; + } return "{$tmpFn}()"; } diff --git a/tests/compiler/nullsafe/receiver-temporary-lifetime.phpt b/tests/compiler/nullsafe/receiver-temporary-lifetime.phpt new file mode 100644 index 00000000..7b5eeaad --- /dev/null +++ b/tests/compiler/nullsafe/receiver-temporary-lifetime.phpt @@ -0,0 +1,55 @@ +--TEST-- +Nullsafe receiver temporaries release weak targets after the statement +--FILE-- +value; + } +} + +function methodReceiver(): array +{ + $target = new NullsafeWeakReceiver(); + $weak = WeakReference::create($target); + $observed = $weak->get()?->label(); + unset($target); + gc_collect_cycles(); + return [$observed, $weak->get() === null]; +} + +function propertyReceiver(): array +{ + $target = new NullsafeWeakReceiver(); + $weak = WeakReference::create($target); + $observed = $weak->get()?->value; + unset($target); + gc_collect_cycles(); + return [$observed, $weak->get() === null]; +} + +function main(): void +{ + var_dump(methodReceiver()); + var_dump(propertyReceiver()); +} + +?> +--EXPECT-- +array(2) { + [0]=> + string(5) "probe" + [1]=> + bool(true) +} +array(2) { + [0]=> + string(5) "probe" + [1]=> + bool(true) +}