feat(stdlib): optimize random function calls

master
韩天峰 8 hours ago
parent 684c47458b
commit 7dee6d0ac3
  1. 15
      phpunit/code/random-direct-calls.php
  2. 6
      phpunit/code/random-invalid-getrandmax-arity.php
  3. 6
      phpunit/code/random-invalid-mt-rand-arity.php
  4. 6
      phpunit/code/random-invalid-rand-arity.php
  5. 6
      phpunit/code/random-invalid-random-bytes-arity.php
  6. 6
      phpunit/code/random-invalid-random-int-arity.php
  7. 70
      phpunit/src/RandomOptimizerTest.php
  8. 19
      src/CompilerBase.php
  9. 55
      src/Optimizer/FuncCallOptimizer.php
  10. 45
      tests/compiler/stdlib/random-direct.phpt

@ -0,0 +1,15 @@
<?php
function randomDirectCalls(): array
{
return [
mt_rand(),
mt_rand(1, 10),
rand(),
rand(10, 1),
random_int(1, 10),
random_bytes(16),
mt_getrandmax(),
getrandmax(),
];
}

@ -0,0 +1,6 @@
<?php
function invalidGetRandMaxArity(): int
{
return mt_getrandmax(1);
}

@ -0,0 +1,6 @@
<?php
function invalidMtRandArity(): int
{
return mt_rand(1);
}

@ -0,0 +1,6 @@
<?php
function invalidRandArity(): int
{
return rand(1, 2, 3);
}

@ -0,0 +1,6 @@
<?php
function invalidRandomBytesArity(): string
{
return random_bytes(1, 2);
}

@ -0,0 +1,6 @@
<?php
function invalidRandomIntArity(): int
{
return random_int(1);
}

@ -0,0 +1,70 @@
<?php
use TypePhp\CompilerTest;
use TypePhp\Exception\TestError;
final class RandomOptimizerTest extends BaseTest
{
public function testRandomCallsUseDirectPhpxWrappersAndFoldFixedMaximum(): void
{
$code = $this->compileFixture('random-direct-calls.php');
self::assertSame(2, substr_count($code, 'php::fn::mt_rand('));
self::assertSame(2, substr_count($code, 'php::fn::rand('));
self::assertSame(1, substr_count($code, 'php::fn::random_int('));
self::assertSame(1, substr_count($code, 'php::fn::random_bytes('));
self::assertSame(2, substr_count($code, '2147483647L'));
self::assertStringNotContainsString('mt_getrandmax', $code);
self::assertStringNotContainsString('getrandmax', $code);
self::assertStringNotContainsString('php::call(', $code);
}
/** @dataProvider invalidArityProvider */
public function testMtRandAndRandRejectUnsupportedArgumentCounts(
string $source,
string $message,
): void {
$this->expectException(TestError::class);
$this->expectExceptionMessage($message);
$this->compileFixture($source);
}
public static function invalidArityProvider(): iterable
{
yield 'mt_rand with one argument' => [
'random-invalid-mt-rand-arity.php',
'mt_rand() expects exactly 0 or 2 arguments, 1 given',
];
yield 'rand with three arguments' => [
'random-invalid-rand-arity.php',
'rand() expects exactly 0 or 2 arguments, 3 given',
];
yield 'random_int with one argument' => [
'random-invalid-random-int-arity.php',
'random_int() expects at least 2 argument(s), 1 given',
];
yield 'random_bytes with two arguments' => [
'random-invalid-random-bytes-arity.php',
'random_bytes() expects at most 1 argument(s), 2 given',
];
yield 'folded mt_getrandmax with an argument' => [
'random-invalid-getrandmax-arity.php',
'mt_getrandmax() expects at most 0 argument(s), 1 given',
];
}
private function compileFixture(string $file): string
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/' . $file;
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$generated = $compiler->convertFile($source);
$code = file_get_contents($generated);
self::assertIsString($code);
return $code;
}
}

