diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index d37cd7c6a91..90b9b70344a 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 | 4545 | -| Phase A inventory files (M2 ratio SSOT) | 4545 | +| PHP files on vm.php path | 4546 | +| Phase A inventory files (M2 ratio SSOT) | 4546 | | Phase A ratio-deferred paths | 0 | | Source constructs flagged (blockers) | 0 | -| Source constructs flagged (warnings) | 12331 | +| Source constructs flagged (warnings) | 12333 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -339,7 +339,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `ext/hash/HashContextJitHelper.php` | 0 | 1 | | `ext/hash/HashFunction.php` | 0 | 1 | | `ext/hash/JitHashAlgos.php` | 0 | 1 | -| `ext/hash/JitHashContext.php` | 0 | 2 | +| `ext/hash/JitHashContext.php` | 0 | 3 | | `ext/hash/MhashRegistry.php` | 0 | 1 | | `ext/hash/Module.php` | 0 | 16 | | `ext/hash/VmHashContext.php` | 0 | 6 | @@ -3279,6 +3279,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `lib/JIT/Builtin/PregEmptyPatternReplaceRuntime.php` | 0 | 2 | | `lib/JIT/Builtin/PregExpandRuntime.php` | 0 | 3 | | `lib/JIT/Builtin/PregMatchRuntime.php` | 0 | 3 | +| `lib/JIT/Builtin/PregReplaceCallbackArrayRuntime.php` | 0 | 1 | | `lib/JIT/Builtin/ProcessExecCaptureLlvm.php` | 0 | 1 | | `lib/JIT/Builtin/ProcessIdentityJit.php` | 0 | 3 | | `lib/JIT/Builtin/ProcessOpen.php` | 0 | 1 | @@ -6187,34 +6188,34 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new DOMException (line 3704) - new DOMException (line 3745) - new DOMException (line 3770) -- new DOMException (line 4394) -- new Variable (line 4418) -- new DOMException (line 4648) -- new DOMException (line 4729) -- new DOMException (line 4771) -- new ObjectEntry (line 4856) -- new DomNodeState (line 4860) -- new Variable (line 4867) -- new ObjectEntry (line 5067) -- new DomNodeState (line 5071) -- new Variable (line 5078) -- new ObjectEntry (line 5098) -- new DomNodeState (line 5101) -- new Variable (line 5109) -- new Variable (line 5254) -- new DOMException (line 5338) -- new DOMException (line 5345) +- new DOMException (line 4408) +- new Variable (line 4432) +- new DOMException (line 4662) +- new DOMException (line 4743) +- new DOMException (line 4785) +- new ObjectEntry (line 4870) +- new DomNodeState (line 4874) +- new Variable (line 4881) +- new ObjectEntry (line 5081) +- new DomNodeState (line 5085) +- new Variable (line 5092) +- new ObjectEntry (line 5112) +- new DomNodeState (line 5115) +- new Variable (line 5123) +- new Variable (line 5268) - new DOMException (line 5352) -- new DOMException (line 5356) - new DOMException (line 5359) -- new DOMException (line 5368) -- new Variable (line 6154) -- new Variable (line 6167) -- new DOMException (line 6249) -- new DOMException (line 6260) -- new DOMException (line 6326) -- new DOMException (line 6334) -- new DOMException (line 6343) +- new DOMException (line 5366) +- new DOMException (line 5370) +- new DOMException (line 5373) +- new DOMException (line 5382) +- new Variable (line 6168) +- new Variable (line 6181) +- new DOMException (line 6263) +- new DOMException (line 6274) +- new DOMException (line 6340) +- new DOMException (line 6348) +- new DOMException (line 6357) - 302 class method(s) - 2 closure(s) @@ -6685,7 +6686,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/hash/HashContextJitHelper.php` **Warnings** (review for bootstrap subset): -- 8 class method(s) +- 9 class method(s) ### `ext/hash/HashFunction.php` @@ -6700,8 +6701,9 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/hash/JitHashContext.php` **Warnings** (review for bootstrap subset): -- new JITVariable (line 151) -- 13 class method(s) +- new JITVariable (line 276) +- new JITVariable (line 303) +- 18 class method(s) ### `ext/hash/MhashRegistry.php` @@ -14759,7 +14761,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/standard/PregJitHelper.php` **Warnings** (review for bootstrap subset): -- 11 class method(s) +- 12 class method(s) - 1 closure(s) ### `ext/standard/PregQuoteJitHelper.php` @@ -24408,11 +24410,11 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new CompileError (line 135) - new CompileError (line 164) - new CompileError (line 182) -- new CompileError (line 229) +- new CompileError (line 224) +- new CompileError (line 242) - new CompileError (line 247) -- new CompileError (line 252) -- new CompileError (line 272) -- new CompileError (line 281) +- new CompileError (line 267) +- new CompileError (line 276) - 23 class method(s) ### `lib/Compiler/SourceLocation.php` @@ -25616,7 +25618,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/JIT/Builtin/HashContextEmbedBridge.php` **Warnings** (review for bootstrap subset): -- new JIT (line 84) +- new JIT (line 81) - 3 class method(s) - 1 closure(s) @@ -26338,10 +26340,15 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/JIT/Builtin/PregMatchRuntime.php` **Warnings** (review for bootstrap subset): -- new JIT (line 496) +- new JIT (line 500) - 15 class method(s) - 2 closure(s) +### `lib/JIT/Builtin/PregReplaceCallbackArrayRuntime.php` + +**Warnings** (review for bootstrap subset): +- 4 class method(s) + ### `lib/JIT/Builtin/ProcessExecCaptureLlvm.php` **Warnings** (review for bootstrap subset): @@ -28298,28 +28305,28 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new Variable (line 2309) - new Variable (line 2322) - new Variable (line 2481) -- new VMVariable (line 3167) -- new VMVariable (line 3185) -- new VMVariable (line 3193) -- new VMVariable (line 3211) -- new ReflectionClassConstant (line 4350) -- new Variable (line 4714) -- new Variable (line 4727) -- new Variable (line 4861) -- new Variable (line 5128) -- new Variable (line 5154) -- new Variable (line 5162) -- new Variable (line 5185) -- new Variable (line 5211) -- new Variable (line 5219) -- new Variable (line 5293) -- new Literal (line 5310) +- new VMVariable (line 3169) +- new VMVariable (line 3187) +- new VMVariable (line 3195) +- new VMVariable (line 3213) +- new ReflectionClassConstant (line 4355) +- new Variable (line 4719) +- new Variable (line 4732) +- new Variable (line 4866) +- new Variable (line 5133) +- new Variable (line 5159) +- new Variable (line 5167) +- new Variable (line 5190) +- new Variable (line 5216) +- new Variable (line 5224) +- new Variable (line 5298) - new Literal (line 5315) - new Literal (line 5320) -- new Variable (line 5331) -- new Variable (line 5364) -- new Variable (line 5387) -- new Variable (line 5751) +- new Literal (line 5325) +- new Variable (line 5336) +- new Variable (line 5369) +- new Variable (line 5392) +- new Variable (line 5756) - 259 class method(s) - 9 closure(s) diff --git a/docs/capabilities.md b/docs/capabilities.md index 1e6620715df..b0d97bbdc05 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -639,7 +639,7 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand. | `preg_quote` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `preg_replace` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `preg_replace_callback` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | -| `preg_replace_callback_array` | yes | yes | yes | standard | VM any callable; JIT/AOT deferred (#3568, #1177); closures use VM lowering | +| `preg_replace_callback_array` | yes | yes | yes | standard | VM any callable; JIT/AOT via PregReplaceCallbackArrayRuntime + PregJitHelper (#3568); AOT PHPT | | `preg_split` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `prev` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `print_r` | yes | yes | yes | standard | JIT PHPT | diff --git a/ext/standard/JitPregReplaceCallbackArray.php b/ext/standard/JitPregReplaceCallbackArray.php index 876a716a327..675fe3476f1 100644 --- a/ext/standard/JitPregReplaceCallbackArray.php +++ b/ext/standard/JitPregReplaceCallbackArray.php @@ -4,17 +4,55 @@ namespace PHPCompiler\ext\standard; +use PHPCompiler\JIT\BasicBlockHelper; +use PHPCompiler\JIT\Builtin\PregReplaceCallbackArrayRuntime; +use PHPCompiler\JIT\Builtin\StringPregMatch; use PHPCompiler\JIT\Context; +use PHPCompiler\JIT\JitStringArg; +use PHPCompiler\JIT\JitValueBox; use PHPCompiler\JIT\Variable as JITVariable; +use PHPLLVM\Builder; use PHPLLVM\Value; -/** LLVM lowering for preg_replace_callback_array() — chain JitPregReplaceCallback (#3568). */ +/** LLVM lowering for preg_replace_callback_array() via PregReplaceCallbackArrayRuntime (#3568). */ final class JitPregReplaceCallbackArray { + private static int $blockSerial = 0; + public static function invoke(Context $context, JITVariable $patterns, JITVariable $subject): Value { - throw new \LogicException( - 'preg_replace_callback_array() is not implemented for JIT/AOT in this compiler build; use bin/vm.php (#3568)' + StringPregMatch::ensureLinked($context); + PregReplaceCallbackArrayRuntime::ensureLinked($context); + + $raw = $context->builder->call( + $context->lookupFunction(PregReplaceCallbackArrayRuntime::ABI_REPLACE_CALLBACK_ARRAY), + PregReplaceCallbackArrayRuntime::patternsToHashtable($context, $patterns), + JitStringArg::lower($context, $subject, 'preg_replace_callback_array() subject') ); + + $strPtrTy = $context->getTypeFromString('__string__*'); + $isError = $context->builder->icmp(Builder::INT_EQ, $raw, $strPtrTy->constNull()); + + $id = (string) (++self::$blockSerial); + $failBlock = BasicBlockHelper::append($context, 'preg_replace_callback_array_fail_'.$id); + $okBlock = BasicBlockHelper::append($context, 'preg_replace_callback_array_ok_'.$id); + $doneBlock = BasicBlockHelper::append($context, 'preg_replace_callback_array_done_'.$id); + + $slot = JitValueBox::alloc($context); + $ptr = JitValueBox::pointer($context, $slot); + $context->builder->branchIf($isError, $failBlock, $okBlock); + + $context->builder->positionAtEnd($failBlock); + $i1 = $context->getTypeFromString('int1'); + JitValueBox::writeBool($context, $slot, $i1->constInt(0, false)); + $context->builder->branch($doneBlock); + + $context->builder->positionAtEnd($okBlock); + $context->builder->call($context->lookupFunction('__value__writeString'), $ptr, $raw); + $context->builder->branch($doneBlock); + + $context->builder->positionAtEnd($doneBlock); + + return $ptr; } } diff --git a/ext/standard/PregJitHelper.php b/ext/standard/PregJitHelper.php index c9fd9d6d8cb..c5d5a5204a3 100644 --- a/ext/standard/PregJitHelper.php +++ b/ext/standard/PregJitHelper.php @@ -130,4 +130,28 @@ static function (array $matches) use ($callbackFnAddr): string { } ); } + + public static function replaceCallbackArrayArgv(HashTable $patterns, string $subject): ?string + { + $ctx = \PHPCompiler\Web\Superglobals::getActiveContext(); + if (null === $ctx) { + throw new \LogicException( + 'PregJitHelper::replaceCallbackArrayArgv() requires an active VM context in this compiler build' + ); + } + + $subjectVar = new \PHPCompiler\VM\Variable(); + $subjectVar->string($subject); + $result = VmPregReplaceCallbackArray::invoke($ctx, $patterns, $subjectVar); + if (false === $result) { + return null; + } + if (\is_string($result)) { + return $result; + } + + throw new \LogicException( + 'preg_replace_callback_array() array subject is not supported for JIT/AOT in this compiler build' + ); + } } diff --git a/ext/standard/preg_replace_callback_array.php b/ext/standard/preg_replace_callback_array.php index a20c2d939db..a030f945562 100644 --- a/ext/standard/preg_replace_callback_array.php +++ b/ext/standard/preg_replace_callback_array.php @@ -14,7 +14,7 @@ use PHPLLVM\Value; /** - * preg_replace_callback_array() — VM any callable; JIT/AOT string user-function callbacks (#3568). + * preg_replace_callback_array() — VM any callable; JIT/AOT via PregReplaceCallbackArrayRuntime (#3568). * * php-src: ext/pcre/php_pcre.c — PHP_FUNCTION(preg_replace_callback_array) */ diff --git a/lib/JIT/Builtin/PregMatchRuntime.php b/lib/JIT/Builtin/PregMatchRuntime.php index 0c083420eb2..bc6b438d7eb 100644 --- a/lib/JIT/Builtin/PregMatchRuntime.php +++ b/lib/JIT/Builtin/PregMatchRuntime.php @@ -46,6 +46,9 @@ final class PregMatchRuntime private const REPLACE_CALLBACK_HELPER = 'PHPCompiler\\ext\\standard\\PregJitHelper::replaceCallbackArgv'; + private const REPLACE_CALLBACK_ARRAY_HELPER = + 'PHPCompiler\\ext\\standard\\PregJitHelper::replaceCallbackArrayArgv'; + private const INVOKE_CALLBACK_HELPER = 'PHPCompiler\\ext\\standard\\PregCallbackInvokeJitHelper::invoke'; private const SPLIT_HELPER = 'PHPCompiler\\ext\\standard\\PregJitHelper::splitArgv'; @@ -62,6 +65,7 @@ final class PregMatchRuntime self::TAKE_MATCH_ALL_EX_HT, self::REPLACE_HELPER, self::REPLACE_CALLBACK_HELPER, + self::REPLACE_CALLBACK_ARRAY_HELPER, self::SPLIT_HELPER, ]; diff --git a/lib/JIT/Builtin/PregReplaceCallbackArrayRuntime.php b/lib/JIT/Builtin/PregReplaceCallbackArrayRuntime.php new file mode 100644 index 00000000000..837989707f7 --- /dev/null +++ b/lib/JIT/Builtin/PregReplaceCallbackArrayRuntime.php @@ -0,0 +1,97 @@ +type)) { + return ArrayBuiltinHelper::nativeListToHashTable($context, $arg); + } + + return ArrayBuiltinHelper::loadHashTable($context, $arg); + } + + public static function ensureLinked(Context $context): void + { + PregMatchRuntime::ensureLinked($context); + self::implementBridgeIfMissing($context); + } + + public static function ensureStandaloneBodies(Context $context): void + { + self::ensureLinked($context); + } + + private static function implementBridgeIfMissing(Context $context): void + { + $abiName = self::ABI_REPLACE_CALLBACK_ARRAY; + $probe = $context->module->getNamedFunction($abiName); + if (null !== $probe && $probe->countBasicBlocks() > 0) { + $context->registerFunction($abiName, $probe); + + return; + } + + $savedBlock = null; + try { + $savedBlock = $context->builder->getInsertBlock(); + } catch (\Throwable) { + } + + $strPtr = $context->getTypeFromString('__string__*'); + $htPtr = $context->getTypeFromString('__hashtable__*'); + $fn = null !== $probe + ? $probe + : $context->module->addFunction( + $abiName, + $context->context->functionType($strPtr, false, $htPtr, $strPtr) + ); + $entry = $fn->appendBasicBlock('preg_replace_callback_array_entry'); + $failBb = $fn->appendBasicBlock('preg_replace_callback_array_fail'); + $okBb = $fn->appendBasicBlock('preg_replace_callback_array_ok'); + $context->builder->positionAtEnd($entry); + $helperFn = JitVmHelperLink::lookupCompiled($context, self::REPLACE_CALLBACK_ARRAY_HELPER, '#3568'); + $raw = $context->builder->call( + $helperFn, + $fn->getParam(0), + $fn->getParam(1) + ); + $isNull = JitNestedHelperCoerce::isHelperResultNull($context, $raw); + $context->builder->branchIf($isNull, $failBb, $okBb); + + $context->builder->positionAtEnd($failBb); + $context->builder->returnValue($strPtr->constNull()); + + $context->builder->positionAtEnd($okBb); + $context->builder->returnValue(JitNestedHelperCoerce::extractStringPtrFromHelperResult($context, $raw)); + $context->registerFunction($abiName, $fn); + + if (null !== $savedBlock) { + $context->builder->positionAtEnd($savedBlock); + } else { + $context->builder->clearInsertionPosition(); + } + } +} diff --git a/script/capability-matrix.php b/script/capability-matrix.php index dd9a71e0c09..9cccbcd54a5 100755 --- a/script/capability-matrix.php +++ b/script/capability-matrix.php @@ -168,7 +168,7 @@ function analyzeInternal(PHPCompiler\Func\Internal $fn): array $notes[] = 'compile-time string user-function callbacks; closures deferred (#1177, #142)'; } if ('preg_replace_callback_array' === $fn->getName() && str_contains($source, 'VmPregReplaceCallbackArray::invoke')) { - $notes[] = 'VM any callable; JIT/AOT deferred (#3568, #1177); closures use VM lowering'; + $notes[] = 'VM any callable; JIT/AOT via PregReplaceCallbackArrayRuntime + PregJitHelper (#3568)'; } if (\PHPCompiler\JIT\SelfHostBuiltinPolicy::isVmOnlyDeferred($fn->getName())) { $deferLib = __DIR__.'/stdlib-jit-deferred-lib.php'; diff --git a/test/fixtures/aot/cases/preg_replace_callback_array.phpt b/test/fixtures/aot/cases/preg_replace_callback_array.phpt new file mode 100644 index 00000000000..43c7d7e3d9a --- /dev/null +++ b/test/fixtures/aot/cases/preg_replace_callback_array.phpt @@ -0,0 +1,13 @@ +--TEST-- +AOT: preg_replace_callback_array() closure pattern map (issue #3568) +--FILE-- + fn(array $m): string => '[' . $m[0] . ']'], + 'a1b2' +); +echo $out, "\n"; +--EXPECT-- +a[1]b[2] +--EXPECT_EXIT-- +0 diff --git a/test/unit/PregReplaceCallbackArrayRuntimeShrinkTest.php b/test/unit/PregReplaceCallbackArrayRuntimeShrinkTest.php new file mode 100644 index 00000000000..8700d3fc690 --- /dev/null +++ b/test/unit/PregReplaceCallbackArrayRuntimeShrinkTest.php @@ -0,0 +1,26 @@ +assertStringContainsString('PregReplaceCallbackArrayRuntime::ensureLinked', $jit); + $this->assertStringNotContainsString('not implemented for JIT/AOT', $jit); + + $runtime = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/PregReplaceCallbackArrayRuntime.php'); + $this->assertStringContainsString('PregJitHelper::replaceCallbackArrayArgv', $runtime); + $this->assertStringContainsString('PregMatchRuntime::ensureLinked', $runtime); + + $helper = (string) file_get_contents(__DIR__.'/../../ext/standard/PregJitHelper.php'); + $this->assertStringContainsString('replaceCallbackArrayArgv', $helper); + $this->assertStringContainsString('VmPregReplaceCallbackArray::invoke', $helper); + } +}