From 0cf6772dd804a7faa3fb3e6cff291b82d4eafe7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?PHP=20Compiler=20Ext=20=E2=80=94=20DOM=20=26=20XML?= Date: Mon, 24 Aug 2026 11:32:48 +0000 Subject: [PATCH] AOT: NestedJIT mb_trim leaves with scrub-like string ABI (#34379) Use two-string public helpers and ascending no-break private bodies so NestedJIT keeps __string__* params and does not SIGSEGV under thin AOT. Co-authored-by: Cursor --- ext/mbstring/JitMbTrim.php | 37 +++-- ext/mbstring/MbTrimJitHelper.php | 237 ++++++++++++++++++----------- lib/JIT/Builtin/MbTrimRuntime.php | 38 ++++- test/unit/MbTrimRuntimeAotTest.php | 8 +- 4 files changed, 209 insertions(+), 111 deletions(-) diff --git a/ext/mbstring/JitMbTrim.php b/ext/mbstring/JitMbTrim.php index ff6546d4424..7fa683036c8 100644 --- a/ext/mbstring/JitMbTrim.php +++ b/ext/mbstring/JitMbTrim.php @@ -86,21 +86,28 @@ public static function invoke(Context $context, int $mode, string $function, arr } BasicBlockHelper::ensureOpenInsertBlock($context, $function.'_runtime'); - $whatPtr = $context->builder->load( - $context->constantStringFromString(null === $what ? '' : $what) - ); - $encPtr = $context->builder->load($context->constantStringFromString($canonical)); - $i64 = $context->getTypeFromString('int64'); - $whatLen = null === $what ? 0 : \strlen($what); - $resultStr = $context->builder->call( - MbTrimRuntime::trimHelper($context), - $str, - $whatPtr, - $encPtr, - $i64->constInt($mode, false), - $i64->constInt(null === $what ? 1 : 0, false), - $i64->constInt($whatLen, false) - ); + if (null === $what) { + $helper = match ($mode) { + 1 => MbTrimRuntime::ltrimDefaultHelper($context), + 2 => MbTrimRuntime::rtrimDefaultHelper($context), + default => MbTrimRuntime::trimDefaultHelper($context), + }; + $encPtr = $context->builder->load($context->constantStringFromString($canonical)); + // Two-string ABI like mb_scrub — raw call; callHelper/`__value__` 1-arg SIGSEGVs. + $resultStr = $context->builder->call($helper, $str, $encPtr); + } else { + if (3 !== $mode) { + throw new \LogicException( + $function.'() with custom $characters only supports trim (both sides) in this compiler build' + ); + } + $whatPtr = $context->builder->load($context->constantStringFromString($what)); + $resultStr = $context->builder->call( + MbTrimRuntime::trimCharsHelper($context), + $str, + $whatPtr + ); + } return self::materializeOwnedString($context, $resultStr); } diff --git a/ext/mbstring/MbTrimJitHelper.php b/ext/mbstring/MbTrimJitHelper.php index da9b239c550..b6b2bd5f211 100644 --- a/ext/mbstring/MbTrimJitHelper.php +++ b/ext/mbstring/MbTrimJitHelper.php @@ -5,122 +5,181 @@ namespace PHPCompiler\ext\mbstring; /** - * mb_trim() / mb_ltrim() / mb_rtrim() NestedJIT runtime (#34379 leftover of #5957/#23883). + * mb_trim() NestedJIT runtime (#34379 leftover of #5957/#23883). * - * Single-function leaf kept small — NestedJIT of large UTF-8 CFGs SIGSEGVs under thin AOT. - * Handles ASCII default whitespace + U+00A0 (C2 A0), mode, and literal `$characters`. - * Broader Unicode defaults (U+1680…U+3000) stay on VM / compile-time fold. + * Public two-string leaves like {@see MbScrubJitHelper::scrubArgv} (`__string__*`). + * Private bodies: ascending for-loops only, no `break`/`continue`/strrev (those + * SIGSEGV under thin AOT when several leaves share a module). * - * Int params: compare `$mode` / `$useDefaultWhat` directly (NestedJIT zeros copied locals — - * peer {@see MbStrwidthJitHelper}). Haystack length via isset-index; `$what` length is - * passed as `$whatLen` (NestedJIT isset on helper string params is unreliable). - * - * php-src: ext/mbstring/mbstring.c — PHP_FUNCTION(mb_trim) / mb_ltrim / mb_rtrim + * Default charset: ASCII ws + U+00A0 (C2 A0). php-src: ext/mbstring/mbstring.c */ final class MbTrimJitHelper { - /** - * @param int $useDefaultWhat 1 = default trim set; 0 = use $what ('' = no trim) - */ - public static function trimArgv( - string $value, - string $what, - string $encoding, - int $mode, - int $useDefaultWhat, - int $whatLen - ): string { - unset($encoding); - - $doLeft = 0; - $doRight = 0; - if (1 === $mode || 3 === $mode) { - $doLeft = 1; + public static function trimDefault(string $value, string $encoding): string + { + if ('8BIT' === $encoding) { + return $value; } - if (2 === $mode || 3 === $mode) { - $doRight = 1; + + return self::trimRightBody(self::trimLeftBody($value)); + } + + public static function ltrimDefault(string $value, string $encoding): string + { + if ('8BIT' === $encoding) { + return $value; } - $byteLen = 0; - while (isset($value[$byteLen])) { - $byteLen = $byteLen + 1; - if ($byteLen > 1048576) { - break; - } + return self::trimLeftBody($value); + } + + public static function rtrimDefault(string $value, string $encoding): string + { + if ('8BIT' === $encoding) { + return $value; } - if (0 === $byteLen) { - return ''; + + return self::trimRightBody($value); + } + + public static function trimChars(string $value, string $what): string + { + if ('' === $what) { + return $value; } - $start = 0; - if (1 === $doLeft) { - while ($start < $byteLen) { - $ch = \substr($value, $start, 1); - $w = 1; - $trim = 0; - if (1 === $useDefaultWhat) { - if (' ' === $ch || "\t" === $ch || "\n" === $ch || "\r" === $ch - || "\0" === $ch || "\x0B" === $ch || "\x0C" === $ch || "\x85" === $ch) { - $trim = 1; - } elseif ("\xC2" === $ch && $start + 1 < $byteLen - && "\xA0" === \substr($value, $start + 1, 1)) { - $trim = 1; - $w = 2; + return self::trimCharsRightBody(self::trimCharsLeftBody($value, $what), $what); + } + + private static function trimLeftBody(string $value): string + { + $n = \strlen($value); + $out = ''; + $started = 0; + $prev = ''; + for ($i = 0; $i < $n; ++$i) { + $c = \substr($value, $i, 1); + $ws = 0; + if (' ' === $c || "\t" === $c || "\n" === $c || "\r" === $c + || "\0" === $c || "\x0B" === $c) { + $ws = 1; + } elseif ("\xA0" === $c && "\xC2" === $prev) { + $ws = 1; + } + if ("\xC2" === $c) { + // Hold C2 until next byte decides NBSP vs content. + $prev = $c; + } else { + if (0 === $started) { + if (1 === $ws) { + // skip leading ws (incl trailing A0 of NBSP) + } else { + if ("\xC2" === $prev) { + $out .= $prev; + } + $started = 1; + $out .= $c; } } else { - $wi = 0; - while ($wi < $whatLen) { - if (\substr($what, $wi, 1) === $ch) { - $trim = 1; - break; - } - $wi = $wi + 1; + if ("\xC2" === $prev) { + $out .= $prev; } + $out .= $c; } - if (0 === $trim) { - break; + $prev = $c; + } + } + if ("\xC2" === $prev && 1 === $started) { + $out .= $prev; + } + + return $out; + } + + private static function trimRightBody(string $value): string + { + $n = \strlen($value); + $last = -1; + $prev = ''; + for ($i = 0; $i < $n; ++$i) { + $c = \substr($value, $i, 1); + $ws = 0; + if (' ' === $c || "\t" === $c || "\n" === $c || "\r" === $c + || "\0" === $c || "\x0B" === $c) { + $ws = 1; + } elseif ("\xA0" === $c && "\xC2" === $prev) { + $ws = 1; + // previous C2 was start of NBSP — retract last if it pointed at C2 + if ($last === $i - 1) { + $last = $i - 2; } - $start = $start + $w; } + if (0 === $ws) { + $last = $i; + } + $prev = $c; + } + if ($last === $n - 1) { + return $value; + } + if ($last < 0) { + return ''; } - $end = $byteLen; - if (1 === $doRight) { - while ($end > $start) { - // Prefer NBSP (2 bytes) at end when present - $w = 1; - $ch = \substr($value, $end - 1, 1); - $trim = 0; - if (1 === $useDefaultWhat) { - if ($end - $start >= 2 && "\xC2" === \substr($value, $end - 2, 1) - && "\xA0" === $ch) { - $trim = 1; - $w = 2; - } elseif (' ' === $ch || "\t" === $ch || "\n" === $ch || "\r" === $ch - || "\0" === $ch || "\x0B" === $ch || "\x0C" === $ch || "\x85" === $ch) { - $trim = 1; - } - } else { - $wi = 0; - while ($wi < $whatLen) { - if (\substr($what, $wi, 1) === $ch) { - $trim = 1; - break; - } - $wi = $wi + 1; + return \substr($value, 0, $last + 1); + } + + private static function trimCharsLeftBody(string $value, string $what): string + { + $n = \strlen($value); + $wlen = \strlen($what); + $out = ''; + $started = 0; + for ($i = 0; $i < $n; ++$i) { + $c = \substr($value, $i, 1); + if (0 === $started) { + $hit = 0; + for ($k = 0; $k < $wlen; ++$k) { + if (\substr($what, $k, 1) === $c) { + $hit = 1; } } - if (0 === $trim) { - break; + if (0 === $hit) { + $started = 1; + $out .= $c; } - $end = $end - $w; + } else { + $out .= $c; } } - if (0 === $start && $end === $byteLen) { + return $out; + } + + private static function trimCharsRightBody(string $value, string $what): string + { + $n = \strlen($value); + $wlen = \strlen($what); + $last = -1; + for ($i = 0; $i < $n; ++$i) { + $c = \substr($value, $i, 1); + $hit = 0; + for ($k = 0; $k < $wlen; ++$k) { + if (\substr($what, $k, 1) === $c) { + $hit = 1; + } + } + if (0 === $hit) { + $last = $i; + } + } + if ($last === $n - 1) { return $value; } + if ($last < 0) { + return ''; + } - return \substr($value, $start, $end - $start); + return \substr($value, 0, $last + 1); } } diff --git a/lib/JIT/Builtin/MbTrimRuntime.php b/lib/JIT/Builtin/MbTrimRuntime.php index dff5d71b248..19b007290a6 100644 --- a/lib/JIT/Builtin/MbTrimRuntime.php +++ b/lib/JIT/Builtin/MbTrimRuntime.php @@ -17,11 +17,20 @@ final class MbTrimRuntime { private const HELPER_PATH = '/ext/mbstring/MbTrimJitHelper.php'; - private const TRIM_LOGICAL = 'PHPCompiler\\ext\\mbstring\\MbTrimJitHelper::trimArgv'; + private const TRIM_DEFAULT = 'PHPCompiler\\ext\\mbstring\\MbTrimJitHelper::trimDefault'; + + private const LTRIM_DEFAULT = 'PHPCompiler\\ext\\mbstring\\MbTrimJitHelper::ltrimDefault'; + + private const RTRIM_DEFAULT = 'PHPCompiler\\ext\\mbstring\\MbTrimJitHelper::rtrimDefault'; + + private const TRIM_CHARS = 'PHPCompiler\\ext\\mbstring\\MbTrimJitHelper::trimChars'; /** @var list */ private const COMPILED_HELPERS = [ - self::TRIM_LOGICAL, + self::TRIM_DEFAULT, + self::LTRIM_DEFAULT, + self::RTRIM_DEFAULT, + self::TRIM_CHARS, ]; public static function ensureLinked(Context $context): void @@ -29,11 +38,32 @@ public static function ensureLinked(Context $context): void self::ensureJitHelperCompiled($context); } - public static function trimHelper(Context $context): LlvmFunction + public static function trimDefaultHelper(Context $context): LlvmFunction + { + self::ensureJitHelperCompiled($context); + + return JitVmHelperLink::lookupCompiled($context, self::TRIM_DEFAULT, 'mb_trim'); + } + + public static function ltrimDefaultHelper(Context $context): LlvmFunction + { + self::ensureJitHelperCompiled($context); + + return JitVmHelperLink::lookupCompiled($context, self::LTRIM_DEFAULT, 'mb_ltrim'); + } + + public static function rtrimDefaultHelper(Context $context): LlvmFunction + { + self::ensureJitHelperCompiled($context); + + return JitVmHelperLink::lookupCompiled($context, self::RTRIM_DEFAULT, 'mb_rtrim'); + } + + public static function trimCharsHelper(Context $context): LlvmFunction { self::ensureJitHelperCompiled($context); - return JitVmHelperLink::lookupCompiled($context, self::TRIM_LOGICAL, 'mb_trim'); + return JitVmHelperLink::lookupCompiled($context, self::TRIM_CHARS, 'mb_trim_chars'); } private static function ensureJitHelperCompiled(Context $context): void diff --git a/test/unit/MbTrimRuntimeAotTest.php b/test/unit/MbTrimRuntimeAotTest.php index a8b75468fd0..17fbc1ff374 100644 --- a/test/unit/MbTrimRuntimeAotTest.php +++ b/test/unit/MbTrimRuntimeAotTest.php @@ -35,10 +35,12 @@ public function testHelperAndLoweringPresent(): void { $root = dirname(__DIR__, 2); $helper = (string) file_get_contents($root.'/ext/mbstring/MbTrimJitHelper.php'); - $this->assertStringContainsString('function trimArgv', $helper); + $this->assertStringContainsString('function trimDefault', $helper); + $this->assertStringContainsString('function ltrimDefault', $helper); + $this->assertStringContainsString('function rtrimDefault', $helper); $runtime = (string) file_get_contents($root.'/lib/JIT/Builtin/MbTrimRuntime.php'); - $this->assertStringContainsString('trimHelper', $runtime); - $this->assertStringContainsString('MbTrimJitHelper::trimArgv', $runtime); + $this->assertStringContainsString('trimDefaultHelper', $runtime); + $this->assertStringContainsString('MbTrimJitHelper::trimDefault', $runtime); $src = (string) file_get_contents($root.'/ext/mbstring/JitMbTrim.php'); $this->assertStringContainsString('MbTrimRuntime::ensureLinked', $src); $this->assertStringNotContainsString(