fix(optimizer): preserve strict builtin validation

debug-ext-lib
韩天峰 4 hours ago
parent 9ad213ea64
commit 312a796498
  1. 4
      phpunit/code/func-call-optimizer-typed-arguments.php
  2. 7
      phpunit/src/ClosureTest.php
  3. 6
      phpunit/src/FuncCallOptimizerTest.php
  4. 2
      src/Generator/ClosureGenerator.php
  5. 91
      src/Optimizer/FuncCallOptimizer.php
  6. 39
      tests/compiler/closure/strict-builtin-fallback.phpt
  7. 2
      tests/compiler/stdlib/round_type_error.phpt
  8. 57
      tests/compiler/stdlib/strict-builtin-union-types.phpt

@ -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);
}

@ -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

@ -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);
}
}

@ -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

@ -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;
}

@ -0,0 +1,39 @@
--TEST--
Closure calls preserve strict builtin argument validation on dynamic fallback
--FILE--
<?php
declare(strict_types=1);
function mixedInt(): mixed
{
return 1;
}
function main(): void
{
$calls = [
'arrow' => 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

@ -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)

@ -0,0 +1,57 @@
--TEST--
Optimized numeric builtins preserve strict union parameter validation
--FILE--
<?php
declare(strict_types=1);
function main(): void
{
var_dump(abs(-3));
var_dump(sqrt(9));
var_dump(floor(1.75));
var_dump(ceil(1.25));
var_dump(round(1.25, 1));
try {
abs('3');
echo "abs=missing TypeError\n";
} catch (TypeError $error) {
echo "abs=TypeError\n";
}
try {
sqrt('9');
echo "sqrt=missing TypeError\n";
} catch (TypeError $error) {
echo "sqrt=TypeError\n";
}
try {
floor('1.75');
echo "floor=missing TypeError\n";
} catch (TypeError $error) {
echo "floor=TypeError\n";
}
try {
ceil('1.25');
echo "ceil=missing TypeError\n";
} catch (TypeError $error) {
echo "ceil=TypeError\n";
}
try {
round('1.25');
echo "round=missing TypeError\n";
} catch (TypeError $error) {
echo "round=TypeError\n";
}
}
?>
--EXPECT--
int(3)
float(3)
float(1)
float(2)
float(1.3)
abs=TypeError
sqrt=TypeError
floor=TypeError
ceil=TypeError
round=TypeError
Loading…
Cancel
Save