test(stdlib): 添加标准库函数边缘情况和错误处理测试

- 添加 abs 函数边缘情况测试,包括 PHP_INT_MIN 和 -0.0
- 添加 array_fill 负数计数参数的 ValueError 测试
- 添加 base_convert 无效进制参数的 ValueError 测试
- 添加 bindec/hexdec/octdec 空输入和无效字符测试
- 添加 chr/ord 函数边界值测试
- 添加 count 函数非数组/对象参数的 TypeError 测试
- 添加 explode 空分隔符和限制参数测试
- 添加 intdiv 除零和溢出错误测试
- 添加 log 函数零、负数和无效底数测试
- 添加 reset/end 非数组参数的 TypeError 测试
- 添加 round 非数值参数的 TypeError 测试
- 添加 str_repeat 负数次数的 ValueError 测试
- 添加 strtotime 时间戳 0 和无效日期测试
- 添加 substr 负偏移和长度边界测试
- 添加 version_compare 操作符参数测试
- 移除未使用的 php_std_url.h 头文件和源文件引用
- 更新函数优化配置以改进数学函数和字符串函数支持
- 添加 is_callable 函数处理程序和 max/min 函数的原始实现
- 为 abs、pow、sqrt 函数添加大数类型回退实现
pull/1/head
韩天峰 2 months ago
parent 8450e69e1c
commit abed2d8d22
  1. 1
      src/Php/CompilerBase.php
  2. 80
      src/Php/Optimizer/FuncCallOptimizer.php
  3. 1
      src/Php/Translator.php
  4. 16
      tests/aot/stdlib/abs_edge.phpt
  5. 20
      tests/aot/stdlib/array_fill_edge.phpt
  6. 14
      tests/aot/stdlib/base_convert_edge.phpt
  7. 20
      tests/aot/stdlib/bindec_edge.phpt
  8. BIN
      tests/aot/stdlib/chr_ord_edge.phpt
  9. 14
      tests/aot/stdlib/count_type_error.phpt
  10. 25
      tests/aot/stdlib/explode_edge.phpt
  11. 14
      tests/aot/stdlib/intdiv_edge.phpt
  12. 18
      tests/aot/stdlib/log_edge.phpt
  13. 17
      tests/aot/stdlib/reset_end_type_error.phpt
  14. 10
      tests/aot/stdlib/round_type_error.phpt
  15. 16
      tests/aot/stdlib/str_repeat_edge.phpt
  16. 16
      tests/aot/stdlib/strtotime_epoch.phpt
  17. 18
      tests/aot/stdlib/substr_edge.phpt
  18. 20
      tests/aot/stdlib/version_compare_operator.phpt

@ -217,7 +217,6 @@ class CompilerBase extends \PhpAot\Core\Translator
'phpx_decimal.h',
'php_aot_helper.h',
'php_std_core.h',
'php_std_url.h',
'php_std_string.h',
'php_std_misc.h',
'php_std_array.h',

