From 8c79d13571424288e584a8aec3879be99b5f4bb5 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sat, 4 Jul 2026 03:12:40 +0000 Subject: [PATCH] Stdlib: number_format() optional RoundingMode fifth parameter (#9438) Wire PHP 8.4 rounding_mode through VM, JIT, and SprintfJitHelper using VmRoundMode/VmRound SSOT; extend __compiler_number_format ABI for mode int. Co-authored-by: Cursor --- ext/standard/JitNumberFormat.php | 22 +++++++++++---- ext/standard/SprintfJitHelper.php | 11 ++++++-- ext/standard/VmNumberFormat.php | 9 ++---- ext/standard/number_format.php | 27 ++++++++++++++---- lib/JIT/Builtin/StringFormat.php | 6 ++-- .../Builtin/StringFormatInventoryStubs.php | 3 +- lib/JIT/Builtin/Type.php | 3 +- .../stdlib/number_format_rounding_mode.phpt | 16 +++++++++++ .../number_format_rounding_mode_jit.phpt | 11 ++++++++ .../cases/number_format_rounding_mode.phpt | 11 ++++++++ ...tainer_gap_number_format_rounding_mode.php | 28 +++++++++++++++++++ test/unit/StringFormatRuntimeShrinkTest.php | 15 ++++++++++ 12 files changed, 138 insertions(+), 24 deletions(-) create mode 100644 test/compliance/cases/stdlib/number_format_rounding_mode.phpt create mode 100644 test/compliance/cases/stdlib/number_format_rounding_mode_jit.phpt create mode 100644 test/fixtures/aot/cases/number_format_rounding_mode.phpt create mode 100644 test/repro/maintainer_gap_number_format_rounding_mode.php diff --git a/ext/standard/JitNumberFormat.php b/ext/standard/JitNumberFormat.php index 24058ded36f..9cdcaa839e7 100644 --- a/ext/standard/JitNumberFormat.php +++ b/ext/standard/JitNumberFormat.php @@ -4,9 +4,11 @@ namespace PHPCompiler\ext\standard; +use PHPCompiler\CompilerVersion; use PHPCompiler\JIT\BasicBlockHelper; use PHPCompiler\JIT\Builtin\TypeErrorRaise; use PHPCompiler\JIT\Context; +use PHPCompiler\JIT\JitRoundModeArg; use PHPCompiler\JIT\JitStringBuiltinArg; use PHPCompiler\JIT\JitValueBox; use PHPCompiler\JIT\NamedOptionalCallArgs; @@ -16,17 +18,21 @@ use PHPLLVM\Value; /** - * LLVM JIT/AOT helper for number_format() (int/float/numeric string, 0–4 args; subset of PHP). + * LLVM JIT/AOT helper for number_format() (int/float/numeric string, 0–5 args; subset of PHP). * - * php-src: ext/standard/number_format.c — Z_PARAM_LONG / Z_PARAM_STR + * php-src: ext/standard/number_format.c — Z_PARAM_LONG / Z_PARAM_STR / RoundingMode */ final class JitNumberFormat { public static function format(Context $context, JITVariable ...$args): Value { $argc = count($args); - if ($argc < 1 || $argc > 4) { - throw new \LogicException('number_format() requires one to four arguments'); + $maxArgs = CompilerVersion::supportsRoundingModeEnum() ? 5 : 4; + if ($argc < 1 || $argc > $maxArgs) { + throw new \LogicException(\sprintf( + 'number_format() requires one to %d arguments', + $maxArgs + )); } if ($context->callerStrictTypes) { @@ -41,16 +47,20 @@ public static function format(Context $context, JITVariable ...$args): Value $decSep = ($argc >= 3 && !NamedOptionalCallArgs::isOmittedOptional($args[2])) ? JitStringBuiltinArg::lower($context, $args[2], 'number_format', 2, 'decimal_separator', '?string') : $context->builder->load($context->constantStringFromString('.')); - $thouSep = (4 === $argc && !NamedOptionalCallArgs::isOmittedOptional($args[3])) + $thouSep = ($argc >= 4 && !NamedOptionalCallArgs::isOmittedOptional($args[3])) ? JitStringBuiltinArg::lower($context, $args[3], 'number_format', 3, 'thousands_separator', '?string') : $context->builder->load($context->constantStringFromString(',')); + $mode = ($argc >= 5 && !NamedOptionalCallArgs::isOmittedOptional($args[4])) + ? JitRoundModeArg::lower($context, $args[4], 'number_format', 'rounding_mode', 5) + : $i64->constInt(StdlibConstants::PHP_ROUND_HALF_UP, false); return $context->builder->call( $context->lookupFunction('__compiler_number_format'), $number, $decimals, $decSep, - $thouSep + $thouSep, + $mode ); } diff --git a/ext/standard/SprintfJitHelper.php b/ext/standard/SprintfJitHelper.php index b7b12e40c16..8d6fdfb510c 100644 --- a/ext/standard/SprintfJitHelper.php +++ b/ext/standard/SprintfJitHelper.php @@ -27,9 +27,16 @@ public static function numberFormat( float $number, int $decimals, string $decimalSeparator, - string $thousandsSeparator + string $thousandsSeparator, + int $roundingMode = StdlibConstants::PHP_ROUND_HALF_UP ): string { - return VmNumberFormat::format($number, $decimals, $decimalSeparator, $thousandsSeparator); + return VmNumberFormat::format( + $number, + $decimals, + $decimalSeparator, + $thousandsSeparator, + $roundingMode + ); } /** diff --git a/ext/standard/VmNumberFormat.php b/ext/standard/VmNumberFormat.php index 7a600525be7..ee828f8ec40 100644 --- a/ext/standard/VmNumberFormat.php +++ b/ext/standard/VmNumberFormat.php @@ -60,7 +60,8 @@ public static function format( float $number, int $decimals = 0, string $decimalSeparator = '.', - string $thousandsSeparator = ',' + string $thousandsSeparator = ',', + int $roundingMode = StdlibConstants::PHP_ROUND_HALF_UP ): string { // php-src ext/standard/math.c _php_math_number_format_ex: non-finite via %F, lowercased if (\is_nan($number)) { @@ -80,11 +81,7 @@ public static function format( $pow *= 10; } - if ($decimals > 0) { - $rounded = round($number, $decimals); - } else { - $rounded = round($number, 0); - } + $rounded = VmRound::mathRound($number, $decimals, $roundingMode); $intPart = (int) floor($rounded); $fracPart = 0; diff --git a/ext/standard/number_format.php b/ext/standard/number_format.php index 820ec3090c1..50eab64cbfa 100644 --- a/ext/standard/number_format.php +++ b/ext/standard/number_format.php @@ -11,6 +11,7 @@ namespace PHPCompiler\ext\standard; +use PHPCompiler\CompilerVersion; use PHPCompiler\Frame; use PHPCompiler\Func\Internal; use PHPCompiler\JIT\Context; @@ -22,22 +23,24 @@ /** * number_format() for integers and floats (C-style locale subset; LLVM JIT/AOT). * - * php-src: ext/standard/number_format.c — Z_PARAM_LONG / Z_PARAM_STR + * php-src: ext/standard/number_format.c — Z_PARAM_LONG / Z_PARAM_STR / RoundingMode */ final class number_format extends Internal { public function execute(Frame $frame): void { $argc = \count($frame->calledArgs); + $maxArgs = CompilerVersion::supportsRoundingModeEnum() ? 5 : 4; if ($argc < 1) { throw new \ArgumentCountError(\sprintf( 'number_format() expects at least 1 argument, %d given', $argc )); } - if ($argc > 4) { + if ($argc > $maxArgs) { throw new \ArgumentCountError(\sprintf( - 'number_format() expects at most 4 arguments, %d given', + 'number_format() expects at most %d arguments, %d given', + $maxArgs, $argc )); } @@ -73,11 +76,21 @@ public function execute(Frame $frame): void 'thousands_separator' ) ?? ',' : ','; + $roundingMode = StdlibConstants::PHP_ROUND_HALF_UP; + if ($argc >= 5) { + $roundingMode = VmRoundMode::resolveRoundModeArg( + $frame->calledArgs[4]->resolveIndirect(), + 'number_format', + 'rounding_mode', + 5 + ); + } $frame->returnVar->string(VmNumberFormat::format( $num, $decimals, $decimalSeparator, - $thousandsSeparator + $thousandsSeparator, + $roundingMode )); } @@ -86,15 +99,17 @@ public function execute(Frame $frame): void public function call(Context $context, JITVariable ...$args): Value { $argc = \count($args); + $maxArgs = CompilerVersion::supportsRoundingModeEnum() ? 5 : 4; if ($argc < 1) { throw new \ArgumentCountError(\sprintf( 'number_format() expects at least 1 argument, %d given', $argc )); } - if ($argc > 4) { + if ($argc > $maxArgs) { throw new \ArgumentCountError(\sprintf( - 'number_format() expects at most 4 arguments, %d given', + 'number_format() expects at most %d arguments, %d given', + $maxArgs, $argc )); } diff --git a/lib/JIT/Builtin/StringFormat.php b/lib/JIT/Builtin/StringFormat.php index 9a111271b1a..36d7305ed11 100644 --- a/lib/JIT/Builtin/StringFormat.php +++ b/lib/JIT/Builtin/StringFormat.php @@ -199,7 +199,7 @@ private static function implementNumberFormatBridge(Context $context): void $strPtr = $context->getTypeFromString('__string__*'); $double = $context->getTypeFromString('double'); $i64 = $context->getTypeFromString('int64'); - $ft = $context->context->functionType($strPtr, false, $double, $i64, $strPtr, $strPtr); + $ft = $context->context->functionType($strPtr, false, $double, $i64, $strPtr, $strPtr, $i64); $fn = null !== $probe ? $probe : $context->module->addFunction($abiName, $ft); @@ -210,12 +210,14 @@ private static function implementNumberFormatBridge(Context $context): void $decimals = $fn->getParam(1); $decSep = $context->builder->call($context->lookupFunction('__string__separate'), $fn->getParam(2)); $thouSep = $context->builder->call($context->lookupFunction('__string__separate'), $fn->getParam(3)); + $mode = $fn->getParam(4); $out = $context->builder->call( self::helperFunction($context, self::NUMBER_FORMAT_HELPER), $num, $decimals, $decSep, - $thouSep + $thouSep, + $mode ); $context->builder->returnValue($out); $context->registerFunction($abiName, $fn); diff --git a/lib/JIT/Builtin/StringFormatInventoryStubs.php b/lib/JIT/Builtin/StringFormatInventoryStubs.php index 26a7a26f815..dc5754b6fe3 100644 --- a/lib/JIT/Builtin/StringFormatInventoryStubs.php +++ b/lib/JIT/Builtin/StringFormatInventoryStubs.php @@ -84,8 +84,9 @@ private static function implementNumberFormatStub(Context $context): void } $strPtr = $context->getTypeFromString('__string__*'); + $double = $context->getTypeFromString('double'); $i64 = $context->getTypeFromString('int64'); - $ft = $context->context->functionType($strPtr, false, $strPtr, $i64, $i64, $i64, $strPtr); + $ft = $context->context->functionType($strPtr, false, $double, $i64, $strPtr, $strPtr, $i64); $fn = null !== $probe ? $probe : $context->module->addFunction($abiName, $ft); diff --git a/lib/JIT/Builtin/Type.php b/lib/JIT/Builtin/Type.php index 7b42ed654ca..aa12b1e2a7d 100755 --- a/lib/JIT/Builtin/Type.php +++ b/lib/JIT/Builtin/Type.php @@ -66,7 +66,8 @@ public function register(): void { $this->context->getTypeFromString('double'), $this->context->getTypeFromString('int64'), $this->context->getTypeFromString('__string__*'), - $this->context->getTypeFromString('__string__*') + $this->context->getTypeFromString('__string__*'), + $this->context->getTypeFromString('int64') ); $fnNumberFormat = $this->context->module->addFunction('__compiler_number_format', $fntypeNumberFormat); $this->context->registerFunction('__compiler_number_format', $fnNumberFormat); diff --git a/test/compliance/cases/stdlib/number_format_rounding_mode.phpt b/test/compliance/cases/stdlib/number_format_rounding_mode.phpt new file mode 100644 index 00000000000..cc6d0c9d7e5 --- /dev/null +++ b/test/compliance/cases/stdlib/number_format_rounding_mode.phpt @@ -0,0 +1,16 @@ +--TEST-- +stdlib number_format() optional rounding_mode (PHP 8.4, ext/standard/number_format.c, #9438) +--FILE-- +assertSame('nan', SprintfJitHelper::numberFormat(NAN, 0, '.', ',')); + $this->assertSame( + '3', + SprintfJitHelper::numberFormat(2.5, 0, '.', '', StdlibConstants::PHP_ROUND_HALF_UP) + ); + $this->assertSame( + '2', + SprintfJitHelper::numberFormat( + 2.5, + 0, + '.', + '', + StdlibConstants::PHP_ROUND_TOWARD_ZERO + ) + ); } }