diff --git a/phpunit/code/func-call-optimizer-typed-arguments.php b/phpunit/code/func-call-optimizer-typed-arguments.php new file mode 100644 index 00000000..86b89128 --- /dev/null +++ b/phpunit/code/func-call-optimizer-typed-arguments.php @@ -0,0 +1,18 @@ +addFiles([$source]); + $compiler->prepareFile($source); + $generated = $compiler->convertFile($source); + $code = file_get_contents($generated); + + self::assertIsString($code); + self::assertSame(2, substr_count($code, 'php::fn::in_array(')); + self::assertSame(1, substr_count($code, 'php::call(')); + self::assertMatchesRegularExpression('/php::toBool\(1L+\)/', $code); + self::assertStringContainsString('php_optimizertypedbool()', $code); + self::assertStringContainsString('php_optimizerdynamicbool()', $code); + self::assertStringNotContainsString('php::toBool(php_optimizerdynamicbool())', $code); + } +} diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index f78f2ff9..d6d9e35c 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -286,9 +286,14 @@ protected function dispatchFuncCall(string $name, Node\Expr\FuncCall $expr, arra $refInfo = $this->getArgReflectionInfo($name); $argTypeStr = $config['args'] ?? ($refInfo['args'] ?? ''); $defaults = $config['defaults'] ?? []; + $variadicType = $config['variadicType'] ?? ($refInfo['variadicType'] ?? ''); + $nullables = $refInfo['nullables'] ?? []; + + if (!$this->hasOptimizerSafeTypedArguments($expr, $argTypeStr, $variadicType)) { + return false; + } if (!empty($config['variadic']) || ($refInfo['variadic'] ?? false)) { - $variadicType = $config['variadicType'] ?? $refInfo['variadicType'] ?? ''; return $this->genVariadicCall($target, $expr, $variadicType); } @@ -299,11 +304,80 @@ protected function dispatchFuncCall(string $name, Node\Expr\FuncCall $expr, arra } } - $nullables = $refInfo['nullables'] ?? []; $args = $this->buildArgList($expr, $argTypeStr, $defaults, $nullables); return $target . '(' . implode(', ', $args) . ')'; } + protected function hasOptimizerSafeTypedArguments( + Node\Expr\FuncCall $expr, + string $argTypeStr, + string $variadicType + ): bool + { + // The optimized ABI conversions are safe for exact types and for the + // compiler's existing statically-known Native scalar conversions. A + // runtime-backed value would lose its zval type before Zend can apply + // strict parameter validation, so keep those calls on php::call(). + // Arrays have no exact argument-conversion helper and use the same + // fallback for every statically unproven value. + $types = $argTypeStr === '' ? [] : explode('_', $argTypeStr); + foreach ($expr->args as $index => $arg) { + // Custom handlers call this helper too. They cannot lower an + // unpacked list as a fixed C++ ABI argument sequence. + if ($arg->unpack) { + return false; + } + $type = $types[$index] ?? $variadicType; + $base = ($type[0] ?? '') === self::ARG_OPTIONAL ? substr($type, 1) : $type; + if (!in_array($base, [ + self::ARG_TYPE_STR, + self::ARG_TYPE_INT, + self::ARG_TYPE_FLOAT, + self::ARG_TYPE_BOOL, + self::ARG_TYPE_ARRAY, + ], true)) { + continue; + } + // Keep the established optimized-null policy. Several stdlib + // wrappers intentionally map literal null to their C++ default, + // including parameters that Reflection no longer marks nullable. + if ($this->isNull($arg->value)) { + continue; + } + $expected = match ($base) { + self::ARG_TYPE_STR => Type::STR, + self::ARG_TYPE_INT => Type::INT, + self::ARG_TYPE_FLOAT => Type::FLOAT, + self::ARG_TYPE_BOOL => Type::BOOL, + self::ARG_TYPE_ARRAY => Type::ARRAY, + }; + $actual = $this->detectTypeOfExpr($arg->value); + if ($actual === $expected) { + continue; + } + if ($this->isNativeType($expected) && $this->isNativeType($actual)) { + continue; + } + return false; + } + + return true; + } + + protected function hasOptimizerSafeReflectedArguments( + string $name, + Node\Expr\FuncCall $expr, + array $config + ): bool + { + $refInfo = $this->getArgReflectionInfo($name); + return $this->hasOptimizerSafeTypedArguments( + $expr, + $config['args'] ?? ($refInfo['args'] ?? ''), + $config['variadicType'] ?? ($refInfo['variadicType'] ?? ''), + ); + } + // ========================================================================= // Auto-detect argument types from PHP reflection // ========================================================================= @@ -768,6 +842,9 @@ protected function genGetParentClass(string $n, Node\Expr\FuncCall $e, array $c) protected function genArrayKeys(string $n, Node\Expr\FuncCall $e, array $c): string|false { + if (!$this->hasOptimizerSafeReflectedArguments($n, $e, $c)) { + return false; + } $cnt = count($e->args); if ($cnt >= 3) { if ($this->detectTypeOfExpr($e->args[2]->value) !== Type::BOOL) { @@ -782,8 +859,11 @@ protected function genArrayKeys(string $n, Node\Expr\FuncCall $e, array $c): str return 'php::fn::array_keys(' . $this->getArg($e, 0) . ')'; } - protected function genArrayKeyExists(string $n, Node\Expr\FuncCall $e, array $c): string + protected function genArrayKeyExists(string $n, Node\Expr\FuncCall $e, array $c): string|false { + if (!$this->hasOptimizerSafeReflectedArguments($n, $e, $c)) { + return false; + } // The C++ receiver is PHP's second argument, but PHP still evaluates // the key first. Resolve both in source order before rearranging them. $key = $this->getArg($e, 0); @@ -791,7 +871,7 @@ protected function genArrayKeyExists(string $n, Node\Expr\FuncCall $e, array $c) return $array . '.offsetExists(' . $key . ')'; } - protected function genRound(string $n, Node\Expr\FuncCall $e, array $c): string + protected function genRound(string $n, Node\Expr\FuncCall $e, array $c): string|false { $type = $this->detectTypeOfExpr($e->args[0]->value); if ($type === Type::DECIMAL) { @@ -801,6 +881,9 @@ protected function genRound(string $n, Node\Expr\FuncCall $e, array $c): string } return 'php::Decimal::round(' . $a0 . ')'; } + if (!$this->hasOptimizerSafeReflectedArguments($n, $e, $c)) { + return false; + } $args = count($e->args); if ($args >= 3) { return 'php::fn::round(' . $this->getArg($e, 0) . ', ' . $this->convertIntExpr($this->getArg($e, 1)) . ', ' . $this->convertIntExpr($this->getArg($e, 2)) . ')'; @@ -811,7 +894,7 @@ protected function genRound(string $n, Node\Expr\FuncCall $e, array $c): string return 'php::fn::round(' . $this->getArg($e, 0) . ')'; } - protected function genCount(string $n, Node\Expr\FuncCall $e, array $c): string + protected function genCount(string $n, Node\Expr\FuncCall $e, array $c): string|false { $receiver = $e->args[0] ?? null; $nativeClass = $receiver instanceof Node\Arg @@ -836,6 +919,10 @@ protected function genCount(string $n, Node\Expr\FuncCall $e, array $c): string )); } + if (!$this->hasOptimizerSafeReflectedArguments($n, $e, $c)) { + return false; + } + $folded = $this->doFoldCountLiteral($e); if ($folded !== false) return $folded; if (count($e->args) >= 2) { @@ -846,6 +933,9 @@ protected function genCount(string $n, Node\Expr\FuncCall $e, array $c): string protected function genDefine(string $n, Node\Expr\FuncCall $e, array $c): string|false { + if (!$this->hasOptimizerSafeReflectedArguments($n, $e, $c)) { + return false; + } $arg = $e->args[0]->value; if ($this->isScalarString($arg) && str_contains($arg->value, '::')) { $this->fatalError($e, 'Invalid define name `' . $arg->value . '`'); @@ -907,8 +997,11 @@ protected function genFuncNumArgs(string $name, Node\Expr\FuncCall $expr, array return (string) count($funcDef->argInfoList); } - protected function genFunctionExists(string $name, Node\Expr\FuncCall $expr, array $config): string + protected function genFunctionExists(string $name, Node\Expr\FuncCall $expr, array $config): string|false { + if (!$this->hasOptimizerSafeReflectedArguments($name, $expr, $config)) { + return false; + } $funcName = $expr->args[0]->value; if ($this->isScalarString($funcName)) { $nameLower = strtolower(trim($funcName->value, '\\')); diff --git a/tests/compiler/stdlib/strict-builtin-typed-arguments.phpt b/tests/compiler/stdlib/strict-builtin-typed-arguments.phpt new file mode 100644 index 00000000..b84d668e --- /dev/null +++ b/tests/compiler/stdlib/strict-builtin-typed-arguments.phpt @@ -0,0 +1,232 @@ +--TEST-- +Optimized builtins preserve strict typed parameter validation +--FILE-- + 1])); + var_dump(array_keys(mixedArray())); + var_dump(function_exists(mixedString())); + var_dump(round(...[1.25, 1])); + + $events = []; + var_dump(in_array( + orderedNeedle($events), + orderedHaystack($events), + orderedStrict($events) + )); + var_dump($events); + + try { + in_array('1', [1], mixedInt()); + echo "in-array-mixed-int=missing TypeError\n"; + } catch (TypeError $error) { + echo "in-array-mixed-int=TypeError\n"; + } + + try { + strlen(mixedInt()); + echo "strlen-mixed-int=missing TypeError\n"; + } catch (TypeError $error) { + echo "strlen-mixed-int=TypeError\n"; + } + + try { + array_fill(mixedString(), 1, 'x'); + echo "array-fill-mixed-string=missing TypeError\n"; + } catch (TypeError $error) { + echo "array-fill-mixed-string=TypeError\n"; + } + + try { + hypot(0.0, mixedString()); + echo "hypot-mixed-string=missing TypeError\n"; + } catch (TypeError $error) { + echo "hypot-mixed-string=TypeError\n"; + } + + try { + in_array('1', mixedInt(), true); + echo "in-array-mixed-haystack=missing TypeError\n"; + } catch (TypeError $error) { + echo "in-array-mixed-haystack=TypeError\n"; + } + + try { + is_callable('strlen', mixedInt()); + echo "is-callable-mixed-int=missing TypeError\n"; + } catch (TypeError $error) { + echo "is-callable-mixed-int=TypeError\n"; + } + + try { + array_merge(mixedInt()); + echo "array-merge-mixed-int=missing TypeError\n"; + } catch (TypeError $error) { + echo "array-merge-mixed-int=TypeError\n"; + } + + try { + array_search('1', [1], mixedArray()); + echo "array-search-mixed-array=missing TypeError\n"; + } catch (TypeError $error) { + echo "array-search-mixed-array=TypeError\n"; + } + + try { + in_array('1', [1], unionInt()); + echo "in-array-union-int=missing TypeError\n"; + } catch (TypeError $error) { + echo "in-array-union-int=TypeError\n"; + } + + try { + array_key_exists('key', mixedInt()); + echo "array-key-exists-mixed-int=missing TypeError\n"; + } catch (TypeError $error) { + echo "array-key-exists-mixed-int=TypeError\n"; + } + + try { + array_keys(mixedInt()); + echo "array-keys-mixed-int=missing TypeError\n"; + } catch (TypeError $error) { + echo "array-keys-mixed-int=TypeError\n"; + } + + try { + round(1.25, mixedString()); + echo "round-mixed-string=missing TypeError\n"; + } catch (TypeError $error) { + echo "round-mixed-string=TypeError\n"; + } + + try { + count([], mixedBool()); + echo "count-mixed-bool=missing TypeError\n"; + } catch (TypeError $error) { + echo "count-mixed-bool=TypeError\n"; + } + + try { + function_exists(mixedInt()); + echo "function-exists-mixed-int=missing TypeError\n"; + } catch (TypeError $error) { + echo "function-exists-mixed-int=TypeError\n"; + } + + try { + define(mixedInt(), 1); + echo "define-mixed-int=missing TypeError\n"; + } catch (TypeError $error) { + echo "define-mixed-int=TypeError\n"; + } +} +?> +--EXPECT-- +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +int(3) +int(1) +float(1) +float(5) +string(2) "ok" +NULL +int(3) +array(1) { + ["value"]=> + int(1) +} +array(0) { +} +bool(false) +float(1.3) +bool(false) +array(3) { + [0]=> + string(6) "needle" + [1]=> + string(8) "haystack" + [2]=> + string(6) "strict" +} +in-array-mixed-int=TypeError +strlen-mixed-int=TypeError +array-fill-mixed-string=TypeError +hypot-mixed-string=TypeError +in-array-mixed-haystack=TypeError +is-callable-mixed-int=TypeError +array-merge-mixed-int=TypeError +array-search-mixed-array=TypeError +in-array-union-int=TypeError +array-key-exists-mixed-int=TypeError +array-keys-mixed-int=TypeError +round-mixed-string=TypeError +count-mixed-bool=TypeError +function-exists-mixed-int=TypeError +define-mixed-int=TypeError