feat(generator): add call argument generator trait for function call handling

- Implement parseNativeCallArgs method for native function argument parsing
- Add buildNativeVariadicArg method for variadic argument handling
- Create parseNamedCallArgs method for named argument processing
- Add isReferenceArgument method for checking reference parameters
- Implement AOT call argument info retrieval methods
- Add method resolution for AOT class and interface methods
- Include argument info by index lookup functionality
- Add reference named argument checking capability
- Implement comprehensive parseCallArgs method for general argument parsing
- Add scoped callback argument parsing for static/self/parent scopes
- Create utility methods for ensuring call args, array args and named args
- Add positional call argument handling with
pull/17/head
韩天峰 2 months ago
parent eeca2e598b
commit a9e93eda28
  1. 878
      src/CompilerBase.php
  2. 782
      src/Generator/CallArgumentGenerator.php
  3. 72
      src/Generator/DefaultArgumentGenerator.php
  4. 134
      src/Parser/FunctionCallTrait.php
  5. 59
      src/Translator.php

@ -24,6 +24,7 @@ use TypePhp\Exception\Redo;
use TypePhp\Exception\Skip;
use TypePhp\Exception\TestError;
use TypePhp\Generator\AnonClassGenerator;
use TypePhp\Generator\CallArgumentGenerator;
use TypePhp\Generator\ClosureGenerator;
use TypePhp\Generator\FiberGenerator;
use TypePhp\Generator\PlaceHolderGenerator;
@ -36,6 +37,7 @@ use TypePhp\Optimizer\LoopVarOptimizer;
use TypePhp\Parser\StdContainerTrait;
use TypePhp\Parser\AssignOpTrait;
use TypePhp\Parser\BinaryOpTrait;
use TypePhp\Parser\FunctionCallTrait;
use TypePhp\Parser\TypeConversionTrait;
use TypePhp\Parser\TypeDetectionTrait;
use TypePhp\Optimizer\FuncCallOptimizer;
@ -78,6 +80,7 @@ class CompilerBase implements PropertyAccessContext
use AstNodeType;
use FuncCallOptimizer;
use AnonClassGenerator;
use CallArgumentGenerator;
use ClosureGenerator;
use FiberGenerator;
use PlaceHolderGenerator;
@ -85,6 +88,7 @@ class CompilerBase implements PropertyAccessContext
use MagicMethodDetector;
use StdContainerTrait;
use BinaryOpTrait;
use FunctionCallTrait;
use TypeConversionTrait;
use TypeDetectionTrait;
use AssignOpTrait;
@ -3605,880 +3609,6 @@ class CompilerBase implements PropertyAccessContext
* argument to the callable on the right. Materialising the left operand
* also avoids relying on C++ argument-evaluation order.
*/
protected function parsePipeOperator(Expr\BinaryOp\Pipe $expr): string
{
$this->assertExprCanBeUsedAsValue($expr->left, 'pipe left operand');
$this->assertExprCanBeUsedAsValue($expr->right, 'pipe callable');
[$leftExpr, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr->left);
$this->appendCapturedStmtLinesToContext($beforeStmts);
$value = $this->addTmpVar(self::TYPE_VAR);
$this->context->beforeStmtLines[] = $value . ' = ' . $leftExpr . ';';
$this->appendCapturedStmtLinesToContext($afterStmts);
$directCall = $this->parsePipeFirstClassCallable($expr->right, $value);
if ($directCall !== null) {
return $directCall;
}
$callable = $this->parseExprAsValue($expr->right);
return 'php::call(' . $callable . ', {' . $value . '})';
}
/**
* Lower a first-class callable used as a pipe target to its direct call.
*
* `trim(...)`, `ClassName::method(...)`, and `$object->method(...)` do
* not need a Closure when the pipe immediately invokes them. Reusing the
* ordinary call parsers preserves native-call optimization, argument
* validation, visibility checks, and the left-to-right evaluation order.
*/
protected function parsePipeFirstClassCallable(NodeAbstract $callable, string $value): ?string
{
if (!$callable instanceof CallLike || !$callable->isFirstClassCallable()) {
return null;
}
$directCall = clone $callable;
$directCall->args = [new Node\Arg(new Variable($value))];
if ($directCall instanceof Expr\FuncCall) {
return $this->parseFuncCall($directCall);
}
if ($directCall instanceof Expr\StaticCall) {
return $this->parseStaticCall($directCall);
}
if ($directCall instanceof Expr\MethodCall) {
return $this->parseMethodCall($directCall);
}
return null;
}
protected function parseFuncCall(Expr\FuncCall $expr): string
{
if ($this->isVarExpr($expr->name)) {
$fn = $this->parseIdentifier($expr->name);
$placeHolder = $fn;
$name = '';
} elseif ($expr->name->getType() === 'Name' or $expr->name->getType() === 'Name_FullyQualified') {
$name = $this->parseIdentifier($expr->name);
if (in_array($name, Constants::UNSUPPORTED_FUNCTIONS)) {
$this->fatalError($expr, 'Unsupported function: `' . $name . '`');
}
if ($name === 'any') {
if (count($expr->args) !== 1 || $expr->args[0]->unpack) {
$this->fatalError($expr, 'The any function expects exactly one non-unpacked argument');
}
return $this->parseExprAsValue($expr->args[0]->value);
}
if ($name === 'objval') {
return $this->genObjvalCall($expr);
}
$nativeFn = $this->findNativeFunction($name);
if ($nativeFn) {
$expr->setAttribute('nativeCall', $nativeFn);
// 函数调用占位符,不是真实的函数调用
if (count($expr->args) === 1 and $this->isPlaceholderExpr($expr->args[0])) {
return $this->genPlaceHolder($this->identifierToStr($expr->name));
}
$this->checkNativeCallArgs($expr, $this->getFunction($nativeFn), $expr->args, $name);
if ($this->shouldUseDynamicCallForNativeArgs($nativeFn, $expr->args)) {
$functionDef = $this->getFunction($nativeFn);
return $this->genRuntimeFunctionCall($this->getFuncPtr($functionDef->getNamespacedName()), $expr->args, $name);
}
try {
return self::PREFIX . $nativeFn . '(' . $this->parseNativeCallArgs($expr->args, $nativeFn) . ')';
} catch (PlaceHolder) {
return $this->genPlaceHolder($this->identifierToStr($expr->name));
}
}
// 动态调用的函数,转换函数名为带有命名空间的全限定名称
$name = $this->getNamespacedFuncName($name);
$this->checkInternalFunctionArgCount($name, $expr);
$code = $this->parseFuncCallWithOptimizer($name, $expr);
if ($code !== false) {
return $code;
}
$placeHolder = $this->identifierToStr($expr->name);
$fn = $this->getFuncPtr($name);
$this->context->beforeStmtLines[] = $this->formatCppLineComment('Func Call: ', $name . '()');
} else {
$tmpVar = $this->addTmpVar(self::TYPE_VAR);
$this->context->beforeStmtLines[] = $tmpVar . ' = ' . $this->parseExpr($expr->name) . ';';
$placeHolder = $fn = $tmpVar;
$name = '';
}
if (empty($expr->args)) {
return 'php::call(' . $fn . ')';
}
try {
return $this->genRuntimeFunctionCall($fn, $expr->args, $name);
} catch (PlaceHolder) {
return $this->genPlaceHolder($placeHolder);
}
}
/**
* @param array<Node\Arg|Node\VariadicPlaceholder> $callArgs
*/
protected function parseNativeCallArgs(array $callArgs, string $nativeFunc, int $parameterOffset = 0): string
{
$argList = [];
$functionDef = $this->getFunction($nativeFunc);
$args = [];
$variadicArgs = [];
$hasNamedArg = false;
$argNameIndex = $this->getFunctionArgNameIndex($functionDef);
$variadicArgIndex = $this->getVariadicArgIndex($functionDef);
// 对命名参数进行重排
foreach ($callArgs as $i => $arg) {
if ($this->isPlaceholderExpr($arg)) {
throw new PlaceHolder();
}
if ($arg->name) {
$argName = $arg->name->name;
$k = $argNameIndex[$argName] ?? null;
if ($k !== null and ($variadicArgIndex === null or $k < $variadicArgIndex)) {
if ($k < $parameterOffset) {
$this->fatalError($arg, 'Named argument cannot target the extension receiver');
}
$args[$k] = $arg;
} else {
$variadicArgs[] = [$argName, $arg];
}
$hasNamedArg = true;
} elseif ($variadicArgIndex !== null and $i + $parameterOffset >= $variadicArgIndex) {
$variadicArgs[] = [null, $arg];
} else {
$args[$i + $parameterOffset] = $arg;
}
}
// 对 key 进行排序,确保参数顺序正确
if ($hasNamedArg) {
// 命名参数中间存在空洞,需要使用默认参数填充
foreach ($functionDef->argInfoList as $k => $argInfo) {
if ($k < $parameterOffset) {
continue;
}
if ($variadicArgIndex !== null and $k === $variadicArgIndex) {
continue;
}
if (!isset($args[$k])) {
if ($argInfo->defaultValue === null) {
$errorNode = null;
foreach ($callArgs as $a) {
if ($a instanceof Node\Arg && $a->name) {
$errorNode = $a;
break;
}
}
$argName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name);
$this->fatalError($errorNode ?? reset($callArgs), 'Named argument `' . $argName . '` is missing default value');
}
$args[$k] = new Node\Arg($argInfo->defaultValue);
}
}
ksort($args);
}
if ($variadicArgIndex !== null and $variadicArgs) {
$args[$variadicArgIndex] = $this->buildNativeVariadicArg($variadicArgs, $functionDef->argInfoList[$variadicArgIndex]);
ksort($args);
}
// 函数只接受一个变长参数,且调用参数为空,直接传入空数组
if (count($args) === 0
and count($functionDef->argInfoList) === $parameterOffset + 1
and $functionDef->argInfoList[$parameterOffset]->variadic) {
return '{}';
}
foreach ($args as $i => $arg) {
if (is_string($arg)) {
$argList[] = $arg;
continue;
}
$argInfo = $this->getArgInfo($arg, $nativeFunc, $i);
$argList[] = $this->getTypeConvertedArg($arg, $argInfo);
}
return implode(', ', $argList);
}
protected function buildNativeVariadicArg(array $variadicArgs, ArgInfo $argInfo): string
{
if (count($variadicArgs) === 1 and $variadicArgs[0][0] === null and $variadicArgs[0][1]->unpack) {
$arg = $variadicArgs[0][1];
if ($this->isVarExpr($arg->value)) {
$var = $this->parseIdentifier($arg->value);
if ($this->getVarType($var) === self::TYPE_ARRAY) {
return $var;
}
}
return $this->convertArrayExpr($this->parseExpr($arg->value));
}
$tmpVar = $this->addTmpVar(self::TYPE_ARRAY);
foreach ($variadicArgs as [$name, $arg]) {
if ($arg->unpack) {
$this->context->beforeStmtLines[] = $tmpVar . '.merge(' . $this->parseArrayArg($arg) . ');';
} elseif ($name !== null) {
$this->context->beforeStmtLines[] = $tmpVar . '.set(' . $this->getLiteralString($name) . ', ' . $this->getTypeConvertedArg($arg, $argInfo) . ');';
} else {
$this->context->beforeStmtLines[] = $tmpVar . '.append(' . $this->getTypeConvertedArg($arg, $argInfo) . ');';
}
}
return $tmpVar;
}
protected function parseNamedCallArgs(array $args, int $firstIndex, array $listArgs): string
{
$namedArgs = [];
foreach ($args as $i => $arg) {
if ($i < $firstIndex) {
continue;
}
if ($arg->name === null) {
$this->fatalError($arg, 'Named argument must follow positional argument');
}
if (!$this->isIdExpr($arg->name)) {
$this->fatalError($arg, 'Named argument must be a string');
}
if (array_key_exists($arg->name->name, $namedArgs)) {
$this->fatalError($arg, "Duplicate named argument `{$arg->name->name}`");
}
$namedArgs[$arg->name->name] = $this->parseCallArgValue($arg);
}
$tmpVar = $this->genTmpVarName();
$array = self::TYPE_ARRAY . ' ' . $tmpVar . ';';
foreach ($namedArgs as $k => $v) {
$array .= $tmpVar . '.set(' . $this->getLiteralString($k) . ', ' . $v . ');' . PHP_EOL;
}
$this->context->beforeStmtLines[] = $array;
$this->context->afterStmtLines[] = $tmpVar . '.unset();';
return Symbol::argList() . '{' . implode(', ', $listArgs) . '}, ' . $tmpVar . '.array()';
}
protected function isReferenceArgument($funcName, $className, $argIndex): bool
{
$argInfo = $this->getAotCallArgInfo($funcName, $className, $argIndex);
if ($argInfo !== null) {
return $argInfo->byRef;
}
if ($className) {
// 动态调用类方法,无法判断参数是否为引用
if ($className === self::DYNAMIC_CALLED_CLASS) {
return false;
}
$param = Reflection::getClassMethodParameter($className, $funcName, $argIndex);
} else {
$param = Reflection::getFunctionParameter($funcName, $argIndex);
}
if ($param) {
return $param->isPassedByReference();
}
// 参数索引超出声明范围,检查最后一个参数是否为变长引用参数(如 &...$rest)
$variadicParam = Reflection::getVariadicParameter($funcName, $className);
return $variadicParam !== null && $variadicParam->isPassedByReference();
}
protected function getAotCallArgInfo(string $funcName, string $className, int $argIndex): ?ArgInfo
{
if ($className !== '') {
$functionDef = $this->findAotMethodFunctionDef($className, $funcName);
if ($functionDef === null) {
return null;
}
return $this->getArgInfoByIndex($functionDef, $argIndex);
}
if (!$this->hasFunction($funcName)) {
return null;
}
return $this->getArgInfoByIndex($this->getFunction($funcName), $argIndex);
}
protected function getAotCallArgInfoByName(string $funcName, string $className, string $argName): ?ArgInfo
{
$functionDef = null;
if ($className !== '') {
$functionDef = $this->findAotMethodFunctionDef($className, $funcName);
} elseif ($this->hasFunction($funcName)) {
$functionDef = $this->getFunction($funcName);
}
if ($functionDef === null) {
return null;
}
$variadicArgInfo = null;
foreach ($functionDef->argInfoList as $argInfo) {
if ($argInfo->variadic) {
$variadicArgInfo = $argInfo;
}
if (($argInfo->phpName ?: $this->unescapeVarName($argInfo->name)) === $argName) {
return $argInfo;
}
}
return $variadicArgInfo;
}
/** Resolve a project class or interface method declaration for AOT call arguments. */
protected function findAotMethodFunctionDef(string $className, string $funcName): ?FunctionDef
{
if ($className === self::DYNAMIC_CALLED_CLASS) {
return null;
}
if ($this->hasInterface($className)) {
return $this->findAotInterfaceMethodFunctionDef($className, $funcName);
}
if (!$this->hasClass($className)) {
return null;
}
$classDef = $this->getClass($className);
while (true) {
if ($classDef->hasMethod($funcName)) {
return $classDef->getMethod($funcName)->functionDef;
}
if ($classDef->hasAbstractMethod($funcName)) {
return $classDef->getAbstractMethod($funcName)->functionDef;
}
foreach ($classDef->implements as $interface) {
$functionDef = $this->findAotInterfaceMethodFunctionDef($interface, $funcName);
if ($functionDef !== null) {
return $functionDef;
}
}
if (!$classDef->extends || !$this->hasClass($classDef->extends)) {
return null;
}
$classDef = $this->getClass($classDef->extends);
}
}
/** Resolve a method from an interface or one of its parent interfaces. */
protected function findAotInterfaceMethodFunctionDef(string $interfaceName, string $funcName): ?FunctionDef
{
$pending = [$interfaceName];
$visited = [];
while ($pending) {
$current = array_pop($pending);
$key = strtolower($current);
if (isset($visited[$key]) || !$this->hasInterface($current)) {
continue;
}
$visited[$key] = true;
$interfaceDef = $this->getInterface($current);
if ($interfaceDef->hasMethod($funcName)) {
return $interfaceDef->methods[strtolower($funcName)]->functionDef;
}
foreach ($interfaceDef->extendsList ?: ($interfaceDef->extends ? [$interfaceDef->extends] : []) as $parent) {
$pending[] = $parent;
}
}
return null;
}
protected function getArgInfoByIndex(FunctionDef $functionDef, int $argIndex): ?ArgInfo
{
if (array_key_exists($argIndex, $functionDef->argInfoList)) {
return $functionDef->argInfoList[$argIndex];
}
if ($functionDef->hasVariadicArg()) {
return $functionDef->argInfoList[array_key_last($functionDef->argInfoList)];
}
return null;
}
protected function isReferenceNamedArgument(string $funcName, string $className, string $argName): bool
{
$argInfo = $this->getAotCallArgInfoByName($funcName, $className, $argName);
if ($argInfo !== null) {
return $argInfo->byRef;
}
if ($className) {
if ($className === self::DYNAMIC_CALLED_CLASS) {
return false;
}
$ref = Reflection::getClass($className);
if (!$ref) {
return false;
}
try {
$params = $ref->getMethod($funcName)->getParameters();
} catch (\ReflectionException) {
return false;
}
} else {
$ref = Reflection::getFunction($funcName);
if (!$ref) {
return false;
}
$params = $ref->getParameters();
}
$variadicParam = null;
foreach ($params as $param) {
if ($param->isVariadic()) {
$variadicParam = $param;
}
if ($param->getName() === $argName) {
return $param->isPassedByReference();
}
}
return $variadicParam !== null && $variadicParam->isPassedByReference();
}
protected function parseCallArgs(
array $args,
string $funcName = '',
string $className = '',
bool $separateNamedArgs = true,
bool $forceArrayArgs = false
): string
{
$list_args = [];
$arrayArgsVar = null;
$argsVar = null;
$namedArgsVar = null;
$namedArgs = [];
$hasNamedArg = false;
$hasUnpack = false;
if ($forceArrayArgs) {
$this->ensureCallArrayArgs($arrayArgsVar, $list_args);
}
foreach ($args as $i => $arg) {
if ($this->isPlaceholderExpr($arg)) {
throw new PlaceHolder();
}
if ($arg->unpack) {
if ($hasNamedArg) {
$this->fatalError($arg, 'Cannot use argument unpacking after named arguments');
}
$hasUnpack = true;
if (!$forceArrayArgs && $separateNamedArgs) {
$callArgs = $this->ensureCallArgs($argsVar, $list_args);
$this->context->beforeStmtLines[] = $callArgs . '.appendUnpacked(' . $this->parseArrayArg($arg) . ');';
} else {
$arrayArgs = $this->ensureCallArrayArgs($arrayArgsVar, $list_args);
$this->context->beforeStmtLines[] = $arrayArgs . '.merge(' . $this->parseArrayArg($arg) . ');';
}
continue;
}
if ($arg->name !== null) {
$hasNamedArg = true;
if (!$this->isIdExpr($arg->name)) {
$this->fatalError($arg, 'Named argument must be a string');
}
if (array_key_exists($arg->name->name, $namedArgs)) {
$this->fatalError($arg, "Duplicate named argument `{$arg->name->name}`");
}
$namedArgs[$arg->name->name] = true;
$byRef = $funcName && $this->isReferenceNamedArgument($funcName, $className, $arg->name->name);
$value = ($byRef || $this->isRefvalCall($arg->value) || $this->isToRefCall($arg->value))
? $this->parseReferenceCallArgValue($arg)
: $this->parseCallArgValue($arg);
if ($separateNamedArgs) {
$namedArgsArray = $this->ensureCallNamedArgs($namedArgsVar);
$this->context->beforeStmtLines[] = $namedArgsArray . '.set(' . $this->getLiteralString($arg->name->name) . ', ' . $value . ');';
} else {
$arrayArgs = $this->ensureCallArrayArgs($arrayArgsVar, $list_args);
$this->context->beforeStmtLines[] = $arrayArgs . '.set(' . $this->getLiteralString($arg->name->name) . ', ' . $value . ');';
}
continue;
}
if ($hasNamedArg) {
$this->fatalError($arg, 'Cannot use positional argument after named argument');
}
if ($hasUnpack) {
$this->fatalError($arg, 'Cannot use positional argument after argument unpacking');
}
$byRef = $funcName && $this->isReferenceArgument($funcName, $className, $i);
if (($funcName === 'call_user_func' || $funcName === 'call_user_func_array') && $i === 0) {
$callback = $this->parseScopedCallbackArg($arg);
if ($callback !== null) {
$this->addPositionalCallArg($callback, $arrayArgsVar, $list_args);
continue;
}
}
if ($this->isVarExpr($arg->value)) {
$name = $this->parseIdentifier($arg->value);
if ($byRef) {
$this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args);
continue;
}
if (!$this->hasVar($name)) {
$this->fatalError($arg, 'Undefined variable `$' . $name . '`');
}
} elseif ($this->isPropertyFetch($arg->value) and $this->isVarExpr($arg->value->var)) {
if ($byRef) {
$this->addPositionalCallArg($this->emitDynamicPropertyFetchRef($arg->value, $arg), $arrayArgsVar, $list_args);
continue;
}
$objectExpr = $this->parseIdentifier($arg->value->var);
if (!$this->hasVar($objectExpr)) {
$this->fatalError($arg, 'Undefined variable `$' . $objectExpr . '`');
}
} elseif ($this->isArrayDimFetch($arg->value) and $this->isVarExpr($arg->value->var)) {
$array = $this->parseIdentifier($arg->value->var);
if ($array === 'GLOBALS') {
$globalVar = $this->parseGlobalsArrayDimFetch($arg->value);
// 全局变量作为引用参数
if ($byRef) {
$ref = $this->addTmpVar(self::TYPE_REF);
$this->context->beforeStmtLines[] = $ref . ' = ' . $globalVar . '.toReference();';
$this->addPositionalCallArg('&' . $ref, $arrayArgsVar, $list_args);
} else {
$this->addPositionalCallArg($globalVar, $arrayArgsVar, $list_args);
}
continue;
}
if ($this->isVarExpr($arg->value->var) and !$this->hasVar($array)) {
$this->fatalError($arg, 'Undefined variable `$' . $array . '`');
}
if ($byRef) {
if ($arg->value->dim === null) {
$this->fatalError($arg, 'Array dimension must be a constant expression');
}
$this->addPositionalCallArg($array . '.itemRef(' . $this->identifierToStr($arg->value->dim) . ')', $arrayArgsVar, $list_args);
continue;
}
} elseif ($this->isReferenceWrapperCall($arg->value)) {
$inner = $this->unwrapReferenceWrapperCall($arg->value, $arg);
if ($this->isVarExpr($inner)) {
$name = $this->parseVariable($inner);
$arg->value = $inner;
$this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args);
continue;
}
$expr = $this->expandRefvalExpr($inner, $arg);
if ($expr !== null) {
$this->addPositionalCallArg($expr, $arrayArgsVar, $list_args);
continue;
}
$this->fatalError($arg, 'The refval function only accepts a variable, array element, or object property');
} else {
if ($byRef) {
if ($this->isScalar($arg->value)) {
$this->fatalError($arg, 'The constants cannot be used as an argument for a reference-type parameter');
}
$tmpRef = $this->genTmpVarName();
$this->addLocalVar($tmpRef, self::TYPE_REF);
$this->context->beforeStmtLines[] = $tmpRef . ' = ' . $this->parseChainedExpr($arg->value, self::OP_REFVAL) . ';';
$this->addPositionalCallArg('&' . $tmpRef, $arrayArgsVar, $list_args);
continue;
}
}
$value = $this->parseCallArgValue($arg);
$this->addPositionalCallArg($value, $arrayArgsVar, $list_args);
}
if ($argsVar !== null) {
return $namedArgsVar !== null ? $argsVar . ', ' . $namedArgsVar . '.array()' : $argsVar;
}
if ($arrayArgsVar !== null) {
return $namedArgsVar !== null ? $arrayArgsVar . ', ' . $namedArgsVar . '.array()' : $arrayArgsVar;
}
$callArgs = Symbol::argList() . '{' . implode(', ', $list_args) . '}';
return $namedArgsVar !== null ? $callArgs . ', ' . $namedArgsVar . '.array()' : $callArgs;
}
protected function parseScopedCallbackArg(Node\Arg $arg): ?string
{
$value = $arg->value;
if (!$value instanceof Expr\Array_ || count($value->items) < 2 || !$this->methodDef) {
return null;
}
$first = $value->items[0];
if (!$first instanceof ArrayItem || $first->key !== null || $first->unpack) {
return null;
}
if (!$first->value instanceof Node\Scalar\String_) {
return null;
}
$scope = strtolower($first->value->value);
$classExpr = match ($scope) {
'static' => ($this->methodDef->flags & Modifiers::STATIC)
? $this->getLiteralString($this->getFullClassName())
: Symbol::getCalledClass(),
'self' => $this->getLiteralString($this->getFullClassName()),
'parent' => $this->classDef->extends ? $this->getLiteralString($this->classDef->extends) : null,
default => null,
};
if ($classExpr === null) {
return null;
}
$items = [$classExpr];
foreach (array_slice($value->items, 1) as $item) {
if (!$item instanceof ArrayItem || $item->key !== null || $item->unpack) {
return null;
}
$this->assertExprCanBeUsedAsValue($item->value, 'callback array item');
$items[] = $this->parseIdentifier($item->value);
}
return $this->genArray($items);
}
protected function ensureCallArgs(?string &$argsVar, array &$listArgs): string
{
if ($argsVar === null) {
$argsVar = $this->genTmpVarName();
$this->context->beforeStmtLines[] = self::TYPE_ARGS . ' ' . $argsVar . '{' . Symbol::argList() . '{' . implode(', ', $listArgs) . '}};';
$listArgs = [];
}
return $argsVar;
}
protected function ensureCallArrayArgs(?string &$arrayArgsVar, array &$listArgs): string
{
if ($arrayArgsVar === null) {
$arrayArgsVar = $this->genTmpVarName();
$this->context->beforeStmtLines[] = self::TYPE_ARRAY . ' ' . $arrayArgsVar . '{' . implode(', ', $listArgs) . '};';
$listArgs = [];
}
return $arrayArgsVar;
}
protected function ensureCallNamedArgs(?string &$namedArgsVar): string
{
if ($namedArgsVar === null) {
$namedArgsVar = $this->genTmpVarName();
$this->context->beforeStmtLines[] = self::TYPE_ARRAY . ' ' . $namedArgsVar . ';';
$this->context->afterStmtLines[] = $namedArgsVar . '.unset();';
}
return $namedArgsVar;
}
protected function addPositionalCallArg(string $value, ?string $arrayArgsVar, array &$listArgs): void
{
if ($arrayArgsVar !== null) {
$this->context->beforeStmtLines[] = $arrayArgsVar . '.append(' . $value . ');';
} else {
$listArgs[] = $value;
}
}
protected function parseCallArgValue(Node\Arg $arg): string
{
$this->assertExprCanBeUsedAsValue($arg->value, 'function argument');
return $this->materializeCallArgValue($arg->value, $this->parseArg($arg));
}
protected function materializeCallArgValue(NodeAbstract $value, string $expr): string
{
if (!$this->shouldMaterializeCallArg($value)) {
return $expr;
}
return 'php_deindirect(' . $expr . ')';
}
protected function shouldMaterializeCallArg(NodeAbstract $value): bool
{
if ($value instanceof Expr\ArrayDimFetch) {
return !$this->isStdContainerExpr($value);
}
return $value instanceof Expr\PropertyFetch;
}
protected function parseReferenceCallArgValue(Node\Arg $arg): string
{
if ($this->isReferenceWrapperCall($arg->value)) {
$arg->value = $this->unwrapReferenceWrapperCall($arg->value, $arg);
}
if ($this->isVarExpr($arg->value)) {
return $this->parseArgRefVar($arg, $this->parseIdentifier($arg->value));
}
if ($this->isPropertyFetch($arg->value) and $this->isVarExpr($arg->value->var)) {
return $this->emitDynamicPropertyFetchRef($arg->value, $arg);
}
if ($this->isArrayDimFetch($arg->value) and $this->isVarExpr($arg->value->var)) {
$array = $this->parseIdentifier($arg->value->var);
if ($array === 'GLOBALS') {
$globalVar = $this->parseGlobalsArrayDimFetch($arg->value);
$ref = $this->addTmpVar(self::TYPE_REF);
$this->context->beforeStmtLines[] = $ref . ' = ' . $globalVar . '.toReference();';
return '&' . $ref;
}
if (!$this->hasVar($array)) {
$this->fatalError($arg, 'Undefined variable `$' . $array . '`');
}
if ($arg->value->dim === null) {
$this->fatalError($arg, 'Array dimension must be a constant expression');
}
return $array . '.itemRef(' . $this->identifierToStr($arg->value->dim) . ')';
}
if ($this->isScalar($arg->value)) {
$this->fatalError($arg, 'The constants cannot be used as an argument for a reference-type parameter');
}
$tmpRef = $this->genTmpVarName();
$this->addLocalVar($tmpRef, self::TYPE_REF);
$this->context->beforeStmtLines[] = $tmpRef . ' = ' . $this->parseChainedExpr($arg->value, self::OP_REFVAL) . ';';
return '&' . $tmpRef;
}
protected function isToRefCall(NodeAbstract $expr): bool
{
return $this->isMethodCall($expr)
&& $this->isNamedMethod($expr->name)
&& $expr->name->toString() === 'toRef';
}
protected function isReferenceWrapperCall(NodeAbstract $expr): bool
{
return $this->isRefvalCall($expr) || $this->isToRefCall($expr);
}
protected function unwrapReferenceWrapperCall(NodeAbstract $expr, NodeAbstract $errorNode): NodeAbstract
{
if ($this->isRefvalCall($expr)) {
if (count($expr->args) !== 1) {
$this->fatalError($errorNode, 'The refval function only accepts one parameter');
}
return $expr->args[0]->value;
}
if ($this->isToRefCall($expr)) {
if (!empty($expr->args)) {
$this->fatalError($errorNode, 'The toRef method does not accept parameters');
}
return $expr->var;
}
$this->fatalError($errorNode, 'Expected a reference wrapper call');
}
/**
* 展开 refval() 调用中的数组元素或对象属性,返回对应的 C++ 引用表达式。
* 若为普通变量则返回 null,由调用方自行处理。
*/
protected function expandRefvalExpr(NodeAbstract $inner, Node\Arg $arg): ?string
{
if ($this->isPropertyFetch($inner) and $this->isVarExpr($inner->var)) {
return $this->emitDynamicPropertyFetchRef($inner, $arg);
}
if ($this->isArrayDimFetch($inner) and $this->isVarExpr($inner->var)) {
$array = $this->parseIdentifier($inner->var);
if ($array === 'GLOBALS') {
$globalVar = $this->parseGlobalsArrayDimFetch($inner);
$ref = $this->addTmpVar(self::TYPE_REF);
$this->context->beforeStmtLines[] = $ref . ' = ' . $globalVar . '.toReference();';
return '&' . $ref;
}
if (!$this->hasVar($array)) {
$this->fatalError($arg, 'Undefined variable `$' . $array . '`');
}
if ($inner->dim === null) {
$this->fatalError($arg, 'Array dimension must be a constant expression');
}
return $array . '.itemRef(' . $this->identifierToStr($inner->dim) . ')';
}
return null;
}
/**
* 仅用于动态调用的参数解析
*/
protected function parseArgRefVar(Node\Arg $arg, string $name): string
{
if (!$this->hasVar($name)) {
// 若参数是引用类型,可以传入未定义变量,将立即创建变量作为引用
$this->addLocalVar($name, self::TYPE_REF);
} else {
// 本地变量,且是原生类型,则转为普通变量
if ($this->hasLocalVar($name) and $this->isNativeType($this->getVarType($name))) {
$this->context->localVars[$name] = self::TYPE_VAR;
}
// 需要引用类型的参数,使用临时变量作为引用,并替换掉实际的参数
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, self::TYPE_REF);
$this->context->beforeStmtLines[] = $tmpVar . ' = ' . $this->parseExpr($arg->value) . '.toReference();';
$name = $tmpVar;
}
// 动态调用,参数列表是 Variant 类型而不是 Reference,必须使用 & 符号取地址,传递指针,以保持引用传递
return '&' . $name;
}
protected function parseArg(Node\Arg $arg): string
{
if ($this->isArrayDimFetch($arg->value) and $this->isStdContainerExpr($arg->value)) {
if ($this->isStdArrayExpr($arg->value)) {
$valueExpr = $this->parseStdArrayDimFetch($arg->value);
$attr = $arg->value->getAttribute('stdArrayDimFetch');
if ($attr['accessLevel'] === $attr['totalLevel']) {
return $this->convertExprFromType($this->context->stdArrays[$attr['var']]['type'], $valueExpr);
} else {
return $this->convertArrayExpr($valueExpr);
}
} else {
$valueExpr = $this->parseStdContainerDimFetch($arg->value);
$attr = $arg->value->getAttribute('stdContainerDimFetch');
return $this->convertExprFromType($this->context->stdContainers[$attr['var']]['type'], $valueExpr);
}
}
$expr = $this->parseIdentifier($arg->value);
if ($this->isVarExpr($arg->value) and $arg->value->name === 'GLOBALS') {
return 'php_globals_array()';
}
if ($this->isVarExpr($arg->value) and $this->isStdContainer($arg->value->name)) {
return $this->convertArrayExpr($expr . '_ref');
}
return $expr;
}
protected function parseOrderedArg(Node\Arg $arg): string
{
if ($this->isArrayDimFetch($arg->value) and $this->isStdContainerExpr($arg->value)) {
return $this->parseArg($arg);
}
if ($this->isVarExpr($arg->value) and $arg->value->name === 'GLOBALS') {
return 'php_globals_array()';
}
if ($this->isVarExpr($arg->value) and $this->isStdContainer($arg->value->name)) {
return $this->convertArrayExpr($this->parseIdentifier($arg->value) . '_ref');
}
return $this->parseOrderedOperand($arg->value, false);
}
protected function parseArrayArg(Node\Arg $expr): string
{
$value = $expr->value;
if ($this->isVarExpr($value)) {
$var = $this->parseIdentifier($value);
if (!$this->hasVar($var)) {
$this->errorUndefinedVariable($value);
}
if ($this->getVarType($var) === self::TYPE_ARRAY) {
return $var;
}
}
return $this->parseExpr($value);
}
protected function parsePostOp(Expr\PostDec|Expr\PostInc $expr, string $op): string
{

@ -0,0 +1,782 @@
<?php
/**
* This file is part of TypePHP.
*
* Call argument lowering shared by native and dynamic call paths.
*/
namespace TypePhp\Generator;
use PhpParser\Modifiers;
use PhpParser\Node;
use PhpParser\Node\ArrayItem;
use PhpParser\Node\Expr;
use PhpParser\NodeAbstract;
use TypePhp\ArgInfo;
use TypePhp\Entity\FunctionDef;
use TypePhp\Exception\PlaceHolder;
use TypePhp\Reflection;
use TypePhp\Symbol;
trait CallArgumentGenerator
{
protected function parseNativeCallArgs(array $callArgs, string $nativeFunc, int $parameterOffset = 0): string
{
$argList = [];
$functionDef = $this->getFunction($nativeFunc);
$args = [];
$variadicArgs = [];
$hasNamedArg = false;
$argNameIndex = $this->getFunctionArgNameIndex($functionDef);
$variadicArgIndex = $this->getVariadicArgIndex($functionDef);
// 对命名参数进行重排
foreach ($callArgs as $i => $arg) {
if ($this->isPlaceholderExpr($arg)) {
throw new PlaceHolder();
}
if ($arg->name) {
$argName = $arg->name->name;
$k = $argNameIndex[$argName] ?? null;
if ($k !== null and ($variadicArgIndex === null or $k < $variadicArgIndex)) {
if ($k < $parameterOffset) {
$this->fatalError($arg, 'Named argument cannot target the extension receiver');
}
$args[$k] = $arg;
} else {
$variadicArgs[] = [$argName, $arg];
}
$hasNamedArg = true;
} elseif ($variadicArgIndex !== null and $i + $parameterOffset >= $variadicArgIndex) {
$variadicArgs[] = [null, $arg];
} else {
$args[$i + $parameterOffset] = $arg;
}
}
// 对 key 进行排序,确保参数顺序正确
if ($hasNamedArg) {
// 命名参数中间存在空洞,需要使用默认参数填充
foreach ($functionDef->argInfoList as $k => $argInfo) {
if ($k < $parameterOffset) {
continue;
}
if ($variadicArgIndex !== null and $k === $variadicArgIndex) {
continue;
}
if (!isset($args[$k])) {
if ($argInfo->defaultValue === null) {
$errorNode = null;
foreach ($callArgs as $a) {
if ($a instanceof Node\Arg && $a->name) {
$errorNode = $a;
break;
}
}
$argName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name);
$this->fatalError($errorNode ?? reset($callArgs), 'Named argument `' . $argName . '` is missing default value');
}
$args[$k] = new Node\Arg($argInfo->defaultValue);
}
}
ksort($args);
}
if ($variadicArgIndex !== null and $variadicArgs) {
$args[$variadicArgIndex] = $this->buildNativeVariadicArg($variadicArgs, $functionDef->argInfoList[$variadicArgIndex]);
ksort($args);
}
// 函数只接受一个变长参数,且调用参数为空,直接传入空数组
if (count($args) === 0
and count($functionDef->argInfoList) === $parameterOffset + 1
and $functionDef->argInfoList[$parameterOffset]->variadic) {
return '{}';
}
foreach ($args as $i => $arg) {
if (is_string($arg)) {
$argList[] = $arg;
continue;
}
$argInfo = $this->getArgInfo($arg, $nativeFunc, $i);
$argList[] = $this->getTypeConvertedArg($arg, $argInfo);
}
return implode(', ', $argList);
}
protected function buildNativeVariadicArg(array $variadicArgs, ArgInfo $argInfo): string
{
if (count($variadicArgs) === 1 and $variadicArgs[0][0] === null and $variadicArgs[0][1]->unpack) {
$arg = $variadicArgs[0][1];
if ($this->isVarExpr($arg->value)) {
$var = $this->parseIdentifier($arg->value);
if ($this->getVarType($var) === self::TYPE_ARRAY) {
return $var;
}
}
return $this->convertArrayExpr($this->parseExpr($arg->value));
}
$tmpVar = $this->addTmpVar(self::TYPE_ARRAY);
foreach ($variadicArgs as [$name, $arg]) {
if ($arg->unpack) {
$this->context->beforeStmtLines[] = $tmpVar . '.merge(' . $this->parseArrayArg($arg) . ');';
} elseif ($name !== null) {
$this->context->beforeStmtLines[] = $tmpVar . '.set(' . $this->getLiteralString($name) . ', ' . $this->getTypeConvertedArg($arg, $argInfo) . ');';
} else {
$this->context->beforeStmtLines[] = $tmpVar . '.append(' . $this->getTypeConvertedArg($arg, $argInfo) . ');';
}
}
return $tmpVar;
}
protected function parseNamedCallArgs(array $args, int $firstIndex, array $listArgs): string
{
$namedArgs = [];
foreach ($args as $i => $arg) {
if ($i < $firstIndex) {
continue;
}
if ($arg->name === null) {
$this->fatalError($arg, 'Named argument must follow positional argument');
}
if (!$this->isIdExpr($arg->name)) {
$this->fatalError($arg, 'Named argument must be a string');
}
if (array_key_exists($arg->name->name, $namedArgs)) {
$this->fatalError($arg, "Duplicate named argument `{$arg->name->name}`");
}
$namedArgs[$arg->name->name] = $this->parseCallArgValue($arg);
}
$tmpVar = $this->genTmpVarName();
$array = self::TYPE_ARRAY . ' ' . $tmpVar . ';';
foreach ($namedArgs as $k => $v) {
$array .= $tmpVar . '.set(' . $this->getLiteralString($k) . ', ' . $v . ');' . PHP_EOL;
}
$this->context->beforeStmtLines[] = $array;
$this->context->afterStmtLines[] = $tmpVar . '.unset();';
return Symbol::argList() . '{' . implode(', ', $listArgs) . '}, ' . $tmpVar . '.array()';
}
protected function isReferenceArgument($funcName, $className, $argIndex): bool
{
$argInfo = $this->getAotCallArgInfo($funcName, $className, $argIndex);
if ($argInfo !== null) {
return $argInfo->byRef;
}
if ($className) {
// 动态调用类方法,无法判断参数是否为引用
if ($className === self::DYNAMIC_CALLED_CLASS) {
return false;
}
$param = Reflection::getClassMethodParameter($className, $funcName, $argIndex);
} else {
$param = Reflection::getFunctionParameter($funcName, $argIndex);
}
if ($param) {
return $param->isPassedByReference();
}
// 参数索引超出声明范围,检查最后一个参数是否为变长引用参数(如 &...$rest)
$variadicParam = Reflection::getVariadicParameter($funcName, $className);
return $variadicParam !== null && $variadicParam->isPassedByReference();
}
protected function getAotCallArgInfo(string $funcName, string $className, int $argIndex): ?ArgInfo
{
if ($className !== '') {
$functionDef = $this->findAotMethodFunctionDef($className, $funcName);
if ($functionDef === null) {
return null;
}
return $this->getArgInfoByIndex($functionDef, $argIndex);
}
if (!$this->hasFunction($funcName)) {
return null;
}
return $this->getArgInfoByIndex($this->getFunction($funcName), $argIndex);
}
protected function getAotCallArgInfoByName(string $funcName, string $className, string $argName): ?ArgInfo
{
$functionDef = null;
if ($className !== '') {
$functionDef = $this->findAotMethodFunctionDef($className, $funcName);
} elseif ($this->hasFunction($funcName)) {
$functionDef = $this->getFunction($funcName);
}
if ($functionDef === null) {
return null;
}
$variadicArgInfo = null;
foreach ($functionDef->argInfoList as $argInfo) {
if ($argInfo->variadic) {
$variadicArgInfo = $argInfo;
}
if (($argInfo->phpName ?: $this->unescapeVarName($argInfo->name)) === $argName) {
return $argInfo;
}
}
return $variadicArgInfo;
}
/** Resolve a project class or interface method declaration for AOT call arguments. */
protected function findAotMethodFunctionDef(string $className, string $funcName): ?FunctionDef
{
if ($className === self::DYNAMIC_CALLED_CLASS) {
return null;
}
if ($this->hasInterface($className)) {
return $this->findAotInterfaceMethodFunctionDef($className, $funcName);
}
if (!$this->hasClass($className)) {
return null;
}
$classDef = $this->getClass($className);
while (true) {
if ($classDef->hasMethod($funcName)) {
return $classDef->getMethod($funcName)->functionDef;
}
if ($classDef->hasAbstractMethod($funcName)) {
return $classDef->getAbstractMethod($funcName)->functionDef;
}
foreach ($classDef->implements as $interface) {
$functionDef = $this->findAotInterfaceMethodFunctionDef($interface, $funcName);
if ($functionDef !== null) {
return $functionDef;
}
}
if (!$classDef->extends || !$this->hasClass($classDef->extends)) {
return null;
}
$classDef = $this->getClass($classDef->extends);
}
}
/** Resolve a method from an interface or one of its parent interfaces. */
protected function findAotInterfaceMethodFunctionDef(string $interfaceName, string $funcName): ?FunctionDef
{
$pending = [$interfaceName];
$visited = [];
while ($pending) {
$current = array_pop($pending);
$key = strtolower($current);
if (isset($visited[$key]) || !$this->hasInterface($current)) {
continue;
}
$visited[$key] = true;
$interfaceDef = $this->getInterface($current);
if ($interfaceDef->hasMethod($funcName)) {
return $interfaceDef->methods[strtolower($funcName)]->functionDef;
}
foreach ($interfaceDef->extendsList ?: ($interfaceDef->extends ? [$interfaceDef->extends] : []) as $parent) {
$pending[] = $parent;
}
}
return null;
}
protected function getArgInfoByIndex(FunctionDef $functionDef, int $argIndex): ?ArgInfo
{
if (array_key_exists($argIndex, $functionDef->argInfoList)) {
return $functionDef->argInfoList[$argIndex];
}
if ($functionDef->hasVariadicArg()) {
return $functionDef->argInfoList[array_key_last($functionDef->argInfoList)];
}
return null;
}
protected function isReferenceNamedArgument(string $funcName, string $className, string $argName): bool
{
$argInfo = $this->getAotCallArgInfoByName($funcName, $className, $argName);
if ($argInfo !== null) {
return $argInfo->byRef;
}
if ($className) {
if ($className === self::DYNAMIC_CALLED_CLASS) {
return false;
}
$ref = Reflection::getClass($className);
if (!$ref) {
return false;
}
try {
$params = $ref->getMethod($funcName)->getParameters();
} catch (\ReflectionException) {
return false;
}
} else {
$ref = Reflection::getFunction($funcName);
if (!$ref) {
return false;
}
$params = $ref->getParameters();
}
$variadicParam = null;
foreach ($params as $param) {
if ($param->isVariadic()) {
$variadicParam = $param;
}
if ($param->getName() === $argName) {
return $param->isPassedByReference();
}
}
return $variadicParam !== null && $variadicParam->isPassedByReference();
}
protected function parseCallArgs(
array $args,
string $funcName = '',
string $className = '',
bool $separateNamedArgs = true,
bool $forceArrayArgs = false
): string
{
$list_args = [];
$arrayArgsVar = null;
$argsVar = null;
$namedArgsVar = null;
$namedArgs = [];
$hasNamedArg = false;
$hasUnpack = false;
if ($forceArrayArgs) {
$this->ensureCallArrayArgs($arrayArgsVar, $list_args);
}
foreach ($args as $i => $arg) {
if ($this->isPlaceholderExpr($arg)) {
throw new PlaceHolder();
}
if ($arg->unpack) {
if ($hasNamedArg) {
$this->fatalError($arg, 'Cannot use argument unpacking after named arguments');
}
$hasUnpack = true;
if (!$forceArrayArgs && $separateNamedArgs) {
$callArgs = $this->ensureCallArgs($argsVar, $list_args);
$this->context->beforeStmtLines[] = $callArgs . '.appendUnpacked(' . $this->parseArrayArg($arg) . ');';
} else {
$arrayArgs = $this->ensureCallArrayArgs($arrayArgsVar, $list_args);
$this->context->beforeStmtLines[] = $arrayArgs . '.merge(' . $this->parseArrayArg($arg) . ');';
}
continue;
}
if ($arg->name !== null) {
$hasNamedArg = true;
if (!$this->isIdExpr($arg->name)) {
$this->fatalError($arg, 'Named argument must be a string');
}
if (array_key_exists($arg->name->name, $namedArgs)) {
$this->fatalError($arg, "Duplicate named argument `{$arg->name->name}`");
}
$namedArgs[$arg->name->name] = true;
$byRef = $funcName && $this->isReferenceNamedArgument($funcName, $className, $arg->name->name);
$value = ($byRef || $this->isRefvalCall($arg->value) || $this->isToRefCall($arg->value))
? $this->parseReferenceCallArgValue($arg)
: $this->parseCallArgValue($arg);
if ($separateNamedArgs) {
$namedArgsArray = $this->ensureCallNamedArgs($namedArgsVar);
$this->context->beforeStmtLines[] = $namedArgsArray . '.set(' . $this->getLiteralString($arg->name->name) . ', ' . $value . ');';
} else {
$arrayArgs = $this->ensureCallArrayArgs($arrayArgsVar, $list_args);
$this->context->beforeStmtLines[] = $arrayArgs . '.set(' . $this->getLiteralString($arg->name->name) . ', ' . $value . ');';
}
continue;
}
if ($hasNamedArg) {
$this->fatalError($arg, 'Cannot use positional argument after named argument');
}
if ($hasUnpack) {
$this->fatalError($arg, 'Cannot use positional argument after argument unpacking');
}
$byRef = $funcName && $this->isReferenceArgument($funcName, $className, $i);
if (($funcName === 'call_user_func' || $funcName === 'call_user_func_array') && $i === 0) {
$callback = $this->parseScopedCallbackArg($arg);
if ($callback !== null) {
$this->addPositionalCallArg($callback, $arrayArgsVar, $list_args);
continue;
}
}
if ($this->isVarExpr($arg->value)) {
$name = $this->parseIdentifier($arg->value);
if ($byRef) {
$this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args);
continue;
}
if (!$this->hasVar($name)) {
$this->fatalError($arg, 'Undefined variable `$' . $name . '`');
}
} elseif ($this->isPropertyFetch($arg->value) and $this->isVarExpr($arg->value->var)) {
if ($byRef) {
$this->addPositionalCallArg($this->emitDynamicPropertyFetchRef($arg->value, $arg), $arrayArgsVar, $list_args);
continue;
}
$objectExpr = $this->parseIdentifier($arg->value->var);
if (!$this->hasVar($objectExpr)) {
$this->fatalError($arg, 'Undefined variable `$' . $objectExpr . '`');
}
} elseif ($this->isArrayDimFetch($arg->value) and $this->isVarExpr($arg->value->var)) {
$array = $this->parseIdentifier($arg->value->var);
if ($array === 'GLOBALS') {
$globalVar = $this->parseGlobalsArrayDimFetch($arg->value);
// 全局变量作为引用参数
if ($byRef) {
$ref = $this->addTmpVar(self::TYPE_REF);
$this->context->beforeStmtLines[] = $ref . ' = ' . $globalVar . '.toReference();';
$this->addPositionalCallArg('&' . $ref, $arrayArgsVar, $list_args);
} else {
$this->addPositionalCallArg($globalVar, $arrayArgsVar, $list_args);
}
continue;
}
if ($this->isVarExpr($arg->value->var) and !$this->hasVar($array)) {
$this->fatalError($arg, 'Undefined variable `$' . $array . '`');
}
if ($byRef) {
if ($arg->value->dim === null) {
$this->fatalError($arg, 'Array dimension must be a constant expression');
}
$this->addPositionalCallArg($array . '.itemRef(' . $this->identifierToStr($arg->value->dim) . ')', $arrayArgsVar, $list_args);
continue;
}
} elseif ($this->isReferenceWrapperCall($arg->value)) {
$inner = $this->unwrapReferenceWrapperCall($arg->value, $arg);
if ($this->isVarExpr($inner)) {
$name = $this->parseVariable($inner);
$arg->value = $inner;
$this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args);
continue;
}
$expr = $this->expandRefvalExpr($inner, $arg);
if ($expr !== null) {
$this->addPositionalCallArg($expr, $arrayArgsVar, $list_args);
continue;
}
$this->fatalError($arg, 'The refval function only accepts a variable, array element, or object property');
} else {
if ($byRef) {
if ($this->isScalar($arg->value)) {
$this->fatalError($arg, 'The constants cannot be used as an argument for a reference-type parameter');
}
$tmpRef = $this->genTmpVarName();
$this->addLocalVar($tmpRef, self::TYPE_REF);
$this->context->beforeStmtLines[] = $tmpRef . ' = ' . $this->parseChainedExpr($arg->value, self::OP_REFVAL) . ';';
$this->addPositionalCallArg('&' . $tmpRef, $arrayArgsVar, $list_args);
continue;
}
}
$value = $this->parseCallArgValue($arg);
$this->addPositionalCallArg($value, $arrayArgsVar, $list_args);
}
if ($argsVar !== null) {
return $namedArgsVar !== null ? $argsVar . ', ' . $namedArgsVar . '.array()' : $argsVar;
}
if ($arrayArgsVar !== null) {
return $namedArgsVar !== null ? $arrayArgsVar . ', ' . $namedArgsVar . '.array()' : $arrayArgsVar;
}
$callArgs = Symbol::argList() . '{' . implode(', ', $list_args) . '}';
return $namedArgsVar !== null ? $callArgs . ', ' . $namedArgsVar . '.array()' : $callArgs;
}
protected function parseScopedCallbackArg(Node\Arg $arg): ?string
{
$value = $arg->value;
if (!$value instanceof Expr\Array_ || count($value->items) < 2 || !$this->methodDef) {
return null;
}
$first = $value->items[0];
if (!$first instanceof ArrayItem || $first->key !== null || $first->unpack) {
return null;
}
if (!$first->value instanceof Node\Scalar\String_) {
return null;
}
$scope = strtolower($first->value->value);
$classExpr = match ($scope) {
'static' => ($this->methodDef->flags & Modifiers::STATIC)
? $this->getLiteralString($this->getFullClassName())
: Symbol::getCalledClass(),
'self' => $this->getLiteralString($this->getFullClassName()),
'parent' => $this->classDef->extends ? $this->getLiteralString($this->classDef->extends) : null,
default => null,
};
if ($classExpr === null) {
return null;
}
$items = [$classExpr];
foreach (array_slice($value->items, 1) as $item) {
if (!$item instanceof ArrayItem || $item->key !== null || $item->unpack) {
return null;
}
$this->assertExprCanBeUsedAsValue($item->value, 'callback array item');
$items[] = $this->parseIdentifier($item->value);
}
return $this->genArray($items);
}
protected function ensureCallArgs(?string &$argsVar, array &$listArgs): string
{
if ($argsVar === null) {
$argsVar = $this->genTmpVarName();
$this->context->beforeStmtLines[] = self::TYPE_ARGS . ' ' . $argsVar . '{' . Symbol::argList() . '{' . implode(', ', $listArgs) . '}};';
$listArgs = [];
}
return $argsVar;
}
protected function ensureCallArrayArgs(?string &$arrayArgsVar, array &$listArgs): string
{
if ($arrayArgsVar === null) {
$arrayArgsVar = $this->genTmpVarName();
$this->context->beforeStmtLines[] = self::TYPE_ARRAY . ' ' . $arrayArgsVar . '{' . implode(', ', $listArgs) . '};';
$listArgs = [];
}
return $arrayArgsVar;
}
protected function ensureCallNamedArgs(?string &$namedArgsVar): string
{
if ($namedArgsVar === null) {
$namedArgsVar = $this->genTmpVarName();
$this->context->beforeStmtLines[] = self::TYPE_ARRAY . ' ' . $namedArgsVar . ';';
$this->context->afterStmtLines[] = $namedArgsVar . '.unset();';
}
return $namedArgsVar;
}
protected function addPositionalCallArg(string $value, ?string $arrayArgsVar, array &$listArgs): void
{
if ($arrayArgsVar !== null) {
$this->context->beforeStmtLines[] = $arrayArgsVar . '.append(' . $value . ');';
} else {
$listArgs[] = $value;
}
}
protected function parseCallArgValue(Node\Arg $arg): string
{
$this->assertExprCanBeUsedAsValue($arg->value, 'function argument');
return $this->materializeCallArgValue($arg->value, $this->parseArg($arg));
}
protected function materializeCallArgValue(NodeAbstract $value, string $expr): string
{
if (!$this->shouldMaterializeCallArg($value)) {
return $expr;
}
return 'php_deindirect(' . $expr . ')';
}
protected function shouldMaterializeCallArg(NodeAbstract $value): bool
{
if ($value instanceof Expr\ArrayDimFetch) {
return !$this->isStdContainerExpr($value);
}
return $value instanceof Expr\PropertyFetch;
}
protected function parseReferenceCallArgValue(Node\Arg $arg): string
{
if ($this->isReferenceWrapperCall($arg->value)) {
$arg->value = $this->unwrapReferenceWrapperCall($arg->value, $arg);
}
if ($this->isVarExpr($arg->value)) {
return $this->parseArgRefVar($arg, $this->parseIdentifier($arg->value));
}
if ($this->isPropertyFetch($arg->value) and $this->isVarExpr($arg->value->var)) {
return $this->emitDynamicPropertyFetchRef($arg->value, $arg);
}
if ($this->isArrayDimFetch($arg->value) and $this->isVarExpr($arg->value->var)) {
$array = $this->parseIdentifier($arg->value->var);
if ($array === 'GLOBALS') {
$globalVar = $this->parseGlobalsArrayDimFetch($arg->value);
$ref = $this->addTmpVar(self::TYPE_REF);
$this->context->beforeStmtLines[] = $ref . ' = ' . $globalVar . '.toReference();';
return '&' . $ref;
}
if (!$this->hasVar($array)) {
$this->fatalError($arg, 'Undefined variable `$' . $array . '`');
}
if ($arg->value->dim === null) {
$this->fatalError($arg, 'Array dimension must be a constant expression');
}
return $array . '.itemRef(' . $this->identifierToStr($arg->value->dim) . ')';
}
if ($this->isScalar($arg->value)) {
$this->fatalError($arg, 'The constants cannot be used as an argument for a reference-type parameter');
}
$tmpRef = $this->genTmpVarName();
$this->addLocalVar($tmpRef, self::TYPE_REF);
$this->context->beforeStmtLines[] = $tmpRef . ' = ' . $this->parseChainedExpr($arg->value, self::OP_REFVAL) . ';';
return '&' . $tmpRef;
}
protected function isToRefCall(NodeAbstract $expr): bool
{
return $this->isMethodCall($expr)
&& $this->isNamedMethod($expr->name)
&& $expr->name->toString() === 'toRef';
}
protected function isReferenceWrapperCall(NodeAbstract $expr): bool
{
return $this->isRefvalCall($expr) || $this->isToRefCall($expr);
}
protected function unwrapReferenceWrapperCall(NodeAbstract $expr, NodeAbstract $errorNode): NodeAbstract
{
if ($this->isRefvalCall($expr)) {
if (count($expr->args) !== 1) {
$this->fatalError($errorNode, 'The refval function only accepts one parameter');
}
return $expr->args[0]->value;
}
if ($this->isToRefCall($expr)) {
if (!empty($expr->args)) {
$this->fatalError($errorNode, 'The toRef method does not accept parameters');
}
return $expr->var;
}
$this->fatalError($errorNode, 'Expected a reference wrapper call');
}
/**
* 展开 refval() 调用中的数组元素或对象属性,返回对应的 C++ 引用表达式。
* 若为普通变量则返回 null,由调用方自行处理。
*/
protected function expandRefvalExpr(NodeAbstract $inner, Node\Arg $arg): ?string
{
if ($this->isPropertyFetch($inner) and $this->isVarExpr($inner->var)) {
return $this->emitDynamicPropertyFetchRef($inner, $arg);
}
if ($this->isArrayDimFetch($inner) and $this->isVarExpr($inner->var)) {
$array = $this->parseIdentifier($inner->var);
if ($array === 'GLOBALS') {
$globalVar = $this->parseGlobalsArrayDimFetch($inner);
$ref = $this->addTmpVar(self::TYPE_REF);
$this->context->beforeStmtLines[] = $ref . ' = ' . $globalVar . '.toReference();';
return '&' . $ref;
}
if (!$this->hasVar($array)) {
$this->fatalError($arg, 'Undefined variable `$' . $array . '`');
}
if ($inner->dim === null) {
$this->fatalError($arg, 'Array dimension must be a constant expression');
}
return $array . '.itemRef(' . $this->identifierToStr($inner->dim) . ')';
}
return null;
}
/**
* 仅用于动态调用的参数解析
*/
protected function parseArgRefVar(Node\Arg $arg, string $name): string
{
if (!$this->hasVar($name)) {
// 若参数是引用类型,可以传入未定义变量,将立即创建变量作为引用
$this->addLocalVar($name, self::TYPE_REF);
} else {
// 本地变量,且是原生类型,则转为普通变量
if ($this->hasLocalVar($name) and $this->isNativeType($this->getVarType($name))) {
$this->context->localVars[$name] = self::TYPE_VAR;
}
// 需要引用类型的参数,使用临时变量作为引用,并替换掉实际的参数
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, self::TYPE_REF);
$this->context->beforeStmtLines[] = $tmpVar . ' = ' . $this->parseExpr($arg->value) . '.toReference();';
$name = $tmpVar;
}
// 动态调用,参数列表是 Variant 类型而不是 Reference,必须使用 & 符号取地址,传递指针,以保持引用传递
return '&' . $name;
}
protected function parseArg(Node\Arg $arg): string
{
if ($this->isArrayDimFetch($arg->value) and $this->isStdContainerExpr($arg->value)) {
if ($this->isStdArrayExpr($arg->value)) {
$valueExpr = $this->parseStdArrayDimFetch($arg->value);
$attr = $arg->value->getAttribute('stdArrayDimFetch');
if ($attr['accessLevel'] === $attr['totalLevel']) {
return $this->convertExprFromType($this->context->stdArrays[$attr['var']]['type'], $valueExpr);
} else {
return $this->convertArrayExpr($valueExpr);
}
} else {
$valueExpr = $this->parseStdContainerDimFetch($arg->value);
$attr = $arg->value->getAttribute('stdContainerDimFetch');
return $this->convertExprFromType($this->context->stdContainers[$attr['var']]['type'], $valueExpr);
}
}
$expr = $this->parseIdentifier($arg->value);
if ($this->isVarExpr($arg->value) and $arg->value->name === 'GLOBALS') {
return 'php_globals_array()';
}
if ($this->isVarExpr($arg->value) and $this->isStdContainer($arg->value->name)) {
return $this->convertArrayExpr($expr . '_ref');
}
return $expr;
}
protected function parseOrderedArg(Node\Arg $arg): string
{
if ($this->isArrayDimFetch($arg->value) and $this->isStdContainerExpr($arg->value)) {
return $this->parseArg($arg);
}
if ($this->isVarExpr($arg->value) and $arg->value->name === 'GLOBALS') {
return 'php_globals_array()';
}
if ($this->isVarExpr($arg->value) and $this->isStdContainer($arg->value->name)) {
return $this->convertArrayExpr($this->parseIdentifier($arg->value) . '_ref');
}
return $this->parseOrderedOperand($arg->value, false);
}
protected function parseArrayArg(Node\Arg $expr): string
{
$value = $expr->value;
if ($this->isVarExpr($value)) {
$var = $this->parseIdentifier($value);
if (!$this->hasVar($var)) {
$this->errorUndefinedVariable($value);
}
if ($this->getVarType($var) === self::TYPE_ARRAY) {
return $var;
}
}
return $this->parseExpr($value);
}
}

