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.
4197 lines
164 KiB
4197 lines
164 KiB
<?php
|
|
/**
|
|
* This file is part of TypePHP.
|
|
*
|
|
* @link https://www.swoole.com/
|
|
* @contact service@swoole.com
|
|
*/
|
|
|
|
namespace TypePhp;
|
|
|
|
use TypePhp\Build\PhpxLocator;
|
|
|
|
use League\CLImate\CLImate;
|
|
use TypePhp\Backend\CompilerBackend;
|
|
use TypePhp\Backend\CompilerFactory;
|
|
use TypePhp\Build\NativeBuildConfigurationTrait;
|
|
use TypePhp\Entity\ArgInfo;
|
|
use TypePhp\Context\FunctionContext;
|
|
use TypePhp\Context\CompilationStateTrait;
|
|
use TypePhp\Diagnostics\CompilerDiagnosticTrait;
|
|
use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic;
|
|
use TypePhp\Diagnostics\CliDiagnosticReporter;
|
|
use TypePhp\Diagnostics\DiagnosticReporter;
|
|
use TypePhp\Diagnostics\ThrowingDiagnosticReporter;
|
|
use TypePhp\Entity\ClassDef;
|
|
use TypePhp\Entity\ConstantDef;
|
|
use TypePhp\Entity\FunctionDef;
|
|
use TypePhp\Entity\InterfaceDef;
|
|
use TypePhp\Entity\MethodDef;
|
|
use TypePhp\Entity\PropertyDef;
|
|
use TypePhp\Exception\DynamicCall;
|
|
use TypePhp\Exception\Redo;
|
|
use TypePhp\Generator\AnonClassGenerator;
|
|
use TypePhp\Generator\CallArgumentGenerator;
|
|
use TypePhp\Generator\ClosureGenerator;
|
|
use TypePhp\Generator\FiberGenerator;
|
|
use TypePhp\Generator\PlaceHolderGenerator;
|
|
use TypePhp\Generator\PropertyPromotion;
|
|
use TypePhp\Generator\Symbol;
|
|
use TypePhp\Generator\Utils;
|
|
use TypePhp\Generator\TypeCheckGenerator;
|
|
use TypePhp\Optimizer\SsaPropOptimizer;
|
|
use TypePhp\Optimizer\SsaTypeOptimizer;
|
|
use TypePhp\Optimizer\LoopVarOptimizer;
|
|
use TypePhp\Parser\StdContainerTrait;
|
|
use TypePhp\Parser\AssignOpTrait;
|
|
use TypePhp\Parser\ArrayExpressionTrait;
|
|
use TypePhp\Parser\AstNodeType;
|
|
use TypePhp\Parser\BinaryOpTrait;
|
|
use TypePhp\Parser\ClassConstantFetchTrait;
|
|
use TypePhp\Parser\ConditionalControlTrait;
|
|
use TypePhp\Parser\ConstantExpressionTrait;
|
|
use TypePhp\Parser\ExceptionControlFlowTrait;
|
|
use TypePhp\Parser\ForeachTrait;
|
|
use TypePhp\Parser\FunctionCallTrait;
|
|
use TypePhp\Parser\LoopControlTrait;
|
|
use TypePhp\Parser\MethodCallTrait;
|
|
use TypePhp\Parser\NullsafeAccessTrait;
|
|
use TypePhp\Parser\PropertyAccessTrait;
|
|
use TypePhp\Parser\SelectionExpressionTrait;
|
|
use TypePhp\Parser\SwitchTrait;
|
|
use TypePhp\Parser\TypeConversionTrait;
|
|
use TypePhp\Parser\TypeDetectionTrait;
|
|
use TypePhp\Parser\UnaryExpressionTrait;
|
|
use TypePhp\Parser\UniversalMethodCall;
|
|
use TypePhp\Optimizer\FuncCallOptimizer;
|
|
use TypePhp\Platform\Linux;
|
|
use TypePhp\Platform\Macos;
|
|
use TypePhp\Platform\PlatformBase;
|
|
use TypePhp\Platform\PlatformFactory;
|
|
use TypePhp\Platform\Windows;
|
|
use TypePhp\Resolver\DeclarationSymbolTrait;
|
|
use TypePhp\Resolver\MagicMethodDetector;
|
|
use TypePhp\Resolver\PropertyAccessContext;
|
|
use TypePhp\Resolver\NativePropertyAccess;
|
|
use TypePhp\Resolver\NameResolutionTrait;
|
|
use TypePhp\Resolver\PropertyAccessResult;
|
|
use TypePhp\Resolver\PropertyAccessResolver;
|
|
use TypePhp\Resolver\Reflection;
|
|
use TypePhp\Symbol\SymbolRepository;
|
|
use TypePhp\TypeSystem\CompositeTypeCheckerTrait;
|
|
use TypePhp\TypeSystem\NativeTypeCompatibilityTrait;
|
|
use PhpParser\Modifiers;
|
|
use PhpParser\Node;
|
|
use PhpParser\Node\ArrayItem;
|
|
use PhpParser\Node\Expr;
|
|
use PhpParser\Node\Expr\CallLike;
|
|
use PhpParser\Node\Expr\Variable;
|
|
use PhpParser\Node\FunctionLike;
|
|
use PhpParser\NodeAbstract;
|
|
use PhpParser\NodeFinder;
|
|
use PhpParser\Parser;
|
|
use PhpParser\ParserFactory;
|
|
use PhpParser\PhpVersion;
|
|
use PhpParser\PrettyPrinter;
|
|
|
|
class CompilerBase implements PropertyAccessContext
|
|
{
|
|
use CompositeTypeCheckerTrait;
|
|
use CompilerDiagnosticTrait;
|
|
use CompilationStateTrait;
|
|
use NativeTypeCompatibilityTrait;
|
|
use NativeBuildConfigurationTrait;
|
|
use DeclarationSymbolTrait;
|
|
use NameResolutionTrait;
|
|
use AstNodeType;
|
|
use FuncCallOptimizer;
|
|
use AnonClassGenerator;
|
|
use CallArgumentGenerator;
|
|
use ClosureGenerator;
|
|
use FiberGenerator;
|
|
use PlaceHolderGenerator;
|
|
use PropertyPromotion;
|
|
use MagicMethodDetector;
|
|
use StdContainerTrait;
|
|
use BinaryOpTrait;
|
|
use ClassConstantFetchTrait;
|
|
use ConditionalControlTrait;
|
|
use ConstantExpressionTrait;
|
|
use ExceptionControlFlowTrait;
|
|
use ForeachTrait;
|
|
use FunctionCallTrait;
|
|
use LoopControlTrait;
|
|
use MethodCallTrait;
|
|
use NullsafeAccessTrait;
|
|
use PropertyAccessTrait;
|
|
use SelectionExpressionTrait;
|
|
use SwitchTrait;
|
|
use TypeConversionTrait;
|
|
use TypeDetectionTrait;
|
|
use UnaryExpressionTrait;
|
|
use AssignOpTrait;
|
|
use ArrayExpressionTrait;
|
|
use UniversalMethodCall;
|
|
use Utils;
|
|
use TypeCheckGenerator;
|
|
use SsaTypeOptimizer;
|
|
use LoopVarOptimizer;
|
|
use SsaPropOptimizer;
|
|
|
|
public const string DEFAULT_PHP_VERSION = '8.5';
|
|
protected const string NATIVE_PROPERTY_VALUE_VAR = 'var';
|
|
protected const string NATIVE_PROPERTY_VALUE_DYNAMIC = 'dynamic';
|
|
protected const int COMPOSITE_TYPE_MISMATCH = -1;
|
|
protected const int COMPOSITE_TYPE_UNKNOWN = 0;
|
|
protected const int COMPOSITE_TYPE_MATCH = 1;
|
|
protected const string ATTR_ARRAY_DIM_FETCH_UPDATE = 'aotArrayDimFetchUpdate';
|
|
protected const string ATTR_PROPERTY_FETCH_UPDATE = 'aotPropertyFetchUpdate';
|
|
protected const string ATTR_STATEMENT_EXPRESSION = 'aotStatementExpression';
|
|
protected const string ATTR_MULTI_RETURN_IMPL = 'aotMultiReturnImpl';
|
|
|
|
/**
|
|
* Keyword methods (to* builtins) with mandated return types.
|
|
* Use findKeywordMethod() for unified lookup including keyword extension methods.
|
|
*/
|
|
public const array KEYWORD_METHOD_MAP = [
|
|
'toInt' => Type::INT,
|
|
'toFloat' => Type::FLOAT,
|
|
'toString' => Type::STR,
|
|
'toBool' => Type::BOOL,
|
|
'toArray' => Type::ARRAY,
|
|
'toStream' => Type::STREAM,
|
|
'toBigInt' => Type::BIGINT,
|
|
'toBigFloat' => Type::BIGFLOAT,
|
|
'toDecimal' => Type::DECIMAL,
|
|
'toObject' => Type::OBJECT,
|
|
'toAny' => Type::VAR,
|
|
'toRef' => Type::REF,
|
|
];
|
|
|
|
private const array STREAM_FUNCTIONS = [
|
|
'fopen',
|
|
'tmpfile',
|
|
'fsockopen',
|
|
'stream_socket_client',
|
|
'stream_socket_accept',
|
|
'popen',
|
|
];
|
|
|
|
/**
|
|
* APIs which cannot have the same semantics in Wasmtime and a browser.
|
|
* Keep this list at the language boundary so a WASI build never degrades
|
|
* into a link error or a browser-only implementation.
|
|
*/
|
|
private const array WASI_UNSUPPORTED_FUNCTIONS = [
|
|
'exec',
|
|
'passthru',
|
|
'popen',
|
|
'proc_close',
|
|
'proc_get_status',
|
|
'proc_nice',
|
|
'proc_open',
|
|
'proc_terminate',
|
|
'shell_exec',
|
|
'system',
|
|
'fsockopen',
|
|
'pfsockopen',
|
|
'stream_socket_accept',
|
|
'stream_socket_client',
|
|
'stream_socket_enable_crypto',
|
|
'stream_socket_get_name',
|
|
'stream_socket_pair',
|
|
'stream_socket_recvfrom',
|
|
'stream_socket_sendto',
|
|
'stream_socket_server',
|
|
'stream_socket_shutdown',
|
|
];
|
|
|
|
private const array WASI_UNSUPPORTED_FUNCTION_PREFIXES = [
|
|
'pcntl_',
|
|
'posix_',
|
|
'socket_',
|
|
];
|
|
public const int DECL_TYPE_OF_RETURN = 1;
|
|
public const int DECL_TYPE_OF_PROPERTY = 2;
|
|
public const int DECL_TYPE_OF_CONST = 3;
|
|
public const int DECL_TYPE_OF_PARAM = 4;
|
|
|
|
public const string VALUE_NAN = 'std::numeric_limits<double>::quiet_NaN()';
|
|
public const string VALUE_INF = 'std::numeric_limits<double>::infinity()';
|
|
public const string VALUE_NULL = 'php::null';
|
|
public const string VALUE_ZERO = 'php::zero';
|
|
public const string VALUE_FALSE = 'php::false_';
|
|
public const string VALUE_TRUE = 'php::true_';
|
|
public const string LITERAL_STRINGS = '_literal_strings';
|
|
public const string ANON_CLASS = '_anon_class_';
|
|
public const string DYNAMIC_CALLED_CLASS = '__dynamic_called_class__';
|
|
public const string STATIC_VAR = '_static_var_';
|
|
public const string GLOBAL_VAR = '_global_var_';
|
|
public const string CONST_VAR = '_const_var_';
|
|
public const string OBJECT_PROP = '_object_prop_';
|
|
public const string CLASS_MAP = 'class_map';
|
|
public const string FUNC_MAP = 'func_map';
|
|
public const string PROP_MAP = 'property_map';
|
|
public const string NAMESPACE_SEPARATOR = '__';
|
|
|
|
public const string PREFIX = 'php_';
|
|
protected const string MULTI_RETURN_NAMESPACE = 'typephp::detail';
|
|
public const string OP_ISSET = 'isset';
|
|
public const string OP_EMPTY = 'empty';
|
|
public const string OP_NOT_EMPTY = 'notEmpty';
|
|
public const string OP_REFVAL = 'toReference';
|
|
public const string OP_NOP = "if (0) {}\n";
|
|
public const string BUILD_MODE_BIN = 'bin';
|
|
public const string BUILD_MODE_EXT = 'ext';
|
|
public const string BUILD_MODE_LIB = 'lib';
|
|
public const string ENTRY_FUNCTION = 'main';
|
|
public const string PHPX_VENDOR_DIR = '/vendor/swoole/phpx';
|
|
protected const string PHASE_IDLE = 'idle';
|
|
protected const string PHASE_PREPARE = 'prepare';
|
|
protected const string PHASE_CONVERT = 'convert';
|
|
|
|
protected string $lang = 'PHP';
|
|
protected int $indentLevel = 0;
|
|
protected string $indentStr = "\t";
|
|
public string $mode = 'cli';
|
|
protected string $osType = 'linux';
|
|
protected string $compilerPhase = self::PHASE_IDLE;
|
|
protected string $cppCompiler = '';
|
|
protected array $literalStrings = [];
|
|
protected int $literalStringIndex = 0;
|
|
protected int $anonClassIndex = 0;
|
|
protected int $classIndex = 0;
|
|
|
|
/**
|
|
* @var array<string, int>
|
|
*/
|
|
protected array $classMap = [];
|
|
/**
|
|
* @var array<string, int>
|
|
*/
|
|
protected array $stdTypeMap = [];
|
|
protected int $funcIndex = 0;
|
|
|
|
/**
|
|
* @var array<string, int>
|
|
*/
|
|
protected array $funcMap = [];
|
|
protected int $propIndex = 0;
|
|
protected array $propMap = [];
|
|
protected const array PHP_RUNTIME_TYPE_MAP = [
|
|
'integer' => Type::INT,
|
|
'double' => Type::FLOAT,
|
|
'boolean' => Type::BOOL,
|
|
];
|
|
protected array $zendTypeMap = [
|
|
'int' => Type::INT,
|
|
'float' => Type::FLOAT,
|
|
'bool' => Type::BOOL,
|
|
'false' => Type::BOOL,
|
|
'true' => Type::BOOL,
|
|
'void' => Type::VOID,
|
|
'never' => Type::VOID,
|
|
'string' => Type::STR,
|
|
'array' => Type::ARRAY,
|
|
'object' => Type::OBJECT,
|
|
'mixed' => Type::VAR,
|
|
'null' => Type::VAR,
|
|
'any' => Type::VAR,
|
|
// callable 类型,可以是字符串、数组、对象
|
|
// 1) 'foo' 函数名称字符串, 2) [ $obj, 'bar' ] 对象方法数组, 3) Closure 对象, 4) [ 'class', 'staticMethod'] 类名+静态方法数组
|
|
'callable' => Type::VAR,
|
|
// iterable 类型,可以是数组或者对象
|
|
'iterable' => Type::VAR,
|
|
'stream' => Type::STREAM,
|
|
'bigint' => Type::BIGINT,
|
|
'bigfloat' => Type::BIGFLOAT,
|
|
'decimal' => Type::DECIMAL,
|
|
'box' => Type::BOX,
|
|
];
|
|
protected array $localHeaders = [];
|
|
protected array $internalFunctions = [];
|
|
protected array $internalConstants = [];
|
|
|
|
/**
|
|
* 存储所有函数、类方法的声明,key 是 符号名称,Value 是函数、类方法所在的文件名称
|
|
* @var array<string, string>
|
|
*/
|
|
protected array $symbolDeclInFile = [];
|
|
|
|
/**
|
|
* 存储所有函数、类方法的调用,key 是 文件名称,Value 是函数、类方法调用的列表数组
|
|
* @var array<string, array<string>>
|
|
*/
|
|
protected array $symbolCallInFile = [];
|
|
protected array $redoAfterDeclare = [];
|
|
protected array $constData = [];
|
|
protected int $optimizeLevel = 0;
|
|
protected int $maxJob = 4;
|
|
protected string $buildMode = self::BUILD_MODE_BIN;
|
|
protected string $cxxFlags = '';
|
|
protected string $cxxStd = 'c++17';
|
|
protected string $march = ''; // --march: target CPU instruction set (e.g. native, x86-64-v3)
|
|
protected string $targetPlatform = ''; // --target-platform: cross-compilation target triple (e.g. aarch64-linux-gnu)
|
|
protected string $ldflags = '';
|
|
protected array $linkLibs = []; // --link-lib / -l: user-specified libraries to link
|
|
protected array $linkPaths = []; // --link-path / -L: user-specified library search paths
|
|
protected int $floatPrecision = 17;
|
|
protected bool $debug = false;
|
|
protected bool $formatCode = false; // --format: enable clang-format (disabled by default)
|
|
protected bool $printBacktraceOnError = true;
|
|
protected bool $noLiteralStrings = false;
|
|
protected bool $noConsole = false; // Windows: hide console window
|
|
protected string $sanitize = ''; // Sanitizer type (address, undefined, etc.)
|
|
protected bool $dryRun = false; // Dry run: only generate C++ code, skip compile & link
|
|
protected array $userIncludePaths = []; // --include-path / -I: user-provided C++ include dirs
|
|
protected array $userDefines = []; // --define / -D: user-provided preprocessor macros
|
|
protected bool $enableLto = false; // --lto: enable Link Time Optimization (-flto)
|
|
protected string $file;
|
|
protected string $dir;
|
|
|
|
/**
|
|
* 原始值,可能包含 `\\` 多层空间.
|
|
*/
|
|
protected string $namespace = '';
|
|
protected string $method = '';
|
|
protected string $function = '';
|
|
protected array $useNamespaces = [];
|
|
protected array $useAliases = [];
|
|
protected array $useFunctions = [];
|
|
protected array $useConstants = [];
|
|
|
|
/**
|
|
* 原始类名,不包含命名空间.
|
|
*/
|
|
protected string $class = '';
|
|
protected string $parentClass = '';
|
|
protected string $interface = '';
|
|
/**
|
|
* @var array<string, ConstantDef>
|
|
*/
|
|
protected array $constants = [];
|
|
/**
|
|
* @var array<string, ClassDef>
|
|
*/
|
|
protected array $classesDefineInFile = [];
|
|
/**
|
|
* @var array<string, InterfaceDef>
|
|
*/
|
|
protected array $interfacesDefineInFile = [];
|
|
/**
|
|
* @var array<string, FunctionDef>
|
|
*/
|
|
protected array $functionDefineInFile = [];
|
|
|
|
protected ?FunctionDef $functionDef = null;
|
|
protected ?ClassDef $classDef = null;
|
|
protected ?MethodDef $methodDef = null;
|
|
protected ?InterfaceDef $interfaceDef = null;
|
|
protected bool $inGeneratorBody = false;
|
|
private ?DiagnosticReporter $diagnosticReporter = null;
|
|
protected FunctionContext $context;
|
|
protected array $superGlobalVars = [
|
|
'_GET' => Type::ARRAY,
|
|
'_POST' => Type::ARRAY,
|
|
'_COOKIE' => Type::ARRAY,
|
|
'_SERVER' => Type::ARRAY,
|
|
'_FILES' => Type::ARRAY,
|
|
'_SESSION' => Type::ARRAY,
|
|
'_REQUEST' => Type::ARRAY,
|
|
'_ENV' => Type::ARRAY,
|
|
'GLOBALS' => Type::ARRAY,
|
|
];
|
|
protected array $globalVars = [];
|
|
protected bool $nativeTypes = false;
|
|
protected bool $decimalTypes = false;
|
|
protected bool $bigintTypes = false;
|
|
protected string $rootPath;
|
|
protected string $buildDir;
|
|
protected string $outputDir = ''; // -o 参数指定的输出目录
|
|
protected int $debugLine = 0;
|
|
protected CLImate $climate;
|
|
protected bool $stubFile = false;
|
|
protected string $stubImportLibrary = '';
|
|
|
|
/** @var array<string, true> */
|
|
protected array $externalImportStubFiles = [];
|
|
protected bool $enableProfiler = false;
|
|
protected bool $noProgress = false;
|
|
protected bool $forTest = false;
|
|
protected Parser $parser;
|
|
protected string $phpVersion = self::DEFAULT_PHP_VERSION;
|
|
protected PrettyPrinter $printer;
|
|
protected bool $isPhpZts = false; // PHP 是否为线程安全版本
|
|
|
|
// Windows 平台:保存检测到的 PHP lib 文件路径
|
|
protected string $windowsPhpEmbedLib = ''; // php8embed.lib 路径
|
|
protected string $windowsPhpCoreLib = ''; // php8ts.lib 或 php8.lib 路径
|
|
|
|
// 新的平台和编译器抽象层(可选使用)
|
|
protected ?PlatformBase $platform = null;
|
|
protected ?CompilerBackend $compilerBackend = null;
|
|
|
|
/**
|
|
* 在预处理阶段获取所有类的方法名称,检测子类和父类中存在的同名方法,解决动态绑定方法调用的问题
|
|
* `static::methodCall()`
|
|
* `$this->methodCall()` 子类和父类中存在同名方法
|
|
* @var array<string, bool>
|
|
*/
|
|
protected array $classMethodOverride = [];
|
|
|
|
/**
|
|
* 存储所有类继承关系,类名必须全部为小写
|
|
* @var array<string, string>
|
|
*/
|
|
protected SymbolRepository $symbols;
|
|
|
|
/**
|
|
* Reverse class hierarchy: parent class (lowercase) => list of child classes (lowercase)
|
|
* @var array<string, string[]>
|
|
*/
|
|
protected array $classSubClasses = [];
|
|
|
|
public function __construct(string $rootPath)
|
|
{
|
|
$this->osType = PHP_OS_FAMILY;
|
|
if (version_compare(PHP_VERSION, '8.2.0', '<')) {
|
|
$this->error('PHP 8.2.0 or later is required');
|
|
}
|
|
if (version_compare(PHP_VERSION, '8.6.0', '>=')) {
|
|
$this->error('PHP 8.6.0 or later is not supported');
|
|
}
|
|
$this->rootPath = $rootPath;
|
|
$this->symbols = new SymbolRepository();
|
|
$this->setPhpVersion(self::DEFAULT_PHP_VERSION);
|
|
$this->printer = new PrettyPrinter\Standard();
|
|
$this->setBuildDir($rootPath . '/build');
|
|
$climate = new CLImate();
|
|
$this->climate = $climate;
|
|
}
|
|
|
|
public function setDiagnosticReporter(DiagnosticReporter $reporter): void
|
|
{
|
|
$this->diagnosticReporter = $reporter;
|
|
}
|
|
|
|
protected function getDiagnosticReporter(): DiagnosticReporter
|
|
{
|
|
if ($this->diagnosticReporter !== null) {
|
|
return $this->diagnosticReporter;
|
|
}
|
|
return $this->forTest
|
|
? new ThrowingDiagnosticReporter()
|
|
: new CliDiagnosticReporter($this->climate, $this->printBacktraceOnError);
|
|
}
|
|
|
|
public function setMode($mode): void
|
|
{
|
|
$this->mode = $mode;
|
|
}
|
|
|
|
/** Set the PHP language version accepted by the parser. */
|
|
public function setPhpVersion(string $version): void
|
|
{
|
|
if (!preg_match('/^8\.(2|3|4|5)(?:\.0)?$/', $version, $matches)) {
|
|
$this->error('Unsupported PHP language version: `' . $version . '`. Supported versions: 8.2, 8.3, 8.4, 8.5');
|
|
}
|
|
|
|
$this->phpVersion = '8.' . $matches[1] . '.0';
|
|
// php-parser's emulative lexer permits the compiler runtime to be
|
|
// older than the selected PHP language version.
|
|
$this->parser = (new ParserFactory())->createForVersion(PhpVersion::fromString($this->phpVersion));
|
|
}
|
|
|
|
public function getPhpVersion(): string
|
|
{
|
|
return $this->phpVersion;
|
|
}
|
|
|
|
public function setIndent(string $indent): void
|
|
{
|
|
$this->indentStr = $indent;
|
|
}
|
|
|
|
public function setIndentLevel(int $level): void
|
|
{
|
|
$this->indentLevel = $level;
|
|
}
|
|
|
|
public function getLang(): string
|
|
{
|
|
return $this->lang;
|
|
}
|
|
|
|
protected function getIndent(): string
|
|
{
|
|
return str_repeat($this->indentStr, $this->indentLevel);
|
|
}
|
|
|
|
protected function getPhpxDir(): string
|
|
{
|
|
try {
|
|
return PhpxLocator::resolve($this->rootPath);
|
|
} catch (\RuntimeException $exception) {
|
|
$this->error($exception->getMessage());
|
|
}
|
|
}
|
|
|
|
protected function getPlatform(): PlatformBase
|
|
{
|
|
if ($this->platform === null) {
|
|
$this->platform = PlatformFactory::create();
|
|
}
|
|
|
|
return $this->platform;
|
|
}
|
|
|
|
protected function getCompilerBackend(): CompilerBackend
|
|
{
|
|
if ($this->compilerBackend === null) {
|
|
$this->cppCompiler = CompilerFactory::detectCompilerName($this->getPlatform(), $this->cppCompiler);
|
|
$this->compilerBackend = CompilerFactory::createByName($this->cppCompiler, $this->getPlatform());
|
|
}
|
|
|
|
return $this->compilerBackend;
|
|
}
|
|
|
|
public function isWindows(): bool
|
|
{
|
|
return $this->getPlatform() instanceof Windows;
|
|
}
|
|
|
|
public function isLinux(): bool
|
|
{
|
|
return $this->getPlatform() instanceof Linux;
|
|
}
|
|
|
|
public function isMacos(): bool
|
|
{
|
|
return $this->getPlatform() instanceof Macos;
|
|
}
|
|
|
|
public function isWasiTarget(): bool
|
|
{
|
|
$target = strtolower($this->targetPlatform);
|
|
return $target === 'wasm32-unknown-wasip2' || $target === 'wasm32-wasip2';
|
|
}
|
|
|
|
protected function assertWasiFunctionSupported(NodeAbstract $expr, string $name): void
|
|
{
|
|
if (!$this->isWasiTarget()) {
|
|
return;
|
|
}
|
|
|
|
$name = strtolower(ltrim($name, '\\'));
|
|
if (in_array($name, self::WASI_UNSUPPORTED_FUNCTIONS, true)) {
|
|
$this->fatalError($expr, "Function `{$name}` is not supported by the WASI target");
|
|
}
|
|
foreach (self::WASI_UNSUPPORTED_FUNCTION_PREFIXES as $prefix) {
|
|
if (str_starts_with($name, $prefix)) {
|
|
$this->fatalError($expr, "Function `{$name}` is not supported by the WASI target");
|
|
}
|
|
}
|
|
}
|
|
|
|
public function isBuildModeBin(): bool
|
|
{
|
|
return $this->buildMode === self::BUILD_MODE_BIN;
|
|
}
|
|
|
|
public function isBuildModeExt(): bool
|
|
{
|
|
return $this->buildMode === self::BUILD_MODE_EXT;
|
|
}
|
|
|
|
public function isBuildModeLib(): bool
|
|
{
|
|
return $this->buildMode === self::BUILD_MODE_LIB;
|
|
}
|
|
|
|
public function isBuildModeEmbed(): bool
|
|
{
|
|
return $this->isBuildModeBin() || $this->isBuildModeLib();
|
|
}
|
|
|
|
public function getPhpDir(): string
|
|
{
|
|
try {
|
|
return $this->getPlatform()->getPhpDir();
|
|
} catch (\RuntimeException $e) {
|
|
$this->error($e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function isScalarInt(Expr $expr): bool
|
|
{
|
|
return $expr instanceof Node\Scalar\LNumber;
|
|
}
|
|
|
|
public function getLine($node): int
|
|
{
|
|
return $node->getLine();
|
|
}
|
|
|
|
public function getType($node): string
|
|
{
|
|
return $node->getType();
|
|
}
|
|
|
|
public function getTypeFromZendType(string $type): string
|
|
{
|
|
return $this->zendTypeMap[$type] ?? self::PHP_RUNTIME_TYPE_MAP[$type] ?? Type::VAR;
|
|
}
|
|
|
|
public function getObjectType(string $object): string
|
|
{
|
|
if (isset($this->context->stableObjects[$object])) {
|
|
return $this->context->stableObjects[$object];
|
|
}
|
|
return $this->context->objects[$object] ?? 'stdClass';
|
|
}
|
|
|
|
protected function getDeclaredObjectType(string $object): string
|
|
{
|
|
if (isset($this->context->declaredObjects[$object])) {
|
|
return $this->context->declaredObjects[$object];
|
|
}
|
|
if (isset($this->context->objects[$object]) || isset($this->context->stableObjects[$object])) {
|
|
return $this->getObjectType($object);
|
|
}
|
|
return '';
|
|
}
|
|
|
|
public function parseExpr(NodeAbstract $expr): string
|
|
{
|
|
if ($expr->hasAttribute('replace')) {
|
|
return $expr->getAttribute('replace');
|
|
}
|
|
$type = $expr->getType();
|
|
$this->writeLog('Line ' . $this->getLine($expr) . ': ' . $type);
|
|
if ($expr->getLine() === $this->debugLine) {
|
|
dump($expr);
|
|
}
|
|
switch ($type) {
|
|
case 'Expr_Isset':
|
|
return $this->parseIsset($expr);
|
|
case 'Expr_Empty':
|
|
return $this->parseEmpty($expr);
|
|
case 'Expr_Assign':
|
|
return $this->parseAssign($expr);
|
|
case 'Expr_AssignRef':
|
|
return $this->parseAssignRef($expr);
|
|
case 'Expr_Print':
|
|
return $this->parsePrint($expr);
|
|
case 'Expr_BinaryOp_Equal':
|
|
return $this->parseBinaryOpEqual($expr);
|
|
case 'Expr_BinaryOp_NotEqual':
|
|
return $this->parseBinaryOpNotEqual($expr);
|
|
case 'Expr_BinaryOp_Identical':
|
|
return $this->parseBinaryOpIdentical($expr);
|
|
case 'Expr_BinaryOp_NotIdentical':
|
|
return $this->parseBinaryOpNotIdentical($expr);
|
|
case 'Expr_BooleanNot':
|
|
return $this->parseBooleanNot($expr);
|
|
case 'Expr_BinaryOp_Plus':
|
|
return $this->parseBinaryOpPlus($expr);
|
|
case 'Expr_BinaryOp_Div':
|
|
return $this->parseBinaryOpDiv($expr);
|
|
case 'Expr_BinaryOp_Smaller':
|
|
return $this->parseBinaryOpSmaller($expr);
|
|
case 'Expr_BinaryOp_SmallerOrEqual':
|
|
return $this->parseBinaryOpSmallerOrEqual($expr);
|
|
case 'Expr_BinaryOp_GreaterOrEqual':
|
|
return $this->parseBinaryOpGreaterOrEqual($expr);
|
|
case 'Expr_BinaryOp_Spaceship':
|
|
return $this->parseBinaryOpSpaceship($expr);
|
|
case 'Expr_BinaryOp_Coalesce':
|
|
return $this->parseBinaryOpCoalesce($expr);
|
|
case 'Expr_PreInc':
|
|
return $this->parsePreInc($expr);
|
|
case 'Expr_PostInc':
|
|
return $this->parsePostInc($expr);
|
|
case 'Expr_PreDec':
|
|
return $this->parsePreDec($expr);
|
|
case 'Expr_PostDec':
|
|
return $this->parsePostDec($expr);
|
|
case 'Expr_AssignOp_Plus':
|
|
return $this->parseAssignOpPlus($expr);
|
|
case 'Expr_AssignOp_Minus':
|
|
return $this->parseAssignOpMinus($expr);
|
|
case 'Expr_AssignOp_Mul':
|
|
return $this->parseAssignOpMul($expr);
|
|
case 'Expr_AssignOp_Div':
|
|
return $this->parseAssignOpDiv($expr);
|
|
case 'Expr_AssignOp_Mod':
|
|
return $this->parseAssignOpMod($expr);
|
|
case 'Expr_AssignOp_Concat':
|
|
return $this->parseAssignOpConcat($expr);
|
|
case 'Expr_AssignOp_ShiftLeft':
|
|
return $this->parseAssignOpShiftLeft($expr);
|
|
case 'Expr_AssignOp_ShiftRight':
|
|
return $this->parseAssignOpShiftRight($expr);
|
|
case 'Expr_AssignOp_BitwiseAnd':
|
|
return $this->parseAssignOpBitwiseAnd($expr);
|
|
case 'Expr_AssignOp_BitwiseOr':
|
|
return $this->parseAssignOpBitwiseOr($expr);
|
|
case 'Expr_AssignOp_BitwiseXor':
|
|
return $this->parseAssignOpBitwiseXor($expr);
|
|
case 'Expr_AssignOp_Pow':
|
|
return $this->parseAssignOpPow($expr);
|
|
case 'Expr_AssignOp_Coalesce':
|
|
return $this->parseAssignOpCoalesce($expr);
|
|
case 'Expr_BinaryOp_Mul':
|
|
return $this->parseBinaryOpMul($expr);
|
|
case 'Expr_BinaryOp_Concat':
|
|
return $this->parseBinaryOpConcat($expr);
|
|
case 'Expr_BinaryOp_Greater':
|
|
return $this->parseBinaryOpGreater($expr);
|
|
case 'Expr_BinaryOp_LogicalAnd':
|
|
case 'Expr_BinaryOp_BooleanAnd':
|
|
return $this->parseBinaryOpLogicalAnd($expr);
|
|
case 'Expr_BinaryOp_LogicalOr':
|
|
case 'Expr_BinaryOp_BooleanOr':
|
|
return $this->parseBinaryOpLogicalOr($expr);
|
|
case 'Expr_BinaryOp_LogicalXor':
|
|
return $this->parseBinaryOpLogicalXor($expr);
|
|
case 'Expr_BinaryOp_Minus':
|
|
return $this->parseBinaryOpMinus($expr);
|
|
case 'Expr_Array':
|
|
return $this->parseArray($expr);
|
|
case 'Expr_ArrayDimFetch':
|
|
return $this->parseArrayDimFetch($expr);
|
|
case 'Expr_PropertyFetch':
|
|
return $this->parsePropertyFetch($expr);
|
|
case 'Expr_NullsafePropertyFetch':
|
|
return $this->parseNullsafePropertyFetch($expr);
|
|
case 'Expr_NullsafeMethodCall':
|
|
return $this->parseNullsafeMethodCall($expr);
|
|
case 'Expr_BinaryOp_ShiftLeft':
|
|
return $this->parseBinaryOpShiftLeft($expr);
|
|
case 'Expr_BinaryOp_ShiftRight':
|
|
return $this->parseBinaryOpShiftRight($expr);
|
|
case 'Expr_BinaryOp_BitwiseAnd':
|
|
return $this->parseBinaryOpBitwiseAnd($expr);
|
|
case 'Expr_BinaryOp_BitwiseOr':
|
|
return $this->parseBinaryOpBitwiseOr($expr);
|
|
case 'Expr_BinaryOp_BitwiseXor':
|
|
return $this->parseBinaryOpBitwiseXor($expr);
|
|
case 'Expr_BinaryOp_Pipe':
|
|
return $this->parsePipeOperator($expr);
|
|
case 'Expr_BitwiseNot':
|
|
return $this->parseBitwiseNot($expr);
|
|
case 'Expr_BinaryOp_Mod':
|
|
return $this->parseBinaryOpMod($expr);
|
|
case 'Expr_BinaryOp_Pow':
|
|
return $this->parseBinaryOpPow($expr);
|
|
case 'Expr_Ternary':
|
|
return $this->parseTernary($expr);
|
|
case 'Expr_Match':
|
|
return $this->parseMatch($expr);
|
|
case 'Expr_FuncCall':
|
|
return $this->parseFuncCall($expr);
|
|
case 'Expr_MethodCall':
|
|
return $this->parseMethodCall($expr);
|
|
case 'Expr_StaticCall':
|
|
return $this->parseStaticCall($expr);
|
|
case 'Expr_StaticPropertyFetch':
|
|
return $this->parseStaticPropertyFetch($expr);
|
|
case 'Expr_ClassConstFetch':
|
|
return $this->parseClassConstFetch($expr);
|
|
case 'Expr_Include':
|
|
return $this->parseInclude($expr);
|
|
case 'Expr_Eval':
|
|
return $this->parseEval($expr);
|
|
case 'Expr_New':
|
|
return $this->parseNew($expr);
|
|
case 'Expr_Clone':
|
|
return $this->parseClone($expr);
|
|
case 'Expr_Instanceof':
|
|
return $this->parseInstanceof($expr);
|
|
case 'Expr_Throw':
|
|
return $this->parseThrow($expr);
|
|
case 'Expr_ShellExec':
|
|
return $this->parseShellExec($expr);
|
|
case 'Expr_Closure':
|
|
return $this->parseClosure($expr);
|
|
case 'Expr_ArrowFunction':
|
|
return $this->parseArrowFunction($expr);
|
|
case 'Name_FullyQualified':
|
|
return $this->parseFullyQualifiedName($expr);
|
|
case 'Scalar_Int':
|
|
case 'Scalar_Float':
|
|
case 'Scalar_String':
|
|
return $this->parseIdentifier($expr);
|
|
case 'Expr_Variable':
|
|
$varName = $this->parseIdentifier($expr);
|
|
$this->requireVar($expr, $varName);
|
|
if ($this->isStdContainer($varName)) {
|
|
return $varName . '_ref';
|
|
}
|
|
// $GLOBALS is an INDIRECT to &EG(symbol_table),
|
|
// whose refcount MUST NOT be directly manipulated.
|
|
// Use php_globals_array() to create a separated copy.
|
|
if ($varName === 'GLOBALS') {
|
|
return 'php_globals_array()';
|
|
}
|
|
return $varName;
|
|
case 'Scalar_MagicConst_File':
|
|
case 'Scalar_MagicConst_Dir':
|
|
case 'Scalar_MagicConst_Line':
|
|
case 'Scalar_MagicConst_Function':
|
|
case 'Scalar_MagicConst_Method':
|
|
case 'Scalar_MagicConst_Class':
|
|
case 'Scalar_MagicConst_Trait':
|
|
return $this->parseMagicConst($expr);
|
|
case 'Scalar_InterpolatedString':
|
|
return $this->parseInterpolatedString($expr);
|
|
case 'Expr_Cast_Int':
|
|
return $this->parseCastInt($expr);
|
|
case 'Expr_Cast_Double':
|
|
return $this->parseCastDouble($expr);
|
|
case 'Expr_Cast_Bool':
|
|
return $this->parseCastBool($expr);
|
|
case 'Expr_Cast_String':
|
|
return $this->parseCastString($expr);
|
|
case 'Expr_Cast_Array':
|
|
return $this->parseCastArray($expr);
|
|
case 'Expr_Cast_Object':
|
|
return $this->parseCastObject($expr);
|
|
case 'Expr_ConstFetch':
|
|
return $this->parseConstFetch($expr);
|
|
case 'Expr_UnaryMinus':
|
|
return $this->parseUnaryMinus($expr);
|
|
case 'Expr_UnaryPlus':
|
|
return $this->parseUnaryPlus($expr);
|
|
case 'InterpolatedStringPart':
|
|
return $this->parseInterpolatedStringPart($expr);
|
|
case 'Expr_ErrorSuppress':
|
|
return $this->parseErrorSuppress($expr);
|
|
case 'Expr_Exit':
|
|
return $this->parseExit($expr);
|
|
case 'Expr_Yield':
|
|
return $this->parseYieldExpr($expr);
|
|
case 'Expr_YieldFrom':
|
|
return $this->parseYieldFromExpr($expr);
|
|
default:
|
|
abort($expr);
|
|
break;
|
|
}
|
|
}
|
|
|
|
public function stop(string $string): never
|
|
{
|
|
$this->climate->red($string . "\n");
|
|
exit(1);
|
|
}
|
|
|
|
public function genTmpVarName(): string
|
|
{
|
|
return 'tmp_var_' . $this->context->tmpVarIndex++;
|
|
}
|
|
|
|
protected function genExtraNamedVariadicArgs(string $var): string
|
|
{
|
|
return $this->getIndent() . 'php::appendCallExtraNamedArgs(' . $var . ');' . PHP_EOL;
|
|
}
|
|
|
|
public function writeFile(string $file, string $content): void
|
|
{
|
|
$dir = dirname($file);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0777, true);
|
|
}
|
|
if (!file_put_contents($file, $content)) {
|
|
throw new \RuntimeException('Can not write file: ' . $file);
|
|
}
|
|
}
|
|
|
|
public function getIncludeDir(): string
|
|
{
|
|
return $this->getBuildDir() . '/include';
|
|
}
|
|
|
|
public function getBuildDir(): string
|
|
{
|
|
return $this->buildDir;
|
|
}
|
|
|
|
public function getUserIncludePaths(): array
|
|
{
|
|
return $this->userIncludePaths;
|
|
}
|
|
|
|
public function getUserDefines(): array
|
|
{
|
|
return $this->userDefines;
|
|
}
|
|
|
|
public function isLtoEnabled(): bool
|
|
{
|
|
return $this->enableLto;
|
|
}
|
|
|
|
public function getLinkLibs(): array
|
|
{
|
|
return $this->linkLibs;
|
|
}
|
|
|
|
public function getLinkPaths(): array
|
|
{
|
|
return $this->linkPaths;
|
|
}
|
|
|
|
public function getMarch(): string
|
|
{
|
|
return $this->march;
|
|
}
|
|
|
|
public function getRelativePath($path, $cwd = ''): string
|
|
{
|
|
$cwd = $cwd ?: getcwd();
|
|
return ltrim($this->removeCommonPrefix($cwd, $path), '/');
|
|
}
|
|
|
|
protected function removeCommonPrefix(string $short, string $long): string
|
|
{
|
|
return $this->getPlatform()->removeCommonPrefix($short, $long);
|
|
}
|
|
|
|
protected function getVarType(string $name): string
|
|
{
|
|
if ($this->hasLocalVar($name)) {
|
|
return $this->context->localVars[$name];
|
|
}
|
|
if ($this->hasScopeGlobalVar($name)) {
|
|
return $this->context->globalVars[$name];
|
|
}
|
|
|
|
return Type::VAR;
|
|
}
|
|
|
|
/**
|
|
* Resolve the ClassDef for an object expression (variable or $this).
|
|
*/
|
|
private function resolveObjectClassDef(Node\Expr $expr): ?ClassDef
|
|
{
|
|
if ($expr instanceof Expr\Variable) {
|
|
$name = $this->parseIdentifier($expr);
|
|
if ($name === 'this_' && $this->classDef) {
|
|
return $this->classDef;
|
|
}
|
|
if ($this->isTypedObject($name)) {
|
|
$className = $this->getObjectType($name);
|
|
if ($this->hasClass($className)) {
|
|
return $this->getClass($className);
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
protected function resetFunction(): void
|
|
{
|
|
$this->context = new FunctionContext();
|
|
$this->function = '';
|
|
$this->functionDef = null;
|
|
}
|
|
|
|
protected function resetMethod(): void
|
|
{
|
|
$this->method = '';
|
|
$this->methodDef = null;
|
|
}
|
|
|
|
protected function enterCompilerPhase(string $phase): string
|
|
{
|
|
$previous = $this->compilerPhase;
|
|
$this->compilerPhase = $phase;
|
|
return $previous;
|
|
}
|
|
|
|
protected function restoreCompilerPhase(string $phase): void
|
|
{
|
|
$this->compilerPhase = $phase;
|
|
}
|
|
|
|
protected function assertCompilerPhase(string $expected, string $feature): void
|
|
{
|
|
if ($this->compilerPhase !== $expected) {
|
|
$this->error("Internal compiler error: {$feature} can only be used during {$expected} phase, current phase is {$this->compilerPhase}");
|
|
}
|
|
}
|
|
|
|
protected function resetClass(): void
|
|
{
|
|
$this->class = '';
|
|
$this->interface = '';
|
|
$this->classDef = null;
|
|
}
|
|
|
|
protected function resetFile(): void
|
|
{
|
|
$this->indentLevel = 0;
|
|
$this->nativeTypes = false;
|
|
$this->decimalTypes = false;
|
|
$this->bigintTypes = false;
|
|
$this->classesDefineInFile = [];
|
|
$this->interfacesDefineInFile = [];
|
|
$this->functionDefineInFile = [];
|
|
$this->stubImportLibrary = '';
|
|
}
|
|
|
|
protected function resetNamespace(): void
|
|
{
|
|
$this->useNamespaces = [];
|
|
$this->useAliases = [];
|
|
$this->useFunctions = [];
|
|
$this->useConstants = [];
|
|
$this->namespace = '';
|
|
}
|
|
|
|
protected function getFunctionName(FunctionLike $v): string
|
|
{
|
|
return $this->getNativeName($this->parseIdentifier($v->name), $this->namespace, $this->class);
|
|
}
|
|
|
|
protected function getFullClassName(): string
|
|
{
|
|
return ltrim($this->namespace . '\\' . $this->class, '\\');
|
|
}
|
|
|
|
protected function getFullClassLikeName(): string
|
|
{
|
|
$name = $this->class !== '' ? $this->class : $this->interface;
|
|
return ltrim($this->namespace . '\\' . $name, '\\');
|
|
}
|
|
|
|
protected function getFullMethodName(string $fullClassName, string $method): string
|
|
{
|
|
return strtolower($fullClassName . '::' . $method);
|
|
}
|
|
|
|
protected function isCurrentConstructor(): bool
|
|
{
|
|
return $this->method === '__construct';
|
|
}
|
|
|
|
protected function getCurrentMethodDisplayName(): string
|
|
{
|
|
return $this->getFullClassName() . '::' . $this->method;
|
|
}
|
|
|
|
protected function assertExprCanBeUsedAsValue(NodeAbstract $expr, string $context = 'value'): void
|
|
{
|
|
// PHP permits using a void/never call as an expression; the expression
|
|
// result is null after the call side effect has run.
|
|
}
|
|
|
|
protected function assertExprCanBeUsedAsCondition(NodeAbstract $expr, string $context = 'condition'): void
|
|
{
|
|
// Conditions are value contexts in PHP. A void/never expression is
|
|
// evaluated for side effects and then coerced from null.
|
|
}
|
|
|
|
protected function isVoidValueExpr(NodeAbstract $expr): bool
|
|
{
|
|
return $this->detectTypeOfExpr($expr) === Type::VOID;
|
|
}
|
|
|
|
protected function wrapVoidExprAsNull(NodeAbstract $expr, string $exprCode): string
|
|
{
|
|
if (!$this->isVoidValueExpr($expr)) {
|
|
return $exprCode;
|
|
}
|
|
|
|
return '((void) (' . $exprCode . '), ' . self::VALUE_NULL . ')';
|
|
}
|
|
|
|
protected function parseExprAsValue(NodeAbstract $expr): string
|
|
{
|
|
return $this->wrapVoidExprAsNull($expr, $this->parseExpr($expr));
|
|
}
|
|
|
|
/**
|
|
* Snapshot a reference-returning call before a by-value container can retain
|
|
* its php::Ref. Assigning to an existing Var detaches the reference, unlike
|
|
* constructing a Variant directly from Ref. Keep the assignment inline so
|
|
* earlier arguments or array elements retain PHP's evaluation order.
|
|
*/
|
|
protected function materializeRefReturnAsValue(NodeAbstract $value, string $expr): string
|
|
{
|
|
if ($value instanceof Expr\CallLike && $this->resolveRefReturningCall($value) !== false) {
|
|
$tmpVar = $this->addTmpVar(Type::VAR);
|
|
return '(' . $tmpVar . ' = ' . $expr . ')';
|
|
}
|
|
return $expr;
|
|
}
|
|
|
|
protected function getObjectPropVarName(string $object, string $prop): string
|
|
{
|
|
return self::OBJECT_PROP . $object . self::NAMESPACE_SEPARATOR . $prop;
|
|
}
|
|
|
|
protected function getObjectPropVarInfo(string $object, string $prop): array
|
|
{
|
|
return $this->context->objectProps[$this->getObjectPropVarName($object, $prop)];
|
|
}
|
|
|
|
protected function getObjectPropInfoByVar(string $var): ?array
|
|
{
|
|
return $this->context->objectProps[$var] ?? null;
|
|
}
|
|
|
|
protected function registerObjectPropVar(string $var, array $info): void
|
|
{
|
|
if (isset($this->context->objectProps[$var])) {
|
|
return;
|
|
}
|
|
$this->context->objectProps[$var] = $info;
|
|
}
|
|
|
|
protected function registerHoistedObjectPropVar(string $var, string $type, string $getter): void
|
|
{
|
|
$info = $this->getHoistedObjectPropInfo($type);
|
|
$this->registerObjectPropVar($var, [
|
|
'type' => $info['type'],
|
|
'getter' => $getter,
|
|
'kind' => $info['kind'],
|
|
]);
|
|
}
|
|
|
|
protected function getNativeName(string $fn, string $ns = '', string $class = ''): string
|
|
{
|
|
$names = [];
|
|
if ($ns) {
|
|
$names[] = $this->escapeNamespace($ns);
|
|
}
|
|
if ($class) {
|
|
$names[] = $this->escapeClass($class);
|
|
}
|
|
if ($fn) {
|
|
$names[] = $this->escapeName($fn);
|
|
}
|
|
return implode(self::NAMESPACE_SEPARATOR, $names);
|
|
}
|
|
|
|
protected function getClassId(string $className): int
|
|
{
|
|
if (isset($this->classMap[$className])) {
|
|
$id = $this->classMap[$className];
|
|
} else {
|
|
$id = $this->classIndex++;
|
|
$this->classMap[$className] = $id;
|
|
}
|
|
return $id;
|
|
}
|
|
|
|
protected function getFuncId(string $funcName): int
|
|
{
|
|
if (isset($this->funcMap[$funcName])) {
|
|
$id = $this->funcMap[$funcName];
|
|
} else {
|
|
$id = $this->funcIndex++;
|
|
$this->funcMap[$funcName] = $id;
|
|
}
|
|
return $id;
|
|
}
|
|
|
|
/**
|
|
* @param string $className 必须是带有命名空间的完整类名
|
|
*/
|
|
protected function getPropertyId(string $className, string $propName): int
|
|
{
|
|
$key = $className . '::' . $propName;
|
|
if (isset($this->propMap[$key])) {
|
|
$id = $this->propMap[$key];
|
|
} else {
|
|
$id = $this->propIndex++;
|
|
$this->propMap[$key] = $id;
|
|
}
|
|
return $id;
|
|
}
|
|
|
|
protected function getClassEntryPtr(string $className): string
|
|
{
|
|
$id = $this->getClassId($className);
|
|
return 'php_get_class(' . $id . ', ' . $this->getLiteralString($className) . ')';
|
|
}
|
|
|
|
protected function getCeWrapper(string $className): string
|
|
{
|
|
if (isset($this->context->ceWrappers[$className])) {
|
|
return $this->context->ceWrappers[$className];
|
|
}
|
|
$object = $this->addTmpVar(Type::OBJECT);
|
|
$this->context->beforeStmtLines[] = 'Z_PTR_P(' . $object . '.ptr()) = ' . $this->getClassEntryPtr($className) . ';';
|
|
$this->context->ceWrappers[$className] = $object;
|
|
return $object;
|
|
}
|
|
|
|
protected function getFuncPtr(string $funcName): string
|
|
{
|
|
return 'php_get_func(' . $this->getFuncId($funcName) . ', ' . $this->getLiteralString($funcName) . ')';
|
|
}
|
|
|
|
protected function getMethodPtr(string $class, string $method): string
|
|
{
|
|
$funcId = $this->getFuncId($class . '::' . $method);
|
|
$classId = $this->getClassId($class);
|
|
return 'php_get_method(' . $funcId . ', ' . $this->getLiteralString($method) . ', ' . $classId . ', ' . $this->getLiteralString($class) . ')';
|
|
}
|
|
|
|
protected function getPropertyOffset(string $class, string $prop): string
|
|
{
|
|
$funcId = $this->getPropertyId($class, $prop);
|
|
$classId = $this->getClassId($class);
|
|
return 'php_get_prop(' . $funcId . ', ' . $this->getLiteralString($prop) . ', ' . $classId . ', ' . $this->getLiteralString($class) . ')';
|
|
}
|
|
|
|
protected function writeLog($msg): void
|
|
{
|
|
if ($this->verbose) {
|
|
echo $msg . PHP_EOL;
|
|
}
|
|
}
|
|
|
|
protected function getLiteralString(string $string): string
|
|
{
|
|
if ($this->noLiteralStrings) {
|
|
return $this->getInlineString($string);
|
|
}
|
|
$index = $this->literalStrings[$string] ?? $this->addLiteralString($string);
|
|
return self::LITERAL_STRINGS . '[' . $index . ']';
|
|
}
|
|
|
|
/**
|
|
* Generates a PHP string value without adding it to the literal-string table.
|
|
*
|
|
* A C++ string literal may contain an embedded NUL, but passing it as a
|
|
* const char* would truncate it at that byte. ZEND_STRL preserves its length.
|
|
*/
|
|
protected function getInlineString(string $string): string
|
|
{
|
|
return Type::STR . '{ZEND_STRL(' . $this->genCharPtr($string, true) . ')}';
|
|
}
|
|
|
|
protected function parseScalar(Node\Scalar $expr): string
|
|
{
|
|
$type = $expr->getType();
|
|
switch ($type) {
|
|
case 'Scalar_Int':
|
|
if ($this->bigintTypes) {
|
|
return 'php::toBigInt(' . $expr->value . ')';
|
|
}
|
|
return $expr->value . $this->getPlatform()->getIntegerLiteralSuffix();
|
|
case 'Scalar_Float':
|
|
if ($this->isBigIntLiteral($expr)) {
|
|
return 'php::toBigInt(' . $this->getLiteralString($this->getBigIntLiteralString($expr)) . ')';
|
|
}
|
|
if ($this->isDecimalLiteral($expr) || $this->decimalTypes) {
|
|
$rawValue = $expr->getAttribute('rawValue');
|
|
$clean = $rawValue !== null ? $this->stripNumericUnderscores($rawValue) : (string) $expr->value;
|
|
return 'php::toDecimal(' . $this->getLiteralString($clean) . ')';
|
|
}
|
|
return $this->parseScalarFloat($expr);
|
|
case 'Scalar_String':
|
|
return $expr->hasAttribute('noLiteralString') ? $this->getInlineString($expr->value) : $this->getLiteralString($expr->value);
|
|
default:
|
|
abort($expr);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a numeric literal's rawValue represents an integer that exceeds int64 range.
|
|
* PHP's parser converts such literals to float (Scalar_Float) when they overflow.
|
|
*/
|
|
/**
|
|
* Check if a Scalar_Float literal should be treated as Decimal.
|
|
* Only "long" floats (>= 16 significant digits) that would lose precision
|
|
* as native PHP float (double) are auto-converted.
|
|
*/
|
|
private function getBigIntLiteralString(Node\Scalar $expr): string
|
|
{
|
|
return $this->stripNumericUnderscores($expr->getAttribute('rawValue'));
|
|
}
|
|
|
|
private function getDecimalLiteralString(Node\Scalar $expr): string
|
|
{
|
|
return $this->stripNumericUnderscores($expr->getAttribute('rawValue'));
|
|
}
|
|
|
|
protected function parseSuperGlobalVar(string $name): string
|
|
{
|
|
if (!$this->hasGlobalVar($name)) {
|
|
$this->addGlobalVar($name, $this->superGlobalVars[$name]);
|
|
}
|
|
if (!$this->hasScopeGlobalVar($name)) {
|
|
$this->addScopeGlobalVar($name, $this->superGlobalVars[$name]);
|
|
}
|
|
return $name;
|
|
}
|
|
|
|
protected function parseVariable(Variable $expr): string
|
|
{
|
|
if (!is_string($expr->name)) {
|
|
$this->fatalError($expr, 'The `$$` syntax is not supported');
|
|
}
|
|
if ($this->isSuperGlobal($expr->name)) {
|
|
return $this->parseSuperGlobalVar($expr->name);
|
|
}
|
|
return $this->escapeVarName($expr->name);
|
|
}
|
|
|
|
protected function parseImplements(array $implements): array
|
|
{
|
|
$list = [];
|
|
foreach ($implements as $implement) {
|
|
$interfaceName = $this->getNamespacedClassName($this->parseIdentifier($implement));
|
|
$list[] = $interfaceName;
|
|
if (!$this->isInternalInterface($interfaceName)) {
|
|
$this->symbolCallInFile[$this->file][] = strtolower($interfaceName);
|
|
}
|
|
}
|
|
return $list;
|
|
}
|
|
|
|
protected function parseArrayKey(NodeAbstract $expr): string
|
|
{
|
|
$key = $this->parseIdentifier($expr);
|
|
if (str_starts_with($key, self::LITERAL_STRINGS)) {
|
|
$key = "{$key}.str()";
|
|
} elseif ($this->isZeroLiteral($expr)) {
|
|
$key = self::VALUE_ZERO;
|
|
}
|
|
return $key;
|
|
}
|
|
|
|
/**
|
|
* Check if a node is a literal zero value.
|
|
*
|
|
* Detects compile-time zero for two purposes:
|
|
* - Division-by-zero guard (any zero form: int, float, negated, numeric string)
|
|
* - C++ null pointer ambiguity guard: Scalar_Int(0) → 0L → nullptr → segfault
|
|
* when passed to functions with zend_string* overloads (setProperty, getProperty, etc.)
|
|
*/
|
|
protected function isZeroLiteral(NodeAbstract $expr): bool
|
|
{
|
|
if ($expr instanceof Node\Scalar\Int_) {
|
|
return $expr->value === 0;
|
|
}
|
|
if ($expr instanceof Node\Scalar\Float_) {
|
|
return $expr->value == 0.0;
|
|
}
|
|
if ($expr instanceof Expr\UnaryMinus || $expr instanceof Expr\UnaryPlus) {
|
|
return $this->isZeroLiteral($expr->expr);
|
|
}
|
|
if ($expr instanceof Node\Scalar\String_) {
|
|
$value = trim($expr->value);
|
|
return $value !== '' && is_numeric($value) && (float)$value == 0.0;
|
|
}
|
|
if ($expr instanceof Node\Expr\ConstFetch or $expr instanceof Node\Expr\ClassConstFetch) {
|
|
return in_array($this->parseExpr($expr), ['0L', '0LL']);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected function parseIdentifier(NodeAbstract $expr): string
|
|
{
|
|
$type = $expr->getType();
|
|
switch ($type) {
|
|
case 'Expr_Variable':
|
|
return $this->parseVariable($expr);
|
|
case 'Name_FullyQualified':
|
|
return '\\' . $expr->name;
|
|
case 'Name':
|
|
case 'VarLikeIdentifier':
|
|
case 'Identifier':
|
|
return $expr->name;
|
|
case 'Scalar_Int':
|
|
case 'Scalar_Float':
|
|
case 'Scalar_String':
|
|
return $this->parseScalar($expr);
|
|
case 'Expr_ConstFetch':
|
|
return $this->parseConstFetch($expr);
|
|
case 'Expr_Assign':
|
|
case 'Expr_AssignRef':
|
|
if (!$this->isVarExpr($expr->var) && !$this->isPropertyFetch($expr->var) && !$this->isArrayDimFetch($expr->var)) {
|
|
$this->fatalError($expr, 'When an assignment expression serves as an rvalue, it must be an assignment of a variable, property, or array element');
|
|
}
|
|
return $this->parseExprAsValue($expr);
|
|
default:
|
|
return $this->parseExprAsValue($expr);
|
|
}
|
|
}
|
|
|
|
protected function parseParamDefaultValue(?NodeAbstract $default): ?string
|
|
{
|
|
if (!$default) {
|
|
return null;
|
|
}
|
|
/*
|
|
* 函数参数默认值只能为字面量,无法使用表达式获取值。
|
|
* 但 PHP 自 5.6 起支持在默认参数值中使用常量表达式,包括
|
|
* 类常量(self::FOO、ClassName::BAR、\Full\Class::BAZ),
|
|
* 编译器需要在编译期将其折叠为对应的字面量。
|
|
*/
|
|
if ($default instanceof Expr\ConstFetch) {
|
|
return $this->parseConstFetch($default, true);
|
|
}
|
|
if ($default instanceof Expr\ClassConstFetch) {
|
|
return $this->parseClassConstFetch($default);
|
|
}
|
|
return $this->parseIdentifier($default);
|
|
}
|
|
|
|
protected function getComment(Node\Stmt $v, string $class): string
|
|
{
|
|
if ($class == 'Stmt_Expression') {
|
|
$class = 'Stmt_Expression(' . $v->expr->getType() . ')';
|
|
}
|
|
|
|
return $this->getIndent() . '// ' . $class . ' [' . $v->getStartLine() . ':' . $v->getEndLine() . ']';
|
|
}
|
|
|
|
/**
|
|
* 在 for/foreach 等包含子语句的语句,之前检查当前待添加的代码是否为空,
|
|
* 如果不为空,需要将语句追加到 {} 作用域符号之前.
|
|
*/
|
|
protected function parseBeforeStmtLines(): string
|
|
{
|
|
if ($this->context->beforeStmtLines) {
|
|
$code = implode(PHP_EOL, $this->context->beforeStmtLines);
|
|
$this->context->beforeStmtLines = [];
|
|
return $code . PHP_EOL;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
protected function parseAfterStmtLines(): string
|
|
{
|
|
if ($this->context->afterStmtLines) {
|
|
$code = implode(PHP_EOL, $this->context->afterStmtLines);
|
|
$this->context->afterStmtLines = [];
|
|
return $code . PHP_EOL;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
protected function parseExprWithCapturedStmts(NodeAbstract $expr): array
|
|
{
|
|
$beforeStmtCount = count($this->context->beforeStmtLines);
|
|
$afterStmtCount = count($this->context->afterStmtLines);
|
|
$value = $this->parseExprAsValue($expr);
|
|
$beforeStmts = array_slice($this->context->beforeStmtLines, $beforeStmtCount);
|
|
$afterStmts = array_slice($this->context->afterStmtLines, $afterStmtCount);
|
|
$this->context->beforeStmtLines = array_slice($this->context->beforeStmtLines, 0, $beforeStmtCount);
|
|
$this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $afterStmtCount);
|
|
return [$value, $beforeStmts, $afterStmts];
|
|
}
|
|
|
|
protected function stringifyParsedExpr(mixed $expr): string
|
|
{
|
|
if (is_string($expr)) {
|
|
return $expr;
|
|
}
|
|
if (is_int($expr) || is_float($expr)) {
|
|
return (string) $expr;
|
|
}
|
|
if (is_object($expr)) {
|
|
if (method_exists($expr, 'toString')) {
|
|
return $expr->toString();
|
|
}
|
|
if (method_exists($expr, '__toString')) {
|
|
return $expr->__toString();
|
|
}
|
|
}
|
|
throw new \LogicException('Parsed expression must be stringable');
|
|
}
|
|
|
|
protected function formatCapturedStmtLines(array $stmts): string
|
|
{
|
|
if (!$stmts) {
|
|
return '';
|
|
}
|
|
return $this->getIndent() . implode(PHP_EOL . $this->getIndent(), $stmts) . PHP_EOL;
|
|
}
|
|
|
|
protected function genConditionWithCapturedStmts(NodeAbstract $cond, string $openPrefix): string
|
|
{
|
|
$this->assertExprCanBeUsedAsCondition($cond);
|
|
[$condExpr, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($cond);
|
|
$code = '';
|
|
$code .= $this->formatCapturedStmtLines($beforeStmts);
|
|
if ($afterStmts) {
|
|
$tmpVar = $this->addTmpVar(Type::VAR);
|
|
$code .= $this->getIndent() . $tmpVar . ' = ' . $condExpr . ';' . PHP_EOL;
|
|
$code .= $this->formatCapturedStmtLines($afterStmts);
|
|
$condExpr = $tmpVar;
|
|
}
|
|
|
|
if ($cond instanceof Expr\Assign) {
|
|
$condExpr = '(' . $condExpr . ')';
|
|
}
|
|
$condExpr = $this->convertConditionExpr($cond, $condExpr);
|
|
$code .= $openPrefix . '(' . $condExpr . ') {' . PHP_EOL;
|
|
return $code;
|
|
}
|
|
|
|
protected function parseBlockStmts(array $stmts): string
|
|
{
|
|
$this->indentLevel++;
|
|
$code = $this->parseStmts($stmts);
|
|
$this->indentLevel--;
|
|
return $code;
|
|
}
|
|
|
|
protected function parseStmts(array $stmts): string
|
|
{
|
|
$this->context->enterScope();
|
|
$lines = [];
|
|
$inLoopTop = $this->context->inLoop;
|
|
$inContinuableLoopTop = $this->context->inContinuableLoop;
|
|
$last = array_key_last($stmts);
|
|
foreach ($stmts as $i => $v) {
|
|
$class = $v->getType();
|
|
$this->context->beforeStmtLines = [];
|
|
$this->context->afterStmtLines = [];
|
|
$result = '';
|
|
$this->writeLog('Line ' . $this->getLine($v) . ': ' . $class);
|
|
$lines[] = $this->genDebugInfo($v);
|
|
$lines[] = $this->getComment($v, $class);
|
|
switch ($class) {
|
|
case 'Stmt_Expression':
|
|
$v->expr->setAttribute(self::ATTR_STATEMENT_EXPRESSION, true);
|
|
$this->assertMustUseResultIsConsumed($v->expr);
|
|
if ($this->inGeneratorBody && $v->expr instanceof Expr\Yield_) {
|
|
$result = $this->parseYieldStmt($v->expr);
|
|
} elseif ($this->inGeneratorBody && $v->expr instanceof Expr\YieldFrom) {
|
|
$result = $this->parseYieldFromStmt($v->expr);
|
|
} else {
|
|
$result = $this->parseExpr($v->expr) . ';';
|
|
}
|
|
break;
|
|
case 'Stmt_Echo':
|
|
$result = $this->parseEcho($v);
|
|
break;
|
|
case 'Stmt_Return':
|
|
$result = $this->parseReturn($v);
|
|
break;
|
|
case 'Stmt_For':
|
|
$this->context->inLoop = true;
|
|
$this->context->inContinuableLoop = true;
|
|
$result = $this->parseFor($v);
|
|
$this->context->inLoop = $inLoopTop;
|
|
$this->context->inContinuableLoop = $inContinuableLoopTop;
|
|
break;
|
|
case 'Stmt_Foreach':
|
|
$this->context->inLoop = true;
|
|
$this->context->inContinuableLoop = true;
|
|
$result = $this->parseForeach($v);
|
|
$this->context->inLoop = $inLoopTop;
|
|
$this->context->inContinuableLoop = $inContinuableLoopTop;
|
|
break;
|
|
case 'Stmt_Switch':
|
|
$this->context->inLoop = true;
|
|
$result = $this->parseSwitch($v);
|
|
$this->context->inLoop = $inLoopTop;
|
|
break;
|
|
case 'Stmt_While':
|
|
$this->context->inLoop = true;
|
|
$this->context->inContinuableLoop = true;
|
|
$result = $this->parseWhile($v);
|
|
$this->context->inLoop = $inLoopTop;
|
|
$this->context->inContinuableLoop = $inContinuableLoopTop;
|
|
break;
|
|
case 'Stmt_Do':
|
|
$this->context->inLoop = true;
|
|
$this->context->inContinuableLoop = true;
|
|
$result = $this->parseDo($v);
|
|
$this->context->inLoop = $inLoopTop;
|
|
$this->context->inContinuableLoop = $inContinuableLoopTop;
|
|
break;
|
|
case 'Stmt_If':
|
|
$result = $this->parseIf($v);
|
|
break;
|
|
case 'Stmt_Break':
|
|
$result = $this->parseBreak($v);
|
|
break;
|
|
case 'Stmt_Goto':
|
|
$result = $this->parseGoto($v);
|
|
break;
|
|
case 'Stmt_Label':
|
|
$result = $this->parseLabel($v);
|
|
if ($i === $last) {
|
|
$result .= self::OP_NOP;
|
|
}
|
|
break;
|
|
case 'Stmt_Continue':
|
|
$result = $this->parseContinue($v);
|
|
break;
|
|
case 'Stmt_Nop':
|
|
break;
|
|
case 'Stmt_Global':
|
|
$result = $this->parseGlobal($v);
|
|
break;
|
|
case 'Stmt_Enum':
|
|
$result = $this->parseEnum($v);
|
|
break;
|
|
case 'Stmt_Static':
|
|
$result = $this->parseStatic($v);
|
|
break;
|
|
case 'Stmt_Unset':
|
|
$result = $this->parseUnset($v);
|
|
break;
|
|
case 'Stmt_TryCatch':
|
|
$result = $this->parseTryCatch($v);
|
|
break;
|
|
case 'Stmt_Block':
|
|
$result = $this->parseStmts($v->stmts);
|
|
break;
|
|
case 'Stmt_Class':
|
|
$this->fatalError($v, 'Cannot declare class in function');
|
|
break;
|
|
case 'Stmt_Function':
|
|
$this->fatalError($v, 'Cannot declare function in function');
|
|
break;
|
|
default:
|
|
abort($v);
|
|
break;
|
|
}
|
|
$lines = array_merge($lines, $this->context->beforeStmtLines);
|
|
$this->context->beforeStmtLines = [];
|
|
if ($result) {
|
|
$lines[] = $result;
|
|
}
|
|
if ($this->context->afterStmtLines) {
|
|
$lines = array_merge($lines, $this->context->afterStmtLines);
|
|
$this->context->afterStmtLines = [];
|
|
}
|
|
}
|
|
|
|
$code = '';
|
|
foreach ($lines as $line) {
|
|
$code .= $this->getIndent() . $line . PHP_EOL;
|
|
}
|
|
$this->context->leaveScope();
|
|
|
|
return $code;
|
|
}
|
|
|
|
protected function assertMustUseResultIsConsumed(NodeAbstract $expr): void
|
|
{
|
|
$functionDef = $this->resolveCalledFunctionDef($expr);
|
|
if ($functionDef?->mustUse) {
|
|
$target = ($functionDef->method ? 'method ' : 'function ') . $functionDef->name . '()';
|
|
$this->error(CompileTimeAttributeDiagnostic::formatPositions(
|
|
'The return value of `' . $functionDef->name . '()` must be used',
|
|
'MustUse',
|
|
$target,
|
|
$functionDef->sourceFile,
|
|
$functionDef->startLine,
|
|
'discarded call',
|
|
$this->file,
|
|
$expr->getStartLine(),
|
|
));
|
|
}
|
|
}
|
|
|
|
protected function resolveCalledFunctionDef(NodeAbstract $expr): ?FunctionDef
|
|
{
|
|
if ($expr instanceof Expr\FuncCall && $expr->name instanceof Node\Name) {
|
|
$name = $this->parseIdentifier($expr->name);
|
|
$native = $this->findNativeFunction($name);
|
|
return $native ? $this->getFunction($native) : null;
|
|
}
|
|
if ($expr instanceof Expr\MethodCall && $expr->name instanceof Node\Identifier) {
|
|
$class = $this->detectClassOfExpr($expr->var);
|
|
if ($class === '' && $expr->var instanceof Expr\Variable && is_string($expr->var->name)) {
|
|
$var = $this->parseVariable($expr->var);
|
|
$class = $var === 'this_' ? $this->getFullClassName() : $this->getDeclaredObjectType($var);
|
|
}
|
|
return $class === '' ? null : $this->findAotMethodFunctionDef($class, $expr->name->toString());
|
|
}
|
|
if ($expr instanceof Expr\StaticCall && $expr->class instanceof Node\Name
|
|
&& $expr->name instanceof Node\Identifier) {
|
|
$class = $this->parseIdentifier($expr->class);
|
|
if ($class === 'self' || $class === 'static') {
|
|
$class = $this->getFullClassName();
|
|
} elseif ($class === 'parent') {
|
|
$class = $this->classDef?->extends ?? '';
|
|
} else {
|
|
$class = $this->getNamespacedClassName($class);
|
|
}
|
|
return $class === '' ? null : $this->findAotMethodFunctionDef($class, $expr->name->toString());
|
|
}
|
|
return null;
|
|
}
|
|
|
|
protected function parseEcho(mixed $v): string
|
|
{
|
|
$lines = [];
|
|
foreach ($v->exprs as $expr) {
|
|
$type = $this->detectTypeOfExpr($expr);
|
|
$parsed = $this->convertExprToStringByType($this->parseExprAsValue($expr), $type);
|
|
$lines[] = 'php::echo(' . $parsed . ');';
|
|
}
|
|
|
|
return implode("\n" . $this->getIndent(), $lines);
|
|
}
|
|
|
|
/**
|
|
* 尽可能转为数字,优先级 浮点 > 整数 > 字符串.
|
|
*/
|
|
protected function parseNumericIdentifier(NodeAbstract $expr): string
|
|
{
|
|
if ($expr->getType() === 'Scalar_String') {
|
|
if ($this->isFloatStr($expr->value)) {
|
|
return (string) floatval($expr->value);
|
|
}
|
|
if ($this->isIntStr($expr->value)) {
|
|
return (string) intval($expr->value);
|
|
}
|
|
if ($expr->value === '0') {
|
|
return '0';
|
|
}
|
|
}
|
|
|
|
return $this->parseIdentifier($expr);
|
|
}
|
|
|
|
protected function detectClassOfExpr(NodeAbstract $expr): string
|
|
{
|
|
if ($this->isNewExpr($expr) and $this->isNameExpr($expr->class)) {
|
|
$class = $this->parseIdentifier($expr->class);
|
|
if ($class === 'self') {
|
|
return $this->getFullClassName();
|
|
}
|
|
if ($class === 'static') {
|
|
// 无法在编译期获得 static 类的准确类名
|
|
return '';
|
|
} else {
|
|
return $this->getNamespacedClassName($class);
|
|
}
|
|
}
|
|
if ($this->isVarExpr($expr)) {
|
|
$object = $this->parseVariable($expr);
|
|
if ($object === 'this_') {
|
|
return $this->getFullClassName();
|
|
}
|
|
if ($this->isTypedObject($object)) {
|
|
return $this->getObjectType($object);
|
|
}
|
|
}
|
|
if ($this->isArrayDimFetch($expr) and $this->isStdContainerExpr($expr)) {
|
|
if ($this->isStdArrayExpr($expr)) {
|
|
if (!$expr->hasAttribute('stdArrayDimFetch')) {
|
|
$this->parseStdArrayDimFetch($expr);
|
|
}
|
|
$attr = $expr->getAttribute('stdArrayDimFetch');
|
|
if ($attr['accessLevel'] === $attr['totalLevel']) {
|
|
return $this->context->stdArrays[$attr['var']]['class'] ?? '';
|
|
}
|
|
return '';
|
|
}
|
|
if (!$expr->hasAttribute('stdContainerDimFetch')) {
|
|
$this->parseStdContainerDimFetch($expr);
|
|
}
|
|
$attr = $expr->getAttribute('stdContainerDimFetch');
|
|
return $this->context->stdContainers[$attr['var']]['class'] ?? '';
|
|
}
|
|
if ($this->isFuncCallExpr($expr) and $this->isNameExpr($expr->name)) {
|
|
$fn = $this->parseIdentifier($expr->name);
|
|
if (count($expr->args) === 2 and $fn === 'objval') {
|
|
return $this->resolveClassNameArg($expr->args[1]->value);
|
|
}
|
|
if ($this->hasFunction($fn)) {
|
|
return $this->getFunction($fn)->returnClass;
|
|
}
|
|
}
|
|
if ($this->isMethodCall($expr) and $this->isNamedMethod($expr->name)) {
|
|
$method = $this->parseIdentifier($expr->name);
|
|
if ($method === 'toObject' and !empty($expr->args)) {
|
|
return $this->resolveClassNameArg($expr->args[0]->value);
|
|
}
|
|
$classDef = $this->resolveObjectClassDef($expr->var);
|
|
if ($classDef !== null && $classDef->hasMethod($method)) {
|
|
return $classDef->getMethod($method)->functionDef->returnClass;
|
|
}
|
|
if ($this->isVarExpr($expr->var)) {
|
|
$object = $this->parseVariable($expr->var);
|
|
try {
|
|
$nativeFunc = $this->findNativeMethod($expr, $object, $method);
|
|
if ($nativeFunc) {
|
|
return $this->getFunction($nativeFunc)->returnClass;
|
|
}
|
|
} catch (DynamicCall) {
|
|
}
|
|
}
|
|
}
|
|
if ($this->isStaticCall($expr) and $this->isNameExpr($expr->class) and $this->isNamedMethod($expr->name)) {
|
|
$class = $this->parseIdentifier($expr->class);
|
|
if ($class === 'self') {
|
|
$class = $this->class;
|
|
} elseif ($class === 'static' or $class === 'parent') {
|
|
return '';
|
|
}
|
|
$class = $this->getNamespacedClassName($class);
|
|
$method = $this->parseIdentifier($expr->name);
|
|
if ($this->hasClass($class)) {
|
|
$classDef = $this->getClass($class);
|
|
if ($classDef->hasMethod($method)) {
|
|
return $classDef->getMethod($method)->functionDef->returnClass;
|
|
}
|
|
}
|
|
$nativeFunc = $this->getNativeMethod($expr, $class, $method);
|
|
if ($nativeFunc) {
|
|
return $this->getFunction($nativeFunc)->returnClass;
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
protected function detectDeclaredClassOfExpr(NodeAbstract $expr): string
|
|
{
|
|
// 对象表达式有两类类型信息:
|
|
// 1. detectClassOfExpr() 返回“实际可推断的类”,例如 new Foo()、typed object 变量;
|
|
// 2. getDeclaredObjectType() 返回变量声明/首次赋值记录的 declared type,可能是接口或抽象类。
|
|
// 参数和属性赋值检查需要先使用实际类;实际类不可知时才退回 declared type。
|
|
$class = $this->detectClassOfExpr($expr);
|
|
if ($class !== '') {
|
|
return $class;
|
|
}
|
|
if ($this->isVarExpr($expr)) {
|
|
return $this->getDeclaredObjectType($this->parseVariable($expr));
|
|
}
|
|
return '';
|
|
}
|
|
|
|
protected function isObjectClassStaticallyAssignableTo(string $class, string $expected): bool
|
|
{
|
|
// 这个函数只回答“编译器在静态阶段能否证明 $class is-a $expected”。
|
|
// 这里禁止使用 class_exists()/interface_exists()/is_a() 去查询当前运行编译器的 PHP 进程:
|
|
// - 编译器进程已加载的 Composer/工具类,不等价于被编译项目运行时可用的类;
|
|
// - 自举编译时还会把编译器自身依赖的外部库误判为项目静态类;
|
|
// - AOT 的静态判断必须只依赖 hasClass()/hasInterface() 记录的项目类图,或明确的内置类/接口。
|
|
// 如果类不属于这些集合,说明它是动态类/外部库类,不能在这里静态判定,应返回 false,
|
|
// 由调用处决定是延迟到运行时 php::toObject()/TypeCheck,还是因为确定 concrete mismatch 而 fatal。
|
|
$class = ltrim($class, '\\');
|
|
$expected = ltrim($expected, '\\');
|
|
if (strcasecmp($class, $expected) === 0) {
|
|
return true;
|
|
}
|
|
|
|
if (!$this->hasClass($class)
|
|
&& !$this->hasInterface($class)
|
|
&& !$this->isInternalClass($class)
|
|
&& !$this->isInternalInterface($class)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return $this->isInheritedFrom($class, $expected);
|
|
}
|
|
|
|
protected function isKnownConcreteObjectExpr(NodeAbstract $expr, string $class): bool
|
|
{
|
|
// “已知 concrete object” 的要求比“表达式写着 new SomeClass”更严格:
|
|
// 只有 AOT 项目类图中的类或内置类,编译器才能在静态阶段确认其继承关系。
|
|
// 外部库类即使出现在 new 表达式中,也不能用当前编译器进程的反射信息判定,
|
|
// 否则会把编译器/Composer 运行环境泄漏进被编译项目的类型系统。
|
|
if ($class === '' || $this->isInterface($class) || $this->isAbstractClass($class)) {
|
|
return false;
|
|
}
|
|
if (!$this->hasClass($class) && !$this->isInternalClass($class)) {
|
|
return false;
|
|
}
|
|
if (!$this->isNewExpr($expr) || !$this->isNameExpr($expr->class)) {
|
|
return false;
|
|
}
|
|
return $this->parseIdentifier($expr->class) !== 'static';
|
|
}
|
|
|
|
protected function resolveClassNameArg(NodeAbstract $arg): string
|
|
{
|
|
if ($this->isScalarString($arg)) {
|
|
return $this->getNamespacedClassName($arg->value);
|
|
}
|
|
if ($this->isClassConstFetch($arg)) {
|
|
if ($this->isNameExpr($arg->class) and $this->isIdExpr($arg->name) and $this->parseIdentifier($arg->name) === 'class') {
|
|
$class = $this->parseIdentifier($arg->class);
|
|
if ($class === 'self') {
|
|
$class = $this->class;
|
|
} elseif ($class === 'parent') {
|
|
if (!$this->classDef || !$this->classDef->extends) {
|
|
$this->fatalError($arg, 'Cannot use "parent" outside a class or class does not extend any class');
|
|
}
|
|
return $this->classDef->extends;
|
|
} elseif ($class === 'static') {
|
|
$this->fatalError($arg, "'static::class' cannot be resolved at compile time, use a concrete class name or 'self::class'");
|
|
}
|
|
return $this->getNamespacedClassName($class);
|
|
}
|
|
}
|
|
$this->fatalError($arg, 'Only string literals or `ClassName::class` constant are supported');
|
|
}
|
|
|
|
/**
|
|
* Resolve whether a call returns by reference. A null result means that
|
|
* dispatch is dynamic and must be checked at runtime.
|
|
*/
|
|
protected function resolveRefReturningCall(Node $expr): ?bool
|
|
{
|
|
if ($expr instanceof Expr\FuncCall && ($this->isNameExpr($expr->name) || $this->isFullNameExpr($expr->name))) {
|
|
$name = $this->parseIdentifier($expr->name);
|
|
$function = $this->findNativeFunction($name);
|
|
if ($function !== false) {
|
|
return $this->getFunction($function)->returnsByRef;
|
|
}
|
|
$reflection = \TypePhp\Resolver\Reflection::getFunction(ltrim($this->getNamespacedFuncName($name), '\\'));
|
|
return $reflection?->returnsReference();
|
|
}
|
|
if ($expr instanceof Expr\FuncCall) {
|
|
return null;
|
|
}
|
|
if ($expr instanceof Expr\MethodCall && $this->isNamedMethod($expr->name) && $this->isVarExpr($expr->var)) {
|
|
$object = $this->parseIdentifier($expr->var);
|
|
$method = $this->parseIdentifier($expr->name);
|
|
if ($object === 'this_') {
|
|
$class = $this->getFullClassName();
|
|
} elseif (isset($this->context->objects[$object])) {
|
|
$class = $this->context->stableObjects[$object] ?? $this->context->objects[$object];
|
|
} else {
|
|
return null;
|
|
}
|
|
try {
|
|
$function = $this->getNativeMethod($expr, $class, $method, false);
|
|
} catch (DynamicCall) {
|
|
return null;
|
|
}
|
|
if ($function !== false) {
|
|
return $this->getFunction($function)->returnsByRef;
|
|
}
|
|
return null;
|
|
}
|
|
if ($expr instanceof Expr\MethodCall) {
|
|
return null;
|
|
}
|
|
if ($expr instanceof Expr\StaticCall && ($this->isNameExpr($expr->class) || $this->isFullNameExpr($expr->class)) && $this->isIdExpr($expr->name)) {
|
|
$class = $this->parseIdentifier($expr->class);
|
|
if ($class === 'self') {
|
|
$class = $this->getFullClassName();
|
|
} elseif ($class === 'parent') {
|
|
if (!$this->classDef || !$this->classDef->extends) {
|
|
return false;
|
|
}
|
|
$class = $this->classDef->extends;
|
|
} elseif ($class === 'static') {
|
|
if (!$this->classDef) {
|
|
return null;
|
|
}
|
|
$class = $this->getFullClassName();
|
|
} else {
|
|
$class = $this->getNamespacedClassName($class);
|
|
}
|
|
$method = $this->parseIdentifier($expr->name);
|
|
try {
|
|
$function = $this->getNativeMethod($expr, $class, $method, false);
|
|
} catch (DynamicCall) {
|
|
return null;
|
|
}
|
|
if ($function !== false) {
|
|
return $this->getFunction($function)->returnsByRef;
|
|
}
|
|
return null;
|
|
}
|
|
if ($expr instanceof Expr\StaticCall) {
|
|
return null;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected function parseReturn(Node\Stmt\Return_ $v): string
|
|
{
|
|
if ($this->functionDef->returnsByRef) {
|
|
if ($v->expr === null) {
|
|
return 'return ' . Type::REF . '{};';
|
|
}
|
|
if ($v->expr instanceof CallLike) {
|
|
$returnsByRef = $this->resolveRefReturningCall($v->expr);
|
|
if ($returnsByRef !== false) {
|
|
return 'return php::toReferenceExact(' . $this->parseExpr($v->expr) . ');';
|
|
}
|
|
}
|
|
if (!$this->isVarExpr($v->expr)
|
|
&& !$this->isPropertyFetch($v->expr)
|
|
&& !$this->isStaticPropertyFetch($v->expr)
|
|
&& !$this->isArrayDimFetch($v->expr)) {
|
|
$this->fatalError($v, 'A function returning by reference must return a variable');
|
|
}
|
|
if ($this->isVarExpr($v->expr)) {
|
|
$name = $this->parseIdentifier($v->expr);
|
|
if (!$this->hasVar($name)) {
|
|
$this->errorUndefinedVariable($v->expr);
|
|
}
|
|
if ($this->hasLocalVar($name) && $this->getVarType($name) !== Type::VAR && $this->getVarType($name) !== Type::REF) {
|
|
$isParameter = false;
|
|
foreach ($this->functionDef->argInfoList as $argInfo) {
|
|
if ($argInfo->name === $name) {
|
|
$isParameter = true;
|
|
break;
|
|
}
|
|
}
|
|
if ($isParameter) {
|
|
$this->fatalError($v, 'A function returning by reference cannot return a native typed parameter');
|
|
}
|
|
// The declaration is emitted after parsing the body, so a local can
|
|
// be promoted to Variant before C++ is generated.
|
|
$this->context->localVars[$name] = Type::VAR;
|
|
}
|
|
return 'return ' . $name . '.toReference();';
|
|
}
|
|
if ($this->isPropertyFetch($v->expr)) {
|
|
return 'return ' . $this->emitDynamicPropertyFetchRef($v->expr, $v) . ';';
|
|
}
|
|
if ($this->isStaticPropertyFetch($v->expr)) {
|
|
return 'return ' . $this->emitStaticPropertyFetchRef($v->expr, $v) . ';';
|
|
}
|
|
return 'return ' . $this->parseChainedExpr($v->expr, self::OP_REFVAL) . ';';
|
|
}
|
|
if ($v->expr === null) {
|
|
$nullExpr = new Expr\ConstFetch(new Node\Name('null'));
|
|
if ($this->shouldCheckClosureReturnType()) {
|
|
$this->checkCompositeTypeAssignment(
|
|
$v,
|
|
$this->context->closureReturnTypeCheck,
|
|
$this->context->closureReturnTypeStr,
|
|
$nullExpr,
|
|
'closure return value'
|
|
);
|
|
} elseif ($this->functionDef->returnTypeCheck && !$this->context->inClosure) {
|
|
$this->checkCompositeTypeAssignment(
|
|
$v,
|
|
$this->functionDef->returnTypeCheck,
|
|
$this->functionDef->returnTypeStr,
|
|
$nullExpr,
|
|
'return value'
|
|
);
|
|
}
|
|
if ($this->functionDef->returnType === Type::VOID and !$this->context->inClosure) {
|
|
return 'return;';
|
|
} elseif ($this->shouldCheckClosureReturnType()) {
|
|
return $this->genClosureCheckedReturn(self::VALUE_NULL);
|
|
} elseif ($this->functionDef->returnTypeCheck && !$this->context->inClosure) {
|
|
return $this->genUnionCheckedReturn(self::VALUE_NULL);
|
|
} else {
|
|
return 'return ' . self::VALUE_NULL . ';';
|
|
}
|
|
}
|
|
if (!$this->context->inClosure && $this->functionDef->hasMultiReturn()) {
|
|
if (!$v->expr instanceof Expr\Array_) {
|
|
throw new \LogicException('Optimized multi-return function must return a fixed array literal');
|
|
}
|
|
|
|
// Assign tuple elements through Variant::operator= instead of constructing
|
|
// temporary Vars. The rvalue overload can transfer an owned zval without
|
|
// refcount churn while retaining PHP value-assignment semantics for
|
|
// references and indirect zvals.
|
|
$remainingVariableUses = [];
|
|
foreach ($v->expr->items as $item) {
|
|
if ($this->isVarExpr($item->value) && is_string($item->value->name)) {
|
|
$name = $this->parseIdentifier($item->value);
|
|
$remainingVariableUses[$name] = ($remainingVariableUses[$name] ?? 0) + 1;
|
|
}
|
|
}
|
|
|
|
$tuple = $this->genTmpVarName();
|
|
$lines = [$this->functionDef->getMultiReturnCppType() . ' ' . $tuple . ';'];
|
|
foreach ($v->expr->items as $index => $item) {
|
|
$value = $this->parseExprAsValue($item->value);
|
|
if ($this->isVarExpr($item->value) && is_string($item->value->name)) {
|
|
$name = $this->parseIdentifier($item->value);
|
|
$remainingVariableUses[$name]--;
|
|
// Only consume a local on its final occurrence. Globals and
|
|
// statics outlive the function and must never be emptied.
|
|
if ($remainingVariableUses[$name] === 0 && $this->hasLocalVar($name)) {
|
|
$value = 'std::move(' . $value . ')';
|
|
}
|
|
}
|
|
$lines[] = 'std::get<' . $index . '>(' . $tuple . ') = ' . $value . ';';
|
|
}
|
|
$lines[] = 'return ' . $tuple . ';';
|
|
return implode(PHP_EOL . $this->getIndent(), $lines);
|
|
}
|
|
// 实际函数的返回值
|
|
$type = $this->detectTypeOfExpr($v->expr);
|
|
if ($this->isCurrentConstructor() && !$this->context->inClosure) {
|
|
$this->fatalError($v, 'Method `' . $this->getCurrentMethodDisplayName() . '()` cannot return a value');
|
|
}
|
|
if ($this->shouldCheckClosureReturnType()) {
|
|
$this->checkCompositeTypeAssignment(
|
|
$v,
|
|
$this->context->closureReturnTypeCheck,
|
|
$this->context->closureReturnTypeStr,
|
|
$v->expr,
|
|
'closure return value'
|
|
);
|
|
} elseif (!$this->context->inClosure && !empty($this->functionDef->returnTypeCheck)) {
|
|
$this->checkCompositeTypeAssignment(
|
|
$v,
|
|
$this->functionDef->returnTypeCheck,
|
|
$this->functionDef->returnTypeStr,
|
|
$v->expr,
|
|
'return value'
|
|
);
|
|
}
|
|
$expr = $this->parseExprAsValue($v->expr);
|
|
$returnType = $this->getReturnType();
|
|
|
|
// 匿名函数的返回值一定是 var
|
|
if (!$this->context->inClosure) {
|
|
if ($returnType === Type::VOID) {
|
|
$this->fatalError($v, 'The return type is void, cannot return any value');
|
|
}
|
|
} else {
|
|
$returnType = Type::VAR;
|
|
}
|
|
|
|
if (!$this->context->inClosure
|
|
&& ($type === Type::VAR || $type === Type::REF)
|
|
&& $this->isStrictScalarType($returnType)) {
|
|
// Keep the zval type until the declared return boundary has been
|
|
// checked. Converting first would silently coerce invalid values.
|
|
$tmpVar = $this->addTmpVar(Type::VAR);
|
|
$code = $tmpVar . ' = (' . $expr . ');' . PHP_EOL;
|
|
$code .= $this->genStrictScalarReturnCheck($tmpVar, $returnType);
|
|
$code .= $this->getIndent() . 'return '
|
|
. $this->convertExprType($tmpVar, $returnType, Type::VAR) . ';';
|
|
return $code;
|
|
}
|
|
|
|
$returnObjectCheckClass = '';
|
|
// 返回值的表达式是一个类的对象
|
|
$objectClass = $this->detectDeclaredClassOfExpr($v->expr);
|
|
$returnClass = $this->context->inClosure ? '' : $this->getReturnClass();
|
|
if ($returnClass) {
|
|
if ($objectClass === '') {
|
|
$returnObjectCheckClass = $returnClass;
|
|
} elseif (!$this->isObjectClassStaticallyAssignableTo($objectClass, $returnClass)) {
|
|
if ($this->isKnownConcreteObjectExpr($v->expr, $objectClass)) {
|
|
$this->fatalError($v, 'The return type is `' . $returnClass . '`, cannot return an instance of `' . $objectClass . '`');
|
|
}
|
|
$returnObjectCheckClass = $returnClass;
|
|
}
|
|
}
|
|
|
|
$exprCode = $this->convertExprType($expr, $returnType, $type);
|
|
if ($returnObjectCheckClass !== '') {
|
|
$exprCode = $this->convertObjectExpr($exprCode, $this->getClassEntryPtr($returnObjectCheckClass));
|
|
}
|
|
// Union/nullable return type: always use tmpVar for runtime check
|
|
if ($this->shouldCheckClosureReturnType()) {
|
|
[$code, $tmpVar] = $this->genClosureCheckedReturnAssignment($exprCode);
|
|
$this->context->afterStmtLines[] = $this->getIndent() . 'return ' . $tmpVar . ';';
|
|
} elseif ($this->functionDef->returnTypeCheck && !$this->context->inClosure) {
|
|
[$code, $tmpVar] = $this->genUnionCheckedReturnAssignment($exprCode);
|
|
$this->context->afterStmtLines[] = $this->getIndent() . 'return ' . $tmpVar . ';';
|
|
} elseif (!$this->isVarExpr($v->expr) and !$this->isScalar($v->expr)) {
|
|
// return 如果使用了 Indirect 语句,可能会导致变量提前析构,出现悬空指针
|
|
// 将 Indirect 赋值给临时变量后,使用 Ctor::Copy 解除了 Indirect,保证内存安全
|
|
$tmpVar = $this->genTmpVarName();
|
|
// 必须提前声明变量,否则在末尾声明并 return 可能会被 gcc 优化掉
|
|
$this->addLocalVar($tmpVar, $returnType);
|
|
$code = $tmpVar . ' = (' . $exprCode . ');' . PHP_EOL;
|
|
// 解析表达式后可能会插入语句,因此需要在末尾添加 return 语句,而不是直接返回
|
|
$this->context->afterStmtLines[] = $this->getIndent() . 'return ' . $tmpVar . ';';
|
|
} else {
|
|
$code = 'return ' . $exprCode . ';';
|
|
}
|
|
|
|
return $code;
|
|
}
|
|
|
|
protected function getMultiReturnImplName(string $nativeName): string
|
|
{
|
|
return self::MULTI_RETURN_NAMESPACE . '::' . self::PREFIX . $nativeName;
|
|
}
|
|
|
|
protected function genClosureCheckedReturn(string $exprCode): string
|
|
{
|
|
[$code, $tmpVar] = $this->genClosureCheckedReturnAssignment($exprCode);
|
|
return $code . $this->getIndent() . 'return ' . $tmpVar . ';';
|
|
}
|
|
|
|
protected function genClosureReturnValue(string $exprCode): string
|
|
{
|
|
if ($this->context->closureReturnTypeCheck) {
|
|
return $this->genClosureCheckedReturn($exprCode);
|
|
}
|
|
|
|
return 'return ' . $exprCode . ';';
|
|
}
|
|
|
|
protected function genClosureReturnNull(): string
|
|
{
|
|
return $this->genClosureReturnValue(self::VALUE_NULL);
|
|
}
|
|
|
|
protected function genUnionCheckedReturn(string $exprCode): string
|
|
{
|
|
[$code, $tmpVar] = $this->genUnionCheckedReturnAssignment($exprCode);
|
|
return $code . $this->getIndent() . 'return ' . $tmpVar . ';';
|
|
}
|
|
|
|
protected function genClosureCheckedReturnAssignment(string $exprCode): array
|
|
{
|
|
return $this->genCheckedReturnAssignment($exprCode, true);
|
|
}
|
|
|
|
protected function genUnionCheckedReturnAssignment(string $exprCode): array
|
|
{
|
|
return $this->genCheckedReturnAssignment($exprCode, false);
|
|
}
|
|
|
|
protected function genCheckedReturnAssignment(string $exprCode, bool $closure): array
|
|
{
|
|
$tmpVar = $this->genTmpVarName();
|
|
$this->addLocalVar($tmpVar, Type::VAR);
|
|
$code = $tmpVar . ' = ' . $exprCode . ';' . PHP_EOL;
|
|
$code .= $closure ? $this->genClosureReturnCheck($tmpVar) : $this->genUnionReturnCheck($tmpVar);
|
|
|
|
return [$code, $tmpVar];
|
|
}
|
|
|
|
protected function shouldCheckClosureReturnType(): bool
|
|
{
|
|
return $this->context->inClosure && $this->context->closureReturnTypeCheck;
|
|
}
|
|
|
|
protected function checkNativeCallArgs(CallLike $expr, FunctionDef $funcDef, array $args, string $name): void
|
|
{
|
|
$this->validateNativeNamedCallArgs($funcDef, $args);
|
|
|
|
if ($this->hasUnpackCallArg($args)) {
|
|
return;
|
|
}
|
|
|
|
$argc = count($args);
|
|
$type = str_contains($name, '::') ? 'Method' : 'Function';
|
|
if ($argc < $funcDef->argCountRequired) {
|
|
$this->fatalError($expr, $type . ' `' . $name . '()` requires ' . $funcDef->argCountRequired . ' arguments, ' . $argc . ' given');
|
|
} elseif (!$funcDef->hasVariadicArg() and count($expr->args) > count($funcDef->argInfoList)) {
|
|
$this->fatalError($expr, $type . ' `' . $name . '()` accepts ' . count($funcDef->argInfoList) . ' arguments, ' . $argc . ' given');
|
|
}
|
|
}
|
|
|
|
protected function getFunctionArgNameIndex(FunctionDef $functionDef): array
|
|
{
|
|
$argNameIndex = [];
|
|
foreach ($functionDef->argInfoList as $k => $argInfo) {
|
|
$argNameIndex[$argInfo->phpName ?: $this->unescapeVarName($argInfo->name)] = $k;
|
|
}
|
|
return $argNameIndex;
|
|
}
|
|
|
|
protected function getVariadicArgIndex(FunctionDef $functionDef): ?int
|
|
{
|
|
$lastIndex = count($functionDef->argInfoList) - 1;
|
|
if ($lastIndex >= 0 and $functionDef->argInfoList[$lastIndex]->variadic) {
|
|
return $lastIndex;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
protected function validateNativeNamedCallArgs(FunctionDef $functionDef, array $callArgs): void
|
|
{
|
|
$hasNamedArg = false;
|
|
$hasUnpack = false;
|
|
$seenNamedArgs = [];
|
|
$providedArgIndexes = [];
|
|
$argNameIndex = $this->getFunctionArgNameIndex($functionDef);
|
|
$variadicArgIndex = $this->getVariadicArgIndex($functionDef);
|
|
|
|
foreach ($callArgs as $i => $arg) {
|
|
if ($this->isPlaceholderExpr($arg)) {
|
|
continue;
|
|
}
|
|
if ($arg instanceof Node\Arg && $arg->unpack) {
|
|
if ($hasNamedArg) {
|
|
$this->fatalError($arg, 'Cannot use argument unpacking after named arguments');
|
|
}
|
|
$hasUnpack = true;
|
|
$providedArgIndexes[$i] = true;
|
|
continue;
|
|
}
|
|
if ($arg->name === null) {
|
|
if ($hasUnpack) {
|
|
$this->fatalError($arg, 'Cannot use positional argument after argument unpacking');
|
|
}
|
|
if ($hasNamedArg) {
|
|
$this->fatalError($arg, 'Cannot use positional argument after named argument');
|
|
}
|
|
$providedArgIndexes[$i] = true;
|
|
continue;
|
|
}
|
|
if (!$this->isIdExpr($arg->name)) {
|
|
$this->fatalError($arg, 'Named argument must be a string');
|
|
}
|
|
|
|
$argName = $arg->name->name;
|
|
if (isset($seenNamedArgs[$argName])) {
|
|
$this->fatalError($arg, "Duplicate named argument `{$argName}`");
|
|
}
|
|
if (!array_key_exists($argName, $argNameIndex)) {
|
|
if ($variadicArgIndex === null) {
|
|
$this->fatalError($arg, "Unknown named argument `{$argName}`");
|
|
}
|
|
$seenNamedArgs[$argName] = true;
|
|
$hasNamedArg = true;
|
|
continue;
|
|
}
|
|
|
|
$argIndex = $argNameIndex[$argName];
|
|
if ($variadicArgIndex !== null and $argIndex === $variadicArgIndex) {
|
|
$seenNamedArgs[$argName] = true;
|
|
$hasNamedArg = true;
|
|
continue;
|
|
}
|
|
if (isset($providedArgIndexes[$argIndex])) {
|
|
$this->fatalError($arg, "Named argument `{$argName}` overwrites previous argument");
|
|
}
|
|
|
|
$seenNamedArgs[$argName] = true;
|
|
$providedArgIndexes[$argIndex] = true;
|
|
$hasNamedArg = true;
|
|
}
|
|
}
|
|
|
|
protected function getNativeMethod(CallLike $expr, string $class, string $method, bool $checkArgs = true): string|false
|
|
{
|
|
if (!$this->hasClass($class)) {
|
|
return false;
|
|
}
|
|
|
|
$classDef = $this->getClass($class);
|
|
$methodDef = null;
|
|
// 递归查找,若子类中未定义方法,则尝试查找父类是否存在此方法
|
|
while (true) {
|
|
if (!$classDef->hasMethod($method)) {
|
|
if (!$classDef->extends) {
|
|
return false;
|
|
}
|
|
if (!$this->hasClass($classDef->extends)) {
|
|
if ($classDef->inheritedFromInternalClass) {
|
|
if (!Reflection::hasMethod($classDef->extends, $method) and !Reflection::hasMethod($classDef->extends, $method . '__call')) {
|
|
$this->fatalError($expr, 'Class `' . $classDef->getNamespacedName() . '` inherits from a internal class, but the class `' .
|
|
$classDef->extends . '` does not have a `' . $method . '` method or a `__call` magic method');
|
|
} else {
|
|
$this->climate->cyan('Dynamically calling internal class method `' . $classDef->extends . '::' . $method . '()`');
|
|
throw new DynamicCall();
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
$classDef = $this->getClass($classDef->extends);
|
|
} else {
|
|
$methodDef = $classDef->getMethod($method);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$this->checkAccessible($classDef, $methodDef->flags)) {
|
|
$this->fatalError($expr, 'Method `' . $classDef->getNamespacedName() . '::' . $method . '()` is not accessible');
|
|
}
|
|
// 函数调用占位符,不是真实的函数调用
|
|
if (count($expr->args) === 1 and $this->isPlaceholderExpr($expr->args[0])) {
|
|
return false;
|
|
}
|
|
if ($checkArgs) {
|
|
$this->checkNativeCallArgs($expr, $methodDef->functionDef, $expr->args, $classDef->getNamespacedName() . '::' . $method);
|
|
}
|
|
return $this->getNativeName($method, $classDef->namespace, $classDef->name);
|
|
}
|
|
|
|
protected function findNativeClassConst(
|
|
NodeAbstract $expr,
|
|
string $class,
|
|
string $const,
|
|
?string $accessingClass = null
|
|
): string|false
|
|
{
|
|
if (!$this->hasClass($class)) {
|
|
return false;
|
|
}
|
|
|
|
$classDef = $this->getClass($class);
|
|
$originClassDef = $classDef;
|
|
$constDef = null;
|
|
// 递归查找,若子类中未定义方法,则尝试查找父类是否存在此方法
|
|
while (true) {
|
|
if (!$classDef->hasConstant($const)) {
|
|
if (!$classDef->extends) {
|
|
break;
|
|
}
|
|
if (!$this->hasClass($classDef->extends)) {
|
|
break;
|
|
}
|
|
$classDef = $this->getClass($classDef->extends);
|
|
} else {
|
|
$constDef = $classDef->getConstant($const);
|
|
break;
|
|
}
|
|
}
|
|
if ($constDef === null) {
|
|
foreach ($this->getClassImplementedInterfaces($originClassDef) as $interfaceName) {
|
|
if (!$this->hasInterface($interfaceName)) {
|
|
continue;
|
|
}
|
|
$interfaceDef = $this->getInterface($interfaceName);
|
|
if (!$interfaceDef->hasConstant($const)) {
|
|
continue;
|
|
}
|
|
$interfaceConstDef = $interfaceDef->constants[$const];
|
|
if ($interfaceConstDef->type === Type::ARRAY) {
|
|
return self::PREFIX . $this->getNativeName($interfaceConstDef->name, $interfaceDef->namespace, $interfaceDef->name);
|
|
}
|
|
$expr->setAttribute('nativeConst', $interfaceConstDef);
|
|
return $interfaceConstDef->value;
|
|
}
|
|
}
|
|
if ($constDef === null) {
|
|
return false;
|
|
}
|
|
if ($classDef instanceof ClassDef
|
|
&& !$this->checkAccessibleByClassName(
|
|
$classDef->getNamespacedName(false),
|
|
$constDef->flags,
|
|
$accessingClass,
|
|
)) {
|
|
$this->fatalError($expr, 'Constant `' . $classDef->getNamespacedName() . '::' . $const . '` is not accessible');
|
|
}
|
|
if ($constDef->type === Type::ARRAY) {
|
|
return self::PREFIX . $this->getNativeName($constDef->name, $classDef->namespace, $classDef->name);
|
|
} else {
|
|
$expr->setAttribute('nativeConst', $constDef);
|
|
return $constDef->value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array<string>
|
|
*/
|
|
protected function getClassImplementedInterfaces(ClassDef $classDef): array
|
|
{
|
|
$interfaces = [];
|
|
$current = $classDef;
|
|
while (true) {
|
|
foreach ($current->implements as $interfaceName) {
|
|
$this->collectInterfaceAndParents($interfaceName, $interfaces);
|
|
}
|
|
if (!$current->extends || !$this->hasClass($current->extends)) {
|
|
break;
|
|
}
|
|
$current = $this->getClass($current->extends);
|
|
}
|
|
|
|
return array_values($interfaces);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $interfaces
|
|
*/
|
|
private function collectInterfaceAndParents(string $interfaceName, array &$interfaces): void
|
|
{
|
|
if (isset($interfaces[$interfaceName])) {
|
|
return;
|
|
}
|
|
$interfaces[$interfaceName] = $interfaceName;
|
|
if (!$this->hasInterface($interfaceName)) {
|
|
return;
|
|
}
|
|
$interfaceDef = $this->getInterface($interfaceName);
|
|
foreach ($interfaceDef->extendsList ?: ($interfaceDef->extends ? [$interfaceDef->extends] : []) as $parentInterface) {
|
|
$this->collectInterfaceAndParents($parentInterface, $interfaces);
|
|
}
|
|
}
|
|
|
|
protected function resetReturnType(Node\Stmt\Return_ $node, string $type): void
|
|
{
|
|
$oriType = $this->functionDef->returnType;
|
|
$this->functionDef->returnType = $type;
|
|
// 返回值变更,需要重新解析
|
|
$this->climate->cyan("Return type changed ({$oriType} -> {$type}) at line {$node->getLine()} retrying...");
|
|
throw new Redo();
|
|
}
|
|
|
|
protected function detectVarType($var): string
|
|
{
|
|
// Unwrap ArrayDimFetch to get the underlying variable type;
|
|
// the dim/index does not affect the base variable's type.
|
|
if ($var instanceof Expr\ArrayDimFetch) {
|
|
return $this->detectVarType($var->var);
|
|
}
|
|
$name = $this->parseIdentifier($var);
|
|
if ($this->isStdContainer($name)) {
|
|
return Type::ARRAY;
|
|
}
|
|
return $this->getVarType($name);
|
|
}
|
|
|
|
protected function detectTypeOfExpr($expr): string
|
|
{
|
|
$exprType = $expr->getType();
|
|
switch ($exprType) {
|
|
case 'Expr_UnaryMinus':
|
|
case 'Expr_UnaryPlus':
|
|
$innerType = $this->detectTypeOfExpr($expr->expr);
|
|
if (
|
|
!$this->nativeTypes
|
|
&& $exprType === 'Expr_UnaryMinus'
|
|
&& $innerType === Type::INT
|
|
&& $this->constantIntValue($expr->expr) === PHP_INT_MIN
|
|
) {
|
|
return Type::FLOAT;
|
|
}
|
|
return $innerType;
|
|
case 'Expr_BooleanNot':
|
|
case 'Expr_BinaryOp_LogicalAnd':
|
|
case 'Expr_BinaryOp_BooleanAnd':
|
|
case 'Expr_BinaryOp_LogicalOr':
|
|
case 'Expr_BinaryOp_BooleanOr':
|
|
case 'Expr_BinaryOp_LogicalXor':
|
|
case 'Expr_BinaryOp_Equal':
|
|
case 'Expr_BinaryOp_NotEqual':
|
|
case 'Expr_BinaryOp_Identical':
|
|
case 'Expr_BinaryOp_NotIdentical':
|
|
case 'Expr_BinaryOp_Smaller':
|
|
case 'Expr_BinaryOp_SmallerOrEqual':
|
|
case 'Expr_BinaryOp_Greater':
|
|
case 'Expr_BinaryOp_GreaterOrEqual':
|
|
return Type::BOOL;
|
|
case 'Expr_BitwiseNot':
|
|
$inner = $this->detectTypeOfExpr($expr->expr);
|
|
return $inner === Type::BIGINT ? Type::BIGINT : Type::INT;
|
|
case 'Expr_Print':
|
|
case 'Expr_Cast_Int':
|
|
return Type::INT;
|
|
case 'Scalar_Int':
|
|
return $this->bigintTypes ? Type::BIGINT : Type::INT;
|
|
case 'Expr_Cast_Float':
|
|
case 'Expr_Cast_Double':
|
|
return Type::FLOAT;
|
|
case 'Scalar_Float':
|
|
if ($this->isBigIntLiteral($expr)) {
|
|
return Type::BIGINT;
|
|
}
|
|
if ($this->isDecimalLiteral($expr) || $this->decimalTypes) {
|
|
return Type::DECIMAL;
|
|
}
|
|
return Type::FLOAT;
|
|
case 'Expr_Cast_Bool':
|
|
case 'Scalar_Bool':
|
|
return Type::BOOL;
|
|
case 'Expr_Array':
|
|
case 'Expr_Cast_Array':
|
|
return Type::ARRAY;
|
|
case 'Expr_BinaryOp_Concat':
|
|
case 'Expr_AssignOp_Concat':
|
|
return Type::STR;
|
|
case 'Expr_Ternary':
|
|
$ifType = $expr->if === null
|
|
? $this->detectTypeOfExpr($expr->cond)
|
|
: $this->detectTypeOfExpr($expr->if);
|
|
$elseType = $this->detectTypeOfExpr($expr->else);
|
|
return $ifType === $elseType ? $ifType : Type::VAR;
|
|
case 'Expr_BinaryOp_Plus':
|
|
case 'Expr_BinaryOp_Minus':
|
|
case 'Expr_BinaryOp_Mul':
|
|
case 'Expr_BinaryOp_Div':
|
|
case 'Expr_BinaryOp_Mod':
|
|
case 'Expr_BinaryOp_Pow':
|
|
case 'Expr_BinaryOp_ShiftLeft':
|
|
case 'Expr_BinaryOp_ShiftRight':
|
|
case 'Expr_BinaryOp_BitwiseAnd':
|
|
case 'Expr_BinaryOp_BitwiseOr':
|
|
case 'Expr_BinaryOp_BitwiseXor':
|
|
$leftType = $this->detectTypeOfExpr($expr->left);
|
|
$rightType = $this->detectTypeOfExpr($expr->right);
|
|
if ($leftType === Type::BIGFLOAT || $rightType === Type::BIGFLOAT) {
|
|
return Type::BIGFLOAT;
|
|
}
|
|
if ($leftType === Type::DECIMAL || $rightType === Type::DECIMAL) {
|
|
return Type::DECIMAL;
|
|
}
|
|
if ($leftType === Type::BIGINT || $rightType === Type::BIGINT) {
|
|
if ($exprType === 'Expr_BinaryOp_Div') {
|
|
// BigInt division produces BigInt (integer division); BigDecimal in future
|
|
return Type::BIGINT;
|
|
}
|
|
return Type::BIGINT;
|
|
}
|
|
if ($leftType === Type::FLOAT || $rightType === Type::FLOAT) {
|
|
return Type::FLOAT;
|
|
}
|
|
if (!$this->nativeTypes && $leftType === Type::INT && $rightType === Type::INT) {
|
|
$op = match ($exprType) {
|
|
'Expr_BinaryOp_Plus' => '+',
|
|
'Expr_BinaryOp_Minus' => '-',
|
|
'Expr_BinaryOp_Mul' => '*',
|
|
'Expr_BinaryOp_Div' => '/',
|
|
'Expr_BinaryOp_Mod' => '%',
|
|
default => null,
|
|
};
|
|
if ($op !== null) {
|
|
$evaluation = $this->evaluateConstantIntArithmetic($expr->left, $expr->right, $op);
|
|
if ($evaluation !== null && is_float($evaluation['result'])) {
|
|
return Type::FLOAT;
|
|
}
|
|
}
|
|
}
|
|
if ($leftType === Type::INT || $rightType === Type::INT) {
|
|
return Type::INT;
|
|
}
|
|
break;
|
|
case 'Expr_FuncCall':
|
|
if ($this->isNameExpr($expr->name)) {
|
|
$name = $this->parseIdentifier($expr->name);
|
|
$globalName = ltrim($name, '\\');
|
|
// Math function optimization: propagate Big* return types
|
|
if (in_array($name, ['abs', 'pow', 'sqrt', 'floor', 'ceil', 'round'], true) && !empty($expr->args)) {
|
|
$argType = $this->detectTypeOfExpr($expr->args[0]->value);
|
|
if (
|
|
$argType === Type::BIGINT
|
|
&& in_array($name, ['abs', 'pow', 'sqrt'], true)
|
|
) {
|
|
return Type::BIGINT;
|
|
}
|
|
if (
|
|
$argType === Type::DECIMAL
|
|
&& in_array($name, ['abs', 'pow', 'sqrt', 'floor', 'ceil', 'round'], true)
|
|
) {
|
|
return Type::DECIMAL;
|
|
}
|
|
if (
|
|
$argType === Type::BIGFLOAT
|
|
&& in_array($name, ['abs', 'sqrt'], true)
|
|
) {
|
|
return Type::BIGFLOAT;
|
|
}
|
|
}
|
|
if (in_array($name, self::STREAM_FUNCTIONS)) {
|
|
return Type::STREAM;
|
|
}
|
|
if ($globalName === 'expected' || $globalName === 'unexpected') {
|
|
return Type::BOOL;
|
|
}
|
|
if (count($expr->args) === 1 and $this->isPlaceholderExpr($expr->args[0])) {
|
|
return Type::OBJECT;
|
|
}
|
|
if ($this->hasFunction($name)) {
|
|
return $this->getFunction($name)->returnType;
|
|
}
|
|
return $this->detectFuncCallReturnType($name);
|
|
}
|
|
break;
|
|
case 'Expr_MethodCall':
|
|
if ($this->isNamedMethod($expr->name)) {
|
|
$method = $this->parseIdentifier($expr->name);
|
|
// keyword methods (to* builtins + __ extensions) — return type is known regardless of receiver
|
|
$kwType = $this->findKeywordMethod($method);
|
|
if ($kwType !== null) {
|
|
return $kwType;
|
|
}
|
|
// Class definition resolution (handles this_, typed VarExpr)
|
|
$classDef = $this->resolveObjectClassDef($expr->var);
|
|
if ($classDef !== null && $classDef->hasMethod($method)) {
|
|
if (count($expr->args) === 1 and $this->isPlaceholderExpr($expr->args[0])) {
|
|
return Type::OBJECT;
|
|
}
|
|
return $classDef->getMethod($method)->getReturnType();
|
|
}
|
|
if ($this->isVarExpr($expr->var)) {
|
|
$object = $this->parseIdentifier($expr->var);
|
|
try {
|
|
$nativeFunc = $this->findNativeMethod($expr, $object, $method);
|
|
if ($nativeFunc) {
|
|
$funcDef = $this->getFunction($nativeFunc);
|
|
return $funcDef->returnType;
|
|
}
|
|
} catch (DynamicCall) {
|
|
// Method inherited from internal class, can't resolve type statically
|
|
}
|
|
if ($this->isTypedObject($object)) {
|
|
return $this->detectMethodCallReturnType($this->getObjectType($object), $method);
|
|
}
|
|
$type = $this->getVarType($object);
|
|
} else {
|
|
$type = $this->detectTypeOfExpr($expr->var);
|
|
}
|
|
if ($type !== Type::VAR && !$this->checkArgType($type, Type::OBJECT)) {
|
|
$retType = $this->detectUniversalMethodReturnType($type, $method);
|
|
if ($retType !== null) {
|
|
return $retType;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 'Expr_StaticCall':
|
|
if ($this->isNameExpr($expr->class) && $this->isIdExpr($expr->name)) {
|
|
// First-class callable syntax creates a Closure, not a method return value
|
|
if (count($expr->args) === 1 and $this->isPlaceholderExpr($expr->args[0])) {
|
|
return Type::OBJECT;
|
|
}
|
|
$className = $this->parseIdentifier($expr->class);
|
|
if (strtolower($className) === 'std') {
|
|
$method = strtolower($this->parseIdentifier($expr->name));
|
|
return match ($method) {
|
|
'int' => Type::INT,
|
|
'float' => Type::FLOAT,
|
|
'bool' => Type::BOOL,
|
|
'bigint' => Type::BIGINT,
|
|
'decimal' => Type::DECIMAL,
|
|
'bigfloat' => Type::BIGFLOAT,
|
|
default => Type::VAR,
|
|
};
|
|
}
|
|
if ($className === 'self') {
|
|
$className = $this->getFullClassName();
|
|
} elseif ($className === 'parent') {
|
|
if ($this->classDef->extends) {
|
|
$className = $this->classDef->extends;
|
|
} else {
|
|
break;
|
|
}
|
|
} elseif ($className === 'static') {
|
|
break;
|
|
} else {
|
|
$className = $this->getNamespacedClassName($className);
|
|
}
|
|
if ($this->hasClass($className)) {
|
|
$classDef = $this->getClass($className);
|
|
$methodName = $this->parseIdentifier($expr->name);
|
|
if ($classDef->hasMethod($methodName)) {
|
|
return $classDef->getMethod($methodName)->getReturnType();
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 'Expr_PropertyFetch':
|
|
if ($this->isIdExpr($expr->name)) {
|
|
// Class definition property type
|
|
$propName = $this->parseIdentifier($expr->name);
|
|
$classDef = $this->resolveObjectClassDef($expr->var);
|
|
if ($classDef !== null && $classDef->hasProperty($propName)) {
|
|
return $classDef->getProperty($propName)->type;
|
|
}
|
|
// Native property var type
|
|
if ($this->isVarExpr($expr->var)) {
|
|
$this->parsePropertyFetch($expr);
|
|
$propVar = $this->getNativePropertyVar($expr);
|
|
if ($propVar !== null) {
|
|
$info = $this->getObjectPropInfoByVar($propVar);
|
|
if ($info !== null) {
|
|
return $info['type'];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 'Expr_StaticPropertyFetch':
|
|
if ($this->isIdExpr($expr->name)) {
|
|
if (!$this->getNativePropertyDef($expr)) {
|
|
$this->resolveNativeStaticPropertyFetch($expr);
|
|
}
|
|
$def = $this->getNativePropertyDef($expr);
|
|
if ($def) {
|
|
return $def->type;
|
|
}
|
|
}
|
|
break;
|
|
case 'Expr_ArrayDimFetch':
|
|
if ($this->isStdArrayExpr($expr)) {
|
|
if (!$expr->hasAttribute('stdArrayDimFetch')) {
|
|
$this->parseStdArrayDimFetch($expr);
|
|
}
|
|
$attr = $expr->getAttribute('stdArrayDimFetch');
|
|
if ($attr['accessLevel'] === $attr['totalLevel']) {
|
|
return $this->context->stdArrays[$attr['var']]['type'];
|
|
} else {
|
|
return Type::ARRAY;
|
|
}
|
|
}
|
|
if ($this->isStdContainerExpr($expr)) {
|
|
if (!$expr->hasAttribute('stdContainerDimFetch')) {
|
|
$this->parseStdContainerDimFetch($expr);
|
|
}
|
|
$attr = $expr->getAttribute('stdContainerDimFetch');
|
|
return $this->context->stdContainers[$attr['var']]['type'];
|
|
}
|
|
break;
|
|
case 'Expr_New':
|
|
return Type::OBJECT;
|
|
case 'Expr_Assign':
|
|
case 'Expr_AssignOp_BitwiseAnd':
|
|
case 'Expr_AssignOp_BitwiseOr':
|
|
case 'Expr_AssignOp_BitwiseXor':
|
|
return $this->detectVarType($expr->var);
|
|
case 'Expr_Variable':
|
|
return $this->detectVarType($expr);
|
|
case 'Expr_ConstFetch':
|
|
return $this->detectConstType($expr);
|
|
case 'Scalar_String':
|
|
return Type::STR;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return Type::VAR;
|
|
}
|
|
|
|
protected function genDynamicPropIncDec($var, string $op, bool $isPre): ?string
|
|
{
|
|
if (!$this->isPropertyFetch($var)) {
|
|
return null;
|
|
}
|
|
|
|
$target = $this->preparePropertyWriteTarget($var);
|
|
$getter = $this->getPropertyHookGetter($var);
|
|
$setter = $this->getPropertyHookSetter($var);
|
|
if ($getter !== null && $setter === null) {
|
|
$this->fatalError($var, 'Cannot write to read-only hooked property');
|
|
}
|
|
if ($getter !== null && $setter !== null) {
|
|
$tmpVar = $this->genTmpVarName();
|
|
$this->addLocalVar($tmpVar, Type::VAR);
|
|
$read = $this->emitPropertyHookGetterCall($var, $getter);
|
|
if ($isPre) {
|
|
$set = $this->emitPropertyHookSetterCall($var, $setter, new Expr\Variable($tmpVar));
|
|
$this->context->beforeStmtLines[] = "{$tmpVar} = {$read} {$op} 1; {$set};";
|
|
} else {
|
|
$nextVar = $this->genTmpVarName();
|
|
$this->addLocalVar($nextVar, Type::VAR);
|
|
$set = $this->emitPropertyHookSetterCall($var, $setter, new Expr\Variable($nextVar));
|
|
$this->context->beforeStmtLines[] = "{$tmpVar} = {$read};";
|
|
$this->context->afterStmtLines[] = "{$nextVar} = {$tmpVar} {$op} 1; {$set};";
|
|
}
|
|
return $tmpVar;
|
|
}
|
|
if ($this->isNativePropertyAccess($var)) {
|
|
return null;
|
|
}
|
|
|
|
$tmpVar = $this->genTmpVarName();
|
|
$this->addLocalVar($tmpVar, Type::VAR);
|
|
if ($isPre) {
|
|
$this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->emitDynamicPropertyFetchRead($var, $target) . " {$op} 1; " . $this->emitDynamicPropertyFetchWrite($var, $tmpVar, $target) . ';';
|
|
} else {
|
|
$this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->emitDynamicPropertyFetchRead($var, $target) . ';';
|
|
$this->context->afterStmtLines[] = $this->emitDynamicPropertyFetchWrite($var, "{$tmpVar} {$op} 1", $target) . ';';
|
|
}
|
|
|
|
return $tmpVar;
|
|
}
|
|
|
|
protected function parsePreInc(Expr\PreInc $expr): string
|
|
{
|
|
$this->assertNotNullsafeWriteContext($expr->var);
|
|
$result = $this->genDynamicPropIncDec($expr->var, '+', true);
|
|
if ($result !== null) {
|
|
return $result;
|
|
}
|
|
|
|
$type = $this->detectVarType($expr->var);
|
|
if ($type === Type::BIGINT || $type === Type::DECIMAL || $type === Type::BIGFLOAT) {
|
|
$this->fatalError($expr, 'Cannot use ++ on ' . $type . '. Use += 1 instead (Big* types are immutable).');
|
|
}
|
|
$result = '++' . $this->parseWritableIdentifier($expr->var);
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* $GLOBALS['var'] 等价于 global $var; $var ,将字符串常量转为变量名称即可
|
|
* 仅限于字面量字符串可以转为变量名称,其他则使用 php::global() 函数获取
|
|
*/
|
|
protected function findNativeFunction(string $funcName): string|false
|
|
{
|
|
// 绝对命名空间的函数
|
|
if ($funcName[0] == '\\') {
|
|
$funcName = ltrim($funcName, '\\');
|
|
$possibleFunctionNames = [$this->escapeName($funcName)];
|
|
} else {
|
|
$possibleFunctionNames = [$this->escapeName($funcName)];
|
|
if (isset($this->useAliases[$funcName])) {
|
|
$possibleFunctionNames[] = $this->escapeName($this->escapeNamespace($this->useAliases[$funcName]));
|
|
}
|
|
if ($this->namespace) {
|
|
$possibleFunctionNames[] = $this->escapeNamespace($this->namespace) . self::NAMESPACE_SEPARATOR . $this->escapeName($funcName);
|
|
}
|
|
if (isset($this->useFunctions[$funcName])) {
|
|
$possibleFunctionNames[] = $this->escapeNamespace($this->useFunctions[$funcName]);
|
|
}
|
|
// 复杂命名空间规则,组合命名空间
|
|
// 例子:use foo\bar; bar\fn();
|
|
foreach ($this->useNamespaces as $use) {
|
|
$ns1 = explode('\\', $use);
|
|
$ns2 = explode('\\', $funcName);
|
|
if ($ns1[array_key_last($ns1)] === $ns2[array_key_first($ns2)]) {
|
|
$ns = array_merge($ns1, $ns2);
|
|
array_splice($ns, array_key_last($ns1) + 1);
|
|
$possibleFunctionNames[] = $this->escapeNamespace(implode('\\', $ns));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($possibleFunctionNames as $nativeFunc) {
|
|
if (str_contains($nativeFunc, '\\')) {
|
|
$nativeFunc = $this->escapeNamespace($nativeFunc);
|
|
}
|
|
$this->checkFunction($nativeFunc);
|
|
if ($this->hasFunction($nativeFunc) && !$this->getFunction($nativeFunc)->method) {
|
|
return $nativeFunc;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected function checkInternalFunctionArgCount(string $funcName, Node\Expr\FuncCall $expr): void
|
|
{
|
|
$ref = Reflection::getFunction($funcName);
|
|
if (!$ref) {
|
|
return;
|
|
}
|
|
$this->validateInternalNamedCallArgs($ref, $expr->args);
|
|
if ($this->hasUnpackCallArg($expr->args)) {
|
|
return;
|
|
}
|
|
$minArgs = $ref->getNumberOfRequiredParameters();
|
|
$maxArgs = $ref->getNumberOfParameters();
|
|
$actualArgCount = count($expr->args);
|
|
if ($minArgs > 0 && $actualArgCount < $minArgs) {
|
|
$this->fatalError($expr, "{$funcName}() expects at least {$minArgs} argument(s), {$actualArgCount} given");
|
|
}
|
|
if (!$ref->isVariadic() && $maxArgs > 0 && $actualArgCount > $maxArgs) {
|
|
$this->fatalError($expr, "{$funcName}() expects at most {$maxArgs} argument(s), {$actualArgCount} given");
|
|
}
|
|
}
|
|
|
|
protected function hasUnpackBeforeNamedArg(array $args): bool
|
|
{
|
|
$hasUnpack = false;
|
|
foreach ($args as $arg) {
|
|
if (!$arg instanceof Node\Arg) {
|
|
continue;
|
|
}
|
|
if ($arg->unpack) {
|
|
$hasUnpack = true;
|
|
} elseif ($hasUnpack && $arg->name !== null) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected function hasUnpackCallArg(array $args): bool
|
|
{
|
|
foreach ($args as $arg) {
|
|
if ($arg instanceof Node\Arg && $arg->unpack) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected function shouldUseDynamicCallForNativeArgs(string $nativeFunc, array $args): bool
|
|
{
|
|
if (!$this->hasUnpackCallArg($args)) {
|
|
return false;
|
|
}
|
|
if ($this->hasUnpackBeforeNamedArg($args)) {
|
|
return true;
|
|
}
|
|
|
|
$variadicArgIndex = $this->getVariadicArgIndex($this->getFunction($nativeFunc));
|
|
foreach ($args as $i => $arg) {
|
|
if (!$arg instanceof Node\Arg || !$arg->unpack) {
|
|
continue;
|
|
}
|
|
if ($variadicArgIndex === null || $i < $variadicArgIndex) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected function genRuntimeFunctionCall(
|
|
string $callable,
|
|
array $args,
|
|
string $funcName = '',
|
|
string $className = '',
|
|
bool $separateNamedArgs = true
|
|
): string {
|
|
return 'php::call(' . $callable . ', ' . $this->parseCallArgs($args, $funcName, $className, $separateNamedArgs) . ')';
|
|
}
|
|
|
|
protected function genRuntimeObjectMethodCall(
|
|
string $object,
|
|
string $method,
|
|
array $args,
|
|
string $funcName = '',
|
|
string $className = ''
|
|
): string {
|
|
return $object . '.call(' . $method . ', ' . $this->parseCallArgs($args, $funcName, $className) . ')';
|
|
}
|
|
|
|
protected function validateInternalNamedCallArgs(\ReflectionFunctionAbstract $ref, array $callArgs): void
|
|
{
|
|
$hasNamedArg = false;
|
|
$hasUnpack = false;
|
|
$seenNamedArgs = [];
|
|
$providedArgIndexes = [];
|
|
$argNameIndex = [];
|
|
$requiredArgIndexes = [];
|
|
$variadicArgIndex = null;
|
|
|
|
foreach ($ref->getParameters() as $i => $param) {
|
|
$argNameIndex[$param->getName()] = $i;
|
|
if (!$param->isOptional() && !$param->isVariadic()) {
|
|
$requiredArgIndexes[$i] = $param->getName();
|
|
}
|
|
if ($param->isVariadic()) {
|
|
$variadicArgIndex = $i;
|
|
}
|
|
}
|
|
|
|
foreach ($callArgs as $i => $arg) {
|
|
if ($this->isPlaceholderExpr($arg)) {
|
|
continue;
|
|
}
|
|
if ($arg instanceof Node\Arg && $arg->unpack) {
|
|
if ($hasNamedArg) {
|
|
$this->fatalError($arg, 'Cannot use argument unpacking after named arguments');
|
|
}
|
|
$hasUnpack = true;
|
|
$providedArgIndexes[$i] = true;
|
|
continue;
|
|
}
|
|
if ($arg->name === null) {
|
|
if ($hasUnpack) {
|
|
$this->fatalError($arg, 'Cannot use positional argument after argument unpacking');
|
|
}
|
|
if ($hasNamedArg) {
|
|
$this->fatalError($arg, 'Cannot use positional argument after named argument');
|
|
}
|
|
$providedArgIndexes[$i] = true;
|
|
continue;
|
|
}
|
|
if (!$this->isIdExpr($arg->name)) {
|
|
$this->fatalError($arg, 'Named argument must be a string');
|
|
}
|
|
|
|
$argName = $arg->name->name;
|
|
if (isset($seenNamedArgs[$argName])) {
|
|
$this->fatalError($arg, "Duplicate named argument `{$argName}`");
|
|
}
|
|
if (!array_key_exists($argName, $argNameIndex)) {
|
|
if ($variadicArgIndex === null) {
|
|
$this->fatalError($arg, "Unknown named argument `{$argName}`");
|
|
}
|
|
$seenNamedArgs[$argName] = true;
|
|
$hasNamedArg = true;
|
|
continue;
|
|
}
|
|
|
|
$argIndex = $argNameIndex[$argName];
|
|
if ($variadicArgIndex !== null && $argIndex === $variadicArgIndex) {
|
|
$seenNamedArgs[$argName] = true;
|
|
$hasNamedArg = true;
|
|
continue;
|
|
}
|
|
if (isset($providedArgIndexes[$argIndex])) {
|
|
$this->fatalError($arg, "Named argument `{$argName}` overwrites previous argument");
|
|
}
|
|
|
|
$seenNamedArgs[$argName] = true;
|
|
$providedArgIndexes[$argIndex] = true;
|
|
$hasNamedArg = true;
|
|
}
|
|
|
|
if ($hasNamedArg && !$hasUnpack) {
|
|
foreach ($requiredArgIndexes as $index => $name) {
|
|
if (!isset($providedArgIndexes[$index])) {
|
|
$this->fatalError($callArgs[array_key_last($callArgs)] ?? null, "Named argument `{$name}` is missing default value");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PHP 8.5 pipe operator: $value |> $callable.
|
|
*
|
|
* The left operand is evaluated first and passed as the single value
|
|
* argument to the callable on the right. Materialising the left operand
|
|
* also avoids relying on C++ argument-evaluation order.
|
|
*/
|
|
|
|
protected function parsePostOp(Expr\PostDec|Expr\PostInc $expr, string $op): string
|
|
{
|
|
$this->assertNotNullsafeWriteContext($expr->var);
|
|
$result = $this->genDynamicPropIncDec($expr->var, $op, false);
|
|
if ($result !== null) {
|
|
return $result;
|
|
}
|
|
|
|
if ($this->isVarExpr($expr->var) or $this->isPropertyFetch($expr->var) or $this->isArrayDimFetch($expr->var)) {
|
|
$var = $this->parseWritableIdentifier($expr->var);
|
|
if ($this->isVarExpr($expr->var) and !$this->hasVar($var)) {
|
|
$this->errorUndefinedVariable($expr->var);
|
|
}
|
|
$type = $this->detectVarType($expr->var);
|
|
if ($type === Type::BIGINT || $type === Type::DECIMAL || $type === Type::BIGFLOAT) {
|
|
$opName = $op === '+' ? '++' : '--';
|
|
$this->fatalError($expr, "Cannot use {$opName} on {$type}. Use " . ($op === '+' ? '+= 1' : '-= 1') . ' instead (Big* types are immutable).');
|
|
}
|
|
return $var . str_repeat($op, 2);
|
|
}
|
|
if ($this->isStaticPropertyFetch($expr->var)) {
|
|
$native = $this->parseNativeStaticPropertyFetch($expr->var);
|
|
if ($native !== null) {
|
|
return $native . str_repeat($op, 2);
|
|
}
|
|
|
|
$class = $this->identifierToStr($expr->var->class);
|
|
$prop = $this->identifierToStr($expr->var->name);
|
|
$tmpVar = $this->genTmpVarName();
|
|
$this->addLocalVar($tmpVar, Type::VAR);
|
|
$this->context->beforeStmtLines[] = $tmpVar . ' = ' . Symbol::getStaticProperty() . '(' . $class . ', ' . $prop . ');';
|
|
$this->context->afterStmtLines[] = Symbol::setStaticProperty() . '(' . $class . ', ' . $prop . ', ' . $tmpVar . ' ' . $op . ' 1);';
|
|
|
|
return $tmpVar;
|
|
}
|
|
$this->fatalError($expr, 'Post-increment operator is not supported for non-variable expressions');
|
|
}
|
|
|
|
protected function parsePostDec(Expr\PostDec $expr): string
|
|
{
|
|
return $this->parsePostOp($expr, '-');
|
|
}
|
|
|
|
protected function parsePostInc(Expr\PostInc $expr): string
|
|
{
|
|
return $this->parsePostOp($expr, '+');
|
|
}
|
|
|
|
protected function parsePreDec(Expr\PreDec $expr): string
|
|
{
|
|
$this->assertNotNullsafeWriteContext($expr->var);
|
|
$result = $this->genDynamicPropIncDec($expr->var, '-', true);
|
|
if ($result !== null) {
|
|
return $result;
|
|
}
|
|
|
|
$type = $this->detectVarType($expr->var);
|
|
if ($type === Type::BIGINT || $type === Type::DECIMAL || $type === Type::BIGFLOAT) {
|
|
$this->fatalError($expr, 'Cannot use -- on ' . $type . '. Use -= 1 instead (Big* types are immutable).');
|
|
}
|
|
$result = '--' . $this->parseWritableIdentifier($expr->var);
|
|
return $result;
|
|
}
|
|
|
|
protected function parsePrint(Expr\Print_ $expr): string
|
|
{
|
|
$this->assertExprCanBeUsedAsValue($expr->expr, 'print operand');
|
|
return 'php::print(' . $this->parseExprAsValue($expr->expr) . ')';
|
|
}
|
|
|
|
protected function formatCppLineComment(string $label, string $text): string
|
|
{
|
|
$lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $text));
|
|
$padding = str_repeat(' ', strlen($label));
|
|
$comments = [];
|
|
foreach ($lines as $i => $line) {
|
|
$comments[] = '// ' . ($i === 0 ? $label : $padding) . $line;
|
|
}
|
|
return implode(PHP_EOL, $comments);
|
|
}
|
|
|
|
protected function packData(string $bytes): string
|
|
{
|
|
$out = '';
|
|
for ($i = 0; $i < strlen($bytes); $i++) {
|
|
$out .= ord($bytes[$i]) . ', ';
|
|
if ($i % 32 == 0) {
|
|
$out .= "\n\t";
|
|
}
|
|
}
|
|
$out .= '0,';
|
|
return $out;
|
|
}
|
|
|
|
protected function addConstData(string $name, string $bytes): void
|
|
{
|
|
$this->constData[$name] = $this->packData($bytes);
|
|
}
|
|
|
|
protected function parseNew(Expr\New_ $expr): string
|
|
{
|
|
$ctorClassName = '';
|
|
// 匿名类
|
|
if ($expr->class instanceof Node\Stmt\Class_) {
|
|
if ($expr->class->name === null) {
|
|
$classDef = $expr->class;
|
|
$className = $this->genAnonClassName();
|
|
$classDef->name = new Node\Identifier($className);
|
|
// 继承父类和接口可能是 use 的名称,需要转换成全限定名称
|
|
if ($classDef->extends !== null) {
|
|
$parentClass = $this->getNamespacedClassName($classDef->extends->toString());
|
|
$classDef->extends = new Node\Name\FullyQualified($parentClass);
|
|
}
|
|
if (!empty($classDef->implements)) {
|
|
foreach ($classDef->implements as $i => $iface) {
|
|
$ifaceName = $this->getNamespacedClassName($iface->toString());
|
|
$classDef->implements[$i] = new Node\Name\FullyQualified($ifaceName);
|
|
}
|
|
}
|
|
// 将匿名类内部的类型引用(方法参数、返回值、属性等)转为全限定名称
|
|
$this->resolveAnonClassTypeNames($classDef);
|
|
$this->context->beforeStmtLines[] = 'static THREAD_LOCAL bool ' . $className . '_defined = false;';
|
|
$classCode = $this->genEmbeddedCode($classDef);
|
|
$this->addConstData($className . '_code', $classCode);
|
|
$this->context->beforeStmtLines[] = 'if (!' . $className . '_defined) {'
|
|
. $className . '_defined = true; php::eval((const char *)' . $className . '_code);}';
|
|
$className = '\\' . $className;
|
|
$cePtr = $this->getClassEntryPtr($className);
|
|
$ctorClassName = $className;
|
|
} else {
|
|
$this->fatalError($expr, 'must be anonymous class');
|
|
}
|
|
} else {
|
|
$className = $this->parseIdentifier($expr->class);
|
|
if ($this->isNameExpr($expr->class)) {
|
|
if ($className === 'static') {
|
|
$cePtr = Symbol::getCalledCe();
|
|
} else {
|
|
if ($className === 'self') {
|
|
$className = $this->getFullClassName();
|
|
} elseif ($className === 'parent') {
|
|
if (!$this->classDef) {
|
|
$this->fatalError($expr, 'Cannot use "parent" outside a class');
|
|
}
|
|
$className = $this->classDef->extends;
|
|
} else {
|
|
$className = $this->getNamespacedClassName($className);
|
|
}
|
|
$ctorClassName = $className;
|
|
if ($this->isAbstractClass($className)) {
|
|
$this->fatalError($expr, "abstract class `{$className}` cannot be instantiated");
|
|
}
|
|
$constructor = $this->findConstructor($className);
|
|
if ($constructor !== null
|
|
&& !$this->checkAccessibleByClassName($constructor['className'], $constructor['flags'])) {
|
|
$this->fatalError(
|
|
$expr,
|
|
'Cannot call ' . $this->visibilityLabel($constructor['flags']) . ' '
|
|
. $constructor['className'] . '::__construct()'
|
|
);
|
|
}
|
|
$cePtr = $this->getClassEntryPtr($className);
|
|
}
|
|
} else {
|
|
$cePtr = $className;
|
|
}
|
|
}
|
|
|
|
$args = $expr->args;
|
|
if (empty($args)) {
|
|
return 'php::newObject(' . $cePtr . ')';
|
|
}
|
|
return 'php::newObject(' . $cePtr . ', ' . $this->parseCallArgs($args, '__construct', $ctorClassName) . ')';
|
|
}
|
|
|
|
protected function parseClone(Expr\Clone_ $expr): string
|
|
{
|
|
$this->assertExprCanBeUsedAsValue($expr->expr, 'clone operand');
|
|
return 'php::clone(' . $this->parseExprAsValue($expr->expr) . ')';
|
|
}
|
|
|
|
protected function parseInstanceof(Expr\Instanceof_ $expr): string
|
|
{
|
|
$this->assertExprCanBeUsedAsValue($expr->expr, 'instanceof operand');
|
|
if ($this->isNameExpr($expr->class)) {
|
|
$value = $this->parseExprAsValue($expr->expr);
|
|
$classPtr = $this->resolveInstanceofClassPtr($expr->class);
|
|
return 'php::instanceOf(' . $value . ', ' . $classPtr . ')';
|
|
} else {
|
|
[$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr->expr);
|
|
$tmpVar = $this->addTmpVar(Type::VAR);
|
|
$this->appendCapturedStmtLinesToContext($beforeStmts);
|
|
$this->context->beforeStmtLines[] = $tmpVar . ' = ' . $value . ';';
|
|
$this->appendCapturedStmtLinesToContext($afterStmts);
|
|
return 'php::instanceOf(' . $tmpVar . ', ' . $this->identifierToStr($expr->class) . ')';
|
|
}
|
|
}
|
|
|
|
protected function resolveInstanceofClassPtr(NodeAbstract $class): string
|
|
{
|
|
$className = $this->parseIdentifier($class);
|
|
if ($className === 'self') {
|
|
$className = $this->getFullClassName();
|
|
} elseif ($className === 'parent') {
|
|
if (!$this->classDef || !$this->classDef->extends) {
|
|
$this->fatalError($class, 'Cannot use "parent" when current class scope has no parent');
|
|
}
|
|
$className = $this->classDef->extends;
|
|
} elseif ($className === 'static') {
|
|
if (!$this->classDef) {
|
|
$this->fatalError($class, 'Cannot use "static" outside a class');
|
|
}
|
|
return Symbol::getCalledCe();
|
|
} else {
|
|
$className = $this->getNamespacedClassName($className);
|
|
}
|
|
return $this->getClassEntryPtr($className);
|
|
}
|
|
|
|
protected function parseInterpolatedString(Node\Scalar\InterpolatedString $expr): string
|
|
{
|
|
$parts = $expr->parts;
|
|
$list = [];
|
|
foreach ($parts as $part) {
|
|
if (!$part instanceof Node\InterpolatedStringPart) {
|
|
$this->assertExprCanBeUsedAsValue($part, 'string interpolation value');
|
|
}
|
|
$list[] = $this->parseExpr($part);
|
|
}
|
|
|
|
return 'php::concat({' . implode(', ', $list) . '})';
|
|
}
|
|
|
|
protected function parseInterpolatedStringPart(Node\InterpolatedStringPart $expr): string
|
|
{
|
|
return '"' . $this->escapeString($expr->value) . '"';
|
|
}
|
|
|
|
protected function parseGlobal(Node\Stmt\Global_ $expr): string
|
|
{
|
|
foreach ($expr->vars as $v) {
|
|
$name = $this->parseVariable($v);
|
|
if (!$this->hasGlobalVar($name)) {
|
|
$this->addGlobalVar($name, Type::VAR);
|
|
}
|
|
if (!$this->hasScopeGlobalVar($name)) {
|
|
$this->addScopeGlobalVar($name, Type::VAR);
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
protected function getArgInfo(Node $arg, string $funcName, int $index): ArgInfo
|
|
{
|
|
if (!$this->hasFunction($funcName)) {
|
|
$this->fatalError($arg, "Function `{$funcName}` is undefined, you must adjust the order of function definition");
|
|
}
|
|
$funcDef = $this->getFunction($funcName);
|
|
if (!array_key_exists($index, $funcDef->argInfoList)) {
|
|
$this->fatalError($arg, "Argument `{$index}` of function `{$funcName}` not found");
|
|
}
|
|
|
|
return $funcDef->argInfoList[$index];
|
|
}
|
|
|
|
protected function parseExit(Expr\Exit_ $node): string
|
|
{
|
|
if (!$node->expr) {
|
|
return 'php::aotExit()';
|
|
}
|
|
$status = $this->parseExprAsValue($node->expr);
|
|
return 'php::aotExit(' . $status . ')';
|
|
}
|
|
|
|
protected function parseStatic(Node\Stmt\Static_ $v): string
|
|
{
|
|
$list = [];
|
|
foreach ($v->vars as $var) {
|
|
$varName = $this->escapeVarName($var->var->name);
|
|
$type = $var->default ? $this->detectTypeOfExpr($var->default) : Type::VAR;
|
|
if ($var->default) {
|
|
$this->assertExprCanBeUsedAsValue($var->default, 'static variable default value');
|
|
}
|
|
$globalVar = $this->addStaticVar($var->var, $varName, $type);
|
|
|
|
$list[] = Type::VAR . ' &' . $varName . ' = ' . $this->escapeGlobalVar($globalVar) . ';';
|
|
if ($var->default) {
|
|
$initState = self::STATIC_VAR . $varName . '_initialized';
|
|
$initCode = $this->getIndent() . 'static bool ' . $initState . ' = false;';
|
|
$initCode .= $this->getIndent() . "if (!{$initState}) { \n";
|
|
$this->indentLevel++;
|
|
$initCode .= $this->getIndent() . "{$initState} = true;\n";
|
|
$initCode .= $this->genStaticVarInitLambda($var, $varName);
|
|
$this->indentLevel--;
|
|
$initCode .= $this->getIndent() . '}';
|
|
$list[] = $initCode;
|
|
}
|
|
}
|
|
|
|
return implode(PHP_EOL . $this->getIndent(), $list);
|
|
}
|
|
|
|
protected function genStaticVarInitLambda(Node\Stmt\StaticVar $var, string $varName): string
|
|
{
|
|
$oriCtx = $this->context;
|
|
|
|
$this->context = new FunctionContext();
|
|
$this->context->arguments = $oriCtx->localVars;
|
|
|
|
$code = '([&](){' . PHP_EOL;
|
|
$body = $this->getIndent() . $varName . ' = ' . $this->parseExpr($var->default) . ';';
|
|
$code .= $this->genScopeVarDecl();
|
|
$code .= $this->parseBeforeStmtLines();
|
|
$code .= $body;
|
|
$code .= $this->parseAfterStmtLines();
|
|
$code .= '})();' . PHP_EOL;
|
|
|
|
$this->context = $oriCtx;
|
|
|
|
return $code;
|
|
}
|
|
|
|
protected function parseEnum(Node\Stmt\Enum_ $v): string
|
|
{
|
|
return 'php::eval("' . $this->escapeString($this->genEmbeddedCode($v)) . '");';
|
|
}
|
|
|
|
protected function parseEval(Expr\Eval_ $expr): string
|
|
{
|
|
$this->assertExprCanBeUsedAsValue($expr->expr, 'eval operand');
|
|
// 对 eval() 指令的 PHP 代码段禁止字面量优化
|
|
$expr->expr->setAttribute('noLiteralString', true);
|
|
return 'php::eval(' . $this->identifierToStr($expr->expr) . ')';
|
|
}
|
|
|
|
protected function parseInclude(Expr\Include_ $expr): string
|
|
{
|
|
$this->assertExprCanBeUsedAsValue($expr->expr, 'include operand');
|
|
switch ($expr->type) {
|
|
case Expr\Include_::TYPE_INCLUDE:
|
|
$type = 'php::INCLUDE';
|
|
break;
|
|
case Expr\Include_::TYPE_INCLUDE_ONCE:
|
|
$type = 'php::INCLUDE_ONCE';
|
|
break;
|
|
case Expr\Include_::TYPE_REQUIRE:
|
|
$type = 'php::REQUIRE';
|
|
break;
|
|
case Expr\Include_::TYPE_REQUIRE_ONCE:
|
|
$type = 'php::REQUIRE_ONCE';
|
|
break;
|
|
default:
|
|
$this->fatalError($expr, 'Invalid include type');
|
|
break;
|
|
}
|
|
|
|
return 'php::include(' . $this->parseIdentifier($expr->expr) . ', ' . $type . ')';
|
|
}
|
|
|
|
protected function parseScalarFloat(Node\Scalar\Float_ $expr): string
|
|
{
|
|
$value = $expr->value;
|
|
|
|
if (is_nan($value)) {
|
|
return self::VALUE_NAN;
|
|
}
|
|
if (is_infinite($value)) {
|
|
return $value > 0 ? self::VALUE_INF : '-' . self::VALUE_INF;
|
|
}
|
|
if (floor($value) == $value && abs($value) < 1e15) {
|
|
return number_format($value, 1, '.', '');
|
|
}
|
|
return sprintf('%.' . $this->floatPrecision . 'g', $value);
|
|
}
|
|
|
|
protected function parseIsset(Expr\Isset_ $expr): string
|
|
{
|
|
$vars = $expr->vars;
|
|
if (count($vars) > 1) {
|
|
$list = [];
|
|
foreach ($vars as $var) {
|
|
$list[] = $this->parseChainedExpr($var, self::OP_ISSET);
|
|
}
|
|
return '(' . implode(' && ', $list) . ')';
|
|
}
|
|
return $this->parseChainedExpr($vars[0], self::OP_ISSET);
|
|
}
|
|
|
|
protected function parseEmpty(Expr\Empty_ $expr): string
|
|
{
|
|
$type = $this->detectTypeOfExpr($expr->expr);
|
|
if (in_array($type, [Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL], true)) {
|
|
return '!(' . $this->convertBoolExpr($this->parseExprAsValue($expr->expr), $type) . ')';
|
|
}
|
|
return $this->parseChainedExpr($expr->expr, self::OP_EMPTY);
|
|
}
|
|
|
|
/**
|
|
* 左值只能为变量、数组、对象属性、对象静态属性
|
|
*/
|
|
protected function checkLeftValue(NodeAbstract $expr): void
|
|
{
|
|
$this->assertNotNullsafeWriteContext($expr);
|
|
if (!$this->isVarExpr($expr) && !$this->isArrayDimFetch($expr) && !$this->isPropertyFetch($expr) && !$this->isStaticPropertyFetch($expr)) {
|
|
$this->fatalError($expr, 'The left value of assignment operation can only be variable, array item, object property, class static property');
|
|
}
|
|
}
|
|
|
|
protected function assertNotNullsafeWriteContext(NodeAbstract $expr): void
|
|
{
|
|
if ($expr instanceof Expr\NullsafePropertyFetch) {
|
|
$this->fatalError($expr, "Can't use nullsafe operator in write context");
|
|
}
|
|
}
|
|
|
|
protected function getChainedFunc(string $op): string
|
|
{
|
|
return match ($op) {
|
|
self::OP_ISSET => 'php::exists',
|
|
self::OP_NOT_EMPTY => 'php::notEmpty',
|
|
default => 'php::' . $op,
|
|
};
|
|
}
|
|
|
|
protected function parseChainedExpr(NodeAbstract $node, string $op, bool $getValue = false): string
|
|
{
|
|
// TypePHP 编译器不允许操作未定义的变量,PHP 的 isset($var) 可能 $var 未定义
|
|
$this->checkVarMustExist($node, $this->parseIdentifier($node));
|
|
$fn = $this->getChainedFunc($op);
|
|
$expr = $node;
|
|
if ($this->isVarExpr($expr)) {
|
|
if (!$getValue) {
|
|
return $fn . '(' . $this->parseExpr($expr) . ')';
|
|
}
|
|
// $getValue is true: fall through to use the chain+result mechanism,
|
|
// which ensures the result type is TYPE_VAR (compatible with ternaries).
|
|
}
|
|
// 单属性读取(非链式)
|
|
if ($this->isPropertyFetch($expr) and $this->isVarExpr($expr->var) and $this->isIdExpr($expr->name)) {
|
|
$prop = $this->parsePropertyFetch($expr);
|
|
if ($this->isNativePropertyAccess($expr)) {
|
|
if ($op === self::OP_REFVAL) {
|
|
return $prop . '.toReference()';
|
|
}
|
|
return $fn . '(' . $prop . ')';
|
|
}
|
|
}
|
|
if ($this->isStaticPropertyFetch($expr) and $this->isNameExpr($expr->class) and $this->isIdExpr($expr->name)) {
|
|
$prop = $this->parseStaticPropertyFetch($expr);
|
|
if ($this->isNativePropertyAccess($expr)) {
|
|
if ($op === self::OP_REFVAL) {
|
|
return $prop . '.toReference()';
|
|
}
|
|
return $fn . '(' . $prop . ')';
|
|
}
|
|
}
|
|
|
|
$list = [];
|
|
while (true) {
|
|
if ($this->isArrayDimFetch($expr)) {
|
|
if ($expr->dim === null) {
|
|
$this->fatalError($expr, 'Cannot use [] for reading');
|
|
}
|
|
$dim = $this->parseIdentifier($expr->dim);
|
|
$list[] = '{php::ArrayDimFetch, ' . Type::VAR . '(' . $dim . ')}';
|
|
} elseif ($this->isPropertyFetch($expr)) {
|
|
$name = $this->identifierToStr($expr->name, literal: true);
|
|
$list[] = '{php::PropertyFetch, ' . Type::VAR . '(' . $name . ')}';
|
|
} elseif ($this->isVarExpr($expr)) {
|
|
$var = $this->parseIdentifier($expr);
|
|
break;
|
|
} else {
|
|
$var = $this->genTmpVarName();
|
|
$this->addLocalVar($var, Type::VAR);
|
|
$this->context->beforeStmtLines[] = $var . '=' . $this->parseExpr($expr) . ';';
|
|
break;
|
|
}
|
|
$expr = $expr->var;
|
|
}
|
|
|
|
$list = array_reverse($list);
|
|
|
|
if ($getValue) {
|
|
$result = $this->addTmpVar(Type::VAR);
|
|
$node->setAttribute('chainOpResult', $result);
|
|
return $fn . '(' . $var . ', {' . implode(', ', $list) . '}, ' . $result . ')';
|
|
} else {
|
|
// toReference(var, {}) 返回空引用,空链时改用成员函数形式
|
|
if ($op === self::OP_REFVAL && empty($list)) {
|
|
return $var . '.toReference()';
|
|
}
|
|
return $fn . '(' . $var . ', {' . implode(', ', $list) . '})';
|
|
}
|
|
}
|
|
|
|
protected function parseCastArray(Expr\Cast\Array_ $expr): string
|
|
{
|
|
$this->assertExprCanBeUsedAsValue($expr->expr, 'cast operand');
|
|
return $this->convertArrayExpr($this->parseExprAsValue($expr->expr));
|
|
}
|
|
|
|
protected function hasGlobalVar(string $name): bool
|
|
{
|
|
return array_key_exists($name, $this->globalVars);
|
|
}
|
|
|
|
protected function hasScopeGlobalVar(string $name): bool
|
|
{
|
|
return array_key_exists($name, $this->context->globalVars);
|
|
}
|
|
|
|
protected function hasStaticVar(string $name): bool
|
|
{
|
|
return array_key_exists($name, $this->context->staticVars);
|
|
}
|
|
|
|
protected function parseCastDouble(mixed $expr): string
|
|
{
|
|
$this->assertExprCanBeUsedAsValue($expr->expr, 'cast operand');
|
|
return $this->convertFloatExpr(
|
|
$this->parseIdentifier($expr->expr),
|
|
$this->detectTypeOfExpr($expr->expr)
|
|
);
|
|
}
|
|
|
|
protected function detectFuncCallReturnType(string $name): string
|
|
{
|
|
$name = ltrim($name, '\\');
|
|
$returnType = Reflection::getFunctionReturnType($name);
|
|
if ($returnType !== null) {
|
|
return $this->getTypeFromZendType($returnType);
|
|
}
|
|
|
|
return Type::VAR;
|
|
}
|
|
|
|
protected function detectMethodCallReturnType(string $class, string $method): string
|
|
{
|
|
$returnType = Reflection::getMethodReturnType($class, $method);
|
|
if ($returnType) {
|
|
return $this->getTypeFromZendType($returnType);
|
|
}
|
|
return Type::VAR;
|
|
}
|
|
|
|
protected function genObjvalCall(Expr\FuncCall $expr): string
|
|
{
|
|
if (count($expr->args) !== 2) {
|
|
$this->fatalError($expr, 'objval() requires exactly 2 arguments');
|
|
}
|
|
$receiver = $this->parseExpr($expr->args[0]->value);
|
|
$className = $this->resolveClassNameArg($expr->args[1]->value);
|
|
return 'php::toObject(' . $receiver . ', ' . $this->getClassEntryPtr($className) . ')';
|
|
}
|
|
|
|
protected function identifierToStr(NodeAbstract $node, bool $require = true, bool $literal = false): string
|
|
{
|
|
$id = $this->parseIdentifier($node);
|
|
if ($this->isVarExpr($node)) {
|
|
if ($require) {
|
|
$this->requireVar($node, $id);
|
|
}
|
|
return $id;
|
|
}
|
|
if ($id === 'self') {
|
|
$id = $this->getNamespacedClassName($this->class);
|
|
} elseif ($id === 'static') {
|
|
return Symbol::getCalledClass();
|
|
}
|
|
if ($this->isNameExpr($node) or $this->isIdExpr($node)) {
|
|
return $literal ? $this->getLiteralString($id) : $this->genCharPtr($id, true);
|
|
}
|
|
if ($this->isZeroLiteral($node)) {
|
|
return self::VALUE_ZERO;
|
|
}
|
|
return $id;
|
|
}
|
|
|
|
protected function requireVar($node, string $var): void
|
|
{
|
|
if (!$this->hasVar($var)) {
|
|
$this->fatalError($node, 'The variable `' . $var . '` is not defined');
|
|
}
|
|
}
|
|
|
|
protected function createPropertyAccessResolver(): PropertyAccessResolver
|
|
{
|
|
$this->assertCompilerPhase(self::PHASE_CONVERT, 'PropertyAccessResolver');
|
|
return new PropertyAccessResolver($this);
|
|
}
|
|
|
|
protected function isSameClassName(string $classA, string $classB): bool
|
|
{
|
|
return strcasecmp(ltrim($classA, '\\'), ltrim($classB, '\\')) === 0;
|
|
}
|
|
|
|
protected function isSameOrSubclassOf(string $class, string $parent): bool
|
|
{
|
|
$class = strtolower(ltrim($class, '\\'));
|
|
$parent = strtolower(ltrim($parent, '\\'));
|
|
while ($class !== '') {
|
|
if ($class === $parent) {
|
|
return true;
|
|
}
|
|
$class = $this->getParentClass($class);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected function canAccessProtectedProperty(string $scope, string $declaringClass): bool
|
|
{
|
|
if ($scope === '') {
|
|
return false;
|
|
}
|
|
return $this->isSameOrSubclassOf($scope, $declaringClass)
|
|
|| $this->isSameOrSubclassOf($declaringClass, $scope);
|
|
}
|
|
|
|
protected function resolveNativeInstanceProperty(NodeAbstract $expr, string $property, string $class): ?PropertyAccessResult
|
|
{
|
|
$scope = $this->class ? $this->getFullClassName() : '';
|
|
return $this->createPropertyAccessResolver()->resolveNativeInstanceProperty($expr, $property, $class, $scope);
|
|
}
|
|
|
|
protected function resolveNativeStaticProperty(NodeAbstract $expr, string $property, string $class): ?PropertyAccessResult
|
|
{
|
|
$scope = $this->class ? $this->getFullClassName() : '';
|
|
return $this->createPropertyAccessResolver()->resolveNativeStaticProperty($expr, $property, $class, $scope);
|
|
}
|
|
|
|
protected function applyNativePropertyAccessResult(NodeAbstract $expr, PropertyAccessResult $result): string
|
|
{
|
|
$offset = $this->getPropertyOffset($result->classDef->getNamespacedName(false), $result->property);
|
|
$expr->setAttribute('nativePropertyAccess', new NativePropertyAccess($offset, $result));
|
|
return $offset;
|
|
}
|
|
|
|
protected function isNativePropertyAccess(NodeAbstract $expr): bool
|
|
{
|
|
return $this->getNativePropertyAccess($expr) !== null;
|
|
}
|
|
|
|
protected function getNativePropertyDef(NodeAbstract $expr): ?PropertyDef
|
|
{
|
|
return $this->getNativePropertyAccess($expr)?->getPropertyDef();
|
|
}
|
|
|
|
protected function getNativePropertyClassDef(NodeAbstract $expr): ?ClassDef
|
|
{
|
|
return $this->getNativePropertyAccess($expr)?->getClassDef();
|
|
}
|
|
|
|
public function getNativePropertyAccess(NodeAbstract $expr): ?NativePropertyAccess
|
|
{
|
|
$access = $expr->getAttribute('nativePropertyAccess');
|
|
return $access instanceof NativePropertyAccess ? $access : null;
|
|
}
|
|
|
|
protected function setNativePropertyVar(NodeAbstract $expr, string $var): void
|
|
{
|
|
$expr->setAttribute('nativePropertyVar', $var);
|
|
}
|
|
|
|
protected function getNativePropertyVar(NodeAbstract $expr): ?string
|
|
{
|
|
$var = $expr->getAttribute('nativePropertyVar');
|
|
return is_string($var) ? $var : null;
|
|
}
|
|
|
|
protected function setNativePropertyValueSource(NodeAbstract $expr, string $source): void
|
|
{
|
|
$expr->setAttribute('nativePropertyValueSource', $source);
|
|
}
|
|
|
|
protected function isNativePropertyTypedValue(NodeAbstract $expr): bool
|
|
{
|
|
return $expr->getAttribute('nativePropertyValueSource') === self::NATIVE_PROPERTY_VALUE_VAR;
|
|
}
|
|
|
|
protected function parseShellExec(Expr\ShellExec $expr): string
|
|
{
|
|
if ($this->isWasiTarget()) {
|
|
$this->fatalError($expr, 'Backtick shell execution is not supported by the WASI target');
|
|
}
|
|
$list = [];
|
|
foreach ($expr->parts as $part) {
|
|
$list[] = $this->identifierToStr($part);
|
|
}
|
|
return 'php::fn::shell_exec(php::concat({' . implode(', ', $list) . '}))';
|
|
}
|
|
|
|
protected function parseGoto(Node\Stmt\Goto_ $v): string
|
|
{
|
|
return 'goto ' . $v->name->name . ';';
|
|
}
|
|
|
|
protected function parseLabel(Node\Stmt\Label $v): string
|
|
{
|
|
return $v->name->name . ':';
|
|
}
|
|
|
|
protected function parseModifiers(int $flags): int
|
|
{
|
|
if (!($flags & Modifiers::PRIVATE) and !($flags & Modifiers::PROTECTED)) {
|
|
$flags |= Modifiers::PUBLIC;
|
|
}
|
|
return $flags;
|
|
}
|
|
|
|
protected function setBuildDir(string $string): void
|
|
{
|
|
if (!is_dir($string)) {
|
|
mkdir($string, 0777, true);
|
|
}
|
|
$resolved = realpath($string);
|
|
if ($resolved === false) {
|
|
throw new \RuntimeException('Failed to resolve build path: ' . $string);
|
|
}
|
|
$this->buildDir = $resolved;
|
|
}
|
|
|
|
protected function isStubFile(string $file): bool
|
|
{
|
|
return str_ends_with($file, '.stub.php');
|
|
}
|
|
|
|
/**
|
|
* @throws \Exception
|
|
*/
|
|
protected function loadFile(string $file): string
|
|
{
|
|
if (!file_exists($file)) {
|
|
throw new \Exception('File not exists: ' . $file);
|
|
}
|
|
$phpCode = file_get_contents($file);
|
|
if (!$phpCode) {
|
|
throw new \Exception('Can not read file: ' . $file);
|
|
}
|
|
if (!mb_check_encoding($phpCode, 'UTF-8')) {
|
|
throw new \Exception('File encoding must be UTF-8, got: ' . mb_detect_encoding($phpCode, ['UTF-8', 'ISO-8859-1', 'GBK', 'Shift_JIS'], true) . ' in ' . $file);
|
|
}
|
|
$this->file = realpath($file);
|
|
$this->dir = dirname($this->file);
|
|
$this->stubFile = $this->isStubFile($file);
|
|
|
|
return $phpCode;
|
|
}
|
|
|
|
protected function parseErrorSuppress(Expr\ErrorSuppress $expr): string
|
|
{
|
|
$tmpVar = $this->genTmpVarName();
|
|
$this->context->beforeStmtLines[] = 'auto ' . $tmpVar . ' = EG(error_reporting);';
|
|
$this->context->beforeStmtLines[] = 'php::call(' . $this->getFuncPtr('error_reporting') . ', {E_FATAL_ERRORS});';
|
|
$code = $this->parseExpr($expr->expr);
|
|
$this->context->afterStmtLines[] = 'php::call(' . $this->getFuncPtr('error_reporting') . ', {' . $tmpVar . '});';
|
|
return $code;
|
|
}
|
|
|
|
protected function checkVar(NodeAbstract $node, string $name, string $defaultType = Type::VAR): void
|
|
{
|
|
if (!$this->hasVar($name)) {
|
|
$this->addLocalVar($name, $defaultType);
|
|
} else {
|
|
if ($this->getVarType($name) !== $defaultType) {
|
|
$this->fatalError($node, 'Cannot assign value to variable $' . $name . ' of type ' . $this->getVarType($name) . ' with type ' . $defaultType);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function checkVarMustExist(NodeAbstract $node, string $name): void
|
|
{
|
|
if ($this->isVarExpr($node) and !$this->hasVar($name)) {
|
|
$this->errorUndefinedVariable($node);
|
|
}
|
|
}
|
|
|
|
protected function checkVarAssignExpr(NodeAbstract $left, string $toType, string $fromType): bool
|
|
{
|
|
if ($toType === Type::VAR or $fromType === Type::VAR) {
|
|
return true;
|
|
}
|
|
// 引用当前没有类型信息,按照 var 处理
|
|
if ($toType === Type::REF or $fromType === Type::REF) {
|
|
return true;
|
|
}
|
|
// 类型一致,可以互相赋值
|
|
if ($toType === $fromType) {
|
|
return true;
|
|
}
|
|
// 原生类型可以互相转换,由 C++ 底层完成
|
|
if ($this->isNativeType($toType) and $this->isNativeType($fromType)) {
|
|
return true;
|
|
}
|
|
// BigInt/BigFloat/Decimal 与原生类型之间可能发生隐式转换,允许重新赋值
|
|
$bigTypes = [Type::BIGINT, Type::DECIMAL, Type::BIGFLOAT];
|
|
if (in_array($toType, $bigTypes, true) or in_array($fromType, $bigTypes, true)) {
|
|
return true;
|
|
}
|
|
$varName = 'variable';
|
|
if ($this->isVarExpr($left)) {
|
|
$varName = '`$' . $this->parseIdentifier($left) . '`';
|
|
}
|
|
$this->fatalError($left, "Cannot re-assign $varName from `{$fromType}` to `{$toType}`");
|
|
}
|
|
|
|
/**
|
|
* Check a value against a composite PHP type when the value's static type
|
|
* is precise enough to prove a mismatch. Composite declarations still use
|
|
* Variant in generated C++, so unknown values must be left to the runtime
|
|
* type check emitted from the same descriptor.
|
|
*
|
|
* The outer descriptor list is a union (OR); an allOf entry represents an
|
|
* intersection (AND). Nullable is represented by an isNull union member.
|
|
*/
|
|
protected function mustNoCall(NodeAbstract $node): void
|
|
{
|
|
$nodeFinder = new NodeFinder();
|
|
$r1 = $nodeFinder->findInstanceOf($node, Expr\StaticCall::class);
|
|
$r2 = $nodeFinder->findInstanceOf($node, Expr\MethodCall::class);
|
|
$r3 = $nodeFinder->findInstanceOf($node, Expr\FuncCall::class);
|
|
if (count($r1) + count($r2) + count($r3) > 0) {
|
|
$this->fatalError($node, 'Calling function or method is not allowed');
|
|
}
|
|
}
|
|
|
|
protected function checkAccessible(ClassDef $classDef, int $flags): bool
|
|
{
|
|
return $this->checkAccessibleByClassName($classDef->getNamespacedName(false), $flags);
|
|
}
|
|
|
|
protected function checkAccessibleByClassName(
|
|
string $declaringClass,
|
|
int $flags,
|
|
?string $accessingClass = null
|
|
): bool
|
|
{
|
|
if ($accessingClass !== null) {
|
|
$accessingClass = ltrim($accessingClass, '\\');
|
|
$scopeClassDef = $this->hasClass($accessingClass)
|
|
? $this->getClass($accessingClass)
|
|
: null;
|
|
} else {
|
|
$scopeClassDef = $this->classDef;
|
|
if ($this->functionDef !== null
|
|
&& $this->functionDef->attributeFactoryScope !== ''
|
|
&& $this->hasClass($this->functionDef->attributeFactoryScope)) {
|
|
$scopeClassDef = $this->getClass($this->functionDef->attributeFactoryScope);
|
|
}
|
|
}
|
|
// 私有方法,只能当前的类使用
|
|
if ($flags & Modifiers::PRIVATE) {
|
|
return $scopeClassDef !== null
|
|
&& $this->isSameClassName($declaringClass, $scopeClassDef->getNamespacedName(false));
|
|
}
|
|
// 保护方法,只能当前类和子类使用
|
|
if ($flags & Modifiers::PROTECTED) {
|
|
if (!$scopeClassDef) {
|
|
return false;
|
|
}
|
|
return $this->canAccessProtectedProperty(
|
|
$scopeClassDef->getNamespacedName(false),
|
|
$declaringClass
|
|
);
|
|
}
|
|
// 类外部调用,只允许调用 public 方法
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 沿继承链查找实际调用的构造函数,包括项目类继承的内部类构造函数。
|
|
*
|
|
* @return array{className: string, flags: int}|null
|
|
*/
|
|
protected function findConstructor(string $className): ?array
|
|
{
|
|
$current = $className;
|
|
while ($current !== '') {
|
|
if ($this->hasClass($current)) {
|
|
$classDef = $this->getClass($current);
|
|
if ($classDef->hasMethod('__construct')) {
|
|
return [
|
|
'className' => $classDef->getNamespacedName(false),
|
|
'flags' => $classDef->getMethod('__construct')->flags,
|
|
];
|
|
}
|
|
$current = $classDef->extends;
|
|
continue;
|
|
}
|
|
if (!$this->isInternalClass($current)) {
|
|
return null;
|
|
}
|
|
|
|
$constructor = Reflection::getClass($current)?->getConstructor();
|
|
if ($constructor === null) {
|
|
return null;
|
|
}
|
|
return [
|
|
'className' => $constructor->getDeclaringClass()->getName(),
|
|
'flags' => $constructor->getModifiers(),
|
|
];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
protected function visibilityLabel(int $flags): string
|
|
{
|
|
if ($flags & Modifiers::PRIVATE) {
|
|
return 'private';
|
|
}
|
|
if ($flags & Modifiers::PROTECTED) {
|
|
return 'protected';
|
|
}
|
|
return 'public';
|
|
}
|
|
|
|
protected function genDebugInfo(?NodeAbstract $stmt = null, string $functionName = '', int $startLine = 0): string
|
|
{
|
|
$code = '';
|
|
if ($this->debug) {
|
|
if ($stmt) {
|
|
$code .= 'php::traceDebugInfo("' . $this->escapeString($this->file) . '", ' . $stmt->getLine() . ');' . PHP_EOL;
|
|
} elseif ($functionName) {
|
|
$code .= 'php::enableDebugInfo();' . PHP_EOL;
|
|
$code .= 'php::pushDebugFrame("' . $this->escapeString($this->file) . '", ' . $startLine . ', "' . $this->escapeString($functionName) . '");' . PHP_EOL;
|
|
$code .= 'ON_SCOPE_EXIT(php::popDebugFrame());' . PHP_EOL;
|
|
} else {
|
|
$code .= 'php::enableDebugInfo();' . PHP_EOL;
|
|
}
|
|
}
|
|
return $code;
|
|
}
|
|
|
|
protected function genLocalVarDecl(array $localVars): string
|
|
{
|
|
$code = '';
|
|
foreach ($localVars as $name => $type) {
|
|
if (isset($this->context->arguments[$name])) {
|
|
continue;
|
|
}
|
|
if (isset($this->context->globalVars[$name])) {
|
|
continue;
|
|
}
|
|
$code .= $this->getIndent();
|
|
if ($type === Type::STD_ARRAY) {
|
|
$info = $this->context->stdArrays[$name];
|
|
if (isset($info['boxExpr'])) {
|
|
$code .= 'auto &' . $name . '_ref = php::toStdContainer<' . $info['decl'] . '>(' . $info['boxExpr'] . ', ' . $info['typeId'] . ');';
|
|
} else {
|
|
$containerType = 'php::StdContainerBox<' . $info['decl'] . '>';
|
|
$code .= 'php::Var ' . $name . ' = php::Var(new ' . $containerType . '(' . $info['typeId'] . '));' . PHP_EOL;
|
|
$code .= $this->getIndent() . 'auto &' . $name . '_ref = ' . $name . '.toBox<' . $containerType . '>()->container;';
|
|
}
|
|
if (!isset($info['boxExpr']) && ($defaultValue = $this->getStdContainerDefaultValueExpr($info['type'])) !== null) {
|
|
$code .= PHP_EOL . $this->getIndent() . 'php::initializeStdContainer(' . $name . '_ref, ' . $defaultValue . ');';
|
|
}
|
|
} elseif ($type === Type::STD_VECTOR) {
|
|
$info = $this->context->stdContainers[$name];
|
|
if (isset($info['boxExpr'])) {
|
|
$code .= 'auto &' . $name . '_ref = php::toStdContainer<' . $info['decl'] . '>(' . $info['boxExpr'] . ', ' . $info['typeId'] . ');';
|
|
} else {
|
|
$containerType = 'php::StdContainerBox<' . $info['decl'] . '>';
|
|
if ($info['size'] !== null) {
|
|
$boxCtor = 'new ' . $containerType . '(' . $info['typeId'] . ', ' . $info['size'] . ')';
|
|
} else {
|
|
$boxCtor = 'new ' . $containerType . '(' . $info['typeId'] . ')';
|
|
}
|
|
$code .= 'php::Var ' . $name . ' = php::Var(' . $boxCtor . ');' . PHP_EOL;
|
|
$code .= $this->getIndent() . 'auto &' . $name . '_ref = ' . $name . '.toBox<' . $containerType . '>()->container;';
|
|
}
|
|
if (!isset($info['boxExpr']) && $info['size'] !== null
|
|
&& ($defaultValue = $this->getStdContainerDefaultValueExpr($info['type'])) !== null) {
|
|
$code .= PHP_EOL . $this->getIndent() . 'php::initializeStdContainer(' . $name . '_ref, ' . $defaultValue . ');';
|
|
}
|
|
} elseif ($type === Type::STD_MAP || $type === Type::STD_ORDERED_MAP) {
|
|
$info = $this->context->stdContainers[$name];
|
|
if (isset($info['boxExpr'])) {
|
|
$code .= 'auto &' . $name . '_ref = php::toStdContainer<' . $info['decl'] . '>(' . $info['boxExpr'] . ', ' . $info['typeId'] . ');';
|
|
} else {
|
|
$containerType = 'php::StdContainerBox<' . $info['decl'] . '>';
|
|
$code .= 'php::Var ' . $name . ' = php::Var(new ' . $containerType . '(' . $info['typeId'] . '));' . PHP_EOL;
|
|
$code .= $this->getIndent() . 'auto &' . $name . '_ref = ' . $name . '.toBox<' . $containerType . '>()->container;';
|
|
}
|
|
} elseif ($type === Type::STREAM || $type === Type::BIGINT || $type === Type::DECIMAL || $type === Type::BIGFLOAT) {
|
|
$code .= Type::VAR . ' ' . $name . ';';
|
|
} else {
|
|
$code .= $type . ' ' . $name;
|
|
if ($type === Type::INT or $type === Type::FLOAT or $type === Type::BOOL) {
|
|
$code .= ' = 0';
|
|
}
|
|
$code .= ';';
|
|
}
|
|
$code .= PHP_EOL;
|
|
}
|
|
return $code;
|
|
}
|
|
|
|
protected function genScopeVarDecl(): string
|
|
{
|
|
$code = '';
|
|
if ($this->context->hasMultiLevelBreak) {
|
|
$code .= $this->getIndent() . 'int _brk_flag = 0;' . PHP_EOL;
|
|
}
|
|
if ($this->context->hasMultiLevelContinue) {
|
|
$code .= $this->getIndent() . 'int _cnt_flag = 0;' . PHP_EOL;
|
|
}
|
|
$code .= $this->genLocalVarDecl($this->context->localVars);
|
|
foreach ($this->context->globalVars as $name => $type) {
|
|
// $GLOBALS is handled via php_globals_array() at each read site
|
|
if ($name === 'GLOBALS') {
|
|
continue;
|
|
}
|
|
$code .= $this->getIndent() . Type::VAR . ' &' . $name . ' = ' . $this->escapeGlobalVar($name) . ';' . PHP_EOL;
|
|
}
|
|
foreach ($this->context->objectProps as $name => $info) {
|
|
if (($info['kind'] ?? 'zval') === 'var') {
|
|
$code .= $this->getIndent() . Type::VAR . ' ' . $name . ' = ' . $info['getter'] . ';' . PHP_EOL;
|
|
} else {
|
|
$zvalMacro = ($info['type'] === Type::FLOAT) ? 'Z_DVAL_P' : 'Z_LVAL_P';
|
|
$code .= $this->getIndent() . $info['type'] . ' &' . $name . ' = ' . $zvalMacro . '(' . $info['getter'] . '.unwrap_ptr());' . PHP_EOL;
|
|
}
|
|
}
|
|
foreach ($this->context->staticPropRefs as $name => $info) {
|
|
$getter = Symbol::getStaticProperty() . '(' . $info['classPtr'] . ', ' . $info['offsetExpr'] . ')';
|
|
if (($info['kind'] ?? 'zval') === 'var') {
|
|
$code .= $this->getIndent() . Type::VAR . ' ' . $name . ' = ' . $getter . ';' . PHP_EOL;
|
|
} else {
|
|
$code .= $this->getIndent() . 'zval *' . $name . ' = ' . $getter . '.unwrap_ptr();' . PHP_EOL;
|
|
}
|
|
}
|
|
return $code;
|
|
}
|
|
|
|
protected function genReturnCode(): string
|
|
{
|
|
if ($this->functionDef->returnsByRef) {
|
|
return $this->getIndent() . 'return ' . Type::REF . '{};';
|
|
}
|
|
if ($this->shouldCheckClosureReturnType()) {
|
|
return $this->genClosureCheckedReturn(self::VALUE_NULL);
|
|
}
|
|
if ($this->functionDef->returnType === Type::VOID) {
|
|
return '';
|
|
}
|
|
if ($this->functionDef->returnTypeCheck && !$this->context->inClosure) {
|
|
return $this->genUnionCheckedReturn(self::VALUE_NULL);
|
|
}
|
|
if ($this->functionDef->returnType === Type::INT
|
|
or $this->functionDef->returnType === Type::FLOAT
|
|
or $this->functionDef->returnType === Type::BOOL) {
|
|
return $this->getIndent() . 'return 0;';
|
|
} else {
|
|
return $this->getIndent() . 'return ' . self::VALUE_NULL . ';';
|
|
}
|
|
}
|
|
|
|
protected function parseFullyQualifiedName(Node\Name\FullyQualified $expr): string
|
|
{
|
|
return $expr->name;
|
|
}
|
|
}
|
|
|