diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index a70ba7f9..55061c2d 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -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', diff --git a/src/Php/Optimizer/FuncCallOptimizer.php b/src/Php/Optimizer/FuncCallOptimizer.php index 7d0dfb37..4c390238 100644 --- a/src/Php/Optimizer/FuncCallOptimizer.php +++ b/src/Php/Optimizer/FuncCallOptimizer.php @@ -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); diff --git a/src/Php/Translator.php b/src/Php/Translator.php index b26a2c2a..82c5c432 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -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'; diff --git a/tests/aot/stdlib/abs_edge.phpt b/tests/aot/stdlib/abs_edge.phpt new file mode 100644 index 00000000..1d856287 --- /dev/null +++ b/tests/aot/stdlib/abs_edge.phpt @@ -0,0 +1,16 @@ +--TEST-- +abs edge cases: PHP_INT_MIN and -0.0 +--FILE-- + +--EXPECT-- +float(9.223372036854776E+18) +float(0) +int(0) +int(5) +float(3.14) diff --git a/tests/aot/stdlib/array_fill_edge.phpt b/tests/aot/stdlib/array_fill_edge.phpt new file mode 100644 index 00000000..2211ebc5 --- /dev/null +++ b/tests/aot/stdlib/array_fill_edge.phpt @@ -0,0 +1,20 @@ +--TEST-- +array_fill edge cases: negative count +--FILE-- +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" +} diff --git a/tests/aot/stdlib/base_convert_edge.phpt b/tests/aot/stdlib/base_convert_edge.phpt new file mode 100644 index 00000000..58b02311 --- /dev/null +++ b/tests/aot/stdlib/base_convert_edge.phpt @@ -0,0 +1,14 @@ +--TEST-- +base_convert edge cases: invalid bases +--FILE-- +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" diff --git a/tests/aot/stdlib/bindec_edge.phpt b/tests/aot/stdlib/bindec_edge.phpt new file mode 100644 index 00000000..f3bb050b --- /dev/null +++ b/tests/aot/stdlib/bindec_edge.phpt @@ -0,0 +1,20 @@ +--TEST-- +bindec/hexdec/octdec edge cases: invalid input +--FILE-- + +--EXPECT-- +int(0) +int(0) +int(0) +int(0) +int(0) +int(10) +int(10) diff --git a/tests/aot/stdlib/chr_ord_edge.phpt b/tests/aot/stdlib/chr_ord_edge.phpt new file mode 100644 index 00000000..8129190f Binary files /dev/null and b/tests/aot/stdlib/chr_ord_edge.phpt differ diff --git a/tests/aot/stdlib/count_type_error.phpt b/tests/aot/stdlib/count_type_error.phpt new file mode 100644 index 00000000..1d79a677 --- /dev/null +++ b/tests/aot/stdlib/count_type_error.phpt @@ -0,0 +1,14 @@ +--TEST-- +count: TypeError for non-array/non-object +--FILE-- +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) diff --git a/tests/aot/stdlib/explode_edge.phpt b/tests/aot/stdlib/explode_edge.phpt new file mode 100644 index 00000000..31779b74 --- /dev/null +++ b/tests/aot/stdlib/explode_edge.phpt @@ -0,0 +1,25 @@ +--TEST-- +explode edge cases: empty delimiter and limit +--FILE-- +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" +} diff --git a/tests/aot/stdlib/intdiv_edge.phpt b/tests/aot/stdlib/intdiv_edge.phpt new file mode 100644 index 00000000..94190a97 --- /dev/null +++ b/tests/aot/stdlib/intdiv_edge.phpt @@ -0,0 +1,14 @@ +--TEST-- +intdiv edge cases: division by zero and PHP_INT_MIN/-1 +--FILE-- +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) diff --git a/tests/aot/stdlib/log_edge.phpt b/tests/aot/stdlib/log_edge.phpt new file mode 100644 index 00000000..dcd696b7 --- /dev/null +++ b/tests/aot/stdlib/log_edge.phpt @@ -0,0 +1,18 @@ +--TEST-- +log edge cases: zero, negative, and base validation +--FILE-- +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) diff --git a/tests/aot/stdlib/reset_end_type_error.phpt b/tests/aot/stdlib/reset_end_type_error.phpt new file mode 100644 index 00000000..70591ddf --- /dev/null +++ b/tests/aot/stdlib/reset_end_type_error.phpt @@ -0,0 +1,17 @@ +--TEST-- +reset/end: TypeError for non-array +--FILE-- +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) diff --git a/tests/aot/stdlib/round_type_error.phpt b/tests/aot/stdlib/round_type_error.phpt new file mode 100644 index 00000000..878eba93 --- /dev/null +++ b/tests/aot/stdlib/round_type_error.phpt @@ -0,0 +1,10 @@ +--TEST-- +round: TypeError for non-numeric +--FILE-- +getMessage() . "\n"; } +var_dump(round(3.7)); +?> +--EXPECT-- +round(): Argument #1 ($num) must be of type int|float +float(4) diff --git a/tests/aot/stdlib/str_repeat_edge.phpt b/tests/aot/stdlib/str_repeat_edge.phpt new file mode 100644 index 00000000..3fa8299d --- /dev/null +++ b/tests/aot/stdlib/str_repeat_edge.phpt @@ -0,0 +1,16 @@ +--TEST-- +str_repeat edge cases: negative times and zero +--FILE-- +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" diff --git a/tests/aot/stdlib/strtotime_epoch.phpt b/tests/aot/stdlib/strtotime_epoch.phpt new file mode 100644 index 00000000..9bd2509d --- /dev/null +++ b/tests/aot/stdlib/strtotime_epoch.phpt @@ -0,0 +1,16 @@ +--TEST-- +strtotime: epoch 0 and invalid date +--FILE-- + +--EXPECT-- +int(0) +int(0) +int(86400) +int(946684800) +bool(false) diff --git a/tests/aot/stdlib/substr_edge.phpt b/tests/aot/stdlib/substr_edge.phpt new file mode 100644 index 00000000..5759bc6b --- /dev/null +++ b/tests/aot/stdlib/substr_edge.phpt @@ -0,0 +1,18 @@ +--TEST-- +substr edge cases: negative offset and length +--FILE-- + +--EXPECT-- +string(1) "o" +string(5) "hello" +string(4) "hell" +string(0) "" +string(0) "" +string(5) "hello" diff --git a/tests/aot/stdlib/version_compare_operator.phpt b/tests/aot/stdlib/version_compare_operator.phpt new file mode 100644 index 00000000..b3157110 --- /dev/null +++ b/tests/aot/stdlib/version_compare_operator.phpt @@ -0,0 +1,20 @@ +--TEST-- +version_compare with operator parameter +--FILE-- +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