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
123 changes: 76 additions & 47 deletions ext/mbstring/JitMbChrOrd.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@
use PHPCompiler\JIT\Context;
use PHPCompiler\JIT\JitNestedHelperCoerce;
use PHPCompiler\JIT\JitStrictIntArg;
use PHPCompiler\JIT\JitStringArg;
use PHPCompiler\JIT\JitStringBuiltinArg;
use PHPCompiler\JIT\JitValueBox;
use PHPCompiler\JIT\Variable as JITVariable;
use PHPLLVM\Value;

/**
* LLVM JIT/AOT for mb_chr() / mb_ord() (php-src ext/mbstring/mbstring.c; #30759 / #34243 / #34250).
* LLVM JIT/AOT for mb_chr() / mb_ord() (php-src ext/mbstring/mbstring.c; #30759 / #34243 / #34250 / #34870).
*
* Compile-time fold via {@see VmMbstring}; runtime via NestedJIT {@see MbChrOrdJitHelper}.
* Peer {@see JitMbSearch}.
* Runtime encoding via NestedJIT (#34870 leftover of #34250; peer {@see JitMbCase} / {@see JitMbSearch}).
*/
final class JitMbChrOrd
{
Expand All @@ -46,29 +47,8 @@ public static function invokeChr(Context $context, array $args): Value
1,
'codepoint'
);
if ($argc >= 2) {
if (JITVariable::TYPE_NULL === $args[1]->type || ($args[1]->isNullConstant ?? false)) {
$encoding = 'UTF-8';
} elseif (JITVariable::TYPE_STRING !== $args[1]->type) {
throw new \LogicException('mb_chr() encoding must be a string literal in this compiler build');
} else {
$encoding = $args[1]->compileTimeString ?? null;
if (null === $encoding) {
throw new \LogicException('mb_chr() encoding must be a string literal in this compiler build');
}
}
} else {
$encoding = 'UTF-8';
}
self::assertSupportedEncoding($encoding);
$encPtr = self::linkAndEncodingPtr($context, $args, $argc, 'mb_chr');

$savedInsert = BasicBlockHelper::tryGetInsertBlock($context);
MbChrOrdRuntime::ensureLinked($context);
if (null !== $savedInsert) {
BasicBlockHelper::restoreInsertBlock($context, $savedInsert);
}

$encPtr = $context->builder->load($context->constantStringFromString($encoding));
$raw = JitNestedHelperCoerce::callHelper(
$context,
MbChrOrdRuntime::chrHelper($context),
Expand Down Expand Up @@ -133,29 +113,8 @@ public static function invokeOrd(Context $context, array $args): Value
}

$string = JitStringBuiltinArg::lowerTrimFamilyString($context, $args[0], 'mb_ord', 0, 'string');
if ($argc >= 2) {
if (JITVariable::TYPE_NULL === $args[1]->type || ($args[1]->isNullConstant ?? false)) {
$encoding = 'UTF-8';
} elseif (JITVariable::TYPE_STRING !== $args[1]->type) {
throw new \LogicException('mb_ord() encoding must be a string literal in this compiler build');
} else {
$encoding = $args[1]->compileTimeString ?? null;
if (null === $encoding) {
throw new \LogicException('mb_ord() encoding must be a string literal in this compiler build');
}
}
} else {
$encoding = 'UTF-8';
}
self::assertSupportedEncoding($encoding);

$savedInsert = BasicBlockHelper::tryGetInsertBlock($context);
MbChrOrdRuntime::ensureLinked($context);
if (null !== $savedInsert) {
BasicBlockHelper::restoreInsertBlock($context, $savedInsert);
}
$encPtr = self::linkAndEncodingPtr($context, $args, $argc, 'mb_ord');

$encPtr = $context->builder->load($context->constantStringFromString($encoding));
$found = JitNestedHelperCoerce::callHelper(
$context,
MbChrOrdRuntime::ordHelper($context),
Expand All @@ -165,6 +124,76 @@ public static function invokeOrd(Context $context, array $args): Value
return StringStrpos::boxFoundOffset($context, $found);
}

/**
* Link MbChrOrdRuntime + resolve encoding ptr; NestedJIT assert when encoding is non-literal
* or an unsupported/invalid name (#34870).
*
* @param list<JITVariable> $args
*/
private static function linkAndEncodingPtr(Context $context, array $args, int $argc, string $function): Value
{
$savedInsert = BasicBlockHelper::tryGetInsertBlock($context);
MbChrOrdRuntime::ensureLinked($context);
if (null !== $savedInsert) {
BasicBlockHelper::restoreInsertBlock($context, $savedInsert);
}
BasicBlockHelper::ensureOpenInsertBlock($context, $function.'_runtime');

[$encPtr, $needsAssert] = self::encodingPtr($context, $args, $argc, $function);
if ($needsAssert) {
$fnName = $context->builder->load($context->constantStringFromString($function));
$context->builder->call(
MbChrOrdRuntime::assertEncodingHelper($context),
$encPtr,
$fnName
);
}

return $encPtr;
}

