From 7dee6d0ac3e34d4f614d057820b786c81ffeda35 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sun, 6 Sep 2026 16:37:51 +0800 Subject: [PATCH] feat(stdlib): optimize random function calls --- phpunit/code/random-direct-calls.php | 15 ++++ .../code/random-invalid-getrandmax-arity.php | 6 ++ phpunit/code/random-invalid-mt-rand-arity.php | 6 ++ phpunit/code/random-invalid-rand-arity.php | 6 ++ .../random-invalid-random-bytes-arity.php | 6 ++ .../code/random-invalid-random-int-arity.php | 6 ++ phpunit/src/RandomOptimizerTest.php | 70 +++++++++++++++++++ src/CompilerBase.php | 19 +++-- src/Optimizer/FuncCallOptimizer.php | 55 ++++++++++++++- tests/compiler/stdlib/random-direct.phpt | 45 ++++++++++++ 10 files changed, 229 insertions(+), 5 deletions(-) create mode 100644 phpunit/code/random-direct-calls.php create mode 100644 phpunit/code/random-invalid-getrandmax-arity.php create mode 100644 phpunit/code/random-invalid-mt-rand-arity.php create mode 100644 phpunit/code/random-invalid-rand-arity.php create mode 100644 phpunit/code/random-invalid-random-bytes-arity.php create mode 100644 phpunit/code/random-invalid-random-int-arity.php create mode 100644 phpunit/src/RandomOptimizerTest.php create mode 100644 tests/compiler/stdlib/random-direct.phpt diff --git a/phpunit/code/random-direct-calls.php b/phpunit/code/random-direct-calls.php new file mode 100644 index 00000000..ea0382c0 --- /dev/null +++ b/phpunit/code/random-direct-calls.php @@ -0,0 +1,15 @@ +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; + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index ee47090e..ad493ce4 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -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"); } diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index 4e49b066..bf0ebd44 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -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); } diff --git a/tests/compiler/stdlib/random-direct.phpt b/tests/compiler/stdlib/random-direct.phpt new file mode 100644 index 00000000..567278a9 --- /dev/null +++ b/tests/compiler/stdlib/random-direct.phpt @@ -0,0 +1,45 @@ +--TEST-- +random functions use direct wrappers and preserve PHP range semantics +--FILE-- += 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)