@ -0,0 +1,72 @@
<?php
/**
* This file is part of TypePHP.
*
* Generates C++ helpers for defaults that require runtime initialization.
*/
namespace TypePhp\Generator;
use TypePhp\ArgInfo;
use TypePhp\Entity\ArrayInitPlan;
use TypePhp\Entity\FunctionDef;
trait DefaultArgumentGenerator
{
protected function getDefaultArgumentType(ArgInfo $argInfo): string
{
$type = $argInfo->type;
if ($type === self::TYPE_STREAM || $type === self::TYPE_BOX) {
return self::TYPE_VAR;
}
return $type;
}
protected function getDefaultArgumentHelperName(FunctionDef $func, ArgInfo $argInfo): string
{
return self::PREFIX . 'default_arg_' . $func->name . '_' . $argInfo->name;
}
protected function genDefaultArgumentExpr(FunctionDef $func, ArgInfo $argInfo): string
{
if (!$argInfo->arrayInitPlan || !$argInfo->arrayInitPlan->requiresRuntimeInit()) {
return $argInfo->default;
}
return $this->getDefaultArgumentHelperName($func, $argInfo) . '()';
}
protected function wrapArrayInitPlan(ArrayInitPlan $plan, string $body): string
{
return "do {\n" . $plan->init . $body . $plan->clean . "} while (0);\n";
}
protected function genDefaultArgumentHelpers(): string
{
$code = '';
foreach ($this->functions as $func) {
foreach ($func->argInfoList as $argInfo) {
$plan = $argInfo->arrayInitPlan;
if (!$plan || !$plan->requiresRuntimeInit()) {
continue;
}
$type = $this->getDefaultArgumentType($argInfo);
$helper = $this->getDefaultArgumentHelperName($func, $argInfo);
$code .= 'static inline ' . $type . ' ' . $helper . "() {\n";
$code .= $plan->init;
if ($plan->clean) {
$code .= $type . ' retval = ' . $plan->expr . ';' . PHP_EOL;
$code .= $plan->clean;
$code .= 'return retval;' . PHP_EOL;
} else {
$code .= 'return ' . $plan->expr . ';' . PHP_EOL;
}
$code .= '}' . PHP_EOL;
}
}
return $code ? $code . PHP_EOL : '';
}
}

