Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions lib/JIT.php
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -7296,13 +7303,25 @@ 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,
$vmVar
);
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,
Expand Down Expand Up @@ -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);
Expand Down
182 changes: 182 additions & 0 deletions lib/JIT/Builtin/Type/Object_.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,6 +33,20 @@ class Object_ extends Type {
private array $classes = [];
/** @var array<string, true> lowercase enum name => registered (#1373, #1356) */
private array $enums = [];

/** @var array<int, list<string>> */
private array $enumCaseOrder = [];

/** @var array<int, array<string, string>> */
private array $enumCaseCanonicalNames = [];

/** @var array<int, ?string> */
private array $enumBackedType = [];

private const ENUM_CASE_SLOT_NAME = 0;

private const ENUM_CASE_SLOT_VALUE = 1;

/** @var array<int, string> class id => canonical name */
private array $classIdToName = [];
/** @var array<string, string> alias lc => canonical class lc (#3178) */
Expand Down Expand Up @@ -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<string> */
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)]);
Expand Down Expand Up @@ -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) {
Expand Down
64 changes: 64 additions & 0 deletions lib/JIT/EnumCasesHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\JIT;

use PHPCompiler\JIT\Builtin\Type\Object_ as ObjectBuiltin;
use PHPCompiler\JIT\Call\Native as NativeCall;

/**
* JIT lowering for synthetic Enum::cases() (issue #3308, #4068).
*
* php-src: Zend/zend_enum.c — zend_enum_list_cases
*/
final class EnumCasesHelper
{
public static function registerCasesMethod(Context $context, ObjectBuiltin $object, int $classId): void
{
$classLc = strtolower(ltrim($object->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();
}
}
}
2 changes: 1 addition & 1 deletion script/capability-syntax-lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();',
Expand Down
51 changes: 51 additions & 0 deletions test/unit/EnumCasesJitCompileTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace PHPCompiler;

use PHPUnit\Framework\TestCase;

require_once __DIR__.'/../LlvmToolchain.php';

/**
* LLVM compile-only verify for Enum::cases() JIT lowering (#4068, #3308).
*
* php-src: Zend/zend_enum.c — zend_enum_list_cases
*
* @group llvm
*/
final class EnumCasesJitCompileTest extends TestCase
{
private string $repoRoot;

protected function setUp(): void
{
$this->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'
<?php
enum Suit: string { case Hearts = 'H'; case Spades = 'S'; }
$cases = Suit::cases();
echo count($cases), ':', $cases[0]->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);
}
}