/**
* Literal UTF-8/ASCII/8BIT → constant string (no assert); otherwise NestedJIT encoding + assert (#34870).
*
* @param list<JITVariable> $args
* @return array{0: Value, 1: bool} encoding ptr, needsAssert
*/
private static function encodingPtr(Context $context, array $args, int $argc, string $function): array
{
if ($argc < 2 || JITVariable::TYPE_NULL === $args[1]->type || ($args[1]->isNullConstant ?? false)) {
$encoding = 'UTF-8';
self::assertSupportedEncoding($encoding);

return [$context->builder->load($context->constantStringFromString($encoding)), false];
}

$encodingLit = JitStringArg::compileTimeLiteral($args[1]);
if (null !== $encodingLit) {
$canonical = MbstringEncodingRegistry::resolve($encodingLit);
if (null !== $canonical && self::isSupportedEncoding($canonical)) {
return [$context->builder->load($context->constantStringFromString($canonical)), false];
}
// Invalid / unsupported literal — NestedJIT assert throws catchable ValueError (#34870).
return [$context->builder->load($context->constantStringFromString($encodingLit)), true];
}

return [
JitStringBuiltinArg::lower(
$context,
$args[1],
$function,
1,
'encoding'
),
true,
];
}

private static function isSupportedEncoding(string $encoding): bool
{
return 'UTF-8' === $encoding || 'ASCII' === $encoding || '8BIT' === $encoding;
}

/**
* @param JITVariable[] $args
*/
Expand Down Expand Up @@ -277,7 +306,7 @@ private static function intOrFalse(Context $context, int|false $result): Value

