diff --git a/lib/JIT.php b/lib/JIT.php index 974fef63466..3da28e5578e 100644 --- a/lib/JIT.php +++ b/lib/JIT.php @@ -6157,10 +6157,17 @@ private function compileBlockInternal( $this->context->pushScope(); $this->context->scope->classId = $this->context->type->object->declareEnum($nameOp); $this->context->scope->className = strtolower($nameOp->value); + if (null !== $op->arg2 && isset($block->constants[$op->arg2])) { + $this->context->type->object->setEnumBackedType( + $this->context->scope->classId, + $block->constants[$op->arg2]->toString() + ); + } if (null !== $this->context->runtime->vmContext) { $this->context->runtime->vmContext->enums[strtolower($nameOp->value)] = true; } $this->compileClass($op->block1, $this->context->scope->classId); + $this->context->type->object->finishEnumClass($this->context->scope->classId); $this->context->popScope(); break; case OpCode::TYPE_DECLARE_CLASS: @@ -7296,6 +7303,10 @@ private function compileClass(?Block $block, int $classId) { } $vm = new VM($this->context->runtime->vmContext); $vmVar = VM\ClassConstMaterializer::materializeSlot($vm, $block, $op->arg2); + if ($this->context->type->object->isEnumClassId($classId)) { + $this->context->type->object->defineEnumCaseConst($classId, $name->value, $vmVar); + break; + } $this->context->type->object->defineClassConst( $classId, $name->value, @@ -7303,6 +7314,14 @@ private function compileClass(?Block $block, int $classId) { ); break; } + if ($this->context->type->object->isEnumClassId($classId)) { + $this->context->type->object->defineEnumCaseConst( + $classId, + $name->value, + $block->constants[$op->arg2] + ); + break; + } $this->context->type->object->defineClassConst( $classId, $name->value, @@ -9298,6 +9317,11 @@ private function initJitStaticCall(Block $block, int $classOpIdx, int $nameOpIdx $parentScopeAllows ); $proxyName = $this->resolveJitStaticMethodProxyName($declaringClassLc, $methodLc); + if (!$this->context->functionIsRegistered($proxyName)) { + if ($this->context->type->object->isEnumClassLc($declaringClassLc) && 'cases' === $methodLc) { + $this->context->type->object->finishEnumClass($declaringClassId); + } + } if (!$this->context->functionIsRegistered($proxyName)) { if ($this->context->type->object->isExternalOnlyClass($declaringClassId)) { $this->context->scope->toCall = $this->context->resolveFunctionProxy($proxyName); diff --git a/lib/JIT/Builtin/Type/Object_.php b/lib/JIT/Builtin/Type/Object_.php index 926e9d62dcb..4de741ceb14 100755 --- a/lib/JIT/Builtin/Type/Object_.php +++ b/lib/JIT/Builtin/Type/Object_.php @@ -15,6 +15,7 @@ use PHPCompiler\JIT\BasicBlockHelper; use PHPCompiler\JIT\ClassConstFetchHelper; use PHPCompiler\JIT\Context; +use PHPCompiler\JIT\EnumCasesHelper; use PHPCompiler\JIT\FiberHelper; use PHPCompiler\JIT\GeneratorHelper; use PHPCompiler\JIT\Builtin\Refcount; @@ -32,6 +33,20 @@ class Object_ extends Type { private array $classes = []; /** @var array lowercase enum name => registered (#1373, #1356) */ private array $enums = []; + + /** @var array> */ + private array $enumCaseOrder = []; + + /** @var array> */ + private array $enumCaseCanonicalNames = []; + + /** @var array */ + private array $enumBackedType = []; + + private const ENUM_CASE_SLOT_NAME = 0; + + private const ENUM_CASE_SLOT_VALUE = 1; + /** @var array class id => canonical name */ private array $classIdToName = []; /** @var array alias lc => canonical class lc (#3178) */ @@ -1058,6 +1073,130 @@ public function declareEnum(Operand $name): int return $this->declareClass($name); } + public function setEnumBackedType(int $classId, ?string $backedType): void + { + $this->enumBackedType[$classId] = $backedType; + } + + public function isEnumClassId(int $classId): bool + { + $lc = $this->classLcForId($classId); + + return null !== $lc && isset($this->enums[$lc]); + } + + public function isEnumClassLc(string $classLc): bool + { + return isset($this->enums[strtolower(ltrim($classLc, '\\'))]); + } + + /** @return list */ + public function enumCaseOrderForClass(int $classId): array + { + return $this->enumCaseOrder[$classId] ?? []; + } + + public function enumCaseCanonicalName(int $classId, string $caseKey): string + { + return $this->enumCaseCanonicalNames[$classId][$caseKey] ?? $caseKey; + } + + public function finishEnumClass(int $classId): void + { + if ($this->isEnumClassId($classId)) { + EnumCasesHelper::registerCasesMethod($this->context, $this, $classId); + } + } + + public function defineEnumCaseConst(int $classId, string $caseName, VMVariable $backing): void + { + if (!$this->isEnumClassId($classId)) { + throw new \LogicException('defineEnumCaseConst requires an enum class id'); + } + $key = strtolower($caseName); + $this->enumCaseOrder[$classId][] = $key; + $this->enumCaseCanonicalNames[$classId][$key] = $caseName; + $this->classConstants[$classId][$key] = [ + 'type' => Variable::fromVMVariable($backing->type), + 'value' => $this->compileTimeValueFromVm($backing), + ]; + } + + public function jitEnumCaseFromBacking(int $classId, string $caseKey): Variable + { + if (!isset($this->classConstants[$classId][$caseKey])) { + throw new \LogicException("Unknown enum case: {$caseKey}"); + } + + return $this->allocEnumCaseSingletonIr( + $classId, + $this->enumCaseCanonicalName($classId, $caseKey), + $this->jitConstantFromEntry($this->classConstants[$classId][$caseKey]) + ); + } + + public function allocEnumCaseSingletonIr(int $classId, string $caseName, Variable $backingJit): Variable + { + $objType = $this->context->getTypeFromString('__object__'); + $obj = $this->context->memory->mallocWithExtra( + $objType, + $this->context->constantFromInteger(16, 'size_t') + ); + $map = $this->context->structFieldMap['__object__']; + $this->context->builder->store( + $this->context->constantFromInteger($classId, 'int64'), + $this->context->builder->structGep($obj, $map['class_id']) + ); + $this->context->builder->store( + $this->context->getTypeFromString('int8')->constInt(1, false), + $this->context->builder->structGep($obj, $map['constructed']) + ); + $typeinfo = $this->context->getTypeFromString('int32')->constInt( + Refcount::TYPE_INFO_TYPE_OBJECT | Refcount::TYPE_INFO_REFCOUNTED, + false + ); + $ref = $this->context->builder->pointerCast( + $obj, + $this->context->getTypeFromString('__ref__virtual*') + ); + $this->context->builder->call( + $this->context->lookupFunction('__ref__init'), + $typeinfo, + $ref + ); + $this->context->builder->call( + $this->context->lookupFunction('__ref__addref'), + $ref + ); + $voidPtr = $this->context->getTypeFromString('void*'); + $nameStr = $this->context->builder->load( + $this->context->constantStringFromString($caseName) + ); + $this->context->builder->store( + $this->context->builder->pointerCast($nameStr, $voidPtr), + $this->propertySlotPtr($obj, self::ENUM_CASE_SLOT_NAME) + ); + $this->propertyStore( + $this->propertySlotPtr($obj, self::ENUM_CASE_SLOT_VALUE), + $backingJit, + Variable::TYPE_VALUE + ); + \PHPCompiler\JIT\Builtin\GcCollectCyclesNative::registerDeclarations($this->context); + \PHPCompiler\JIT\Builtin\GcCollectCyclesRuntime::ensureLinked($this->context); + $this->context->builder->call( + $this->context->lookupFunction('phpc_gc_register'), + $this->context->builder->pointerCast($obj, $this->context->getTypeFromString('int8*')), + $this->context->constantFromInteger(0, 'int32') + ); + + return new Variable( + $this->context, + Variable::TYPE_OBJECT, + Variable::KIND_VALUE, + $obj + ); + } + public function hasDeclaredClass(string $name): bool { return isset($this->classes[strtolower($name)]); @@ -2796,10 +2935,53 @@ public function propertySlotFor(PHPLLVM\Value $obj, string $class, string $name) throw new \LogicException('Property slot not found: '.$class.'::$'.$name); } + private function enumCasePropertyFetch(PHPLLVM\Value $obj, int $classId, string $nameLc): Variable + { + if ('value' === $nameLc && null === ($this->enumBackedType[$classId] ?? null)) { + \PHPCompiler\JIT\Builtin\ErrorRaise::emitRaise( + $this->context, + 'Attempt to read property "value" on unit enum case '.$this->classNameForId($classId) + ); + } + $slot = $this->propertySlotPtr( + $obj, + 'name' === $nameLc ? self::ENUM_CASE_SLOT_NAME : self::ENUM_CASE_SLOT_VALUE + ); + $loaded = $this->context->builder->load($slot); + if ('name' === $nameLc) { + return new Variable( + $this->context, + Variable::TYPE_STRING, + Variable::KIND_VALUE, + $this->context->builder->pointerCast( + $loaded, + $this->context->getTypeFromString('__string__*') + ) + ); + } + $storage = BasicBlockHelper::entryAlloca($this->context, $this->context->getTypeFromString('__value__')); + $valueMap = $this->context->structFieldMap['__value__']; + $this->context->builder->store( + $this->context->getTypeFromString('int8')->constInt(Variable::TYPE_NULL, false), + $this->context->builder->structGep($storage, $valueMap['type']) + ); + $this->context->builder->call( + $this->context->lookupFunction('__object__load_value_slot'), + $slot, + $storage + ); + + return new Variable($this->context, Variable::TYPE_VALUE, Variable::KIND_VARIABLE, $storage); + } + public function propertyFetch(PHPLLVM\Value $obj, string $class, string $name): Variable { $classId = $this->lookup('' !== $class ? $class : 'stdclass'); $className = $this->classNameForId($classId); + $nameLc = strtolower($name); + if ($this->isEnumClassId($classId) && ('name' === $nameLc || 'value' === $nameLc)) { + return $this->enumCasePropertyFetch($obj, $classId, $nameLc); + } $nameId = $this->propNameMap[$name] ?? null; $hasProp = false; if (null !== $nameId) { diff --git a/lib/JIT/EnumCasesHelper.php b/lib/JIT/EnumCasesHelper.php new file mode 100644 index 00000000000..b88eab92772 --- /dev/null +++ b/lib/JIT/EnumCasesHelper.php @@ -0,0 +1,64 @@ +classNameForId($classId), '\\')); + $funcName = $classLc.'::cases'; + if ($context->functionIsRegistered($funcName)) { + return; + } + $caseKeys = $object->enumCaseOrderForClass($classId); + if ([] === $caseKeys) { + return; + } + + $void = $context->getTypeFromString('void'); + $valuePtr = $context->getTypeFromString('__value__*'); + $fnType = $context->context->functionType($valuePtr, false); + $fn = $context->module->addFunction($funcName, $fnType); + $lc = strtolower($funcName); + $context->functions[$lc] = $fn; + $context->functionProxies[$lc] = new NativeCall($fn, $funcName, []); + + $restore = $context->builder->getInsertBlock(); + $entry = $fn->appendBasicBlock('entry'); + $context->builder->positionAtEnd($entry); + $ht = HashTableHelper::alloc($context); + foreach ($caseKeys as $index => $caseKey) { + $caseObj = $object->jitEnumCaseFromBacking($classId, $caseKey); + HashTableHelper::setAtIndex( + $context, + $ht, + $context->getTypeFromString('int64')->constInt($index, false), + $caseObj + ); + } + $slot = JitValueBox::alloc($context); + $ptr = JitValueBox::pointer($context, $slot); + $context->builder->call( + $context->lookupFunction('__value__writeHashtable'), + $ptr, + $ht + ); + $context->builder->returnValue($ptr); + if (null !== $restore) { + $context->builder->positionAtEnd($restore); + } else { + $context->builder->clearInsertionPosition(); + } + } +} diff --git a/script/capability-syntax-lib.php b/script/capability-syntax-lib.php index 828d67bba42..673e89c190e 100644 --- a/script/capability-syntax-lib.php +++ b/script/capability-syntax-lib.php @@ -51,7 +51,7 @@ function syntaxRowDefinitions(): array 'notes' => [ 'Backed enum case objects with `->name` / `->value`; string context coerces to backed scalar (#3518)', '`Foo::Bar` singleton fetch; `enum_exists` registry; `implements` interface list + instance methods + `instanceof` (#3373)', - 'static methods (#2299); `Enum::cases()` VM (#3308); AOT fixture enum_backed.phpt (#3076)', + 'static methods (#2299); `Enum::cases()` JIT (#3308, #4068); AOT fixture enum_backed.phpt (#3076)', '`BackedEnum::from()` / `tryFrom()` VM lookup with Zend-parity ValueError (#3114); JIT deferred', ], 'probe' => 'interface L { public function n(): string; } enum S: string implements L { case A = "a"; public function n(): string { return $this->name; } } echo S::A->n();', diff --git a/test/unit/EnumCasesJitCompileTest.php b/test/unit/EnumCasesJitCompileTest.php new file mode 100644 index 00000000000..186d400fbcc --- /dev/null +++ b/test/unit/EnumCasesJitCompileTest.php @@ -0,0 +1,51 @@ +repoRoot = dirname(__DIR__, 2); + if (!LlvmToolchain::isReady($this->repoRoot)) { + $reason = LlvmToolchain::readyFailureReason() ?? 'LLVM 9 toolchain not available'; + $this->markTestSkipped($reason.' — Enum::cases() JIT compile test needs LLVM (#4068)'); + } + } + + public function testEnumCasesModuleVerify(): void + { + $code = <<<'PHP' +name, "\n"; +PHP; + $runtime = new Runtime(); + $block = $runtime->parseAndCompile($code, 'enum_cases_jit_compile.php'); + $runtime->jitCompileBlock($block); + + $context = $runtime->loadJitContext(); + $bc = $context->module->printToString(); + $this->assertStringContainsString('suit::cases', $bc); + $verify = new \ReflectionMethod($context, 'compileCommon'); + $verify->setAccessible(true); + $verify->invoke($context); + $this->addToAssertionCount(1); + } +}