feat(stdlib): lower ctype calls without extension

master
韩天峰 16 hours ago
parent b184e47e5e
commit 13576008d0
  1. 16
      phpunit/code/ctype-direct-calls.php
  2. 39
      phpunit/src/CtypeOptimizerTest.php
  3. 4
      src/CompilerBase.php
  4. 76
      src/Optimizer/FuncCallOptimizer.php
  5. 5
      src/Translator.php
  6. 67
      tests/compiler/stdlib/ctype-direct.phpt

@ -0,0 +1,16 @@
<?php
function ctypeDirectCalls(mixed $char): bool
{
$result = ctype_alnum($char);
$result = ctype_alpha($char) && $result;
$result = ctype_cntrl($char) && $result;
$result = ctype_digit($char) && $result;
$result = ctype_lower($char) && $result;
$result = ctype_graph($char) && $result;
$result = ctype_print($char) && $result;
$result = ctype_punct($char) && $result;
$result = ctype_space($char) && $result;
$result = ctype_upper($char) && $result;
return ctype_xdigit(text: $char) && $result;
}

@ -0,0 +1,39 @@
<?php
use TypePhp\CompilerTest;
final class CtypeOptimizerTest extends BaseTest
{
public function testCtypeCallsUseDirectPhpxWrappers(): void
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/ctype-direct-calls.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$generated = $compiler->convertFile($source);
$code = file_get_contents($generated);
self::assertIsString($code);
foreach ([
'ctype_alnum',
'ctype_alpha',
'ctype_cntrl',
'ctype_digit',
'ctype_lower',
'ctype_graph',
'ctype_print',
'ctype_punct',
'ctype_space',
'ctype_upper',
'ctype_xdigit',
] as $function) {
self::assertSame(1, substr_count($code, 'php::fn::' . $function . '('), $function);
}
self::assertStringNotContainsString('get_persistent_func', $code);
self::assertStringNotContainsString('php::call(', $code);
self::assertStringContainsString('result = php::fn::ctype_alnum(_php__var__char);', $code);
}
}

