From 97420e85c2d5814112400e0f49a8a587c6244093 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sun, 26 Jul 2026 02:05:25 +0000 Subject: [PATCH 1/2] Stdlib: AOT loadXML user-script textContent detach + childNodes (#23251) Keep the thin user-script loadXML path but materialize live children, route textContent writes through the DOM bridge, and serialize saveXML($node) from updated slots so held parentNode/childNodes match Zend. Co-authored-by: Cursor --- docs/bootstrap-inventory.md | 172 ++++++------ ext/dom/DomElementTextContentJitHelper.php | 16 +- ext/dom/JitDomDocumentElement.php | 189 ++++++++++++- ext/dom/JitDomDocumentMethodKernel.php | 32 +++ ext/dom/JitDomElementTextContent.php | 263 ++++++++++++++++-- ext/dom/JitDomLoadXMLUserScript.php | 30 +- ext/dom/JitDomNodeListLength.php | 3 + ext/dom/JitDomSaveXML.php | 2 +- ext/dom/JitDomSaveXMLUserScript.php | 87 +++++- lib/JIT.php | 17 ++ .../Builtin/DomElementTextContentRuntime.php | 64 ++++- .../Type/ObjectInstancePropertyLlvm.php | 2 +- .../cases/dom_loadxml_textcontent_detach.phpt | 22 ++ ...e_23251_aot_loadxml_textcontent_detach.php | 12 + 14 files changed, 788 insertions(+), 123 deletions(-) create mode 100644 test/fixtures/aot/cases/dom_loadxml_textcontent_detach.phpt create mode 100644 test/repro/issue_23251_aot_loadxml_textcontent_detach.php diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index 020acbba25..bff46f0855 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -12,7 +12,7 @@ Regenerate: `php script/bootstrap-inventory.php` | Phase A inventory files (M2 ratio SSOT) | 6563 | | Phase A ratio-deferred paths | 0 | | Source constructs flagged (blockers) | 0 | -| Source constructs flagged (warnings) | 21119 | +| Source constructs flagged (warnings) | 21129 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -370,10 +370,10 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `ext/dom/JitDomCreateElement.php` | 0 | 7 | | `ext/dom/JitDomCreateElementNS.php` | 0 | 1 | | `ext/dom/JitDomCreateTextNode.php` | 0 | 2 | -| `ext/dom/JitDomDocumentElement.php` | 0 | 4 | +| `ext/dom/JitDomDocumentElement.php` | 0 | 10 | | `ext/dom/JitDomDocumentMethodKernel.php` | 0 | 3 | | `ext/dom/JitDomElementNavigationProperty.php` | 0 | 3 | -| `ext/dom/JitDomElementTextContent.php` | 0 | 3 | +| `ext/dom/JitDomElementTextContent.php` | 0 | 7 | | `ext/dom/JitDomGetElementById.php` | 0 | 5 | | `ext/dom/JitDomGetElementsByTagName.php` | 0 | 1 | | `ext/dom/JitDomGetElementsByTagNameUserScript.php` | 0 | 2 | @@ -8117,7 +8117,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/dom/DomElementTextContentJitHelper.php` **Warnings** (review for bootstrap subset): -- 1 class method(s) +- 3 class method(s) ### `ext/dom/DomExceptionConstants.php` @@ -8817,15 +8817,21 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag **Warnings** (review for bootstrap subset): - new JITVariable (line 50) -- new JITVariable (line 77) -- new JITVariable (line 98) -- 4 class method(s) +- new JITVariable (line 88) +- new JITVariable (line 100) +- new JITVariable (line 106) +- new JITVariable (line 152) +- new JITVariable (line 163) +- new JITVariable (line 229) +- new JITVariable (line 235) +- new JITVariable (line 261) +- 9 class method(s) ### `ext/dom/JitDomDocumentMethodKernel.php` **Warnings** (review for bootstrap subset): -- new JIT (line 1393) -- 77 class method(s) +- new JIT (line 1425) +- 79 class method(s) - 1 closure(s) ### `ext/dom/JitDomElementNavigationProperty.php` @@ -8838,9 +8844,13 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/dom/JitDomElementTextContent.php` **Warnings** (review for bootstrap subset): -- new JITVariable (line 37) -- new JITVariable (line 50) -- 3 class method(s) +- new JITVariable (line 72) +- new JITVariable (line 130) +- new JITVariable (line 209) +- new JITVariable (line 228) +- new JITVariable (line 239) +- new JITVariable (line 255) +- 7 class method(s) ### `ext/dom/JitDomGetElementById.php` @@ -8917,10 +8927,10 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/dom/JitDomLoadXMLUserScript.php` **Warnings** (review for bootstrap subset): -- new JITVariable (line 96) -- new JITVariable (line 158) -- new JITVariable (line 175) -- 11 class method(s) +- new JITVariable (line 115) +- new JITVariable (line 177) +- new JITVariable (line 194) +- 12 class method(s) ### `ext/dom/JitDomNodeChildProperty.php` @@ -8946,7 +8956,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/dom/JitDomNodeListLength.php` **Warnings** (review for bootstrap subset): -- new JITVariable (line 31) +- new JITVariable (line 34) - 2 class method(s) ### `ext/dom/JitDomNormalize.php` @@ -8989,7 +8999,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/dom/JitDomSaveXMLUserScript.php` **Warnings** (review for bootstrap subset): -- 3 class method(s) +- 6 class method(s) ### `ext/dom/JitDomStandaloneAotInitKernel.php` @@ -42199,68 +42209,68 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new Type (line 10652) - new Variable (line 10674) - new Type (line 10682) -- new Variable (line 10869) -- new Variable (line 10890) -- new Variable (line 12043) -- new Variable (line 12068) -- new Variable (line 12089) -- new CompileError (line 12836) -- new CompileError (line 12938) -- new Variable (line 12995) -- new Variable (line 13378) -- new Variable (line 13398) -- new Variable (line 13419) -- new Variable (line 13453) -- new Variable (line 13488) -- new Variable (line 13582) -- new Variable (line 13606) -- new Variable (line 13616) -- new Variable (line 13790) -- new Variable (line 13808) -- new Variable (line 13858) -- new Variable (line 13885) -- new Variable (line 13911) -- new Variable (line 14241) -- new Variable (line 14609) -- new Variable (line 14639) -- new Variable (line 14658) -- new Variable (line 14688) -- new Variable (line 14725) -- new Variable (line 14773) -- new Variable (line 14816) -- new VM\Variable (line 15161) -- new Variable (line 15195) -- new Variable (line 15246) -- new Variable (line 15262) -- new Variable (line 15281) -- new Variable (line 15288) -- new OpCode (line 15308) -- new Variable (line 15309) -- new Variable (line 15481) -- new Variable (line 15519) -- new OpCode (line 15533) -- new Variable (line 15534) -- new Variable (line 15593) -- new OpCode (line 15614) -- new Variable (line 15615) -- new Variable (line 15712) -- new Variable (line 15734) -- new Variable (line 15802) -- new Operand\Literal (line 15811) -- new Variable (line 15826) -- new Variable (line 16015) -- new VM\PropertyIsInitializedHandler (line 16107) -- new JIT\Call\RuntimeIndirectInstanceMethodCall (line 16147) -- new JIT\Call\RuntimeIndirectInstanceMethodCall (line 16365) -- new JIT\Call\RuntimeIndirectInstanceMethodCall (line 16470) -- new JIT\Call\VmCoerceVariableToString (line 16594) -- new JIT\Call\IncludePathResolverResolve (line 16864) -- new Variable (line 17922) -- new VM (line 17966) -- new VM\Variable (line 17975) -- new Variable (line 17996) -- new Variable (line 18042) -- new Variable (line 18566) +- new Variable (line 10878) +- new Variable (line 10899) +- new Variable (line 12052) +- new Variable (line 12077) +- new Variable (line 12098) +- new CompileError (line 12845) +- new CompileError (line 12947) +- new Variable (line 13004) +- new Variable (line 13387) +- new Variable (line 13407) +- new Variable (line 13428) +- new Variable (line 13462) +- new Variable (line 13497) +- new Variable (line 13599) +- new Variable (line 13623) +- new Variable (line 13633) +- new Variable (line 13807) +- new Variable (line 13825) +- new Variable (line 13875) +- new Variable (line 13902) +- new Variable (line 13928) +- new Variable (line 14258) +- new Variable (line 14626) +- new Variable (line 14656) +- new Variable (line 14675) +- new Variable (line 14705) +- new Variable (line 14742) +- new Variable (line 14790) +- new Variable (line 14833) +- new VM\Variable (line 15178) +- new Variable (line 15212) +- new Variable (line 15263) +- new Variable (line 15279) +- new Variable (line 15298) +- new Variable (line 15305) +- new OpCode (line 15325) +- new Variable (line 15326) +- new Variable (line 15498) +- new Variable (line 15536) +- new OpCode (line 15550) +- new Variable (line 15551) +- new Variable (line 15610) +- new OpCode (line 15631) +- new Variable (line 15632) +- new Variable (line 15729) +- new Variable (line 15751) +- new Variable (line 15819) +- new Operand\Literal (line 15828) +- new Variable (line 15843) +- new Variable (line 16032) +- new VM\PropertyIsInitializedHandler (line 16124) +- new JIT\Call\RuntimeIndirectInstanceMethodCall (line 16164) +- new JIT\Call\RuntimeIndirectInstanceMethodCall (line 16382) +- new JIT\Call\RuntimeIndirectInstanceMethodCall (line 16487) +- new JIT\Call\VmCoerceVariableToString (line 16611) +- new JIT\Call\IncludePathResolverResolve (line 16881) +- new Variable (line 17939) +- new VM (line 17983) +- new VM\Variable (line 17992) +- new Variable (line 18013) +- new Variable (line 18059) +- new Variable (line 18583) - 372 class method(s) - 7 closure(s) @@ -42836,7 +42846,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/JIT/Builtin/DomElementTextContentRuntime.php` **Warnings** (review for bootstrap subset): -- 1 class method(s) +- 3 class method(s) ### `lib/JIT/Builtin/DomGetElementByIdRuntime.php` diff --git a/ext/dom/DomElementTextContentJitHelper.php b/ext/dom/DomElementTextContentJitHelper.php index 53f4b957dc..910a11b6d6 100644 --- a/ext/dom/DomElementTextContentJitHelper.php +++ b/ext/dom/DomElementTextContentJitHelper.php @@ -6,11 +6,25 @@ use PHPCompiler\VM\ObjectEntry; -/** DOMElement::$textContent for user-script AOT property reads (#17954). */ +/** DOMElement::$textContent for user-script AOT property reads/writes (#17954, #23251). */ final class DomElementTextContentJitHelper { public static function textContentArgv(ObjectEntry $element): string { return VmDom::readTextContent($element); } + + /** php-src dom_node_textContent_write — detach children + insert text (#20646, #23251). */ + public static function writeTextContentArgv(ObjectEntry $element, string $value): void + { + $ctx = \PHPCompiler\VM\VmActiveContextJitHelper::resolve(); + VmDom::writeTextContent($ctx, $element, $value); + } + + /** php-src dom_node_node_value_write (#20646, #23251). */ + public static function writeNodeValueArgv(ObjectEntry $element, string $value): void + { + $ctx = \PHPCompiler\VM\VmActiveContextJitHelper::resolve(); + VmDom::writeNodeValue($ctx, $element, $value); + } } diff --git a/ext/dom/JitDomDocumentElement.php b/ext/dom/JitDomDocumentElement.php index 9a0d25b7cc..09791b93be 100644 --- a/ext/dom/JitDomDocumentElement.php +++ b/ext/dom/JitDomDocumentElement.php @@ -11,7 +11,7 @@ use PHPCompiler\JIT\Variable as JITVariable; use PHPLLVM\Value; -/** LLVM lowering for DOMDocument::$documentElement in user-script AOT (#18478, #19455). */ +/** LLVM lowering for DOMDocument::$documentElement in user-script AOT (#18478, #19455, #23251). */ final class JitDomDocumentElement { private const CLASS_DOCUMENT = 'DOMDocument'; @@ -45,7 +45,7 @@ public static function fetch(Object_ $objectType, Value $obj): JITVariable BasicBlockHelper::ensureOpenInsertBlock($context, 'dom_document_element_us'); $tag = DomParseSimpleXmlJitHelper::rootTagArgv($xml); $element = JitDomCreateElement::materializeElementFromLiteral($context, $tag); - self::syncFirstChildFromXml($context, $element, $xml); + self::syncChildrenFromXml($context, $element, $xml); return new JITVariable( $context, @@ -55,35 +55,198 @@ public static function fetch(Object_ $objectType, Value $obj): JITVariable ); } - /** Seed firstChild/lastChild slots from compile-time XML (#19455). */ - private static function syncFirstChildFromXml( + /** + * Seed firstChild/lastChild/parentNode/sibling slots from compile-time XML (#19455, #23251). + * + * Element children are required so held references survive textContent writes (detach). + */ + private static function syncChildrenFromXml( \PHPCompiler\JIT\Context $context, Value $element, string $xml ): void { - $node = DomParseSimpleXmlJitHelper::firstChildNodeArgv($xml); - if (null === $node || 'comment' !== $node['kind']) { - // Comment-only roots for #19455; other kinds keep prior null-slot behavior. + $children = self::directElementChildTags($xml); + if ([] === $children) { + $node = DomParseSimpleXmlJitHelper::firstChildNodeArgv($xml); + if (null === $node || 'comment' !== $node['kind']) { + return; + } + $child = JitDomCreateComment::materialize($context, $node['data']); + self::linkSingleChild($context, $element, $child); + self::storeChildNodesLength($context, $element, 1); + return; } - $child = JitDomCreateComment::materialize($context, $node['data']); + + $objectType = $context->type->object; + $prev = null; + $first = null; + $last = null; + foreach ($children as $childTag) { + $child = JitDomCreateElement::materializeElementFromLiteral($context, $childTag); + self::ensureLinkProps($context); + $parentJit = new JITVariable( + $context, + JITVariable::TYPE_OBJECT, + JITVariable::KIND_VALUE, + $element + ); + $objectType->propertyStore( + $objectType->propertySlotFor($child, self::CLASS_ELEMENT, VmDom::PROP_PARENT_NODE), + $parentJit, + JITVariable::TYPE_VALUE + ); + if (null !== $prev) { + $childJit = new JITVariable( + $context, + JITVariable::TYPE_OBJECT, + JITVariable::KIND_VALUE, + $child + ); + $prevJit = new JITVariable( + $context, + JITVariable::TYPE_OBJECT, + JITVariable::KIND_VALUE, + $prev + ); + $objectType->propertyStore( + $objectType->propertySlotFor($prev, 'DOMNode', VmDom::PROP_NEXT_SIBLING), + $childJit, + JITVariable::TYPE_VALUE + ); + $objectType->propertyStore( + $objectType->propertySlotFor($child, 'DOMNode', VmDom::PROP_PREVIOUS_SIBLING), + $prevJit, + JITVariable::TYPE_VALUE + ); + } + if (null === $first) { + $first = $child; + } + $last = $child; + $prev = $child; + } + if (null !== $first && null !== $last) { + self::storeFirstLast($context, $element, $first, $last); + } + self::storeChildNodesLength($context, $element, \count($children)); + } + + /** Attach a DOMNodeList with the given length for user-script childNodes (#23251). */ + public static function storeChildNodesLength( + \PHPCompiler\JIT\Context $context, + Value $element, + int $length + ): void { + $objectType = $context->type->object; + $nodeClassId = $objectType->lookup('DOMNode'); + $listClassId = $objectType->lookup('DOMNodeList'); + if (!$objectType->hasProperty($nodeClassId, VmDom::PROP_CHILD_NODES)) { + $objectType->defineProperty($nodeClassId, VmDom::PROP_CHILD_NODES, JITVariable::TYPE_OBJECT); + } + if (!$objectType->hasProperty($listClassId, 'length')) { + $objectType->defineProperty($listClassId, 'length', JITVariable::TYPE_NATIVE_LONG); + } + $list = $objectType->allocate($listClassId); + $objectType->markObjectConstructed($list); + $lengthVar = new JITVariable( + $context, + JITVariable::TYPE_NATIVE_LONG, + JITVariable::KIND_VALUE, + $context->getTypeFromString('int64')->constInt($length, false) + ); + $objectType->propertyStore( + $objectType->propertySlotFor($list, 'DOMNodeList', 'length'), + $lengthVar, + JITVariable::TYPE_NATIVE_LONG + ); + $listJit = new JITVariable( + $context, + JITVariable::TYPE_OBJECT, + JITVariable::KIND_VALUE, + $list + ); + $objectType->propertyStore( + $objectType->propertySlotFor($element, 'DOMNode', VmDom::PROP_CHILD_NODES), + $listJit, + JITVariable::TYPE_OBJECT + ); + } + + /** @return list */ + private static function directElementChildTags(string $xml): array + { + if (!preg_match('/<([a-zA-Z_][\w:.-]*)(?:\s[^>]*)?>/', $xml, $root, PREG_OFFSET_CAPTURE)) { + return []; + } + $afterRoot = (int) $root[0][1] + \strlen($root[0][0]); + $close = stripos($xml, '', $afterRoot); + $inner = false === $close + ? substr($xml, $afterRoot) + : substr($xml, $afterRoot, $close - $afterRoot); + $tags = []; + if (!preg_match_all('/<([a-zA-Z_][\w:.-]*)(?:\s[^>]*)?\/?>/', $inner, $matches)) { + return []; + } + foreach ($matches[1] as $tag) { + $tags[] = strtolower($tag); + } + + return $tags; + } + + private static function ensureLinkProps(\PHPCompiler\JIT\Context $context): void + { $objectType = $context->type->object; $nodeClassId = $objectType->lookup('DOMNode'); - foreach ([VmDom::PROP_FIRST_CHILD, VmDom::PROP_LAST_CHILD] as $prop) { + $elementClassId = $objectType->lookup(self::CLASS_ELEMENT); + foreach ([VmDom::PROP_FIRST_CHILD, VmDom::PROP_LAST_CHILD, VmDom::PROP_NEXT_SIBLING, VmDom::PROP_PREVIOUS_SIBLING] as $prop) { if (!$objectType->hasProperty($nodeClassId, $prop)) { $objectType->defineProperty($nodeClassId, $prop, JITVariable::TYPE_VALUE); } } - $childJit = new JITVariable( + if (!$objectType->hasProperty($elementClassId, VmDom::PROP_PARENT_NODE)) { + $objectType->defineProperty($elementClassId, VmDom::PROP_PARENT_NODE, JITVariable::TYPE_VALUE); + } + } + + private static function linkSingleChild( + \PHPCompiler\JIT\Context $context, + Value $element, + Value $child + ): void { + self::ensureLinkProps($context); + self::storeFirstLast($context, $element, $child, $child); + } + + private static function storeFirstLast( + \PHPCompiler\JIT\Context $context, + Value $element, + Value $first, + Value $last + ): void { + $objectType = $context->type->object; + $firstJit = new JITVariable( + $context, + JITVariable::TYPE_OBJECT, + JITVariable::KIND_VALUE, + $first + ); + $lastJit = new JITVariable( $context, JITVariable::TYPE_OBJECT, JITVariable::KIND_VALUE, - $child + $last ); - foreach ([VmDom::PROP_FIRST_CHILD, VmDom::PROP_LAST_CHILD] as $prop) { + foreach ( + [ + [VmDom::PROP_FIRST_CHILD, $firstJit], + [VmDom::PROP_LAST_CHILD, $lastJit], + ] as [$prop, $jit] + ) { $objectType->propertyStore( $objectType->propertySlotFor($element, 'DOMNode', $prop), - $childJit, + $jit, JITVariable::TYPE_VALUE ); } diff --git a/ext/dom/JitDomDocumentMethodKernel.php b/ext/dom/JitDomDocumentMethodKernel.php index ac221060c2..2ebea24371 100644 --- a/ext/dom/JitDomDocumentMethodKernel.php +++ b/ext/dom/JitDomDocumentMethodKernel.php @@ -507,6 +507,38 @@ public static function ensureElementTextContentBridge(Context $context): void ); } + public static function ensureElementTextContentWriteBridge(Context $context): void + { + self::ensureBridge( + $context, + DomElementTextContentRuntime::ABI_WRITE_TEXT_CONTENT, + 'dom_element_text_content_write_user_script', + [ + $context->getTypeFromString('__object__*'), + $context->getTypeFromString('__string__*'), + ], + $context->context->voidType(), + 'PHPCompiler\\ext\\dom\\DomElementTextContentJitHelper::writeTextContentArgv', + '/ext/dom/DomElementTextContentJitHelper.php' + ); + } + + public static function ensureElementNodeValueWriteBridge(Context $context): void + { + self::ensureBridge( + $context, + DomElementTextContentRuntime::ABI_WRITE_NODE_VALUE, + 'dom_element_node_value_write_user_script', + [ + $context->getTypeFromString('__object__*'), + $context->getTypeFromString('__string__*'), + ], + $context->context->voidType(), + 'PHPCompiler\\ext\\dom\\DomElementTextContentJitHelper::writeNodeValueArgv', + '/ext/dom/DomElementTextContentJitHelper.php' + ); + } + public static function ensureIsConnectedBridge(Context $context): void { self::ensureBridge( diff --git a/ext/dom/JitDomElementTextContent.php b/ext/dom/JitDomElementTextContent.php index 80320ec07f..ea9f1da082 100644 --- a/ext/dom/JitDomElementTextContent.php +++ b/ext/dom/JitDomElementTextContent.php @@ -5,59 +5,265 @@ namespace PHPCompiler\ext\dom; use PHPCompiler\ext\dom\JitDomDocumentMethodKernel; +use PHPCompiler\JIT\BasicBlockHelper; use PHPCompiler\JIT\Builtin\DomElementTextContentRuntime; use PHPCompiler\JIT\Builtin\Type\Object_; +use PHPCompiler\JIT\Builtin\Type\ObjectInstancePropertyLlvm; use PHPCompiler\JIT\Context; +use PHPCompiler\JIT\JitStringBuiltinArg; use PHPCompiler\JIT\JitValueBox; use PHPCompiler\JIT\Variable as JITVariable; +use PHPLLVM\Builder; use PHPLLVM\Value; -/** LLVM lowering for DOMElement::$textContent on PHP-mutated nodes (#17954). */ +/** + * LLVM lowering for DOMElement::$textContent / $nodeValue (#17954, #23251). + * + * User-script AOT: detach held children via parentNode slots, then install a text child. + * DomRegistry-backed nodes also call writeTextContent / writeNodeValue. + */ final class JitDomElementTextContent { private const CLASS_ELEMENT = 'DOMElement'; private const PROP_TEXT_CONTENT = 'textContent'; + private const PROP_NODE_VALUE = 'nodeValue'; + public static function fetch(Object_ $objectType, Value $obj): JITVariable + { + return self::fetchNamed($objectType, $obj, self::PROP_TEXT_CONTENT); + } + + public static function fetchNamed(Object_ $objectType, Value $obj, string $propName): JITVariable { $context = $objectType->jitContext(); - if (JitDomDocumentMethodKernel::shouldUse($context)) { + $propLc = strtolower($propName); + + if (JitDomDocumentMethodKernel::shouldUse($context) + && JitDomLoadXMLUserScript::lastLoadWasPureUserScript() + ) { $classId = $objectType->lookup(self::CLASS_ELEMENT); - if (!$objectType->hasProperty($classId, self::PROP_TEXT_CONTENT)) { - $objectType->defineProperty($classId, self::PROP_TEXT_CONTENT, JITVariable::TYPE_STRING); + $slotProp = 'nodevalue' === $propLc ? self::PROP_NODE_VALUE : self::PROP_TEXT_CONTENT; + if (!$objectType->hasProperty($classId, $slotProp)) { + $objectType->defineProperty($classId, $slotProp, JITVariable::TYPE_STRING); } - $slot = $objectType->propertySlotFor($obj, self::CLASS_ELEMENT, self::PROP_TEXT_CONTENT); - $loaded = $context->builder->load($slot); - $typed = $context->builder->pointerCast( - $loaded, - $context->getTypeFromString('__string__*') + $var = ObjectInstancePropertyLlvm::propertyFetchDeclaredSlot( + $objectType, + $obj, + self::CLASS_ELEMENT, + $slotProp, + $classId ); + $var->objectPropertyReceiver = $obj; + $var->objectPropertyName = $slotProp; + $var->objectPropertyClassName = self::CLASS_ELEMENT; + $var->objectPropertyType = JITVariable::TYPE_STRING; - return new JITVariable( - $context, - JITVariable::TYPE_STRING, - JITVariable::KIND_VALUE, - $typed - ); + return $var; } + DomElementTextContentRuntime::ensureLinked($context); $str = $context->builder->call( $context->lookupFunction(DomElementTextContentRuntime::ABI_NAME), $obj ); - return new JITVariable( + $var = new JITVariable( $context, JITVariable::TYPE_STRING, JITVariable::KIND_VALUE, $str ); + $var->objectPropertyReceiver = $obj; + $var->objectPropertyName = $propName; + $var->objectPropertyClassName = self::CLASS_ELEMENT; + $var->objectPropertyType = JITVariable::TYPE_STRING; + + return $var; } public static function isDomElementTextContent(string $classLc, string $propLc): bool { - return self::CLASS_ELEMENT === $classLc && 'textcontent' === $propLc; + if (!\in_array(strtolower($propLc), ['textcontent', 'nodevalue'], true)) { + return false; + } + $classLc = strtolower($classLc); + if ('domelement' === $classLc) { + return true; + } + // User-script AOT often loses DOMElement type on temps after documentElement assign + // (CFG userType → object); still route textContent writes through the DOM bridge (#23251). + if (JitDomLoadXMLUserScript::lastLoadWasPureUserScript() + && \in_array($classLc, ['object', 'stdclass', ''], true) + ) { + return true; + } + + return false; + } + + /** + * @return bool true when the store was handled (caller must skip propertyStore) + */ + public static function tryEmitStore(Context $context, JITVariable $lvalue, JITVariable $value): bool + { + $prop = $lvalue->objectPropertyName ?? ''; + $class = $lvalue->objectPropertyClassName ?? ''; + if (!self::isDomElementTextContent(strtolower($class), strtolower($prop))) { + return false; + } + if (null === $lvalue->objectPropertyReceiver) { + return false; + } + + $str = self::loadStringValue($context, $value); + $receiver = $lvalue->objectPropertyReceiver; + + if (JitDomDocumentMethodKernel::shouldUse($context) + && JitDomLoadXMLUserScript::lastLoadWasPureUserScript() + ) { + self::emitUserScriptDetachAndReplace($context, $receiver, $str); + if (null !== $lvalue->objectPropertySlot) { + $context->type->object->propertyStore( + $lvalue->objectPropertySlot, + new JITVariable( + $context, + JITVariable::TYPE_STRING, + JITVariable::KIND_VALUE, + $str + ), + JITVariable::TYPE_STRING + ); + } + + return true; + } + + DomElementTextContentRuntime::ensureWriteLinked($context); + $abi = 'nodevalue' === strtolower($prop) + ? DomElementTextContentRuntime::ABI_WRITE_NODE_VALUE + : DomElementTextContentRuntime::ABI_WRITE_TEXT_CONTENT; + $context->builder->call( + $context->lookupFunction($abi), + $receiver, + $str + ); + + return true; + } + + /** Null parentNode on current children, install a single text stand-in child (#23251). */ + private static function emitUserScriptDetachAndReplace( + Context $context, + Value $receiver, + Value $textStr + ): void { + $objectType = $context->type->object; + $nodeClassId = $objectType->lookup('DOMNode'); + $elementClassId = $objectType->lookup(self::CLASS_ELEMENT); + foreach ([VmDom::PROP_FIRST_CHILD, VmDom::PROP_LAST_CHILD] as $prop) { + if (!$objectType->hasProperty($nodeClassId, $prop)) { + $objectType->defineProperty($nodeClassId, $prop, JITVariable::TYPE_VALUE); + } + } + if (!$objectType->hasProperty($elementClassId, VmDom::PROP_PARENT_NODE)) { + $objectType->defineProperty($elementClassId, VmDom::PROP_PARENT_NODE, JITVariable::TYPE_VALUE); + } + + foreach ([VmDom::PROP_FIRST_CHILD, VmDom::PROP_LAST_CHILD] as $prop) { + // firstChild/lastChild are TYPE_VALUE slots holding __value__ boxes (#23251). + $childSlot = $objectType->propertySlotFor($receiver, 'DOMNode', $prop); + $slotPtr = $context->builder->load($childSlot); + $voidPtr = $context->getTypeFromString('void*'); + $isNullSlot = $context->builder->icmp( + Builder::INT_EQ, + $slotPtr, + $voidPtr->constNull() + ); + $detachBlock = BasicBlockHelper::append($context, 'dom_tc_detach_'.$prop); + $contBlock = BasicBlockHelper::append($context, 'dom_tc_cont_'.$prop); + $context->builder->branchIf($isNullSlot, $contBlock, $detachBlock); + $context->builder->positionAtEnd($detachBlock); + $valuePtr = $context->builder->pointerCast( + $slotPtr, + $context->getTypeFromString('__value__*') + ); + $childObj = $context->builder->call( + $context->lookupFunction('__value__readObject'), + $valuePtr + ); + $isNullObj = $context->builder->icmp( + Builder::INT_EQ, + $childObj, + $context->getTypeFromString('__object__*')->constNull() + ); + $doDetach = BasicBlockHelper::append($context, 'dom_tc_do_detach_'.$prop); + $context->builder->branchIf($isNullObj, $contBlock, $doDetach); + $context->builder->positionAtEnd($doDetach); + $nullSlot = JitValueBox::alloc($context); + $context->builder->call( + $context->lookupFunction('__value__writeNull'), + JitValueBox::pointer($context, $nullSlot) + ); + $nullVar = new JITVariable( + $context, + JITVariable::TYPE_VALUE, + JITVariable::KIND_VARIABLE, + $nullSlot + ); + $objectType->propertyStore( + $objectType->propertySlotFor($childObj, self::CLASS_ELEMENT, VmDom::PROP_PARENT_NODE), + $nullVar, + JITVariable::TYPE_VALUE + ); + $context->builder->branch($contBlock); + $context->builder->positionAtEnd($contBlock); + } + + $textNode = JitDomCreateTextNode::materialize($context); + if (!$objectType->hasProperty($elementClassId, self::PROP_TEXT_CONTENT)) { + $objectType->defineProperty($elementClassId, self::PROP_TEXT_CONTENT, JITVariable::TYPE_STRING); + } + $textJitStr = new JITVariable( + $context, + JITVariable::TYPE_STRING, + JITVariable::KIND_VALUE, + $textStr + ); + $objectType->propertyStore( + $objectType->propertySlotFor($textNode, self::CLASS_ELEMENT, self::PROP_TEXT_CONTENT), + $textJitStr, + JITVariable::TYPE_STRING + ); + $textJit = new JITVariable( + $context, + JITVariable::TYPE_OBJECT, + JITVariable::KIND_VALUE, + $textNode + ); + $objectType->propertyStore( + $objectType->propertySlotFor($receiver, 'DOMNode', VmDom::PROP_FIRST_CHILD), + $textJit, + JITVariable::TYPE_VALUE + ); + $objectType->propertyStore( + $objectType->propertySlotFor($receiver, 'DOMNode', VmDom::PROP_LAST_CHILD), + $textJit, + JITVariable::TYPE_VALUE + ); + $parentJit = new JITVariable( + $context, + JITVariable::TYPE_OBJECT, + JITVariable::KIND_VALUE, + $receiver + ); + $objectType->propertyStore( + $objectType->propertySlotFor($textNode, self::CLASS_ELEMENT, VmDom::PROP_PARENT_NODE), + $parentJit, + JITVariable::TYPE_VALUE + ); + JitDomDocumentElement::storeChildNodesLength($context, $receiver, 1); } public static function loadObjectFromReceiver(Context $context, JITVariable $receiver): Value @@ -74,4 +280,27 @@ public static function loadObjectFromReceiver(Context $context, JITVariable $rec throw new \LogicException('DOMElement::$textContent receiver must be an object'); } + + private static function loadStringValue(Context $context, JITVariable $value): Value + { + if (JITVariable::TYPE_STRING === $value->type) { + return $context->helper->loadValue($value); + } + if (JITVariable::TYPE_NULL === $value->type || $value->isNullConstant) { + return $context->builder->load($context->constantStringFromString('')); + } + if (JITVariable::TYPE_VALUE === $value->type) { + return $context->builder->call( + $context->lookupFunction('__value__readString'), + JitValueBox::valuePtrFromVariable($context, $value) + ); + } + + $lit = JitStringBuiltinArg::compileTimeLiteral($value) ?? $value->compileTimeString; + if (null !== $lit) { + return $context->builder->load($context->constantStringFromString($lit)); + } + + return $context->builder->load($context->constantStringFromString('')); + } } diff --git a/ext/dom/JitDomLoadXMLUserScript.php b/ext/dom/JitDomLoadXMLUserScript.php index 5bd1f11c06..2708d8f94a 100644 --- a/ext/dom/JitDomLoadXMLUserScript.php +++ b/ext/dom/JitDomLoadXMLUserScript.php @@ -13,7 +13,7 @@ use PHPCompiler\JIT\Variable as JITVariable; use PHPLLVM\Value; -/** User-script standalone AOT: compile-time DOMDocument::loadXML() (#18268, #19211). */ +/** User-script standalone AOT: compile-time DOMDocument::loadXML() (#18268, #19211, #23251). */ final class JitDomLoadXMLUserScript { private const CLASS_DOCUMENT = 'DOMDocument'; @@ -24,14 +24,23 @@ final class JitDomLoadXMLUserScript private static ?string $lastCompileTimeXml = null; + /** True when loadXML used the compile-time user-script path (no DomLoadXMLRuntime tree). */ + private static bool $lastLoadWasPureUserScript = false; + public static function lastCompileTimeXml(): ?string { return self::$lastCompileTimeXml; } + public static function lastLoadWasPureUserScript(): bool + { + return self::$lastLoadWasPureUserScript; + } + public static function rememberCompileTimeXml(string $xml): void { self::$lastCompileTimeXml = $xml; + self::$lastLoadWasPureUserScript = false; } public static function shouldUse(Context $context): bool @@ -65,6 +74,16 @@ public static function tryInvoke(Context $context, JITVariable ...$args): ?Value } self::$lastCompileTimeXml = $lit; + self::$lastLoadWasPureUserScript = true; + // Declare textContent/nodeValue on DOMElement so forWrite hasProperty skips + // dynamic-property deprecation (hasProperty does not walk DOMNode; #23251). + $objectType = $context->type->object; + $elementClassId = $objectType->lookup(self::CLASS_ELEMENT); + foreach ([self::PROP_TEXT_CONTENT, 'nodeValue'] as $prop) { + if (!$objectType->hasProperty($elementClassId, $prop)) { + $objectType->defineProperty($elementClassId, $prop, JITVariable::TYPE_STRING); + } + } foreach (DomParseSimpleXmlIdsJitHelper::parseIndexedElementIds($lit) as $parsed) { self::materializeIndexedElement($context, $args[0], $parsed); } @@ -187,12 +206,11 @@ private static function storeElementTextContent(Context $context, Value $element private static function ensureDocumentPropertyLayout(Context $context): void { - $object = $context->type->object; - $classId = $object->lookup(self::CLASS_DOCUMENT); - if ($object->hasProperty($classId, VmDom::PROP_ELEMENT_ID_MAP)) { - return; + $objectType = $context->type->object; + $classId = $objectType->lookup(self::CLASS_DOCUMENT); + if (!$objectType->hasProperty($classId, VmDom::PROP_ELEMENT_ID_MAP)) { + $objectType->defineProperty($classId, VmDom::PROP_ELEMENT_ID_MAP, JITVariable::TYPE_VALUE); } - $object->defineProperty($classId, VmDom::PROP_ELEMENT_ID_MAP, JITVariable::TYPE_VALUE); } private static function loadObjectArg(Context $context, JITVariable $arg): Value diff --git a/ext/dom/JitDomNodeListLength.php b/ext/dom/JitDomNodeListLength.php index 15fe4eac90..1d917fa8de 100644 --- a/ext/dom/JitDomNodeListLength.php +++ b/ext/dom/JitDomNodeListLength.php @@ -25,7 +25,10 @@ public static function isDomNodeListLength(string $classLc, string $propLc): boo public static function fetch(Object_ $objectType, Value $obj): JITVariable { $context = $objectType->jitContext(); + // Tag-list GLOBAL_COUNT is only valid for pure ID-map / user-script live lists. + // After full DomLoadXMLRuntime, childNodes and query lists use DomRegistry slots (#23251). if (JitDomDocumentMethodKernel::shouldUse($context) + && JitDomLoadXMLUserScript::lastLoadWasPureUserScript() && null !== $context->module->getNamedGlobal(DomUserScriptLiveTagListLlvm::GLOBAL_COUNT) ) { return new JITVariable( diff --git a/ext/dom/JitDomSaveXML.php b/ext/dom/JitDomSaveXML.php index 3b53fb3b58..e002e5f414 100644 --- a/ext/dom/JitDomSaveXML.php +++ b/ext/dom/JitDomSaveXML.php @@ -20,7 +20,7 @@ public static function invoke(Context $context, JITVariable ...$args): Value } if (JitDomSaveXMLUserScript::shouldUse($context)) { - $us = JitDomSaveXMLUserScript::tryInvoke($context); + $us = JitDomSaveXMLUserScript::tryInvoke($context, ...$args); if (null !== $us) { return $us; } diff --git a/ext/dom/JitDomSaveXMLUserScript.php b/ext/dom/JitDomSaveXMLUserScript.php index 049794738e..2f9f1febb9 100644 --- a/ext/dom/JitDomSaveXMLUserScript.php +++ b/ext/dom/JitDomSaveXMLUserScript.php @@ -4,11 +4,15 @@ namespace PHPCompiler\ext\dom; +use PHPCompiler\ext\standard\JitStringConcat; +use PHPCompiler\JIT\Builtin\Type\ObjectInstancePropertyLlvm; use PHPCompiler\JIT\Context; use PHPCompiler\JIT\JitValueBox; +use PHPCompiler\JIT\NamedOptionalCallArgs; +use PHPCompiler\JIT\Variable as JITVariable; use PHPLLVM\Value; -/** User-script standalone AOT: pure-LLVM DOMDocument::saveXML() (#18268). */ +/** User-script standalone AOT: pure-LLVM DOMDocument::saveXML() (#18268, #23251). */ final class JitDomSaveXMLUserScript { public static function shouldUse(Context $context): bool @@ -16,8 +20,19 @@ public static function shouldUse(Context $context): bool return JitDomLoadHTMLUserScript::shouldUse($context); } - public static function tryInvoke(Context $context): ?Value + /** + * @param JITVariable ...$args document [, node] + */ + public static function tryInvoke(Context $context, JITVariable ...$args): ?Value { + // saveXML($node) after textContent mutation: serialize from node slots (#23251). + if (\count($args) >= 2 && !NamedOptionalCallArgs::isOmittedOptional($args[1])) { + $serialized = self::trySerializeNode($context, $args[1]); + if (null !== $serialized) { + return $serialized; + } + } + $xmlLit = JitDomLoadXMLUserScript::lastCompileTimeXml(); if (null === $xmlLit || '' === trim($xmlLit)) { return null; @@ -31,9 +46,77 @@ public static function tryInvoke(Context $context): ?Value return self::boxConstantString($context, $out); } + private static function trySerializeNode(Context $context, JITVariable $nodeVar): ?Value + { + if (!JitDomLoadXMLUserScript::lastLoadWasPureUserScript()) { + return null; + } + $xmlLit = JitDomLoadXMLUserScript::lastCompileTimeXml(); + if (null === $xmlLit) { + return null; + } + $node = self::loadObjectArg($context, $nodeVar); + $objectType = $context->type->object; + $elementClassId = $objectType->lookup('DOMElement'); + if (!$objectType->hasProperty($elementClassId, 'textContent')) { + $objectType->defineProperty($elementClassId, 'textContent', JITVariable::TYPE_STRING); + } + // Prefer compile-time root tag — documentElement temps often lose DOMElement type + // so runtime tagName reads can see an empty dynamic slot (#23251). + $tagStr = $context->builder->load( + $context->constantStringFromString(DomParseSimpleXmlJitHelper::rootTagArgv($xmlLit)) + ); + $textVar = ObjectInstancePropertyLlvm::propertyFetchDeclaredSlot( + $objectType, + $node, + 'DOMElement', + 'textContent', + $elementClassId + ); + $textStr = $context->helper->loadValue($textVar); + $lt = $context->builder->load($context->constantStringFromString('<')); + $gt = $context->builder->load($context->constantStringFromString('>')); + $ltSlash = $context->builder->load($context->constantStringFromString('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('DOMDocument::saveXML() node must be an object'); + } + private static function boxConstantString(Context $context, string $lit): Value { $str = $context->builder->load($context->constantStringFromString($lit)); + + return self::boxStringValue($context, $str); + } + + private static function boxStringValue(Context $context, Value $str): Value + { $owned = $context->builder->call( $context->lookupFunction('__string__separate'), $str diff --git a/lib/JIT.php b/lib/JIT.php index 707d4b5c3e..4c83e64b5b 100644 --- a/lib/JIT.php +++ b/lib/JIT.php @@ -10851,6 +10851,15 @@ private function compileBlockInternal( } assert($obj->type->type === Type::TYPE_OBJECT); $declaringClass = $this->resolvePropertyDeclaringClass($obj, $block, $propName); + // User-script AOT: documentElement temps often lose DOMElement userType (#23251). + if ( + null !== $propName + && \PHPCompiler\ext\dom\JitDomLoadXMLUserScript::lastLoadWasPureUserScript() + && \in_array(strtolower($propName), ['textcontent', 'nodevalue'], true) + && \in_array(strtolower($declaringClass), ['object', 'stdclass', ''], true) + ) { + $declaringClass = 'DOMElement'; + } $receiver = $this->loadPropertyFetchReceiver($obj); $phiDest = $this->ternaryEchoPhiPropertyFetchDest($block, $i); if (null !== $phiDest) { @@ -13561,6 +13570,14 @@ private function assignOperand(Operand $resultOp, Variable $value, bool $force = return; } + // DOMElement::$textContent / $nodeValue — before temp promotion clears receiver (#23251). + if (\PHPCompiler\ext\dom\JitDomElementTextContent::tryEmitStore( + $this->context, + $result, + $value + )) { + return; + } if ( $force && Variable::KIND_VALUE === $result->kind diff --git a/lib/JIT/Builtin/DomElementTextContentRuntime.php b/lib/JIT/Builtin/DomElementTextContentRuntime.php index 6936e3ba63..449aa7c4c8 100644 --- a/lib/JIT/Builtin/DomElementTextContentRuntime.php +++ b/lib/JIT/Builtin/DomElementTextContentRuntime.php @@ -9,18 +9,28 @@ use PHPCompiler\JIT\Context; use PHPCompiler\JIT\JitVmHelperLink; -/** JIT/AOT link for DOMElement::$textContent via DomElementTextContentJitHelper (#17954). */ +/** JIT/AOT link for DOMElement::$textContent via DomElementTextContentJitHelper (#17954, #23251). */ final class DomElementTextContentRuntime { public const ABI_NAME = '__phpc_dom_element_text_content'; + public const ABI_WRITE_TEXT_CONTENT = '__phpc_dom_element_text_content_write'; + + public const ABI_WRITE_NODE_VALUE = '__phpc_dom_element_node_value_write'; + private const HELPER_PATH = '/ext/dom/DomElementTextContentJitHelper.php'; private const HELPER = 'PHPCompiler\\ext\\dom\\DomElementTextContentJitHelper::textContentArgv'; + private const HELPER_WRITE_TEXT = 'PHPCompiler\\ext\\dom\\DomElementTextContentJitHelper::writeTextContentArgv'; + + private const HELPER_WRITE_NODE_VALUE = 'PHPCompiler\\ext\\dom\\DomElementTextContentJitHelper::writeNodeValueArgv'; + /** @var list */ private const COMPILED_HELPERS = [ self::HELPER, + self::HELPER_WRITE_TEXT, + self::HELPER_WRITE_NODE_VALUE, ]; public static function ensureLinked(Context $context): void @@ -52,4 +62,56 @@ public static function ensureLinked(Context $context): void '#17954' ); } + + public static function ensureWriteLinked(Context $context): void + { + if (JitDomDocumentMethodKernel::shouldUse($context)) { + JitDomDocumentMethodKernel::ensureElementTextContentWriteBridge($context); + JitDomDocumentMethodKernel::ensureElementNodeValueWriteBridge($context); + + return; + } + + self::ensureWriteAbi( + $context, + self::ABI_WRITE_TEXT_CONTENT, + 'dom_element_text_content_write_bridge', + self::HELPER_WRITE_TEXT + ); + self::ensureWriteAbi( + $context, + self::ABI_WRITE_NODE_VALUE, + 'dom_element_node_value_write_bridge', + self::HELPER_WRITE_NODE_VALUE + ); + } + + private static function ensureWriteAbi( + Context $context, + string $abi, + string $entry, + string $helper + ): void { + $probe = $context->module->getNamedFunction($abi); + if (null !== $probe && $probe->countBasicBlocks() > 0) { + $context->registerFunction($abi, $probe); + + return; + } + + $objPtr = $context->getTypeFromString('__object__*'); + $strPtr = $context->getTypeFromString('__string__*'); + $void = $context->context->voidType(); + JitVmHelperLink::ensureBridge( + $context, + $abi, + $entry, + [$objPtr, $strPtr], + $void, + $helper, + self::HELPER_PATH, + self::COMPILED_HELPERS, + '#23251' + ); + } } diff --git a/lib/JIT/Builtin/Type/ObjectInstancePropertyLlvm.php b/lib/JIT/Builtin/Type/ObjectInstancePropertyLlvm.php index 0e109c7c0b..a115f51e74 100644 --- a/lib/JIT/Builtin/Type/ObjectInstancePropertyLlvm.php +++ b/lib/JIT/Builtin/Type/ObjectInstancePropertyLlvm.php @@ -34,7 +34,7 @@ public static function propertyFetchOrdinary( return \PHPCompiler\ext\dom\JitDomElementNavigationProperty::fetch($object, $obj, $name); } if (\PHPCompiler\ext\dom\JitDomElementTextContent::isDomElementTextContent($classLc, strtolower($name))) { - return \PHPCompiler\ext\dom\JitDomElementTextContent::fetch($object, $obj); + return \PHPCompiler\ext\dom\JitDomElementTextContent::fetchNamed($object, $obj, $name); } if (\PHPCompiler\ext\dom\JitDomNodeListLength::isDomNodeListLength($classLc, strtolower($name))) { return \PHPCompiler\ext\dom\JitDomNodeListLength::fetch($object, $obj); diff --git a/test/fixtures/aot/cases/dom_loadxml_textcontent_detach.phpt b/test/fixtures/aot/cases/dom_loadxml_textcontent_detach.phpt new file mode 100644 index 0000000000..ed6ab15203 --- /dev/null +++ b/test/fixtures/aot/cases/dom_loadxml_textcontent_detach.phpt @@ -0,0 +1,22 @@ +--TEST-- +AOT: loadXML user-script textContent write detaches held children (#23251) +--FILE-- +loadXML(''); +$r = $d->documentElement; +$a = $r->firstChild; +$b = $a->nextSibling; +$r->textContent = 'z'; +echo 'a_parent_null=', ($a->parentNode === null ? 'true' : 'false'), "\n"; +echo 'b_parent_null=', ($b->parentNode === null ? 'true' : 'false'), "\n"; +echo 'kids=', $r->childNodes->length, "\n"; +echo 'text=', $r->textContent, "\n"; +echo 'xml=', trim($d->saveXML($r)), "\n"; +?> +--EXPECT-- +a_parent_null=true +b_parent_null=true +kids=1 +text=z +xml=z diff --git a/test/repro/issue_23251_aot_loadxml_textcontent_detach.php b/test/repro/issue_23251_aot_loadxml_textcontent_detach.php new file mode 100644 index 0000000000..753aa4bcc0 --- /dev/null +++ b/test/repro/issue_23251_aot_loadxml_textcontent_detach.php @@ -0,0 +1,12 @@ +loadXML(""); +$r = $d->documentElement; +$a = $r->firstChild; +$b = $a->nextSibling; +$r->textContent = "z"; +echo "a_parent_null=", ($a->parentNode === null ? "true" : "false"), "\n"; +echo "b_parent_null=", ($b->parentNode === null ? "true" : "false"), "\n"; +echo "kids=", $r->childNodes->length, "\n"; +echo "text=", $r->textContent, "\n"; +echo "xml=", trim($d->saveXML($r)), "\n"; From dbc6fbf6dbca78d133c9800cf98ae12cd719fccb Mon Sep 17 00:00:00 2001 From: PurHur Date: Sun, 26 Jul 2026 02:06:43 +0000 Subject: [PATCH 2/2] Docs: refresh bootstrap-inventory after #23251 rebase Co-authored-by: Cursor --- docs/bootstrap-inventory.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index bff46f0855..0bbd3ac63b 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -12,7 +12,7 @@ Regenerate: `php script/bootstrap-inventory.php` | Phase A inventory files (M2 ratio SSOT) | 6563 | | Phase A ratio-deferred paths | 0 | | Source constructs flagged (blockers) | 0 | -| Source constructs flagged (warnings) | 21129 | +| Source constructs flagged (warnings) | 21127 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -5430,7 +5430,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `lib/JIT/Builtin/WeakRefRuntime.php` | 0 | 1 | | `lib/JIT/Builtin/WeakRefSetup.php` | 0 | 1 | | `lib/JIT/Builtin/ZendDoubleStringRuntime.php` | 0 | 1 | -| `lib/JIT/Builtin/ZlibRuntime.php` | 0 | 3 | +| `lib/JIT/Builtin/ZlibRuntime.php` | 0 | 1 | | `lib/JIT/Call.php` | 0 | 1 | | `lib/JIT/Call/ClosureBind.php` | 0 | 1 | | `lib/JIT/Call/ClosureBindTo.php` | 0 | 1 | @@ -41106,7 +41106,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/BuiltinParamNames.php` **Warnings** (review for bootstrap subset): -- new ArgumentCountError (line 838) +- new ArgumentCountError (line 841) - 16 class method(s) - 1 closure(s) @@ -45841,9 +45841,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/JIT/Builtin/ZlibRuntime.php` **Warnings** (review for bootstrap subset): -- new JIT (line 244) - 7 class method(s) -- 1 closure(s) ### `lib/JIT/Call.php`