From 927b87ae023f5b31077b2c7ce42c9db3162d66bb Mon Sep 17 00:00:00 2001 From: PurHur Date: Thu, 18 Jun 2026 15:50:21 +0000 Subject: [PATCH] php-in-php: route JIT strip_tags through StripTagsJitHelper (#9196) JIT modules call compiled StripTagsJitHelper PHP (mirrors VmString::stripTags) instead of the default LLVM tag parser; AOT standalone keeps StringStripTagsStandaloneLlvm until native link can host compiled helpers reliably (LastErrorRuntime pattern). Co-authored-by: Cursor --- ext/standard/JitStripTags.php | 2 +- ext/standard/StripTagsJitHelper.php | 166 ++++++++++++++++++ lib/JIT/Builtin/StringStripTags.php | 109 +++++++++++- ....php => StringStripTagsStandaloneLlvm.php} | 7 +- .../compiler_lib_spine_smoke/main.php | 3 +- ...rglobalsStripTagsRuntimeStandaloneTest.php | 2 +- test/unit/StripTagsRuntimeShrinkTest.php | 28 +++ 7 files changed, 309 insertions(+), 8 deletions(-) create mode 100644 ext/standard/StripTagsJitHelper.php rename lib/JIT/Builtin/{StringStripTagsJit.php => StringStripTagsStandaloneLlvm.php} (99%) create mode 100644 test/unit/StripTagsRuntimeShrinkTest.php diff --git a/ext/standard/JitStripTags.php b/ext/standard/JitStripTags.php index 19020b4447c..84f8ff19eca 100644 --- a/ext/standard/JitStripTags.php +++ b/ext/standard/JitStripTags.php @@ -14,7 +14,7 @@ use PHPLLVM\Value; /** - * LLVM JIT/AOT helpers for strip_tags() (mirrors VmString::stripTags). + * JIT/AOT helpers for strip_tags() — runtime via StripTagsJitHelper (#9196); array allowed-tags markup here. */ final class JitStripTags { diff --git a/ext/standard/StripTagsJitHelper.php b/ext/standard/StripTagsJitHelper.php new file mode 100644 index 00000000000..e057f565248 --- /dev/null +++ b/ext/standard/StripTagsJitHelper.php @@ -0,0 +1,166 @@ +', $i + 4); + if (false !== $end) { + $i = $end + 3; + continue; + } + } + if ($i + 1 < $len && '', $i + 2); + if (false !== $end) { + $i = $end + 2; + continue; + } + } + $gt = self::findSubstring($input, '>', $i + 1); + if (false === $gt) { + $out .= $ch; + ++$i; + continue; + } + $tagContent = \substr($input, $i + 1, $gt - $i - 1); + $tagName = self::extractTagName($tagContent); + if (null !== $tagName && [] !== $allowed && self::isTagAllowed($tagName, $allowed)) { + $out .= \substr($input, $i, $gt - $i + 1); + } + $i = $gt + 1; + } + + return $out; + } + + /** + * @return list + */ + private static function parseAllowedTags(string $allowedTags): array + { + $tags = []; + $len = \strlen($allowedTags); + $i = 0; + while ($i < $len) { + if ('<' !== $allowedTags[$i]) { + ++$i; + continue; + } + $gt = self::findSubstring($allowedTags, '>', $i + 1); + if (false === $gt) { + break; + } + $name = self::extractTagName(\substr($allowedTags, $i + 1, $gt - $i - 1)); + if (null !== $name && '' !== $name) { + $tags[] = $name; + } + $i = $gt + 1; + } + + return $tags; + } + + private static function extractTagName(string $tagContent): ?string + { + $len = \strlen($tagContent); + $i = 0; + while ($i < $len && self::isTagWhitespace($tagContent[$i])) { + ++$i; + } + if ($i < $len && '/' === $tagContent[$i]) { + ++$i; + } + if ($i >= $len) { + return null; + } + $start = $i; + while ($i < $len) { + $ch = $tagContent[$i]; + if (self::isTagWhitespace($ch) || '>' === $ch || '/' === $ch) { + break; + } + if (!self::isTagChar($ch)) { + return null; + } + ++$i; + } + if ($start === $i) { + return null; + } + + return self::asciiLower(\substr($tagContent, $start, $i - $start)); + } + + /** + * @param list $allowed + */ + private static function isTagAllowed(string $tagName, array $allowed): bool + { + $tagName = self::asciiLower($tagName); + foreach ($allowed as $name) { + if ($tagName === $name) { + return true; + } + } + + return false; + } + + private static function isTagWhitespace(string $ch): bool + { + return ' ' === $ch || "\t" === $ch || "\n" === $ch || "\r" === $ch || "\0" === $ch || "\x0B" === $ch; + } + + private static function isTagChar(string $ch): bool + { + return ($ch >= 'a' && $ch <= 'z') + || ($ch >= 'A' && $ch <= 'Z') + || ($ch >= '0' && $ch <= '9'); + } + + private static function asciiLower(string $text): string + { + $out = ''; + $len = \strlen($text); + for ($i = 0; $i < $len; ++$i) { + $ch = $text[$i]; + if ($ch >= 'A' && $ch <= 'Z') { + $out .= \chr(\ord($ch) + 32); + } else { + $out .= $ch; + } + } + + return $out; + } + + private static function findSubstring(string $haystack, string $needle, int $offset = 0): int|false + { + $pos = \strpos($haystack, $needle, $offset); + + return false === $pos ? false : $pos; + } +} diff --git a/lib/JIT/Builtin/StringStripTags.php b/lib/JIT/Builtin/StringStripTags.php index b5894bfa729..bb6f97f943a 100644 --- a/lib/JIT/Builtin/StringStripTags.php +++ b/lib/JIT/Builtin/StringStripTags.php @@ -4,20 +4,123 @@ namespace PHPCompiler\JIT\Builtin; +use PHPCompiler\JIT; +use PHPCompiler\JIT\Builtin; use PHPCompiler\JIT\Context; +use PHPLLVM\Value\Function_ as LlvmFunction; /** - * JIT/AOT link for __compiler_strip_tags LLVM runtime. + * JIT/AOT link for __compiler_strip_tags via StripTagsJitHelper PHP (#9196). + * + * JIT/normal modules use compiled PHP SSOT; AOT standalone keeps {@see StringStripTagsStandaloneLlvm} + * until native link can host compiled VmString helpers reliably. */ final class StringStripTags { + private const HELPER_PATH = '/ext/standard/StripTagsJitHelper.php'; + + private const STRIP_TAGS_HELPER = 'PHPCompiler\\ext\\standard\\StripTagsJitHelper::stripTags'; + + /** @var list */ + private const COMPILED_HELPERS = [ + self::STRIP_TAGS_HELPER, + ]; + public static function ensureLinked(Context $context): void { - StringStripTagsJit::implement($context); + self::implement($context); } public static function ensureStandaloneBodies(Context $context): void { - StringStripTagsJit::implement($context); + self::implement($context); + } + + public static function implement(Context $context): void + { + if (Builtin::LOAD_TYPE_STANDALONE === $context->loadType) { + StringStripTagsStandaloneLlvm::implement($context); + + return; + } + + self::implementBridge($context, '__compiler_strip_tags', self::STRIP_TAGS_HELPER, 2); + } + + private static function implementBridge( + Context $context, + string $abiName, + string $helperLogical, + int $paramCount + ): void { + $probe = $context->module->getNamedFunction($abiName); + if (null !== $probe && $probe->countBasicBlocks() > 0) { + $context->registerFunction($abiName, $probe); + + return; + } + + $strPtr = $context->getTypeFromString('__string__*'); + $params = \array_fill(0, $paramCount, $strPtr); + $ft = $context->context->functionType($strPtr, false, ...$params); + $fn = null !== $probe + ? $probe + : $context->module->addFunction($abiName, $ft); + + self::ensureJitHelperCompiled($context); + + $entry = $fn->appendBasicBlock('strip_tags_bridge_entry'); + $context->builder->positionAtEnd($entry); + $callArgs = []; + for ($i = 0; $i < $paramCount; ++$i) { + $callArgs[] = $fn->getParam($i); + } + $result = $context->builder->call( + self::helperFunction($context, $helperLogical), + ...$callArgs + ); + $context->builder->returnValue($result); + $context->registerFunction($abiName, $fn); + $context->builder->clearInsertionPosition(); + } + + private static function helperFunction(Context $context, string $logical): LlvmFunction + { + self::ensureJitHelperCompiled($context); + $lc = \strtolower($logical); + $fn = $context->functions[$lc] ?? null; + if (null === $fn) { + throw new \LogicException($logical.' missing after StripTagsJitHelper compile (#9196)'); + } + + return $fn; + } + + private static function ensureJitHelperCompiled(Context $context): void + { + $missing = false; + foreach (self::COMPILED_HELPERS as $logical) { + if (!isset($context->functions[\strtolower($logical)])) { + $missing = true; + break; + } + } + if (!$missing) { + return; + } + + $runtime = $context->runtime; + $path = \dirname(__DIR__, 3).self::HELPER_PATH; + $block = $runtime->parseAndCompile((string) \file_get_contents($path), 'StripTagsJitHelper.php'); + if (null === $block) { + throw new \LogicException('StripTagsJitHelper.php parseAndCompile failed (#9196)'); + } + $jit = new JIT($context); + $jit->compile($block); + foreach (self::COMPILED_HELPERS as $logical) { + if (!isset($context->functions[\strtolower($logical)])) { + throw new \LogicException($logical.' was not compiled for JIT (#9196)'); + } + } } } diff --git a/lib/JIT/Builtin/StringStripTagsJit.php b/lib/JIT/Builtin/StringStripTagsStandaloneLlvm.php similarity index 99% rename from lib/JIT/Builtin/StringStripTagsJit.php rename to lib/JIT/Builtin/StringStripTagsStandaloneLlvm.php index 3bff8686a8b..3de801591f3 100644 --- a/lib/JIT/Builtin/StringStripTagsJit.php +++ b/lib/JIT/Builtin/StringStripTagsStandaloneLlvm.php @@ -12,9 +12,12 @@ use PHPLLVM\Value\Function_ as LlvmFunction; /** - * LLVM body for __compiler_strip_tags and st_* helper parity from superglobals_refresh.c. + * LLVM body for __compiler_strip_tags — AOT standalone only (#9196). + * + * JIT uses {@see StripTagsJitHelper} PHP; keep this until compiled PHP static storage is + * reliable in native standalone link (same pattern as {@see LastErrorRuntimeLlvm}). */ -final class StringStripTagsJit +final class StringStripTagsStandaloneLlvm { private const ALLOWED_TAG_CAP = 32; diff --git a/test/selfhost/compiler_lib_spine_smoke/main.php b/test/selfhost/compiler_lib_spine_smoke/main.php index b4f7f5e35f2..f66a53b0823 100644 --- a/test/selfhost/compiler_lib_spine_smoke/main.php +++ b/test/selfhost/compiler_lib_spine_smoke/main.php @@ -2075,8 +2075,9 @@ require_once __DIR__.'/../../../lib/JIT/Builtin/StringStrGetcsvJit.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringStreamCsv.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringStrftime.php'; +require_once __DIR__.'/../../../ext/standard/StripTagsJitHelper.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringStripTags.php'; -require_once __DIR__.'/../../../lib/JIT/Builtin/StringStripTagsJit.php'; +require_once __DIR__.'/../../../lib/JIT/Builtin/StringStripTagsStandaloneLlvm.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringStripcslashesRuntime.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringStripslashes.php'; require_once __DIR__.'/../../../lib/JIT/Builtin/StringStrnatcasecmp.php'; diff --git a/test/unit/JIT/SuperglobalsStripTagsRuntimeStandaloneTest.php b/test/unit/JIT/SuperglobalsStripTagsRuntimeStandaloneTest.php index 36c9d138544..d966d8ee2ec 100644 --- a/test/unit/JIT/SuperglobalsStripTagsRuntimeStandaloneTest.php +++ b/test/unit/JIT/SuperglobalsStripTagsRuntimeStandaloneTest.php @@ -9,7 +9,7 @@ use PHPUnit\Framework\TestCase; /** - * AOT standalone must define strip_tags runtime in LLVM, not superglobals_refresh.c. + * Issue #9196: standalone AOT keeps LLVM strip_tags; JIT path uses StripTagsJitHelper. * * @group aot-lint */ diff --git a/test/unit/StripTagsRuntimeShrinkTest.php b/test/unit/StripTagsRuntimeShrinkTest.php new file mode 100644 index 00000000000..33e330670aa --- /dev/null +++ b/test/unit/StripTagsRuntimeShrinkTest.php @@ -0,0 +1,28 @@ +assertStringContainsString('StripTagsJitHelper', $source); + $this->assertStringNotContainsString('StringStripTagsJit', $source); + $this->assertStringContainsString('StringStripTagsStandaloneLlvm', $source); + $this->assertFileDoesNotExist(__DIR__.'/../../lib/JIT/Builtin/StringStripTagsJit.php'); + } + + public function testStripTagsJitHelperIsSelfContained(): void + { + $source = (string) file_get_contents(__DIR__.'/../../ext/standard/StripTagsJitHelper.php'); + $this->assertStringContainsString('extractTagName', $source); + $this->assertStringNotContainsString('return VmString::', $source); + $this->assertStringNotContainsString('ctype_alpha', $source); + } +}