diff --git a/ext/random/random.c b/ext/random/random.c index cc1dec4ac8c7c..81aabe866841f 100644 --- a/ext/random/random.c +++ b/ext/random/random.c @@ -62,6 +62,17 @@ # include #endif +// The nextFloat() method requires the underlying 'double' representation to be IEEE-754. +#ifdef __STDC_IEC_559__ +/* A double has 53 bits of precision, thus we must not + * use the full 64 bits of the uint64_t, because we would + * introduce a bias / rounding error. + */ +#if DBL_MANT_DIG == 53 +#define HAVE_RANDOMIZER_FLOAT +#endif +#endif + #include "random_arginfo.h" PHPAPI ZEND_DECLARE_MODULE_GLOBALS(random) diff --git a/ext/random/random.stub.php b/ext/random/random.stub.php index 0a178f2657dc2..d02b4cd495b15 100644 --- a/ext/random/random.stub.php +++ b/ext/random/random.stub.php @@ -133,6 +133,12 @@ public function __construct(?Engine $engine = null) {} public function nextInt(): int {} +#if HAVE_RANDOMIZER_FLOAT + public function nextFloat(): float {} + + public function getFloat(float $min, float $max): float {} +#endif + public function getInt(int $min, int $max): int {} public function getBytes(int $length): string {} diff --git a/ext/random/random_arginfo.h b/ext/random/random_arginfo.h index fc272607360d0..2a341f68df34f 100644 --- a/ext/random/random_arginfo.h +++ b/ext/random/random_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 6cc9022516ce23c2e95af30606db43e9fc28e38a */ + * Stub hash: 85e67324eb9e1865048f07e2389fb5c3fc7f13e4 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_lcg_value, 0, 0, IS_DOUBLE, 0) ZEND_END_ARG_INFO() @@ -90,6 +90,18 @@ ZEND_END_ARG_INFO() #define arginfo_class_Random_Randomizer_nextInt arginfo_mt_getrandmax +#if HAVE_RANDOMIZER_FLOAT +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Random_Randomizer_nextFloat, 0, 0, IS_DOUBLE, 0) +ZEND_END_ARG_INFO() +#endif + +#if HAVE_RANDOMIZER_FLOAT +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Random_Randomizer_getFloat, 0, 2, IS_DOUBLE, 0) + ZEND_ARG_TYPE_INFO(0, min, IS_DOUBLE, 0) + ZEND_ARG_TYPE_INFO(0, max, IS_DOUBLE, 0) +ZEND_END_ARG_INFO() +#endif + #define arginfo_class_Random_Randomizer_getInt arginfo_random_int #define arginfo_class_Random_Randomizer_getBytes arginfo_random_bytes @@ -131,6 +143,12 @@ ZEND_METHOD(Random_Engine_Xoshiro256StarStar, jump); ZEND_METHOD(Random_Engine_Xoshiro256StarStar, jumpLong); ZEND_METHOD(Random_Randomizer, __construct); ZEND_METHOD(Random_Randomizer, nextInt); +#if HAVE_RANDOMIZER_FLOAT +ZEND_METHOD(Random_Randomizer, nextFloat); +#endif +#if HAVE_RANDOMIZER_FLOAT +ZEND_METHOD(Random_Randomizer, getFloat); +#endif ZEND_METHOD(Random_Randomizer, getInt); ZEND_METHOD(Random_Randomizer, getBytes); ZEND_METHOD(Random_Randomizer, shuffleArray); @@ -207,6 +225,12 @@ static const zend_function_entry class_Random_CryptoSafeEngine_methods[] = { static const zend_function_entry class_Random_Randomizer_methods[] = { ZEND_ME(Random_Randomizer, __construct, arginfo_class_Random_Randomizer___construct, ZEND_ACC_PUBLIC) ZEND_ME(Random_Randomizer, nextInt, arginfo_class_Random_Randomizer_nextInt, ZEND_ACC_PUBLIC) +#if HAVE_RANDOMIZER_FLOAT + ZEND_ME(Random_Randomizer, nextFloat, arginfo_class_Random_Randomizer_nextFloat, ZEND_ACC_PUBLIC) +#endif +#if HAVE_RANDOMIZER_FLOAT + ZEND_ME(Random_Randomizer, getFloat, arginfo_class_Random_Randomizer_getFloat, ZEND_ACC_PUBLIC) +#endif ZEND_ME(Random_Randomizer, getInt, arginfo_class_Random_Randomizer_getInt, ZEND_ACC_PUBLIC) ZEND_ME(Random_Randomizer, getBytes, arginfo_class_Random_Randomizer_getBytes, ZEND_ACC_PUBLIC) ZEND_ME(Random_Randomizer, shuffleArray, arginfo_class_Random_Randomizer_shuffleArray, ZEND_ACC_PUBLIC) diff --git a/ext/random/randomizer.c b/ext/random/randomizer.c index 0e3d90120b6fc..27d28654d0f35 100644 --- a/ext/random/randomizer.c +++ b/ext/random/randomizer.c @@ -88,6 +88,105 @@ PHP_METHOD(Random_Randomizer, __construct) } /* }}} */ +#if HAVE_RANDOMIZER_FLOAT +/* {{{ Generate a float in [0, 1) */ +PHP_METHOD(Random_Randomizer, nextFloat) +{ + php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS); + uint64_t result; + size_t total_size; + + ZEND_PARSE_PARAMETERS_NONE(); + + result = 0; + total_size = 0; + do { + uint64_t r = randomizer->algo->generate(randomizer->status); + result = result | (r << (total_size * 8)); + total_size += randomizer->status->last_generated_size; + if (EG(exception)) { + RETURN_THROWS(); + } + } while (total_size < sizeof(uint64_t)); + + const double step_size = 1.0 / (1ULL << 53); + + /* Use the upper 53 bits, because some engine's lower bits + * are of lower quality. + */ + result = (result >> 11); + + RETURN_DOUBLE(step_size * result); +} +/* }}} */ + +static double getFloat_gamma_low(double x) +{ + return x - nextdown(x); +} + +static double getFloat_gamma_high(double x) +{ + return nextup(x) - x; +} + +static double getFloat_gamma(double x, double y) +{ + return (fabs(x) > fabs(y)) ? getFloat_gamma_high(x) : getFloat_gamma_low(y); +} + +static uint64_t getFloat_ceilint(double a, double b, double g) +{ + double s = b / g - a / g; + double e; + + if (fabs(a) <= fabs(b)) { + e = -a / g - (s - b / g); + } else { + e = b / g - (s + a / g); + } + + double si = ceil(s); + + return (s != si) ? (uint64_t)si : (uint64_t)si + (e > 0); +} + +/* {{{ Generates a random float within [min, max). + * + * The algorithm used is the γ-section algorithm as published in: + * + * Drawing Random Floating-Point Numbers from an Interval. Frédéric + * Goualard, ACM Trans. Model. Comput. Simul., 32:3, 2022. + * https://doi.org/10.1145/3503512 + */ +PHP_METHOD(Random_Randomizer, getFloat) +{ + php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS); + double min, max; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_DOUBLE(min) + Z_PARAM_DOUBLE(max) + ZEND_PARSE_PARAMETERS_END(); + + if (UNEXPECTED(max < min)) { + zend_argument_value_error(2, "must be greater than or equal to argument #1 ($min)"); + RETURN_THROWS(); + } + + double g = getFloat_gamma(min, max); + uint64_t hi = getFloat_ceilint(min, max, g); + uint64_t k = randomizer->algo->range(randomizer->status, 1, hi); + + if (fabs(min) <= fabs(max)) { + RETURN_DOUBLE(k == hi ? min : max - k * g); + } else { + RETURN_DOUBLE(min + (k - 1) * g); + } +} +/* }}} */ +#endif // HAVE_RANDOMIZER_FLOAT + /* {{{ Generate positive random number */ PHP_METHOD(Random_Randomizer, nextInt) { diff --git a/ext/random/tests/03_randomizer/methods/nextFloat.phpt b/ext/random/tests/03_randomizer/methods/nextFloat.phpt new file mode 100644 index 0000000000000..81d27bd005f32 --- /dev/null +++ b/ext/random/tests/03_randomizer/methods/nextFloat.phpt @@ -0,0 +1,55 @@ +--TEST-- +Random: Randomizer: nextFloat(): Basic functionality +--SKIPIF-- + +--FILE-- +nextFloat(); + + if ($result < 0 || $result >= 1) { + die("failure: out of range at {$i}"); + } + } +} + +die('success'); + +?> +--EXPECT-- +Random\Engine\Mt19937 +Random\Engine\Mt19937 +Random\Engine\PcgOneseq128XslRr64 +Random\Engine\Xoshiro256StarStar +Random\Engine\Secure +Random\Engine\Test\TestShaEngine +success diff --git a/ext/random/tests/03_randomizer/methods/nextFloat_spacing.phpt b/ext/random/tests/03_randomizer/methods/nextFloat_spacing.phpt new file mode 100644 index 0000000000000..ece8b215c8ca4 --- /dev/null +++ b/ext/random/tests/03_randomizer/methods/nextFloat_spacing.phpt @@ -0,0 +1,47 @@ +--TEST-- +Random: Randomizer: nextFloat(): Return values are evenly spaced. +--SKIPIF-- + +--FILE-- +value; + } +} + +$zero = new Randomizer(new StaticEngine("\x00\x00\x00\x00\x00\x00\x00\x00")); +$one = new Randomizer(new StaticEngine("\x00\x08\x00\x00\x00\x00\x00\x00")); +$two = new Randomizer(new StaticEngine("\x00\x10\x00\x00\x00\x00\x00\x00")); + +$max_minus_two = new Randomizer(new StaticEngine("\x00\xe8\xff\xff\xff\xff\xff\xff")); +$max_minus_one = new Randomizer(new StaticEngine("\x00\xf0\xff\xff\xff\xff\xff\xff")); +$max = new Randomizer(new StaticEngine("\x00\xf8\xff\xff\xff\xff\xff\xff")); + +var_dump($one->nextFloat() - $one->nextFloat() === $zero->nextFloat()); +var_dump($two->nextFloat() - $one->nextFloat() === $one->nextFloat()); +var_dump($max->nextFloat() - $max_minus_one->nextFloat() === $one->nextFloat()); +var_dump($max_minus_one->nextFloat() - $max_minus_two->nextFloat() === $one->nextFloat()); +var_dump($max->nextFloat() - $max_minus_two->nextFloat() === $two->nextFloat()); + +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true)