@ -57,30 +57,38 @@ trait FuncCallOptimizer
private function buildFuncCallConfig(): array
{
$simple = [
'urlencode', 'urldecode', 'rawurlencode', 'rawurldecode',
'base64_encode', 'method_exists', 'property_exists',
'implode', 'str_replace', 'array_column', 'array_reverse',
'array_sum', 'array_product', 'array_key_first', 'array_key_last',
'array_combine', 'array_flip', 'array_intersect', 'array_values',
'method_exists', 'property_exists',
'number_format',
'implode',
'array_key_first', 'array_key_last',
'array_values',
'version_compare', 'gettype',
'is_array', 'is_string', 'is_object', 'is_resource',
'is_scalar', 'is_numeric', 'is_callable', 'is_countable', 'is_iterable',
'array_is_list', 'is_dir', 'is_file', 'realpath', 'time',
'parse_url', 'base64_decode',
'in_array', 'array_search', 'array_unique', 'array_filter', 'array_reduce',
'date', 'strtotime', 'md5', 'print_r',
'strstr', 'strripos', 'strrpos', 'is_a', 'is_subclass_of',
'sort', 'rsort', 'asort', 'arsort', 'ksort',
'array_pop', 'array_shift', 'reset', 'end',
'microtime', 'hrtime', 'uniqid',
'is_scalar', 'is_numeric', 'is_countable', 'is_iterable',
'array_is_list', 'is_dir', 'is_file', 'file_exists', 'realpath', 'time',
'in_array', 'array_search',
'date', 'strtotime', 'md5', 'sha1', 'hash', 'print_r',
'json_encode', 'json_decode', 'serialize', 'unserialize',
'random_int', 'random_bytes', 'mt_rand', 'rand',
'strstr', 'strrpos', 'is_a', 'is_subclass_of',
'reset', 'end',
'uniqid',
'dirname', 'basename',
// Math: trig, hyperbolic, exp/log, misc
'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2',
'sinh', 'cosh', 'tanh', 'asinh', 'acosh', 'atanh',
'pi', 'exp', 'expm1', 'log', 'log10', 'log1p',
'hypot', 'deg2rad', 'rad2deg', 'fmod', 'fdiv', 'fpow', 'intdiv',
// Math: is_* checks
'is_finite', 'is_infinite', 'is_nan',
// Math: base conversion
'decbin', 'decoct', 'dechex', 'bindec', 'hexdec', 'octdec', 'base_convert',
];
$extra = [
// Aliases (PHP function name → C++ target name)
'join' => 'implode',
'str_ireplace' => 'str_replace',
'stristr' => 'strstr',
'stristr' => 'stristr',
'strlen' => ['constFold' => self::FOLD_STRING_LEN],
'ord' => [],
@ -95,28 +103,18 @@ trait FuncCallOptimizer
'str_starts_with' => [],
'str_ends_with' => [],
'str_contains' => [],
'strtr' => [],
'strncmp' => ['constFold' => self::FOLD_CMP3],
'strncasecmp' => ['constFold' => self::FOLD_CMP3],
'htmlspecialchars' => [],
'htmlentities' => [],
'htmlspecialchars_decode' => [],
'html_entity_decode' => [],
'strip_tags' => [],
'explode' => [],
'strpos' => [],
'stripos' => [],
'substr' => [],
'str_repeat' => [],
'str_pad' => [],
'array_slice' => [],
'array_chunk' => [],
'array_fill' => [],
// Variadic
'array_diff' => ['variadic' => true],
'array_merge' => ['variadic' => true],
'array_merge_recursive' => ['variadic' => true],
// Compile-time fold with defaults
'class_exists' => ['constFold' => self::FOLD_KNOWN_CLASS, 'defaults' => [1 => 'true']],
@ -130,15 +128,18 @@ trait FuncCallOptimizer
self::TYPE_BIGINT => 'php::BigInt::abs',
self::TYPE_BIGFLOAT => 'php::BigFloat::abs',
self::TYPE_DECIMAL => 'php::Decimal::abs',
'fallback' => 'php::std::abs',
]],
'pow' => ['bigDispatch' => [
self::TYPE_BIGINT => 'php::BigInt::pow',
self::TYPE_DECIMAL => 'php::Decimal::pow',
'fallback' => 'php::std::pow',
]],
'sqrt' => ['bigDispatch' => [
self::TYPE_BIGINT => 'php::BigInt::sqrt',
self::TYPE_DECIMAL => 'php::Decimal::sqrt',
self::TYPE_BIGFLOAT => 'php::BigFloat::sqrt',
'fallback' => 'php::std::sqrt',
]],
'floor' => ['bigDispatch' => [
self::TYPE_DECIMAL => 'php::Decimal::floor',
@ -169,13 +170,13 @@ trait FuncCallOptimizer
'func_get_args' => ['handler' => 'genFuncGetArgsOptimized'],
'func_num_args' => ['handler' => 'genFuncNumArgsOptimized'],
'compact' => ['handler' => 'genCompactOptimized'],
'max' => ['handler' => 'genMaxMin'],
'min' => ['handler' => 'genMaxMin'],
'array_keys' => ['handler' => 'genArrayKeys'],
'array_key_exists' => ['handler' => 'genArrayKeyExists'],
'round' => ['handler' => 'genRound'],
'count' => ['handler' => 'genCount'],
'define' => ['handler' => 'genDefine'],
'is_callable' => ['handler' => 'genIsCallable'],
];
$config = $extra;
@ -538,6 +539,14 @@ trait FuncCallOptimizer
return $this->parseIdentifier($e->args[0]->value) . '.isNull()';
}
private function genIsCallable(string $n, Node\Expr\FuncCall $e, array $c): string|false
{
if (count($e->args) >= 3) {
return false;
}
return $this->dispatchFuncCall('is_callable', $e, ['target' => 'php::std::is_callable']);
}
private function genGetClassOptimized(string $n, Node\Expr\FuncCall $e, array $c): string
{
$obj = $e->args[0]->value;
@ -589,19 +598,6 @@ trait FuncCallOptimizer
return $this->genCompactOrig($e);
}
private function genMaxMin(string $n, Node\Expr\FuncCall $e, array $c): string
{
$target = 'php::std::' . $n;
if (count($e->args) == 1) {
return $target . '(' . $this->getArg($e, 0) . ')';
}
$a = [];
foreach ($e->args as $arg) {
$a[] = $this->parseExpr($arg->value);
}
return $target . '(php::Array{' . implode(', ', $a) . '})';
}
private function genArrayKeys(string $n, Node\Expr\FuncCall $e, array $c): string
{
$cnt = count($e->args);

@ -974,7 +974,6 @@ CODE;
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/php_std_core.cpp';
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/php_std_array.cpp';
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/php_std_string.cpp';
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/php_std_url.cpp';
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/php_std_datetime.cpp';
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/php_std_fs.cpp';
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/php_std_math.cpp';

@ -0,0 +1,16 @@
--TEST--
abs edge cases: PHP_INT_MIN and -0.0
--FILE--
<?php
var_dump(abs(PHP_INT_MIN));
var_dump(abs(-0.0));
var_dump(abs(0));
var_dump(abs(-5));
var_dump(abs(3.14));
?>
--EXPECT--
float(9.223372036854776E+18)
float(0)
int(0)
int(5)
float(3.14)

@ -0,0 +1,20 @@
--TEST--
array_fill edge cases: negative count
--FILE--
<?php
try { var_dump(array_fill(0, -1, "x")); } catch (ValueError $e) { echo get_class($e) . ": " . $e->getMessage() . "\n"; }
var_dump(array_fill(0, 0, "x"));
var_dump(array_fill(5, 3, "banana"));
?>
--EXPECT--
ValueError: array_fill(): Argument #2 ($count) must be greater than or equal to 0
array(0) {
}
array(3) {
[5]=>
string(6) "banana"
[6]=>
string(6) "banana"
[7]=>
string(6) "banana"
}

@ -0,0 +1,14 @@
--TEST--
base_convert edge cases: invalid bases
--FILE--
<?php
try { var_dump(base_convert("10", 1, 10)); } catch (ValueError $e) { echo get_class($e) . ": " . $e->getMessage() . "\n"; }
try { var_dump(base_convert("10", 10, 37)); } catch (ValueError $e) { echo get_class($e) . ": " . $e->getMessage() . "\n"; }
var_dump(base_convert("10", 2, 10));
var_dump(base_convert("FF", 16, 10));
?>
--EXPECT--
ValueError: base_convert(): Argument #2 ($from_base) must be between 2 and 36 (inclusive)
ValueError: base_convert(): Argument #3 ($to_base) must be between 2 and 36 (inclusive)
string(1) "2"
string(3) "255"

@ -0,0 +1,20 @@
--TEST--
bindec/hexdec/octdec edge cases: invalid input
--FILE--
<?php
var_dump(bindec(""));
var_dump(hexdec(""));
var_dump(octdec(""));
var_dump(bindec("2"));
var_dump(hexdec("G"));
var_dump(bindec("1010"));
var_dump(hexdec("A"));
?>
--EXPECT--
int(0)
int(0)
int(0)
int(0)
int(0)
int(10)
int(10)

@ -0,0 +1,14 @@
--TEST--
count: TypeError for non-array/non-object
--FILE--
<?php
try { count("hello"); } catch (TypeError $e) { echo $e->getMessage() . "\n"; }
try { count(42); } catch (TypeError $e) { echo $e->getMessage() . "\n"; }
try { count(null); } catch (TypeError $e) { echo $e->getMessage() . "\n"; }
var_dump(count([1,2,3]));
?>
--EXPECT--
count(): Argument #1 ($value) must be of type Countable|array
count(): Argument #1 ($value) must be of type Countable|array
count(): Argument #1 ($value) must be of type Countable|array
int(3)

@ -0,0 +1,25 @@
--TEST--
explode edge cases: empty delimiter and limit
--FILE--
<?php
try { var_dump(explode("", "test")); } catch (ValueError $e) { echo get_class($e) . ": " . $e->getMessage() . "\n"; }
var_dump(explode(",", ""));
var_dump(explode(",", "a,b,c", 0));
var_dump(explode(",", "a,b,c", -1));
?>
--EXPECT--
ValueError: explode(): Argument #1 ($separator) must not be empty
array(1) {
[0]=>
string(0) ""
}
array(1) {
[0]=>
string(5) "a,b,c"
}
array(2) {
[0]=>
string(1) "a"
[1]=>
string(1) "b"
}

@ -0,0 +1,14 @@
--TEST--
intdiv edge cases: division by zero and PHP_INT_MIN/-1
--FILE--
<?php
try { var_dump(intdiv(10, 0)); } catch (DivisionByZeroError $e) { echo get_class($e) . ": " . $e->getMessage() . "\n"; }
try { var_dump(intdiv(PHP_INT_MIN, -1)); } catch (ArithmeticError $e) { echo get_class($e) . ": " . $e->getMessage() . "\n"; }
var_dump(intdiv(10, 3));
var_dump(intdiv(-10, 3));
?>
--EXPECT--
DivisionByZeroError: Division by zero
ArithmeticError: Division of PHP_INT_MIN by -1 is not an integer
int(3)
int(-3)

@ -0,0 +1,18 @@
--TEST--
log edge cases: zero, negative, and base validation
--FILE--
<?php
var_dump(log(0));
var_dump(log(-1));
try { var_dump(log(1, 0)); } catch (ValueError $e) { echo get_class($e) . ": " . $e->getMessage() . "\n"; }
var_dump(log(1, 1));
var_dump(log(8, 2));
var_dump(log(100, 10));
?>
--EXPECT--
float(-INF)
float(NAN)
ValueError: log(): Argument #2 ($base) must be greater than 0
float(NAN)
float(3)
float(2)

@ -0,0 +1,17 @@
--TEST--
reset/end: TypeError for non-array
--FILE--
<?php
$str = "hello";
try { reset($str); } catch (TypeError $e) { echo $e->getMessage() . "\n"; }
$int = 42;
try { end($int); } catch (TypeError $e) { echo $e->getMessage() . "\n"; }
$arr = ["a" => 1, "b" => 2];
var_dump(reset($arr));
var_dump(end($arr));
?>
--EXPECT--
reset(): Argument #1 ($array) must be of type array
end(): Argument #1 ($array) must be of type array
int(1)
int(2)

@ -0,0 +1,10 @@
--TEST--
round: TypeError for non-numeric
--FILE--
<?php
try { round("hello"); } catch (TypeError $e) { echo $e->getMessage() . "\n"; }
var_dump(round(3.7));
?>
--EXPECT--
round(): Argument #1 ($num) must be of type int|float
float(4)

@ -0,0 +1,16 @@
--TEST--
str_repeat edge cases: negative times and zero
--FILE--
<?php
try { str_repeat("x", -1); } catch (ValueError $e) { echo get_class($e) . ": " . $e->getMessage() . "\n"; }
var_dump(str_repeat("x", 0));
var_dump(str_repeat("x", 3));
var_dump(str_repeat("", 5));
var_dump(str_repeat("ab", 2));
?>
--EXPECT--
ValueError: str_repeat(): Argument #2 ($times) must be greater than or equal to 0
string(0) ""
string(3) "xxx"
string(0) ""
string(4) "abab"

@ -0,0 +1,16 @@
--TEST--
strtotime: epoch 0 and invalid date
--FILE--
<?php
var_dump(strtotime("@0"));
var_dump(strtotime("1970-01-01 00:00:00 UTC"));
var_dump(strtotime("1970-01-02 00:00:00 UTC"));
var_dump(strtotime("2000-01-01 00:00:00 UTC"));
var_dump(strtotime("invalid-date-string"));
?>
--EXPECT--
int(0)
int(0)
int(86400)
int(946684800)
bool(false)

@ -0,0 +1,18 @@
--TEST--
substr edge cases: negative offset and length
--FILE--
<?php
var_dump(substr("hello", -1));
var_dump(substr("hello", -10));
var_dump(substr("hello", 0, -1));
var_dump(substr("hello", 0, -10));
var_dump(substr("hello", 10));
var_dump(substr("hello", 0, 100));
?>
--EXPECT--
string(1) "o"
string(5) "hello"
string(4) "hell"
string(0) ""
string(0) ""
string(5) "hello"

@ -0,0 +1,20 @@
--TEST--
version_compare with operator parameter
--FILE--
<?php
var_dump(version_compare("1.2.3", "1.2.3"));
var_dump(version_compare("1.2.3", "1.2.4"));
var_dump(version_compare("1.2.3", "1.2.4", "<"));
var_dump(version_compare("1.2.3", "1.2.4", "<="));
var_dump(version_compare("1.2.3", "1.2.3", "=="));
var_dump(version_compare("1.2.3", "1.2.3", "!="));
try { version_compare("1.0", "2.0", "invalid"); } catch (ValueError $e) { echo $e->getMessage() . "\n"; }
?>
--EXPECT--
int(0)
int(-1)
bool(true)
bool(true)
bool(true)
bool(false)
version_compare(): Argument #3 ($operator) must be a valid comparison operator
Loading…
Cancel
Save