Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions ext/mbstring/JitMbSearch.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<JITVariable> $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
*/
Expand Down
42 changes: 42 additions & 0 deletions ext/mbstring/MbSearchJitHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 4 additions & 11 deletions ext/mbstring/mb_stripos.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
}
}
17 changes: 14 additions & 3 deletions lib/JIT/Builtin/MbSearchRuntime.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,23 @@
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
{
private const HELPER_PATH = '/ext/mbstring/MbSearchJitHelper.php';

private const STRPOS_LOGICAL = 'PHPCompiler\\ext\\mbstring\\MbSearchJitHelper::strposArgv';

private const STRIPOS_LOGICAL = 'PHPCompiler\\ext\\mbstring\\MbSearchJitHelper::striposArgv';

/** @var list<string> */
private const COMPILED_HELPERS = [
self::STRPOS_LOGICAL,
self::STRIPOS_LOGICAL,
];

public static function ensureLinked(Context $context): void
Expand All @@ -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'
);
}
}
17 changes: 17 additions & 0 deletions test/repro/issue_34158_mb_stripos_runtime_aot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

/**
* #34158 — mb_stripos() runtime (non-literal) args under AOT / NestedJIT.
* php-src: ext/mbstring/mbstring.c PHP_FUNCTION(mb_stripos)
*/
$h = 'ABC';
$n = 'b';
$miss = 'z';
$jp = '日本語';

echo 'hit=', var_export(mb_stripos($h, $n), true), "\n";
echo 'miss=', var_export(mb_stripos($h, $miss), true), "\n";
echo 'lit=', var_export(mb_stripos('ABC', 'b'), true), "\n";
echo 'jp=', var_export(mb_stripos($jp, '本'), true), "\n";
83 changes: 83 additions & 0 deletions test/unit/Issue34158MbStriposRuntimeAotTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace PHPCompiler;

use PHPUnit\Framework\TestCase;

/**
* AOT: mb_stripos() runtime args via MbSearchJitHelper (#34158 / leftover #34146).
*
* @see php-src ext/mbstring/mbstring.c PHP_FUNCTION(mb_stripos)
*
* @group llvm
* @group aot
*/
final class Issue34158MbStriposRuntimeAotTest extends TestCase
{
public function testAotRuntimeMatchMatchesZend(): void
{
if (!LlvmToolchain::hasLibrary(dirname(__DIR__, 2))) {
$this->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);
}
}
}
Loading