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.
874 lines
37 KiB
874 lines
37 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\Node;
|
|
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,
|
|
bool $deferTrailingDefaults = false,
|
|
): string
|
|
{
|
|
$functionDef = $this->getFunction($nativeFunc);
|
|
$providedArgs = [];
|
|
$defaultArgs = [];
|
|
$sourceArgs = [];
|
|
$variadicArgCount = 0;
|
|
$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');
|
|
}
|
|
$providedArgs[$k] = true;
|
|
$sourceArgs[] = [$k, null, $arg];
|
|
} else {
|
|
if ($variadicArgIndex === null) {
|
|
$this->fatalError($arg, "Unknown named argument `{$argName}`");
|
|
}
|
|
$sourceArgs[] = [$variadicArgIndex, $argName, $arg];
|
|
$variadicArgCount++;
|
|
}
|
|
$hasNamedArg = true;
|
|
} elseif ($variadicArgIndex !== null and $i + $parameterOffset >= $variadicArgIndex) {
|
|
$sourceArgs[] = [$variadicArgIndex, null, $arg];
|
|
$variadicArgCount++;
|
|
} else {
|
|
$argIndex = $i + $parameterOffset;
|
|
$providedArgs[$argIndex] = true;
|
|
$sourceArgs[] = [$argIndex, null, $arg];
|
|
}
|
|
}
|
|
// Fill ABI holes first, but do not sort yet. User expressions must be
|
|
// lowered in source order; sorting raw AST arguments here would also
|
|
// reorder their side effects.
|
|
if ($hasNamedArg) {
|
|
$lastProvidedIndex = $providedArgs === []
|
|
? $parameterOffset - 1
|
|
: max(array_keys($providedArgs));
|
|
if ($deferTrailingDefaults && $variadicArgCount > 0) {
|
|
$lastProvidedIndex = $variadicArgIndex;
|
|
}
|
|
// 命名参数中间存在空洞,需要使用默认参数填充
|
|
foreach ($functionDef->argInfoList as $k => $argInfo) {
|
|
if ($k < $parameterOffset) {
|
|
continue;
|
|
}
|
|
if ($variadicArgIndex !== null and $k === $variadicArgIndex) {
|
|
continue;
|
|
}
|
|
if (!isset($providedArgs[$k])) {
|
|
// A Native virtual overload must omit a trailing default
|
|
// so the dynamically selected implementation supplies it.
|
|
// A named-argument hole before a later argument cannot be
|
|
// represented by a positional C++ overload without a
|
|
// presence mask, so reject that uncommon shape explicitly.
|
|
if ($deferTrailingDefaults && $k > $lastProvidedIndex) {
|
|
continue;
|
|
}
|
|
if ($deferTrailingDefaults) {
|
|
$this->fatalError(
|
|
reset($callArgs),
|
|
'Named calls to Native virtual methods cannot skip an earlier optional parameter',
|
|
);
|
|
}
|
|
if ($argInfo->default === '') {
|
|
$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');
|
|
}
|
|
// Defaults are resolved in the declaration scope. Re-parsing
|
|
// the original AST here would evaluate self/parent/private
|
|
// class constants in the caller's scope instead.
|
|
$defaultArgs[$k] = $this->genDefaultArgumentExpr($nativeFunc, $k);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 函数只接受一个变长参数,且调用参数为空,直接传入空数组
|
|
if (count($sourceArgs) === 0
|
|
and count($functionDef->argInfoList) === $parameterOffset + 1
|
|
and $functionDef->argInfoList[$parameterOffset]->variadic) {
|
|
return $deferTrailingDefaults ? '' : '{}';
|
|
}
|
|
|
|
$resolvedArgs = [];
|
|
$variadicVar = null;
|
|
$callableName = $functionDef->displayName ?: $functionDef->getNamespacedName();
|
|
|
|
// Evaluate every supplied argument in PHP source order. The resulting
|
|
// expressions/temporaries may then be rearranged safely for the native
|
|
// C++ ABI without changing observable call order.
|
|
foreach ($sourceArgs as [$argIndex, $variadicName, $arg]) {
|
|
if ($argIndex !== $variadicArgIndex) {
|
|
$argInfo = $this->getArgInfo($arg, $nativeFunc, $argIndex);
|
|
$resolvedArgs[$argIndex] = $this->getTypeConvertedArg(
|
|
$arg,
|
|
$argInfo,
|
|
$callableName,
|
|
$argIndex
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// A single unpacked native array is already the ABI value. Reuse
|
|
// it directly instead of allocating and merging a temporary array.
|
|
if ($variadicArgCount === 1 && $arg->unpack && $this->isVarExpr($arg->value)) {
|
|
$var = $this->parseIdentifier($arg->value);
|
|
if ($this->getVarType($var) === Type::ARRAY) {
|
|
$resolvedArgs[$variadicArgIndex] = $var;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$variadicVar ??= $this->addTmpVar(Type::ARRAY);
|
|
$argInfo = $functionDef->argInfoList[$variadicArgIndex];
|
|
if ($arg->unpack) {
|
|
$this->context->beforeStmtLines[] = $variadicVar . '.merge(' . $this->parseArrayArg($arg) . ');';
|
|
} elseif ($variadicName !== null) {
|
|
$value = $this->getTypeConvertedArg($arg, $argInfo, $callableName, $variadicArgIndex);
|
|
$this->context->beforeStmtLines[] = $variadicVar . '.set('
|
|
. $this->getLiteralString($variadicName) . ', ' . $value . ');';
|
|
} else {
|
|
$value = $this->getTypeConvertedArg($arg, $argInfo, $callableName, $variadicArgIndex);
|
|
$this->context->beforeStmtLines[] = $variadicVar . '.append(' . $value . ');';
|
|
}
|
|
}
|
|
|
|
// Defaults have no caller-side evaluation. Add them after user
|
|
// arguments, then sort only the already-lowered values for the ABI.
|
|
foreach ($defaultArgs as $i => $defaultArg) {
|
|
$resolvedArgs[$i] = $defaultArg;
|
|
}
|
|
if ($variadicVar !== null) {
|
|
$resolvedArgs[$variadicArgIndex] = $variadicVar;
|
|
}
|
|
ksort($resolvedArgs);
|
|
return implode(', ', $resolvedArgs);
|
|
}
|
|
|
|
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,
|
|
bool $preserveExistingReferences = 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) . ');';
|
|
if ($arg->getAttribute(self::ATTR_SCOPED_CALLBACK) === 'normalize-unpacked') {
|
|
$this->context->beforeStmtLines[] = 'php::normalizeCallableClass('
|
|
. $callArgs . ', 0, ' . $this->getCallableScopeExpr() . ');';
|
|
}
|
|
} 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))
|
|
|| ($preserveExistingReferences && $this->isExistingReferenceCallArg($arg));
|
|
if ($byRef) {
|
|
$this->assertReadonlyPropertyReferenceForbidden($arg->value, $arg, false);
|
|
}
|
|
$value = ($byRef || $this->isRefvalCall($arg->value) || $this->isToRefCall($arg->value))
|
|
? $this->parseReferenceCallArgValue($arg)
|
|
: $this->parseCallArgValue($arg);
|
|
$value = $this->wrapScopedCallbackArg($arg, $value);
|
|
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))
|
|
|| ($preserveExistingReferences && $this->isExistingReferenceCallArg($arg));
|
|
if ($byRef) {
|
|
$this->assertReadonlyPropertyReferenceForbidden($arg->value, $arg, false);
|
|
}
|
|
$scopedCallback = $arg->getAttribute(self::ATTR_SCOPED_CALLBACK);
|
|
if ($scopedCallback !== null) {
|
|
if ($this->isVarExpr($arg->value)) {
|
|
$name = $this->parseIdentifier($arg->value);
|
|
if (!$this->hasVar($name)) {
|
|
$this->fatalError($arg, 'Undefined variable `$' . $name . '`');
|
|
}
|
|
}
|
|
$value = $this->wrapScopedCallbackArg($arg, $this->parseCallArgValue($arg));
|
|
$this->addPositionalCallArg($value, $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;
|
|
}
|
|
|
|
private function isExistingReferenceCallArg(Node\Arg $arg): bool
|
|
{
|
|
if (!$this->isVarExpr($arg->value)) {
|
|
return $this->isReferenceWrapperCall($arg->value);
|
|
}
|
|
$name = $this->parseIdentifier($arg->value);
|
|
return $this->hasVar($name) && $this->getVarType($name) === Type::REF;
|
|
}
|
|
|
|
protected function wrapScopedCallbackArg(Node\Arg $arg, string $value): string
|
|
{
|
|
$mode = $arg->getAttribute(self::ATTR_SCOPED_CALLBACK);
|
|
if ($mode === null || !$this->methodDef) {
|
|
return $value;
|
|
}
|
|
|
|
$helper = match ($mode) {
|
|
'normalize' => 'normalizeCallableClass',
|
|
default => 'prepareScopedCallback',
|
|
};
|
|
return 'php::' . $helper . '(' . $value . ', ' . $this->getCallableScopeExpr() . ')';
|
|
}
|
|
|
|
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');
|
|
if ($this->isVarExpr($arg->value)) {
|
|
$this->assertStdContainerDoesNotEscapeNativeObjects(
|
|
$arg,
|
|
$this->parseIdentifier($arg->value),
|
|
);
|
|
}
|
|
$class = $this->detectClassOfExpr($arg->value);
|
|
if ($class !== '' && $this->isNativeObjectClass($class)) {
|
|
$this->fatalError(
|
|
$arg,
|
|
'Native objects cannot cross a dynamic PHP/ZendVM call boundary'
|
|
);
|
|
}
|
|
// C++17 evaluates php::ArgList{...} elements from left to right, but a
|
|
// later argument may emit captured beforeStmtLines while being lowered.
|
|
// Those statements are placed before the whole outer call and would
|
|
// overtake an earlier Call left inside the initializer list. Complete
|
|
// each direct Call in a temporary before lowering the next argument.
|
|
$expr = $arg->value instanceof Expr\FuncCall
|
|
|| $arg->value instanceof Expr\MethodCall
|
|
|| $arg->value instanceof Expr\StaticCall
|
|
? $this->parseOrderedArg($arg)
|
|
: $this->parseArg($arg);
|
|
return $this->materializeCallArgValue($arg->value, $expr);
|
|
}
|
|
|
|
protected function materializeCallArgValue(NodeAbstract $value, string $expr): string
|
|
{
|
|
// A Native property fetch is a typed C++ pointer, never an INDIRECT
|
|
// zval. Passing it through php_deindirect() would box the pointer as a
|
|
// bool/Variant and break the Native ABI. Dynamic Zend calls reject the
|
|
// value before reaching here; direct Native calls keep it unchanged.
|
|
if ($this->isNativeObjectClass($this->detectClassOfExpr($value))) {
|
|
return $expr;
|
|
}
|
|
// 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);
|
|
}
|
|
|
|
$this->assertNativeObjectReferenceForbidden($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);
|
|
} elseif ($this->getVarType($name) === Type::REF) {
|
|
return '&' . $name;
|
|
} 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);
|
|
}
|
|
|
|
}
|
|
|