- 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 withpull/17/head
parent
eeca2e598b
commit
a9e93eda28
5 changed files with 995 additions and 930 deletions
@ -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); |
||||
} |
||||
} |
||||
} |
||||
|
||||
Loading…
Reference in new issue