Skip to content

Randomize better checking #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
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)