diff --git a/lib/JIT/Builtin/Type/ObjectDestructorLlvm.php b/lib/JIT/Builtin/Type/ObjectDestructorLlvm.php new file mode 100644 index 00000000000..52833abdb97 --- /dev/null +++ b/lib/JIT/Builtin/Type/ObjectDestructorLlvm.php @@ -0,0 +1,146 @@ +jitContext(); + $objPtr = $context->getTypeFromString('__object__*'); + $void = $context->getTypeFromString('void'); + $fnType = $context->context->functionType($void, false, $objPtr); + $fn = $context->module->getNamedFunction('__object__invoke_destructor'); + if (null !== $fn && $fn->countBasicBlocks() > 0) { + return; + } + if (null === $fn) { + $fn = $context->module->addFunction('__object__invoke_destructor', $fnType); + $context->registerFunction('__object__invoke_destructor', $fn); + } + $entry = $fn->appendBasicBlock('entry'); + $context->builder->positionAtEnd($entry); + $obj = $fn->getParam(0); + $classIds = $object->classIdsWithDestructor(); + if ([] === $classIds) { + $context->builder->returnVoid(); + $context->builder->clearInsertionPosition(); + + return; + } + $constructed = $context->builder->load( + $context->builder->structGep($obj, $context->structFieldMap['__object__']['constructed']) + ); + $notReady = $fn->appendBasicBlock('destruct_not_constructed'); + $ready = $fn->appendBasicBlock('destruct_ready'); + $done = $fn->appendBasicBlock('destruct_done'); + $isReady = $context->builder->icmp( + PHPLLVM\Builder::INT_NE, + $constructed, + $context->getTypeFromString('int8')->constInt(0, false) + ); + $context->builder->branchIf($isReady, $ready, $notReady); + $context->builder->positionAtEnd($notReady); + $context->builder->branch($done); + $context->builder->positionAtEnd($ready); + self::emitDestructDispatchForObject($object, $fn, $obj, $classIds, $done); + $context->builder->positionAtEnd($done); + $context->builder->returnVoid(); + $context->builder->clearInsertionPosition(); + } + + /** + * @param list $classIds + */ + public static function emitDestructDispatchForObject( + Object_ $object, + Value\Function_ $fn, + Value $obj, + array $classIds, + PHPLLVM\BasicBlock $outerDone + ): void { + $context = $object->jitContext(); + $objMap = $context->structFieldMap['__object__']; + $classIdVal = $context->builder->load( + $context->builder->structGep($obj, $objMap['class_id']) + ); + if (1 === \count($classIds)) { + $onlyId = $classIds[0]; + $callBlock = $fn->appendBasicBlock('destruct_magic_single_call'); + $skipBlock = $fn->appendBasicBlock('destruct_magic_single_skip'); + $expected = $context->constantFromInteger($onlyId, 'int64'); + $isId = $context->builder->icmp(PHPLLVM\Builder::INT_EQ, $classIdVal, $expected); + $context->builder->branchIf($isId, $callBlock, $skipBlock); + $context->builder->positionAtEnd($callBlock); + self::emitDestructMagicCallForClass($object, $obj, $onlyId); + $context->builder->branch($outerDone); + $context->builder->positionAtEnd($skipBlock); + $context->builder->branch($outerDone); + + return; + } + $done = $fn->appendBasicBlock('destruct_magic_done'); + $fallback = $fn->appendBasicBlock('destruct_magic_unknown'); + $caseBlocks = []; + foreach ($classIds as $id) { + $caseBlocks[] = $fn->appendBasicBlock('destruct_magic_class_'.$id); + } + $checkBlock = $context->builder->getInsertBlock(); + foreach ($classIds as $i => $id) { + $context->builder->positionAtEnd($checkBlock); + $expected = $context->constantFromInteger($id, 'int64'); + $isId = $context->builder->icmp(PHPLLVM\Builder::INT_EQ, $classIdVal, $expected); + $nextCheck = $i + 1 < \count($classIds) + ? $fn->appendBasicBlock('destruct_magic_try_'.($i + 1)) + : $fallback; + $context->builder->branchIf($isId, $caseBlocks[$i], $nextCheck); + $context->builder->positionAtEnd($caseBlocks[$i]); + self::emitDestructMagicCallForClass($object, $obj, $id); + $context->builder->branch($done); + $checkBlock = $nextCheck; + } + $context->builder->positionAtEnd($fallback); + $context->builder->branch($done); + $context->builder->positionAtEnd($done); + $context->builder->branch($outerDone); + } + + public static function emitDestructMagicCallForClass(Object_ $object, Value $obj, int $classId): void + { + $context = $object->jitContext(); + $className = $object->classNameForId($classId); + $proxyName = strtolower($className).'::'.'__destruct'; + if (!$context->functionIsRegistered($proxyName)) { + return; + } + $refVirtual = $context->builder->pointerCast( + $obj, + $context->getTypeFromString('__ref__virtual*') + ); + $context->refcount->addref($refVirtual); + $context->builder->call( + $context->lookupFunction('phpc_destruct_set_allow_delref'), + $context->getTypeFromString('int32')->constInt(0, false) + ); + $objVar = new Variable( + $context, + Variable::TYPE_OBJECT, + Variable::KIND_VALUE, + $obj + ); + $toCall = $context->resolveFunctionProxy($proxyName); + $prevStrict = $context->callerStrictTypes; + $context->callerStrictTypes = false; + $toCall->call($context, $objVar); + $context->callerStrictTypes = $prevStrict; + } +} diff --git a/lib/JIT/Builtin/Type/ObjectInstancePropertyLlvm.php b/lib/JIT/Builtin/Type/ObjectInstancePropertyLlvm.php new file mode 100644 index 00000000000..fc5d04367d6 --- /dev/null +++ b/lib/JIT/Builtin/Type/ObjectInstancePropertyLlvm.php @@ -0,0 +1,200 @@ +jitContext(); + $className = $object->classNameForId($classId); + $nameId = $object->propNameIdFor($name); + $hasProp = false; + if (null !== $nameId) { + foreach ($object->propertySetsForClass($classId) as $propset) { + if ($propset[0] === $nameId) { + $hasProp = true; + break; + } + } + } + if (!$hasProp) { + $object->defineProperty($classId, $name, $object->externalPropertyJitType($class, $name)); + $nameId = $object->propNameIdAfterDefine($name); + } + foreach ($object->propertySetsForClass($classId) as $propset) { + if ($propset[0] === $nameId) { + $slot = $object->propertySlotPtr($obj, $propset[3]); + $loaded = $context->builder->load($slot); + if (Variable::TYPE_VALUE === $propset[2]) { + $valueType = $context->getTypeFromString('__value__'); + $storage = BasicBlockHelper::entryAlloca($context, $valueType); + $valueMap = $context->structFieldMap['__value__']; + $context->builder->store( + $context->getTypeFromString('int8')->constInt(Variable::TYPE_NULL, false), + $context->builder->structGep($storage, $valueMap['type']) + ); + $context->builder->call( + $context->lookupFunction('__object__load_value_slot'), + $slot, + $storage + ); + $var = new Variable( + $context, + $propset[2], + Variable::KIND_VARIABLE, + $storage, + ); + $var->objectPropertySlot = $slot; + $var->objectPropertyType = $propset[2]; + $var->objectPropertyReceiver = $obj; + $var->objectPropertyName = $propset[1]; + $var->objectPropertyClassName = $className; + $var->objectPropertyDnfArms = $object->dnfArmsForProperty($classId, $propset[1]); + $object->recordSlotReceiver($slot, $obj); + + return $var; + } + if (Variable::TYPE_HASHTABLE === $propset[2]) { + $htPtr = $context->builder->pointerCast( + $loaded, + $context->getTypeFromString('__hashtable__*') + ); + $var = new Variable( + $context, + Variable::TYPE_HASHTABLE, + Variable::KIND_VALUE, + $htPtr + ); + $var->objectPropertySlot = $slot; + $var->objectPropertyType = $propset[2]; + $var->objectPropertyReceiver = $obj; + $var->objectPropertyName = $propset[1]; + $var->objectPropertyClassName = $className; + $var->objectPropertyDnfArms = $object->dnfArmsForProperty($classId, $propset[1]); + $object->recordSlotReceiver($slot, $obj); + + return $var; + } + $llvmType = Variable::getStringType($propset[2]); + $typed = $context->builder->pointerCast( + $loaded, + $context->getTypeFromString($llvmType) + ); + $var = new Variable( + $context, + $propset[2], + Variable::KIND_VALUE, + $typed, + ); + $var->objectPropertySlot = $slot; + $var->objectPropertyType = $propset[2]; + $var->objectPropertyReceiver = $obj; + $var->objectPropertyName = $propset[1]; + $var->objectPropertyClassName = $className; + $var->objectPropertyDnfArms = $object->dnfArmsForProperty($classId, $propset[1]); + $object->recordSlotReceiver($slot, $obj); + + return $var; + } + } + throw new \LogicException("Could not find property $name for class $classId"); + } + + public static function boxFetchedPropertyIntoValue( + Object_ $object, + Value $destSlot, + Variable $fetched, + int $propertyType + ): void { + $context = $object->jitContext(); + $destPtr = JitValueBox::pointer($context, $destSlot); + if (Variable::TYPE_VALUE === $propertyType) { + $context->builder->call( + $context->lookupFunction('__object__load_value_slot'), + $fetched->objectPropertySlot, + $destSlot + ); + + return; + } + if (Variable::TYPE_HASHTABLE === $propertyType) { + $htPtr = $context->builder->pointerCast( + $context->builder->load($fetched->objectPropertySlot), + $context->getTypeFromString('__hashtable__*') + ); + $context->builder->call( + $context->lookupFunction('__value__writeHashtable'), + $destPtr, + $htPtr + ); + + return; + } + if (Variable::TYPE_NATIVE_LONG === $propertyType) { + $context->builder->call( + $context->lookupFunction('__value__writeLong'), + $destPtr, + $context->builder->load($fetched->value) + ); + + return; + } + if (Variable::TYPE_NATIVE_BOOL === $propertyType) { + JitValueBox::writeBool( + $context, + $destSlot, + $context->builder->load($fetched->value) + ); + + return; + } + if (Variable::TYPE_NATIVE_DOUBLE === $propertyType) { + $context->builder->call( + $context->lookupFunction('__value__writeDouble'), + $destPtr, + $context->builder->load($fetched->value) + ); + + return; + } + if (Variable::TYPE_STRING === $propertyType) { + $context->builder->call( + $context->lookupFunction('__value__writeString'), + $destPtr, + $context->builder->load($fetched->value) + ); + + return; + } + if (Variable::TYPE_OBJECT === $propertyType) { + $context->builder->call( + $context->lookupFunction('__value__writeObject'), + $destPtr, + $context->builder->load($fetched->value) + ); + + return; + } + + throw new \LogicException( + 'Dynamic property fetch JIT box unsupported type: '.Variable::getStringType($propertyType) + ); + } +} diff --git a/lib/JIT/Builtin/Type/ObjectStaticPropertyInitLlvm.php b/lib/JIT/Builtin/Type/ObjectStaticPropertyInitLlvm.php new file mode 100644 index 00000000000..77de60df125 --- /dev/null +++ b/lib/JIT/Builtin/Type/ObjectStaticPropertyInitLlvm.php @@ -0,0 +1,229 @@ +jitContext(); + if (Variable::TYPE_NATIVE_DOUBLE === $jitType) { + $llvmType = $context->getTypeFromString('double'); + $float = null !== $value && VMVariable::TYPE_FLOAT === $value->type ? $value->toFloat() : 0.0; + + return $llvmType->constReal($float); + } + $llvmType = $context->getTypeFromString( + Variable::TYPE_NATIVE_BOOL === $jitType ? 'int1' : 'int64' + ); + $int = 0; + if (null !== $value) { + $int = match ($value->type) { + VMVariable::TYPE_INTEGER => $value->toInt(), + VMVariable::TYPE_BOOLEAN => $value->toBool() ? 1 : 0, + default => 0, + }; + } + + return $llvmType->constInt($int, false); + } + + public static function initStringDefault(Object_ $object, Value $global, VMVariable $value): void + { + if (VMVariable::TYPE_STRING !== $value->type) { + throw new \LogicException('Static string property default must be a string'); + } + $context = $object->jitContext(); + $restore = $context->builder->getInsertBlock(); + $context->positionBuilderAtInitEmission(); + $str = $context->builder->load( + $context->constantStringFromString($value->toString()) + ); + $owned = $context->builder->call( + $context->lookupFunction('__string__separate'), + $str + ); + $context->builder->store($owned, $global); + if (null !== $restore) { + BasicBlockHelper::restoreInsertBlock($context, $restore); + } + } + + /** Allocate a null {@see __value__} box for untyped static properties (bootstrap JIT helpers). */ + public static function initValueNull(Object_ $object, Value $global): void + { + $context = $object->jitContext(); + $restore = $context->builder->getInsertBlock(); + $context->positionBuilderAtInitEmission(); + $valueType = $context->getTypeFromString('__value__'); + $heapVal = $context->memory->malloc($valueType); + $heapPtr = $context->builder->pointerCast( + $heapVal, + $context->getTypeFromString('__value__*') + ); + $context->builder->call( + $context->lookupFunction('__value__writeNull'), + $heapPtr + ); + $context->builder->store($heapPtr, $global); + if (null !== $restore) { + BasicBlockHelper::restoreInsertBlock($context, $restore); + } + } + + /** Initialize a typed static array property with an empty hashtable (#8716). */ + public static function initHashtableEmpty(Object_ $object, Value $global): void + { + $context = $object->jitContext(); + $restore = $context->builder->getInsertBlock(); + $context->positionBuilderAtInitEmission(); + $ht = HashTableHelper::alloc($context); + $context->builder->store($ht, $global); + if (null !== $restore) { + BasicBlockHelper::restoreInsertBlock($context, $restore); + } + } + + /** + * WeakRef registry slot tables allocate on first write — eager __init__ hashtable alloc + * runs before runtime tables are ready and segfaults HelloWorld AOT (#11437). + */ + public static function deferHashtableInitInAot(Object_ $object, int $classId): bool + { + $context = $object->jitContext(); + if (\PHPCompiler\JIT\Builtin::LOAD_TYPE_STANDALONE !== $context->loadType) { + return false; + } + $classLc = strtolower(ltrim($object->classNameForId($classId), '\\')); + + return 'phpcompiler\\ext\\standard\\weakrefregistryjithelper' === $classLc; + } + + /** Box an empty compile-time array default into a union/DNF static {@see __value__} property (#8708, #8719, DomRegistry::$states #6140). */ + public static function initValueEmptyArray(Object_ $object, Value $global): void + { + $context = $object->jitContext(); + $restore = $context->builder->getInsertBlock(); + $context->positionBuilderAtInitEmission(); + $valueType = $context->getTypeFromString('__value__'); + $heapVal = $context->memory->malloc($valueType); + $heapPtr = $context->builder->pointerCast( + $heapVal, + $context->getTypeFromString('__value__*') + ); + $ht = HashTableHelper::alloc($context); + $context->builder->call( + $context->lookupFunction('__value__writeHashtable'), + $heapPtr, + $ht + ); + $context->builder->store($heapPtr, $global); + if (null !== $restore) { + BasicBlockHelper::restoreInsertBlock($context, $restore); + } + } + + /** Box a compile-time scalar default into a union/DNF static {@see __value__} property (#8726). */ + public static function initValueScalarDefault(Object_ $object, Value $global, VMVariable $default): void + { + $context = $object->jitContext(); + $restore = $context->builder->getInsertBlock(); + $context->positionBuilderAtInitEmission(); + $valueType = $context->getTypeFromString('__value__'); + $heapVal = $context->memory->malloc($valueType); + $heapPtr = $context->builder->pointerCast( + $heapVal, + $context->getTypeFromString('__value__*') + ); + $valueMap = $context->structFieldMap['__value__']; + $context->builder->store( + $context->getTypeFromString('int8')->constInt($default->type, false), + $context->builder->structGep($heapVal, $valueMap['type']) + ); + if (VMVariable::TYPE_STRING === $default->type) { + $str = $context->builder->load( + $context->constantStringFromString($default->toString()) + ); + $owned = $context->builder->call( + $context->lookupFunction('__string__separate'), + $str + ); + $context->builder->call( + $context->lookupFunction('__value__writeString'), + $heapPtr, + $owned + ); + } elseif (VMVariable::TYPE_INTEGER === $default->type) { + $context->builder->call( + $context->lookupFunction('__value__writeLong'), + $heapPtr, + $context->getTypeFromString('int64')->constInt($default->toInt(), false) + ); + } elseif (VMVariable::TYPE_FLOAT === $default->type) { + $context->builder->call( + $context->lookupFunction('__value__writeDouble'), + $heapPtr, + $context->getTypeFromString('double')->constReal($default->toFloat()) + ); + } elseif (VMVariable::TYPE_BOOLEAN === $default->type) { + $context->builder->call( + $context->lookupFunction('__value__writeLong'), + $heapPtr, + $context->getTypeFromString('int64')->constInt($default->toBool() ? 1 : 0, false) + ); + } else { + throw new \LogicException( + 'Static union/DNF property default must be a scalar compile-time constant' + ); + } + $context->builder->store($heapPtr, $global); + if (null !== $restore) { + BasicBlockHelper::restoreInsertBlock($context, $restore); + } + } + + /** Box a compile-time enum case singleton into a typed static {@see __value__} property (#5891). */ + public static function initValueEnumCase(Object_ $object, Value $global, VMVariable $default): void + { + $enumClass = EnumCaseSupport::enumClassForCaseVariable($default); + if (null === $enumClass) { + throw new \LogicException('Static enum case property default requires enum class'); + } + $context = $object->jitContext(); + $enumClassId = $object->lookup(strtolower($enumClass->name)); + $caseKey = strtolower(EnumCaseSupport::enumCaseNameForVariable($default)); + $globalName = $object->ensureEnumCaseSingletonGlobal($enumClassId, $caseKey); + $context->emitInInit(function (Context $ctx) use ($global, $globalName): void { + $objGlobal = $ctx->module->getNamedGlobal($globalName); + if (null === $objGlobal) { + throw new \LogicException("Missing enum case singleton global: {$globalName}"); + } + $valueType = $ctx->getTypeFromString('__value__'); + $heapVal = $ctx->memory->malloc($valueType); + $heapPtr = $ctx->builder->pointerCast( + $heapVal, + $ctx->getTypeFromString('__value__*') + ); + $obj = $ctx->builder->load($objGlobal); + $ctx->builder->call( + $ctx->lookupFunction('__value__writeObject'), + $heapPtr, + $obj + ); + $ctx->builder->store($heapPtr, $global); + }); + } +} diff --git a/lib/JIT/Builtin/Type/Object_.php b/lib/JIT/Builtin/Type/Object_.php index 0ec806e8180..ac5286c2c20 100755 --- a/lib/JIT/Builtin/Type/Object_.php +++ b/lib/JIT/Builtin/Type/Object_.php @@ -244,7 +244,7 @@ public function implement(): void public function shutdown(): void { - $this->implementInvokeDestructor(); + ObjectDestructorLlvm::implementInvokeDestructor($this); } /** Emit shutdown destructor pass into the current IR insertion point (#4013). */ @@ -272,7 +272,7 @@ public function hasUserDestructors(): bool /** * @return list */ - private function classIdsWithDestructor(): array + public function classIdsWithDestructor(): array { $ids = []; foreach ($this->methodVisibility as $classId => $methods) { @@ -284,133 +284,6 @@ private function classIdsWithDestructor(): array return $ids; } - private function implementInvokeDestructor(): void - { - $objPtr = $this->context->getTypeFromString('__object__*'); - $void = $this->context->getTypeFromString('void'); - $fnType = $this->context->context->functionType($void, false, $objPtr); - $fn = $this->context->module->getNamedFunction('__object__invoke_destructor'); - if (null !== $fn && $fn->countBasicBlocks() > 0) { - return; - } - if (null === $fn) { - $fn = $this->context->module->addFunction('__object__invoke_destructor', $fnType); - $this->context->registerFunction('__object__invoke_destructor', $fn); - } - $entry = $fn->appendBasicBlock('entry'); - $this->context->builder->positionAtEnd($entry); - $obj = $fn->getParam(0); - $classIds = $this->classIdsWithDestructor(); - if ([] === $classIds) { - $this->context->builder->returnVoid(); - $this->context->builder->clearInsertionPosition(); - - return; - } - $constructed = $this->context->builder->load( - $this->context->builder->structGep($obj, $this->context->structFieldMap['__object__']['constructed']) - ); - $notReady = $fn->appendBasicBlock('destruct_not_constructed'); - $ready = $fn->appendBasicBlock('destruct_ready'); - $done = $fn->appendBasicBlock('destruct_done'); - $isReady = $this->context->builder->icmp( - PHPLLVM\Builder::INT_NE, - $constructed, - $this->context->getTypeFromString('int8')->constInt(0, false) - ); - $this->context->builder->branchIf($isReady, $ready, $notReady); - $this->context->builder->positionAtEnd($notReady); - $this->context->builder->branch($done); - $this->context->builder->positionAtEnd($ready); - $this->emitDestructDispatchForObject($fn, $obj, $classIds, $done); - $this->context->builder->positionAtEnd($done); - $this->context->builder->returnVoid(); - $this->context->builder->clearInsertionPosition(); - } - - /** - * @param list $classIds - */ - private function emitDestructDispatchForObject( - PHPLLVM\Value\Function_ $fn, - PHPLLVM\Value $obj, - array $classIds, - PHPLLVM\BasicBlock $outerDone - ): void { - $objMap = $this->context->structFieldMap['__object__']; - $classIdVal = $this->context->builder->load( - $this->context->builder->structGep($obj, $objMap['class_id']) - ); - if (1 === \count($classIds)) { - $onlyId = $classIds[0]; - $callBlock = $fn->appendBasicBlock('destruct_magic_single_call'); - $skipBlock = $fn->appendBasicBlock('destruct_magic_single_skip'); - $expected = $this->context->constantFromInteger($onlyId, 'int64'); - $isId = $this->context->builder->icmp(PHPLLVM\Builder::INT_EQ, $classIdVal, $expected); - $this->context->builder->branchIf($isId, $callBlock, $skipBlock); - $this->context->builder->positionAtEnd($callBlock); - $this->emitDestructMagicCallForClass($obj, $onlyId); - $this->context->builder->branch($outerDone); - $this->context->builder->positionAtEnd($skipBlock); - $this->context->builder->branch($outerDone); - - return; - } - $done = $fn->appendBasicBlock('destruct_magic_done'); - $fallback = $fn->appendBasicBlock('destruct_magic_unknown'); - $caseBlocks = []; - foreach ($classIds as $id) { - $caseBlocks[] = $fn->appendBasicBlock('destruct_magic_class_'.$id); - } - $checkBlock = $this->context->builder->getInsertBlock(); - foreach ($classIds as $i => $id) { - $this->context->builder->positionAtEnd($checkBlock); - $expected = $this->context->constantFromInteger($id, 'int64'); - $isId = $this->context->builder->icmp(PHPLLVM\Builder::INT_EQ, $classIdVal, $expected); - $nextCheck = $i + 1 < \count($classIds) - ? $fn->appendBasicBlock('destruct_magic_try_'.($i + 1)) - : $fallback; - $this->context->builder->branchIf($isId, $caseBlocks[$i], $nextCheck); - $this->context->builder->positionAtEnd($caseBlocks[$i]); - $this->emitDestructMagicCallForClass($obj, $id); - $this->context->builder->branch($done); - $checkBlock = $nextCheck; - } - $this->context->builder->positionAtEnd($fallback); - $this->context->builder->branch($done); - $this->context->builder->positionAtEnd($done); - $this->context->builder->branch($outerDone); - } - - private function emitDestructMagicCallForClass(PHPLLVM\Value $obj, int $classId): void - { - $className = $this->classNameForId($classId); - $proxyName = strtolower($className).'::'.'__destruct'; - if (!$this->context->functionIsRegistered($proxyName)) { - return; - } - $refVirtual = $this->context->builder->pointerCast( - $obj, - $this->context->getTypeFromString('__ref__virtual*') - ); - $this->context->refcount->addref($refVirtual); - $this->context->builder->call( - $this->context->lookupFunction('phpc_destruct_set_allow_delref'), - $this->context->getTypeFromString('int32')->constInt(0, false) - ); - $objVar = new Variable( - $this->context, - Variable::TYPE_OBJECT, - Variable::KIND_VALUE, - $obj - ); - $toCall = $this->context->resolveFunctionProxy($proxyName); - $prevStrict = $this->context->callerStrictTypes; - $this->context->callerStrictTypes = false; - $toCall->call($this->context, $objVar); - $this->context->callerStrictTypes = $prevStrict; - } - /** Property slot count stored at allocation — replaces phpc_object_prop_count (#6749). */ private function implementObjectPropCount(): void { @@ -1202,7 +1075,7 @@ private function objectHeaderSize(): PHPLLVM\Value ); } - private function propertySlotPtr(PHPLLVM\Value $obj, int $slotIndex): PHPLLVM\Value + public function propertySlotPtr(PHPLLVM\Value $obj, int $slotIndex): PHPLLVM\Value { $i8p = $this->context->getTypeFromString('int8*'); $voidpp = $this->context->getTypeFromString('void**'); @@ -1218,6 +1091,29 @@ private function propertySlotPtr(PHPLLVM\Value $obj, int $slotIndex): PHPLLVM\Va ); } + public function propNameIdFor(string $name): ?int + { + return $this->propNameMap[$name] ?? null; + } + + public function propNameIdAfterDefine(string $name): int + { + return $this->propNameMap[$name]; + } + + /** + * @return list + */ + public function propertySetsForClass(int $classId): array + { + return $this->properties[$classId] ?? []; + } + + public function recordSlotReceiver(PHPLLVM\Value $slot, PHPLLVM\Value $obj): void + { + $this->slotReceivers[spl_object_id($slot)] = $obj; + } + public function declareClass(Operand $name): int { if (!$name instanceof Literal) { @@ -4355,12 +4251,12 @@ public function defineStaticProperty( } elseif (Variable::TYPE_NATIVE_BOOL === $jitType) { $llvmType = $this->context->getTypeFromString('int1'); $global = $this->context->module->addGlobal($llvmType, $globalName); - $global->setInitializer($this->staticPropertyScalarInitializer($jitType, $default)); + $global->setInitializer(ObjectStaticPropertyInitLlvm::scalarInitializer($this, $jitType, $default)); } else { $llvmTypeName = Variable::TYPE_NATIVE_DOUBLE === $jitType ? 'double' : 'int64'; $llvmType = $this->context->getTypeFromString($llvmTypeName); $global = $this->context->module->addGlobal($llvmType, $globalName); - $global->setInitializer($this->staticPropertyScalarInitializer($jitType, $default)); + $global->setInitializer(ObjectStaticPropertyInitLlvm::scalarInitializer($this, $jitType, $default)); } $entry = [ 'type' => $jitType, @@ -4380,12 +4276,12 @@ public function defineStaticProperty( $this->staticPropertyVisibility[$classId][$key] = MethodVisibility::mask($visibilityFlags); $this->staticPropertyDeclaringClassId[$classId][$key] = $classId; if (Variable::TYPE_STRING === $jitType && null !== $default) { - $this->initStaticStringPropertyDefault($global, $default); + ObjectStaticPropertyInitLlvm::initStringDefault($this, $global, $default); } if (Variable::TYPE_HASHTABLE === $jitType) { if (null === $default || VMVariable::TYPE_ARRAY === $default->type) { - if (!$this->deferStaticHashtableInitInAot($classId)) { - $this->initStaticHashtablePropertyEmpty($global); + if (!ObjectStaticPropertyInitLlvm::deferHashtableInitInAot($this, $classId)) { + ObjectStaticPropertyInitLlvm::initHashtableEmpty($this, $global); } } else { throw new \LogicException( @@ -4394,216 +4290,14 @@ public function defineStaticProperty( } } if (Variable::TYPE_VALUE === $jitType && null !== $default && EnumCaseSupport::isEnumCaseVariable($default)) { - $this->initStaticValuePropertyEnumCase($global, $default); + ObjectStaticPropertyInitLlvm::initValueEnumCase($this, $global, $default); } elseif (Variable::TYPE_VALUE === $jitType && null !== $default && VMVariable::TYPE_ARRAY === $default->type) { - $this->initStaticValuePropertyEmptyArray($global); + ObjectStaticPropertyInitLlvm::initValueEmptyArray($this, $global); } elseif (Variable::TYPE_VALUE === $jitType && null !== $default && VMVariable::TYPE_NULL !== $default->type) { - $this->initStaticValuePropertyScalarDefault($global, $default); + ObjectStaticPropertyInitLlvm::initValueScalarDefault($this, $global, $default); } elseif (Variable::TYPE_VALUE === $jitType && (null === $default || VMVariable::TYPE_NULL === $default->type)) { - $this->initStaticValuePropertyNull($global); - } - } - - private function staticPropertyScalarInitializer(int $jitType, ?VMVariable $value): \PHPLLVM\Value - { - if (Variable::TYPE_NATIVE_DOUBLE === $jitType) { - $llvmType = $this->context->getTypeFromString('double'); - $float = null !== $value && VMVariable::TYPE_FLOAT === $value->type ? $value->toFloat() : 0.0; - - return $llvmType->constReal($float); - } - $llvmType = $this->context->getTypeFromString( - Variable::TYPE_NATIVE_BOOL === $jitType ? 'int1' : 'int64' - ); - $int = 0; - if (null !== $value) { - $int = match ($value->type) { - VMVariable::TYPE_INTEGER => $value->toInt(), - VMVariable::TYPE_BOOLEAN => $value->toBool() ? 1 : 0, - default => 0, - }; - } - - return $llvmType->constInt($int, false); - } - - private function initStaticStringPropertyDefault(\PHPLLVM\Value $global, VMVariable $value): void - { - if (VMVariable::TYPE_STRING !== $value->type) { - throw new \LogicException('Static string property default must be a string'); - } - $restore = $this->context->builder->getInsertBlock(); - $this->context->positionBuilderAtInitEmission(); - $str = $this->context->builder->load( - $this->context->constantStringFromString($value->toString()) - ); - $owned = $this->context->builder->call( - $this->context->lookupFunction('__string__separate'), - $str - ); - $this->context->builder->store($owned, $global); - if (null !== $restore) { - BasicBlockHelper::restoreInsertBlock($this->context, $restore); - } - } - - /** Allocate a null {@see __value__} box for untyped static properties (bootstrap JIT helpers). */ - private function initStaticValuePropertyNull(\PHPLLVM\Value $global): void - { - $restore = $this->context->builder->getInsertBlock(); - $this->context->positionBuilderAtInitEmission(); - $valueType = $this->context->getTypeFromString('__value__'); - $heapVal = $this->context->memory->malloc($valueType); - $heapPtr = $this->context->builder->pointerCast( - $heapVal, - $this->context->getTypeFromString('__value__*') - ); - $this->context->builder->call( - $this->context->lookupFunction('__value__writeNull'), - $heapPtr - ); - $this->context->builder->store($heapPtr, $global); - if (null !== $restore) { - BasicBlockHelper::restoreInsertBlock($this->context, $restore); - } - } - - /** Initialize a typed static array property with an empty hashtable (#8716). */ - private function initStaticHashtablePropertyEmpty(\PHPLLVM\Value $global): void - { - $restore = $this->context->builder->getInsertBlock(); - $this->context->positionBuilderAtInitEmission(); - $ht = HashTableHelper::alloc($this->context); - $this->context->builder->store($ht, $global); - if (null !== $restore) { - BasicBlockHelper::restoreInsertBlock($this->context, $restore); - } - } - - /** - * WeakRef registry slot tables allocate on first write — eager __init__ hashtable alloc - * runs before runtime tables are ready and segfaults HelloWorld AOT (#11437). - */ - private function deferStaticHashtableInitInAot(int $classId): bool - { - if (\PHPCompiler\JIT\Builtin::LOAD_TYPE_STANDALONE !== $this->context->loadType) { - return false; - } - $classLc = strtolower(ltrim($this->classNameForId($classId), '\\')); - - return 'phpcompiler\\ext\\standard\\weakrefregistryjithelper' === $classLc; - } - - /** Box an empty compile-time array default into a union/DNF static {@see __value__} property (#8708, #8719, DomRegistry::$states #6140). */ - private function initStaticValuePropertyEmptyArray(\PHPLLVM\Value $global): void - { - $restore = $this->context->builder->getInsertBlock(); - $this->context->positionBuilderAtInitEmission(); - $valueType = $this->context->getTypeFromString('__value__'); - $heapVal = $this->context->memory->malloc($valueType); - $heapPtr = $this->context->builder->pointerCast( - $heapVal, - $this->context->getTypeFromString('__value__*') - ); - $ht = HashTableHelper::alloc($this->context); - $this->context->builder->call( - $this->context->lookupFunction('__value__writeHashtable'), - $heapPtr, - $ht - ); - $this->context->builder->store($heapPtr, $global); - if (null !== $restore) { - BasicBlockHelper::restoreInsertBlock($this->context, $restore); - } - } - - /** Box a compile-time scalar default into a union/DNF static {@see __value__} property (#8726). */ - private function initStaticValuePropertyScalarDefault(\PHPLLVM\Value $global, VMVariable $default): void - { - $restore = $this->context->builder->getInsertBlock(); - $this->context->positionBuilderAtInitEmission(); - $valueType = $this->context->getTypeFromString('__value__'); - $heapVal = $this->context->memory->malloc($valueType); - $heapPtr = $this->context->builder->pointerCast( - $heapVal, - $this->context->getTypeFromString('__value__*') - ); - $valueMap = $this->context->structFieldMap['__value__']; - $this->context->builder->store( - $this->context->getTypeFromString('int8')->constInt($default->type, false), - $this->context->builder->structGep($heapVal, $valueMap['type']) - ); - if (VMVariable::TYPE_STRING === $default->type) { - $str = $this->context->builder->load( - $this->context->constantStringFromString($default->toString()) - ); - $owned = $this->context->builder->call( - $this->context->lookupFunction('__string__separate'), - $str - ); - $this->context->builder->call( - $this->context->lookupFunction('__value__writeString'), - $heapPtr, - $owned - ); - } elseif (VMVariable::TYPE_INTEGER === $default->type) { - $this->context->builder->call( - $this->context->lookupFunction('__value__writeLong'), - $heapPtr, - $this->context->getTypeFromString('int64')->constInt($default->toInt(), false) - ); - } elseif (VMVariable::TYPE_FLOAT === $default->type) { - $this->context->builder->call( - $this->context->lookupFunction('__value__writeDouble'), - $heapPtr, - $this->context->getTypeFromString('double')->constReal($default->toFloat()) - ); - } elseif (VMVariable::TYPE_BOOLEAN === $default->type) { - $this->context->builder->call( - $this->context->lookupFunction('__value__writeLong'), - $heapPtr, - $this->context->getTypeFromString('int64')->constInt($default->toBool() ? 1 : 0, false) - ); - } else { - throw new \LogicException( - 'Static union/DNF property default must be a scalar compile-time constant' - ); - } - $this->context->builder->store($heapPtr, $global); - if (null !== $restore) { - BasicBlockHelper::restoreInsertBlock($this->context, $restore); - } - } - - /** Box a compile-time enum case singleton into a typed static {@see __value__} property (#5891). */ - private function initStaticValuePropertyEnumCase(\PHPLLVM\Value $global, VMVariable $default): void - { - $enumClass = EnumCaseSupport::enumClassForCaseVariable($default); - if (null === $enumClass) { - throw new \LogicException('Static enum case property default requires enum class'); + ObjectStaticPropertyInitLlvm::initValueNull($this, $global); } - $enumClassId = $this->lookup(strtolower($enumClass->name)); - $caseKey = strtolower(EnumCaseSupport::enumCaseNameForVariable($default)); - $globalName = $this->ensureEnumCaseSingletonGlobal($enumClassId, $caseKey); - $this->context->emitInInit(function (Context $ctx) use ($global, $globalName): void { - $objGlobal = $ctx->module->getNamedGlobal($globalName); - if (null === $objGlobal) { - throw new \LogicException("Missing enum case singleton global: {$globalName}"); - } - $valueType = $ctx->getTypeFromString('__value__'); - $heapVal = $ctx->memory->malloc($valueType); - $heapPtr = $ctx->builder->pointerCast( - $heapVal, - $ctx->getTypeFromString('__value__*') - ); - $obj = $ctx->builder->load($objGlobal); - $ctx->builder->call( - $ctx->lookupFunction('__value__writeObject'), - $heapPtr, - $obj - ); - $ctx->builder->store($heapPtr, $global); - }); } public function staticPropertyUnset(int $classId, string $name): void @@ -5589,7 +5283,7 @@ public function propertyFetch(PHPLLVM\Value $obj, string $class, string $name): return ObjectEnumCasePropertyLlvm::propertyFetchEnumCaseRuntimeDispatch($this, $obj, $nameLc, $enumIds); } - return $this->propertyFetchOrdinary($obj, $class, $name, $classId); + return ObjectInstancePropertyLlvm::propertyFetchOrdinary($this, $obj, $class, $name, $classId); } /** @@ -5607,104 +5301,10 @@ public function registeredEnumClassIds(): array return $ids; } - private function propertyFetchOrdinary( - PHPLLVM\Value $obj, - string $class, - string $name, - int $classId - ): Variable { - $className = $this->classNameForId($classId); - $nameId = $this->propNameMap[$name] ?? null; - $hasProp = false; - if (null !== $nameId) { - foreach ($this->properties[$classId] as $propset) { - if ($propset[0] === $nameId) { - $hasProp = true; - break; - } - } - } - if (!$hasProp) { - $this->defineProperty($classId, $name, $this->externalPropertyJitType($class, $name)); - $nameId = $this->propNameMap[$name]; - } - foreach ($this->properties[$classId] as $propset) { - if ($propset[0] === $nameId) { - $slot = $this->propertySlotPtr($obj, $propset[3]); - $loaded = $this->context->builder->load($slot); - if (Variable::TYPE_VALUE === $propset[2]) { - $valueType = $this->context->getTypeFromString('__value__'); - $storage = BasicBlockHelper::entryAlloca($this->context, $valueType); - $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 - ); - $var = new Variable( - $this->context, - $propset[2], - Variable::KIND_VARIABLE, - $storage, - ); - $var->objectPropertySlot = $slot; - $var->objectPropertyType = $propset[2]; - $var->objectPropertyReceiver = $obj; - $var->objectPropertyName = $propset[1]; - $var->objectPropertyClassName = $className; - $var->objectPropertyDnfArms = $this->dnfArmsForProperty($classId, $propset[1]); - $this->slotReceivers[spl_object_id($slot)] = $obj; - - return $var; - } - if (Variable::TYPE_HASHTABLE === $propset[2]) { - $htPtr = $this->context->builder->pointerCast( - $loaded, - $this->context->getTypeFromString('__hashtable__*') - ); - $var = new Variable( - $this->context, - Variable::TYPE_HASHTABLE, - Variable::KIND_VALUE, - $htPtr - ); - $var->objectPropertySlot = $slot; - $var->objectPropertyType = $propset[2]; - $var->objectPropertyReceiver = $obj; - $var->objectPropertyName = $propset[1]; - $var->objectPropertyClassName = $className; - $var->objectPropertyDnfArms = $this->dnfArmsForProperty($classId, $propset[1]); - $this->slotReceivers[spl_object_id($slot)] = $obj; - - return $var; - } - $llvmType = Variable::getStringType($propset[2]); - $typed = $this->context->builder->pointerCast( - $loaded, - $this->context->getTypeFromString($llvmType) - ); - $var = new Variable( - $this->context, - $propset[2], - Variable::KIND_VALUE, - $typed, - ); - $var->objectPropertySlot = $slot; - $var->objectPropertyType = $propset[2]; - $var->objectPropertyReceiver = $obj; - $var->objectPropertyName = $propset[1]; - $var->objectPropertyClassName = $className; - $var->objectPropertyDnfArms = $this->dnfArmsForProperty($classId, $propset[1]); - $this->slotReceivers[spl_object_id($slot)] = $obj; - - return $var; - } - } - throw new \LogicException("Could not find property $name for class $classId"); + public function boxFetchedPropertyIntoValueBox(PHPLLVM\Value $destSlot, Variable $fetched): void + { + $propertyType = $fetched->objectPropertyType ?? $fetched->type; + ObjectInstancePropertyLlvm::boxFetchedPropertyIntoValue($this, $destSlot, $fetched, $propertyType); } /** @@ -5826,7 +5426,7 @@ public function propertyFetchDynamic( $this->context->builder->positionAtEnd($caseBlock); $fetched = $this->propertyFetch($obj, $class, $propName); TypedPropertyUninitGuard::emitBeforeRead($this->context, $fetched); - $this->boxFetchedPropertyIntoValue($destSlot, $fetched, $propset[2]); + ObjectInstancePropertyLlvm::boxFetchedPropertyIntoValue($this, $destSlot, $fetched, $propset[2]); $this->context->builder->branch($done); $checkBlock = $nextCheck; } @@ -5857,91 +5457,6 @@ public function propertyFetchDynamic( ); } - public function boxFetchedPropertyIntoValueBox(PHPLLVM\Value $destSlot, Variable $fetched): void - { - $propertyType = $fetched->objectPropertyType ?? $fetched->type; - $this->boxFetchedPropertyIntoValue($destSlot, $fetched, $propertyType); - } - - private function boxFetchedPropertyIntoValue( - PHPLLVM\Value $destSlot, - Variable $fetched, - int $propertyType - ): void { - $destPtr = JitValueBox::pointer($this->context, $destSlot); - if (Variable::TYPE_VALUE === $propertyType) { - $this->context->builder->call( - $this->context->lookupFunction('__object__load_value_slot'), - $fetched->objectPropertySlot, - $destSlot - ); - - return; - } - if (Variable::TYPE_HASHTABLE === $propertyType) { - $htPtr = $this->context->builder->pointerCast( - $this->context->builder->load($fetched->objectPropertySlot), - $this->context->getTypeFromString('__hashtable__*') - ); - $this->context->builder->call( - $this->context->lookupFunction('__value__writeHashtable'), - $destPtr, - $htPtr - ); - - return; - } - if (Variable::TYPE_NATIVE_LONG === $propertyType) { - $this->context->builder->call( - $this->context->lookupFunction('__value__writeLong'), - $destPtr, - $this->context->builder->load($fetched->value) - ); - - return; - } - if (Variable::TYPE_NATIVE_BOOL === $propertyType) { - JitValueBox::writeBool( - $this->context, - $destSlot, - $this->context->builder->load($fetched->value) - ); - - return; - } - if (Variable::TYPE_NATIVE_DOUBLE === $propertyType) { - $this->context->builder->call( - $this->context->lookupFunction('__value__writeDouble'), - $destPtr, - $this->context->builder->load($fetched->value) - ); - - return; - } - if (Variable::TYPE_STRING === $propertyType) { - $this->context->builder->call( - $this->context->lookupFunction('__value__writeString'), - $destPtr, - $this->context->builder->load($fetched->value) - ); - - return; - } - if (Variable::TYPE_OBJECT === $propertyType) { - $this->context->builder->call( - $this->context->lookupFunction('__value__writeObject'), - $destPtr, - $this->context->builder->load($fetched->value) - ); - - return; - } - - throw new \LogicException( - 'Dynamic property fetch JIT box unsupported type: '.Variable::getStringType($propertyType) - ); - } - /** * Persist assignment to an instance property slot (void** on the object, issue #58). */ diff --git a/test/unit/ObjectPropertyRuntimeShrinkTest.php b/test/unit/ObjectPropertyRuntimeShrinkTest.php index 5af928c79ed..62cb09c8104 100644 --- a/test/unit/ObjectPropertyRuntimeShrinkTest.php +++ b/test/unit/ObjectPropertyRuntimeShrinkTest.php @@ -44,6 +44,32 @@ public function testObjectMonolithDelegatesExitStatusLowering(): void $this->assertStringNotContainsString('exit_status_obj_enum_', $object); } + public function testObjectMonolithDelegatesDestructorLowering(): void + { + $object = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/Type/Object_.php'); + $this->assertStringContainsString('ObjectDestructorLlvm::implementInvokeDestructor', $object); + $this->assertStringNotContainsString('private function emitDestructDispatchForObject', $object); + $this->assertStringNotContainsString('private function emitDestructMagicCallForClass', $object); + } + + public function testObjectMonolithDelegatesStaticPropertyInit(): void + { + $object = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/Type/Object_.php'); + $this->assertStringContainsString('ObjectStaticPropertyInitLlvm::scalarInitializer', $object); + $this->assertStringContainsString('ObjectStaticPropertyInitLlvm::initValueNull', $object); + $this->assertStringNotContainsString('private function initStaticValuePropertyNull', $object); + $this->assertStringNotContainsString('private function staticPropertyScalarInitializer', $object); + } + + public function testObjectMonolithDelegatesInstancePropertyFetch(): void + { + $object = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/Type/Object_.php'); + $this->assertStringContainsString('ObjectInstancePropertyLlvm::propertyFetchOrdinary', $object); + $this->assertStringContainsString('ObjectInstancePropertyLlvm::boxFetchedPropertyIntoValue', $object); + $this->assertStringNotContainsString('private function propertyFetchOrdinary', $object); + $this->assertStringNotContainsString('private function boxFetchedPropertyIntoValue', $object); + } + public function testEnumStringCastErrorMessageMatchesZend(): void { $msg = EnumCasePropertyJitHelper::enumStringCastErrorMessage('E');