From 483849c90d4219e65d57de30daa957bcb450cd49 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sun, 23 Aug 2026 06:50:37 +0000 Subject: [PATCH] Stdlib: NestedJIT runtime mb_stripos() for AOT (#34158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend MbSearchJitHelper with striposArgv (ASCII A–Z fold + existing UTF-8 search) so non-constant haystack/needle compile under thin AOT, peer of #34146. Co-authored-by: Cursor --- ext/mbstring/JitMbSearch.php | 54 ++++++++++++ ext/mbstring/MbSearchJitHelper.php | 42 ++++++++++ ext/mbstring/mb_stripos.php | 15 +--- lib/JIT/Builtin/MbSearchRuntime.php | 17 +++- .../issue_34158_mb_stripos_runtime_aot.php | 17 ++++ .../Issue34158MbStriposRuntimeAotTest.php | 83 +++++++++++++++++++ 6 files changed, 214 insertions(+), 14 deletions(-) create mode 100644 test/repro/issue_34158_mb_stripos_runtime_aot.php create mode 100644 test/unit/Issue34158MbStriposRuntimeAotTest.php diff --git a/ext/mbstring/JitMbSearch.php b/ext/mbstring/JitMbSearch.php index 826e10cca0..e2a6054d24 100644 --- a/ext/mbstring/JitMbSearch.php +++ b/ext/mbstring/JitMbSearch.php @@ -76,6 +76,60 @@ public static function invokeStrpos(Context $context, array $args): Value return StringStrpos::boxFoundOffset($context, $found); } + /** + * mb_stripos() — fold literals, else NestedJIT {@see MbSearchJitHelper::striposArgv} (#34158). + * + * @param list $args + */ + public static function invokeStripos(Context $context, array $args): Value + { + $argc = \count($args); + if ($argc < 2 || $argc > 4) { + throw new \LogicException('mb_stripos() requires two to four arguments'); + } + $folded = self::tryStriposFold($context, $args); + if (null !== $folded) { + return $folded; + } + + $hay = JitStringBuiltinArg::lowerTrimFamilyString($context, $args[0], 'mb_stripos', 0, 'haystack'); + $needle = JitStringBuiltinArg::lowerTrimFamilyString($context, $args[1], 'mb_stripos', 1, 'needle'); + $i64 = $context->getTypeFromString('int64'); + $offset = $argc >= 3 + ? JitStrictIntArg::lower($context, $args[2], 'mb_stripos', 3, 'offset') + : $i64->constInt(0, false); + if ($argc >= 4) { + if (JITVariable::TYPE_NULL === $args[3]->type || ($args[3]->isNullConstant ?? false)) { + $encoding = 'UTF-8'; + } elseif (JITVariable::TYPE_STRING !== $args[3]->type) { + throw new \LogicException('mb_stripos() encoding must be a string literal in this compiler build'); + } else { + $encoding = $args[3]->compileTimeString ?? null; + if (null === $encoding) { + throw new \LogicException('mb_stripos() encoding must be a string literal in this compiler build'); + } + } + } else { + $encoding = 'UTF-8'; + } + self::assertSupportedEncoding($encoding); + + $savedInsert = BasicBlockHelper::tryGetInsertBlock($context); + MbSearchRuntime::ensureLinked($context); + if (null !== $savedInsert) { + BasicBlockHelper::restoreInsertBlock($context, $savedInsert); + } + + $encPtr = $context->builder->load($context->constantStringFromString($encoding)); + $found = JitNestedHelperCoerce::callHelper( + $context, + MbSearchRuntime::striposHelper($context), + [$hay, $needle, $offset, $encPtr] + ); + + return StringStrpos::boxFoundOffset($context, $found); + } + /** * @param JITVariable[] $args */ diff --git a/ext/mbstring/MbSearchJitHelper.php b/ext/mbstring/MbSearchJitHelper.php index 1a06f78da3..7dda109751 100644 --- a/ext/mbstring/MbSearchJitHelper.php +++ b/ext/mbstring/MbSearchJitHelper.php @@ -33,6 +33,48 @@ public static function strposArgv( return self::utf8Strpos($haystack, $needle, $offset); } + /** + * mb_stripos() — case-insensitive (#34158 leftover of #34146). + * + * NestedJIT-safe fold: ASCII A–Z → a–z only (UTF-8 lead bytes are ≥128 so untouched). + * Full Unicode case maps remain on the VM / compile-time fold path via {@see VmMbstring::stripos}. + */ + public static function striposArgv( + string $haystack, + string $needle, + int $offset, + string $encoding + ): int { + $haystack = self::asciiLower($haystack); + $needle = self::asciiLower($needle); + if ('ASCII' === $encoding || '8BIT' === $encoding) { + return self::byteStrpos($haystack, $needle, $offset); + } + + return self::utf8Strpos($haystack, $needle, $offset); + } + + /** ASCII A–Z → a–z; leaves UTF-8 multibyte sequences unchanged. */ + private static function asciiLower(string $string): string + { + $byteLen = \strlen($string); + $out = ''; + $i = 0; + while ($i < $byteLen) { + $ch = \substr($string, $i, 1); + $byte = \ord($ch); + if ($byte >= 65 && $byte <= 90) { + // Avoid chr() under NestedJIT (typed as mixed → TypeError). + $out = $out.\substr('abcdefghijklmnopqrstuvwxyz', $byte - 65, 1); + } else { + $out = $out.$ch; + } + $i = $i + 1; + } + + return $out; + } + private static function byteStrpos(string $haystack, string $needle, int $offset): int { $hayLen = \strlen($haystack); diff --git a/ext/mbstring/mb_stripos.php b/ext/mbstring/mb_stripos.php index 48e51c7540..fd3245a3a8 100644 --- a/ext/mbstring/mb_stripos.php +++ b/ext/mbstring/mb_stripos.php @@ -14,7 +14,9 @@ use PHPLLVM\Value; /** - * mb_stripos() — case-insensitive multibyte search (php-src ext/mbstring/mbstring.c; #7015). + * mb_stripos() — case-insensitive multibyte search (php-src ext/mbstring/mbstring.c; #7015, #34158). + * + * JIT/AOT: compile-time fold via {@see JitMbSearch}; runtime NestedJIT {@see MbSearchJitHelper}. */ final class mb_stripos extends Internal { @@ -61,15 +63,6 @@ public function execute(Frame $frame): void public function call(Context $context, JITVariable ...$args): Value { - $argc = \count($args); - if ($argc < 2 || $argc > 4) { - throw new \LogicException('mb_stripos() requires two to four arguments'); - } - $folded = JitMbSearch::tryStriposFold($context, $args); - if (null !== $folded) { - return $folded; - } - - throw new \LogicException('mb_stripos() is not lowered for JIT/AOT in this compiler build'); + return JitMbSearch::invokeStripos($context, $args); } } diff --git a/lib/JIT/Builtin/MbSearchRuntime.php b/lib/JIT/Builtin/MbSearchRuntime.php index 1e69484193..d687b43410 100644 --- a/lib/JIT/Builtin/MbSearchRuntime.php +++ b/lib/JIT/Builtin/MbSearchRuntime.php @@ -9,9 +9,10 @@ use PHPLLVM\Value\Function_ as LlvmFunction; /** - * JIT/AOT link hook for mb_strpos() — compiles MbSearchJitHelper (#34146 leftover of #27187). + * JIT/AOT link hook for mb_strpos() / mb_stripos() — compiles MbSearchJitHelper + * (#34146 / #34158 leftover of #27187). * - * php-src: ext/mbstring/mbstring.c — PHP_FUNCTION(mb_strpos) + * php-src: ext/mbstring/mbstring.c — PHP_FUNCTION(mb_strpos), PHP_FUNCTION(mb_stripos) */ final class MbSearchRuntime { @@ -19,9 +20,12 @@ final class MbSearchRuntime private const STRPOS_LOGICAL = 'PHPCompiler\\ext\\mbstring\\MbSearchJitHelper::strposArgv'; + private const STRIPOS_LOGICAL = 'PHPCompiler\\ext\\mbstring\\MbSearchJitHelper::striposArgv'; + /** @var list */ private const COMPILED_HELPERS = [ self::STRPOS_LOGICAL, + self::STRIPOS_LOGICAL, ]; public static function ensureLinked(Context $context): void @@ -36,13 +40,20 @@ public static function strposHelper(Context $context): LlvmFunction return JitVmHelperLink::lookupCompiled($context, self::STRPOS_LOGICAL, '#34146'); } + public static function striposHelper(Context $context): LlvmFunction + { + self::ensureJitHelperCompiled($context); + + return JitVmHelperLink::lookupCompiled($context, self::STRIPOS_LOGICAL, '#34158'); + } + private static function ensureJitHelperCompiled(Context $context): void { JitVmHelperLink::ensureCompiled( $context, self::HELPER_PATH, self::COMPILED_HELPERS, - '#34146' + '#34158' ); } } diff --git a/test/repro/issue_34158_mb_stripos_runtime_aot.php b/test/repro/issue_34158_mb_stripos_runtime_aot.php new file mode 100644 index 0000000000..354703eb99 --- /dev/null +++ b/test/repro/issue_34158_mb_stripos_runtime_aot.php @@ -0,0 +1,17 @@ +markTestSkipped('LLVM 9 toolchain not available'); + } + $this->assertAotMatchesZend(__DIR__.'/../repro/issue_34158_mb_stripos_runtime_aot.php'); + } + + public function testHelperAndLoweringPresent(): void + { + $root = dirname(__DIR__, 2); + $helper = (string) file_get_contents($root.'/ext/mbstring/MbSearchJitHelper.php'); + $this->assertStringContainsString('function striposArgv', $helper); + $runtime = (string) file_get_contents($root.'/lib/JIT/Builtin/MbSearchRuntime.php'); + $this->assertStringContainsString('striposHelper', $runtime); + $src = (string) file_get_contents($root.'/ext/mbstring/mb_stripos.php'); + $this->assertStringContainsString('JitMbSearch::invokeStripos', $src); + $this->assertStringContainsString('#34158', $src); + $this->assertStringNotContainsString( + "throw new \\LogicException('mb_stripos() is not lowered for JIT/AOT", + $src + ); + $this->assertFileDoesNotExist($root.'/lib/AOT/runtime/mb_stripos.c'); + } + + private function assertAotMatchesZend(string $src): void + { + $zend = $this->runPhp($src); + $aot = $this->runAot($src); + $this->assertSame($zend, $aot); + } + + private function runPhp(string $src): string + { + $cmd = escapeshellarg(PHP_BINARY).' '.escapeshellarg($src); + exec($cmd.' 2>&1', $out, $rc); + $this->assertSame(0, $rc, implode("\n", $out)); + + return implode("\n", $out); + } + + private function runAot(string $src): string + { + $root = dirname(__DIR__, 2); + $bin = sys_get_temp_dir().'/mb_stripos_34158_'.getmypid().'_'.md5($src); + $cmd = 'env PHP_COMPILER_HELPER_RUNTIME_O=0 ' + .escapeshellarg(PHP_BINARY).' ' + .escapeshellarg($root.'/bin/compile.php') + .' -o '.escapeshellarg($bin).' '.escapeshellarg($src); + $cwd = getcwd(); + chdir($root); + try { + exec($cmd.' 2>&1', $compOut, $compRc); + $this->assertSame(0, $compRc, implode("\n", $compOut)); + $this->assertFileExists($bin); + exec(escapeshellarg($bin).' 2>&1', $out, $rc); + $this->assertSame(0, $rc, implode("\n", $out)); + + return implode("\n", $out); + } finally { + chdir($cwd); + @unlink($bin); + } + } +}