@ -0,0 +1,134 @@
<?php
/**
* This file is part of TypePHP.
*
* Resolves pipe targets and ordinary function calls.
*/
namespace TypePhp\Parser;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\CallLike;
use PhpParser\Node\Expr\Variable;
use PhpParser\NodeAbstract;
use TypePhp\Constants;
use TypePhp\Exception\PlaceHolder;
trait FunctionCallTrait
{
protected function parsePipeOperator(Expr\BinaryOp\Pipe $expr): string
{
$this->assertExprCanBeUsedAsValue($expr->left, 'pipe left operand');
$this->assertExprCanBeUsedAsValue($expr->right, 'pipe callable');
[$leftExpr, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr->left);
$this->appendCapturedStmtLinesToContext($beforeStmts);
$value = $this->addTmpVar(self::TYPE_VAR);
$this->context->beforeStmtLines[] = $value . ' = ' . $leftExpr . ';';
$this->appendCapturedStmtLinesToContext($afterStmts);
$directCall = $this->parsePipeFirstClassCallable($expr->right, $value);
if ($directCall !== null) {
return $directCall;
}
$callable = $this->parseExprAsValue($expr->right);
return 'php::call(' . $callable . ', {' . $value . '})';
}
/**
* Lower a first-class callable used as a pipe target to its direct call.
*
* `trim(...)`, `ClassName::method(...)`, and `$object->method(...)` do
* not need a Closure when the pipe immediately invokes them. Reusing the
* ordinary call parsers preserves native-call optimization, argument
* validation, visibility checks, and the left-to-right evaluation order.
*/
protected function parsePipeFirstClassCallable(NodeAbstract $callable, string $value): ?string
{
if (!$callable instanceof CallLike || !$callable->isFirstClassCallable()) {
return null;
}
$directCall = clone $callable;
$directCall->args = [new Node\Arg(new Variable($value))];
if ($directCall instanceof Expr\FuncCall) {
return $this->parseFuncCall($directCall);
}
if ($directCall instanceof Expr\StaticCall) {
return $this->parseStaticCall($directCall);
}
if ($directCall instanceof Expr\MethodCall) {
return $this->parseMethodCall($directCall);
}
return null;
}
protected function parseFuncCall(Expr\FuncCall $expr): string
{
if ($this->isVarExpr($expr->name)) {
$fn = $this->parseIdentifier($expr->name);
$placeHolder = $fn;
$name = '';
} elseif ($expr->name->getType() === 'Name' or $expr->name->getType() === 'Name_FullyQualified') {
$name = $this->parseIdentifier($expr->name);
if (in_array($name, Constants::UNSUPPORTED_FUNCTIONS)) {
$this->fatalError($expr, 'Unsupported function: `' . $name . '`');
}
if ($name === 'any') {
if (count($expr->args) !== 1 || $expr->args[0]->unpack) {
$this->fatalError($expr, 'The any function expects exactly one non-unpacked argument');
}
return $this->parseExprAsValue($expr->args[0]->value);
}
if ($name === 'objval') {
return $this->genObjvalCall($expr);
}
$nativeFn = $this->findNativeFunction($name);
if ($nativeFn) {
$expr->setAttribute('nativeCall', $nativeFn);
// 函数调用占位符,不是真实的函数调用
if (count($expr->args) === 1 and $this->isPlaceholderExpr($expr->args[0])) {
return $this->genPlaceHolder($this->identifierToStr($expr->name));
}
$this->checkNativeCallArgs($expr, $this->getFunction($nativeFn), $expr->args, $name);
if ($this->shouldUseDynamicCallForNativeArgs($nativeFn, $expr->args)) {
$functionDef = $this->getFunction($nativeFn);
return $this->genRuntimeFunctionCall($this->getFuncPtr($functionDef->getNamespacedName()), $expr->args, $name);
}
try {
return self::PREFIX . $nativeFn . '(' . $this->parseNativeCallArgs($expr->args, $nativeFn) . ')';
} catch (PlaceHolder) {
return $this->genPlaceHolder($this->identifierToStr($expr->name));
}
}
// 动态调用的函数,转换函数名为带有命名空间的全限定名称
$name = $this->getNamespacedFuncName($name);
$this->checkInternalFunctionArgCount($name, $expr);
$code = $this->parseFuncCallWithOptimizer($name, $expr);
if ($code !== false) {
return $code;
}
$placeHolder = $this->identifierToStr($expr->name);
$fn = $this->getFuncPtr($name);
$this->context->beforeStmtLines[] = $this->formatCppLineComment('Func Call: ', $name . '()');
} else {
$tmpVar = $this->addTmpVar(self::TYPE_VAR);
$this->context->beforeStmtLines[] = $tmpVar . ' = ' . $this->parseExpr($expr->name) . ';';
$placeHolder = $fn = $tmpVar;
$name = '';
}
if (empty($expr->args)) {
return 'php::call(' . $fn . ')';
}
try {
return $this->genRuntimeFunctionCall($fn, $expr->args, $name);
} catch (PlaceHolder) {
return $this->genPlaceHolder($placeHolder);
}
}
}

