From 5017048b75534adbf544efd780ad7d7b246e5456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Wed, 5 Oct 2022 21:25:05 +0200 Subject: [PATCH 01/14] random: Add Randomizer::nextFloat() --- ext/random/random.stub.php | 2 ++ ext/random/random_arginfo.h | 6 +++++- ext/random/randomizer.c | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/ext/random/random.stub.php b/ext/random/random.stub.php index 69049a837b2c2..78ee280adc4d7 100644 --- a/ext/random/random.stub.php +++ b/ext/random/random.stub.php @@ -133,6 +133,8 @@ public function __construct(?Engine $engine = null) {} public function nextInt(): int {} + public function nextFloat(): float {} + 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 1da1b8576b196..5c937916946e7 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: a4226bc7838eba98c5a935b279f681a7d083c0b2 */ + * Stub hash: cde385e02a1131b73ebb878b9e8bbf7a36f3c156 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_lcg_value, 0, 0, IS_DOUBLE, 0) ZEND_END_ARG_INFO() @@ -90,6 +90,8 @@ ZEND_END_ARG_INFO() #define arginfo_class_Random_Randomizer_nextInt arginfo_mt_getrandmax +#define arginfo_class_Random_Randomizer_nextFloat arginfo_lcg_value + #define arginfo_class_Random_Randomizer_getInt arginfo_random_int #define arginfo_class_Random_Randomizer_getBytes arginfo_random_bytes @@ -136,6 +138,7 @@ ZEND_METHOD(Random_Engine_Xoshiro256StarStar, jump); ZEND_METHOD(Random_Engine_Xoshiro256StarStar, jumpLong); ZEND_METHOD(Random_Randomizer, __construct); ZEND_METHOD(Random_Randomizer, nextInt); +ZEND_METHOD(Random_Randomizer, nextFloat); ZEND_METHOD(Random_Randomizer, getInt); ZEND_METHOD(Random_Randomizer, getBytes); ZEND_METHOD(Random_Randomizer, getBytesFromString); @@ -213,6 +216,7 @@ 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) + ZEND_ME(Random_Randomizer, nextFloat, arginfo_class_Random_Randomizer_nextFloat, ZEND_ACC_PUBLIC) 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, getBytesFromString, arginfo_class_Random_Randomizer_getBytesFromString, ZEND_ACC_PUBLIC) diff --git a/ext/random/randomizer.c b/ext/random/randomizer.c index a95e6b0fdd8a8..35331d3758c67 100644 --- a/ext/random/randomizer.c +++ b/ext/random/randomizer.c @@ -109,6 +109,41 @@ PHP_METHOD(Random_Randomizer, nextInt) } /* }}} */ +/* {{{ 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)); + + /* 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. + */ + 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); +} +/* }}} */ + /* {{{ Generate random number in range */ PHP_METHOD(Random_Randomizer, getInt) { From 404342bef022b172477a285d3a652dfbb22b5a06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Mon, 10 Oct 2022 17:40:57 +0200 Subject: [PATCH 02/14] random: Check that doubles are IEEE-754 in Randomizer::nextFloat() --- ext/random/randomizer.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ext/random/randomizer.c b/ext/random/randomizer.c index 35331d3758c67..860a08d611be9 100644 --- a/ext/random/randomizer.c +++ b/ext/random/randomizer.c @@ -118,6 +118,11 @@ PHP_METHOD(Random_Randomizer, nextFloat) ZEND_PARSE_PARAMETERS_NONE(); +#ifndef __STDC_IEC_559__ + zend_throw_exception(random_ce_Random_RandomException, "The nextFloat() method requires the underlying 'double' representation to be IEEE-754.", 0); + RETURN_THROWS(); +#endif + result = 0; total_size = 0; do { @@ -133,6 +138,7 @@ PHP_METHOD(Random_Randomizer, nextFloat) * use the full 64 bits of the uint64_t, because we would * introduce a bias / rounding error. */ + ZEND_ASSERT(DBL_MANT_DIG == 53); const double step_size = 1.0 / (1ULL << 53); /* Use the upper 53 bits, because some engine's lower bits From 8a316785063848db811e44b070021255a0a5be9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Mon, 10 Oct 2022 18:00:30 +0200 Subject: [PATCH 03/14] random: Add Randomizer::nextFloat() tests --- .../03_randomizer/methods/nextFloat.phpt | 49 +++++++++++++++++++ .../methods/nextFloat_spacing.phpt | 41 ++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 ext/random/tests/03_randomizer/methods/nextFloat.phpt create mode 100644 ext/random/tests/03_randomizer/methods/nextFloat_spacing.phpt 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..c8e7046226ec4 --- /dev/null +++ b/ext/random/tests/03_randomizer/methods/nextFloat.phpt @@ -0,0 +1,49 @@ +--TEST-- +Random: Randomizer: nextFloat(): Basic functionality +--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..133d5ff056eab --- /dev/null +++ b/ext/random/tests/03_randomizer/methods/nextFloat_spacing.phpt @@ -0,0 +1,41 @@ +--TEST-- +Random: Randomizer: nextFloat(): Return values are evenly spaced. +--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) From 918ef259c6bf056c0391cd0b76b6a736449ed9e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Tue, 11 Oct 2022 23:09:36 +0200 Subject: [PATCH 04/14] random: Add Randomizer::getFloat() implementing the y-section algorithm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The algorithm is 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 --- ext/random/random.stub.php | 2 + ext/random/random_arginfo.h | 9 ++- ext/random/randomizer.c | 116 +++++++++++++++++++++++++++++------- 3 files changed, 105 insertions(+), 22 deletions(-) diff --git a/ext/random/random.stub.php b/ext/random/random.stub.php index 78ee280adc4d7..71a336aa979e4 100644 --- a/ext/random/random.stub.php +++ b/ext/random/random.stub.php @@ -135,6 +135,8 @@ public function nextInt(): int {} public function nextFloat(): float {} + public function getFloat(float $min, float $max): float {} + 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 5c937916946e7..5cf1306c9386f 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: cde385e02a1131b73ebb878b9e8bbf7a36f3c156 */ + * Stub hash: 773fdbcd817ace97bb0b5cfc01d02a5c69e2b1aa */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_lcg_value, 0, 0, IS_DOUBLE, 0) ZEND_END_ARG_INFO() @@ -92,6 +92,11 @@ ZEND_END_ARG_INFO() #define arginfo_class_Random_Randomizer_nextFloat arginfo_lcg_value +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() + #define arginfo_class_Random_Randomizer_getInt arginfo_random_int #define arginfo_class_Random_Randomizer_getBytes arginfo_random_bytes @@ -139,6 +144,7 @@ ZEND_METHOD(Random_Engine_Xoshiro256StarStar, jumpLong); ZEND_METHOD(Random_Randomizer, __construct); ZEND_METHOD(Random_Randomizer, nextInt); ZEND_METHOD(Random_Randomizer, nextFloat); +ZEND_METHOD(Random_Randomizer, getFloat); ZEND_METHOD(Random_Randomizer, getInt); ZEND_METHOD(Random_Randomizer, getBytes); ZEND_METHOD(Random_Randomizer, getBytesFromString); @@ -217,6 +223,7 @@ 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) ZEND_ME(Random_Randomizer, nextFloat, arginfo_class_Random_Randomizer_nextFloat, ZEND_ACC_PUBLIC) + ZEND_ME(Random_Randomizer, getFloat, arginfo_class_Random_Randomizer_getFloat, ZEND_ACC_PUBLIC) 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, getBytesFromString, arginfo_class_Random_Randomizer_getBytesFromString, ZEND_ACC_PUBLIC) diff --git a/ext/random/randomizer.c b/ext/random/randomizer.c index 860a08d611be9..56f5f01df2693 100644 --- a/ext/random/randomizer.c +++ b/ext/random/randomizer.c @@ -88,27 +88,6 @@ PHP_METHOD(Random_Randomizer, __construct) } /* }}} */ -/* {{{ Generate positive random number */ -PHP_METHOD(Random_Randomizer, nextInt) -{ - php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS); - uint64_t result; - - ZEND_PARSE_PARAMETERS_NONE(); - - result = randomizer->algo->generate(randomizer->status); - if (EG(exception)) { - RETURN_THROWS(); - } - if (randomizer->status->last_generated_size > sizeof(zend_long)) { - zend_throw_exception(random_ce_Random_RandomException, "Generated value exceeds size of int", 0); - RETURN_THROWS(); - } - - RETURN_LONG((zend_long) (result >> 1)); -} -/* }}} */ - /* {{{ Generate a float in [0, 1) */ PHP_METHOD(Random_Randomizer, nextFloat) { @@ -150,6 +129,101 @@ PHP_METHOD(Random_Randomizer, nextFloat) } /* }}} */ +static double getFloat_gamma_low(double x) +{ + return x - nextafter(x, -DBL_MAX); +} + +static double getFloat_gamma_high(double x) +{ + return nextafter(x, DBL_MAX) - x; +} + +static double getFloat_gamma(double x, double y) +{ + double high = getFloat_gamma_high(x); + double low = getFloat_gamma_low(y); + + return high > low ? high : low; +} + +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(); + +#ifndef __STDC_IEC_559__ + zend_throw_exception(random_ce_Random_RandomException, "The getFloat() method requires the underlying 'double' representation to be IEEE-754.", 0); + RETURN_THROWS(); +#endif + + 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); + } +} +/* }}} */ + +/* {{{ Generate positive random number */ +PHP_METHOD(Random_Randomizer, nextInt) +{ + php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS); + uint64_t result; + + ZEND_PARSE_PARAMETERS_NONE(); + + result = randomizer->algo->generate(randomizer->status); + if (EG(exception)) { + RETURN_THROWS(); + } + if (randomizer->status->last_generated_size > sizeof(zend_long)) { + zend_throw_exception(random_ce_Random_RandomException, "Generated value exceeds size of int", 0); + RETURN_THROWS(); + } + + RETURN_LONG((zend_long) (result >> 1)); +} +/* }}} */ + /* {{{ Generate random number in range */ PHP_METHOD(Random_Randomizer, getInt) { From 9cb4291bc7fbc23c78eb8eaaa36a49c3053764b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Thu, 13 Oct 2022 19:57:17 +0200 Subject: [PATCH 05/14] random: Implement getFloat_gamma() optimization see https://github.com/php/php-src/pull/9679/files#r994668327 --- ext/random/randomizer.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ext/random/randomizer.c b/ext/random/randomizer.c index 56f5f01df2693..e602b5e87e7ce 100644 --- a/ext/random/randomizer.c +++ b/ext/random/randomizer.c @@ -141,10 +141,7 @@ static double getFloat_gamma_high(double x) static double getFloat_gamma(double x, double y) { - double high = getFloat_gamma_high(x); - double low = getFloat_gamma_low(y); - - return high > low ? high : low; + return (fabs(x) > fabs(y)) ? getFloat_gamma_high(x) : getFloat_gamma_low(y); } static uint64_t getFloat_ceilint(double a, double b, double g) From 63dc454133b168315c89e20a90e24dfa193a62b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Fri, 28 Oct 2022 20:45:34 +0200 Subject: [PATCH 06/14] random: Add Random\IntervalBoundary --- ext/random/php_random.h | 3 ++ ext/random/random.c | 6 +++ ext/random/random.stub.php | 9 +++- ext/random/random_arginfo.h | 23 ++++++++- ext/random/randomizer.c | 99 +++++++++++++++++++++++++++++++++---- 5 files changed, 128 insertions(+), 12 deletions(-) diff --git a/ext/random/php_random.h b/ext/random/php_random.h index a4665b5d10aca..4d61bb00efc05 100644 --- a/ext/random/php_random.h +++ b/ext/random/php_random.h @@ -270,8 +270,11 @@ extern PHPAPI zend_class_entry *random_ce_Random_Engine_PcgOneseq128XslRr64; extern PHPAPI zend_class_entry *random_ce_Random_Engine_Mt19937; extern PHPAPI zend_class_entry *random_ce_Random_Engine_Xoshiro256StarStar; extern PHPAPI zend_class_entry *random_ce_Random_Engine_Secure; + extern PHPAPI zend_class_entry *random_ce_Random_Randomizer; +extern PHPAPI zend_class_entry *random_ce_Random_IntervalBoundary; + static inline php_random_engine *php_random_engine_from_obj(zend_object *object) { return (php_random_engine *)((char *)(object) - XtOffsetOf(php_random_engine, std)); } diff --git a/ext/random/random.c b/ext/random/random.c index 5f6ae0c720681..37a5879f1a9fe 100644 --- a/ext/random/random.c +++ b/ext/random/random.c @@ -26,6 +26,7 @@ #include "php.h" +#include "Zend/zend_enum.h" #include "Zend/zend_exceptions.h" #include "php_random.h" @@ -76,6 +77,8 @@ PHPAPI zend_class_entry *random_ce_Random_Engine_Secure; PHPAPI zend_class_entry *random_ce_Random_Randomizer; +PHPAPI zend_class_entry *random_ce_Random_IntervalBoundary; + PHPAPI zend_class_entry *random_ce_Random_RandomError; PHPAPI zend_class_entry *random_ce_Random_BrokenRandomEngineError; PHPAPI zend_class_entry *random_ce_Random_RandomException; @@ -896,6 +899,9 @@ PHP_MINIT_FUNCTION(random) random_randomizer_object_handlers.free_obj = randomizer_free_obj; random_randomizer_object_handlers.clone_obj = NULL; + /* Random\IntervalBoundary */ + random_ce_Random_IntervalBoundary = register_class_Random_IntervalBoundary(); + register_random_symbols(module_number); return SUCCESS; diff --git a/ext/random/random.stub.php b/ext/random/random.stub.php index 71a336aa979e4..e87aec3a27ced 100644 --- a/ext/random/random.stub.php +++ b/ext/random/random.stub.php @@ -135,7 +135,7 @@ public function nextInt(): int {} public function nextFloat(): float {} - public function getFloat(float $min, float $max): float {} + public function getFloat(float $min, float $max, IntervalBoundary $boundary = IntervalBoundary::ClosedOpen): float {} public function getInt(int $min, int $max): int {} @@ -154,6 +154,13 @@ public function __serialize(): array {} public function __unserialize(array $data): void {} } + enum IntervalBoundary { + case ClosedOpen; + case ClosedClosed; + case OpenClosed; + case OpenOpen; + } + /** * @strict-properties */ diff --git a/ext/random/random_arginfo.h b/ext/random/random_arginfo.h index 5cf1306c9386f..5fc34c95aee63 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: 773fdbcd817ace97bb0b5cfc01d02a5c69e2b1aa */ + * Stub hash: 7b9594d2eadb778ecec34114b67f2d0ae8bbb58a */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_lcg_value, 0, 0, IS_DOUBLE, 0) ZEND_END_ARG_INFO() @@ -95,6 +95,7 @@ ZEND_END_ARG_INFO() 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_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, boundary, Random\\IntervalBoundary, 0, "Random\\IntervalBoundary::ClosedOpen") ZEND_END_ARG_INFO() #define arginfo_class_Random_Randomizer_getInt arginfo_random_int @@ -236,6 +237,11 @@ static const zend_function_entry class_Random_Randomizer_methods[] = { }; +static const zend_function_entry class_Random_IntervalBoundary_methods[] = { + ZEND_FE_END +}; + + static const zend_function_entry class_Random_RandomError_methods[] = { ZEND_FE_END }; @@ -343,6 +349,21 @@ static zend_class_entry *register_class_Random_Randomizer(void) return class_entry; } +static zend_class_entry *register_class_Random_IntervalBoundary(void) +{ + zend_class_entry *class_entry = zend_register_internal_enum("Random\\IntervalBoundary", IS_UNDEF, class_Random_IntervalBoundary_methods); + + zend_enum_add_case_cstr(class_entry, "ClosedOpen", NULL); + + zend_enum_add_case_cstr(class_entry, "ClosedClosed", NULL); + + zend_enum_add_case_cstr(class_entry, "OpenClosed", NULL); + + zend_enum_add_case_cstr(class_entry, "OpenOpen", NULL); + + return class_entry; +} + static zend_class_entry *register_class_Random_RandomError(zend_class_entry *class_entry_Error) { zend_class_entry ce, *class_entry; diff --git a/ext/random/randomizer.c b/ext/random/randomizer.c index e602b5e87e7ce..8c04b94b38b10 100644 --- a/ext/random/randomizer.c +++ b/ext/random/randomizer.c @@ -24,6 +24,7 @@ #include "ext/standard/php_array.h" #include "ext/standard/php_string.h" +#include "Zend/zend_enum.h" #include "Zend/zend_exceptions.h" static inline void randomizer_common_init(php_random_randomizer *randomizer, zend_object *engine_object) { @@ -160,6 +161,58 @@ static uint64_t getFloat_ceilint(double a, double b, double g) return (s != si) ? (uint64_t)si : (uint64_t)si + (e > 0); } +static double getFloat_closed_open(const php_random_algo *algo, php_random_status *status, double min, double max) +{ + double g = getFloat_gamma(min, max); + uint64_t hi = getFloat_ceilint(min, max, g); + uint64_t k = algo->range(status, 1, hi); + + if (fabs(min) <= fabs(max)) { + return k == hi ? min : max - k * g; + } else { + return min + (k - 1) * g; + } +} + +static double getFloat_closed_closed(const php_random_algo *algo, php_random_status *status, double min, double max) +{ + double g = getFloat_gamma(min, max); + uint64_t hi = getFloat_ceilint(min, max, g); + uint64_t k = algo->range(status, 0, hi); + + if (fabs(min) <= fabs(max)) { + return k == hi ? min : max - k * g; + } else { + return k == hi ? max : min + k * g; + } +} + +static double getFloat_open_closed(const php_random_algo *algo, php_random_status *status, double min, double max) +{ + double g = getFloat_gamma(min, max); + uint64_t hi = getFloat_ceilint(min, max, g); + uint64_t k = algo->range(status, 0, hi - 1); + + if (fabs(min) <= fabs(max)) { + return max - k * g; + } else { + return k == (hi - 1) ? max : min + (k + 1) * g; + } +} + +static double getFloat_open_open(const php_random_algo *algo, php_random_status *status, double min, double max) +{ + double g = getFloat_gamma(min, max); + uint64_t hi = getFloat_ceilint(min, max, g); + uint64_t k = algo->range(status, 1, hi - 1); + + if (fabs(min) <= fabs(max)) { + return max - k * g; + } else { + return min + k * g; + } +} + /* {{{ Generates a random float within [min, max). * * The algorithm used is the γ-section algorithm as published in: @@ -172,10 +225,14 @@ PHP_METHOD(Random_Randomizer, getFloat) { php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS); double min, max; + zend_object *bounds = NULL; + zend_string *bounds_name = zend_string_init("ClosedOpen", strlen("ClosedOpen"), 1); - ZEND_PARSE_PARAMETERS_START(2, 2) + ZEND_PARSE_PARAMETERS_START(2, 3) Z_PARAM_DOUBLE(min) Z_PARAM_DOUBLE(max) + Z_PARAM_OPTIONAL + Z_PARAM_OBJ_OF_CLASS(bounds, random_ce_Random_IntervalBoundary); ZEND_PARSE_PARAMETERS_END(); #ifndef __STDC_IEC_559__ @@ -183,19 +240,41 @@ PHP_METHOD(Random_Randomizer, getFloat) RETURN_THROWS(); #endif - if (UNEXPECTED(max < min)) { - zend_argument_value_error(2, "must be greater than or equal to argument #1 ($min)"); - RETURN_THROWS(); + if (bounds) { + zval *case_name = zend_enum_fetch_case_name(bounds); + bounds_name = Z_STR_P(case_name); } + + if (zend_string_equals_literal(bounds_name, "ClosedOpen")) { + if (UNEXPECTED(max <= min)) { + zend_argument_value_error(2, "must be greater than 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); + RETURN_DOUBLE(getFloat_closed_open(randomizer->algo, randomizer->status, min, max)); + } else if (zend_string_equals_literal(bounds_name, "ClosedClosed")) { + if (UNEXPECTED(max < min)) { + zend_argument_value_error(2, "must be greater than or equal to argument #1 ($min)"); + RETURN_THROWS(); + } - if (fabs(min) <= fabs(max)) { - RETURN_DOUBLE(k == hi ? min : max - k * g); + RETURN_DOUBLE(getFloat_closed_closed(randomizer->algo, randomizer->status, min, max)); + } else if (zend_string_equals_literal(bounds_name, "OpenClosed")) { + if (UNEXPECTED(max <= min)) { + zend_argument_value_error(2, "must be greater than argument #1 ($min)"); + RETURN_THROWS(); + } + + RETURN_DOUBLE(getFloat_open_closed(randomizer->algo, randomizer->status, min, max)); + } else if (zend_string_equals_literal(bounds_name, "OpenOpen")) { + if (UNEXPECTED(max <= min)) { + zend_argument_value_error(2, "must be greater than argument #1 ($min)"); + RETURN_THROWS(); + } + + RETURN_DOUBLE(getFloat_open_open(randomizer->algo, randomizer->status, min, max)); } else { - RETURN_DOUBLE(min + (k - 1) * g); + ZEND_UNREACHABLE(); } } /* }}} */ From 18f2ab816284c39c81a528d77ae1823de1afa46f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Sun, 27 Nov 2022 19:45:17 +0100 Subject: [PATCH 07/14] =?UTF-8?q?random:=20Split=20the=20implementation=20?= =?UTF-8?q?of=20=CE=B3-section=20into=20its=20own=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ext/random/config.m4 | 1 + ext/random/config.w32 | 2 +- ext/random/gammasection.c | 115 ++++++++++++++++++++++++++++++++++++++ ext/random/php_random.h | 5 ++ ext/random/randomizer.c | 99 ++------------------------------ 5 files changed, 128 insertions(+), 94 deletions(-) create mode 100644 ext/random/gammasection.c diff --git a/ext/random/config.m4 b/ext/random/config.m4 index a8e6d5a568991..8ed67b9fddaca 100644 --- a/ext/random/config.m4 +++ b/ext/random/config.m4 @@ -25,6 +25,7 @@ PHP_NEW_EXTENSION(random, engine_xoshiro256starstar.c \ engine_secure.c \ engine_user.c \ + gammasection.c \ randomizer.c, no,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1) PHP_INSTALL_HEADERS([ext/random], [php_random.h]) diff --git a/ext/random/config.w32 b/ext/random/config.w32 index bfbd153c1680d..e66e094039d19 100644 --- a/ext/random/config.w32 +++ b/ext/random/config.w32 @@ -1,4 +1,4 @@ EXTENSION("random", "random.c", false /* never shared */, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); PHP_RANDOM="yes"; -ADD_SOURCES(configure_module_dirname, "engine_combinedlcg.c engine_mt19937.c engine_pcgoneseq128xslrr64.c engine_xoshiro256starstar.c engine_secure.c engine_user.c randomizer.c", "random"); +ADD_SOURCES(configure_module_dirname, "engine_combinedlcg.c engine_mt19937.c engine_pcgoneseq128xslrr64.c engine_xoshiro256starstar.c engine_secure.c engine_user.c gammasection.c randomizer.c", "random"); PHP_INSTALL_HEADERS("ext/random", "php_random.h"); diff --git a/ext/random/gammasection.c b/ext/random/gammasection.c new file mode 100644 index 0000000000000..dd6c77b83431e --- /dev/null +++ b/ext/random/gammasection.c @@ -0,0 +1,115 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Tim Düsterhus | + | | + | Based on code from: Frédéric Goualard | + +----------------------------------------------------------------------+ +*/ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "php.h" +#include "php_random.h" +#include + +/* This file implements 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 + */ + +static double gamma_low(double x) +{ + return x - nextafter(x, -DBL_MAX); +} + +static double gamma_high(double x) +{ + return nextafter(x, DBL_MAX) - x; +} + +static double gamma_max(double x, double y) +{ + return (fabs(x) > fabs(y)) ? gamma_high(x) : gamma_low(y); +} + +static uint64_t 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); +} + +PHPAPI double php_random_gammasection_closed_open(const php_random_algo *algo, php_random_status *status, double min, double max) +{ + double g = gamma_max(min, max); + uint64_t hi = ceilint(min, max, g); + uint64_t k = algo->range(status, 1, hi); + + if (fabs(min) <= fabs(max)) { + return k == hi ? min : max - k * g; + } else { + return min + (k - 1) * g; + } +} + +PHPAPI double php_random_gammasection_closed_closed(const php_random_algo *algo, php_random_status *status, double min, double max) +{ + double g = gamma_max(min, max); + uint64_t hi = ceilint(min, max, g); + uint64_t k = algo->range(status, 0, hi); + + if (fabs(min) <= fabs(max)) { + return k == hi ? min : max - k * g; + } else { + return k == hi ? max : min + k * g; + } +} + +PHPAPI double php_random_gammasection_open_closed(const php_random_algo *algo, php_random_status *status, double min, double max) +{ + double g = gamma_max(min, max); + uint64_t hi = ceilint(min, max, g); + uint64_t k = algo->range(status, 0, hi - 1); + + if (fabs(min) <= fabs(max)) { + return max - k * g; + } else { + return k == (hi - 1) ? max : min + (k + 1) * g; + } +} + +PHPAPI double php_random_gammasection_open_open(const php_random_algo *algo, php_random_status *status, double min, double max) +{ + double g = gamma_max(min, max); + uint64_t hi = ceilint(min, max, g); + uint64_t k = algo->range(status, 1, hi - 1); + + if (fabs(min) <= fabs(max)) { + return max - k * g; + } else { + return min + k * g; + } +} diff --git a/ext/random/php_random.h b/ext/random/php_random.h index 4d61bb00efc05..f942fff8f7859 100644 --- a/ext/random/php_random.h +++ b/ext/random/php_random.h @@ -309,6 +309,11 @@ PHPAPI void php_random_pcgoneseq128xslrr64_advance(php_random_status_state_pcgon PHPAPI void php_random_xoshiro256starstar_jump(php_random_status_state_xoshiro256starstar *state); PHPAPI void php_random_xoshiro256starstar_jump_long(php_random_status_state_xoshiro256starstar *state); +PHPAPI double php_random_gammasection_closed_open(const php_random_algo *algo, php_random_status *status, double min, double max); +PHPAPI double php_random_gammasection_closed_closed(const php_random_algo *algo, php_random_status *status, double min, double max); +PHPAPI double php_random_gammasection_open_closed(const php_random_algo *algo, php_random_status *status, double min, double max); +PHPAPI double php_random_gammasection_open_open(const php_random_algo *algo, php_random_status *status, double min, double max); + extern zend_module_entry random_module_entry; # define phpext_random_ptr &random_module_entry diff --git a/ext/random/randomizer.c b/ext/random/randomizer.c index 8c04b94b38b10..a48968992c5da 100644 --- a/ext/random/randomizer.c +++ b/ext/random/randomizer.c @@ -130,96 +130,9 @@ PHP_METHOD(Random_Randomizer, nextFloat) } /* }}} */ -static double getFloat_gamma_low(double x) -{ - return x - nextafter(x, -DBL_MAX); -} - -static double getFloat_gamma_high(double x) -{ - return nextafter(x, DBL_MAX) - 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); -} - -static double getFloat_closed_open(const php_random_algo *algo, php_random_status *status, double min, double max) -{ - double g = getFloat_gamma(min, max); - uint64_t hi = getFloat_ceilint(min, max, g); - uint64_t k = algo->range(status, 1, hi); - - if (fabs(min) <= fabs(max)) { - return k == hi ? min : max - k * g; - } else { - return min + (k - 1) * g; - } -} - -static double getFloat_closed_closed(const php_random_algo *algo, php_random_status *status, double min, double max) -{ - double g = getFloat_gamma(min, max); - uint64_t hi = getFloat_ceilint(min, max, g); - uint64_t k = algo->range(status, 0, hi); - - if (fabs(min) <= fabs(max)) { - return k == hi ? min : max - k * g; - } else { - return k == hi ? max : min + k * g; - } -} - -static double getFloat_open_closed(const php_random_algo *algo, php_random_status *status, double min, double max) -{ - double g = getFloat_gamma(min, max); - uint64_t hi = getFloat_ceilint(min, max, g); - uint64_t k = algo->range(status, 0, hi - 1); - - if (fabs(min) <= fabs(max)) { - return max - k * g; - } else { - return k == (hi - 1) ? max : min + (k + 1) * g; - } -} - -static double getFloat_open_open(const php_random_algo *algo, php_random_status *status, double min, double max) -{ - double g = getFloat_gamma(min, max); - uint64_t hi = getFloat_ceilint(min, max, g); - uint64_t k = algo->range(status, 1, hi - 1); - - if (fabs(min) <= fabs(max)) { - return max - k * g; - } else { - return min + k * g; - } -} - -/* {{{ Generates a random float within [min, max). - * - * The algorithm used is the γ-section algorithm as published in: +/* {{{ Generates a random float within a configurable interval. * - * 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 + * This method uses the γ-section algorithm by Frédéric Goualard. */ PHP_METHOD(Random_Randomizer, getFloat) { @@ -251,28 +164,28 @@ PHP_METHOD(Random_Randomizer, getFloat) RETURN_THROWS(); } - RETURN_DOUBLE(getFloat_closed_open(randomizer->algo, randomizer->status, min, max)); + RETURN_DOUBLE(php_random_gammasection_closed_open(randomizer->algo, randomizer->status, min, max)); } else if (zend_string_equals_literal(bounds_name, "ClosedClosed")) { if (UNEXPECTED(max < min)) { zend_argument_value_error(2, "must be greater than or equal to argument #1 ($min)"); RETURN_THROWS(); } - RETURN_DOUBLE(getFloat_closed_closed(randomizer->algo, randomizer->status, min, max)); + RETURN_DOUBLE(php_random_gammasection_closed_closed(randomizer->algo, randomizer->status, min, max)); } else if (zend_string_equals_literal(bounds_name, "OpenClosed")) { if (UNEXPECTED(max <= min)) { zend_argument_value_error(2, "must be greater than argument #1 ($min)"); RETURN_THROWS(); } - RETURN_DOUBLE(getFloat_open_closed(randomizer->algo, randomizer->status, min, max)); + RETURN_DOUBLE(php_random_gammasection_open_closed(randomizer->algo, randomizer->status, min, max)); } else if (zend_string_equals_literal(bounds_name, "OpenOpen")) { if (UNEXPECTED(max <= min)) { zend_argument_value_error(2, "must be greater than argument #1 ($min)"); RETURN_THROWS(); } - RETURN_DOUBLE(getFloat_open_open(randomizer->algo, randomizer->status, min, max)); + RETURN_DOUBLE(php_random_gammasection_open_open(randomizer->algo, randomizer->status, min, max)); } else { ZEND_UNREACHABLE(); } From b80ecbb9da84de412cef237949070f17f830bcb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Sun, 27 Nov 2022 20:09:00 +0100 Subject: [PATCH 08/14] random: Add tests for Randomizer::getFloat() --- .../tests/03_randomizer/methods/getFloat.phpt | 50 ++++++++++++++++ .../03_randomizer/methods/getFloat_error.phpt | 59 +++++++++++++++++++ .../03_randomizer/methods/nextFloat.phpt | 2 +- 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 ext/random/tests/03_randomizer/methods/getFloat.phpt create mode 100644 ext/random/tests/03_randomizer/methods/getFloat_error.phpt diff --git a/ext/random/tests/03_randomizer/methods/getFloat.phpt b/ext/random/tests/03_randomizer/methods/getFloat.phpt new file mode 100644 index 0000000000000..1fcec7f3e223a --- /dev/null +++ b/ext/random/tests/03_randomizer/methods/getFloat.phpt @@ -0,0 +1,50 @@ +--TEST-- +Random: Randomizer: getFloat(): Basic functionality +--FILE-- +getFloat(-$i, $i, IntervalBoundary::ClosedClosed); + + if ($result > $i || $result < -$i) { + 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/getFloat_error.phpt b/ext/random/tests/03_randomizer/methods/getFloat_error.phpt new file mode 100644 index 0000000000000..30ed2df066585 --- /dev/null +++ b/ext/random/tests/03_randomizer/methods/getFloat_error.phpt @@ -0,0 +1,59 @@ +--TEST-- +Random: Randomizer: getFloat(): Parameters are correctly validated +--FILE-- +name, PHP_EOL; + + try { + var_dump(randomizer()->getFloat(0.0, -0.1, $boundary)); + } catch (ValueError $e) { + echo $e->getMessage(), PHP_EOL; + } + + try { + var_dump(randomizer()->getFloat(0.0, 0.0, $boundary)); + } catch (ValueError $e) { + echo $e->getMessage(), PHP_EOL; + } + + try { + // Both values round to the same float. + var_dump(randomizer()->getFloat(100_000_000_000_000_000.0, 100_000_000_000_000_000.1, $boundary)); + } catch (ValueError $e) { + echo $e->getMessage(), PHP_EOL; + } +} + +?> +--EXPECT-- +ClosedClosed +Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than or equal to argument #1 ($min) +float(0) +float(1.0E+17) +ClosedOpen +Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) +Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) +Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) +OpenClosed +Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) +Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) +Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) +OpenOpen +Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) +Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) +Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) diff --git a/ext/random/tests/03_randomizer/methods/nextFloat.phpt b/ext/random/tests/03_randomizer/methods/nextFloat.phpt index c8e7046226ec4..4a583d00e78d9 100644 --- a/ext/random/tests/03_randomizer/methods/nextFloat.phpt +++ b/ext/random/tests/03_randomizer/methods/nextFloat.phpt @@ -30,7 +30,7 @@ foreach ($engines as $engine) { for ($i = 0; $i < 10_000; $i++) { $result = $randomizer->nextFloat(); - if ($result < 0 || $result >= 1) { + if ($result >= 1 || $result < 0) { die("failure: out of range at {$i}"); } } From a323cb069ecab469659789bc3f873fd1b8a9d602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Fri, 9 Dec 2022 21:10:12 +0100 Subject: [PATCH 09/14] =?UTF-8?q?random:=20Fix=20=CE=B3-section=20for=2032?= =?UTF-8?q?-bit=20systems?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ext/random/gammasection.c | 8 ++++---- ext/random/php_random.h | 2 ++ ext/random/random.c | 8 ++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ext/random/gammasection.c b/ext/random/gammasection.c index dd6c77b83431e..fb0c2cd3bf94d 100644 --- a/ext/random/gammasection.c +++ b/ext/random/gammasection.c @@ -66,7 +66,7 @@ PHPAPI double php_random_gammasection_closed_open(const php_random_algo *algo, p { double g = gamma_max(min, max); uint64_t hi = ceilint(min, max, g); - uint64_t k = algo->range(status, 1, hi); + uint64_t k = 1 + php_random_range64(algo, status, hi - 1); /* [1, hi] */ if (fabs(min) <= fabs(max)) { return k == hi ? min : max - k * g; @@ -79,7 +79,7 @@ PHPAPI double php_random_gammasection_closed_closed(const php_random_algo *algo, { double g = gamma_max(min, max); uint64_t hi = ceilint(min, max, g); - uint64_t k = algo->range(status, 0, hi); + uint64_t k = php_random_range64(algo, status, hi); /* [0, hi] */ if (fabs(min) <= fabs(max)) { return k == hi ? min : max - k * g; @@ -92,7 +92,7 @@ PHPAPI double php_random_gammasection_open_closed(const php_random_algo *algo, p { double g = gamma_max(min, max); uint64_t hi = ceilint(min, max, g); - uint64_t k = algo->range(status, 0, hi - 1); + uint64_t k = php_random_range64(algo, status, hi - 1); /* [0, hi - 1] */ if (fabs(min) <= fabs(max)) { return max - k * g; @@ -105,7 +105,7 @@ PHPAPI double php_random_gammasection_open_open(const php_random_algo *algo, php { double g = gamma_max(min, max); uint64_t hi = ceilint(min, max, g); - uint64_t k = algo->range(status, 1, hi - 1); + uint64_t k = 1 + php_random_range64(algo, status, hi - 2); /* [1, hi - 1] */ if (fabs(min) <= fabs(max)) { return max - k * g; diff --git a/ext/random/php_random.h b/ext/random/php_random.h index f942fff8f7859..52a6b787edde5 100644 --- a/ext/random/php_random.h +++ b/ext/random/php_random.h @@ -293,6 +293,8 @@ PHPAPI void php_random_status_free(php_random_status *status, const bool persist PHPAPI php_random_engine *php_random_engine_common_init(zend_class_entry *ce, zend_object_handlers *handlers, const php_random_algo *algo); PHPAPI void php_random_engine_common_free_object(zend_object *object); PHPAPI zend_object *php_random_engine_common_clone_object(zend_object *object); +PHPAPI uint32_t php_random_range32(const php_random_algo *algo, php_random_status *status, uint32_t umax); +PHPAPI uint64_t php_random_range64(const php_random_algo *algo, php_random_status *status, uint64_t umax); PHPAPI zend_long php_random_range(const php_random_algo *algo, php_random_status *status, zend_long min, zend_long max); PHPAPI const php_random_algo *php_random_default_algo(void); PHPAPI php_random_status *php_random_default_status(void); diff --git a/ext/random/random.c b/ext/random/random.c index 37a5879f1a9fe..e35b3bbba47eb 100644 --- a/ext/random/random.c +++ b/ext/random/random.c @@ -89,7 +89,7 @@ static zend_object_handlers random_engine_xoshiro256starstar_object_handlers; static zend_object_handlers random_engine_secure_object_handlers; static zend_object_handlers random_randomizer_object_handlers; -static inline uint32_t rand_range32(const php_random_algo *algo, php_random_status *status, uint32_t umax) +PHPAPI uint32_t php_random_range32(const php_random_algo *algo, php_random_status *status, uint32_t umax) { uint32_t result, limit; size_t total_size = 0; @@ -145,7 +145,7 @@ static inline uint32_t rand_range32(const php_random_algo *algo, php_random_stat return result % umax; } -static inline uint64_t rand_range64(const php_random_algo *algo, php_random_status *status, uint64_t umax) +PHPAPI uint64_t php_random_range64(const php_random_algo *algo, php_random_status *status, uint64_t umax) { uint64_t result, limit; size_t total_size = 0; @@ -313,10 +313,10 @@ PHPAPI zend_long php_random_range(const php_random_algo *algo, php_random_status zend_ulong umax = (zend_ulong) max - (zend_ulong) min; if (umax > UINT32_MAX) { - return (zend_long) (rand_range64(algo, status, umax) + min); + return (zend_long) (php_random_range64(algo, status, umax) + min); } - return (zend_long) (rand_range32(algo, status, umax) + min); + return (zend_long) (php_random_range32(algo, status, umax) + min); } /* }}} */ From bd0bee46e57f820a5394d9f0f2b096f304ab92e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Fri, 9 Dec 2022 21:36:59 +0100 Subject: [PATCH 10/14] random: Replace check for __STDC_IEC_559__ by compile-time check for DBL_MANT_DIG --- ext/random/randomizer.c | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/ext/random/randomizer.c b/ext/random/randomizer.c index a48968992c5da..c29b3697db79a 100644 --- a/ext/random/randomizer.c +++ b/ext/random/randomizer.c @@ -98,11 +98,6 @@ PHP_METHOD(Random_Randomizer, nextFloat) ZEND_PARSE_PARAMETERS_NONE(); -#ifndef __STDC_IEC_559__ - zend_throw_exception(random_ce_Random_RandomException, "The nextFloat() method requires the underlying 'double' representation to be IEEE-754.", 0); - RETURN_THROWS(); -#endif - result = 0; total_size = 0; do { @@ -118,7 +113,9 @@ PHP_METHOD(Random_Randomizer, nextFloat) * use the full 64 bits of the uint64_t, because we would * introduce a bias / rounding error. */ - ZEND_ASSERT(DBL_MANT_DIG == 53); +#if DBL_MANT_DIG != 53 +# error "Random_Randomizer::nextFloat(): Requires DBL_MANT_DIG == 53 to work." +#endif const double step_size = 1.0 / (1ULL << 53); /* Use the upper 53 bits, because some engine's lower bits @@ -148,11 +145,6 @@ PHP_METHOD(Random_Randomizer, getFloat) Z_PARAM_OBJ_OF_CLASS(bounds, random_ce_Random_IntervalBoundary); ZEND_PARSE_PARAMETERS_END(); -#ifndef __STDC_IEC_559__ - zend_throw_exception(random_ce_Random_RandomException, "The getFloat() method requires the underlying 'double' representation to be IEEE-754.", 0); - RETURN_THROWS(); -#endif - if (bounds) { zval *case_name = zend_enum_fetch_case_name(bounds); bounds_name = Z_STR_P(case_name); From 69a0b8d71b7fef4c8f04365769c86949361a638d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Mon, 12 Dec 2022 17:36:10 +0100 Subject: [PATCH 11/14] random: Drop nextFloat_spacing.phpt --- .../methods/nextFloat_spacing.phpt | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 ext/random/tests/03_randomizer/methods/nextFloat_spacing.phpt diff --git a/ext/random/tests/03_randomizer/methods/nextFloat_spacing.phpt b/ext/random/tests/03_randomizer/methods/nextFloat_spacing.phpt deleted file mode 100644 index 133d5ff056eab..0000000000000 --- a/ext/random/tests/03_randomizer/methods/nextFloat_spacing.phpt +++ /dev/null @@ -1,41 +0,0 @@ ---TEST-- -Random: Randomizer: nextFloat(): Return values are evenly spaced. ---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) From ed8049b7998bc7772d9e119abd296d9ce9322fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Mon, 12 Dec 2022 17:36:26 +0100 Subject: [PATCH 12/14] random: Optimize Randomizer::getFloat() implementation --- ext/random/randomizer.c | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/ext/random/randomizer.c b/ext/random/randomizer.c index c29b3697db79a..71306e0c21524 100644 --- a/ext/random/randomizer.c +++ b/ext/random/randomizer.c @@ -136,7 +136,7 @@ PHP_METHOD(Random_Randomizer, getFloat) php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS); double min, max; zend_object *bounds = NULL; - zend_string *bounds_name = zend_string_init("ClosedOpen", strlen("ClosedOpen"), 1); + int bounds_type = 'C' + sizeof("ClosedOpen") - 1; ZEND_PARSE_PARAMETERS_START(2, 3) Z_PARAM_DOUBLE(min) @@ -147,38 +147,41 @@ PHP_METHOD(Random_Randomizer, getFloat) if (bounds) { zval *case_name = zend_enum_fetch_case_name(bounds); - bounds_name = Z_STR_P(case_name); + zend_string *bounds_name = Z_STR_P(case_name); + + bounds_type = ZSTR_VAL(bounds_name)[0] + ZSTR_LEN(bounds_name); } - if (zend_string_equals_literal(bounds_name, "ClosedOpen")) { + switch (bounds_type) { + case 'C' + sizeof("ClosedOpen") - 1: if (UNEXPECTED(max <= min)) { zend_argument_value_error(2, "must be greater than argument #1 ($min)"); RETURN_THROWS(); } RETURN_DOUBLE(php_random_gammasection_closed_open(randomizer->algo, randomizer->status, min, max)); - } else if (zend_string_equals_literal(bounds_name, "ClosedClosed")) { + case 'C' + sizeof("ClosedClosed") - 1: if (UNEXPECTED(max < min)) { zend_argument_value_error(2, "must be greater than or equal to argument #1 ($min)"); RETURN_THROWS(); } RETURN_DOUBLE(php_random_gammasection_closed_closed(randomizer->algo, randomizer->status, min, max)); - } else if (zend_string_equals_literal(bounds_name, "OpenClosed")) { + case 'O' + sizeof("OpenClosed") - 1: if (UNEXPECTED(max <= min)) { zend_argument_value_error(2, "must be greater than argument #1 ($min)"); RETURN_THROWS(); } RETURN_DOUBLE(php_random_gammasection_open_closed(randomizer->algo, randomizer->status, min, max)); - } else if (zend_string_equals_literal(bounds_name, "OpenOpen")) { + case 'O' + sizeof("OpenOpen") - 1: if (UNEXPECTED(max <= min)) { zend_argument_value_error(2, "must be greater than argument #1 ($min)"); RETURN_THROWS(); } RETURN_DOUBLE(php_random_gammasection_open_open(randomizer->algo, randomizer->status, min, max)); - } else { + default: ZEND_UNREACHABLE(); } } From c51cf820fee76931f943354c8b352324be57590b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Tue, 13 Dec 2022 23:30:22 +0100 Subject: [PATCH 13/14] random: Reject non-finite parameters in Randomizer::getFloat() --- ext/random/randomizer.c | 10 ++++ .../03_randomizer/methods/getFloat_error.phpt | 60 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/ext/random/randomizer.c b/ext/random/randomizer.c index 71306e0c21524..0a801e35c74c6 100644 --- a/ext/random/randomizer.c +++ b/ext/random/randomizer.c @@ -145,6 +145,16 @@ PHP_METHOD(Random_Randomizer, getFloat) Z_PARAM_OBJ_OF_CLASS(bounds, random_ce_Random_IntervalBoundary); ZEND_PARSE_PARAMETERS_END(); + if (!zend_finite(min)) { + zend_argument_value_error(1, "must be finite"); + RETURN_THROWS(); + } + + if (!zend_finite(max)) { + zend_argument_value_error(2, "must be finite"); + RETURN_THROWS(); + } + if (bounds) { zval *case_name = zend_enum_fetch_case_name(bounds); zend_string *bounds_name = Z_STR_P(case_name); diff --git a/ext/random/tests/03_randomizer/methods/getFloat_error.phpt b/ext/random/tests/03_randomizer/methods/getFloat_error.phpt index 30ed2df066585..1e200f2507a69 100644 --- a/ext/random/tests/03_randomizer/methods/getFloat_error.phpt +++ b/ext/random/tests/03_randomizer/methods/getFloat_error.phpt @@ -19,6 +19,42 @@ foreach ([ ] as $boundary) { echo $boundary->name, PHP_EOL; + try { + var_dump(randomizer()->getFloat(NAN, 0.0, $boundary)); + } catch (ValueError $e) { + echo $e->getMessage(), PHP_EOL; + } + + try { + var_dump(randomizer()->getFloat(INF, 0.0, $boundary)); + } catch (ValueError $e) { + echo $e->getMessage(), PHP_EOL; + } + + try { + var_dump(randomizer()->getFloat(-INF, 0.0, $boundary)); + } catch (ValueError $e) { + echo $e->getMessage(), PHP_EOL; + } + + try { + var_dump(randomizer()->getFloat(0.0, NAN, $boundary)); + } catch (ValueError $e) { + echo $e->getMessage(), PHP_EOL; + } + + try { + var_dump(randomizer()->getFloat(0.0, INF, $boundary)); + } catch (ValueError $e) { + echo $e->getMessage(), PHP_EOL; + } + + try { + var_dump(randomizer()->getFloat(0.0, -INF, $boundary)); + } catch (ValueError $e) { + echo $e->getMessage(), PHP_EOL; + } + try { var_dump(randomizer()->getFloat(0.0, -0.1, $boundary)); } catch (ValueError $e) { @@ -42,18 +78,42 @@ foreach ([ ?> --EXPECT-- ClosedClosed +Random\Randomizer::getFloat(): Argument #1 ($min) must be finite +Random\Randomizer::getFloat(): Argument #1 ($min) must be finite +Random\Randomizer::getFloat(): Argument #1 ($min) must be finite +Random\Randomizer::getFloat(): Argument #2 ($max) must be finite +Random\Randomizer::getFloat(): Argument #2 ($max) must be finite +Random\Randomizer::getFloat(): Argument #2 ($max) must be finite Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than or equal to argument #1 ($min) float(0) float(1.0E+17) ClosedOpen +Random\Randomizer::getFloat(): Argument #1 ($min) must be finite +Random\Randomizer::getFloat(): Argument #1 ($min) must be finite +Random\Randomizer::getFloat(): Argument #1 ($min) must be finite +Random\Randomizer::getFloat(): Argument #2 ($max) must be finite +Random\Randomizer::getFloat(): Argument #2 ($max) must be finite +Random\Randomizer::getFloat(): Argument #2 ($max) must be finite Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) OpenClosed +Random\Randomizer::getFloat(): Argument #1 ($min) must be finite +Random\Randomizer::getFloat(): Argument #1 ($min) must be finite +Random\Randomizer::getFloat(): Argument #1 ($min) must be finite +Random\Randomizer::getFloat(): Argument #2 ($max) must be finite +Random\Randomizer::getFloat(): Argument #2 ($max) must be finite +Random\Randomizer::getFloat(): Argument #2 ($max) must be finite Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) OpenOpen +Random\Randomizer::getFloat(): Argument #1 ($min) must be finite +Random\Randomizer::getFloat(): Argument #1 ($min) must be finite +Random\Randomizer::getFloat(): Argument #1 ($min) must be finite +Random\Randomizer::getFloat(): Argument #2 ($max) must be finite +Random\Randomizer::getFloat(): Argument #2 ($max) must be finite +Random\Randomizer::getFloat(): Argument #2 ($max) must be finite Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) Random\Randomizer::getFloat(): Argument #2 ($max) must be greater than argument #1 ($min) From 1c3d9970cc8e81080c1b841858417d87900cb4e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Wed, 14 Dec 2022 17:41:34 +0100 Subject: [PATCH 14/14] =?UTF-8?q?random:=20Add=20NEWS/UPGRADING=20for=20Ra?= =?UTF-8?q?ndomizer=E2=80=99s=20float=20functionality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NEWS | 1 + UPGRADING | 2 ++ 2 files changed, 3 insertions(+) diff --git a/NEWS b/NEWS index 6b45dc03c3117..ac32c41274f8c 100644 --- a/NEWS +++ b/NEWS @@ -56,6 +56,7 @@ PHP NEWS - Random: . Added Randomizer::getBytesFromString(). (Joshua Rüsweg) + . Added Randomizer::nextFloat(), ::getFloat(), and IntervalBoundary. (timwolla) - Reflection: . Fix GH-9470 (ReflectionMethod constructor should not find private parent diff --git a/UPGRADING b/UPGRADING index fb40184c7d54d..7a6fdac636bb1 100644 --- a/UPGRADING +++ b/UPGRADING @@ -66,6 +66,8 @@ PHP 8.3 UPGRADE NOTES - Random: . Added Randomizer::getBytesFromString(). RFC: https://wiki.php.net/rfc/randomizer_additions + . Added Randomizer::nextFloat(), ::getFloat(), and IntervalBoundary. + RFC: https://wiki.php.net/rfc/randomizer_additions - Sockets: . Added socket_atmark to checks if the socket is OOB marked.