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