@ -4704,6 +4704,10 @@ class CompilerBase implements PropertyAccessContext
protected function detectFuncCallReturnType(string $name): string
{
$name = ltrim($name, '\\');
$config = $this->getFuncCallConfig()[$name] ?? null;
if (is_array($config) && isset($config['returnType'])) {
return $config['returnType'];
}
$returnType = Reflection::getFunctionReturnType($name);
if ($returnType !== null) {
return $this->getTypeFromZendType($returnType);

@ -38,6 +38,20 @@ trait FuncCallOptimizer
protected const int FOLD_KNOWN_CONSTANT = 7;
protected const int FOLD_SSA_TYPE = 8;
protected const array CTYPE_FUNCTIONS = [
'ctype_alnum',
'ctype_alpha',
'ctype_cntrl',
'ctype_digit',
'ctype_lower',
'ctype_graph',
'ctype_print',
'ctype_punct',
'ctype_space',
'ctype_upper',
'ctype_xdigit',
];
/** @var array<string,string|array>|null */
protected ?array $_funcCallConfig = null;
@ -196,6 +210,19 @@ trait FuncCallOptimizer
'is_callable' => ['handler' => 'genIsCallable'],
];
// PHPX implements these directly with <cctype>. They remain
// available even when the target libphp has no ext/ctype.
foreach (self::CTYPE_FUNCTIONS as $name) {
$extra[$name] = [
'args' => 'v',
'minArgs' => 1,
'maxArgs' => 1,
'namedArgs' => ['text'],
'returnType' => Type::BOOL,
'intrinsic' => true,
];
}
$config = $extra;
foreach ($simple as $name) {
if (!isset($config[$name])) {
@ -211,6 +238,11 @@ trait FuncCallOptimizer
protected function parseFuncCallWithOptimizer(string $name, Node\Expr\FuncCall $expr): string|false
{
$config = $this->getFuncCallConfig()[$name] ?? null;
if ($config === null) {
return false;
}
foreach ($expr->args as $arg) {
if ($this->isPlaceholderExpr($arg)) {
return false;
@ -219,16 +251,23 @@ trait FuncCallOptimizer
// with the syntactic argument list. Named arguments and unpacking
// require Zend's runtime binding/expansion semantics, so reject
// them before any optimizer-specific handler can consume them.
if ($arg instanceof Node\Arg && ($arg->name !== null || $arg->unpack)) {
return false;
if ($arg instanceof Node\Arg) {
if ($arg->unpack) {
return false;
}
if ($arg->name !== null) {
$namedArgs = $config['namedArgs'] ?? null;
if ($namedArgs === null) {
return false;
}
$argName = $arg->name->toString();
if (!in_array($argName, $namedArgs, true)) {
$this->fatalError($arg, "Unknown named parameter \${$argName}");
}
}
}
}
$config = $this->getFuncCallConfig()[$name] ?? null;
if ($config === null) {
return false;
}
// Optimized php::fn::* calls must obey the same ZendVM escape boundary
// as the generic call generator. The four scalar conversions are
// language-level Native keyword aliases and are lowered to an exact
@ -256,7 +295,10 @@ trait FuncCallOptimizer
if (!$arg instanceof Node\Arg) {
continue;
}
if ($this->isVarExpr($arg->value) && is_string($arg->value->name) && !$this->hasVar($arg->value->name)) {
if ($this->isVarExpr($arg->value)
&& is_string($arg->value->name)
&& !$this->hasVar($this->parseIdentifier($arg->value))
) {
return false;
}
}
@ -290,9 +332,11 @@ trait FuncCallOptimizer
protected function dispatchFuncCall(string $name, Node\Expr\FuncCall $expr, array $config): string|false
{
// Named arguments and unpack (...) expansion require runtime handling; fall back to the dynamic call path.
// Unpack expansion and ordinary named arguments require runtime
// handling. Single-argument intrinsics may opt in after validating
// their stable PHP parameter name in parseFuncCallWithOptimizer().
foreach ($expr->args as $arg) {
if ($arg->name !== null || $arg->unpack) {
if ($arg->unpack || ($arg->name !== null && !isset($config['namedArgs']))) {
return false;
}
}
@ -310,6 +354,18 @@ trait FuncCallOptimizer
$variadicType = $config['variadicType'] ?? ($refInfo['variadicType'] ?? '');
$nullables = $refInfo['nullables'] ?? [];
if (!$this->hasUnpackCallArg($expr->args)) {
$argCount = count($expr->args);
$minArgs = $config['minArgs'] ?? ($refInfo['minArgs'] ?? 0);
$maxArgs = $config['maxArgs'] ?? ($refInfo['maxArgs'] ?? 0);
if ($argCount < $minArgs) {
$this->fatalError($expr, "{$name}() expects at least {$minArgs} argument(s), {$argCount} given");
}
if ($maxArgs > 0 && $argCount > $maxArgs) {
$this->fatalError($expr, "{$name}() expects at most {$maxArgs} argument(s), {$argCount} given");
}
}
if (!$this->hasOptimizerSafeTypedArguments(
$expr,
$argTypeStr,

@ -143,6 +143,11 @@ class Translator extends Preprocessor
}
$this->internalFunctions[$functionName] = true;
}
foreach ($this->getFuncCallConfig() as $functionName => $config) {
if (is_array($config) && ($config['intrinsic'] ?? false)) {
$this->internalFunctions[$functionName] = true;
}
}
unset($this->internalFunctions[self::ENTRY_FUNCTION]);
$this->internalConstants = $this->loadInternalConstants();
if ($this->climate->arguments->defined('help')) {

@ -0,0 +1,67 @@
--TEST--
ctype functions use direct C classification without ext-ctype
--FILE--
<?php
function classifyDigit(mixed $value): bool
{
return ctype_digit($value);
}
function main(): void
{
var_dump(ctype_alnum('Abc123'));
var_dump(ctype_alpha('AbC'));
var_dump(ctype_cntrl("\n"));
var_dump(classifyDigit('0123'));
var_dump(ctype_lower('abc'));
var_dump(ctype_graph('!A1'));
var_dump(ctype_print(' A1!'));
var_dump(ctype_punct('!?'));
var_dump(ctype_space(" \t\n"));
var_dump(ctype_upper('ABC'));
var_dump(ctype_xdigit(text: 'A09f'));
var_dump(ctype_digit('12a'));
var_dump(ctype_alpha('abc1'));
var_dump(ctype_space(" \tX"));
var_dump(ctype_print("\n"));
var_dump(ctype_digit(''));
// Preserve ext/ctype's legacy integer classification without calling its
// zif handlers. Other non-string values simply classify as false.
var_dump(ctype_digit(49));
var_dump(ctype_alpha(65));
var_dump(ctype_digit(1000));
var_dump(ctype_alpha(1000));
var_dump(ctype_print(-1000));
var_dump(ctype_digit(null));
var_dump(ctype_digit(false));
var_dump(ctype_digit([]));
}
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(true)
bool(true)
bool(true)
bool(false)
bool(true)
bool(false)
bool(false)
bool(false)
Loading…
Cancel
Save