From 06942b0f2ebabf6db19c30d22a888b326bc2b7e0 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sun, 24 May 2026 03:02:53 +0000 Subject: [PATCH] Fix keyed list destructuring VM scope across CFG splits (#1234) String-key array literals before ["key" => $x] destructuring split the compile block for AOT; successor frames lost the array temp until slot values were inherited from the jump predecessor. Simplify the split to a single jump and add a compliance PHPT. Co-authored-by: Cursor --- docs/unsupported-syntax.md | 2 +- lib/Block.php | 6 ++++++ lib/Compiler.php | 14 +++----------- .../cases/language/keyed_list_destruct.phpt | 11 +++++++++++ 4 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 test/compliance/cases/language/keyed_list_destruct.phpt diff --git a/docs/unsupported-syntax.md b/docs/unsupported-syntax.md index 49f57be930d..46659643f94 100644 --- a/docs/unsupported-syntax.md +++ b/docs/unsupported-syntax.md @@ -23,7 +23,7 @@ Exit code `0` when the entry (and best-effort `include`/`require` targets with s Operands that are not foldable (variables, function calls, non-literal concatenation) emit a **stderr warning** (`dynamic include/require (not followed)`) and are not traversed. Runtime include for VM/JIT/AOT is tracked separately ([#54](https://github.com/PurHur/php-compiler/issues/54), [#85](https://github.com/PurHur/php-compiler/issues/85)). -Some constructs (for example `break`/`continue`, `list()` / short-list destructuring, and prefix/postfix `++`/`--`) are lowered by php-cfg before the compiler sees them; they compile in VM/JIT/AOT like ordinary assign and arithmetic. +Some constructs (for example `break`/`continue`, `list()` / short-list / keyed-list destructuring (`["a" => $x]`), and prefix/postfix `++`/`--`) are lowered by php-cfg before the compiler sees them; they compile in VM/JIT/AOT like ordinary assign and array fetches. ## Known gaps (tracking issues) diff --git a/lib/Block.php b/lib/Block.php index 49c0168d248..760d3b49364 100755 --- a/lib/Block.php +++ b/lib/Block.php @@ -339,6 +339,12 @@ public function getFrame(Context $context, ?Frame $frame = null): Frame { throw new \LogicException("Could not resolve argument"); } } else { + if (null !== $frame && isset($frame->scope[$pos])) { + // Preserve predecessor slot values across CFG splits (e.g. string-key + // array literal before keyed list destructuring, issue #1234). + $scope[$pos] = $frame->scope[$pos]; + continue; + } if (null !== $frame) { $inherited = self::findVariableInParentFrames($op, $frame); if (null !== $inherited) { diff --git a/lib/Compiler.php b/lib/Compiler.php index b54a8581016..2c919b6b6db 100755 --- a/lib/Compiler.php +++ b/lib/Compiler.php @@ -226,21 +226,13 @@ private function needsCfgSplitBeforeStringDimFetch(Op $op, Block $block): bool private function splitCfgBlockAfterStringKeyedArray(Block $block): Block { - $hub = new Block($block->orig); - $hub->inheritScopeFrom($block); - $this->inheritFuncFromParent($hub, $block); - $jumpToHub = new OpCode(OpCode::TYPE_JUMP); - $jumpToHub->block1 = $hub; - $block->addOpCode($jumpToHub); - $cont = new Block($block->orig); $cont->inheritScopeFrom($block); - $this->inheritFuncFromParent($cont, $hub); + $this->inheritFuncFromParent($cont, $block); $jumpToCont = new OpCode(OpCode::TYPE_JUMP); $jumpToCont->block1 = $cont; - $hub->addOpCode($jumpToCont); - $hub->parents[] = $block; - $cont->parents[] = $hub; + $block->addOpCode($jumpToCont); + $cont->parents[] = $block; return $cont; } diff --git a/test/compliance/cases/language/keyed_list_destruct.phpt b/test/compliance/cases/language/keyed_list_destruct.phpt new file mode 100644 index 00000000000..0d940507f9c --- /dev/null +++ b/test/compliance/cases/language/keyed_list_destruct.phpt @@ -0,0 +1,11 @@ +--TEST-- +keyed list destructuring with string keys (issue #1234) +--FILE-- + $x, "b" => $y] = array("a" => 1, "b" => 2); +echo $x, $y, "\n"; +["name" => $name] = array("name" => "Ada"); +echo $name, "\n"; +--EXPECT-- +12 +Ada