TypePHP 编译器
https://swoole.com/aot/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
791 lines
32 KiB
791 lines
32 KiB
<?php
|
|
/**
|
|
* This file is part of TypePHP.
|
|
*
|
|
* Call argument lowering shared by native and dynamic call paths.
|
|
*/
|
|
|
|
namespace TypePhp\Generator;
|
|
|
|
use TypePhp\Type;
|
|
|
|
use PhpParser\Modifiers;
|
|
use PhpParser\Node;
|
|
use PhpParser\Node\ArrayItem;
|
|
use PhpParser\Node\Expr;
|
|
use PhpParser\NodeAbstract;
|
|
use TypePhp\Entity\ArgInfo;
|
|
use TypePhp\Entity\FunctionDef;
|
|
use TypePhp\Exception\PlaceHolder;
|
|
use TypePhp\Resolver\Reflection;
|
|
use TypePhp\Generator\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) === Type::ARRAY) {
|
|
return $var;
|
|
}
|
|
}
|
|
return $this->convertArrayExpr($this->parseExpr($arg->value));
|
|
}
|
|
|
|
$tmpVar = $this->addTmpVar(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 = 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(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, 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[] = 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[] = Type::ARRAY . ' ' . $arrayArgsVar . '{' . implode(', ', $listArgs) . '};';
|
|
$listArgs = [];
|
|
}
|
|
return $arrayArgsVar;
|
|
}
|
|
|
|
protected function ensureCallNamedArgs(?string &$namedArgsVar): string
|
|
{
|
|
if ($namedArgsVar === null) {
|
|
$namedArgsVar = $this->genTmpVarName();
|
|
$this->context->beforeStmtLines[] = 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
|
|
{
|
|
// A call that returns by reference yields a live php::Ref aliasing the
|
|
// callee's storage. When such a call feeds a by-value argument, PHP takes
|
|
// a value snapshot at evaluation time (left to right), so later mutations
|
|
// to the aliased storage must not be observable. The dynamic ArgList keeps
|
|
// references verbatim (Ctor::CopyRef), so we dereference into a temporary
|
|
// value at the point of the call.
|
|
$expr = $this->materializeRefReturnAsValue($value, $expr);
|
|
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(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, 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(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, Type::REF);
|
|
} else {
|
|
// 本地变量,且是原生类型,则转为普通变量
|
|
if ($this->hasLocalVar($name) and $this->isNativeType($this->getVarType($name))) {
|
|
$this->context->localVars[$name] = Type::VAR;
|
|
}
|
|
// 需要引用类型的参数,使用临时变量作为引用,并替换掉实际的参数
|
|
$tmpVar = $this->genTmpVarName();
|
|
$this->addLocalVar($tmpVar, 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) === Type::ARRAY) {
|
|
return $var;
|
|
}
|
|
}
|
|
return $this->parseExpr($value);
|
|
}
|
|
|
|
}
|
|
|
|
|