@ -3438,16 +3438,27 @@ class CompilerBase implements PropertyAccessContext
protected function checkInternalFunctionArgCount(string $funcName, Node\Expr\FuncCall $expr): void
{
$ref = Reflection::getFunction($funcName);
if (!$ref) {
return;
if ($ref) {
$this->validateInternalNamedCallArgs($ref, $expr->args);
}
$this->validateInternalNamedCallArgs($ref, $expr->args);
if ($this->hasUnpackCallArg($expr->args)) {
return;
}
$actualArgCount = count($expr->args);
$config = $this->getFuncCallConfig()[ltrim($funcName, '\\')] ?? null;
$allowedArgCounts = is_array($config) ? ($config['argCounts'] ?? null) : null;
if (is_array($allowedArgCounts) && !in_array($actualArgCount, $allowedArgCounts, true)) {
$expected = implode(' or ', $allowedArgCounts);
$this->fatalError(
$expr,
"{$funcName}() expects exactly {$expected} arguments, {$actualArgCount} given",
);
}
if (!$ref) {
return;
}
$minArgs = $ref->getNumberOfRequiredParameters();
$maxArgs = $ref->getNumberOfParameters();
$actualArgCount = count($expr->args);
if ($minArgs > 0 && $actualArgCount < $minArgs) {
$this->fatalError($expr, "{$funcName}() expects at least {$minArgs} argument(s), {$actualArgCount} given");
}

@ -87,7 +87,6 @@ trait FuncCallOptimizer
'base64_encode', 'base64_decode',
'urlencode', 'urldecode', 'rawurlencode', 'rawurldecode',
'json_encode', 'json_decode', 'serialize', 'unserialize',
'random_int', 'random_bytes', 'mt_rand', 'rand',
'strstr', 'strrpos', 'is_a', 'is_subclass_of',
'uniqid',
'dirname', 'basename',
@ -146,6 +145,56 @@ trait FuncCallOptimizer
'intrinsic' => true,
],
// Random extension core functions. mt_rand()/rand() accept only
// zero or two arguments; a min/max range cannot express that
// discontinuous arity rule.
'mt_rand' => [
'args' => '?i_?i',
'argCounts' => [0, 2],
'minArgs' => 0,
'maxArgs' => 2,
'returnType' => Type::INT,
'intrinsic' => true,
],
'rand' => [
'args' => '?i_?i',
'argCounts' => [0, 2],
'minArgs' => 0,
'maxArgs' => 2,
'returnType' => Type::INT,
'intrinsic' => true,
],
'random_int' => [
'args' => 'i_i',
'minArgs' => 2,
'maxArgs' => 2,
'returnType' => Type::INT,
'intrinsic' => true,
],
'random_bytes' => [
'args' => 'i',
'minArgs' => 1,
'maxArgs' => 1,
'returnType' => Type::STR,
'intrinsic' => true,
],
'mt_getrandmax' => [
'args' => '',
'minArgs' => 0,
'maxArgs' => 0,
'returnType' => Type::INT,
'constantResult' => '2147483647L',
'intrinsic' => true,
],
'getrandmax' => [
'args' => '',
'minArgs' => 0,
'maxArgs' => 0,
'returnType' => Type::INT,
'constantResult' => '2147483647L',
'intrinsic' => true,
],
'strncmp' => ['constFold' => self::FOLD_CMP3],
'strncasecmp' => ['constFold' => self::FOLD_CMP3],
'explode' => [],
@ -404,6 +453,10 @@ trait FuncCallOptimizer
return false;
}
if (isset($config['constantResult'])) {
return $config['constantResult'];
}
if (!empty($config['variadic']) || ($refInfo['variadic'] ?? false)) {
return $this->genVariadicCall($target, $expr, $variadicType);
}

@ -0,0 +1,45 @@
--TEST--
random functions use direct wrappers and preserve PHP range semantics
--FILE--
<?php
function main(): void
{
$mt = mt_rand();
var_dump($mt >= 0 && $mt <= mt_getrandmax());
$mtRange = mt_rand(10, 20);
var_dump($mtRange >= 10 && $mtRange <= 20);
$randRange = rand(20, 10);
var_dump($randRange >= 10 && $randRange <= 20);
$secure = random_int(5, 5);
var_dump($secure);
var_dump(strlen(random_bytes(8)));
var_dump(mt_getrandmax());
var_dump(getrandmax());
try {
mt_rand(20, 10);
} catch (ValueError $error) {
echo $error->getMessage(), "\n";
}
try {
random_int(20, 10);
} catch (ValueError $error) {
echo $error->getMessage(), "\n";
}
}
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
int(5)
int(8)
int(2147483647)
int(2147483647)
mt_rand(): Argument #2 ($max) must be greater than or equal to argument #1 ($min)
random_int(): Argument #1 ($min) must be less than or equal to argument #2 ($max)
Loading…
Cancel
Save