diff --git a/ext/dom/VmDomInstanceInvoke.php b/ext/dom/VmDomInstanceInvoke.php index 502abb9362..36e2463278 100644 --- a/ext/dom/VmDomInstanceInvoke.php +++ b/ext/dom/VmDomInstanceInvoke.php @@ -4,24 +4,66 @@ namespace PHPCompiler\ext\dom; +use PHPCompiler\VM\ObjectEntry; use PHPCompiler\VM\Variable; use PHPCompiler\VM\VariableObject; /** * DOM instance method dispatch for JIT/AOT helpers (#17130). * - * Keep this file to a single compiled entrypoint; bodies live in VmDomJitDispatch. + * Keep this file to per-arity entrypoints; bodies live in VmDomJitDispatch. */ final class VmDomInstanceInvoke { - public static function invokeArgv(Variable $receiver, string $methodLc, Variable $argsTable): Variable + public static function invoke0Object(Variable $receiver, string $methodLc): Variable { + return self::dispatch($receiver, $methodLc); + } + + public static function invoke1Object(Variable $receiver, string $methodLc, Variable $a1): Variable + { + return self::dispatch($receiver, $methodLc, $a1); + } + + public static function invoke2Object( + Variable $receiver, + string $methodLc, + Variable $a1, + Variable $a2 + ): Variable { + return self::dispatch($receiver, $methodLc, $a1, $a2); + } + + public static function invoke3Object( + Variable $receiver, + string $methodLc, + Variable $a1, + Variable $a2, + Variable $a3 + ): Variable { + return self::dispatch($receiver, $methodLc, $a1, $a2, $a3); + } + + public static function invoke4Object( + Variable $receiver, + string $methodLc, + Variable $a1, + Variable $a2, + Variable $a3, + Variable $a4 + ): Variable { + return self::dispatch($receiver, $methodLc, $a1, $a2, $a3, $a4); + } + + private static function dispatch(Variable $receiver, string $methodLc, Variable ...$extra): Variable + { + $self = VariableObject::entry($receiver->resolveIndirect()); $ctx = VmDomJitFrame::vmContext(); - $extra = VmDomJitDispatch::unpackArgs($argsTable); - $self = VariableObject::entry($receiver); $methodLc = strtolower($methodLc); return match ($methodLc) { + 'loadhtml' => VmDomJitDispatch::loadHTML($ctx, $self, $extra), + 'getelementbyid' => VmDomJitDispatch::getElementById($self, $extra), 'createelement' => VmDomJitDispatch::createElement($ctx, $self, $extra), 'appendchild' => VmDomJitDispatch::appendChild($ctx, $self, $extra), 'setattribute' => VmDomJitDispatch::setAttribute($ctx, $self, $extra), diff --git a/ext/dom/VmDomJitDispatch.php b/ext/dom/VmDomJitDispatch.php index 9409fc219d..b7d2bd6538 100644 --- a/ext/dom/VmDomJitDispatch.php +++ b/ext/dom/VmDomJitDispatch.php @@ -15,6 +15,44 @@ */ final class VmDomJitDispatch { + /** + * @param list $extra + */ + public static function loadHTML(VmContext $ctx, ObjectEntry $document, array $extra): Variable + { + $html = self::stringArg($extra[0] ?? self::missingArg('loadHTML', 0), 'loadHTML', 0); + $options = 0; + if (isset($extra[1])) { + $optionsVar = $extra[1]->resolveIndirect(); + if (Variable::TYPE_INTEGER !== $optionsVar->type) { + throw new \TypeError('DOMDocument::loadHTML(): Argument #2 ($options) must be of type int'); + } + $options = $optionsVar->toInt(); + } + $ok = VmDom::loadHTML($ctx, $document, $html, $options, VmDomJitFrame::executingFrame()); + $var = new Variable(); + $var->bool($ok); + + return $var; + } + + /** + * @param list $extra + */ + public static function getElementById(ObjectEntry $document, array $extra): Variable + { + $id = self::stringArg($extra[0] ?? self::missingArg('getElementById', 0), 'getElementById', 0); + $found = VmDom::getElementById($document, $id); + $var = new Variable(); + if (null === $found) { + $var->null(); + } else { + $var->object($found); + } + + return $var; + } + /** * @param list $extra */ diff --git a/ext/dom/VmDomJitFrame.php b/ext/dom/VmDomJitFrame.php index 6fcbd0c961..693b99fc76 100644 --- a/ext/dom/VmDomJitFrame.php +++ b/ext/dom/VmDomJitFrame.php @@ -8,40 +8,32 @@ use PHPCompiler\VM\Context as VmContext; use PHPCompiler\Web\Superglobals; -/** Active frame lookup for DOM JIT helpers — not nested-compiled (#17130). */ +/** Active VM context for DOM JIT/AOT helpers (#17130). */ final class VmDomJitFrame { public static function vmContext(): VmContext { - $ctx = self::executingFrame()->vmContext; + $ctx = Superglobals::getActiveContext(); if (null === $ctx) { - throw new \LogicException('VmDomJitFrame requires VM context'); + throw new \LogicException( + 'VmDomJitFrame requires an active VM context in this compiler build' + ); } return $ctx; } - public static function executingFrame(): Frame + public static function executingFrame(): ?Frame { $ctx = Superglobals::getActiveContext(); if (null === $ctx) { - throw new \LogicException( - 'VmDomJitFrame requires an active VM context in this compiler build' - ); + return null; } $vm = $ctx->runtime->vm; if (null === $vm) { - throw new \LogicException( - 'VmDomJitFrame requires an active VM in this compiler build' - ); - } - $frame = $vm->currentExecutingFrame(); - if (null === $frame) { - throw new \LogicException( - 'VmDomJitFrame requires an active executing frame in this compiler build' - ); + return null; } - return $frame; + return $vm->currentExecutingFrame(); } } diff --git a/lib/JIT/Builtin/DomInstanceMethodRuntime.php b/lib/JIT/Builtin/DomInstanceMethodRuntime.php index 4327ba3782..6c4d26ae38 100644 --- a/lib/JIT/Builtin/DomInstanceMethodRuntime.php +++ b/lib/JIT/Builtin/DomInstanceMethodRuntime.php @@ -5,32 +5,91 @@ namespace PHPCompiler\JIT\Builtin; use PHPCompiler\JIT\Context; +use PHPCompiler\JIT\JitValueBox; use PHPCompiler\JIT\JitVmHelperLink; +use PHPCompiler\JIT\Variable; use PHPLLVM\Value; /** - * JIT/AOT bridge for ext/dom instance methods via DomInstanceMethodJitHelper (#17130). + * JIT/AOT bridge for ext/dom instance methods via VmDomInstanceInvoke (#17130). * * php-src: ext/dom/php_dom.c — DOM*::method handlers */ final class DomInstanceMethodRuntime { - public const ABI = '__phpc_jit_dom_instance_method'; + public const MAX_EXTRA_ARGS = 4; private const HELPER_PATH = '/ext/dom/VmDomInstanceInvoke.php'; - private const INVOKE_HELPER = 'PHPCompiler\\ext\\dom\\VmDomInstanceInvoke::invokeArgv'; + private const ABI_PREFIX = '__phpc_jit_dom_instance_method_'; + + /** @var array */ + private const INVOKE_BY_ARITY = [ + 0 => 'PHPCompiler\\ext\\dom\\VmDomInstanceInvoke::invoke0Object', + 1 => 'PHPCompiler\\ext\\dom\\VmDomInstanceInvoke::invoke1Object', + 2 => 'PHPCompiler\\ext\\dom\\VmDomInstanceInvoke::invoke2Object', + 3 => 'PHPCompiler\\ext\\dom\\VmDomInstanceInvoke::invoke3Object', + 4 => 'PHPCompiler\\ext\\dom\\VmDomInstanceInvoke::invoke4Object', + ]; /** @var list */ private const COMPILED_HELPERS = [ - self::INVOKE_HELPER, + self::INVOKE_BY_ARITY[0], + self::INVOKE_BY_ARITY[1], + self::INVOKE_BY_ARITY[2], + self::INVOKE_BY_ARITY[3], + self::INVOKE_BY_ARITY[4], ]; + public static function invoke( + Context $context, + int $extraArgCount, + string $methodLc, + Variable $receiver, + Variable ...$extraArgs + ): Value { + if ($extraArgCount !== \count($extraArgs)) { + throw new \LogicException('DomInstanceMethodRuntime arity mismatch'); + } + if ($extraArgCount > self::MAX_EXTRA_ARGS) { + throw new \LogicException('Too many arguments for DOM instance method JIT bridge'); + } + self::ensureBridge($context, $extraArgCount); + $llvmArgs = [ + self::receiverValuePtr($context, $receiver), + $context->builder->load($context->constantStringFromString($methodLc)), + ]; + foreach ($extraArgs as $arg) { + $llvmArgs[] = JitValueBox::valuePtrFromVariable($context, $arg); + } + + return $context->builder->call( + $context->lookupFunction(self::ABI_PREFIX.$extraArgCount), + ...$llvmArgs + ); + } + public static function ensureLinked(Context $context): void { - $probe = $context->module->getNamedFunction(self::ABI); + for ($i = 0; $i <= self::MAX_EXTRA_ARGS; ++$i) { + self::ensureBridge($context, $i); + } + } + + public static function ensureStandaloneBodies(Context $context): void + { + self::ensureLinked($context); + } + + public static function ensureBridge(Context $context, int $extraArgCount): void + { + if ($extraArgCount < 0 || $extraArgCount > self::MAX_EXTRA_ARGS) { + throw new \LogicException('Invalid DOM instance method JIT bridge arity'); + } + $abi = self::ABI_PREFIX.$extraArgCount; + $probe = $context->module->getNamedFunction($abi); if (null !== $probe && $probe->countBasicBlocks() > 0) { - $context->registerFunction(self::ABI, $probe); + $context->registerFunction($abi, $probe); return; } @@ -43,13 +102,17 @@ public static function ensureLinked(Context $context): void $valuePtr = $context->getTypeFromString('__value__*'); $strPtr = $context->getTypeFromString('__string__*'); + $paramTypes = [$valuePtr, $strPtr]; + for ($i = 0; $i < $extraArgCount; ++$i) { + $paramTypes[] = $valuePtr; + } JitVmHelperLink::ensureBridge( $context, - self::ABI, - 'dom_instance_method_bridge_entry', - [$valuePtr, $strPtr, $valuePtr], + $abi, + 'dom_instance_method_bridge_'.$extraArgCount, + $paramTypes, $valuePtr, - self::INVOKE_HELPER, + self::INVOKE_BY_ARITY[$extraArgCount], self::HELPER_PATH, self::COMPILED_HELPERS, '#17130' @@ -62,20 +125,23 @@ public static function ensureLinked(Context $context): void } } - public static function invoke( - Context $context, - Value $receiverBox, - string $methodLc, - Value $argsBox - ): Value { - self::ensureLinked($context); - $methodConst = $context->builder->load($context->constantStringFromString($methodLc)); + private static function receiverValuePtr(Context $context, Variable $receiver): Value + { + if (Variable::TYPE_VALUE === $receiver->type) { + return JitValueBox::valuePtrFromVariable($context, $receiver); + } + $slot = JitValueBox::alloc($context); + $ptr = JitValueBox::pointer($context, $slot); + if (Variable::TYPE_OBJECT === $receiver->type) { + $context->builder->call( + $context->lookupFunction('__value__writeObject'), + $ptr, + $context->helper->loadValue($receiver) + ); - return $context->builder->call( - $context->lookupFunction(self::ABI), - $receiverBox, - $methodConst, - $argsBox - ); + return $ptr; + } + + throw new \LogicException('DOM instance method receiver must be object or value box'); } } diff --git a/lib/JIT/Call/DomInstanceMethod.php b/lib/JIT/Call/DomInstanceMethod.php index d427b8323c..36f950bb5a 100644 --- a/lib/JIT/Call/DomInstanceMethod.php +++ b/lib/JIT/Call/DomInstanceMethod.php @@ -7,13 +7,10 @@ use PHPCompiler\JIT\Builtin\DomInstanceMethodRuntime; use PHPCompiler\JIT\Call; use PHPCompiler\JIT\Context; -use PHPCompiler\JIT\HashTableHelper; -use PHPCompiler\JIT\JitValueBox; -use PHPCompiler\JIT\NestedJitCompileScope; use PHPCompiler\JIT\Variable; use PHPLLVM\Value; -/** ext/dom instance methods — JIT/AOT via DomInstanceMethodJitHelper (#17130). */ +/** ext/dom instance methods — JIT/AOT via VmDomInstanceInvoke (#17130). */ final class DomInstanceMethod implements Call { public function __construct( @@ -27,57 +24,14 @@ public function call(Context $context, Variable ...$args): Value if ([] === $args) { throw new \LogicException($this->classLc.'::'.$this->methodLc.'() called without $this'); } - - $receiverBox = self::operandToValueBox($context, $args[0]); - $argsHt = HashTableHelper::alloc($context); - $i64 = $context->getTypeFromString('int64'); - foreach (\array_slice($args, 1) as $i => $arg) { - HashTableHelper::setAtIndex( - $context, - $argsHt, - $i64->constInt($i, false), - $arg - ); - } - $argsBox = self::boxedHashtable($context, $argsHt); - - if (!NestedJitCompileScope::isActive()) { - DomInstanceMethodRuntime::ensureLinked($context); - } - - return DomInstanceMethodRuntime::invoke($context, $receiverBox, $this->methodLc, $argsBox); - } - - private static function operandToValueBox(Context $context, Variable $arg): Value - { - if (Variable::TYPE_VALUE === $arg->type) { - return JitValueBox::valuePtrFromVariable($context, $arg); - } - $slot = JitValueBox::alloc($context); - $ptr = JitValueBox::pointer($context, $slot); - if (Variable::TYPE_OBJECT === $arg->type) { - $context->builder->call( - $context->lookupFunction('__value__writeObject'), - $ptr, - $context->helper->loadValue($arg) - ); - - return $ptr; - } - - throw new \LogicException('DOM instance method receiver must be object or value box'); - } - - private static function boxedHashtable(Context $context, Value $ht): Value - { - $slot = JitValueBox::alloc($context); - $ptr = JitValueBox::pointer($context, $slot); - $context->builder->call( - $context->lookupFunction('__value__writeHashtable'), - $ptr, - $ht + $extra = \array_slice($args, 1); + + return DomInstanceMethodRuntime::invoke( + $context, + \count($extra), + $this->methodLc, + $args[0], + ...$extra ); - - return $ptr; } } diff --git a/lib/JIT/Call/VariableToObject.php b/lib/JIT/Call/VariableToObject.php new file mode 100644 index 0000000000..02cc58622b --- /dev/null +++ b/lib/JIT/Call/VariableToObject.php @@ -0,0 +1,25 @@ +builder->call($context->lookupFunction('__value__readObject'), $ptr); + } +} diff --git a/lib/JIT/DomInstanceMethodJit.php b/lib/JIT/DomInstanceMethodJit.php index 253c3f63f4..b2b883b2ca 100644 --- a/lib/JIT/DomInstanceMethodJit.php +++ b/lib/JIT/DomInstanceMethodJit.php @@ -45,7 +45,7 @@ public static function registerKnownProxies(Context $context): void /** @var array> */ private const KNOWN_METHODS = [ - 'domdocument' => ['createelement', 'appendchild'], + 'domdocument' => ['createelement', 'appendchild', 'loadhtml', 'getelementbyid'], 'domnode' => ['appendchild'], 'domelement' => ['setattribute'], 'domtokenlist' => ['add', 'contains', 'item', 'toggle', 'remove'], diff --git a/lib/JIT/NestedVmVariableMethodLlvm.php b/lib/JIT/NestedVmVariableMethodLlvm.php index 30f2a40701..2dc4a2ca55 100644 --- a/lib/JIT/NestedVmVariableMethodLlvm.php +++ b/lib/JIT/NestedVmVariableMethodLlvm.php @@ -12,6 +12,7 @@ final class NestedVmVariableMethodLlvm /** @var array> */ private const METHOD_HANDLERS = [ 'resolveindirect' => Call\VariableResolveIndirect::class, + 'toobject' => Call\VariableToObject::class, 'tostring' => Call\VariableToString::class, 'toint' => Call\VariableToInt::class, 'tofloat' => Call\VariableToFloat::class, diff --git a/test/fixtures/aot/cases/dom/loadhtml.phpt b/test/fixtures/aot/cases/dom/loadhtml.phpt new file mode 100644 index 0000000000..c9689d8ab2 --- /dev/null +++ b/test/fixtures/aot/cases/dom/loadhtml.phpt @@ -0,0 +1,15 @@ +--TEST-- +AOT: DOMDocument::loadHTML + getElementById via DOM JIT bridge (#17130) +--ENV-- +PHP_COMPILER_PROFILE=8.4 +--FILE-- +loadHTML('
'); +$div = $d->getElementById('x'); +echo null === $div ? "no_div\n" : "div_ok\n"; +--EXPECT-- +div_ok diff --git a/test/repro/aot_dom_loadhtml_probe.php b/test/repro/aot_dom_loadhtml_probe.php new file mode 100644 index 0000000000..54b57d3aef --- /dev/null +++ b/test/repro/aot_dom_loadhtml_probe.php @@ -0,0 +1,8 @@ +loadHTML('
'); +$div = $d->getElementById('x'); +echo null === $div ? "no_div\n" : "div_ok\n"; diff --git a/test/unit/DomLoadHtmlJitDispatchTest.php b/test/unit/DomLoadHtmlJitDispatchTest.php new file mode 100644 index 0000000000..26d1bb8b98 --- /dev/null +++ b/test/unit/DomLoadHtmlJitDispatchTest.php @@ -0,0 +1,34 @@ +assertStringContainsString("'loadhtml'", $invoke); + $this->assertStringContainsString("'getelementbyid'", $invoke); + $this->assertStringContainsString('function loadHTML', $dispatch); + $this->assertStringContainsString('function getElementById', $dispatch); + } + + public function testDomInstanceMethodRuntimeDeclaresCompiledHelpers(): void + { + $source = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/DomInstanceMethodRuntime.php'); + $this->assertStringContainsString('COMPILED_HELPERS', $source); + $this->assertStringContainsString('VmDomInstanceInvoke::invoke1Object', $source); + } + + public function testNestedVariableToObjectHandlerRegistered(): void + { + $source = (string) file_get_contents(__DIR__.'/../../lib/JIT/NestedVmVariableMethodLlvm.php'); + $this->assertStringContainsString('VariableToObject::class', $source); + } +}