From b3d97397e0cab6b1a12b274aca3a030f3de67705 Mon Sep 17 00:00:00 2001 From: PurHur Date: Thu, 21 May 2026 16:18:04 +0000 Subject: [PATCH] Fix AOT superglobal isset and coalesce lowering (partial #99, #148) - Lower ?? into the assignee slot when php-cfg emits Assign immediately after Coalesce - Reuse isset container/dim slots for the left-branch fetch (wrong $_GET slot) - Resolve isset targets before branching so child blocks inherit scope - Use runtime __hashtable__offsetIsSetStringKey for superglobal isset in JIT - Branch coalesce merge from branch tail blocks, not entry blocks AOT aot-link: 117 tests, 1 remaining failure (coalesce_get_present empty output). VM coalesce with $_GET works; LLVM echo after merge still needs follow-up. Co-authored-by: Cursor --- lib/Compiler.php | 60 +++++++++++++++++++++++++++++++---------- lib/JIT.php | 8 +++--- lib/JIT/IssetHelper.php | 12 ++++----- 3 files changed, 56 insertions(+), 24 deletions(-) diff --git a/lib/Compiler.php b/lib/Compiler.php index 6ea4147e..81aa8978 100755 --- a/lib/Compiler.php +++ b/lib/Compiler.php @@ -88,7 +88,13 @@ protected function compileOps(array $ops, Block $block): void { if ($child instanceof Op\Expr\Isset_ && count($child->vars) > 1) { $block = $this->compileIssetMulti($child, $block); } elseif ($child instanceof Op\Expr\BinaryOp\Coalesce) { - $block = $this->compileCoalesce($child, $block); + $assignVar = ($i + 1 < $opCount) + ? $this->resolveCoalesceAssignVar($child, $ops[$i + 1]) + : null; + $block = $this->compileCoalesce($child, $block, $assignVar); + if (null !== $assignVar) { + ++$i; + } } elseif ($child instanceof Op\Expr\NullsafePropertyFetch) { $block = $this->compileNullsafePropertyFetch($child, $block); } elseif ( @@ -129,6 +135,31 @@ private function isArrayDimFetchOnlyCoalesceLeft( return $left === $fetch->result; } + /** + * php-cfg emits Assign immediately after Coalesce; lower into the assignee slot (#99, #148). + */ + private function resolveCoalesceAssignVar(Op\Expr\BinaryOp\Coalesce $coalesce, Op $next): ?Operand + { + if (!$next instanceof Op\Expr\Assign) { + return null; + } + $expr = $next->expr; + if ($expr === $coalesce->result) { + return $next->var; + } + while ($expr instanceof Temporary) { + if ($expr === $coalesce->result) { + return $next->var; + } + if (null === $expr->original) { + break; + } + $expr = $expr->original; + } + + return null; + } + protected function compileClassLike(Op\Stmt\ClassLike $class, Block $block): OpCode { $type = 0; if ($class instanceof Op\Stmt\Class_) { @@ -635,9 +666,16 @@ protected function compileIsset(Op\Expr\Isset_ $expr, Block $block): array )]; } - protected function compileCoalesce(Op\Expr\BinaryOp\Coalesce $expr, Block $block): Block + protected function compileCoalesce(Op\Expr\BinaryOp\Coalesce $expr, Block $block, ?Operand $assignVar = null): Block { - $resultSlot = $this->compileOperand($expr->result, $block, false); + $resultOperand = null !== $assignVar ? $assignVar : $expr->result; + $resultSlot = $this->compileOperand($resultOperand, $block, false); + + $checkSlot = $this->compileBoolTemporary($block); + $dimFetch = $this->findCoalesceArrayDimFetch($expr->left, $block); + $issetTarget = null !== $dimFetch + ? $this->resolveIssetTargetFromArrayDimFetch($dimFetch, $block) + : $this->resolveCoalesceIssetTarget($expr->left, $block); $endBlock = new Block($block->orig); $endBlock->inheritUndefinedLocals = true; @@ -657,12 +695,6 @@ protected function compileCoalesce(Op\Expr\BinaryOp\Coalesce $expr, Block $block $leftBlock = new Block($block->orig); $leftBlock->inheritUndefinedLocals = true; $leftBlock->inheritScopeFrom($block); - - $checkSlot = $this->compileBoolTemporary($block); - $dimFetch = $this->findCoalesceArrayDimFetch($expr->left, $block); - $issetTarget = null !== $dimFetch - ? $this->resolveIssetTargetFromArrayDimFetch($dimFetch, $block) - : $this->resolveCoalesceIssetTarget($expr->left, $block); if (null !== $issetTarget) { [$containerSlot, $dimSlot] = $issetTarget; $block->addOpCode(new OpCode( @@ -672,13 +704,13 @@ protected function compileCoalesce(Op\Expr\BinaryOp\Coalesce $expr, Block $block $dimSlot )); if (null !== $dimFetch) { - $this->compileArrayDimFetchRead($dimFetch, $leftBlock); - $leftSlot = $this->compileOperand($dimFetch->result, $leftBlock, true); + // Reuse isset container/dim slots from $block — re-compiling in $leftBlock + // would bind $_GET to a different slot and read the wrong hashtable (#99, #148). $leftBlock->addOpCode(new OpCode( - OpCode::TYPE_ASSIGN, + OpCode::TYPE_ARRAY_DIM_FETCH, $resultSlot, - $resultSlot, - $leftSlot + $containerSlot, + $dimSlot )); } else { $leftSlot = $this->compileOperand($expr->left, $leftBlock, true); diff --git a/lib/JIT.php b/lib/JIT.php index 46474530..371a4363 100644 --- a/lib/JIT.php +++ b/lib/JIT.php @@ -171,7 +171,7 @@ public function compileSubBlock( return $this->compileBlockInternal($func, $block, $limit, null, ...$args); } - + private function compileBlockInternal( PHPLLVM\Value $func, Block $block, @@ -559,15 +559,17 @@ private function compileBlockInternal( $this->context->helper->loadValue($this->context->getVariableFromOp($block->getOperand($op->arg2))) ); $leftBb = JIT\CoalesceHelper::compileBranch($this, $func, $op->block1); + $leftTail = $builder->getInsertBlock(); $rightBb = JIT\CoalesceHelper::compileBranch($this, $func, $op->block2); + $rightTail = $builder->getInsertBlock(); $builder->positionAtEnd($branchBlock); $this->context->freeDeadVariables($func, $branchBlock, $block); $builder->branchIf($condition, $leftBb, $rightBb); if (null !== $op->block3) { $mergeBb = JIT\BasicBlockHelper::append($this->context, 'coalesce_merge'); - $builder->positionAtEnd($leftBb); + $builder->positionAtEnd($leftTail); $builder->branch($mergeBb); - $builder->positionAtEnd($rightBb); + $builder->positionAtEnd($rightTail); $builder->branch($mergeBb); $builder->positionAtEnd($mergeBb); diff --git a/lib/JIT/IssetHelper.php b/lib/JIT/IssetHelper.php index d65ca5e6..304a2c0f 100644 --- a/lib/JIT/IssetHelper.php +++ b/lib/JIT/IssetHelper.php @@ -156,16 +156,14 @@ private static function compileHashTableOffsetIsSet( $superglobalName = self::superglobalName($container, $containerOp); if (null !== $superglobalName) { $key = $dim->compileTimeString ?? self::literalStringKey($dimOp); + $ht = $context->helper->loadValue($container); if (null !== $key) { - $known = SuperglobalInit::compileTimeOffsetIsSet( - $context, - $superglobalName, - $key + return $context->builder->call( + $context->lookupFunction('__hashtable__offsetIsSetStringKey'), + $ht, + $context->helper->loadValue($dim) ); - - return $context->getTypeFromString('int1')->constInt($known ? 1 : 0, false); } - $ht = $context->helper->loadValue($container); if (Variable::TYPE_STRING === $dim->type) { return $context->builder->call( $context->lookupFunction('__hashtable__offsetIsSetStringKey'),