private static function assertSupportedEncoding(string $encoding): void
{
if ('UTF-8' !== $encoding && 'ASCII' !== $encoding && '8BIT' !== $encoding) {
if (!self::isSupportedEncoding($encoding)) {
throw new \LogicException(
'mb_chr()/mb_ord() JIT only supports UTF-8, ASCII, or 8BIT encoding literals in this compiler build'
);
Expand Down
46 changes: 45 additions & 1 deletion ext/mbstring/MbChrOrdJitHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use PHPCompiler\JIT\Builtin\StringStrpos;

/**
* mb_chr() / mb_ord() for compiled JIT/AOT modules (#34243 / #34250 leftovers of #30759).
* mb_chr() / mb_ord() for compiled JIT/AOT modules (#34243 / #34250 leftovers of #30759; #34870).
*
* mb_ord: Returns {@see StringStrpos::NOT_FOUND} (-1) on invalid first character so callers can box int|false.
* mb_chr: Returns string|false (false → NestedJIT nullish for {@see JitMbChrOrd} boxing).
Expand All @@ -16,6 +16,8 @@
* — those silent-return / misbehave under thin AOT NestedJIT. Encode/decode is inlined with strlen/ord/substr
* and range compares (peer {@see MbSearchJitHelper}). Avoid PHP {@see chr()} (typed mixed → TypeError under NestedJIT).
*
* Runtime encoding validation (#34870) — int-returning assert (string-returning NestedJIT throws SIGSEGV).
*
* SSOT (VM / compile-time fold): {@see VmMbstring::chr()} / {@see VmMbstring::ord()}
* php-src: ext/mbstring/mbstring.c — PHP_FUNCTION(mb_chr) / PHP_FUNCTION(mb_ord)
*
Expand All @@ -24,6 +26,24 @@
*/
final class MbChrOrdJitHelper
{
/**
* Int-returning encoding check — NestedJIT ValueError from string-returning helpers
* SIGSEGVs under thin AOT; int helpers match {@see MbCaseJitHelper::assertEncodingArgv} (#34870 / #34858).
*
* Encoding is Argument #2 for mb_chr / mb_ord.
*/
public static function assertEncodingArgv(string $encoding, string $function): int
{
if ('' === self::canon($encoding)) {
// Concat (not sprintf) — NestedJIT sprintf+throw breaks module verify (#34625).
throw new \ValueError(
$function.'(): Argument #2 ($encoding) must be a valid encoding, "'.$encoding.'" given'
);
}

return 1;
}

/**
* All 256 bytes as a literal — NestedJIT-safe substitute for chr($b).
*/
Expand Down Expand Up @@ -53,13 +73,34 @@ private static function byte(int $b): string
return \substr(self::allBytes(), $b, 1);
}

private static function canon(string $encoding): string
{
if ('UTF-8' === $encoding || 'utf-8' === $encoding || 'UTF8' === $encoding || 'utf8' === $encoding) {
return 'UTF-8';
}
if (
'ASCII' === $encoding || 'ascii' === $encoding
|| 'US-ASCII' === $encoding || 'us-ascii' === $encoding
) {
return 'ASCII';
}
if ('8BIT' === $encoding || '8bit' === $encoding || 'BINARY' === $encoding || 'binary' === $encoding) {
return '8BIT';
}

return '';
}

/**
* mb_chr() — encode codepoint, or false when out of range / surrogate.
*
* Encoding must already be validated via {@see assertEncodingArgv} (#34870).
*
* @return string|false
*/
public static function chrArgv(int $codepoint, string $encoding)
{
$encoding = self::canon($encoding);
if ('ASCII' === $encoding || '8BIT' === $encoding) {
if ($codepoint < 0 || $codepoint > 255) {
return false;
Expand Down Expand Up @@ -119,12 +160,15 @@ private static function divFloor(int $n, int $d): int

/**
* mb_ord() — first character codepoint, or NOT_FOUND when the lead sequence is invalid.
*
* Encoding must already be validated via {@see assertEncodingArgv} (#34870).
*/
public static function ordArgv(string $string, string $encoding): int
{
if ('' === $string) {
throw new \ValueError('mb_ord(): Argument #1 ($string) must not be empty');
}
$encoding = self::canon($encoding);
if ('ASCII' === $encoding || '8BIT' === $encoding) {
return \ord(\substr($string, 0, 1));
}
Expand Down
12 changes: 11 additions & 1 deletion lib/JIT/Builtin/MbChrOrdRuntime.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use PHPLLVM\Value\Function_ as LlvmFunction;

/**
* JIT/AOT link hook for mb_chr() / mb_ord() — compiles MbChrOrdJitHelper (#34243 / #34250).
* JIT/AOT link hook for mb_chr() / mb_ord() — compiles MbChrOrdJitHelper (#34243 / #34250 / #34870).
*
* php-src: ext/mbstring/mbstring.c — PHP_FUNCTION(mb_chr) / PHP_FUNCTION(mb_ord)
*/
Expand All @@ -21,10 +21,13 @@ final class MbChrOrdRuntime

private const CHR_LOGICAL = 'PHPCompiler\\ext\\mbstring\\MbChrOrdJitHelper::chrArgv';

private const ASSERT_ENCODING_LOGICAL = 'PHPCompiler\\ext\\mbstring\\MbChrOrdJitHelper::assertEncodingArgv';

/** @var list<string> */
private const COMPILED_HELPERS = [
self::ORD_LOGICAL,
self::CHR_LOGICAL,
self::ASSERT_ENCODING_LOGICAL,
];

public static function ensureLinked(Context $context): void
Expand All @@ -46,6 +49,13 @@ public static function chrHelper(Context $context): LlvmFunction
return JitVmHelperLink::lookupCompiled($context, self::CHR_LOGICAL, '#34250');
}

public static function assertEncodingHelper(Context $context): LlvmFunction
{
self::ensureJitHelperCompiled($context);

return JitVmHelperLink::lookupCompiled($context, self::ASSERT_ENCODING_LOGICAL, 'mb_chr_ord_encoding');
}

private static function ensureJitHelperCompiled(Context $context): void
{
JitVmHelperLink::ensureCompiled(
Expand Down
21 changes: 21 additions & 0 deletions test/repro/mb_chr_ord_runtime_encoding_aot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

/**
* #34870 — mb_chr()/mb_ord() with runtime encoding under thin AOT.
* php-src: ext/mbstring/mbstring.c PHP_FUNCTION(mb_chr|mb_ord)
*/
$e = 'UTF-'.'8';
echo 'chr=', mb_chr(0x3042, $e), "\n";
echo 'ord=', mb_ord('あ', $e), "\n";
$ascii = 'ASC'.'II';
echo 'chr_ascii=', var_export(mb_chr(0x41, $ascii), true), "\n";
echo 'ord_ascii=', mb_ord('A', $ascii), "\n";
try {
$bad = 'NOPE';
echo mb_chr(65, $bad);
echo "no error\n";
} catch (ValueError $err) {
echo 'err=', $err->getMessage(), "\n";
}
Loading
Loading