From 97798f00a0ce4f963d8b19ba1519e6f2a465c81d Mon Sep 17 00:00:00 2001 From: PurHur Date: Sun, 2 Aug 2026 09:37:00 +0000 Subject: [PATCH 1/2] Stdlib: fix AOT hexdec/bindec NestedJIT insert and string ABI (#26884) Restore the IR insert block after mid-emit helper/bridge link, pass __string__* into phpc_basetozval_result, and parse digits with NestedJIT-safe recursion so AOT matches Zend for the issue repro (255/10). Co-authored-by: Cursor --- ext/standard/MathBaseConvertJitHelper.php | 285 +++++++++++++++++- ext/standard/Module.php | 5 +- ext/standard/base_convert_.php | 3 +- ext/standard/bindec.php | 5 +- ext/standard/hexdec.php | 5 +- ext/standard/octdec.php | 5 +- lib/JIT/Builtin/MathBaseConvert.php | 4 +- lib/JIT/Builtin/MathBaseConvertRuntime.php | 57 ++-- test/unit/HexdecBindecAot26884Test.php | 49 +++ .../unit/MathBaseConvertRuntimeShrinkTest.php | 21 +- 10 files changed, 375 insertions(+), 64 deletions(-) create mode 100644 test/unit/HexdecBindecAot26884Test.php diff --git a/ext/standard/MathBaseConvertJitHelper.php b/ext/standard/MathBaseConvertJitHelper.php index 726a4c63bcf..048970fd0a3 100644 --- a/ext/standard/MathBaseConvertJitHelper.php +++ b/ext/standard/MathBaseConvertJitHelper.php @@ -5,10 +5,18 @@ namespace PHPCompiler\ext\standard; /** - * base_convert() / radix parse for compiled JIT/AOT modules (#9584, php-in-PHP). + * base_convert() / radix parse for compiled JIT/AOT modules (#9584, #26884, php-in-PHP). * - * SSOT: {@see VmMath} - * php-src: ext/standard/math.c — _php_math_basetozval, _php_math_zvaltobase, PHP_FUNCTION(base_convert) + * NestedJIT thin-AOT constraints (#26884): + * - Do not call {@see VmMath} — NestedJIT stubs unresolved cross-class statics to null. + * - Avoid sprintf / ctype_* / unpack. + * - Use substr() for portable one-byte chars (Zend offset ≠ NestedJIT int byte). + * - Prefer recursion over while/for accumulation — NestedJIT loop induction failed to + * advance past the first digit (hexdec("ff") → 15) while unrolled/recursive paths are green. + * - Bridge must pass `__string__*`. + * + * SSOT algorithm mirrors VmMath::baseToZval / baseConvert. + * php-src: ext/standard/math.c */ final class MathBaseConvertJitHelper { @@ -20,23 +28,40 @@ final class MathBaseConvertJitHelper public static function baseConvert(string $number, int $fromBase, int $toBase): string { - $result = VmMath::baseConvert($number, $fromBase, $toBase); - self::$lastInvalidChars = VmMath::takeInvalidRadixCharsDeprecation(); + if ($fromBase < 2 || $fromBase > 36) { + throw new \ValueError('base_convert(): Argument #2 ($from_base) must be between 2 and 36 (inclusive)'); + } + if ($toBase < 2 || $toBase > 36) { + throw new \ValueError('base_convert(): Argument #3 ($to_base) must be between 2 and 36 (inclusive)'); + } + + $tag = self::parseBaseToZval($number, $fromBase); + if (1 === $tag) { + return self::doubleToBase(self::$lastDouble, $toBase); + } - return $result; + return self::longToBase(self::$lastLong, $toBase); } /** @return int 0=long result, 1=double result (LLVM i32 ABI) */ public static function parseBaseToZval(string $str, int $base): int { - $value = VmMath::baseToZval($str, $base); - self::$lastInvalidChars = VmMath::takeInvalidRadixCharsDeprecation(); - if (\is_float($value)) { - self::$lastDouble = $value; + self::$lastInvalidChars = false; + self::$lastLong = 0; + self::$lastDouble = 0.0; + + $len = \strlen($str); + $start = self::skipLeadingSpaces($str, 0, $len); + $end = self::skipTrailingSpaces($str, $start, $len); + $start = self::skipRadixPrefix($str, $start, $end, $base); + + $parsed = self::parseRec($str, $start, $end, $base, 0); + if ($parsed < 0) { + self::$lastDouble = self::floatRec($str, $start, $end, $base, 0.0); return 1; } - self::$lastLong = (int) $value; + self::$lastLong = $parsed; return 0; } @@ -64,4 +89,242 @@ public static function resetForTest(): void self::$lastDouble = 0.0; self::$lastInvalidChars = false; } + + private static function skipLeadingSpaces(string $str, int $start, int $len): int + { + if ($start >= $len) { + return $start; + } + if (!self::isSpaceChar(\substr($str, $start, 1))) { + return $start; + } + + return self::skipLeadingSpaces($str, $start + 1, $len); + } + + private static function skipTrailingSpaces(string $str, int $start, int $end): int + { + if ($end <= $start) { + return $end; + } + if (!self::isSpaceChar(\substr($str, $end - 1, 1))) { + return $end; + } + + return self::skipTrailingSpaces($str, $start, $end - 1); + } + + private static function skipRadixPrefix(string $str, int $start, int $end, int $base): int + { + if ($end - $start < 2) { + return $start; + } + $c0 = \substr($str, $start, 1); + $c1 = \substr($str, $start + 1, 1); + if (16 === $base && '0' === $c0 && ('x' === $c1 || 'X' === $c1)) { + return $start + 2; + } + if (8 === $base && '0' === $c0 && ('o' === $c1 || 'O' === $c1)) { + return $start + 2; + } + if (2 === $base && '0' === $c0 && ('b' === $c1 || 'B' === $c1)) { + return $start + 2; + } + + return $start; + } + + /** + * Recursive digit walk — NestedJIT-safe (#26884). + * + * @return int accumulated long, or -1 when integer range exhausted (use floatRec) + */ + private static function parseRec(string $str, int $pos, int $end, int $base, int $num): int + { + if ($pos >= $end) { + return $num; + } + $digit = self::radixDigitChar(\substr($str, $pos, 1), $base); + if ($digit < 0) { + self::$lastInvalidChars = true; + + return self::parseRec($str, $pos + 1, $end, $base, $num); + } + + $cutoffDiv = (int) (9223372036854775807 / $base); + $cutlim = 9223372036854775807 - $cutoffDiv * $base; + if ($num > $cutoffDiv || ($num === $cutoffDiv && $digit > $cutlim)) { + return -1; + } + + return self::parseRec($str, $pos + 1, $end, $base, $num * $base + $digit); + } + + private static function floatRec(string $str, int $pos, int $end, int $base, float $fnum): float + { + if ($pos >= $end) { + return $fnum; + } + $digit = self::radixDigitChar(\substr($str, $pos, 1), $base); + if ($digit < 0) { + self::$lastInvalidChars = true; + + return self::floatRec($str, $pos + 1, $end, $base, $fnum); + } + + return self::floatRec($str, $pos + 1, $end, $base, $fnum * (float) $base + (float) $digit); + } + + private static function isSpaceChar(string $c): bool + { + return ' ' === $c || "\t" === $c || "\n" === $c || "\r" === $c || "\v" === $c || "\f" === $c; + } + + /** @return int digit or -1 when invalid for $base */ + private static function radixDigitChar(string $c, int $base): int + { + if ('0' === $c) { + $digit = 0; + } elseif ('1' === $c) { + $digit = 1; + } elseif ('2' === $c) { + $digit = 2; + } elseif ('3' === $c) { + $digit = 3; + } elseif ('4' === $c) { + $digit = 4; + } elseif ('5' === $c) { + $digit = 5; + } elseif ('6' === $c) { + $digit = 6; + } elseif ('7' === $c) { + $digit = 7; + } elseif ('8' === $c) { + $digit = 8; + } elseif ('9' === $c) { + $digit = 9; + } elseif ('A' === $c || 'a' === $c) { + $digit = 10; + } elseif ('B' === $c || 'b' === $c) { + $digit = 11; + } elseif ('C' === $c || 'c' === $c) { + $digit = 12; + } elseif ('D' === $c || 'd' === $c) { + $digit = 13; + } elseif ('E' === $c || 'e' === $c) { + $digit = 14; + } elseif ('F' === $c || 'f' === $c) { + $digit = 15; + } elseif ('G' === $c || 'g' === $c) { + $digit = 16; + } elseif ('H' === $c || 'h' === $c) { + $digit = 17; + } elseif ('I' === $c || 'i' === $c) { + $digit = 18; + } elseif ('J' === $c || 'j' === $c) { + $digit = 19; + } elseif ('K' === $c || 'k' === $c) { + $digit = 20; + } elseif ('L' === $c || 'l' === $c) { + $digit = 21; + } elseif ('M' === $c || 'm' === $c) { + $digit = 22; + } elseif ('N' === $c || 'n' === $c) { + $digit = 23; + } elseif ('O' === $c || 'o' === $c) { + $digit = 24; + } elseif ('P' === $c || 'p' === $c) { + $digit = 25; + } elseif ('Q' === $c || 'q' === $c) { + $digit = 26; + } elseif ('R' === $c || 'r' === $c) { + $digit = 27; + } elseif ('S' === $c || 's' === $c) { + $digit = 28; + } elseif ('T' === $c || 't' === $c) { + $digit = 29; + } elseif ('U' === $c || 'u' === $c) { + $digit = 30; + } elseif ('V' === $c || 'v' === $c) { + $digit = 31; + } elseif ('W' === $c || 'w' === $c) { + $digit = 32; + } elseif ('X' === $c || 'x' === $c) { + $digit = 33; + } elseif ('Y' === $c || 'y' === $c) { + $digit = 34; + } elseif ('Z' === $c || 'z' === $c) { + $digit = 35; + } else { + return -1; + } + + return $digit < $base ? $digit : -1; + } + + private static function longToBase(int $arg, int $base): string + { + if ($base < 2 || $base > 36) { + return ''; + } + if (0 === $arg) { + return '0'; + } + + $negative = $arg < 0; + $n = $negative ? -$arg : $arg; + if ($negative && $arg === -9223372036854775807 - 1) { + return self::doubleToBase((float) $arg, $base); + } + $digits = '0123456789abcdefghijklmnopqrstuvwxyz'; + $out = ''; + while ($n > 0) { + $out = $digits[$n % $base].$out; + $n = (int) ($n / $base); + } + + return $negative ? '-'.$out : $out; + } + + private static function doubleToBase(float $fvalue, int $base): string + { + if ($base < 2 || $base > 36) { + return ''; + } + if ($fvalue === \INF || $fvalue === -\INF) { + throw new \ValueError('An infinite value cannot be converted to base '.(string) $base); + } + + if ($fvalue >= 0.0) { + $fvalue = (float) ((int) $fvalue); + } else { + $trunc = (float) ((int) $fvalue); + $fvalue = $trunc === $fvalue ? $trunc : $trunc - 1.0; + } + if (0.0 === $fvalue) { + return '0'; + } + + $negative = $fvalue < 0.0; + if ($negative) { + $fvalue = -$fvalue; + } + + $digits = '0123456789abcdefghijklmnopqrstuvwxyz'; + $buf = ''; + while ($fvalue >= 1.0) { + $whole = (int) ($fvalue / (float) $base); + $digit = (int) ($fvalue - ((float) $whole) * (float) $base); + if ($digit < 0) { + $digit = 0; + } + if ($digit >= $base) { + $digit = $base - 1; + } + $buf = $digits[$digit].$buf; + $fvalue = $fvalue / (float) $base; + } + + return $negative ? '-'.$buf : $buf; + } } diff --git a/ext/standard/Module.php b/ext/standard/Module.php index 74ad6918a7f..70a442856d0 100755 --- a/ext/standard/Module.php +++ b/ext/standard/Module.php @@ -1064,12 +1064,13 @@ public function jitInit(JIT\Context $context): void try { $context->lookupFunction('phpc_basetozval_result'); } catch (\Throwable $e) { - $charPtr = $context->getTypeFromString('char*'); + // `__string__*` ABI — NestedJIT-safe (peer #26884); was char*/i8* + strlen init. + $strPtr = $context->getTypeFromString('__string__*'); $i64 = $context->getTypeFromString('int64'); $i64Ptr = $context->getTypeFromString('int64*'); $doublePtr = $context->getTypeFromString('double*'); $i32 = $context->getTypeFromString('int32'); - $ft = $context->context->functionType($i32, false, $charPtr, $i64, $i64Ptr, $doublePtr); + $ft = $context->context->functionType($i32, false, $strPtr, $i64, $i64Ptr, $doublePtr); $fn = $context->module->addFunction('phpc_basetozval_result', $ft); $context->registerFunction('phpc_basetozval_result', $fn); } diff --git a/ext/standard/base_convert_.php b/ext/standard/base_convert_.php index 1f6a71ab17d..6cfa1866a01 100644 --- a/ext/standard/base_convert_.php +++ b/ext/standard/base_convert_.php @@ -75,7 +75,6 @@ public function call(Context $context, JITVariable ...$args): Value ); } MathBaseConvert::ensureLinked($context); - $ptr = $this->stringDataPtr($context, $num); $fromBase = $context->callerStrictTypes ? JitIntdiv::lowerIntBuiltinArgForCaller($context, $args[1], 'base_convert', 2, 'from_base') : JitIntdiv::lowerIntBuiltinArg($context, $args[1], 'base_convert', 2, 'from_base'); @@ -84,6 +83,6 @@ public function call(Context $context, JITVariable ...$args): Value : JitIntdiv::lowerIntBuiltinArg($context, $args[2], 'base_convert', 3, 'to_base'); $fn = $context->lookupFunction('phpc_base_convert'); - return $context->builder->call($fn, $ptr, $fromBase, $toBase); + return $context->builder->call($fn, $num, $fromBase, $toBase); } } diff --git a/ext/standard/bindec.php b/ext/standard/bindec.php index 58f2e43a807..dea7308f551 100644 --- a/ext/standard/bindec.php +++ b/ext/standard/bindec.php @@ -61,10 +61,7 @@ public function call(Context $context, JITVariable ...$args): Value return MathBaseConvert::baseToZvalCall( $context, - $this->stringDataPtr( - $context, - self::jitStringArg($context, $args[0]) - ), + self::jitStringArg($context, $args[0]), 2 ); } diff --git a/ext/standard/hexdec.php b/ext/standard/hexdec.php index 85a77ccfaab..381bb917725 100644 --- a/ext/standard/hexdec.php +++ b/ext/standard/hexdec.php @@ -61,10 +61,7 @@ public function call(Context $context, JITVariable ...$args): Value return MathBaseConvert::baseToZvalCall( $context, - $this->stringDataPtr( - $context, - self::jitStringArg($context, $args[0]) - ), + self::jitStringArg($context, $args[0]), 16 ); } diff --git a/ext/standard/octdec.php b/ext/standard/octdec.php index 11702ba34c4..6429f24b31f 100644 --- a/ext/standard/octdec.php +++ b/ext/standard/octdec.php @@ -61,10 +61,7 @@ public function call(Context $context, JITVariable ...$args): Value return MathBaseConvert::baseToZvalCall( $context, - $this->stringDataPtr( - $context, - self::jitStringArg($context, $args[0]) - ), + self::jitStringArg($context, $args[0]), 8 ); } diff --git a/lib/JIT/Builtin/MathBaseConvert.php b/lib/JIT/Builtin/MathBaseConvert.php index 5e7256ab6af..7b9f5ca2a30 100644 --- a/lib/JIT/Builtin/MathBaseConvert.php +++ b/lib/JIT/Builtin/MathBaseConvert.php @@ -19,9 +19,9 @@ public static function ensureLinked(Context $context): void MathBaseConvertRuntime::ensureLinked($context); } - public static function baseToZvalCall(Context $context, Value $strDataPtr, int $base): Value + public static function baseToZvalCall(Context $context, Value $strPtr, int $base): Value { - return MathBaseConvertRuntime::baseToZvalCall($context, $strDataPtr, $base); + return MathBaseConvertRuntime::baseToZvalCall($context, $strPtr, $base); } public static function implement(Context $context): void diff --git a/lib/JIT/Builtin/MathBaseConvertRuntime.php b/lib/JIT/Builtin/MathBaseConvertRuntime.php index c8f13161641..63fa6cb820f 100644 --- a/lib/JIT/Builtin/MathBaseConvertRuntime.php +++ b/lib/JIT/Builtin/MathBaseConvertRuntime.php @@ -16,10 +16,11 @@ use PHPLLVM\Value\Function_ as LlvmFunction; /** - * JIT/AOT link for phpc_base_convert / phpc_basetozval_result via MathBaseConvertJitHelper PHP (#9584). + * JIT/AOT link for phpc_base_convert / phpc_basetozval_result via MathBaseConvertJitHelper PHP (#9584, #26884). * * Helper compile: {@see JitVmHelperLink::ensureCompiled} (peer GlobalIntrospectionNameRuntime #22070). * Replaces {@see MathBaseConvertJit} LLVM (~950 LOC). SSOT: {@see \PHPCompiler\ext\standard\VmMath}. + * Call-site {@see ensureLinked} restores the caller insert block after bridge emit (peer #26869). * php-src: ext/standard/math.c */ final class MathBaseConvertRuntime @@ -58,6 +59,11 @@ public static function ensureLinked(Context $context): void public static function implement(Context $context): void { + // NestedJIT of MathBaseConvertJitHelper must not emit outer ABI bridges (#26884). + if (NestedJitCompileScope::isActive()) { + return; + } + $probe = $context->module->getNamedFunction('phpc_base_convert'); if (null !== $probe && $probe->countBasicBlocks() > 0) { self::registerLinkedRuntime($context); @@ -65,11 +71,18 @@ public static function implement(Context $context): void return; } + // Preserve caller insert block — clearInsertionPosition alone orphans mid-emit + // (hexdec/bindec thin AOT: "Current basic block has no parent function", #26884 / peer #26869). + $savedInsert = BasicBlockHelper::tryGetInsertBlock($context); self::ensureJitHelperCompiled($context); self::implementIfMissing($context, 'phpc_base_convert', self::implementBaseConvertBridge(...)); self::implementIfMissing($context, 'phpc_basetozval_result', self::implementBaseToZvalResultBridge(...)); self::registerLinkedRuntime($context); - $context->builder->clearInsertionPosition(); + if (null !== $savedInsert) { + BasicBlockHelper::restoreInsertBlock($context, $savedInsert); + } else { + $context->builder->clearInsertionPosition(); + } } /** @@ -98,7 +111,6 @@ private static function declareFunction(Context $context, string $name): LlvmFun // fall through } - $i8p = $context->getTypeFromString('int8*'); $i64 = $context->getTypeFromString('int64'); $i64Ptr = $context->getTypeFromString('int64*'); $doublePtr = $context->getTypeFromString('double*'); @@ -108,11 +120,11 @@ private static function declareFunction(Context $context, string $name): LlvmFun return match ($name) { 'phpc_base_convert' => $context->module->addFunction( $name, - $context->context->functionType($strPtr, false, $i8p, $i64, $i64) + $context->context->functionType($strPtr, false, $strPtr, $i64, $i64) ), 'phpc_basetozval_result' => $context->module->addFunction( $name, - $context->context->functionType($i32, false, $i8p, $i64, $i64Ptr, $doublePtr) + $context->context->functionType($i32, false, $strPtr, $i64, $i64Ptr, $doublePtr) ), default => throw new \LogicException('Unknown base_convert JIT helper: '.$name), }; @@ -129,11 +141,11 @@ private static function implementBaseConvertBridge(Context $context, LlvmFunctio $fromI64 = $context->builder->sext($from, $i64); $toI64 = $context->builder->sext($to, $i64); - // NestedJIT helpers take `__string__*`; bridge ABI is NUL-terminated i8* (#26511). - $str = self::stringFromCstr($context, $fn->getParam(0)); + // Pass `__string__*` straight through — i8*/__string__init round-trip made NestedJIT + // string offsets ints so ord() TypeError'd under thin AOT (#26884). $result = $context->builder->call( self::helperFunction($context, self::BASE_CONVERT), - $str, + $fn->getParam(0), $fromI64, $toI64 ); @@ -153,10 +165,9 @@ private static function implementBaseToZvalResultBridge(Context $context, LlvmFu $baseI64 = $context->builder->sext($base, $i64); // NestedJIT maps PHP `int` returns to i64; phpc_basetozval_result is i32 (#26511). - $str = self::stringFromCstr($context, $fn->getParam(0)); $tagWide = $context->builder->call( self::helperFunction($context, self::PARSE_BASE_TO_ZVAL), - $str, + $fn->getParam(0), $baseI64 ); $tag = $context->builder->trunc($tagWide, $i32); @@ -243,7 +254,7 @@ private static function emitInvalidRadixCharsDeprecationIfNeeded(Context $contex $context->builder->positionAtEnd($cont); } - public static function baseToZvalCall(Context $context, $strDataPtr, int $base) + public static function baseToZvalCall(Context $context, Value $strPtr, int $base): Value { self::ensureLinked($context); $i32 = $context->getTypeFromString('int32'); @@ -253,7 +264,7 @@ public static function baseToZvalCall(Context $context, $strDataPtr, int $base) $doubleOut = BasicBlockHelper::entryAlloca($context, $double); $isDouble = $context->builder->call( $context->lookupFunction('phpc_basetozval_result'), - $strDataPtr, + $strPtr, $i64->constInt($base, false), $longOut, $doubleOut @@ -304,28 +315,6 @@ private static function helperFunction(Context $context, string $logical): LlvmF return $fn; } - /** Bridge i8* C strings into NestedJIT `__string__*` (peer StringNaturalCompare; no entryAlloca). */ - private static function stringFromCstr(Context $context, Value $cstr): Value - { - $i8p = $context->getTypeFromString('int8*'); - $i64 = $context->getTypeFromString('int64'); - $null = $i8p->constNull(); - $empty = $context->builder->pointerCast($context->constantFromString(''), $i8p); - - $isNull = $context->builder->icmp(Builder::INT_EQ, $cstr, $null); - $ptr = $context->builder->select($isNull, $empty, $cstr); - $len = $context->builder->call($context->lookupFunction('strlen'), $ptr); - $lenI64 = $len->typeOf() === $i64 - ? $len - : $context->builder->zExt($len, $i64); - - return $context->builder->call( - $context->lookupFunction('__string__init'), - $lenI64, - $ptr - ); - } - private static function ensureJitHelperCompiled(Context $context): void { JitVmHelperLink::ensureCompiled( diff --git a/test/unit/HexdecBindecAot26884Test.php b/test/unit/HexdecBindecAot26884Test.php new file mode 100644 index 00000000000..a6f639d9d67 --- /dev/null +++ b/test/unit/HexdecBindecAot26884Test.php @@ -0,0 +1,49 @@ +markTestSkipped('LLVM 9 toolchain not available'); + } + $root = dirname(__DIR__, 2); + $src = sys_get_temp_dir().'/phpc_hexdec_bindec_26884_'.getmypid().'.php'; + file_put_contents($src, <<<'PHP' +&1'; + exec($compile, $compileOut, $compileRc); + $this->assertSame(0, $compileRc, implode("\n", $compileOut)); + $this->assertFileExists($bin); + try { + for ($i = 0; $i < 3; ++$i) { + $runOut = []; + exec(escapeshellarg($bin).' 2>&1', $runOut, $runRc); + $this->assertSame(0, $runRc, 'run '.($i + 1).': '.implode("\n", $runOut)); + $this->assertSame("255\n10\n", implode("\n", $runOut)."\n"); + } + } finally { + @unlink($src); + @unlink($bin); + } + } +} diff --git a/test/unit/MathBaseConvertRuntimeShrinkTest.php b/test/unit/MathBaseConvertRuntimeShrinkTest.php index bf6b9002792..a6ab196617a 100644 --- a/test/unit/MathBaseConvertRuntimeShrinkTest.php +++ b/test/unit/MathBaseConvertRuntimeShrinkTest.php @@ -28,12 +28,31 @@ public function testMathBaseConvertRuntimeUsesJitHelperNotLlvmLoops(): void $source = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/MathBaseConvertRuntime.php'); $this->assertStringContainsString('MathBaseConvertJitHelper', $source); $this->assertStringContainsString('JitVmHelperLink::ensureCompiled', $source); + $this->assertStringContainsString('BasicBlockHelper::tryGetInsertBlock', $source); + $this->assertStringContainsString('BasicBlockHelper::restoreInsertBlock', $source); + $this->assertStringContainsString('NestedJitCompileScope::isActive()', $source); $this->assertStringNotContainsString('NestedJitCompileScope::run', $source); $this->assertStringNotContainsString('parseAndCompile', $source); $this->assertStringNotContainsString('emitBaseToZvalCore', $source); $this->assertStringNotContainsString('emitDigitValue', $source); $this->assertStringNotContainsString('sgen_loop_head', $source); - $this->assertLessThan(360, \substr_count($source, "\n") + 1); + $this->assertLessThan(380, \substr_count($source, "\n") + 1); + } + + /** NestedJIT must not call VmMath — thin AOT stubs that to null (#26884). */ + public function testMathBaseConvertJitHelperIsNestedJitSafeInline(): void + { + $source = (string) file_get_contents(__DIR__.'/../../ext/standard/MathBaseConvertJitHelper.php'); + $this->assertStringContainsString('radixDigitChar', $source); + $this->assertStringContainsString('parseRec', $source); + $this->assertStringContainsString('substr(', $source); + $this->assertDoesNotMatchRegularExpression('/VmMath::\w+\s*\(/', $source); + $this->assertStringNotContainsString('ctype_space', $source); + $this->assertStringNotContainsString('ctype_digit', $source); + $this->assertStringNotContainsString('sprintf(', $source); + $this->assertStringNotContainsString('\\ord(', $source); + $runtime = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/MathBaseConvertRuntime.php'); + $this->assertStringNotContainsString('stringFromCstr', $runtime); } public function testMathBaseConvertJitHelperMatchesVmMath(): void From 0cf5bf7a7293bbe1ed30dcb8338765e709b73fef Mon Sep 17 00:00:00 2001 From: PurHur Date: Sun, 2 Aug 2026 09:38:20 +0000 Subject: [PATCH 2/2] Docs: refresh bootstrap inventory for MathBaseConvert NestedJIT helpers Co-authored-by: Cursor --- docs/bootstrap-inventory.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index ea26f4fad67..e50335475ff 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -29406,7 +29406,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/standard/MathBaseConvertJitHelper.php` **Warnings** (review for bootstrap subset): -- 6 class method(s) +- 15 class method(s) ### `ext/standard/MemoryJitHelper.php` @@ -44998,7 +44998,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/JIT/Builtin/MathBaseConvertRuntime.php` **Warnings** (review for bootstrap subset): -- 12 class method(s) +- 11 class method(s) ### `lib/JIT/Builtin/MathCeil.php`