Skip to content
Closed
11 changes: 11 additions & 0 deletions ext/random/random.c
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@
# include <sanitizer/msan_interface.h>
#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)
Expand Down
6 changes: 6 additions & 0 deletions ext/random/random.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down
26 changes: 25 additions & 1 deletion ext/random/random_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

99 changes: 99 additions & 0 deletions ext/random/randomizer.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
55 changes: 55 additions & 0 deletions ext/random/tests/03_randomizer/methods/nextFloat.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
--TEST--
Random: Randomizer: nextFloat(): Basic functionality
--SKIPIF--
<?php
if (!method_exists("Randomizer", "nextFloat")) {
die("skip Randomizer::nextFloat not available");
}
?>
--FILE--
<?php

use Random\Engine;
use Random\Engine\Mt19937;
use Random\Engine\PcgOneseq128XslRr64;
use Random\Engine\Secure;
use Random\Engine\Test\TestShaEngine;
use Random\Engine\Xoshiro256StarStar;
use Random\Randomizer;

require __DIR__ . "/../../engines.inc";

$engines = [];
$engines[] = new Mt19937(null, MT_RAND_MT19937);
$engines[] = new Mt19937(null, MT_RAND_PHP);
$engines[] = new PcgOneseq128XslRr64();
$engines[] = new Xoshiro256StarStar();
$engines[] = new Secure();
$engines[] = new TestShaEngine();

foreach ($engines as $engine) {
echo $engine::class, PHP_EOL;

$randomizer = new Randomizer($engine);

// Basic range test.
for ($i = 0; $i < 10_000; $i++) {
$result = $randomizer->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
47 changes: 47 additions & 0 deletions ext/random/tests/03_randomizer/methods/nextFloat_spacing.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
--TEST--
Random: Randomizer: nextFloat(): Return values are evenly spaced.
--SKIPIF--
<?php
if (!method_exists("Randomizer", "nextFloat")) {
die("skip Randomizer::nextFloat not available");
}
?>
--FILE--
<?php

use Random\Engine;
use Random\Randomizer;

final class StaticEngine implements Engine
{
public function __construct(private string $value)
{
}

public function generate(): string
{
return $this->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)