@ -11,7 +11,6 @@ namespace TypePhp;
use MJS\TopSort\Implementations\StringSort;
use TypePhp\Analysis\SsaBuilder;
use TypePhp\Backend\CompilerFactory;
use TypePhp\Entity\ArrayInitPlan;
use TypePhp\Entity\ClassDef;
use TypePhp\Entity\ClassLikeDef;
use TypePhp\Entity\ConstantDef;
@ -24,6 +23,7 @@ use TypePhp\Exception\Skip;
use TypePhp\Exception\SyntaxError;
use TypePhp\Exception\Unsupported;
use TypePhp\Generator\ResourceFileGenerator;
use TypePhp\Generator\DefaultArgumentGenerator;
use TypePhp\Platform\PlatformFactory;
use TypePhp\Platform\Windows;
use PhpParser\Modifiers;
@ -38,6 +38,8 @@ use Symfony\Component\Yaml\Yaml;
class Translator extends Preprocessor
{
use DefaultArgumentGenerator;
public const string VERSION = '0.3.0';
public const string APP_NAME = 'TypePHP Compiler (AOT)';
protected string $targetName = 'app';
@ -63,61 +65,6 @@ class Translator extends Preprocessor
return $func->method && str_ends_with($func->name, self::NAMESPACE_SEPARATOR . '__construct');
}
protected function getDefaultArgumentType(ArgInfo $argInfo): string
{
$type = $argInfo->type;
if ($type === self::TYPE_STREAM || $type === self::TYPE_BOX) {
return self::TYPE_VAR;
}
return $type;
}
protected function getDefaultArgumentHelperName(FunctionDef $func, ArgInfo $argInfo): string
{
return self::PREFIX . 'default_arg_' . $func->name . '_' . $argInfo->name;
}
protected function genDefaultArgumentExpr(FunctionDef $func, ArgInfo $argInfo): string
{
if (!$argInfo->arrayInitPlan || !$argInfo->arrayInitPlan->requiresRuntimeInit()) {
return $argInfo->default;
}
return $this->getDefaultArgumentHelperName($func, $argInfo) . '()';
}
protected function wrapArrayInitPlan(ArrayInitPlan $plan, string $body): string
{
return "do {\n" . $plan->init . $body . $plan->clean . "} while (0);\n";
}
protected function genDefaultArgumentHelpers(): string
{
$code = '';
foreach ($this->functions as $func) {
foreach ($func->argInfoList as $argInfo) {
$plan = $argInfo->arrayInitPlan;
if (!$plan || !$plan->requiresRuntimeInit()) {
continue;
}
$type = $this->getDefaultArgumentType($argInfo);
$helper = $this->getDefaultArgumentHelperName($func, $argInfo);
$code .= 'static inline ' . $type . ' ' . $helper . "() {\n";
$code .= $plan->init;
if ($plan->clean) {
$code .= $type . ' retval = ' . $plan->expr . ';' . PHP_EOL;
$code .= $plan->clean;
$code .= 'return retval;' . PHP_EOL;
} else {
$code .= 'return ' . $plan->expr . ';' . PHP_EOL;
}
$code .= '}' . PHP_EOL;
}
}
return $code ? $code . PHP_EOL : '';
}
public function __construct(string $rootPath)
{

Loading…
Cancel
Save