From bb54eee9a9ca37e2650372f9b312e3adc7c7268c Mon Sep 17 00:00:00 2001 From: PurHur Date: Mon, 3 Aug 2026 04:43:53 +0000 Subject: [PATCH 1/3] Stdlib: AOT Dom\Attr::rename + XMLDocument::createFromString wiring (#27108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire living Attr/Element rename and attribute methods through DomInstanceMethod for user-script AOT, and add Dom\XMLDocument::createFromString Call so phpc build no longer fails with object::rename() / ExternalMethod NULL. Runtime AOT still segfaults on living Dom class_id/property fetch after createFromString — next is LLVM materialization (peer JitDomLoadXMLUserScript) or NestedJIT class_id sync. Co-authored-by: Cursor --- ...omXmlDocumentCreateFromStringJitHelper.php | 36 ++++++++ ext/dom/JitDomDocumentMethodKernel.php | 18 ++++ ext/dom/JitDomXmlDocumentCreateFromString.php | 92 +++++++++++++++++++ ext/dom/VmDomInstanceInvoke.php | 5 + ext/dom/VmDomJitDispatch.php | 92 +++++++++++++++++++ ext/dom/VmDomLiving.php | 6 +- lib/JIT.php | 67 ++++++++++++++ .../DomXmlDocumentCreateFromStringRuntime.php | 52 +++++++++++ .../Call/DomXmlDocumentCreateFromString.php | 33 +++++++ lib/JIT/Context.php | 4 + lib/JIT/DomInstanceMethodJit.php | 50 ++++++++++ .../compiler_lib_spine_smoke/main.php | 4 + test/unit/DomInstanceMethodJitTest.php | 14 +++ 13 files changed, 471 insertions(+), 2 deletions(-) create mode 100644 ext/dom/DomXmlDocumentCreateFromStringJitHelper.php create mode 100644 ext/dom/JitDomXmlDocumentCreateFromString.php create mode 100644 lib/JIT/Builtin/DomXmlDocumentCreateFromStringRuntime.php create mode 100644 lib/JIT/Call/DomXmlDocumentCreateFromString.php diff --git a/ext/dom/DomXmlDocumentCreateFromStringJitHelper.php b/ext/dom/DomXmlDocumentCreateFromStringJitHelper.php new file mode 100644 index 00000000000..ce906c9b861 --- /dev/null +++ b/ext/dom/DomXmlDocumentCreateFromStringJitHelper.php @@ -0,0 +1,36 @@ +type) { + throw new \DOMException( + 'Invalid State Error', + DomExceptionConstants::INVALID_STATE_ERR + ); + } + + return $var->toObject(); + } +} diff --git a/ext/dom/JitDomDocumentMethodKernel.php b/ext/dom/JitDomDocumentMethodKernel.php index 94f76f2093a..7fad5a823b2 100644 --- a/ext/dom/JitDomDocumentMethodKernel.php +++ b/ext/dom/JitDomDocumentMethodKernel.php @@ -30,6 +30,7 @@ use PHPCompiler\JIT\Builtin\DomSyncElementIdMapRuntime; use PHPCompiler\JIT\Builtin\DomXPathEvaluateRuntime; use PHPCompiler\JIT\Builtin\DomXPathQueryRuntime; +use PHPCompiler\JIT\Builtin\DomXmlDocumentCreateFromStringRuntime; use PHPCompiler\JIT\Context; use PHPCompiler\JIT\JitNestedHelperCoerce; use PHPCompiler\JIT\JitValueBox; @@ -232,6 +233,23 @@ public static function ensureCreateAttributeBridge(Context $context): void ); } + /** Dom\XMLDocument::createFromString() — thin AOT (#27108). */ + public static function ensureXmlDocumentCreateFromStringBridge(Context $context): void + { + self::ensureContextBridge( + $context, + DomXmlDocumentCreateFromStringRuntime::ABI_NAME, + 'dom_xml_document_create_from_string_user_script', + [ + $context->getTypeFromString('__string__*'), + $context->getTypeFromString('int64'), + ], + $context->getTypeFromString('__object__*'), + 'PHPCompiler\\ext\\dom\\DomXmlDocumentCreateFromStringJitHelper::createFromStringArgv', + '/ext/dom/DomXmlDocumentCreateFromStringJitHelper.php' + ); + } + public static function ensureSetAttributeNodeBridge(Context $context): void { self::ensureBridge( diff --git a/ext/dom/JitDomXmlDocumentCreateFromString.php b/ext/dom/JitDomXmlDocumentCreateFromString.php new file mode 100644 index 00000000000..829e4e3963b --- /dev/null +++ b/ext/dom/JitDomXmlDocumentCreateFromString.php @@ -0,0 +1,92 @@ +type || $sourceArg->isNullConstant) { + JitStringBuiltinArg::emitNullStringParamDeprecation( + $context, + 'Dom\\XMLDocument::createFromString', + 0, + 'source' + ); + } + + $sourceLit = JitStringBuiltinArg::compileTimeLiteral($sourceArg) ?? $sourceArg->compileTimeString; + if (null !== $sourceLit && '' === $sourceLit) { + TypeErrorRaise::emitValueError( + $context, + 'Dom\\XMLDocument::createFromString(): Argument #1 ($source) must not be empty' + ); + } + + $xmlStr = JitStringBuiltinArg::lower( + $context, + $sourceArg, + 'Dom\\XMLDocument::createFromString', + 0, + 'source' + ); + $options = $context->getTypeFromString('int64')->constInt(0, false); + if (null !== $optionsArg && !NamedOptionalCallArgs::isOmittedOptional($optionsArg)) { + $options = JitIntdiv::lowerIntBuiltinArg( + $context, + $optionsArg, + 'Dom\\XMLDocument::createFromString()', + 2, + 'options' + ); + } + + VmActiveContextInitLlvm::requestThinStandaloneInit($context); + DomXmlDocumentCreateFromStringRuntime::ensureLinked($context); + + $document = $context->builder->call( + $context->lookupFunction(DomXmlDocumentCreateFromStringRuntime::ABI_NAME), + $xmlStr, + $options + ); + // Box in the caller — ABI returns __object__* (do not return callee alloca value*). + $slot = JitValueBox::alloc($context); + $ptr = JitValueBox::pointer($context, $slot); + $context->builder->call( + $context->lookupFunction('__value__writeObject'), + $ptr, + $document + ); + + return JitValueBox::normalizeValuePtr($context, $ptr); + } +} diff --git a/ext/dom/VmDomInstanceInvoke.php b/ext/dom/VmDomInstanceInvoke.php index 26635e2d27a..57ce4737516 100644 --- a/ext/dom/VmDomInstanceInvoke.php +++ b/ext/dom/VmDomInstanceInvoke.php @@ -63,6 +63,8 @@ private static function dispatch(Variable $receiver, string $methodLc, Variable return match ($methodLc) { 'createelement' => VmDomJitDispatch::createElement($ctx, $self, $extra), + 'createattribute' => VmDomJitDispatch::createAttribute($ctx, $self, $extra), + 'createattributens' => VmDomJitDispatch::createAttributeNS($ctx, $self, $extra), 'loadhtml' => VmDomJitDispatch::loadHTML($ctx, $self, $extra), 'loadxml' => VmDomJitDispatch::loadXML($ctx, $self, $extra), 'load' => VmDomJitDispatch::load($ctx, $self, $extra), @@ -82,6 +84,8 @@ private static function dispatch(Variable $receiver, string $methodLc, Variable 'adoptnode' => VmDomJitDispatch::adoptNode($ctx, $self, $extra), 'getattribute' => VmDomJitDispatch::getAttribute($self, $extra), 'getattributens' => VmDomJitDispatch::getAttributeNS($self, $extra), + 'hasattribute' => VmDomJitDispatch::hasAttribute($self, $extra), + 'hasattributens' => VmDomJitDispatch::hasAttributeNS($self, $extra), 'getattributenode' => VmDomJitDispatch::getAttributeNode($ctx, $self, $extra), 'setattribute' => VmDomJitDispatch::setAttribute($ctx, $self, $extra), 'removeattribute' => VmDomJitDispatch::removeAttribute($ctx, $self, $extra), @@ -117,6 +121,7 @@ private static function dispatch(Variable $receiver, string $methodLc, Variable 'closest' => VmDomJitDispatch::closest($self, $extra), 'matches' => VmDomJitDispatch::matches($self, $extra), 'savehtml' => VmDomJitDispatch::saveHtml($self, $extra), + 'rename' => VmDomJitDispatch::rename($ctx, $self, $extra), default => throw new \Error('Call to undefined method '.$self->class->name.'::'.$methodLc.'()'), }; } diff --git a/ext/dom/VmDomJitDispatch.php b/ext/dom/VmDomJitDispatch.php index ac7912bfff8..4db743282ea 100644 --- a/ext/dom/VmDomJitDispatch.php +++ b/ext/dom/VmDomJitDispatch.php @@ -244,6 +244,35 @@ public static function createElement(VmContext $ctx, ObjectEntry $document, arra return VmDom::createElement($ctx, $name, $document, $value); } + /** + * @param list $extra + */ + public static function createAttribute(VmContext $ctx, ObjectEntry $document, array $extra): Variable + { + $name = self::stringArg($extra[0] ?? self::missingArg('createAttribute', 0), 'createAttribute', 0); + + return VmDom::createAttribute($ctx, $name, $document); + } + + /** + * @param list $extra + */ + public static function createAttributeNS(VmContext $ctx, ObjectEntry $document, array $extra): Variable + { + $namespace = self::nullableStringArg( + $extra[0] ?? self::missingArg('createAttributeNS', 0), + 'createAttributeNS', + 0 + ); + $qualifiedName = self::stringArg( + $extra[1] ?? self::missingArg('createAttributeNS', 1), + 'createAttributeNS', + 1 + ); + + return VmDom::createAttributeNS($ctx, $namespace, $qualifiedName, $document); + } + /** * @param list $extra */ @@ -488,6 +517,39 @@ public static function createDocumentType(VmContext $ctx, ObjectEntry $implement return VmDom::createDocumentType($ctx, $qualifiedName, $publicId, $systemId); } + /** + * @param list $extra + */ + public static function hasAttribute(ObjectEntry $element, array $extra): Variable + { + $name = self::stringArg($extra[0] ?? self::missingArg('hasAttribute', 0), 'hasAttribute', 0); + $var = new Variable(); + $var->bool(VmDom::hasAttribute($element, $name)); + + return $var; + } + + /** + * @param list $extra + */ + public static function hasAttributeNS(ObjectEntry $element, array $extra): Variable + { + $namespace = self::nullableStringArg( + $extra[0] ?? self::missingArg('hasAttributeNS', 0), + 'hasAttributeNS', + 0 + ); + $localName = self::stringArg( + $extra[1] ?? self::missingArg('hasAttributeNS', 1), + 'hasAttributeNS', + 1 + ); + $var = new Variable(); + $var->bool(VmDom::hasAttributeNS($element, $namespace, $localName)); + + return $var; + } + /** * @param list $extra */ @@ -898,6 +960,36 @@ public static function nodeContains(ObjectEntry $node, array $extra): Variable return $result; } + /** + * Dom\Attr::rename / Dom\Element::rename — php-src element.c (#21083, #27108). + * + * @param list $extra + */ + public static function rename(VmContext $ctx, ObjectEntry $receiver, array $extra): Variable + { + if (\count($extra) !== 2) { + $label = VmDom::isAttr($receiver) ? 'Dom\\Attr::rename()' : 'Dom\\Element::rename()'; + throw new \ArgumentCountError(sprintf( + '%s expects exactly 2 arguments, %d given', + $label, + \count($extra) + )); + } + $namespaceUri = self::nullableStringArg($extra[0], 'rename', 0); + $qualifiedName = self::stringArg($extra[1], 'rename', 1); + if (VmDom::isAttr($receiver)) { + VmDomLiving::renameAttr($ctx, $receiver, $namespaceUri, $qualifiedName); + } elseif (VmDom::isElement($receiver)) { + VmDomLiving::renameElement($receiver, $namespaceUri, $qualifiedName); + } else { + throw new \Error('Call to undefined method '.$receiver->class->name.'::rename()'); + } + $null = new Variable(); + $null->null(); + + return $null; + } + /** * @param list $extra */ diff --git a/ext/dom/VmDomLiving.php b/ext/dom/VmDomLiving.php index 67d846fe60c..357a0ac3d64 100644 --- a/ext/dom/VmDomLiving.php +++ b/ext/dom/VmDomLiving.php @@ -1005,7 +1005,8 @@ private static function applyLivingElementClassMap(DomNodeState $state): void self::applyLivingLeafClassMap($state, self::CLASS_HTML_ELEMENT); } - private static function applyLivingXmlElementClassMap(DomNodeState $state): void + /** @internal used by DomXmlDocumentCreateFromStringJitHelper (#27108). */ + public static function applyLivingXmlElementClassMap(DomNodeState $state): void { self::applyLivingLeafClassMap($state, self::CLASS_ELEMENT); } @@ -1258,7 +1259,8 @@ private static function htmlSourceHasMarkupAfter(string $source, int $from): boo } /** php-src ext/dom/xml_document.c check_options_validity(). */ - private static function assertValidXmlParseOptions(int $options, string $method): void + /** @internal used by DomXmlDocumentCreateFromStringJitHelper (#27108). */ + public static function assertValidXmlParseOptions(int $options, string $method): void { $allowed = LibxmlConstants::LIBXML_RECOVER | LibxmlConstants::LIBXML_NOENT diff --git a/lib/JIT.php b/lib/JIT.php index c9b4f471583..a00a43406ca 100644 --- a/lib/JIT.php +++ b/lib/JIT.php @@ -18878,6 +18878,31 @@ private function initJitMethodCall( JIT\DomInstanceMethodJit::ensureProxy($this->context, 'dom\\namednodemap::'.$methodLc); JIT\DomInstanceMethodJit::ensureProxy($this->context, 'dom\\dtdnamednodemap::'.$methodLc); } + // getAttributeNode / documentElement temps lower as :object — bind living rename (#27108). + if ('rename' === $methodLc) { + JIT\DomInstanceMethodJit::ensureProxy($this->context, 'dom\\attr::rename'); + JIT\DomInstanceMethodJit::ensureProxy($this->context, 'dom\\element::rename'); + JIT\DomInstanceMethodJit::ensureProxy($this->context, 'dom\\htmlelement::rename'); + } + if ( + 'hasattribute' === $methodLc + || 'hasattributens' === $methodLc + || 'getattribute' === $methodLc + || 'getattributens' === $methodLc + || 'getattributenode' === $methodLc + || 'getattributenodens' === $methodLc + ) { + JIT\DomInstanceMethodJit::ensureProxy($this->context, 'dom\\element::'.$methodLc); + JIT\DomInstanceMethodJit::ensureProxy($this->context, 'dom\\htmlelement::'.$methodLc); + if ('hasattribute' !== $methodLc && 'hasattributens' !== $methodLc) { + JIT\DomInstanceMethodJit::ensureProxy($this->context, 'domelement::'.$methodLc); + } + } + if ('createattribute' === $methodLc || 'createattributens' === $methodLc) { + JIT\DomInstanceMethodJit::ensureProxy($this->context, 'dom\\xmldocument::'.$methodLc); + JIT\DomInstanceMethodJit::ensureProxy($this->context, 'dom\\document::'.$methodLc); + JIT\DomInstanceMethodJit::ensureProxy($this->context, 'domdocument::'.$methodLc); + } $runtimeCandidates = $this->buildRuntimeInstanceMethodCandidatesByClassId($methodLc); if ([] !== $runtimeCandidates) { $this->context->scope->toCall = new JIT\Call\RuntimeIndirectInstanceMethodCall( @@ -18906,6 +18931,48 @@ private function initJitMethodCall( ); $this->context->scope->args = [$receiverVar]; + return; + } + // Living Dom\* may be omitted from allClassNamesById — bind rename bridge (#27108). + // DomInstanceMethod only ships methodLc; VmDomJitDispatch::rename selects Attr/Element. + if ( + 'rename' === $methodLc + && $this->context->functionIsRegistered('dom\\attr::rename') + ) { + $this->context->scope->toCall = $this->context->resolveFunctionProxy( + 'dom\\attr::rename' + ); + $this->context->scope->args = [$receiverVar]; + + return; + } + if ( + ( + 'hasattribute' === $methodLc + || 'hasattributens' === $methodLc + || 'getattribute' === $methodLc + || 'getattributens' === $methodLc + || 'getattributenode' === $methodLc + || 'getattributenodens' === $methodLc + ) + && $this->context->functionIsRegistered('dom\\element::'.$methodLc) + ) { + $this->context->scope->toCall = $this->context->resolveFunctionProxy( + 'dom\\element::'.$methodLc + ); + $this->context->scope->args = [$receiverVar]; + + return; + } + if ( + ('createattribute' === $methodLc || 'createattributens' === $methodLc) + && $this->context->functionIsRegistered('dom\\xmldocument::'.$methodLc) + ) { + $this->context->scope->toCall = $this->context->resolveFunctionProxy( + 'dom\\xmldocument::'.$methodLc + ); + $this->context->scope->args = [$receiverVar]; + return; } } diff --git a/lib/JIT/Builtin/DomXmlDocumentCreateFromStringRuntime.php b/lib/JIT/Builtin/DomXmlDocumentCreateFromStringRuntime.php new file mode 100644 index 00000000000..5677ce3c416 --- /dev/null +++ b/lib/JIT/Builtin/DomXmlDocumentCreateFromStringRuntime.php @@ -0,0 +1,52 @@ + */ + private const COMPILED_HELPERS = [ + self::HELPER, + ]; + + public static function ensureLinked(Context $context): void + { + if (JitDomDocumentMethodKernel::shouldUse($context)) { + JitDomDocumentMethodKernel::ensureXmlDocumentCreateFromStringBridge($context); + + return; + } + + $strPtr = $context->getTypeFromString('__string__*'); + $i64 = $context->getTypeFromString('int64'); + $objPtr = $context->getTypeFromString('__object__*'); + JitVmHelperLink::ensureBridge( + $context, + self::ABI_NAME, + 'dom_xml_document_create_from_string_bridge', + [$strPtr, $i64], + $objPtr, + self::HELPER, + self::HELPER_PATH, + self::COMPILED_HELPERS, + '#27108' + ); + } +} diff --git a/lib/JIT/Call/DomXmlDocumentCreateFromString.php b/lib/JIT/Call/DomXmlDocumentCreateFromString.php new file mode 100644 index 00000000000..252fe376503 --- /dev/null +++ b/lib/JIT/Call/DomXmlDocumentCreateFromString.php @@ -0,0 +1,33 @@ + php-src ext/dom/php_dom.stub.php */ + public array $paramNames = ['source', 'options', 'overrideEncoding']; + + /** Static factory — no implicit $this. */ + public int $namedArgsReceiverPrefix = 0; + + public function call(Context $context, Variable ...$args): Value + { + return JitDomXmlDocumentCreateFromString::invoke($context, ...$args); + } +} diff --git a/lib/JIT/Context.php b/lib/JIT/Context.php index 2e240410792..7a7611ce238 100755 --- a/lib/JIT/Context.php +++ b/lib/JIT/Context.php @@ -1218,6 +1218,10 @@ private function defineBuiltins(int $loadType): void { $this->functionProxies['datetimezone::gettransitions'] = new Call\DateTimeZoneGetTransitions(); // Locale::canonicalize — avoid ExternalMethod null stub on user-script AOT (#20760). $this->functionProxies['locale::canonicalize'] = new \PHPCompiler\ext\intl\LocaleCanonicalize(); + // Dom\XMLDocument::createFromString — avoid ExternalMethod silent NULL (#27108). + if (CompilerVersion::supportsDomLivingStandardNamespace()) { + $this->functionProxies['dom\\xmldocument::createfromstring'] = new Call\DomXmlDocumentCreateFromString(); + } if (CompilerVersion::supportsDomTokenList()) { DomInstanceMethodJit::registerKnownProxies($this); } diff --git a/lib/JIT/DomInstanceMethodJit.php b/lib/JIT/DomInstanceMethodJit.php index de8983023e3..0d7e3c8eef5 100644 --- a/lib/JIT/DomInstanceMethodJit.php +++ b/lib/JIT/DomInstanceMethodJit.php @@ -153,6 +153,24 @@ private static function isUserScriptGenericDomMethod(string $proxyLc): bool 'dom\\htmldocument::getelementbyid' => true, 'dom\\htmldocument::savehtml' => true, 'domimplementation::createdocumenttype' => true, + // Dom\Attr::rename / Dom\Element::rename — php-src @implementation-alias (#21083, #27108). + 'dom\\attr::rename' => true, + 'dom\\element::rename' => true, + 'dom\\htmlelement::rename' => true, + // Living attribute APIs via VmDomInstanceInvoke DomRegistry path (#27108). + 'dom\\element::hasattribute' => true, + 'dom\\element::hasattributens' => true, + 'dom\\element::getattribute' => true, + 'dom\\element::getattributens' => true, + 'dom\\element::getattributenode' => true, + 'dom\\element::getattributenodens' => true, + 'dom\\htmlelement::hasattribute' => true, + 'dom\\htmlelement::getattribute' => true, + 'dom\\htmlelement::getattributens' => true, + 'dom\\htmlelement::getattributenode' => true, + 'dom\\document::createattribute' => true, + 'dom\\xmldocument::createattribute' => true, + 'dom\\htmldocument::createattribute' => true, ]; /** User-script AOT: nested VmDomInstanceInvoke JIT aborts — use VmClassMethod lowering (#15407, #17391). */ @@ -494,6 +512,22 @@ public static function ensureProxy(Context $context, string $proxyName): void || 'dom\\htmldocument::queryselectorall' === $lc || 'dom\\htmldocument::getelementbyid' === $lc || 'dom\\htmldocument::savehtml' === $lc + || 'dom\\attr::rename' === $lc + || 'dom\\element::rename' === $lc + || 'dom\\htmlelement::rename' === $lc + || 'dom\\element::hasattribute' === $lc + || 'dom\\element::hasattributens' === $lc + || 'dom\\element::getattribute' === $lc + || 'dom\\element::getattributens' === $lc + || 'dom\\element::getattributenode' === $lc + || 'dom\\element::getattributenodens' === $lc + || 'dom\\htmlelement::hasattribute' === $lc + || 'dom\\htmlelement::getattribute' === $lc + || 'dom\\htmlelement::getattributens' === $lc + || 'dom\\htmlelement::getattributenode' === $lc + || 'dom\\document::createattribute' === $lc + || 'dom\\xmldocument::createattribute' === $lc + || 'dom\\htmldocument::createattribute' === $lc ) { if (!preg_match('/^(dom\\\\[a-z0-9_]+)::([a-z0-9_]+)$/', $lc, $livingMatches)) { return; @@ -655,6 +689,22 @@ public static function registerKnownProxies(Context $context): void self::ensureProxy($context, 'dom\\htmldocument::queryselectorall'); self::ensureProxy($context, 'dom\\htmldocument::getelementbyid'); self::ensureProxy($context, 'dom\\htmldocument::savehtml'); + self::ensureProxy($context, 'dom\\attr::rename'); + self::ensureProxy($context, 'dom\\element::rename'); + self::ensureProxy($context, 'dom\\htmlelement::rename'); + self::ensureProxy($context, 'dom\\element::hasattribute'); + self::ensureProxy($context, 'dom\\element::hasattributens'); + self::ensureProxy($context, 'dom\\element::getattribute'); + self::ensureProxy($context, 'dom\\element::getattributens'); + self::ensureProxy($context, 'dom\\element::getattributenode'); + self::ensureProxy($context, 'dom\\element::getattributenodens'); + self::ensureProxy($context, 'dom\\htmlelement::hasattribute'); + self::ensureProxy($context, 'dom\\htmlelement::getattribute'); + self::ensureProxy($context, 'dom\\htmlelement::getattributens'); + self::ensureProxy($context, 'dom\\htmlelement::getattributenode'); + self::ensureProxy($context, 'dom\\document::createattribute'); + self::ensureProxy($context, 'dom\\xmldocument::createattribute'); + self::ensureProxy($context, 'dom\\htmldocument::createattribute'); return; } diff --git a/test/selfhost/compiler_lib_spine_smoke/main.php b/test/selfhost/compiler_lib_spine_smoke/main.php index 06b48ded1af..cea5a80bdd6 100644 --- a/test/selfhost/compiler_lib_spine_smoke/main.php +++ b/test/selfhost/compiler_lib_spine_smoke/main.php @@ -334,6 +334,7 @@ require_once __DIR__.'/../../../ext/dom/DomLoadHTMLJitHelper.php'; require_once __DIR__.'/../../../ext/dom/DomLoadJitHelper.php'; require_once __DIR__.'/../../../ext/dom/DomLoadXMLJitHelper.php'; +require_once __DIR__.'/../../../ext/dom/DomXmlDocumentCreateFromStringJitHelper.php'; require_once __DIR__.'/../../../ext/dom/DomNodeChildPropertyJitHelper.php'; require_once __DIR__.'/../../../ext/dom/DomNodeListItemJitHelper.php'; require_once __DIR__.'/../../../ext/dom/DomNodePropertySupport.php'; @@ -436,6 +437,7 @@ require_once __DIR__.'/../../../ext/dom/JitDomLoadHTMLUserScript.php'; require_once __DIR__.'/../../../ext/dom/JitDomLoadUserScript.php'; require_once __DIR__.'/../../../ext/dom/JitDomLoadXML.php'; +require_once __DIR__.'/../../../ext/dom/JitDomXmlDocumentCreateFromString.php'; require_once __DIR__.'/../../../ext/dom/JitDomLoadXMLUserScript.php'; require_once __DIR__.'/../../../ext/dom/JitDomNodeChildProperty.php'; require_once __DIR__.'/../../../ext/dom/JitDomNodeIsConnected.php'; @@ -4829,6 +4831,7 @@ require_once __DIR__.'/../../../lib/JIT/Builtin/DomLoadHTMLRuntime.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/DomLoadRuntime.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/DomLoadXMLRuntime.php'; +require_once __DIR__.'/../../../lib/JIT/Builtin/DomXmlDocumentCreateFromStringRuntime.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/DomNodeChildNodeMutationRuntime.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/DomNodeChildPropertyRuntime.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/DomNodeIsConnectedRuntime.php'; @@ -5413,6 +5416,7 @@ require_once __DIR__.'/../../../lib/JIT/Call/DomDocumentLoadHTML.php'; require_once __DIR__.'/../../../lib/JIT/Call/DomDocumentLoadHTMLFile.php'; require_once __DIR__.'/../../../lib/JIT/Call/DomDocumentLoadXML.php'; +require_once __DIR__.'/../../../lib/JIT/Call/DomXmlDocumentCreateFromString.php'; require_once __DIR__.'/../../../lib/JIT/Call/DomDocumentNormalizeDocument.php'; require_once __DIR__.'/../../../lib/JIT/Call/DomDocumentSaveHTML.php'; require_once __DIR__.'/../../../lib/JIT/Call/DomDocumentSaveHTMLFile.php'; diff --git a/test/unit/DomInstanceMethodJitTest.php b/test/unit/DomInstanceMethodJitTest.php index 9d76cdad2af..9444c70e8dd 100644 --- a/test/unit/DomInstanceMethodJitTest.php +++ b/test/unit/DomInstanceMethodJitTest.php @@ -18,6 +18,20 @@ public function testRecognizesDomInstanceMethodProxyNames(): void $this->assertFalse(DomInstanceMethodJit::isDomInstanceMethodProxy('splobjectstorage::attach')); } + public function testRecognizesLivingAttrRenameProxyUnderUserScriptAot(): void + { + // USER_SCRIPT_DIRECT_METHODS path (UserScriptAotEnv) — #27108. + putenv('PHP_COMPILER_AOT_USER_SCRIPT=1'); + try { + $this->assertTrue(DomInstanceMethodJit::shouldDeferToVmClassMethodLowering()); + $this->assertTrue(DomInstanceMethodJit::isDomInstanceMethodProxy('dom\\attr::rename')); + $this->assertTrue(DomInstanceMethodJit::isDomInstanceMethodProxy('dom\\element::rename')); + $this->assertFalse(DomInstanceMethodJit::isDomInstanceMethodProxy('object::rename')); + } finally { + putenv('PHP_COMPILER_AOT_USER_SCRIPT'); + } + } + public function testEnsureProxyRegistersCallableLowering(): void { $this->markTestSkipped('loadJitContext() is too heavy for default unit gate (#17130)'); From 7b39736a29a2c094cbb57207ce18474d6790d06b Mon Sep 17 00:00:00 2001 From: PurHur Date: Mon, 3 Aug 2026 07:48:00 +0000 Subject: [PATCH 2/3] Stdlib: document NestedJIT ObjectEntry vs thin AOT layout for #27108 Clarify why createFromString AOT still segfaults on get_class/documentElement: ObjectEntry* is not a thin __object__ (class_id load/remap both crash). Co-authored-by: Cursor --- ext/dom/DomXmlDocumentCreateFromStringJitHelper.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ext/dom/DomXmlDocumentCreateFromStringJitHelper.php b/ext/dom/DomXmlDocumentCreateFromStringJitHelper.php index ce906c9b861..fe70f8fae9d 100644 --- a/ext/dom/DomXmlDocumentCreateFromStringJitHelper.php +++ b/ext/dom/DomXmlDocumentCreateFromStringJitHelper.php @@ -13,8 +13,10 @@ * * php-src: ext/dom/xml_document.c — load_from_helper(DOM_LOAD_STRING) * - * Note: thin-AOT get_class / property fetch on the returned living document still needs - * Dom\ class_id sync or LLVM materialization (peer JitDomLoadXMLUserScript) — see #27108. + * Note: thin-AOT get_class / documentElement still segfault — NestedJIT ObjectEntry* is + * not thin __object__ layout (class_id load crashes; rewriting class_id also crashes). + * Next: LLVM materialize Dom\XMLDocument (peer JitDomLoadXMLUserScript) + DomRegistry + * Attr slots for rename, or NestedJIT-only get_class/property with no thin class_id (#27108). */ final class DomXmlDocumentCreateFromStringJitHelper { From 0bb340c98b3f5500a4b1505a8d04a0ab4d9aefe4 Mon Sep 17 00:00:00 2001 From: PurHur Date: Mon, 3 Aug 2026 09:17:02 +0000 Subject: [PATCH 3/3] Stdlib: AOT Dom\Attr::rename + XMLDocument::createFromString (#27108) Materialize living Dom\XMLDocument/Attr in thin AOT (avoid NestedJIT ObjectEntry), LLVM rename with cache rekey/dup DOMException, and fix method_exists type-tag masking plus Attr $value assigns that shared a detached multi-class property box. Co-authored-by: Cursor --- docs/bootstrap-inventory.md | 465 ++++++++++-------- ext/dom/DomParseSimpleXmlJitHelper.php | 31 ++ ext/dom/DomUserScriptAttributeCacheLlvm.php | 142 +++++- ext/dom/JitDomAttrRename.php | 190 +++++++ ext/dom/JitDomAttributeNodeNS.php | 94 +++- ext/dom/JitDomCreateElement.php | 42 +- ext/dom/JitDomDocumentElement.php | 20 +- ext/dom/JitDomElementTextContent.php | 62 ++- ext/dom/JitDomLoadXMLUserScript.php | 17 +- ext/dom/JitDomXmlDocumentCreateFromString.php | 151 +++++- ext/standard/JitMethodExists.php | 16 +- .../Type/ObjectInstancePropertyLlvm.php | 24 +- lib/JIT/Builtin/Type/Object_.php | 16 + lib/JIT/Call/DomAttrRename.php | 23 + lib/JIT/Call/DomElementGetAttribute.php | 32 +- lib/JIT/Call/DomElementGetAttributeNS.php | 60 +++ lib/JIT/Call/DomElementHasAttribute.php | 43 ++ lib/JIT/DomInstanceMethodJit.php | 76 ++- lib/JIT/TryCatchHelper.php | 15 +- 19 files changed, 1211 insertions(+), 308 deletions(-) create mode 100644 ext/dom/JitDomAttrRename.php create mode 100644 lib/JIT/Call/DomAttrRename.php create mode 100644 lib/JIT/Call/DomElementGetAttributeNS.php create mode 100644 lib/JIT/Call/DomElementHasAttribute.php diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index b0c2409888d..8770e43e286 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -8,11 +8,11 @@ Regenerate: `php script/bootstrap-inventory.php` | Metric | Count | |--------|------:| -| PHP files on vm.php path | 6866 | -| Phase A inventory files (M2 ratio SSOT) | 6866 | +| PHP files on vm.php path | 6874 | +| Phase A inventory files (M2 ratio SSOT) | 6874 | | Phase A ratio-deferred paths | 0 | | Source constructs flagged (blockers) | 0 | -| Source constructs flagged (warnings) | 22139 | +| Source constructs flagged (warnings) | 22166 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -321,6 +321,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `ext/dom/DomXPathEvaluateJitHelper.php` | 0 | 1 | | `ext/dom/DomXPathQueryJitHelper.php` | 0 | 1 | | `ext/dom/DomXPathSerializeDeny.php` | 0 | 3 | +| `ext/dom/DomXmlDocumentCreateFromStringJitHelper.php` | 0 | 2 | | `ext/dom/ElementAppendChild.php` | 0 | 1 | | `ext/dom/ElementClosest.php` | 0 | 3 | | `ext/dom/ElementConstruct.php` | 0 | 2 | @@ -375,6 +376,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `ext/dom/ImplementationHasFeature.php` | 0 | 1 | | `ext/dom/JitDomAppendChild.php` | 0 | 2 | | `ext/dom/JitDomAppendChildUserScript.php` | 0 | 3 | +| `ext/dom/JitDomAttrRename.php` | 0 | 3 | | `ext/dom/JitDomAttributeNodeNS.php` | 0 | 3 | | `ext/dom/JitDomC14N.php` | 0 | 1 | | `ext/dom/JitDomCreateComment.php` | 0 | 2 | @@ -386,7 +388,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `ext/dom/JitDomDocumentElement.php` | 0 | 9 | | `ext/dom/JitDomDocumentMethodKernel.php` | 0 | 1 | | `ext/dom/JitDomElementNavigationProperty.php` | 0 | 3 | -| `ext/dom/JitDomElementTextContent.php` | 0 | 7 | +| `ext/dom/JitDomElementTextContent.php` | 0 | 8 | | `ext/dom/JitDomGetElementById.php` | 0 | 3 | | `ext/dom/JitDomGetElementsByTagName.php` | 0 | 1 | | `ext/dom/JitDomGetElementsByTagNameUserScript.php` | 0 | 2 | @@ -419,6 +421,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `ext/dom/JitDomXPathEvaluateUserScript.php` | 0 | 1 | | `ext/dom/JitDomXPathQuery.php` | 0 | 1 | | `ext/dom/JitDomXPathQueryUserScript.php` | 0 | 5 | +| `ext/dom/JitDomXmlDocumentCreateFromString.php` | 0 | 3 | | `ext/dom/LivingImplementationCreateDocument.php` | 0 | 3 | | `ext/dom/LivingImplementationCreateDocumentType.php` | 0 | 3 | | `ext/dom/LivingPrivateConstruct.php` | 0 | 1 | @@ -494,7 +497,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `ext/dom/VmDom.php` | 0 | 417 | | `ext/dom/VmDomCollectionDimension.php` | 0 | 1 | | `ext/dom/VmDomInstanceInvoke.php` | 0 | 2 | -| `ext/dom/VmDomJitDispatch.php` | 0 | 55 | +| `ext/dom/VmDomJitDispatch.php` | 0 | 60 | | `ext/dom/VmDomJitFrame.php` | 0 | 1 | | `ext/dom/VmDomLiving.php` | 0 | 21 | | `ext/dom/VmDomSimpleXmlBridge.php` | 0 | 4 | @@ -5112,6 +5115,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `lib/JIT/Builtin/DomSyncElementIdMapRuntime.php` | 0 | 1 | | `lib/JIT/Builtin/DomXPathEvaluateRuntime.php` | 0 | 1 | | `lib/JIT/Builtin/DomXPathQueryRuntime.php` | 0 | 1 | +| `lib/JIT/Builtin/DomXmlDocumentCreateFromStringRuntime.php` | 0 | 1 | | `lib/JIT/Builtin/EmbedObEchoBridge.php` | 0 | 1 | | `lib/JIT/Builtin/EmbedObOutput.php` | 0 | 1 | | `lib/JIT/Builtin/EnumCasesRuntime.php` | 0 | 1 | @@ -5670,6 +5674,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `lib/JIT/Call/DateTimeSetTimezone.php` | 0 | 1 | | `lib/JIT/Call/DateTimeZoneConstruct.php` | 0 | 1 | | `lib/JIT/Call/DateTimeZoneGetTransitions.php` | 0 | 1 | +| `lib/JIT/Call/DomAttrRename.php` | 0 | 1 | | `lib/JIT/Call/DomDocumentAppendChild.php` | 0 | 1 | | `lib/JIT/Call/DomDocumentCreateAttribute.php` | 0 | 1 | | `lib/JIT/Call/DomDocumentCreateAttributeNS.php` | 0 | 1 | @@ -5689,8 +5694,10 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `lib/JIT/Call/DomDocumentSaveHTMLFile.php` | 0 | 1 | | `lib/JIT/Call/DomDocumentSaveXML.php` | 0 | 1 | | `lib/JIT/Call/DomElementGetAttribute.php` | 0 | 1 | +| `lib/JIT/Call/DomElementGetAttributeNS.php` | 0 | 1 | | `lib/JIT/Call/DomElementGetAttributeNode.php` | 0 | 1 | | `lib/JIT/Call/DomElementGetAttributeNodeNS.php` | 0 | 1 | +| `lib/JIT/Call/DomElementHasAttribute.php` | 0 | 1 | | `lib/JIT/Call/DomElementRemoveAttribute.php` | 0 | 1 | | `lib/JIT/Call/DomElementSetAttribute.php` | 0 | 1 | | `lib/JIT/Call/DomElementSetAttributeNode.php` | 0 | 1 | @@ -5719,6 +5726,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `lib/JIT/Call/DomNodeReplaceWith.php` | 0 | 1 | | `lib/JIT/Call/DomXPathEvaluate.php` | 0 | 1 | | `lib/JIT/Call/DomXPathQuery.php` | 0 | 1 | +| `lib/JIT/Call/DomXmlDocumentCreateFromString.php` | 0 | 1 | | `lib/JIT/Call/ExceptionConstruct.php` | 0 | 6 | | `lib/JIT/Call/ExceptionGetCode.php` | 0 | 1 | | `lib/JIT/Call/ExceptionGetMessage.php` | 0 | 1 | @@ -5872,10 +5880,10 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `lib/JIT/CoalesceHelper.php` | 0 | 1 | | `lib/JIT/CompileCache.php` | 0 | 1 | | `lib/JIT/CompilerOperandChainNative.php` | 0 | 3 | -| `lib/JIT/Context.php` | 0 | 122 | +| `lib/JIT/Context.php` | 0 | 123 | | `lib/JIT/DatePeriodForeachSnapshot.php` | 0 | 3 | | `lib/JIT/DnfParamCheck.php` | 0 | 28 | -| `lib/JIT/DomInstanceMethodJit.php` | 0 | 56 | +| `lib/JIT/DomInstanceMethodJit.php` | 0 | 62 | | `lib/JIT/DynamicObjectReadonlyGuard.php` | 0 | 1 | | `lib/JIT/DynamicPropertyDeprecationGuard.php` | 0 | 1 | | `lib/JIT/EmitTuMode.php` | 0 | 1 | @@ -6022,7 +6030,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `lib/JIT/StaticPropertyVisibilityJitGuard.php` | 0 | 2 | | `lib/JIT/StringOffsetHelper.php` | 0 | 1 | | `lib/JIT/SuperglobalInit.php` | 0 | 5 | -| `lib/JIT/TryCatchHelper.php` | 0 | 10 | +| `lib/JIT/TryCatchHelper.php` | 0 | 11 | | `lib/JIT/TryCatchState.php` | 0 | 2 | | `lib/JIT/TypeCheck.php` | 0 | 4 | | `lib/JIT/TypedPropertyUninitGuard.php` | 0 | 1 | @@ -8773,7 +8781,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/dom/DomParseSimpleXmlJitHelper.php` **Warnings** (review for bootstrap subset): -- 20 class method(s) +- 21 class method(s) ### `ext/dom/DomRegistry.php` @@ -8819,7 +8827,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/dom/DomUserScriptAttributeCacheLlvm.php` **Warnings** (review for bootstrap subset): -- 7 class method(s) +- 12 class method(s) ### `ext/dom/DomUserScriptElementCacheLlvm.php` @@ -8853,6 +8861,12 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new Exception (line 30) - 4 class method(s) +### `ext/dom/DomXmlDocumentCreateFromStringJitHelper.php` + +**Warnings** (review for bootstrap subset): +- new DOMException (line 30) +- 1 class method(s) + ### `ext/dom/ElementAppendChild.php` **Warnings** (review for bootstrap subset): @@ -9158,12 +9172,19 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new JITVariable (line 39) - 3 class method(s) +### `ext/dom/JitDomAttrRename.php` + +**Warnings** (review for bootstrap subset): +- new ArgumentCountError (line 60) +- new JITVariable (line 170) +- 7 class method(s) + ### `ext/dom/JitDomAttributeNodeNS.php` **Warnings** (review for bootstrap subset): -- new JITVariable (line 421) -- new JITVariable (line 436) -- 31 class method(s) +- new JITVariable (line 465) +- new JITVariable (line 484) +- 32 class method(s) ### `ext/dom/JitDomC14N.php` @@ -9195,12 +9216,12 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag **Warnings** (review for bootstrap subset): - new JITVariable (line 112) -- new JITVariable (line 196) -- new JITVariable (line 223) -- new JITVariable (line 308) -- new JITVariable (line 328) -- new JITVariable (line 350) -- new JITVariable (line 352) +- new JITVariable (line 201) +- new JITVariable (line 229) +- new JITVariable (line 314) +- new JITVariable (line 334) +- new JITVariable (line 356) +- new JITVariable (line 358) - 16 class method(s) ### `ext/dom/JitDomCreateElementNS.php` @@ -9222,20 +9243,20 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/dom/JitDomDocumentElement.php` **Warnings** (review for bootstrap subset): -- new JITVariable (line 102) -- new JITVariable (line 114) -- new JITVariable (line 120) -- new JITVariable (line 166) -- new JITVariable (line 177) -- new JITVariable (line 221) -- new JITVariable (line 227) -- new JITVariable (line 253) +- new JITVariable (line 106) +- new JITVariable (line 118) +- new JITVariable (line 124) +- new JITVariable (line 170) +- new JITVariable (line 181) +- new JITVariable (line 225) +- new JITVariable (line 231) +- new JITVariable (line 257) - 9 class method(s) ### `ext/dom/JitDomDocumentMethodKernel.php` **Warnings** (review for bootstrap subset): -- 80 class method(s) +- 81 class method(s) ### `ext/dom/JitDomElementNavigationProperty.php` @@ -9248,12 +9269,13 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag **Warnings** (review for bootstrap subset): - new JITVariable (line 72) -- new JITVariable (line 130) -- new JITVariable (line 204) -- new JITVariable (line 234) -- new JITVariable (line 245) -- new JITVariable (line 261) -- 8 class method(s) +- new JITVariable (line 154) +- new JITVariable (line 180) +- new JITVariable (line 254) +- new JITVariable (line 284) +- new JITVariable (line 295) +- new JITVariable (line 311) +- 9 class method(s) ### `ext/dom/JitDomGetElementById.php` @@ -9326,11 +9348,11 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/dom/JitDomLoadXMLUserScript.php` **Warnings** (review for bootstrap subset): -- new JITVariable (line 155) -- new JITVariable (line 182) -- new JITVariable (line 245) -- new JITVariable (line 262) -- 14 class method(s) +- new JITVariable (line 170) +- new JITVariable (line 197) +- new JITVariable (line 260) +- new JITVariable (line 277) +- 16 class method(s) ### `ext/dom/JitDomNodeChildProperty.php` @@ -9439,6 +9461,13 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - 6 class method(s) - 1 closure(s) +### `ext/dom/JitDomXmlDocumentCreateFromString.php` + +**Warnings** (review for bootstrap subset): +- new ArgumentCountError (line 40) +- new JITVariable (line 175) +- 5 class method(s) + ### `ext/dom/LivingImplementationCreateDocument.php` **Warnings** (review for bootstrap subset): @@ -10241,7 +10270,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/dom/VmDomInstanceInvoke.php` **Warnings** (review for bootstrap subset): -- new Error (line 120) +- new Error (line 125) - 6 class method(s) ### `ext/dom/VmDomJitDispatch.php` @@ -10256,52 +10285,57 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new Variable (line 192) - new Variable (line 210) - new Variable (line 230) -- new ArgumentCountError (line 253) -- new Variable (line 262) -- new ArgumentCountError (line 274) -- new Variable (line 279) -- new Variable (line 301) -- new Variable (line 313) -- new Variable (line 325) -- new Variable (line 337) -- new Variable (line 349) -- new Variable (line 375) -- new Variable (line 387) -- new ArgumentCountError (line 470) -- new ArgumentCountError (line 475) -- new Variable (line 498) -- new Variable (line 516) -- new Variable (line 555) -- new Variable (line 603) -- new Variable (line 632) -- new Variable (line 659) -- new ArgumentCountError (line 673) -- new Variable (line 677) -- new Variable (line 689) -- new Variable (line 701) -- new Variable (line 713) -- new Error (line 727) -- new Error (line 739) -- new Variable (line 742) -- new Error (line 761) -- new Variable (line 773) -- new Variable (line 791) -- new Error (line 808) -- new Variable (line 824) -- new Variable (line 847) -- new Variable (line 868) -- new Variable (line 895) -- new Variable (line 921) -- new Variable (line 933) -- new Variable (line 947) -- new Variable (line 959) -- new Variable (line 1009) -- new Variable (line 1021) -- new Variable (line 1038) -- new Variable (line 1063) -- new Variable (line 1105) -- new ArgumentCountError (line 1154) -- 67 class method(s) +- new ArgumentCountError (line 282) +- new Variable (line 291) +- new ArgumentCountError (line 303) +- new Variable (line 308) +- new Variable (line 330) +- new Variable (line 342) +- new Variable (line 354) +- new Variable (line 366) +- new Variable (line 378) +- new Variable (line 404) +- new Variable (line 416) +- new ArgumentCountError (line 499) +- new ArgumentCountError (line 504) +- new Variable (line 526) +- new Variable (line 547) +- new Variable (line 560) +- new Variable (line 578) +- new Variable (line 617) +- new Variable (line 665) +- new Variable (line 694) +- new Variable (line 721) +- new ArgumentCountError (line 735) +- new Variable (line 739) +- new Variable (line 751) +- new Variable (line 763) +- new Variable (line 775) +- new Error (line 789) +- new Error (line 801) +- new Variable (line 804) +- new Error (line 823) +- new Variable (line 835) +- new Variable (line 853) +- new Error (line 870) +- new Variable (line 886) +- new Variable (line 909) +- new Variable (line 930) +- new Variable (line 957) +- new ArgumentCountError (line 972) +- new Error (line 985) +- new Variable (line 987) +- new Variable (line 1013) +- new Variable (line 1025) +- new Variable (line 1039) +- new Variable (line 1051) +- new Variable (line 1101) +- new Variable (line 1113) +- new Variable (line 1130) +- new Variable (line 1155) +- new Variable (line 1197) +- new ArgumentCountError (line 1246) +- 72 class method(s) ### `ext/dom/VmDomJitFrame.php` @@ -10327,9 +10361,9 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new DOMException (line 705) - new Variable (line 711) - new Variable (line 745) -- new ObjectEntry (line 1448) -- new HashTable (line 1472) -- new Variable (line 1474) +- new ObjectEntry (line 1450) +- new HashTable (line 1474) +- new Variable (line 1476) - 59 class method(s) - 1 closure(s) @@ -28204,7 +28238,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/standard/JitMethodExists.php` **Warnings** (review for bootstrap subset): -- new JITVariable (line 101) +- new JITVariable (line 103) - 10 class method(s) ### `ext/standard/JitMimeContentType.php` @@ -44210,20 +44244,20 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new Variable (line 18461) - new VM\PropertyIsInitializedHandler (line 18558) - new JIT\Call\RuntimeIndirectInstanceMethodCall (line 18614) -- new JIT\Call\RuntimeIndirectInstanceMethodCall (line 18958) -- new JIT\Call\RuntimeIndirectInstanceMethodCall (line 19128) -- new JIT\Call\VmCoerceVariableToString (line 19255) -- new JIT\Call\NoOpConstruct (line 19427) -- new JIT\Call\IncludePathResolverResolve (line 19597) -- new Operand\Literal (line 19684) -- new Operand\Literal (line 19687) -- new JIT\Call\NestedJitCompileScopeIsActiveTrue (line 19730) -- new JIT\Call\RuntimeIndirectStaticMethodCall (line 19877) -- new VM (line 20975) -- new VM\Variable (line 20984) -- new Variable (line 21005) -- new Variable (line 21051) -- new Variable (line 21759) +- new JIT\Call\RuntimeIndirectInstanceMethodCall (line 18983) +- new JIT\Call\RuntimeIndirectInstanceMethodCall (line 19195) +- new JIT\Call\VmCoerceVariableToString (line 19322) +- new JIT\Call\NoOpConstruct (line 19494) +- new JIT\Call\IncludePathResolverResolve (line 19664) +- new Operand\Literal (line 19751) +- new Operand\Literal (line 19754) +- new JIT\Call\NestedJitCompileScopeIsActiveTrue (line 19797) +- new JIT\Call\RuntimeIndirectStaticMethodCall (line 19944) +- new VM (line 21042) +- new VM\Variable (line 21051) +- new Variable (line 21072) +- new Variable (line 21118) +- new Variable (line 21826) - 418 class method(s) - 20 closure(s) @@ -44947,6 +44981,11 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag **Warnings** (review for bootstrap subset): - 1 class method(s) +### `lib/JIT/Builtin/DomXmlDocumentCreateFromStringRuntime.php` + +**Warnings** (review for bootstrap subset): +- 1 class method(s) + ### `lib/JIT/Builtin/EmbedObEchoBridge.php` **Warnings** (review for bootstrap subset): @@ -47531,7 +47570,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new Variable (line 97) - new Variable (line 118) - new Variable (line 150) -- new Variable (line 260) +- new Variable (line 279) - 5 class method(s) ### `lib/JIT/Builtin/Type/ObjectStaticPropertyInitLlvm.php` @@ -47563,25 +47602,25 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new Variable (line 2558) - new Variable (line 2571) - new Variable (line 2770) -- new VMVariable (line 3961) -- new VMVariable (line 3981) -- new VMVariable (line 3991) -- new VMVariable (line 4011) -- new ReflectionClassConstant (line 5702) -- new Variable (line 6121) -- new Variable (line 6147) -- new Variable (line 6155) -- new Variable (line 6178) -- new Variable (line 6204) -- new Variable (line 6212) -- new Variable (line 6286) -- new Literal (line 6303) -- new Literal (line 6308) -- new Literal (line 6313) -- new Variable (line 6324) -- new Variable (line 6357) -- new Variable (line 6380) -- new Variable (line 6744) +- new VMVariable (line 3977) +- new VMVariable (line 3997) +- new VMVariable (line 4007) +- new VMVariable (line 4027) +- new ReflectionClassConstant (line 5718) +- new Variable (line 6137) +- new Variable (line 6163) +- new Variable (line 6171) +- new Variable (line 6194) +- new Variable (line 6220) +- new Variable (line 6228) +- new Variable (line 6302) +- new Literal (line 6319) +- new Literal (line 6324) +- new Literal (line 6329) +- new Variable (line 6340) +- new Variable (line 6373) +- new Variable (line 6396) +- new Variable (line 6760) - 291 class method(s) - 8 closure(s) @@ -47867,6 +47906,11 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag **Warnings** (review for bootstrap subset): - 1 class method(s) +### `lib/JIT/Call/DomAttrRename.php` + +**Warnings** (review for bootstrap subset): +- 1 class method(s) + ### `lib/JIT/Call/DomDocumentAppendChild.php` **Warnings** (review for bootstrap subset): @@ -47960,7 +48004,12 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/JIT/Call/DomElementGetAttribute.php` **Warnings** (review for bootstrap subset): -- 1 class method(s) +- 2 class method(s) + +### `lib/JIT/Call/DomElementGetAttributeNS.php` + +**Warnings** (review for bootstrap subset): +- 2 class method(s) ### `lib/JIT/Call/DomElementGetAttributeNode.php` @@ -47972,6 +48021,11 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag **Warnings** (review for bootstrap subset): - 1 class method(s) +### `lib/JIT/Call/DomElementHasAttribute.php` + +**Warnings** (review for bootstrap subset): +- 1 class method(s) + ### `lib/JIT/Call/DomElementRemoveAttribute.php` **Warnings** (review for bootstrap subset): @@ -48113,6 +48167,11 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag **Warnings** (review for bootstrap subset): - 1 class method(s) +### `lib/JIT/Call/DomXmlDocumentCreateFromString.php` + +**Warnings** (review for bootstrap subset): +- 1 class method(s) + ### `lib/JIT/Call/ExceptionConstruct.php` **Warnings** (review for bootstrap subset): @@ -49091,19 +49150,20 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new Call\DateTimeModify (line 1227) - new Call\DateTimeModify (line 1228) - new Call\DateTimeZoneGetTransitions (line 1230) -- new Result (line 1598) -- new Result (line 1620) -- new Variable (line 2298) -- new Variable (line 2435) -- new Variable (line 2689) -- new Variable (line 2747) -- new VMVariable (line 2909) -- new VMVariable (line 2925) -- new VMVariable (line 2931) -- new VMVariable (line 2937) -- new VMVariable (line 2948) -- new Variable (line 2978) -- new Variable (line 3021) +- new Call\DomXmlDocumentCreateFromString (line 1235) +- new Result (line 1602) +- new Result (line 1624) +- new Variable (line 2302) +- new Variable (line 2439) +- new Variable (line 2693) +- new Variable (line 2751) +- new VMVariable (line 2913) +- new VMVariable (line 2929) +- new VMVariable (line 2935) +- new VMVariable (line 2941) +- new VMVariable (line 2952) +- new Variable (line 2982) +- new Variable (line 3025) - 106 class method(s) - 32 closure(s) @@ -49149,61 +49209,67 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/JIT/DomInstanceMethodJit.php` **Warnings** (review for bootstrap subset): -- new Call\DomDocumentCreateElement (line 169) -- new Call\DomDocumentCreateElementNS (line 174) -- new Call\DomDocumentCreateComment (line 179) -- new Call\DomDocumentLoad (line 184) -- new Call\DomDocumentLoadHTML (line 189) -- new Call\DomDocumentLoadHTMLFile (line 194) -- new Call\DomDocumentGetElementById (line 199) -- new Call\DomDocumentImportNode (line 204) -- new Call\DomInstanceMethod (line 218) -- new Call\DomElementGetAttribute (line 226) -- new Call\DomElementSetAttribute (line 231) -- new Call\DomElementRemoveAttribute (line 236) -- new Call\DomElementGetAttributeNode (line 241) -- new Call\DomElementGetAttributeNodeNS (line 246) -- new Call\DomElementSetAttributeNodeNS (line 251) -- new Call\DomDocumentCreateAttributeNS (line 256) -- new Call\DomDocumentCreateAttribute (line 261) -- new Call\DomElementSetAttributeNode (line 266) -- new Call\DomDocumentLoadXML (line 271) -- new Call\DomDocumentSaveXML (line 276) -- new Call\DomDocumentSaveHTML (line 281) -- new Call\DomDocumentSaveHTMLFile (line 286) -- new Call\DomDocumentGetElementsByTagName (line 291) -- new Call\DomDocumentAppendChild (line 296) -- new Call\DomNodeAppendChild (line 301) -- new Call\DomNodeAppendChild (line 306) -- new Call\DomNodeAppendChild (line 311) -- new Call\DomNodeAppend (line 320) -- new Call\DomNodePrepend (line 329) -- new Call\DomNodeReplaceChildren (line 338) -- new Call\DomElementToggleAttribute (line 343) -- new Call\DomNodeContains (line 348) -- new Call\DomNodeCompareDocumentPosition (line 353) -- new Call\DomNodeGetRootNode (line 358) -- new Call\DomNodeIsEqualNode (line 363) -- new Call\DomNodeIsSameNode (line 368) -- new Call\DomNodeC14N (line 373) -- new Call\DomNodeRemoveChild (line 378) -- new Call\DomNodeReplaceChild (line 383) -- new Call\DomNodeInsertBefore (line 388) -- new Call\DomNodeAfter (line 398) -- new Call\DomNodeBefore (line 408) -- new Call\DomNodeReplaceWith (line 418) -- new Call\DomNodeChildRemove (line 428) -- new Call\DomNodeNormalize (line 437) -- new Call\DomDocumentNormalizeDocument (line 442) -- new Call\DomDocumentCreateDocumentFragment (line 447) -- new Call\DomImplementationCreateDocumentType (line 452) -- new Call\DomXPathQuery (line 457) -- new Call\DomXPathEvaluate (line 462) -- new Call\DomNodeListItem (line 467) -- new Call\DomInstanceMethod (line 489) -- new Call\DomInstanceMethod (line 501) -- new Call\DomInstanceMethod (line 515) -- new Call\DomInstanceMethod (line 536) +- new Call\DomDocumentCreateElement (line 189) +- new Call\DomDocumentCreateElementNS (line 194) +- new Call\DomDocumentCreateComment (line 199) +- new Call\DomDocumentLoad (line 204) +- new Call\DomDocumentLoadHTML (line 209) +- new Call\DomDocumentLoadHTMLFile (line 214) +- new Call\DomDocumentGetElementById (line 219) +- new Call\DomDocumentImportNode (line 224) +- new Call\DomInstanceMethod (line 238) +- new Call\DomElementGetAttributeNode (line 249) +- new Call\DomElementGetAttribute (line 258) +- new Call\DomElementGetAttributeNS (line 266) +- new Call\DomElementHasAttribute (line 274) +- new Call\DomAttrRename (line 279) +- new Call\DomDocumentCreateAttribute (line 288) +- new Call\DomElementGetAttributeNode (line 293) +- new Call\DomElementGetAttribute (line 298) +- new Call\DomElementSetAttribute (line 303) +- new Call\DomElementRemoveAttribute (line 308) +- new Call\DomElementGetAttributeNodeNS (line 313) +- new Call\DomElementSetAttributeNodeNS (line 318) +- new Call\DomDocumentCreateAttributeNS (line 323) +- new Call\DomDocumentCreateAttribute (line 328) +- new Call\DomElementSetAttributeNode (line 333) +- new Call\DomDocumentLoadXML (line 338) +- new Call\DomDocumentSaveXML (line 343) +- new Call\DomDocumentSaveHTML (line 348) +- new Call\DomDocumentSaveHTMLFile (line 353) +- new Call\DomDocumentGetElementsByTagName (line 358) +- new Call\DomDocumentAppendChild (line 363) +- new Call\DomNodeAppendChild (line 368) +- new Call\DomNodeAppendChild (line 373) +- new Call\DomNodeAppendChild (line 378) +- new Call\DomNodeAppend (line 387) +- new Call\DomNodePrepend (line 396) +- new Call\DomNodeReplaceChildren (line 405) +- new Call\DomElementToggleAttribute (line 410) +- new Call\DomNodeContains (line 415) +- new Call\DomNodeCompareDocumentPosition (line 420) +- new Call\DomNodeGetRootNode (line 425) +- new Call\DomNodeIsEqualNode (line 430) +- new Call\DomNodeIsSameNode (line 435) +- new Call\DomNodeC14N (line 440) +- new Call\DomNodeRemoveChild (line 445) +- new Call\DomNodeReplaceChild (line 450) +- new Call\DomNodeInsertBefore (line 455) +- new Call\DomNodeAfter (line 465) +- new Call\DomNodeBefore (line 475) +- new Call\DomNodeReplaceWith (line 485) +- new Call\DomNodeChildRemove (line 495) +- new Call\DomNodeNormalize (line 504) +- new Call\DomDocumentNormalizeDocument (line 509) +- new Call\DomDocumentCreateDocumentFragment (line 514) +- new Call\DomImplementationCreateDocumentType (line 519) +- new Call\DomXPathQuery (line 524) +- new Call\DomXPathEvaluate (line 529) +- new Call\DomNodeListItem (line 534) +- new Call\DomInstanceMethod (line 556) +- new Call\DomInstanceMethod (line 573) +- new Call\DomInstanceMethod (line 587) +- new Call\DomInstanceMethod (line 608) - 7 class method(s) ### `lib/JIT/DynamicObjectReadonlyGuard.php` @@ -50151,12 +50217,13 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new TryCatchHandler (line 162) - new TryCatchHandler (line 321) - new Variable (line 458) -- new Variable (line 533) -- new Variable (line 552) -- new Variable (line 554) -- new Variable (line 857) -- new Type (line 884) -- new Variable (line 889) +- new Variable (line 534) +- new Variable (line 545) +- new Variable (line 565) +- new Variable (line 567) +- new Variable (line 870) +- new Type (line 897) +- new Variable (line 902) - 35 class method(s) ### `lib/JIT/TryCatchState.php` diff --git a/ext/dom/DomParseSimpleXmlJitHelper.php b/ext/dom/DomParseSimpleXmlJitHelper.php index 30fd15c7fa8..ef70e5bc389 100644 --- a/ext/dom/DomParseSimpleXmlJitHelper.php +++ b/ext/dom/DomParseSimpleXmlJitHelper.php @@ -329,6 +329,37 @@ public static function rootTagArgv(string $xml): string return 'root'; } + /** + * Root open-tag attributes for user-script AOT createFromString / loadXML (#27108). + * + * @return list + */ + public static function rootAttributesArgv(string $xml): array + { + if (!preg_match('/<([a-zA-Z_][\w:.-]*)((?:\s[^>]*)?)\/?>/', $xml, $root)) { + return []; + } + $attrs = $root[2] ?? ''; + if ('' === trim($attrs)) { + return []; + } + $out = []; + if (!preg_match_all('/([A-Za-z_][\w:.-]*)\s*=\s*"([^"]*)"/', $attrs, $pairs, PREG_SET_ORDER) + && !preg_match_all("/([A-Za-z_][\w:.-]*)\s*=\s*'([^']*)'/", $attrs, $pairs, PREG_SET_ORDER) + ) { + return []; + } + foreach ($pairs as $pair) { + $qname = $pair[1]; + if (0 === stripos($qname, 'xmlns')) { + continue; + } + $out[] = ['qname' => $qname, 'value' => $pair[2]]; + } + + return $out; + } + /** * Document-element textContent for user-script AOT (#25475). * diff --git a/ext/dom/DomUserScriptAttributeCacheLlvm.php b/ext/dom/DomUserScriptAttributeCacheLlvm.php index 1aeee892ad1..0b36fdae2c6 100644 --- a/ext/dom/DomUserScriptAttributeCacheLlvm.php +++ b/ext/dom/DomUserScriptAttributeCacheLlvm.php @@ -12,58 +12,135 @@ * * Each compile-time (namespace, localName) pair maps to a module global holding * a DOMAttr*. Avoids DomRegistry bridges that segfault in standalone AOT. + * + * State is keyed by LLVM module id so NestedJIT module switches do not wipe + * the main-module present/value maps (#27108 rename dup check). */ final class DomUserScriptAttributeCacheLlvm { - /** @var array */ - private static array $slotByKey = []; - - /** @var object|null */ - private static $moduleIdentity = null; - - private static ?string $lastCreateNamespace = null; - - private static ?string $lastCreateLocalName = null; + /** + * @var array, + * presentByKey: array, + * valueByKey: array, + * lastCreateNamespace: ?string, + * lastCreateLocalName: ?string + * }> + */ + private static array $byModule = []; public static function rememberCreate(string $namespace, string $qualifiedName): void { + // rememberCreate is compile-time only; bind to whatever module is current later via store. $pos = strpos($qualifiedName, ':'); $local = false === $pos ? $qualifiedName : substr($qualifiedName, $pos + 1); - self::$lastCreateNamespace = $namespace; - self::$lastCreateLocalName = $local; + self::$pendingCreate = [$namespace, $local]; } + /** @var null|array{0: string, 1: string} */ + private static ?array $pendingCreate = null; + public static function lastCreateNamespace(): ?string { - return self::$lastCreateNamespace; + return self::$pendingCreate[0] ?? null; } public static function lastCreateLocalName(): ?string { - return self::$lastCreateLocalName; + return self::$pendingCreate[1] ?? null; } - /** Compile-time cache presence check for getAttribute dispatch (#19281). */ + /** Compile-time cache presence check for getAttribute / hasAttribute (#19281, #27108). */ public static function hasLiteralKey(string $namespace, string $localName): bool + { + foreach (self::$byModule as $state) { + $key = $namespace."\0".$localName; + if (isset($state['presentByKey'][$key]) || isset($state['slotByKey'][$key])) { + return true; + } + } + + return false; + } + + /** True when a non-null Attr was stored for this key this module (#27108). */ + public static function hasPresentLiteral(string $namespace, string $localName): bool + { + foreach (self::$byModule as $state) { + if (isset($state['presentByKey'][$namespace."\0".$localName])) { + return true; + } + } + + return false; + } + + public static function literalValue(string $namespace, string $localName): ?string { $key = $namespace."\0".$localName; + foreach (self::$byModule as $state) { + if (isset($state['valueByKey'][$key])) { + return $state['valueByKey'][$key]; + } + } - return isset(self::$slotByKey[$key]); + return null; } public static function storeLiteral( Context $context, string $namespace, string $localName, - Value $attr + Value $attr, + ?string $value = null ): Value { + $state = &self::state($context); + if (null !== self::$pendingCreate) { + $state['lastCreateNamespace'] = self::$pendingCreate[0]; + $state['lastCreateLocalName'] = self::$pendingCreate[1]; + } $global = self::slotGlobal($context, $namespace, $localName); $prev = $context->builder->load($global); $context->builder->store($attr, $global); + $key = $namespace."\0".$localName; + $state['presentByKey'][$key] = true; + if (null !== $value) { + $state['valueByKey'][$key] = $value; + } return $prev; } + public static function rekeyLiteral( + Context $context, + string $oldNamespace, + string $oldLocalName, + string $newNamespace, + string $newLocalName, + Value $attr + ): void { + $state = &self::state($context); + $objPtr = $context->getTypeFromString('__object__*'); + $oldGlobal = self::slotGlobal($context, $oldNamespace, $oldLocalName); + $context->builder->store($objPtr->constNull(), $oldGlobal); + $oldKey = $oldNamespace."\0".$oldLocalName; + $value = $state['valueByKey'][$oldKey] ?? null; + unset($state['presentByKey'][$oldKey], $state['valueByKey'][$oldKey]); + self::storeLiteral($context, $newNamespace, $newLocalName, $attr, $value); + } + + public static function clearLiteral( + Context $context, + string $namespace, + string $localName + ): void { + $state = &self::state($context); + $objPtr = $context->getTypeFromString('__object__*'); + $global = self::slotGlobal($context, $namespace, $localName); + $context->builder->store($objPtr->constNull(), $global); + unset($state['presentByKey'][$namespace."\0".$localName], $state['valueByKey'][$namespace."\0".$localName]); + } + public static function lookupLiteral( Context $context, string $namespace, @@ -74,33 +151,44 @@ public static function lookupLiteral( return $context->builder->load($global); } - private static function slotGlobal(Context $context, string $namespace, string $localName): Value + /** @return array{slotByKey: array, presentByKey: array, valueByKey: array, lastCreateNamespace: ?string, lastCreateLocalName: ?string} */ + private static function &state(Context $context): array { - $module = $context->module; - if (self::$moduleIdentity !== $module) { - self::$moduleIdentity = $module; - self::$slotByKey = []; - self::$lastCreateNamespace = null; - self::$lastCreateLocalName = null; + $id = spl_object_id($context->module); + if (!isset(self::$byModule[$id])) { + self::$byModule[$id] = [ + 'slotByKey' => [], + 'presentByKey' => [], + 'valueByKey' => [], + 'lastCreateNamespace' => null, + 'lastCreateLocalName' => null, + ]; } + return self::$byModule[$id]; + } + + private static function slotGlobal(Context $context, string $namespace, string $localName): Value + { + $state = &self::state($context); $key = $namespace."\0".$localName; - if (isset(self::$slotByKey[$key])) { - return self::$slotByKey[$key]; + if (isset($state['slotByKey'][$key])) { + return $state['slotByKey'][$key]; } + $module = $context->module; $objPtr = $context->getTypeFromString('__object__*'); $globalName = '__phpc_dom_us_attr_'.substr(hash('crc32b', $key), 0, 8); $existing = $module->getNamedGlobal($globalName); if (null !== $existing) { - self::$slotByKey[$key] = $existing; + $state['slotByKey'][$key] = $existing; return $existing; } $global = $module->addGlobal($objPtr, $globalName); $global->setInitializer($objPtr->constNull()); - self::$slotByKey[$key] = $global; + $state['slotByKey'][$key] = $global; return $global; } diff --git a/ext/dom/JitDomAttrRename.php b/ext/dom/JitDomAttrRename.php new file mode 100644 index 00000000000..c422aa6e904 --- /dev/null +++ b/ext/dom/JitDomAttrRename.php @@ -0,0 +1,190 @@ +compileTimeString; + if (null === $qLit) { + throw new \LogicException('Dom\\Attr::rename() requires a compile-time qualifiedName in this AOT path (#27108)'); + } + + $nsArg = (null === $nsLit || '' === $nsLit) ? '' : $nsLit; + $pos = strpos($qLit, ':'); + $prefix = false === $pos ? '' : substr($qLit, 0, $pos); + $local = false === $pos ? $qLit : substr($qLit, $pos + 1); + + $isNoopRename = null !== self::$lastAttrKey + && self::$lastAttrKey[0] === $nsArg + && self::$lastAttrKey[1] === $local; + + // Dup vs any other present Attr — independent of orphan/fetch globals (#27108). + if (!$isNoopRename && DomUserScriptAttributeCacheLlvm::hasPresentLiteral($nsArg, $local)) { + self::emitDomException( + $context, + 'An attribute with the given name in the given namespace already exists', + DomExceptionConstants::INVALID_MODIFICATION_ERR + ); + $slot = JitValueBox::alloc($context); + $context->builder->call( + $context->lookupFunction('__value__writeNull'), + JitValueBox::pointer($context, $slot) + ); + + return JitValueBox::normalizeValuePtr($context, JitValueBox::pointer($context, $slot)); + } + + $attr = self::loadObjectArg($context, $args[0]); + // Living Attr.name is QName (#26024). + self::storeString($context, $attr, self::PROP_NODE_NAME, $qLit); + self::storeString($context, $attr, self::PROP_NAME, $qLit); + self::storeString($context, $attr, self::PROP_LOCAL_NAME, $local); + self::storeString($context, $attr, self::PROP_PREFIX, $prefix); + self::storeString($context, $attr, self::PROP_NAMESPACE_URI, $nsArg); + + $isOrphan = self::$lastAttrIsOrphan; + if ($isOrphan) { + // Consume one orphan rename so later attached renames can still rekey. + self::$lastAttrIsOrphan = false; + } + if (!$isOrphan && null !== self::$lastAttrKey) { + [$oldNs, $oldLocal] = self::$lastAttrKey; + if ($oldNs !== $nsArg || $oldLocal !== $local) { + DomUserScriptAttributeCacheLlvm::rekeyLiteral( + $context, + $oldNs, + $oldLocal, + $nsArg, + $local, + $attr + ); + } + self::$lastAttrKey = [$nsArg, $local]; + } + + $slot = JitValueBox::alloc($context); + $context->builder->call( + $context->lookupFunction('__value__writeNull'), + JitValueBox::pointer($context, $slot) + ); + + return JitValueBox::normalizeValuePtr($context, JitValueBox::pointer($context, $slot)); + } + + private static function nullableStringLit(JITVariable $arg): ?string + { + if (JITVariable::TYPE_NULL === $arg->type || $arg->isNullConstant) { + return null; + } + + return JitStringBuiltinArg::compileTimeLiteral($arg) ?? $arg->compileTimeString; + } + + private static function loadObjectArg(Context $context, JITVariable $arg): Value + { + if (JITVariable::TYPE_OBJECT === $arg->type) { + return $context->helper->loadValue($arg); + } + if (JITVariable::TYPE_VALUE === $arg->type) { + return $context->builder->call( + $context->lookupFunction('__value__readObject'), + JitValueBox::valuePtrFromVariable($context, $arg) + ); + } + + throw new \LogicException('Dom\\Attr::rename() receiver must be an object'); + } + + private static function storeString(Context $context, Value $obj, string $prop, string $lit): void + { + $objectType = $context->type->object; + $classId = $objectType->lookup(self::CLASS_ATTR); + if (!$objectType->hasProperty($classId, $prop)) { + $objectType->defineProperty($classId, $prop, JITVariable::TYPE_STRING); + } + $str = $context->builder->load($context->constantStringFromString($lit)); + $owned = $context->builder->call( + $context->lookupFunction('__string__separate'), + $str + ); + $propVar = new JITVariable($context, JITVariable::TYPE_STRING, JITVariable::KIND_VALUE, $owned); + $objectType->propertyStore( + $objectType->propertySlotFor($obj, self::CLASS_ATTR, $prop), + $propVar, + JITVariable::TYPE_STRING + ); + } + + private static function emitDomException(Context $context, string $message, int $code): void + { + TryCatchHelper::emitCatchableClassError( + $context, + 'DOMException', + $message, + null, + '', + 0, + $code + ); + } +} diff --git a/ext/dom/JitDomAttributeNodeNS.php b/ext/dom/JitDomAttributeNodeNS.php index 6c85826f9f8..41fa99070db 100644 --- a/ext/dom/JitDomAttributeNodeNS.php +++ b/ext/dom/JitDomAttributeNodeNS.php @@ -196,10 +196,22 @@ private static function invokeCreateAttributeUserScript(Context $context, JITVar } if (null !== $nameLit) { DomUserScriptAttributeCacheLlvm::rememberCreate('', $nameLit); + $living = null !== JitDomLoadXMLUserScript::lastDocumentClass() + && str_starts_with(JitDomLoadXMLUserScript::lastDocumentClass(), 'Dom\\'); + if ($living) { + JitDomAttrRename::rememberOrphan(); + } return self::boxObjectResult( $context, - self::materializeAttrFromLiterals($context, '', $nameLit, '') + self::materializeAttrFromLiterals( + $context, + '', + $nameLit, + '', + $living ? self::CLASS_LIVING_ATTR : self::CLASS_ATTR, + $living + ) ); } @@ -316,29 +328,59 @@ public static function materializeAttrFromLiterals( Context $context, string $namespace, string $qualifiedName, - string $value + string $value, + string $className = self::CLASS_ATTR, + bool $livingNameIsQName = false ): Value { [$prefix, $localName] = self::splitQualifiedName($qualifiedName); $objectType = $context->type->object; - $classId = $objectType->lookup(self::CLASS_ATTR); + $classId = $objectType->lookup($className); self::ensureAttrPropertyLayout($objectType, $classId); $obj = $objectType->allocate($classId); $objectType->markObjectConstructed($obj); - self::storeStringProperty($context, $obj, self::PROP_NODE_NAME, $qualifiedName); - // php-src ext/dom/attr.c: Attr.name is local name (#19754). - self::storeStringProperty($context, $obj, self::PROP_NAME, $localName); - self::storeStringProperty($context, $obj, self::PROP_VALUE, $value); - self::storeStringProperty($context, $obj, self::PROP_NODE_VALUE, $value); - self::storeStringProperty($context, $obj, self::PROP_NAMESPACE_URI, $namespace); - self::storeStringProperty($context, $obj, self::PROP_LOCAL_NAME, $localName); - self::storeStringProperty($context, $obj, self::PROP_PREFIX, $prefix); - self::storeNullProperty($context, $obj, self::PROP_OWNER_ELEMENT); + self::storeStringProperty($context, $obj, self::PROP_NODE_NAME, $qualifiedName, $className); + // php-src attr.c: legacy DOMAttr.name is local (#19754); living Attr.name is QName (#26024). + $nameProp = $livingNameIsQName ? $qualifiedName : $localName; + self::storeStringProperty($context, $obj, self::PROP_NAME, $nameProp, $className); + self::storeStringProperty($context, $obj, self::PROP_VALUE, $value, $className); + self::storeStringProperty($context, $obj, self::PROP_NODE_VALUE, $value, $className); + self::storeStringProperty($context, $obj, self::PROP_NAMESPACE_URI, $namespace, $className); + self::storeStringProperty($context, $obj, self::PROP_LOCAL_NAME, $localName, $className); + self::storeStringProperty($context, $obj, self::PROP_PREFIX, $prefix, $className); + self::storeNullProperty($context, $obj, self::PROP_OWNER_ELEMENT, $className); return $obj; } + public const CLASS_LIVING_ATTR = 'Dom\\Attr'; + + /** Seed Dom\Attr::rename for method_exists under thin AOT (#27108). */ + public static function ensureLivingAttrMethods(Context $context): void + { + $objectType = $context->type->object; + $classId = $objectType->lookup(self::CLASS_LIVING_ATTR); + self::ensureAttrPropertyLayout($objectType, $classId); + $pub = \PHPCfg\Func::FLAG_PUBLIC; + if (!$objectType->hasMethod($classId, 'rename')) { + $objectType->defineMethodVisibility($classId, 'rename', $pub); + } + $elemId = $objectType->lookup('Dom\\Element'); + foreach ([ + 'getattributenode', 'getattribute', 'hasattribute', 'getattributens', + 'hasattributens', 'getattributenodens', 'setattribute', + ] as $method) { + if (!$objectType->hasMethod($elemId, $method)) { + $objectType->defineMethodVisibility($elemId, $method, $pub); + } + } + $docId = $objectType->lookup('Dom\\XMLDocument'); + if (!$objectType->hasMethod($docId, 'createattribute')) { + $objectType->defineMethodVisibility($docId, 'createattribute', $pub); + } + } + private static function materializeAttrFromRuntime( Context $context, Value $namespace, @@ -402,17 +444,19 @@ private static function storeStringProperty( Context $context, Value $obj, string $prop, - string $lit + string $lit, + string $className = self::CLASS_ATTR ): void { $str = $context->builder->load($context->constantStringFromString($lit)); - self::storeStringPropertyValue($context, $obj, $prop, $str); + self::storeStringPropertyValue($context, $obj, $prop, $str, $className); } private static function storeStringPropertyValue( Context $context, Value $obj, string $prop, - Value $str + Value $str, + string $className = self::CLASS_ATTR ): void { $owned = $context->builder->call( $context->lookupFunction('__string__separate'), @@ -420,14 +464,18 @@ private static function storeStringPropertyValue( ); $propVar = new JITVariable($context, JITVariable::TYPE_STRING, JITVariable::KIND_VALUE, $owned); $context->type->object->propertyStore( - $context->type->object->propertySlotFor($obj, self::CLASS_ATTR, $prop), + $context->type->object->propertySlotFor($obj, $className, $prop), $propVar, JITVariable::TYPE_STRING ); } - private static function storeNullProperty(Context $context, Value $obj, string $prop): void - { + private static function storeNullProperty( + Context $context, + Value $obj, + string $prop, + string $className = self::CLASS_ATTR + ): void { $slot = JitValueBox::alloc($context); $context->builder->call( $context->lookupFunction('__value__writeNull'), @@ -435,7 +483,7 @@ private static function storeNullProperty(Context $context, Value $obj, string $ ); $propVar = new JITVariable($context, JITVariable::TYPE_VALUE, JITVariable::KIND_VARIABLE, $slot); $context->type->object->propertyStore( - $context->type->object->propertySlotFor($obj, self::CLASS_ATTR, $prop), + $context->type->object->propertySlotFor($obj, $className, $prop), $propVar, JITVariable::TYPE_NULL ); @@ -663,7 +711,7 @@ public static function invokeRemoveAttribute(Context $context, JITVariable ...$a } /** - * DOMElement::getAttributeNode() — user-script AOT live Attr cache (#19281). + * DOMElement::getAttributeNode() — user-script AOT live Attr cache (#19281, #27108). */ public static function invokeGetAttributeNode(Context $context, JITVariable ...$args): Value { @@ -673,6 +721,8 @@ public static function invokeGetAttributeNode(Context $context, JITVariable ...$ BasicBlockHelper::ensureOpenInsertBlock($context, 'dom_getattrnode_cont'); $nameLit = self::compileTimeStringArg($args[1]); if (null !== $nameLit) { + \PHPCompiler\ext\dom\JitDomAttrRename::rememberFetchedKey('', $nameLit); + return self::boxNullableObjectResult( $context, DomUserScriptAttributeCacheLlvm::lookupLiteral($context, '', $nameLit) @@ -711,7 +761,9 @@ public static function invokeGetAttributeLive(Context $context, JITVariable ...$ $context->builder->branch($doneBlock); $context->builder->positionAtEnd($objBlock); - $valueVar = $context->type->object->propertyFetch($attr, self::CLASS_ATTR, self::PROP_VALUE); + // Attr cache may hold Dom\Attr (living createFromString) or DOMAttr (#27108). + $attrClass = self::CLASS_LIVING_ATTR; + $valueVar = $context->type->object->propertyFetch($attr, $attrClass, self::PROP_VALUE); $str = $context->helper->loadValue($valueVar); $context->builder->store(self::boxStringResult($context, $str), $resultSlot); $context->builder->branch($doneBlock); diff --git a/ext/dom/JitDomCreateElement.php b/ext/dom/JitDomCreateElement.php index 03e8f51ed5a..b59bda692fa 100644 --- a/ext/dom/JitDomCreateElement.php +++ b/ext/dom/JitDomCreateElement.php @@ -142,34 +142,38 @@ private static function compileTimeStringArg(JITVariable $arg): ?string return $arg->compileTimeString; } - public static function materializeElementFromLiteral(Context $context, string $nameLit): Value - { + public static function materializeElementFromLiteral( + Context $context, + string $nameLit, + string $className = self::CLASS_ELEMENT + ): Value { $objectType = $context->type->object; - $classId = $objectType->lookup(self::CLASS_ELEMENT); + $classId = $objectType->lookup($className); self::ensureElementPropertyLayout($objectType, $classId); $obj = $objectType->allocate($classId); $objectType->markObjectConstructed($obj); $nameStr = $context->builder->load($context->constantStringFromString($nameLit)); - self::storeStringProperty($context, $obj, self::CLASS_ELEMENT, self::PROP_NODE_NAME, $nameStr); - self::storeStringProperty($context, $obj, self::CLASS_ELEMENT, self::PROP_TAG_NAME, $nameStr); - self::storeNullProperty($context, $obj, self::CLASS_ELEMENT, self::PROP_ATTRIBUTES); + self::storeStringProperty($context, $obj, $className, self::PROP_NODE_NAME, $nameStr); + self::storeStringProperty($context, $obj, $className, self::PROP_TAG_NAME, $nameStr); + self::storeNullProperty($context, $obj, $className, self::PROP_ATTRIBUTES); // Always seed textContent/nodeValue — defineProperty alone leaves a null // __string__* and pure-user-script fetches segfault (#25475 / re-#23251). - self::storeTextContentSlots($context, $obj, ''); + self::storeTextContentSlots($context, $obj, '', $className); return $obj; } - /** User-script AOT: materialize DOMElement with textContent (#18493, #25475). */ + /** User-script AOT: materialize DOMElement / Dom\Element with textContent (#18493, #25475, #27108). */ public static function materializeElementWithTextContent( Context $context, string $tag, - string $textContent + string $textContent, + string $className = self::CLASS_ELEMENT ): Value { - $obj = self::materializeElementFromLiteral($context, $tag); - self::storeTextContentSlots($context, $obj, $textContent); + $obj = self::materializeElementFromLiteral($context, $tag, $className); + self::storeTextContentSlots($context, $obj, $textContent, $className); return $obj; } @@ -178,10 +182,11 @@ public static function materializeElementWithTextContent( public static function storeTextContentSlots( Context $context, Value $element, - string $textContent + string $textContent, + string $className = self::CLASS_ELEMENT ): void { $objectType = $context->type->object; - $classId = $objectType->lookup(self::CLASS_ELEMENT); + $classId = $objectType->lookup($className); foreach ([self::PROP_TEXT_CONTENT, 'nodeValue', VmDom::PROP_USER_SCRIPT_INNER_XML] as $prop) { if (!$objectType->hasProperty($classId, $prop)) { $objectType->defineProperty($classId, $prop, JITVariable::TYPE_STRING); @@ -195,23 +200,24 @@ public static function storeTextContentSlots( ); $propVar = new JITVariable($context, JITVariable::TYPE_STRING, JITVariable::KIND_VALUE, $owned); $objectType->propertyStore( - $objectType->propertySlotFor($element, self::CLASS_ELEMENT, $prop), + $objectType->propertySlotFor($element, $className, $prop), $propVar, JITVariable::TYPE_STRING ); } // Default empty; loadXML seeds real child markup via storeUserScriptInnerXml (#26757). - self::storeUserScriptInnerXml($context, $element, ''); + self::storeUserScriptInnerXml($context, $element, '', $className); } /** Seed/replace {@see VmDom::PROP_USER_SCRIPT_INNER_XML} for AOT saveXML (#26757 / #26765). */ public static function storeUserScriptInnerXml( Context $context, Value $element, - string $innerXml + string $innerXml, + string $className = self::CLASS_ELEMENT ): void { $objectType = $context->type->object; - $classId = $objectType->lookup(self::CLASS_ELEMENT); + $classId = $objectType->lookup($className); if (!$objectType->hasProperty($classId, VmDom::PROP_USER_SCRIPT_INNER_XML)) { $objectType->defineProperty($classId, VmDom::PROP_USER_SCRIPT_INNER_XML, JITVariable::TYPE_STRING); } @@ -222,7 +228,7 @@ public static function storeUserScriptInnerXml( ); $innerVar = new JITVariable($context, JITVariable::TYPE_STRING, JITVariable::KIND_VALUE, $ownedInner); $objectType->propertyStore( - $objectType->propertySlotFor($element, self::CLASS_ELEMENT, VmDom::PROP_USER_SCRIPT_INNER_XML), + $objectType->propertySlotFor($element, $className, VmDom::PROP_USER_SCRIPT_INNER_XML), $innerVar, JITVariable::TYPE_STRING ); diff --git a/ext/dom/JitDomDocumentElement.php b/ext/dom/JitDomDocumentElement.php index b925f201125..f98bf6d08a9 100644 --- a/ext/dom/JitDomDocumentElement.php +++ b/ext/dom/JitDomDocumentElement.php @@ -12,19 +12,24 @@ use PHPCompiler\JIT\Variable as JITVariable; use PHPLLVM\Value; -/** LLVM lowering for DOMDocument::$documentElement in user-script AOT (#18478, #19455, #23251). */ +/** LLVM lowering for DOMDocument / Dom\XMLDocument::$documentElement (#18478, #19455, #23251, #27108). */ final class JitDomDocumentElement { private const CLASS_DOCUMENT = 'DOMDocument'; + private const CLASS_XML_DOCUMENT = 'Dom\\XMLDocument'; + private const CLASS_ELEMENT = 'DOMElement'; private const PROP_DOCUMENT_ELEMENT = 'documentElement'; public static function isDomDocumentElement(string $classLc, string $propLc): bool { - return 'domdocument' === strtolower($classLc) - && 'documentelement' === strtolower($propLc); + $classLc = strtolower(str_replace('/', '\\', ltrim($classLc, '\\'))); + $propLc = strtolower($propLc); + + return ('domdocument' === $classLc || 'dom\\xmldocument' === $classLc) + && 'documentelement' === $propLc; } public static function fetch(Object_ $objectType, Value $obj): JITVariable @@ -38,13 +43,12 @@ public static function fetch(Object_ $objectType, Value $obj): JITVariable ); } - // Prefer the documentElement pinned at loadXML (pure path) or DomRegistry (#26757). - // Rematerializing a fresh shallow element each fetch dropped saveXML children and - // made appendChild mutate a throwaway object. + // Prefer the documentElement pinned at loadXML / createFromString (#26757, #27108). if (null !== JitDomLoadXMLUserScript::lastCompileTimeXml() || !JitDomLoadXMLUserScript::lastLoadWasPureUserScript() ) { - $docClassId = $objectType->lookup(self::CLASS_DOCUMENT); + $docClass = JitDomLoadXMLUserScript::lastDocumentClass() ?? self::CLASS_DOCUMENT; + $docClassId = $objectType->lookup($docClass); if (!$objectType->hasProperty($docClassId, self::PROP_DOCUMENT_ELEMENT)) { $objectType->defineProperty($docClassId, self::PROP_DOCUMENT_ELEMENT, JITVariable::TYPE_OBJECT); } @@ -52,7 +56,7 @@ public static function fetch(Object_ $objectType, Value $obj): JITVariable return ObjectInstancePropertyLlvm::propertyFetchDeclaredSlot( $objectType, $obj, - self::CLASS_DOCUMENT, + $docClass, self::PROP_DOCUMENT_ELEMENT, $docClassId ); diff --git a/ext/dom/JitDomElementTextContent.php b/ext/dom/JitDomElementTextContent.php index 5fab98f050c..e6136719863 100644 --- a/ext/dom/JitDomElementTextContent.php +++ b/ext/dom/JitDomElementTextContent.php @@ -85,10 +85,11 @@ public static function fetchNamed(Object_ $objectType, Value $obj, string $propN public static function isDomElementTextContent(string $classLc, string $propLc): bool { - if (!\in_array(strtolower($propLc), ['textcontent', 'nodevalue'], true)) { + $propLc = strtolower($propLc); + $classLc = strtolower(str_replace('/', '\\', ltrim($classLc, '\\'))); + if (!\in_array($propLc, ['textcontent', 'nodevalue'], true)) { return false; } - $classLc = strtolower($classLc); if ('domelement' === $classLc) { return true; } @@ -103,6 +104,25 @@ public static function isDomElementTextContent(string $classLc, string $propLc): return false; } + /** Dom\Attr / DOMAttr::$value|nodeValue — sync TYPE_STRING slots (#27108). */ + public static function isDomAttrValueProperty(string $classLc, string $propLc): bool + { + $propLc = strtolower($propLc); + $classLc = strtolower(str_replace('/', '\\', ltrim($classLc, '\\'))); + if (\in_array($classLc, ['dom\\attr', 'domattr'], true) + && \in_array($propLc, ['value', 'nodevalue'], true) + ) { + return true; + } + // Temps often lose Dom\Attr as CFG `object`. Prefer `$value` only — `$nodeValue` on + // `object` still belongs to the DOMElement textContent bridge (#23251). + return 'value' === $propLc + && JitDomLoadXMLUserScript::lastLoadWasPureUserScript() + && null !== JitDomLoadXMLUserScript::lastDocumentClass() + && str_starts_with((string) JitDomLoadXMLUserScript::lastDocumentClass(), 'Dom\\') + && \in_array($classLc, ['object', 'stdclass', ''], true); + } + /** * @return bool true when the store was handled (caller must skip propertyStore) */ @@ -110,15 +130,45 @@ public static function tryEmitStore(Context $context, JITVariable $lvalue, JITVa { $prop = $lvalue->objectPropertyName ?? ''; $class = $lvalue->objectPropertyClassName ?? ''; - if (!self::isDomElementTextContent(strtolower($class), strtolower($prop))) { + $propLc = strtolower($prop); + $classLc = strtolower(str_replace('/', '\\', ltrim($class, '\\'))); + if (null === $lvalue->objectPropertyReceiver) { return false; } - if (null === $lvalue->objectPropertyReceiver) { + + $receiver = $lvalue->objectPropertyReceiver; + + // Living / classic Attr value|nodeValue — direct TYPE_STRING slot write (#27108). + // Must run before isDomElementTextContent so Attr::$nodeValue is not treated as Element. + if (self::isDomAttrValueProperty($classLc, $propLc)) { + $str = self::loadStringValue($context, $value); + $attrClass = ('domattr' === $classLc) ? 'DOMAttr' : 'Dom\\Attr'; + JitDomAttributeNodeNS::ensureLivingAttrMethods($context); + $owned = $context->builder->call( + $context->lookupFunction('__string__separate'), + $str + ); + foreach (['value', 'nodeValue'] as $syncProp) { + $context->type->object->propertyStore( + $context->type->object->propertySlotFor($receiver, $attrClass, $syncProp), + new JITVariable( + $context, + JITVariable::TYPE_STRING, + JITVariable::KIND_VALUE, + $owned + ), + JITVariable::TYPE_STRING + ); + } + + return true; + } + + if (!self::isDomElementTextContent($classLc, $propLc)) { return false; } $str = self::loadStringValue($context, $value); - $receiver = $lvalue->objectPropertyReceiver; if (JitDomDocumentMethodKernel::shouldUse($context) && JitDomLoadXMLUserScript::lastLoadWasPureUserScript() @@ -141,7 +191,7 @@ public static function tryEmitStore(Context $context, JITVariable $lvalue, JITVa } DomElementTextContentRuntime::ensureWriteLinked($context); - $abi = 'nodevalue' === strtolower($prop) + $abi = 'nodevalue' === $propLc ? DomElementTextContentRuntime::ABI_WRITE_NODE_VALUE : DomElementTextContentRuntime::ABI_WRITE_TEXT_CONTENT; $context->builder->call( diff --git a/ext/dom/JitDomLoadXMLUserScript.php b/ext/dom/JitDomLoadXMLUserScript.php index 06578d070a4..c8f57b7179c 100644 --- a/ext/dom/JitDomLoadXMLUserScript.php +++ b/ext/dom/JitDomLoadXMLUserScript.php @@ -28,6 +28,9 @@ final class JitDomLoadXMLUserScript /** True when loadXML used the compile-time user-script path (no DomLoadXMLRuntime tree). */ private static bool $lastLoadWasPureUserScript = false; + /** Document class that owns PROP_DOCUMENT_ELEMENT for the last pure materialize (#27108). */ + private static ?string $lastDocumentClass = null; + public static function lastCompileTimeXml(): ?string { return self::$lastCompileTimeXml; @@ -38,6 +41,11 @@ public static function lastLoadWasPureUserScript(): bool return self::$lastLoadWasPureUserScript; } + public static function lastDocumentClass(): ?string + { + return self::$lastDocumentClass; + } + /** * Mark the last load as pure user-script LLVM nodes (no DomRegistry tree). * @@ -49,10 +57,16 @@ public static function markLastLoadPureUserScript(): void self::$lastLoadWasPureUserScript = true; } - public static function rememberCompileTimeXml(string $xml): void + public static function rememberCompileTimeXml(string $xml, string $documentClass = self::CLASS_DOCUMENT): void { self::$lastCompileTimeXml = $xml; self::$lastLoadWasPureUserScript = false; + self::$lastDocumentClass = $documentClass; + } + + public static function rememberLivingDocumentClass(string $documentClass): void + { + self::$lastDocumentClass = $documentClass; } public static function shouldUse(Context $context): bool @@ -99,6 +113,7 @@ public static function tryInvoke(Context $context, JITVariable ...$args): ?Value } self::$lastCompileTimeXml = $lit; + self::$lastDocumentClass = self::CLASS_DOCUMENT; self::markLastLoadPureUserScript(); // Declare textContent/nodeValue on DOMElement so forWrite hasProperty skips // dynamic-property deprecation (hasProperty does not walk DOMNode; #23251). diff --git a/ext/dom/JitDomXmlDocumentCreateFromString.php b/ext/dom/JitDomXmlDocumentCreateFromString.php index 829e4e3963b..98ee3f5f065 100644 --- a/ext/dom/JitDomXmlDocumentCreateFromString.php +++ b/ext/dom/JitDomXmlDocumentCreateFromString.php @@ -4,6 +4,7 @@ namespace PHPCompiler\ext\dom; +use PHPCompiler\CompilerVersion; use PHPCompiler\ext\standard\JitIntdiv; use PHPCompiler\JIT\BasicBlockHelper; use PHPCompiler\JIT\Builtin\DomXmlDocumentCreateFromStringRuntime; @@ -20,9 +21,18 @@ * LLVM lowering for Dom\XMLDocument::createFromString() (#27108, #19581). * * php-src: ext/dom/xml_document.c — Dom\XMLDocument::createFromString + * + * Thin standalone AOT: compile-time string literals materialize a main-module + * Dom\XMLDocument (peer {@see JitDomLoadXMLUserScript}) so get_class / + * documentElement / Attr::rename do not touch NestedJIT ObjectEntry layout. */ final class JitDomXmlDocumentCreateFromString { + private const CLASS_DOCUMENT = 'Dom\\XMLDocument'; + + /** Document element uses classic DOMElement layout (tagName/nodeName slots; #27108). */ + private const CLASS_ELEMENT = 'DOMElement'; + public static function invoke(Context $context, JITVariable ...$args): Value { BasicBlockHelper::ensureOpenInsertBlock($context, 'dom_xml_create_from_string_cont'); @@ -52,6 +62,34 @@ public static function invoke(Context $context, JITVariable ...$args): Value ); } + $optionsLit = 0; + if (null !== $optionsArg && !NamedOptionalCallArgs::isOmittedOptional($optionsArg)) { + $opt = $optionsArg->compileTimeLong; + if (null === $opt) { + // Non-literal options → NestedJIT parse path. + return self::invokeViaHelper($context, $sourceArg, $optionsArg); + } + $optionsLit = (int) $opt; + } + + if (0 === $optionsLit && null !== $sourceLit && '' !== $sourceLit + && JitDomDocumentMethodKernel::shouldUse($context) + && CompilerVersion::supportsDomLivingStandardNamespace() + ) { + $us = self::tryMaterializeUserScript($context, $sourceLit); + if (null !== $us) { + return $us; + } + } + + return self::invokeViaHelper($context, $sourceArg, $optionsArg); + } + + private static function invokeViaHelper( + Context $context, + JITVariable $sourceArg, + ?JITVariable $optionsArg + ): Value { $xmlStr = JitStringBuiltinArg::lower( $context, $sourceArg, @@ -78,7 +116,6 @@ public static function invoke(Context $context, JITVariable ...$args): Value $xmlStr, $options ); - // Box in the caller — ABI returns __object__* (do not return callee alloca value*). $slot = JitValueBox::alloc($context); $ptr = JitValueBox::pointer($context, $slot); $context->builder->call( @@ -89,4 +126,116 @@ public static function invoke(Context $context, JITVariable ...$args): Value return JitValueBox::normalizeValuePtr($context, $ptr); } + + /** + * Allocate Dom\XMLDocument + documentElement + Attr cache in the main module (#27108). + */ + private static function tryMaterializeUserScript(Context $context, string $source): ?Value + { + $lit = VmDom::stripLeadingUtf8Bom($source); + $forParse = ltrim($lit); + if ('' === $forParse || '<' !== $forParse[0]) { + return null; + } + if (1 !== preg_match('/<([a-zA-Z_][\w:.-]*)/', $forParse)) { + return null; + } + // Inter-element blanks need full DomRegistry parse (peer loadXML). + if (JitDomLoadXMLUserScript::xmlContainsInterElementBlankText($forParse)) { + return null; + } + + BasicBlockHelper::ensureOpenInsertBlock($context, 'dom_xml_cfs_us_materialize'); + JitDomLoadXMLUserScript::rememberCompileTimeXml($forParse, self::CLASS_DOCUMENT); + JitDomLoadXMLUserScript::markLastLoadPureUserScript(); + JitDomLoadXMLUserScript::rememberLivingDocumentClass(self::CLASS_DOCUMENT); + + $objectType = $context->type->object; + $docClassId = $objectType->lookup(self::CLASS_DOCUMENT); + $elemClassId = $objectType->lookup(self::CLASS_ELEMENT); + self::ensureDocumentLayout($objectType, $docClassId); + self::ensureElementLayout($objectType, $elemClassId); + JitDomAttributeNodeNS::ensureLivingAttrMethods($context); + + $document = $objectType->allocate($docClassId); + $objectType->markObjectConstructed($document); + + $tag = DomParseSimpleXmlJitHelper::rootTagArgv($forParse); + $text = DomParseSimpleXmlJitHelper::rootTextContentArgv($forParse); + $inner = DomParseSimpleXmlJitHelper::rootInnerXmlArgv($forParse); + $element = JitDomCreateElement::materializeElementWithTextContent( + $context, + $tag, + $text, + self::CLASS_ELEMENT + ); + JitDomCreateElement::storeUserScriptInnerXml($context, $element, $inner, self::CLASS_ELEMENT); + JitDomDocumentElement::syncChildrenFromXmlPublic($context, $element, $forParse); + + $elemJit = new JITVariable( + $context, + JITVariable::TYPE_OBJECT, + JITVariable::KIND_VALUE, + $element + ); + $objectType->propertyStore( + $objectType->propertySlotFor($document, self::CLASS_DOCUMENT, VmDom::PROP_DOCUMENT_ELEMENT), + $elemJit, + JITVariable::TYPE_OBJECT + ); + DomUserScriptPinnedRootLlvm::pin($context, $element); + + foreach (DomParseSimpleXmlJitHelper::rootAttributesArgv($forParse) as $attrPair) { + $qname = $attrPair['qname']; + $value = $attrPair['value']; + $pos = strpos($qname, ':'); + $local = false === $pos ? $qname : substr($qname, $pos + 1); + $attr = JitDomAttributeNodeNS::materializeAttrFromLiterals( + $context, + '', + $qname, + $value, + JitDomAttributeNodeNS::CLASS_LIVING_ATTR, + true + ); + DomUserScriptAttributeCacheLlvm::storeLiteral($context, '', $local, $attr, $value); + // Also key by qname when unprefixed (local === qname). + if ($local !== $qname) { + DomUserScriptAttributeCacheLlvm::storeLiteral($context, '', $qname, $attr, $value); + } + } + + $slot = JitValueBox::alloc($context); + $ptr = JitValueBox::pointer($context, $slot); + $context->builder->call( + $context->lookupFunction('__value__writeObject'), + $ptr, + $document + ); + + return JitValueBox::normalizeValuePtr($context, $ptr); + } + + private static function ensureDocumentLayout( + \PHPCompiler\JIT\Builtin\Type\Object_ $objectType, + int $docClassId + ): void { + if (!$objectType->hasProperty($docClassId, VmDom::PROP_DOCUMENT_ELEMENT)) { + $objectType->defineProperty($docClassId, VmDom::PROP_DOCUMENT_ELEMENT, JITVariable::TYPE_OBJECT); + } + } + + private static function ensureElementLayout( + \PHPCompiler\JIT\Builtin\Type\Object_ $objectType, + int $elemClassId + ): void { + foreach (['nodeName', 'tagName', 'textContent', 'nodeValue', VmDom::PROP_USER_SCRIPT_INNER_XML] as $prop) { + if (!$objectType->hasProperty($elemClassId, $prop)) { + $objectType->defineProperty($elemClassId, $prop, JITVariable::TYPE_STRING); + } + } + if (!$objectType->hasProperty($elemClassId, 'attributes')) { + $objectType->defineProperty($elemClassId, 'attributes', JITVariable::TYPE_VALUE); + } + } } diff --git a/ext/standard/JitMethodExists.php b/ext/standard/JitMethodExists.php index de92388448f..5a2ea8839ca 100644 --- a/ext/standard/JitMethodExists.php +++ b/ext/standard/JitMethodExists.php @@ -61,20 +61,22 @@ private static function invokeFromValueBox( $context->builder->structGep($valuePtr, $typeField) ); $i8 = $context->getTypeFromString('int8'); + // Value-box tags may include IS_REFCOUNTED; compare the low 7 bits (#27108). + $kind = $context->builder->and($typeByte, $i8->constInt(0x7f, false)); $isNull = $context->builder->icmp( Builder::INT_EQ, - $typeByte, + $kind, $i8->constInt(Variable::TYPE_NULL, false) ); $isObject = $context->builder->icmp( Builder::INT_EQ, - $typeByte, - $i8->constInt(Variable::TYPE_OBJECT, false) + $kind, + $i8->constInt(JITVariable::TYPE_OBJECT & 0x7f, false) ); $isString = $context->builder->icmp( Builder::INT_EQ, - $typeByte, - $i8->constInt(Variable::TYPE_STRING, false) + $kind, + $i8->constInt(JITVariable::TYPE_STRING & 0x7f, false) ); $nullBlock = BasicBlockHelper::append($context, 'method_exists_null'); @@ -208,6 +210,10 @@ private static function existsForClassIdLiteralMethod( ): Value { $i1 = $context->getTypeFromString('int1'); $object = $context->type->object; + // Ensure living Dom\Attr::rename is visible for method_exists (#27108). + if ('rename' === strtolower($method)) { + \PHPCompiler\ext\dom\JitDomAttributeNodeNS::ensureLivingAttrMethods($context); + } $exists = $i1->constInt(0, false); foreach ($object->allClassNamesById() as $id => $className) { $isClass = $context->builder->icmp( diff --git a/lib/JIT/Builtin/Type/ObjectInstancePropertyLlvm.php b/lib/JIT/Builtin/Type/ObjectInstancePropertyLlvm.php index 1fa77d67e08..b5298a82e8c 100644 --- a/lib/JIT/Builtin/Type/ObjectInstancePropertyLlvm.php +++ b/lib/JIT/Builtin/Type/ObjectInstancePropertyLlvm.php @@ -184,6 +184,25 @@ private static function tryPropertyFetchByRuntimeClass( if ([] === $candidates) { return null; } + // Living Dom\Attr::$value|nodeValue shares the name "value" with + // SensitiveParameterValue — multi-candidate dispatch returns a detached + // TYPE_VALUE box that cannot accept assigns (#27108). Prefer Dom\Attr + // declared slots when the user-script document is living Dom\*. + $propLc = strtolower($name); + if (\in_array($propLc, ['value', 'nodevalue'], true) + && null !== \PHPCompiler\ext\dom\JitDomLoadXMLUserScript::lastDocumentClass() + && str_starts_with( + (string) \PHPCompiler\ext\dom\JitDomLoadXMLUserScript::lastDocumentClass(), + 'Dom\\' + ) + ) { + foreach ($candidates as $id => $className) { + $classLc = strtolower(str_replace('/', '\\', ltrim($className, '\\'))); + if ('dom\\attr' === $classLc || 'domattr' === $classLc) { + return self::propertyFetchOrdinary($object, $obj, $className, $name, $id); + } + } + } if (1 === \count($candidates)) { $classId = array_key_first($candidates); $className = $candidates[$classId]; @@ -323,10 +342,11 @@ public static function boxFetchedPropertyIntoValue( return; } if (Variable::TYPE_STRING === $propertyType) { + // KIND_VALUE already holds `__string__*`; load() would pass `__string__` by value (#27108). $context->builder->call( $context->lookupFunction('__value__writeString'), $destPtr, - $context->builder->load($fetched->value) + $context->helper->loadValue($fetched) ); return; @@ -335,7 +355,7 @@ public static function boxFetchedPropertyIntoValue( $context->builder->call( $context->lookupFunction('__value__writeObject'), $destPtr, - $context->builder->load($fetched->value) + $context->helper->loadValue($fetched) ); return; diff --git a/lib/JIT/Builtin/Type/Object_.php b/lib/JIT/Builtin/Type/Object_.php index 1b20620563f..59377b9a54a 100755 --- a/lib/JIT/Builtin/Type/Object_.php +++ b/lib/JIT/Builtin/Type/Object_.php @@ -3518,6 +3518,22 @@ private function registerExternalClass(string $lcname, string $displayName): voi $this->defineProperty($id, 'tagName', Variable::TYPE_STRING); $this->defineProperty($id, 'attributes', Variable::TYPE_VALUE); } + if ('dom\\attr' === $lcname) { + // Living Dom\Attr for thin AOT method_exists / property layout (#27108). + foreach ([ + 'nodeName', 'name', 'value', 'nodeValue', + 'namespaceURI', 'localName', 'prefix', + ] as $prop) { + $this->defineProperty($id, $prop, Variable::TYPE_STRING); + } + $this->defineProperty($id, 'ownerElement', Variable::TYPE_VALUE); + $this->defineMethodVisibility($id, 'rename', \PHPCfg\Func::FLAG_PUBLIC); + } + if ('dom\\xmldocument' === $lcname) { + $this->defineProperty($id, 'documentElement', Variable::TYPE_OBJECT); + $this->defineMethodVisibility($id, 'createfromstring', \PHPCfg\Func::FLAG_PUBLIC | \PHPCfg\Func::FLAG_STATIC); + $this->defineMethodVisibility($id, 'createattribute', \PHPCfg\Func::FLAG_PUBLIC); + } if ('splobjectstorage' === $lcname) { $this->splObjectStorageClassId = $id; $this->defineProperty($id, '__spl_ht', Variable::TYPE_HASHTABLE); diff --git a/lib/JIT/Call/DomAttrRename.php b/lib/JIT/Call/DomAttrRename.php new file mode 100644 index 00000000000..7a21e6c8ef9 --- /dev/null +++ b/lib/JIT/Call/DomAttrRename.php @@ -0,0 +1,23 @@ +compileTimeString; + } + if (null !== $nameLit && DomUserScriptAttributeCacheLlvm::hasPresentLiteral('', $nameLit)) { + return self::boxConstantString( + $context, + DomUserScriptAttributeCacheLlvm::literalValue('', $nameLit) ?? '' + ); + } // Live Attr cache when setAttribute/getAttributeNode populated it (#19281); // otherwise fall back to importNode/getElementById HTML-id stub (#19212). @@ -27,4 +40,21 @@ public function call(Context $context, Variable ...$args): Value return JitDomImportNode::invokeGetAttribute($context, ...$args); } + + private static function boxConstantString(Context $context, string $lit): Value + { + $str = $context->builder->load($context->constantStringFromString($lit)); + $owned = $context->builder->call( + $context->lookupFunction('__string__separate'), + $str + ); + $slot = JitValueBox::alloc($context); + $context->builder->call( + $context->lookupFunction('__value__writeString'), + JitValueBox::pointer($context, $slot), + $owned + ); + + return JitValueBox::normalizeValuePtr($context, $slot); + } } diff --git a/lib/JIT/Call/DomElementGetAttributeNS.php b/lib/JIT/Call/DomElementGetAttributeNS.php new file mode 100644 index 00000000000..e789ad0108d --- /dev/null +++ b/lib/JIT/Call/DomElementGetAttributeNS.php @@ -0,0 +1,60 @@ +compileTimeString; + if (null === $nsLit && (Variable::TYPE_NULL === $args[1]->type || $args[1]->isNullConstant)) { + $nsLit = ''; + } + $localLit = JitStringBuiltinArg::compileTimeLiteral($args[2]) ?? $args[2]->compileTimeString; + if (null === $nsLit || null === $localLit + || !DomUserScriptAttributeCacheLlvm::hasPresentLiteral($nsLit, $localLit) + ) { + return self::boxConstantString($context, ''); + } + + return self::boxConstantString( + $context, + DomUserScriptAttributeCacheLlvm::literalValue($nsLit, $localLit) ?? '' + ); + } + + private static function boxConstantString(Context $context, string $lit): Value + { + $str = $context->builder->load($context->constantStringFromString($lit)); + $owned = $context->builder->call( + $context->lookupFunction('__string__separate'), + $str + ); + $slot = JitValueBox::alloc($context); + $context->builder->call( + $context->lookupFunction('__value__writeString'), + JitValueBox::pointer($context, $slot), + $owned + ); + + return JitValueBox::normalizeValuePtr($context, $slot); + } +} diff --git a/lib/JIT/Call/DomElementHasAttribute.php b/lib/JIT/Call/DomElementHasAttribute.php new file mode 100644 index 00000000000..3bff31b1e69 --- /dev/null +++ b/lib/JIT/Call/DomElementHasAttribute.php @@ -0,0 +1,43 @@ +compileTimeString; + $present = null !== $nameLit + && DomUserScriptAttributeCacheLlvm::hasPresentLiteral('', $nameLit); + + $slot = JitValueBox::alloc($context); + $i64 = $context->getTypeFromString('int64'); + $context->builder->call( + $context->lookupFunction('__value__writeLong'), + JitValueBox::pointer($context, $slot), + $i64->constInt($present ? 1 : 0, false) + ); + + return JitValueBox::normalizeValuePtr($context, $slot); + } +} diff --git a/lib/JIT/DomInstanceMethodJit.php b/lib/JIT/DomInstanceMethodJit.php index 0d7e3c8eef5..1ef36fdfde2 100644 --- a/lib/JIT/DomInstanceMethodJit.php +++ b/lib/JIT/DomInstanceMethodJit.php @@ -157,7 +157,9 @@ private static function isUserScriptGenericDomMethod(string $proxyLc): bool 'dom\\attr::rename' => true, 'dom\\element::rename' => true, 'dom\\htmlelement::rename' => true, - // Living attribute APIs via VmDomInstanceInvoke DomRegistry path (#27108). + // Living attribute APIs via LLVM user-script path (#27108). + 'domelement::hasattribute' => true, + 'domelement::getattributens' => true, 'dom\\element::hasattribute' => true, 'dom\\element::hasattributens' => true, 'dom\\element::getattribute' => true, @@ -240,18 +242,50 @@ public static function ensureProxy(Context $context, string $proxyName): void return; } - if ('domelement::getattribute' === $lc || 'domnode::getattribute' === $lc) { + if ('domelement::getattributenode' === $lc + || 'dom\\element::getattributenode' === $lc + || 'dom\\htmlelement::getattributenode' === $lc + ) { + $context->functionProxies[$lc] = new Call\DomElementGetAttributeNode(); + + return; + } + if ('domelement::getattribute' === $lc + || 'domnode::getattribute' === $lc + || 'dom\\element::getattribute' === $lc + || 'dom\\htmlelement::getattribute' === $lc + ) { $context->functionProxies[$lc] = new Call\DomElementGetAttribute(); return; } - if ('domelement::setattribute' === $lc) { - $context->functionProxies[$lc] = new Call\DomElementSetAttribute(); + if ('domelement::getattributens' === $lc + || 'dom\\element::getattributens' === $lc + || 'dom\\htmlelement::getattributens' === $lc + ) { + $context->functionProxies[$lc] = new Call\DomElementGetAttributeNS(); return; } - if ('domelement::removeattribute' === $lc) { - $context->functionProxies[$lc] = new Call\DomElementRemoveAttribute(); + if ('domelement::hasattribute' === $lc + || 'dom\\element::hasattribute' === $lc + || 'dom\\htmlelement::hasattribute' === $lc + ) { + $context->functionProxies[$lc] = new Call\DomElementHasAttribute(); + + return; + } + if ('dom\\attr::rename' === $lc) { + $context->functionProxies[$lc] = new Call\DomAttrRename(); + + return; + } + if ('domdocument::createattribute' === $lc + || 'dom\\xmldocument::createattribute' === $lc + || 'dom\\document::createattribute' === $lc + || 'dom\\htmldocument::createattribute' === $lc + ) { + $context->functionProxies[$lc] = new Call\DomDocumentCreateAttribute(); return; } @@ -260,6 +294,21 @@ public static function ensureProxy(Context $context, string $proxyName): void return; } + if ('domelement::getattribute' === $lc || 'domnode::getattribute' === $lc) { + $context->functionProxies[$lc] = new Call\DomElementGetAttribute(); + + return; + } + if ('domelement::setattribute' === $lc) { + $context->functionProxies[$lc] = new Call\DomElementSetAttribute(); + + return; + } + if ('domelement::removeattribute' === $lc) { + $context->functionProxies[$lc] = new Call\DomElementRemoveAttribute(); + + return; + } if ('domelement::getattributenodens' === $lc) { $context->functionProxies[$lc] = new Call\DomElementGetAttributeNodeNS(); @@ -512,22 +561,11 @@ public static function ensureProxy(Context $context, string $proxyName): void || 'dom\\htmldocument::queryselectorall' === $lc || 'dom\\htmldocument::getelementbyid' === $lc || 'dom\\htmldocument::savehtml' === $lc - || 'dom\\attr::rename' === $lc || 'dom\\element::rename' === $lc || 'dom\\htmlelement::rename' === $lc - || 'dom\\element::hasattribute' === $lc || 'dom\\element::hasattributens' === $lc - || 'dom\\element::getattribute' === $lc - || 'dom\\element::getattributens' === $lc - || 'dom\\element::getattributenode' === $lc || 'dom\\element::getattributenodens' === $lc - || 'dom\\htmlelement::hasattribute' === $lc - || 'dom\\htmlelement::getattribute' === $lc - || 'dom\\htmlelement::getattributens' === $lc - || 'dom\\htmlelement::getattributenode' === $lc - || 'dom\\document::createattribute' === $lc - || 'dom\\xmldocument::createattribute' === $lc - || 'dom\\htmldocument::createattribute' === $lc + || 'dom\\htmlelement::getattributenodens' === $lc ) { if (!preg_match('/^(dom\\\\[a-z0-9_]+)::([a-z0-9_]+)$/', $lc, $livingMatches)) { return; @@ -672,6 +710,8 @@ public static function registerKnownProxies(Context $context): void self::ensureProxy($context, 'dom\\htmldocument::importnode'); self::ensureProxy($context, 'domelement::getattribute'); self::ensureProxy($context, 'domnode::getattribute'); + self::ensureProxy($context, 'domelement::hasattribute'); + self::ensureProxy($context, 'domelement::getattributens'); self::ensureProxy($context, 'domelement::setattribute'); self::ensureProxy($context, 'domelement::removeattribute'); self::ensureProxy($context, 'domelement::getattributenode'); diff --git a/lib/JIT/TryCatchHelper.php b/lib/JIT/TryCatchHelper.php index ceaf7d4f219..586c2ea3bbe 100644 --- a/lib/JIT/TryCatchHelper.php +++ b/lib/JIT/TryCatchHelper.php @@ -491,7 +491,8 @@ public static function emitCatchableClassError( string $message, ?\PHPCompiler\JIT $jit = null, string $file = '', - int $line = 0 + int $line = 0, + int $code = 0 ): void { JitThrow::registerDeclarations($context); JitThrow::ensureLinked($context); @@ -537,6 +538,18 @@ public static function emitCatchableClassError( $msgStr ); $object->storeInstanceProperty($obj, $className, 'message', $msgVar); + if ($code !== 0) { + if (!$object->hasProperty($classId, ExceptionSupport::PROP_CODE)) { + $object->defineProperty($classId, ExceptionSupport::PROP_CODE, Variable::TYPE_NATIVE_LONG); + } + $codeVar = new Variable( + $context, + Variable::TYPE_NATIVE_LONG, + Variable::KIND_VALUE, + $context->constantFromInteger($code) + ); + $object->storeInstanceProperty($obj, $className, ExceptionSupport::PROP_CODE, $codeVar); + } // Stamp file/line like zend_exception_get_props so getFile()/getLine() work (#24397). if ('' === $file) {