diff --git a/phpunit/code/func-call-optimizer-typed-arguments.php b/phpunit/code/func-call-optimizer-typed-arguments.php index 8805fe2a..e7adc1ec 100644 --- a/phpunit/code/func-call-optimizer-typed-arguments.php +++ b/phpunit/code/func-call-optimizer-typed-arguments.php @@ -28,4 +28,8 @@ function optimizerTypedArgumentCalls(): void in_array('1', [1], optimizerDynamicBool()); strlen(null); json_decode('null', null); + floor('1.5'); + round('1.25'); + floor(1.5); + round(1.25); } diff --git a/phpunit/src/ClosureTest.php b/phpunit/src/ClosureTest.php index da013322..5dd4c845 100644 --- a/phpunit/src/ClosureTest.php +++ b/phpunit/src/ClosureTest.php @@ -13,9 +13,12 @@ class ClosureTest extends \BaseTest $translator = $compiler; $compiler->addFiles([$testFile]); $compiler->prepareFile($testFile); - $compiler->convertFile($testFile); + $generated = $compiler->convertFile($testFile); + $code = file_get_contents($generated); - $this->assertTrue(true); + self::assertIsString($code); + self::assertSame(3, substr_count($code, 'php::newClosureWithParameters(')); + self::assertSame(3, substr_count($code, 'php::ClosureStrictTypes::Enabled')); } public function testClosureRebindingIsRejectedAtCompileTime(): void diff --git a/phpunit/src/FuncCallOptimizerTest.php b/phpunit/src/FuncCallOptimizerTest.php index 24d4915e..3c264419 100644 --- a/phpunit/src/FuncCallOptimizerTest.php +++ b/phpunit/src/FuncCallOptimizerTest.php @@ -20,7 +20,9 @@ final class FuncCallOptimizerTest extends BaseTest self::assertSame(1, substr_count($code, 'php::fn::in_array(')); self::assertSame(1, substr_count($code, 'php::fn::hypot(')); self::assertSame(1, substr_count($code, 'php::fn::json_decode(')); - self::assertSame(3, substr_count($code, 'php::call(')); + self::assertSame(1, substr_count($code, 'php::fn::floor(')); + self::assertSame(1, substr_count($code, 'php::fn::round(')); + self::assertSame(5, substr_count($code, 'php::call(')); self::assertStringContainsString('php_optimizertypedbool()', $code); self::assertStringContainsString('php_optimizertypedint()', $code); self::assertStringContainsString('php_optimizertypedfloat()', $code); @@ -31,5 +33,7 @@ final class FuncCallOptimizerTest extends BaseTest self::assertStringContainsString('php::fn::hypot(php::toFloat(', $code); self::assertStringContainsString('php::ArgList{php::null}', $code); self::assertMatchesRegularExpression('/php::fn::json_decode\([^;]+php::null\);/', $code); + self::assertStringContainsString('php::fn::floor(1.5)', $code); + self::assertStringContainsString('php::fn::round(1.25)', $code); } } diff --git a/src/Generator/ClosureGenerator.php b/src/Generator/ClosureGenerator.php index df24d589..04d83c41 100644 --- a/src/Generator/ClosureGenerator.php +++ b/src/Generator/ClosureGenerator.php @@ -49,7 +49,7 @@ trait ClosureGenerator . $this->escapeBool(!$param->variadic && $param->default === null) . '}'; } return 'php::newClosureWithParameters(' . $callback . ', ' . $uses . ', ' . $thisArg . ', ' . $scope - . ', { ' . implode(', ', $parameterDescriptors) . ' })'; + . ', { ' . implode(', ', $parameterDescriptors) . ' }, php::ClosureStrictTypes::Enabled)'; } protected function parseArrowFunction(Expr\ArrowFunction $expr): string diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index 37fe4a0d..43b148dd 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -127,31 +127,43 @@ trait FuncCallOptimizer 'defined' => ['constFold' => self::FOLD_KNOWN_CONSTANT], // Big* dispatch - 'abs' => ['bigDispatch' => [ - Type::BIGINT => 'php::BigInt::abs', - Type::BIGFLOAT => 'php::BigFloat::abs', - Type::DECIMAL => 'php::Decimal::abs', - 'fallback' => 'php::fn::abs', - ]], + 'abs' => [ + 'bigDispatch' => [ + Type::BIGINT => 'php::BigInt::abs', + Type::BIGFLOAT => 'php::BigFloat::abs', + Type::DECIMAL => 'php::Decimal::abs', + 'fallback' => 'php::fn::abs', + ], + 'fallbackArgTypes' => [[Type::INT, Type::FLOAT]], + ], 'pow' => ['bigDispatch' => [ Type::BIGINT => 'php::BigInt::pow', Type::DECIMAL => 'php::Decimal::pow', 'fallback' => 'php::fn::pow', ]], - 'sqrt' => ['bigDispatch' => [ - Type::BIGINT => 'php::BigInt::sqrt', - Type::DECIMAL => 'php::Decimal::sqrt', - Type::BIGFLOAT => 'php::BigFloat::sqrt', - 'fallback' => 'php::fn::sqrt', - ]], - 'floor' => ['bigDispatch' => [ - Type::DECIMAL => 'php::Decimal::floor', - 'fallback' => 'php::fn::floor', - ]], - 'ceil' => ['bigDispatch' => [ - Type::DECIMAL => 'php::Decimal::ceil', - 'fallback' => 'php::fn::ceil', - ]], + 'sqrt' => [ + 'bigDispatch' => [ + Type::BIGINT => 'php::BigInt::sqrt', + Type::DECIMAL => 'php::Decimal::sqrt', + Type::BIGFLOAT => 'php::BigFloat::sqrt', + 'fallback' => 'php::fn::sqrt', + ], + 'fallbackArgTypes' => [[Type::INT, Type::FLOAT]], + ], + 'floor' => [ + 'bigDispatch' => [ + Type::DECIMAL => 'php::Decimal::floor', + 'fallback' => 'php::fn::floor', + ], + 'fallbackArgTypes' => [[Type::INT, Type::FLOAT]], + ], + 'ceil' => [ + 'bigDispatch' => [ + Type::DECIMAL => 'php::Decimal::ceil', + 'fallback' => 'php::fn::ceil', + ], + 'fallbackArgTypes' => [[Type::INT, Type::FLOAT]], + ], // Type conversions 'strval' => ['conversion' => self::ARG_TYPE_STR], @@ -259,7 +271,11 @@ trait FuncCallOptimizer return $this->{$config['handler']}($name, $expr, $config); } if (isset($config['bigDispatch'])) { - return $this->dispatchBigType($expr, $config['bigDispatch']); + return $this->dispatchBigType( + $expr, + $config['bigDispatch'], + $config['fallbackArgTypes'] ?? [], + ); } if (isset($config['conversion'])) { return $this->dispatchConversion($expr, $config['conversion']); @@ -664,10 +680,25 @@ trait FuncCallOptimizer }; } - protected function dispatchBigType(Node\Expr\FuncCall $expr, array $dispatch): string|false + protected function dispatchBigType( + Node\Expr\FuncCall $expr, + array $dispatch, + array $fallbackArgTypes = [], + ): string|false { $type = $this->detectTypeOfExpr($expr->args[0]->value); - $target = $dispatch[$type] ?? $dispatch['fallback'] ?? null; + $target = $dispatch[$type] ?? null; + if ($target === null) { + foreach ($fallbackArgTypes as $index => $acceptedTypes) { + $arg = $expr->args[$index] ?? null; + if (!$arg instanceof Node\Arg + || !in_array($this->detectTypeOfExpr($arg->value), $acceptedTypes, true) + ) { + return false; + } + } + $target = $dispatch['fallback'] ?? null; + } if (!$target) { return false; } @@ -982,6 +1013,20 @@ trait FuncCallOptimizer } return 'php::Decimal::round(' . $a0 . ')'; } + // Reflection reports int|float as a union, which is represented by a + // raw Variant in the generic ABI metadata. The direct round() wrapper + // accepts that Variant and performs its own numeric conversion, so it + // is only strict-compatible when the source type is already proven. + if (!in_array($type, [Type::INT, Type::FLOAT], true)) { + return false; + } + // PHP 8.4+ also declares $mode as int|RoundingMode. The direct PHPX + // wrapper takes an integer; enum objects must remain on Zend dispatch. + if (count($e->args) >= 3 + && $this->detectTypeOfExpr($e->args[2]->value) !== Type::INT + ) { + return false; + } if (!$this->hasOptimizerSafeReflectedArguments($n, $e, $c)) { return false; } diff --git a/tests/compiler/closure/strict-builtin-fallback.phpt b/tests/compiler/closure/strict-builtin-fallback.phpt new file mode 100644 index 00000000..33d631a5 --- /dev/null +++ b/tests/compiler/closure/strict-builtin-fallback.phpt @@ -0,0 +1,39 @@ +--TEST-- +Closure calls preserve strict builtin argument validation on dynamic fallback +--FILE-- + static fn() => in_array('1', [1], mixedInt()), + 'closure' => static function (): float { + return sin('1'); + }, + 'round' => static fn() => round('1.25'), + 'floor' => static function (): float { + return floor('1.5'); + }, + ]; + + foreach ($calls as $name => $call) { + try { + $call(); + echo $name, "=missing TypeError\n"; + } catch (TypeError $error) { + echo $name, "=TypeError\n"; + } + } +} +?> +--EXPECT-- +arrow=TypeError +closure=TypeError +round=TypeError +floor=TypeError diff --git a/tests/compiler/stdlib/round_type_error.phpt b/tests/compiler/stdlib/round_type_error.phpt index 878eba93..0eb6a518 100644 --- a/tests/compiler/stdlib/round_type_error.phpt +++ b/tests/compiler/stdlib/round_type_error.phpt @@ -6,5 +6,5 @@ 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 +round(): Argument #1 ($num) must be of type int|float, string given float(4) diff --git a/tests/compiler/stdlib/strict-builtin-union-types.phpt b/tests/compiler/stdlib/strict-builtin-union-types.phpt new file mode 100644 index 00000000..004eada1 --- /dev/null +++ b/tests/compiler/stdlib/strict-builtin-union-types.phpt @@ -0,0 +1,57 @@ +--TEST-- +Optimized numeric builtins preserve strict union parameter validation +--FILE-- + +--EXPECT-- +int(3) +float(3) +float(1) +float(2) +float(1.3) +abs=TypeError +sqrt=TypeError +floor=TypeError +ceil=TypeError +round=TypeError