From 8c96d6493fd31653bb0c0c92bdda8b8fc35a0680 Mon Sep 17 00:00:00 2001 From: PurHur Date: Tue, 18 Aug 2026 02:14:10 +0000 Subject: [PATCH] =?UTF-8?q?AOT:=20lower=20remaining=20unimplemented=20rows?= =?UTF-8?q?=20=E2=80=94=20enum=20const,=20$obj::call,=20interface=20const,?= =?UTF-8?q?=20static=20array=20store=20(#31967)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix strtol assignment sites blocked by #31988, variable static calls via runtime class id, interface self:: const inheritance at JIT define time, enum case class constants after DECLARE_ENUM, and boxed static array property stores. Guard with Issue31967RemainingAotTest. Co-authored-by: Cursor --- ext/filter/JitFilter.php | 4 +- ext/standard/JitChr.php | 4 +- ext/standard/JitImageTypeArg.php | 4 +- ext/standard/JitIntdiv.php | 4 +- ext/standard/JitSessionStorageKernel.php | 8 +- ext/standard/JitSleep.php | 4 +- lib/JIT.php | 157 +++++++++++++--- lib/JIT/BackedEnumFromJit.php | 31 ++-- .../Builtin/Type/ObjectStaticPropertyLlvm.php | 25 ++- lib/JIT/Builtin/Type/Object_.php | 167 +++++++++++++++++- .../Call/RuntimeIndirectStaticMethodCall.php | 3 +- lib/JIT/ClassConstFetchHelperTrait.php | 5 +- lib/JIT/HashTableMergeLlvm.php | 4 +- lib/JIT/HashTableWriteLlvm.php | 10 +- lib/JIT/JitLongArg.php | 4 +- lib/JIT/VmConstantJit.php | 9 + lib/VM/VmUnaryPlus.php | 4 +- lib/VM/VmValueCompare.php | 4 +- test/repro/issue_31967_enum_class_const.php | 5 + .../issue_31967_interface_self_const.php | 5 + test/repro/issue_31967_static_array_store.php | 7 + .../issue_31967_variable_static_call.php | 9 + test/unit/Issue31967RemainingAotTest.php | 62 +++++++ 23 files changed, 461 insertions(+), 78 deletions(-) create mode 100644 test/repro/issue_31967_enum_class_const.php create mode 100644 test/repro/issue_31967_interface_self_const.php create mode 100644 test/repro/issue_31967_static_array_store.php create mode 100644 test/repro/issue_31967_variable_static_call.php create mode 100644 test/unit/Issue31967RemainingAotTest.php diff --git a/ext/filter/JitFilter.php b/ext/filter/JitFilter.php index b529e2e321a..88d1cdfc3ae 100644 --- a/ext/filter/JitFilter.php +++ b/ext/filter/JitFilter.php @@ -1259,9 +1259,9 @@ private static function stringToInt64(Context $context, Value $strPtr): Value $nullEnd = $context->getTypeFromString('int8*')->constNull(); $context->builder->store($nullEnd, $endPtrSlot); $i32 = $context->getTypeFromString('int32'); - $parsed = // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). + // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). LibcExtern::ensureStrtolDecl($context); - $context->builder->call( + $parsed = $context->builder->call( $context->lookupFunction('strtol'), $charPtr, $endPtrSlot, diff --git a/ext/standard/JitChr.php b/ext/standard/JitChr.php index b428d367c55..c3d659ad9af 100644 --- a/ext/standard/JitChr.php +++ b/ext/standard/JitChr.php @@ -258,9 +258,9 @@ private static function stringPtrToLong(Context $context, Value $strPtr): Value $charPtr = $context->builder->structGep($strPtr, $map['value']); $endPtrSlot = $context->builder->alloca($context->getTypeFromString('int8*'), 1, 'chr_strtol_end'); $context->builder->store($context->getTypeFromString('int8*')->constNull(), $endPtrSlot); - $raw = // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). + // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). LibcExtern::ensureStrtolDecl($context); - $context->builder->call( + $raw = $context->builder->call( $context->lookupFunction('strtol'), $charPtr, $endPtrSlot, diff --git a/ext/standard/JitImageTypeArg.php b/ext/standard/JitImageTypeArg.php index fb23d552fb9..ab361109f63 100644 --- a/ext/standard/JitImageTypeArg.php +++ b/ext/standard/JitImageTypeArg.php @@ -237,9 +237,9 @@ private static function stringPtrToLong(Context $context, Value $strPtr): Value $charPtr = $context->builder->structGep($strPtr, $map['value']); $endPtrSlot = $context->builder->alloca($context->getTypeFromString('int8*'), 1, 'imgext_strtol_end'); $context->builder->store($context->getTypeFromString('int8*')->constNull(), $endPtrSlot); - $raw = // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). + // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). LibcExtern::ensureStrtolDecl($context); - $context->builder->call( + $raw = $context->builder->call( $context->lookupFunction('strtol'), $charPtr, $endPtrSlot, diff --git a/ext/standard/JitIntdiv.php b/ext/standard/JitIntdiv.php index 0ee8ca169ec..7fdda1b1ec7 100644 --- a/ext/standard/JitIntdiv.php +++ b/ext/standard/JitIntdiv.php @@ -689,9 +689,9 @@ private static function stringPtrToLong(Context $context, Value $strPtr): Value $charPtr = $context->builder->structGep($strPtr, $map['value']); $endPtrSlot = $context->builder->alloca($context->getTypeFromString('int8*'), 1, 'intdiv_strtol_end'); $context->builder->store($context->getTypeFromString('int8*')->constNull(), $endPtrSlot); - $raw = // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). + // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). LibcExtern::ensureStrtolDecl($context); - $context->builder->call( + $raw = $context->builder->call( $context->lookupFunction('strtol'), $charPtr, $endPtrSlot, diff --git a/ext/standard/JitSessionStorageKernel.php b/ext/standard/JitSessionStorageKernel.php index 50fb78b97fd..35259960ad6 100644 --- a/ext/standard/JitSessionStorageKernel.php +++ b/ext/standard/JitSessionStorageKernel.php @@ -380,9 +380,9 @@ private static function emitSessionWireLoadFromPath(Context $context, Value $pat $context->builder->positionAtEnd($bbSNum); $numStart = $context->builder->inBoundsGEP($afterPipe, $i64->constInt(2, false)); $endPtr = $context->builder->alloca($i8p); - $valLen = // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). + // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). LibcExtern::ensureStrtolDecl($context); - $context->builder->call( + $valLen = $context->builder->call( $context->lookupFunction('strtol'), $numStart, $endPtr, @@ -431,9 +431,9 @@ private static function emitSessionWireLoadFromPath(Context $context, Value $pat $context->builder->positionAtEnd($bbINum); $iStart = $context->builder->inBoundsGEP($afterPipe, $i64->constInt(2, false)); $iEndPtr = $context->builder->alloca($i8p); - $iVal = // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). + // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). LibcExtern::ensureStrtolDecl($context); - $context->builder->call( + $iVal = $context->builder->call( $context->lookupFunction('strtol'), $iStart, $iEndPtr, diff --git a/ext/standard/JitSleep.php b/ext/standard/JitSleep.php index cdf6468274f..c78d6826db6 100644 --- a/ext/standard/JitSleep.php +++ b/ext/standard/JitSleep.php @@ -368,9 +368,9 @@ private static function stringPtrToLong(Context $context, Value $strPtr): Value $charPtr = $context->builder->structGep($strPtr, $map['value']); $endPtrSlot = $context->builder->alloca($context->getTypeFromString('int8*'), 1, 'sleep_strtol_end'); $context->builder->store($context->getTypeFromString('int8*')->constNull(), $endPtrSlot); - $raw = // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). + // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). LibcExtern::ensureStrtolDecl($context); - $context->builder->call( + $raw = $context->builder->call( $context->lookupFunction('strtol'), $charPtr, $endPtrSlot, diff --git a/lib/JIT.php b/lib/JIT.php index 662d3f0e088..38f1277df1d 100644 --- a/lib/JIT.php +++ b/lib/JIT.php @@ -7985,6 +7985,15 @@ private function compileBlockInternal( if ($this->context->coalesceAssignTargets->contains($operand)) { continue; } + $slot = $block->slotForOperand($operand); + // Folded enum cases must wait until DECLARE_ENUM (classes are hoisted first, #31967). + if ( + null !== $slot + && isset($block->constants[$slot]) + && VM\Variable::TYPE_ENUM_CASE === $block->constants[$slot]->type + ) { + continue; + } $this->context->makeVariableFromOp($func, $basicBlock, $block, $operand); } $blockKey = spl_object_id($block); @@ -12322,7 +12331,15 @@ private function compileBlockInternal( $this->context->scope->classId, $nameOp->value ); + $savedEnumInsert = JIT\BasicBlockHelper::tryGetInsertBlock($this->context); $this->context->type->object->finishEnumClass($this->context->scope->classId); + if (null !== $savedEnumInsert) { + JIT\BasicBlockHelper::restoreInsertBlock($this->context, $savedEnumInsert); + } + $this->context->type->object->flushPendingEnumCaseClassConsts(strtolower($nameOp->value)); + if (null !== $savedEnumInsert) { + JIT\BasicBlockHelper::restoreInsertBlock($this->context, $savedEnumInsert); + } $this->context->popScope(); break; case OpCode::TYPE_DECLARE_CLASS: @@ -12395,6 +12412,14 @@ private function compileBlockInternal( if (AttributeClassRegistry::isRegisteredAttributeClass($op->attributeEntries)) { $this->context->type->object->markAttributeClass($nameOp->value); } + // Register implements before compileClass so `const Y = self::X` can + // resolve interface constants (zend_constants.c / #31967). + if ([] !== $op->classImplements) { + $this->context->type->object->setClassInterfaces( + $nameOp->value, + $op->classImplements + ); + } $this->compileClass($op->block1, $this->context->scope->classId); if ($parentOp instanceof Operand\Literal) { $this->context->type->object->inheritReadonlyFromParent( @@ -12410,12 +12435,6 @@ private function compileBlockInternal( strtolower(ltrim($parentOp->value, '\\')) ); } - if ([] !== $op->classImplements) { - $this->context->type->object->setClassInterfaces( - $nameOp->value, - $op->classImplements - ); - } $this->context->type->object->inheritInterfaceConstants( $this->context->scope->classId, $nameOp->value @@ -21458,7 +21477,7 @@ private function initJitStaticCall(Block $block, int $classOpIdx, int $nameOpIdx return; } - // Runtime variable classname: resolve via emitResolveClassId guards (#30059). + // Runtime `$obj::method()` / `$className::method()` — dispatch by class_id (#31967). $classVar = $this->context->getVariableFromOp($classOp); if ( JIT\Variable::TYPE_OBJECT !== $classVar->type @@ -21473,7 +21492,27 @@ private function initJitStaticCall(Block $block, int $classOpIdx, int $nameOpIdx return; } - throw new \LogicException('Static call class must be a literal'); + $methodLc = strtolower($nameOp->value); + $candidates = $this->buildRuntimeStaticMethodCandidatesByClassId($methodLc); + if ([] === $candidates) { + throw new \LogicException('Static call class must be a literal'); + } + $classIdVal = JIT\ClassConstFetchHelper::emitResolveClassId( + $this->context->type->object, + $block, + $classVar, + $classOp + ); + $this->context->scope->toCall = new JIT\Call\RuntimeIndirectStaticMethodCall( + $methodLc, + $candidates, + $block, + false, + $classIdVal + ); + $this->context->scope->args = []; + + return; } $selfScope = 'self' === strtolower((string) $classOp->value); $staticScope = 'static' === strtolower((string) $classOp->value); @@ -22913,11 +22952,23 @@ private function jitClassConstDefineValue( !isset($block->constants[$op->arg2]) || $block->constants[$op->arg2]->is(VM\Variable::TYPE_NULL) ) { - $vm = new VM($this->context->runtime->vmContext); - $className = $this->context->type->object->classNameForId($classId); - $rootBlock = $this->context->jitFunctionRootBlock ?? $this->context->jitEnclosingBlock; - VM\ClassConstMaterializer::seedReferencedClasses($vm, $rootBlock, $block, $op->arg2); - $value = VM\ClassConstMaterializer::materializeSlot($vm, $block, $op->arg2, $className); + // Resolve from JIT class/enum maps first. AOT DECLARE_* does not seed VM + // ClassEntry tables the materializer walks for `self::X` / `E::Case` (#31967). + if (null !== $this->tryResolveEnumCaseClassConstInit($block, $op->arg2)) { + $value = new VM\Variable(); + $value->null(); + } else { + $inherited = $this->tryResolveInheritedClassConstInit($block, $op->arg2, $classId); + if (null !== $inherited) { + $value = $inherited; + } else { + $vm = new VM($this->context->runtime->vmContext); + $className = $this->context->type->object->classNameForId($classId); + $rootBlock = $this->context->jitFunctionRootBlock ?? $this->context->jitEnclosingBlock; + VM\ClassConstMaterializer::seedReferencedClasses($vm, $rootBlock, $block, $op->arg2); + $value = VM\ClassConstMaterializer::materializeSlot($vm, $block, $op->arg2, $className); + } + } } else { $value = $block->constants[$op->arg2]; } @@ -23796,19 +23847,11 @@ private function ensureJitGlobal(string $name): Variable /** * Resolve `public const X = SomeEnum::Case` when VM materialization lacks the enum (#4445). * - * @return array{0: int, 1: string}|null enum class id + case key (lowercase) + * @return array{0: int, 1: string}|null enum class id + case-sensitive key (#25910) */ private function tryResolveEnumCaseClassConstInit(Block $block, int $valueSlot): ?array { - $fetchOp = null; - foreach ($block->opCodes as $initOp) { - if (OpCode::TYPE_DECLARE_CLASS_CONST === $initOp->type && $valueSlot === $initOp->arg2) { - break; - } - if (OpCode::TYPE_CLASS_CONST_FETCH === $initOp->type) { - $fetchOp = $initOp; - } - } + $fetchOp = $this->classConstFetchOpForInitSlot($block, $valueSlot); if (null === $fetchOp) { return null; } @@ -23817,12 +23860,74 @@ private function tryResolveEnumCaseClassConstInit(Block $block, int $valueSlot): if (!$enumClassOp instanceof Operand\Literal || !$caseOp instanceof Operand\Literal) { return null; } - $enumClassId = $this->context->type->object->lookup(strtolower($enumClassOp->value)); - if (!$this->context->type->object->isEnumClassId($enumClassId)) { + $enumLc = strtolower(ltrim((string) $enumClassOp->value, '\\')); + if (!$this->context->type->object->isEnumClassLc($enumLc)) { + return null; + } + $enumClassId = $this->context->type->object->lookup($enumLc); + + return [$enumClassId, \PHPCompiler\ClassConstName::key((string) $caseOp->value)]; + } + + /** + * `const Y = self::X` when X is inherited from an interface already compiled in JIT (#31967). + * + * php-src: Zend/zend_constants.c — zend_get_class_constant_ex walks interfaces. + */ + private function tryResolveInheritedClassConstInit(Block $block, int $valueSlot, int $classId): ?VM\Variable + { + $fetchOp = $this->classConstFetchOpForInitSlot($block, $valueSlot); + if (null === $fetchOp) { + return null; + } + $classOp = $block->getOperand($fetchOp->arg2); + $constOp = $block->getOperand($fetchOp->arg3); + if (!$classOp instanceof Operand\Literal || !$constOp instanceof Operand\Literal) { return null; } + $object = $this->context->type->object; + $className = (string) $classOp->value; + $lc = strtolower(ltrim($className, '\\')); + $fromId = $classId; + if ('parent' === $lc) { + $parentLc = $object->parentClassLc($object->classNameForId($classId)); + if (null === $parentLc || !$object->hasDeclaredClass($parentLc)) { + return null; + } + $fromId = $object->lookup($parentLc); + } elseif ('self' !== $lc && 'static' !== $lc) { + if (!$object->hasDeclaredClass($lc)) { + return null; + } + $fromId = $object->lookup($lc); + } + $constKey = \PHPCompiler\ClassConstName::key((string) $constOp->value); + $holdingId = $object->resolveClassConstHoldingId($fromId, $constKey); + if (null === $holdingId) { + return null; + } + + return $object->vmScalarFromClassConst($holdingId, $constKey); + } + + private function classConstFetchOpForInitSlot(Block $block, int $valueSlot): ?OpCode + { + $matched = null; + $lastFetch = null; + foreach ($block->opCodes as $initOp) { + if (OpCode::TYPE_DECLARE_CLASS_CONST === $initOp->type && $valueSlot === $initOp->arg2) { + break; + } + if (OpCode::TYPE_CLASS_CONST_FETCH !== $initOp->type) { + continue; + } + $lastFetch = $initOp; + if ($valueSlot === $initOp->arg1) { + $matched = $initOp; + } + } - return [$enumClassId, strtolower($caseOp->value)]; + return $matched ?? $lastFetch; } private function ensureJitFunctionStatic(string $storageKey): Variable diff --git a/lib/JIT/BackedEnumFromJit.php b/lib/JIT/BackedEnumFromJit.php index bb022e48834..93b6833c112 100644 --- a/lib/JIT/BackedEnumFromJit.php +++ b/lib/JIT/BackedEnumFromJit.php @@ -64,22 +64,31 @@ public static function emitFromFunction( ); $restore = $context->builder->getInsertBlock(); + $savedLowering = $context->loweringLlvmFunction; $entry = $fn->appendBasicBlock('entry'); $context->builder->positionAtEnd($entry); - $arg = $fn->getParam(0); - + // String from() uses parentFunction() for normalize blocks; if loweringLlvmFunction + // is parked on __init__/main, those blocks land in the wrong function (#31967). if ('string' === $backedType) { - self::emitStringBackedBody($context, $object, $classId, $className, $caseKeys, $arg, $isTry); - } elseif ('int' === $backedType) { - self::emitIntBackedBody($context, $object, $classId, $className, $caseKeys, $arg, $isTry); - } else { - throw new \LogicException('Unsupported enum backing type for JIT from(): '.$backedType); + $context->loweringLlvmFunction = $fn; } + $arg = $fn->getParam(0); - if (null !== $restore) { - $context->builder->positionAtEnd($restore); - } else { - $context->builder->clearInsertionPosition(); + try { + if ('string' === $backedType) { + self::emitStringBackedBody($context, $object, $classId, $className, $caseKeys, $arg, $isTry); + } elseif ('int' === $backedType) { + self::emitIntBackedBody($context, $object, $classId, $className, $caseKeys, $arg, $isTry); + } else { + throw new \LogicException('Unsupported enum backing type for JIT from(): '.$backedType); + } + } finally { + $context->loweringLlvmFunction = $savedLowering; + if (null !== $restore) { + $context->builder->positionAtEnd($restore); + } else { + $context->builder->clearInsertionPosition(); + } } } diff --git a/lib/JIT/Builtin/Type/ObjectStaticPropertyLlvm.php b/lib/JIT/Builtin/Type/ObjectStaticPropertyLlvm.php index a745301cebb..bda13a12ea5 100644 --- a/lib/JIT/Builtin/Type/ObjectStaticPropertyLlvm.php +++ b/lib/JIT/Builtin/Type/ObjectStaticPropertyLlvm.php @@ -8,6 +8,7 @@ use PHPCompiler\JIT\Builtin\ErrorRaise; use PHPCompiler\JIT\Builtin\TypeErrorRaise; use PHPCompiler\JIT\Context; +use PHPCompiler\JIT\HashTableWriteLlvm; use PHPCompiler\JIT\JitNestedHelperCoerce; use PHPCompiler\JIT\JitStringArg; use PHPCompiler\JIT\JitStringCompare; @@ -522,6 +523,25 @@ private static function storeValueBox(Object_ $object, Value $global, Variable $ return; } + if (0 !== ($value->type & Variable::IS_NATIVE_ARRAY)) { + HashTableWriteLlvm::promoteNativeArrayVariableToHashtable($context, $value); + } + + if (Variable::TYPE_NULL === $value->type && null !== $value->value) { + $llvmTy = $context->getStringFromType($value->value->typeOf()); + if (str_contains($llvmTy, '__hashtable__')) { + $value->type = Variable::TYPE_HASHTABLE; + } elseif (str_contains($llvmTy, '__value__')) { + $ptr = Variable::KIND_VARIABLE === $value->kind + ? JitValueBox::pointer($context, $value->value) + : $value->value; + $context->builder->store($ptr, $global); + $value->addref(); + + return; + } + } + $heapVal = $context->memory->malloc($valueType); $heapPtr = $context->builder->pointerCast($heapVal, $valuePtrTy); $valueMap = $context->structFieldMap['__value__']; @@ -581,10 +601,7 @@ private static function storeValueBox(Object_ $object, Value $global, Variable $ $context->helper->loadValue($value) ); } else { - throw new \LogicException( - 'JIT static property boxed store does not support value type ' - .Variable::getStringType($value->type) - ); + JitValueBox::assignToPointer($context, $heapPtr, $value); } $context->builder->store($heapPtr, $global); diff --git a/lib/JIT/Builtin/Type/Object_.php b/lib/JIT/Builtin/Type/Object_.php index 183602f2617..e3085bd7195 100755 --- a/lib/JIT/Builtin/Type/Object_.php +++ b/lib/JIT/Builtin/Type/Object_.php @@ -132,6 +132,13 @@ class Object_ extends Type { /** @var array> */ private array $classConstants = []; + /** + * `const X = E::Case` when E is compiled after the holding class (hoisted DECLARE_CLASS, #31967). + * + * @var list holdingId, constName, enumLc, caseKey + */ + private array $pendingEnumCaseClassConsts = []; + /** @var array> class id => const lc => visibility flags (#4651, #6664) */ private array $constVisibility = []; @@ -2054,8 +2061,11 @@ public function defineEnumCaseConst(int $classId, string $caseName, VMVariable $ public function jitEnumCaseFromBacking(int $classId, string $caseKey): Variable { + $caseKey = \PHPCompiler\ClassConstName::key($caseKey); if (!isset($this->classConstants[$classId][$caseKey])) { - throw new \LogicException("Unknown enum case: {$caseKey}"); + throw new \LogicException( + 'Unknown enum case: '.$this->classNameForId($classId).'::'.$caseKey + ); } $globalName = $this->ensureEnumCaseSingletonGlobal($classId, $caseKey); @@ -2069,6 +2079,20 @@ public function jitEnumCaseFromBacking(int $classId, string $caseKey): Variable return $var; } + /** + * Enum case object by name without {@see lookup()} stub registration (#31967). + */ + public function jitEnumCaseNamed(string $enumName, string $caseKey): Variable + { + $enumLc = strtolower(ltrim($enumName, '\\')); + $caseKey = \PHPCompiler\ClassConstName::key($caseKey); + if (!$this->isEnumClassLc($enumLc) || !isset($this->classes[$enumLc])) { + throw new \LogicException("Unknown enum case: {$enumName}::{$caseKey}"); + } + + return $this->jitEnumCaseFromBacking($this->classes[$enumLc], $caseKey); + } + public function allocEnumCaseSingletonIr(int $classId, string $caseName, Variable $backingJit): Variable { $objType = $this->context->getTypeFromString('__object__'); @@ -5692,6 +5716,22 @@ public function defineClassConst(int $classId, string $name, VMVariable $value): $this->classConstDisplayNames[$classId][$key] = $name; $this->classConstDeclaringLc[$classId][$key] = strtolower(ltrim($this->classNameForId($classId), '\\')); unset($this->classConstMapGlobals[$classId]); + if (EnumCaseSupport::isEnumCaseVariable($value)) { + $enumClass = EnumCaseSupport::enumClassForCaseVariable($value); + if (null === $enumClass) { + throw new \LogicException('Class constant enum case requires enum class'); + } + $caseKey = \PHPCompiler\ClassConstName::key(EnumCaseSupport::enumCaseNameForVariable($value)); + $enumLc = strtolower(ltrim($enumClass->name, '\\')); + if (!$this->isEnumClassLc($enumLc)) { + $this->queueEnumCaseClassConst($classId, $name, $enumLc, $caseKey); + + return; + } + $this->defineClassConstEnumCaseRef($classId, $name, $this->lookup($enumLc), $caseKey); + + return; + } if (VMVariable::TYPE_ARRAY === $value->type) { $table = $value->toArray(); if (!$table instanceof \PHPCompiler\VM\HashTable) { @@ -5723,6 +5763,11 @@ public function defineClassConst(int $classId, string $name, VMVariable $value): if (EnumCaseSupport::isEnumCase($object)) { $enumClassLc = strtolower($object->class->name); $caseKey = \PHPCompiler\ClassConstName::key((string) ($object->enumCaseName ?? '')); + if (!$this->isEnumClassLc($enumClassLc)) { + $this->queueEnumCaseClassConst($classId, $name, $enumClassLc, $caseKey); + + return; + } $this->defineClassConstEnumCaseRef($classId, $key, $this->lookup($enumClassLc), $caseKey); return; @@ -5796,6 +5841,46 @@ private function defineClassConstMapGlobal(int $classId): void }); } + public function queueEnumCaseClassConst( + int $holdingClassId, + string $constName, + string $enumLc, + string $caseKey + ): void { + $this->pendingEnumCaseClassConsts[] = [ + $holdingClassId, + $constName, + strtolower(ltrim($enumLc, '\\')), + \PHPCompiler\ClassConstName::key($caseKey), + ]; + } + + public function flushPendingEnumCaseClassConsts(?string $onlyEnumLc = null): void + { + if (null !== $onlyEnumLc) { + $onlyEnumLc = strtolower(ltrim($onlyEnumLc, '\\')); + } + $kept = []; + foreach ($this->pendingEnumCaseClassConsts as $item) { + [$holdingId, $constName, $enumLc, $caseKey] = $item; + if (null !== $onlyEnumLc && $enumLc !== $onlyEnumLc) { + $kept[] = $item; + continue; + } + if (!$this->isEnumClassLc($enumLc) || !isset($this->classes[$enumLc])) { + $kept[] = $item; + continue; + } + $enumId = $this->classes[$enumLc]; + if (!isset($this->classConstants[$enumId][$caseKey])) { + $kept[] = $item; + continue; + } + $this->bindEnumCaseClassConst($holdingId, $constName, $enumId, $caseKey); + } + $this->pendingEnumCaseClassConsts = $kept; + } + public function defineClassConstEnumCaseRef( int $holdingClassId, string $constName, @@ -5805,13 +5890,27 @@ public function defineClassConstEnumCaseRef( $constKey = \PHPCompiler\ClassConstName::key($constName); $this->classConstDisplayNames[$holdingClassId][$constKey] = $constName; $caseKey = \PHPCompiler\ClassConstName::key($caseKey); - if (!$this->isEnumClassId($enumClassId)) { - throw new \LogicException('Class constant enum case reference requires an enum class id'); - } - if ('' === $caseKey || !isset($this->classConstants[$enumClassId][$caseKey])) { - $enumLc = $this->classNameForId($enumClassId); - throw new \LogicException("Unknown enum case for class constant: {$enumLc}::{$caseKey}"); + if (!$this->isEnumClassId($enumClassId) || '' === $caseKey || !isset($this->classConstants[$enumClassId][$caseKey])) { + $enumLc = strtolower(ltrim($this->classNameForId($enumClassId), '\\')); + if ('' === $enumLc) { + throw new \LogicException('Class constant enum case reference requires an enum class id'); + } + $this->queueEnumCaseClassConst($holdingClassId, $constName, $enumLc, $caseKey); + + return; } + $this->bindEnumCaseClassConst($holdingClassId, $constName, $enumClassId, $caseKey); + } + + private function bindEnumCaseClassConst( + int $holdingClassId, + string $constName, + int $enumClassId, + string $caseKey + ): void { + $constKey = \PHPCompiler\ClassConstName::key($constName); + $this->classConstDisplayNames[$holdingClassId][$constKey] = $constName; + $caseKey = \PHPCompiler\ClassConstName::key($caseKey); $globalName = $this->ensureEnumCaseSingletonGlobal($enumClassId, $caseKey); $entry = [ 'type' => Variable::TYPE_OBJECT, @@ -5819,6 +5918,7 @@ public function defineClassConstEnumCaseRef( ]; $this->rejectIncompatibleTraitClassConstOverride($holdingClassId, $constKey, $constName, $entry); unset($this->traitConstSources[$holdingClassId][$constKey]); + unset($this->classConstMapGlobals[$holdingClassId]); $this->classConstants[$holdingClassId][$constKey] = $entry; } @@ -6450,11 +6550,25 @@ public function resolveClassConstHoldingId(int $classId, string $constKey): ?int } $parentLc = $this->parentClassLc($this->classNameForId($currentId)); if (null === $parentLc || !isset($this->classes[$parentLc])) { - return null; + break; } $currentId = $this->classes[$parentLc]; } + $classLc = strtolower(ltrim($this->classNameForId($classId), '\\')); + foreach ($this->allInterfacesForClassLc($classLc) as $ifaceLc) { + if ($ifaceLc === $classLc || !isset($this->classes[$ifaceLc])) { + continue; + } + $ifaceId = $this->classes[$ifaceLc]; + if (isset($this->classConstants[$ifaceId][$constKey])) { + $vis = $this->constVisibility($ifaceId, $constKey); + if (($vis & \PHPCfg\Func::FLAG_PRIVATE) === 0) { + return $ifaceId; + } + } + } + return null; } @@ -6584,6 +6698,43 @@ public function classConstantsForId(int $classId): array return $out; } + /** + * Scalar class-constant value as a VM variable for AOT const-expr lowering (#31967). + */ + public function vmScalarFromClassConst(int $classId, string $constKey): ?VMVariable + { + $key = \PHPCompiler\ClassConstName::key($constKey); + $entry = $this->classConstants[$classId][$key] ?? null; + if (null === $entry || !array_key_exists('value', $entry)) { + return null; + } + $v = new VMVariable(); + switch ($entry['type']) { + case Variable::TYPE_NATIVE_LONG: + $v->int((int) $entry['value']); + + return $v; + case Variable::TYPE_NATIVE_DOUBLE: + $v->float((float) $entry['value']); + + return $v; + case Variable::TYPE_NATIVE_BOOL: + $v->bool((bool) $entry['value']); + + return $v; + case Variable::TYPE_STRING: + $v->string((string) $entry['value']); + + return $v; + case Variable::TYPE_NULL: + $v->null(); + + return $v; + default: + return null; + } + } + public function classConstDisplayName(int $classId, string $constKey): string { $key = \PHPCompiler\ClassConstName::key($constKey); diff --git a/lib/JIT/Call/RuntimeIndirectStaticMethodCall.php b/lib/JIT/Call/RuntimeIndirectStaticMethodCall.php index c3857ef9e89..fbfd5cc7c22 100644 --- a/lib/JIT/Call/RuntimeIndirectStaticMethodCall.php +++ b/lib/JIT/Call/RuntimeIndirectStaticMethodCall.php @@ -39,6 +39,7 @@ public function __construct( public readonly array $candidatesByClassId, public readonly Block $enclosingBlock, public readonly bool $bindCallerThis = false, + public readonly ?Value $runtimeClassId = null, ) { } @@ -50,7 +51,7 @@ public function call(Context $context, Variable ...$args): Value // Same LSB class-id path as static::CONST / static::class — must not call // emitEffectiveLateStaticClassId here: ensureLinked clears the insert block (#19614). - $classId = ClassConstFetchHelper::emitStaticKeywordClassIdForPseudoConst( + $classId = $this->runtimeClassId ?? ClassConstFetchHelper::emitStaticKeywordClassIdForPseudoConst( $context->type->object, $this->enclosingBlock ); diff --git a/lib/JIT/ClassConstFetchHelperTrait.php b/lib/JIT/ClassConstFetchHelperTrait.php index 919a1dd5931..3906ae6f4cb 100644 --- a/lib/JIT/ClassConstFetchHelperTrait.php +++ b/lib/JIT/ClassConstFetchHelperTrait.php @@ -273,9 +273,12 @@ public static function emitResolveClassId( if (Variable::TYPE_OBJECT === $classVar->type) { $context = $objectType->jitContext(); $objMap = $context->structFieldMap['__object__']; + $objPtr = Variable::KIND_VARIABLE === $classVar->kind + ? $context->builder->load($classVar->value) + : $classVar->value; return $context->builder->load( - $context->builder->structGep($classVar->value, $objMap['class_id']) + $context->builder->structGep($objPtr, $objMap['class_id']) ); } // Zend: class operand must be string or object — not bool/int/float/null/array (#30059). diff --git a/lib/JIT/HashTableMergeLlvm.php b/lib/JIT/HashTableMergeLlvm.php index f7151d3cdec..da41d1d9f10 100644 --- a/lib/JIT/HashTableMergeLlvm.php +++ b/lib/JIT/HashTableMergeLlvm.php @@ -197,9 +197,9 @@ private static function mergeStringKey( ); $endPtrSlot = $context->builder->alloca($i8p, 1, 'ht_merge_sk_end_'.$tag); $context->builder->store($i8p->constNull(), $endPtrSlot); - $parsed = // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). + // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). LibcExtern::ensureStrtolDecl($context); - $context->builder->call( + $parsed = $context->builder->call( $context->lookupFunction('strtol'), $charPtr, $endPtrSlot, diff --git a/lib/JIT/HashTableWriteLlvm.php b/lib/JIT/HashTableWriteLlvm.php index 31beb23f710..497891d7969 100644 --- a/lib/JIT/HashTableWriteLlvm.php +++ b/lib/JIT/HashTableWriteLlvm.php @@ -1433,10 +1433,10 @@ private static function setAtKeyCoercingNumericStringBody( $builder->positionAtEnd($tryInt); $endPtrSlot = $builder->alloca($i8p, 1, 'arr_key_strtol_end'); $builder->store($i8p->constNull(), $endPtrSlot); + // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). + LibcExtern::ensureStrtolDecl($context); $parsed = $builder->call( - // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). - LibcExtern::ensureStrtolDecl($context); - $context->lookupFunction('strtol'), + $context->lookupFunction('strtol'), $charPtr, $endPtrSlot, $context->getTypeFromString('int32')->constInt(10, false) @@ -1520,9 +1520,9 @@ public static function spreadAddElement( $context->builder->positionAtEnd($tryInt); $endPtrSlot = $context->builder->alloca($i8p, 1, 'ht_spread_add_end'); $context->builder->store($i8p->constNull(), $endPtrSlot); - $parsed = // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). + // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). LibcExtern::ensureStrtolDecl($context); - $context->builder->call( + $parsed = $context->builder->call( $context->lookupFunction('strtol'), $charPtr, $endPtrSlot, diff --git a/lib/JIT/JitLongArg.php b/lib/JIT/JitLongArg.php index c0bdf1b4bb8..79a80d8bed0 100644 --- a/lib/JIT/JitLongArg.php +++ b/lib/JIT/JitLongArg.php @@ -165,9 +165,9 @@ private static function lowerStringToLong(Context $context, Value $strPtr, int $ $charPtr = $context->builder->structGep($strPtr, $map['value']); $endPtrSlot = $context->builder->alloca($i8p, 1, 'jit_long_arg_strtol_end'); $context->builder->store($i8p->constNull(), $endPtrSlot); - $parsed = // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). + // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). LibcExtern::ensureStrtolDecl($context); - $context->builder->call( + $parsed = $context->builder->call( $context->lookupFunction('strtol'), $charPtr, $endPtrSlot, diff --git a/lib/JIT/VmConstantJit.php b/lib/JIT/VmConstantJit.php index fd515bd42ba..06fee58ecdf 100644 --- a/lib/JIT/VmConstantJit.php +++ b/lib/JIT/VmConstantJit.php @@ -53,11 +53,20 @@ public static function toVariable(Context $context, VmVariable $vm): Variable return $nullVar; case VmVariable::TYPE_ARRAY: return self::arrayVariable($context, $vm); + case VmVariable::TYPE_ENUM_CASE: + return self::enumCaseVariable($context, $vm); default: throw new \LogicException('Unsupported compile-time constant for JIT (vm type '.$vm->type.')'); } } + private static function enumCaseVariable(Context $context, VmVariable $vm): Variable + { + $case = $vm->toEnumCase(); + + return $context->type->object->jitEnumCaseNamed($case->enumClass->name, $case->caseName); + } + private static function arrayVariable(Context $context, VmVariable $vm): Variable { $ht = $vm->toArray(); diff --git a/lib/VM/VmUnaryPlus.php b/lib/VM/VmUnaryPlus.php index fed421993ce..1ef35dbe4d8 100644 --- a/lib/VM/VmUnaryPlus.php +++ b/lib/VM/VmUnaryPlus.php @@ -154,9 +154,9 @@ private static function numericStringToLong(Context $context, Value $strPtr): Va $charPtr = $context->builder->structGep($strPtr, $map['value']); $endPtrSlot = $context->builder->alloca($i8p, 1, 'unary_plus_strtol_end'); $context->builder->store($i8p->constNull(), $endPtrSlot); - $parsed = // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). + // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). LibcExtern::ensureStrtolDecl($context); - $context->builder->call( + $parsed = $context->builder->call( $context->lookupFunction('strtol'), $charPtr, $endPtrSlot, diff --git a/lib/VM/VmValueCompare.php b/lib/VM/VmValueCompare.php index 7440ce92848..875a9a74f00 100644 --- a/lib/VM/VmValueCompare.php +++ b/lib/VM/VmValueCompare.php @@ -1008,9 +1008,9 @@ public static function looseEqualStringToNativeLong( $endPtrSlot = $context->builder->alloca($i8p, 1, 'loose_strlong_strtol_end'); $nullEnd = $i8p->constNull(); $context->builder->store($nullEnd, $endPtrSlot); - $parsed = // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). + // strtol(3) via LibcExtern::ensureStrtolDecl after always-on drop (#31988). LibcExtern::ensureStrtolDecl($context); - $context->builder->call( + $parsed = $context->builder->call( $context->lookupFunction('strtol'), $charPtr, $endPtrSlot, diff --git a/test/repro/issue_31967_enum_class_const.php b/test/repro/issue_31967_enum_class_const.php new file mode 100644 index 00000000000..bfe64a4c03c --- /dev/null +++ b/test/repro/issue_31967_enum_class_const.php @@ -0,0 +1,5 @@ +value; diff --git a/test/repro/issue_31967_interface_self_const.php b/test/repro/issue_31967_interface_self_const.php new file mode 100644 index 00000000000..add3ba8bf61 --- /dev/null +++ b/test/repro/issue_31967_interface_self_const.php @@ -0,0 +1,5 @@ + + */ + public static function reproProvider(): array + { + return [ + ['issue_31967_variable_static_call.php', 'U', '$obj::method()'], + ['issue_31967_enum_class_const.php', 'h', 'enum case class const'], + ['issue_31967_interface_self_const.php', '20', 'interface self:: const'], + ['issue_31967_static_array_store.php', '1', 'static array store'], + ]; + } + + /** + * @dataProvider reproProvider + */ + public function testAotReproMatchesZend(string $file, string $expected, string $label): void + { + if (!LlvmToolchain::hasLibrary(dirname(__DIR__, 2))) { + $this->markTestSkipped('LLVM 9 toolchain not available'); + } + $root = dirname(__DIR__, 2); + $src = $root.'/test/repro/'.$file; + $this->assertFileExists($src, $label); + $bin = sys_get_temp_dir().'/phpc_issue_31967_'.getmypid().'_'.md5($file).'.bin'; + $compile = 'env PHP_COMPILER_HELPER_RUNTIME_O=0 '.escapeshellarg(PHP_BINARY).' ' + .escapeshellarg($root.'/bin/compile.php') + .' -o '.escapeshellarg($bin).' '.escapeshellarg($src).' 2>&1'; + exec($compile, $compileOut, $compileRc); + $this->assertSame(0, $compileRc, $label.' compile: '.implode("\n", $compileOut)); + $this->assertFileExists($bin); + try { + $runOut = []; + exec(escapeshellarg($bin).' 2>&1', $runOut, $runRc); + $this->assertSame(0, $runRc, $label.' run: '.implode("\n", $runOut)); + $this->assertSame($expected, implode("\n", $runOut)); + } finally { + @unlink($bin); + } + } +}