refactor(compiler): migrate symbol storage to repository pattern with diagnostic reporter

- Replace direct array access with SymbolRepository for functions, classes, interfaces
- Introduce DiagnosticReporter interface with CLI and throwing implementations
- Add NativeBuilder for compile/link command management and parallel processing
- Create CommandOptions base class for compile and link options handling
- Implement project YAML loader with PHP version and OS family conditions
- Remove old array properties and update all related method calls
- Update compiler diagnostic methods to use new reporter pattern
- Refactor native compilation to use builder pattern with proper options
- Modify test utilities to work with new symbol repository structure
pull/17/head
韩天峰 2 months ago
parent 6de4dc42bd
commit 9d3ea98199
  1. 6
      phpunit/src/NativePropertyTest.php
  2. 15
      phpunit/src/PreprocessorTest.php
  3. 6
      phpunit/src/SsaAnalysisTest.php
  4. 20
      src/Build/CommandOptions.php
  5. 13
      src/Build/CompileOptions.php
  6. 7
      src/Build/LinkOptions.php
  7. 125
      src/Build/NativeBuilder.php
  8. 32
      src/Build/NativeCommandOptionsTrait.php
  9. 26
      src/CompilerBase.php
  10. 140
      src/Config/ProjectYamlLoader.php
  11. 20
      src/Context/CompilationStateTrait.php
  12. 29
      src/Diagnostics/CliDiagnosticReporter.php
  13. 13
      src/Diagnostics/CompilerDiagnosticTrait.php
  14. 12
      src/Diagnostics/DiagnosticReporter.php
  15. 18
      src/Diagnostics/ThrowingDiagnosticReporter.php
  16. 3
      src/Generator/DefaultArgumentGenerator.php
  17. 2
      src/Optimizer/SsaPropOptimizer.php
  18. 7
      src/Preprocessor.php
  19. 50
      src/Symbol/SymbolRepository.php
  20. 445
      src/Translator.php

@ -38,8 +38,10 @@ class NativePropertyTest extends \BaseTest
}
$code = file_get_contents($outputFile);
$this->assertStringContainsString('php::getStaticProperty(php_get_called_class(this_), "count")', $code);
$this->assertStringContainsString('php::getStaticProperty(php_get_called_class(this_), "count") = php::toInt(value)', $code);
$this->assertStringContainsString('tmp_var_0 = php_get_called_class(this_);', $code);
$this->assertStringContainsString('php::getStaticProperty((tmp_var_0.isObject()', $code);
$this->assertStringContainsString('php::getStaticProperty((tmp_var_0.isObject() ? php::fn::get_class(tmp_var_0)', $code);
$this->assertStringContainsString('= php::toInt(value);', $code);
}
public function testNativeIntPropertyAssignOpUsesNativeReference(): void

@ -56,6 +56,17 @@ class PreprocessorTest extends TestCase
private function setProperty(string $name, mixed $value): void
{
if (in_array($name, ['classes', 'interfaces', 'functions', 'classExtends'], true)) {
$symbols = $this->getProperty('symbols');
$method = match ($name) {
'classes' => 'replaceClasses',
'interfaces' => 'replaceInterfaces',
'functions' => 'replaceFunctions',
'classExtends' => 'replaceParents',
};
$symbols->{$method}($value);
return;
}
$prop = $this->ref->getProperty($name);
$prop->setAccessible(true);
$prop->setValue($this->compiler, $value);
@ -63,6 +74,10 @@ class PreprocessorTest extends TestCase
private function getProperty(string $name): mixed
{
if (in_array($name, ['classes', 'interfaces', 'functions'], true)) {
$symbols = $this->getProperty('symbols');
return $symbols->{$name}();
}
$prop = $this->ref->getProperty($name);
$prop->setAccessible(true);
return $prop->getValue($this->compiler);

@ -69,6 +69,12 @@ class SsaAnalysisTest extends TestCase
private function setProperty(string $name, mixed $value): void
{
if ($name === 'classes') {
$prop = $this->ref->getProperty('symbols');
$prop->setAccessible(true);
$prop->getValue($this->compiler)->replaceClasses($value);
return;
}
$prop = $this->ref->getProperty($name);
$prop->setAccessible(true);
$prop->setValue($this->compiler, $value);

@ -0,0 +1,20 @@
<?php
namespace TypePhp\Build;
use ArrayAccess;
use LogicException;
/** @implements ArrayAccess<string, mixed> */
abstract class CommandOptions implements ArrayAccess
{
final public function __construct(protected readonly array $values)
{
}
public function toArray(): array { return $this->values; }
public function offsetExists(mixed $offset): bool { return isset($this->values[$offset]); }
public function offsetGet(mixed $offset): mixed { return $this->values[$offset] ?? null; }
public function offsetSet(mixed $offset, mixed $value): never { throw new LogicException('Command options are immutable'); }
public function offsetUnset(mixed $offset): never { throw new LogicException('Command options are immutable'); }
}

@ -0,0 +1,13 @@
<?php
namespace TypePhp\Build;
final class CompileOptions extends CommandOptions
{
public function with(string $name, mixed $value): self
{
$values = $this->values;
$values[$name] = $value;
return new self($values);
}
}

@ -0,0 +1,7 @@
<?php
namespace TypePhp\Build;
final class LinkOptions extends CommandOptions
{
}

@ -0,0 +1,125 @@
<?php
namespace TypePhp\Build;
use Closure;
use TypePhp\Backend\CompilerBackend;
final readonly class NativeBuilder
{
public function __construct(private CompilerBackend $backend)
{
}
public function compileCommand(string $source, string $object, CompileOptions $options, ?string $language): string
{
if ($language === null) {
return $this->backend->buildCompileCommand($source, $object, $options->toArray());
}
if ($language === 'c') {
return $this->backend->buildCCompileCommand($source, $object, $options->toArray());
}
return $this->backend->buildNativeCompileCommand($source, $object, $options->toArray(), $language);
}
public function linkCommand(array $objects, string $target, LinkOptions $options): string
{
return $this->backend->buildLinkCommand($objects, $target, $options->toArray());
}
/** @return array{command: string, output: list<string>, status: int} */
public function compile(string $source, string $object, CompileOptions $options, ?string $language, bool $quiet): array
{
$command = $this->compileCommand($source, $object, $options, $language);
$output = [];
if ($quiet) {
exec($command . ' 2>&1', $output, $status);
} else {
passthru($command, $status);
}
return ['command' => $command, 'output' => $output, 'status' => $status];
}
/** @return array{command: string, output: list<string>, status: int, generated: bool} */
public function link(array $objects, string $target, LinkOptions $options): array
{
$command = $this->linkCommand($objects, $target, $options);
try {
exec($command . ' 2>&1', $output, $status);
return [
'command' => $command,
'output' => $output,
'status' => $status,
'generated' => file_exists($target),
];
} finally {
$this->cleanup();
}
}
/**
* @param Closure(string): string $objectFile
* @param Closure(string, string): void $worker
* @param Closure(): int $fork
* @param Closure(): array{int, int} $wait
* @param Closure(int): bool $succeeded
* @return array{objects: list<string>, failures: list<string>}
*/
public function dispatchParallel(
array $sources,
int $jobs,
Closure $objectFile,
Closure $worker,
Closure $fork,
Closure $wait,
Closure $succeeded,
): array {
$queue = array_values($sources);
$running = [];
$objects = [];
$failures = [];
while ($queue !== [] || $running !== []) {
while (count($running) < $jobs && $queue !== []) {
$source = array_shift($queue);
$object = $objectFile($source);
$pid = $fork();
if ($pid === -1) {
$failures[] = $source;
array_push($failures, ...$queue);
$queue = [];
break;
}
if ($pid === 0) {
try {
$worker($source, $object);
exit(is_file($object) ? 0 : 1);
} catch (\Throwable) {
exit(1);
}
}
$running[$pid] = ['source' => $source, 'object' => $object];
}
if ($running === []) {
break;
}
[$pid, $status] = $wait();
$task = $running[$pid] ?? null;
unset($running[$pid]);
if ($task === null) {
continue;
}
if ($succeeded($status)) {
$objects[] = $task['object'];
} else {
$failures[] = $task['source'];
}
}
return ['objects' => $objects, 'failures' => $failures];
}
public function cleanup(): void
{
$this->backend->cleanupResponseFile();
}
}

@ -12,7 +12,7 @@ use TypePhp\Metadata\Constants;
trait NativeCommandOptionsTrait
{
protected function getCommonCompileCommandOptions(): array
protected function getCommonCompileCommandOptions(): CompileOptions
{
$includePaths = $this->getIncludePaths();
if (!empty($this->userIncludePaths)) {
@ -24,7 +24,7 @@ trait NativeCommandOptionsTrait
$userDefines[] = 'TYPEPHP_NO_MAIN=1';
}
return [
return new CompileOptions([
'include_paths' => $includePaths,
'optimize' => $this->optimizeLevel,
'debug' => $this->debug,
@ -37,39 +37,37 @@ trait NativeCommandOptionsTrait
'prof_output' => $this->targetName . '.prof',
'user_defines' => $userDefines,
'lto' => $this->enableLto,
];
]);
}
protected function getCompileCommandOptions(): array
protected function getCompileCommandOptions(): CompileOptions
{
$options = $this->getCommonCompileCommandOptions();
$options['cpp_std'] = $this->cxxStd;
$options['cxxflags'] = $this->cxxFlags;
$options['suppressed_warnings'] = Constants::MSVC_SUPPRESSED_WARNINGS ?? [];
return $options;
return $options
->with('cpp_std', $this->cxxStd)
->with('cxxflags', $this->cxxFlags)
->with('suppressed_warnings', Constants::MSVC_SUPPRESSED_WARNINGS ?? []);
}
protected function getCCompileCommandOptions(): array
protected function getCCompileCommandOptions(): CompileOptions
{
$options = $this->getCommonCompileCommandOptions();
$options['suppressed_warnings'] = ['4244', '4146'];
return $options;
return $options->with('suppressed_warnings', ['4244', '4146']);
}
protected function getNativeCompileCommandOptions(string $language = ''): array
protected function getNativeCompileCommandOptions(string $language = ''): CompileOptions
{
$options = $this->getCommonCompileCommandOptions();
$options['suppressed_warnings'] = Constants::MSVC_SUPPRESSED_WARNINGS ?? [];
$options = $options->with('suppressed_warnings', Constants::MSVC_SUPPRESSED_WARNINGS ?? []);
if ($language === 'objective-c++') {
$options['cpp_std'] = $this->cxxStd;
$options['cxxflags'] = $this->cxxFlags;
$options = $options->with('cpp_std', $this->cxxStd)->with('cxxflags', $this->cxxFlags);
}
return $options;
}
protected function getLinkCommandOptions(): array
protected function getLinkCommandOptions(): LinkOptions
{
$libraryPaths = array_merge($this->getLibraryPaths(), $this->linkPaths);
$libraries = $this->getLibraries();
@ -95,6 +93,6 @@ trait NativeCommandOptionsTrait
$options['rpath'] = $rpaths;
}
return $options;
return new LinkOptions($options);
}
}

@ -16,6 +16,9 @@ use TypePhp\Entity\ArgInfo;
use TypePhp\Context\FunctionContext;
use TypePhp\Context\CompilationStateTrait;
use TypePhp\Diagnostics\CompilerDiagnosticTrait;
use TypePhp\Diagnostics\CliDiagnosticReporter;
use TypePhp\Diagnostics\DiagnosticReporter;
use TypePhp\Diagnostics\ThrowingDiagnosticReporter;
use TypePhp\Entity\ClassDef;
use TypePhp\Entity\ConstantDef;
use TypePhp\Entity\FunctionDef;
@ -77,6 +80,7 @@ use TypePhp\Resolver\PropertyWriteTarget;
use TypePhp\Resolver\Reflection;
use TypePhp\Resolver\StaticPropertyFetchResolution;
use TypePhp\Resolver\StaticPropertyFetchTarget;
use TypePhp\Symbol\SymbolRepository;
use TypePhp\TypeSystem\CompositeTypeCheckerTrait;
use TypePhp\TypeSystem\NativeTypeCompatibilityTrait;
use PhpParser\Modifiers;
@ -360,19 +364,16 @@ class CompilerBase implements PropertyAccessContext
/**
* @var array<string, InterfaceDef>
*/
protected array $interfaces = [];
/**
* 存储所有函数、类方法的定义,key 是 native name,命名空间需要转为 `_`,并且必须为小写
* @var array<string, FunctionDef>
*/
protected array $functions = [];
/**
* key 类名,包含命名空间
* @var array<string, ClassDef>
*/
protected array $classes = [];
/**
* @var array<string, ConstantDef>
@ -404,6 +405,7 @@ class CompilerBase implements PropertyAccessContext
protected ?MethodDef $methodDef = null;
protected ?InterfaceDef $interfaceDef = null;
protected bool $inGeneratorBody = false;
private ?DiagnosticReporter $diagnosticReporter = null;
protected FunctionContext $context;
protected array $superGlobalVars = [
'_GET' => self::TYPE_ARRAY,
@ -454,7 +456,7 @@ class CompilerBase implements PropertyAccessContext
* 存储所有类继承关系,类名必须全部为小写
* @var array<string, string>
*/
protected array $classExtends = [];
protected SymbolRepository $symbols;
/**
* Reverse class hierarchy: parent class (lowercase) => list of child classes (lowercase)
@ -472,6 +474,7 @@ class CompilerBase implements PropertyAccessContext
$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');
@ -479,6 +482,21 @@ class CompilerBase implements PropertyAccessContext
$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;

@ -0,0 +1,140 @@
<?php
namespace TypePhp\Config;
use Closure;
use Symfony\Component\Yaml\Yaml;
final class ProjectYamlLoader
{
/** @param Closure(string): never $error */
public function __construct(
private string $phpVersion,
private readonly Closure $error,
private readonly string $osFamily = PHP_OS_FAMILY,
) {
}
public function setPhpVersion(string $phpVersion): void
{
$this->phpVersion = $phpVersion;
}
public function load(string $path): array
{
$config = Yaml::parseFile($path);
if (!is_array($config)) {
($this->error)('Project YAML root must be a map');
}
return $config;
}
/** @return array{0: string, 1: string|null} */
public function parseSourceEntry(mixed $entry): array
{
if (is_string($entry)) {
return [$entry, null];
}
if (!is_array($entry)) {
($this->error)('Each `sources` entry must be a string or map');
}
$path = $entry['path'] ?? $entry['source'] ?? $entry['file'] ?? null;
if (!is_string($path) || trim($path) === '') {
($this->error)('Conditional `sources` entries must include a non-empty `path`');
}
$condition = $entry['if'] ?? $entry['when'] ?? null;
if ($condition !== null && !is_string($condition)) {
($this->error)('Source condition must be a string');
}
return [$path, $condition];
}
public function evaluateCondition(string $condition): bool
{
$condition = trim($condition);
if ($condition === '') {
($this->error)('Source condition must not be empty');
}
$expr = $this->replacePhpVersionComparisons($condition);
$expr = $this->replaceOsFamilyComparisons($expr, $condition);
if (preg_match('/[A-Za-z_]/', $expr)
|| !preg_match('/^[0-9\s<>=!&|().+-]+$/', $expr)
|| preg_match('/(?<![&])&(?!&)|(?<![|])\|(?!\|)/', $expr)) {
($this->error)('Unsupported source condition: `' . $condition . '`');
}
try {
return (bool) eval('return (' . $expr . ');');
} catch (\Throwable) {
($this->error)('Invalid source condition: `' . $condition . '`');
}
}
private function replacePhpVersionComparisons(string $condition): string
{
$literal = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'';
$operator = '(>=|<=|==|!=|<>|=|>|<|lt|le|gt|ge|eq|ne)';
$patterns = [
'/\bPHP_VERSION_ID\b\s*' . $operator . '\s*([0-9]+)/i' => fn(array $m): bool => version_compare($this->phpVersion, $this->versionIdToString((int) $m[2]), strtolower($m[1])),
'/([0-9]+)\s*' . $operator . '\s*\bPHP_VERSION_ID\b/i' => fn(array $m): bool => version_compare($this->versionIdToString((int) $m[1]), $this->phpVersion, strtolower($m[2])),
'/\bPHP_VERSION\b\s*' . $operator . '\s*(' . $literal . ')/i' => function (array $m): bool {
$version = stripcslashes(($m[3] ?? '') !== '' ? $m[3] : $m[4]);
$this->assertVersion($version);
return version_compare($this->phpVersion, $version, strtolower($m[1]));
},
'/(' . $literal . ')\s*' . $operator . '\s*\bPHP_VERSION\b/i' => function (array $m): bool {
$version = stripcslashes($m[2] !== '' ? $m[2] : $m[3]);
$this->assertVersion($version);
return version_compare($version, $this->phpVersion, strtolower($m[4]));
},
];
$expr = $condition;
foreach ($patterns as $pattern => $compare) {
$expr = preg_replace_callback($pattern, fn(array $m): string => $compare($m) ? '1' : '0', $expr);
if ($expr === null) {
($this->error)('Invalid source condition: `' . $condition . '`');
}
}
if (preg_match('/\bPHP_VERSION(?:_ID)?\b/', $expr)) {
($this->error)('Unsupported source condition: `' . $condition . '`');
}
return $expr;
}
private function replaceOsFamilyComparisons(string $expr, string $condition): string
{
$literal = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'';
$expr = preg_replace_callback('/\bPHP_OS_FAMILY\b\s*(==|!=)\s*(' . $literal . ')/i', function (array $m): string {
$expected = stripcslashes(($m[3] ?? '') !== '' ? $m[3] : $m[4]);
$this->assertOsFamily($expected);
return (($this->osFamily === $expected) xor ($m[1] === '!=')) ? '1' : '0';
}, $expr);
$expr = preg_replace_callback('/(' . $literal . ')\s*(==|!=)\s*\bPHP_OS_FAMILY\b/i', function (array $m): string {
$expected = stripcslashes($m[2] !== '' ? $m[2] : $m[3]);
$this->assertOsFamily($expected);
return (($expected === $this->osFamily) xor ($m[4] === '!=')) ? '1' : '0';
}, $expr ?? '');
if ($expr === null || preg_match('/\bPHP_OS_FAMILY\b/', $expr)) {
($this->error)('Unsupported source condition: `' . $condition . '`');
}
return $expr;
}
private function assertVersion(string $version): void
{
if ($version === '' || !preg_match('/^[0-9A-Za-z_.+\-]+$/', $version)) {
($this->error)('Invalid PHP_VERSION literal: `' . $version . '`');
}
}
private function assertOsFamily(string $osFamily): void
{
if (!in_array($osFamily, ['Windows', 'BSD', 'Darwin', 'Solaris', 'Linux', 'Unknown'], true)) {
($this->error)('Invalid PHP_OS_FAMILY literal: `' . $osFamily . '`');
}
}
private function versionIdToString(int $versionId): string
{
return intdiv($versionId, 10000) . '.' . intdiv($versionId % 10000, 100) . '.' . ($versionId % 100);
}
}

@ -106,7 +106,7 @@ trait CompilationStateTrait
protected function addFunction(string $name, FunctionDef $functionDef): void
{
$this->functions[$this->escapeFunction($name)] = $functionDef;
$this->symbols->putFunction($this->escapeFunction($name), $functionDef);
}
/**
@ -114,47 +114,47 @@ trait CompilationStateTrait
*/
protected function hasFunction(string $name): bool
{
return array_key_exists($this->escapeFunction($name), $this->functions);
return $this->symbols->hasFunction($this->escapeFunction($name));
}
protected function getFunction(string $name): FunctionDef
{
return $this->functions[$this->escapeFunction($name)];
return $this->symbols->function($this->escapeFunction($name));
}
protected function addClass(string $name, ClassDef $classDef): void
{
$this->classes[$this->escapeClass($name)] = $classDef;
$this->symbols->putClass($this->escapeClass($name), $classDef);
}
protected function getClass(string $name): ClassDef
{
return $this->classes[$this->escapeClass($name)];
return $this->symbols->class($this->escapeClass($name));
}
public function getClassDef(string $name): ?ClassDef
{
return $this->classes[$this->escapeClass($name)] ?? null;
return $this->symbols->findClass($this->escapeClass($name));
}
public function getParentClass(string $class): string
{
return $this->classExtends[strtolower(ltrim($class, '\\'))] ?? '';
return $this->symbols->parent(strtolower(ltrim($class, '\\')));
}
protected function hasClass(string $name): bool
{
return array_key_exists($this->escapeClass($name), $this->classes);
return $this->symbols->hasClass($this->escapeClass($name));
}
protected function hasInterface(string $name): bool
{
return array_key_exists($this->escapeClass($name), $this->interfaces);
return $this->symbols->hasInterface($this->escapeClass($name));
}
protected function getInterface(string $name): InterfaceDef
{
return $this->interfaces[$this->escapeClass($name)];
return $this->symbols->interface($this->escapeClass($name));
}
protected function checkFunction(string $name): void

@ -0,0 +1,29 @@
<?php
namespace TypePhp\Diagnostics;
use League\CLImate\CLImate;
use PhpParser\Node;
final readonly class CliDiagnosticReporter implements DiagnosticReporter
{
public function __construct(
private CLImate $climate,
private bool $printBacktrace = false,
) {
}
public function fatal(string $message): never
{
$this->climate->red("Fatal error: {$message}");
if ($this->printBacktrace) {
debug_print_backtrace();
}
exit(255);
}
public function warning(Node $node, string $file, string $message): void
{
$this->climate->magenta("{$message} in {$file}:{$node->getStartLine()}");
}
}

@ -11,7 +11,6 @@ namespace TypePhp\Diagnostics;
use PhpParser\Node;
use PhpParser\Node\Expr\Variable;
use PhpParser\NodeAbstract;
use TypePhp\Exception\TestError;
trait CompilerDiagnosticTrait
{
@ -20,15 +19,7 @@ trait CompilerDiagnosticTrait
*/
public function error(string $msg): never
{
if ($this->forTest) {
throw new TestError($msg);
}
$this->climate->red("Fatal error: {$msg}");
if ($this->printBacktraceOnError) {
debug_print_backtrace();
}
exit(255);
$this->getDiagnosticReporter()->fatal($msg);
}
public function fatalError(NodeAbstract $node, string $msg): never
@ -38,7 +29,7 @@ trait CompilerDiagnosticTrait
protected function warning(Node $node, string $msg): void
{
$this->climate->magenta("{$msg} in {$this->file}:{$node->getStartLine()}");
$this->getDiagnosticReporter()->warning($node, $this->file, $msg);
}
protected function errorUndefinedVariable(Variable $node): never

@ -0,0 +1,12 @@
<?php
namespace TypePhp\Diagnostics;
use PhpParser\Node;
interface DiagnosticReporter
{
public function fatal(string $message): never;
public function warning(Node $node, string $file, string $message): void;
}

@ -0,0 +1,18 @@
<?php
namespace TypePhp\Diagnostics;
use PhpParser\Node;
use TypePhp\Exception\TestError;
final class ThrowingDiagnosticReporter implements DiagnosticReporter
{
public function fatal(string $message): never
{
throw new TestError($message);
}
public function warning(Node $node, string $file, string $message): void
{
}
}

@ -44,7 +44,7 @@ trait DefaultArgumentGenerator
protected function genDefaultArgumentHelpers(): string
{
$code = '';
foreach ($this->functions as $func) {
foreach ($this->symbols->functions() as $func) {
foreach ($func->argInfoList as $argInfo) {
$plan = $argInfo->arrayInitPlan;
if (!$plan || !$plan->requiresRuntimeInit()) {
@ -69,4 +69,3 @@ trait DefaultArgumentGenerator
return $code ? $code . PHP_EOL : '';
}
}

@ -336,7 +336,7 @@ trait SsaPropOptimizer
*/
protected function isClassSafeForPropHoisting(string $className): bool
{
$classDef = $this->classes[$this->escapeClass($className)] ?? null;
$classDef = $this->symbols->findClass($this->escapeClass($className));
if (!$classDef) {
return false;
}

@ -532,7 +532,7 @@ class Preprocessor extends CompilerBase
if ($parentClassLower === $fullClassNameLower) {
$this->fatalError($class, "Class {$fullClassName} cannot extend itself");
}
$this->classExtends[$fullClassNameLower] = $parentClassLower;
$this->symbols->setParent($fullClassNameLower, $parentClassLower);
$this->classSubClasses[$parentClassLower][] = $fullClassNameLower;
if (!$this->isInternalClass($parentClassLower)) {
$this->symbolCallInFile[$this->file][] = $parentClassLower;
@ -792,8 +792,7 @@ class Preprocessor extends CompilerBase
$this->classMethodOverride[$fullMethodNameLower] = $isOverridden;
// 查找父类是否有同名方法,递归向上标记父类方法已被覆盖
while (isset($this->classExtends[$fullClassNameLower])) {
$parentClass = $this->classExtends[$fullClassNameLower];
while (($parentClass = $this->symbols->parent($fullClassNameLower)) !== '') {
$parentMethodLower = strtolower($parentClass . '::' . $this->method);
if (isset($this->classMethodOverride[$parentMethodLower])) {
$this->classMethodOverride[$parentMethodLower] = true;
@ -855,7 +854,7 @@ class Preprocessor extends CompilerBase
}
$this->symbolDeclInFile[$interfaceNameLower] = $this->file;
$this->interfaces[$this->escapeClass($interfaceName)] = $this->interfaceDef;
$this->symbols->putInterface($this->escapeClass($interfaceName), $this->interfaceDef);
$this->interfacesDefineInFile[$interfaceName] = $this->interfaceDef;
foreach ($v->stmts as $stmt) {

@ -0,0 +1,50 @@
<?php
namespace TypePhp\Symbol;
use TypePhp\Entity\ClassDef;
use TypePhp\Entity\FunctionDef;
use TypePhp\Entity\InterfaceDef;
final class SymbolRepository
{
/** @var array<string, FunctionDef> */
private array $functions = [];
/** @var array<string, ClassDef> */
private array $classes = [];
/** @var array<string, InterfaceDef> */
private array $interfaces = [];
/** @var array<string, string> */
private array $parents = [];
public function putFunction(string $key, FunctionDef $definition): void { $this->functions[$key] = $definition; }
public function hasFunction(string $key): bool { return array_key_exists($key, $this->functions); }
public function function(string $key): FunctionDef { return $this->functions[$key]; }
/** @return array<string, FunctionDef> */
public function functions(): array { return $this->functions; }
public function putClass(string $key, ClassDef $definition): void { $this->classes[$key] = $definition; }
public function hasClass(string $key): bool { return array_key_exists($key, $this->classes); }
public function class(string $key): ClassDef { return $this->classes[$key]; }
public function findClass(string $key): ?ClassDef { return $this->classes[$key] ?? null; }
/** @return array<string, ClassDef> */
public function classes(): array { return $this->classes; }
public function putInterface(string $key, InterfaceDef $definition): void { $this->interfaces[$key] = $definition; }
public function hasInterface(string $key): bool { return array_key_exists($key, $this->interfaces); }
public function interface(string $key): InterfaceDef { return $this->interfaces[$key]; }
/** @return array<string, InterfaceDef> */
public function interfaces(): array { return $this->interfaces; }
public function setParent(string $class, string $parent): void { $this->parents[$class] = $parent; }
public function parent(string $class): string { return $this->parents[$class] ?? ''; }
/** @param array<string, FunctionDef> $functions */
public function replaceFunctions(array $functions): void { $this->functions = $functions; }
/** @param array<string, ClassDef> $classes */
public function replaceClasses(array $classes): void { $this->classes = $classes; }
/** @param array<string, InterfaceDef> $interfaces */
public function replaceInterfaces(array $interfaces): void { $this->interfaces = $interfaces; }
/** @param array<string, string> $parents */
public function replaceParents(array $parents): void { $this->parents = $parents; }
}

@ -13,7 +13,9 @@ use TypePhp\Analysis\SsaBuilder;
use TypePhp\Backend\CompilerFactory;
use TypePhp\Build\FileScanner;
use TypePhp\Build\NativeCommandOptionsTrait;
use TypePhp\Build\NativeBuilder;
use TypePhp\Build\SourcePipelineTrait;
use TypePhp\Config\ProjectYamlLoader;
use TypePhp\Build\ResourceCompilationTrait;
use TypePhp\Entity\ArgInfo;
use TypePhp\Entity\ClassDef;
@ -41,9 +43,6 @@ use PhpParser\Node\ArrayItem;
use PhpParser\Node\Stmt\Foreach_;
use PhpParser\NodeAbstract;
use PhpParser\NodeTraverser;
use Ajaxray\AnsiKit\AnsiTerminal;
use Ajaxray\AnsiKit\Components\Progressbar;
use Symfony\Component\Yaml\Yaml;
class Translator extends Preprocessor
{
@ -59,6 +58,8 @@ class Translator extends Preprocessor
protected bool $hasExplicitOutput = false;
protected ?string $explicitOutputExtension = null;
protected array $sourceDirs = [];
private ?ProjectYamlLoader $projectYamlLoader = null;
private ?NativeBuilder $nativeBuilder = null;
protected bool $verbose = false;
protected array $phpSrcFiles = [];
protected array $ignorePaths = [];
@ -801,7 +802,7 @@ CODE;
$code .= $this->getIndent() . "PHP_FE(cli_get_process_title, arginfo_cli_get_process_title)\n";
}
foreach ($this->functions as $functionDef) {
foreach ($this->symbols->functions() as $functionDef) {
if ($this->isBuildModeExt() and $functionDef->name === self::ENTRY_FUNCTION) {
continue;
}
@ -852,7 +853,7 @@ CODE;
}
$code .= '// static property ' . PHP_EOL;
foreach ($this->classes as $classDef) {
foreach ($this->symbols->classes() as $classDef) {
foreach ($classDef->properties as $property) {
if (!$property->isStatic() || !$property->arrayInitPlan || !$property->default) {
continue;
@ -900,7 +901,7 @@ CODE;
}
// Clean up inherited array constants from child classes
foreach ($this->classes as $className => $classDef) {
foreach ($this->symbols->classes() as $className => $classDef) {
$ownConstNames = [];
foreach ($classDef->constants as $constant) {
if ($constant->type === self::TYPE_ARRAY) {
@ -909,8 +910,8 @@ CODE;
}
$parentName = $this->escapeClass($classDef->extends);
while ($parentName && isset($this->classes[$parentName])) {
$parentDef = $this->classes[$parentName];
while ($parentName && $this->symbols->hasClass($parentName)) {
$parentDef = $this->symbols->class($parentName);
foreach ($parentDef->constants as $constant) {
if ($constant->type === self::TYPE_ARRAY && !isset($ownConstNames[$constant->name])) {
$ownConstNames[$constant->name] = true;
@ -955,7 +956,7 @@ CODE;
$code .= 'php_app_init();' . PHP_EOL;
if ($this->isBuildModeBin()) {
$entryFunction = $this->functions[self::ENTRY_FUNCTION];
$entryFunction = $this->symbols->function(self::ENTRY_FUNCTION);
$entryFile = $entryFunction->sourceFile;
$entryFileArg = $this->genCharPtr($entryFile, true);
$entryPrefix = str_repeat("\n", max(0, $entryFunction->startLine - 1));
@ -1155,19 +1156,19 @@ CODE;
$this->invalidateMiscObjectCache($objectFile);
}
$cmd = $this->buildCompileFileCommand($cppFile, $objectFile);
$language = $this->getLanguageFromExtension($cppFile);
$options = match ($language) {
null => $this->getCompileCommandOptions(),
'c' => $this->getCCompileCommandOptions(),
default => $this->getNativeCompileCommandOptions($language),
};
$result = $this->getNativeBuilder()->compile($cppFile, $objectFile, $options, $language, $parallel);
if (!$parallel) {
$this->climate->comment($cmd);
}
// 在并行模式下,抑制 passthru 的输出
if ($parallel) {
exec($cmd . ' 2>&1', $output, $ret);
} else {
passthru($cmd, $ret);
$this->climate->comment($result['command']);
}
if ($ret !== 0) {
if ($parallel && !empty($output)) {
foreach ($output as $line) {
if ($result['status'] !== 0) {
if ($parallel && !empty($result['output'])) {
foreach ($result['output'] as $line) {
$this->climate->red($line);
}
}
@ -1182,27 +1183,12 @@ CODE;
protected function buildCompileFileCommand(string $sourceFile, string $objectFile): string
{
$language = $this->getLanguageFromExtension($sourceFile);
if ($language === null) {
return $this->getCompilerBackend()->buildCompileCommand(
$sourceFile,
$objectFile,
$this->getCompileCommandOptions()
);
}
if ($language === 'c') {
return $this->getCompilerBackend()->buildCCompileCommand(
$sourceFile,
$objectFile,
$this->getCCompileCommandOptions()
);
}
return $this->getCompilerBackend()->buildNativeCompileCommand(
$sourceFile,
$objectFile,
$this->getNativeCompileCommandOptions($language),
$language
);
$options = match ($language) {
null => $this->getCompileCommandOptions(),
'c' => $this->getCCompileCommandOptions(),
default => $this->getNativeCompileCommandOptions($language),
};
return $this->getNativeBuilder()->compileCommand($sourceFile, $objectFile, $options, $language);
}
public function compile(array $sourceFiles): array
@ -1316,137 +1302,32 @@ CODE;
return pcntl_wifexited($status) && pcntl_wexitstatus($status) === 0;
}
protected function getCompileChildFailureReason(int $status): string
{
if (pcntl_wifsignaled($status)) {
return 'terminated by signal ' . pcntl_wtermsig($status);
}
if (pcntl_wifexited($status)) {
return 'exited with status ' . pcntl_wexitstatus($status);
}
return 'terminated abnormally';
}
protected function compileWithPcntl(array $sourceFiles, int $job): array
{
// 检查 pcntl 扩展是否可用
if (!function_exists('pcntl_fork')) {
$this->climate->warning('pcntl extension not available, using sequential compilation');
return $this->compileSourceFile($sourceFiles);
}
$objectFiles = [];
$totalFiles = count($sourceFiles);
$runningProcesses = 0;
$processPipes = [];
$fileQueue = $sourceFiles;
$compiledCount = 0;
$failedFiles = [];
$this->climate->lightBlue("Starting parallel compilation with {$job} jobs for {$totalFiles} files");
$result = $this->getNativeBuilder()->dispatchParallel(
$sourceFiles,
$job,
fn(string $source): string => $this->getObjectFile($source),
function (string $source, string $object): void {
$this->compileFile($source, $object, true);
},
fn(): int => $this->pcntlFork(),
fn(): array => $this->waitForCompileChild(),
fn(int $status): bool => $this->compileChildSucceeded($status),
);
if (!$this->noProgress) {
$progress = new Progressbar();
$progress->barStyle([AnsiTerminal::FG_GREEN])
->percentageStyle([AnsiTerminal::TEXT_BOLD])
->labelStyle([AnsiTerminal::FG_CYAN]);
$progress->renderInPlace(0, $totalFiles, 'Compiling');
}
while ($compiledCount < $totalFiles) {
// 启动新进程,直到达到最大并发数
while ($runningProcesses < $job && !empty($fileQueue)) {
$cppFile = array_shift($fileQueue);
$objectFile = $this->getObjectFile($cppFile);
$pid = $this->pcntlFork();
if ($pid == -1) {
$failedFiles[] = $cppFile;
foreach ($fileQueue as $queuedFile) {
$failedFiles[] = $queuedFile;
}
$compiledCount += count($fileQueue) + 1;
$fileQueue = [];
$this->climate->red('Failed to fork compiler process; no additional files will be scheduled');
break;
}
if ($pid === 0) {
// 子进程:执行编译
try {
$this->compileFile($cppFile, $objectFile, true);
if (!is_file($objectFile)) {
exit(1);
}
exit(0);
} catch (\Throwable $e) {
// 在子进程中不抛出异常,直接退出
exit(1);
}
} else {
// 父进程:记录子进程
$processPipes[$pid] = ['file' => $cppFile, 'object' => $objectFile];
$runningProcesses++;
}
}
// 等待任意一个子进程完成
if ($runningProcesses > 0) {
[$pid, $status] = $this->waitForCompileChild();
$processInfo = $processPipes[$pid] ?? null;
unset($processPipes[$pid]);
$runningProcesses--;
if (!$this->compileChildSucceeded($status)) {
$failedFile = $processInfo['file'] ?? 'unknown';
$failedFiles[] = $failedFile;
echo PHP_EOL;
$reason = $this->getCompileChildFailureReason($status);
$this->climate->red("Compilation failed: {$failedFile} ({$reason})");
echo PHP_EOL;
} elseif ($processInfo) {
$objectFiles[] = $processInfo['object'];
}
$compiledCount++;
if ($this->noProgress) {
$percent = intval($compiledCount / $totalFiles * 100);
$file = $processInfo['file'] ?? 'unknown';
$fileShorted = $this->removeCommonPrefix($this->buildDir, $file);
$this->climate->white("[{$compiledCount}/{$totalFiles}] {$percent}% {$fileShorted}");
} else {
$progress->renderInPlace($compiledCount, $totalFiles, 'Compiling');
}
}
}
// 确保所有子进程都已结束
while ($runningProcesses > 0) {
[$pid, $status] = $this->waitForCompileChild();
$processInfo = $processPipes[$pid] ?? null;
unset($processPipes[$pid]);
$runningProcesses--;
$compiledCount++;
if (!$this->compileChildSucceeded($status)) {
$failedFile = $processInfo['file'] ?? 'unknown';
$failedFiles[] = $failedFile;
$reason = $this->getCompileChildFailureReason($status);
$this->climate->red("Compilation failed: {$failedFile} ({$reason})");
} elseif ($processInfo) {
$objectFiles[] = $processInfo['object'];
}
if ($this->noProgress && $processInfo) {
$percent = intval($compiledCount / $totalFiles * 100);
$this->climate->darkGray("[{$compiledCount}/{$totalFiles}] {$percent}% {$processInfo['file']}");
}
}
echo PHP_EOL;
if (!empty($failedFiles)) {
throw new \Exception('Compilation failed for: ' . implode(', ', $failedFiles));
if ($result['failures'] !== []) {
throw new \Exception('Compilation failed for: ' . implode(', ', $result['failures']));
}
$this->climate->green("Successfully compiled {$totalFiles} files");
return $objectFiles;
return $result['objects'];
}
public function output(string $message, string $style = 'out'): void
@ -1456,11 +1337,7 @@ CODE;
protected function buildLinkCommand(array $objectFiles, string $targetFile): string
{
return $this->getCompilerBackend()->buildLinkCommand(
$objectFiles,
$targetFile,
$this->getLinkCommandOptions()
);
return $this->getNativeBuilder()->linkCommand($objectFiles, $targetFile, $this->getLinkCommandOptions());
}
public function build(array $objectFiles): string
@ -1475,29 +1352,16 @@ CODE;
}
}
$backend = $this->getCompilerBackend();
$buildError = null;
try {
$linkCmd = $this->buildLinkCommand($objectFiles, $targetFile);
$this->climate->comment($linkCmd);
// 执行链接并捕获输出
exec($linkCmd . ' 2>&1', $output, $ret);
// 显示输出(如果有)
if (!empty($output)) {
foreach ($output as $line) {
$this->climate->out($line);
}
}
if ($ret !== 0) {
$buildError = 'link failed: ' . $targetFile;
} elseif (!file_exists($targetFile)) {
$buildError = 'target file not generated: ' . $targetFile;
}
} finally {
$backend->cleanupResponseFile();
$result = $this->getNativeBuilder()->link($objectFiles, $targetFile, $this->getLinkCommandOptions());
$this->climate->comment($result['command']);
foreach ($result['output'] as $line) {
$this->climate->out($line);
}
if ($result['status'] !== 0) {
$buildError = 'link failed: ' . $targetFile;
} elseif (!$result['generated']) {
$buildError = 'target file not generated: ' . $targetFile;
}
if ($buildError !== null) {
@ -1509,6 +1373,11 @@ CODE;
return $targetFile;
}
protected function getNativeBuilder(): NativeBuilder
{
return $this->nativeBuilder ??= new NativeBuilder($this->getCompilerBackend());
}
public function isRunRequested(): bool
{
return $this->climate->arguments->defined('run');
@ -1562,7 +1431,7 @@ CODE;
}
$code .= $this->genDefaultArgumentHelpers();
foreach ($this->functions as $name => $func) {
foreach ($this->symbols->functions() as $name => $func) {
$code .= 'extern ' . ($func->returnsByRef ? self::TYPE_REF : $func->returnType) . ' ' . self::PREFIX . $name . '(';
$list = [];
if ($func->method) {
@ -1715,7 +1584,7 @@ CODE;
*/
private function getClassLikesWithConstants(): array
{
return array_merge($this->classes, $this->interfaces);
return array_merge($this->symbols->classes(), $this->symbols->interfaces());
}
protected function getFilesFromDir(string $path): array
@ -1744,7 +1613,7 @@ CODE;
}
// Propagate array constants to child classes that don't override them
foreach ($this->classes as $className => $classDef) {
foreach ($this->symbols->classes() as $className => $classDef) {
$ownConstNames = [];
foreach ($classDef->constants as $constant) {
if ($constant->type === self::TYPE_ARRAY) {
@ -1753,8 +1622,8 @@ CODE;
}
$parentName = $this->escapeClass($classDef->extends);
while ($parentName && isset($this->classes[$parentName])) {
$parentDef = $this->classes[$parentName];
while ($parentName && $this->symbols->hasClass($parentName)) {
$parentDef = $this->symbols->class($parentName);
foreach ($parentDef->constants as $constant) {
if ($constant->type === self::TYPE_ARRAY && !isset($ownConstNames[$constant->name])) {
$ownConstNames[$constant->name] = true;
@ -1819,7 +1688,7 @@ CODE;
protected function parseProjectYaml(string $path): array
{
$cfg = Yaml::parseFile($path);
$cfg = $this->getProjectYamlLoader()->load($path);
$projectDir = dirname($path);
if (array_key_exists('php-version', $cfg) && !$this->climate->arguments->defined('php-version')) {
@ -2067,187 +1936,19 @@ CODE;
*/
protected function parseProjectYamlSourceEntry(mixed $entry): array
{
if (is_string($entry)) {
return [$entry, null];
}
if (!is_array($entry)) {
$this->error('Each `sources` entry must be a string or map');
}
$path = $entry['path'] ?? $entry['source'] ?? $entry['file'] ?? null;
if (!is_string($path) || trim($path) === '') {
$this->error('Conditional `sources` entries must include a non-empty `path`');
}
$condition = $entry['if'] ?? $entry['when'] ?? null;
if ($condition !== null && !is_string($condition)) {
$this->error('Source condition must be a string');
}
return [$path, $condition];
return $this->getProjectYamlLoader()->parseSourceEntry($entry);
}
protected function evaluateProjectYamlCondition(string $condition): bool
{
$condition = trim($condition);
if ($condition === '') {
$this->error('Source condition must not be empty');
}
$expr = $this->replaceProjectYamlPhpVersionComparisons($condition);
$expr = $this->replaceProjectYamlPhpOsFamilyComparisons($expr, $condition);
if (preg_match('/[A-Za-z_]/', $expr)) {
$this->error('Unsupported source condition: `' . $condition . '`');
}
if (!preg_match('/^[0-9\s<>=!&|().+-]+$/', $expr)) {
$this->error('Unsupported source condition: `' . $condition . '`');
}
if (preg_match('/(?<![&])&(?!&)|(?<![|])\|(?!\|)/', $expr)) {
$this->error('Unsupported source condition: `' . $condition . '`');
}
try {
/** @phpstan-ignore-next-line */
return (bool) eval('return (' . $expr . ');');
} catch (\ParseError|\Throwable) {
$this->error('Invalid source condition: `' . $condition . '`');
}
}
protected function replaceProjectYamlPhpVersionComparisons(string $condition): string
{
$versionLiteral = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'';
$operator = '(>=|<=|==|!=|<>|=|>|<|lt|le|gt|ge|eq|ne)';
$expr = preg_replace_callback(
'/\bPHP_VERSION_ID\b\s*' . $operator . '\s*([0-9]+)/i',
function (array $matches): string {
return version_compare($this->phpVersion, $this->phpVersionIdToString((int) $matches[2]), $this->normalizeProjectYamlVersionOperator($matches[1])) ? '1' : '0';
},
$condition
);
if ($expr === null) {
$this->error('Invalid source condition: `' . $condition . '`');
}
$expr = preg_replace_callback(
'/([0-9]+)\s*' . $operator . '\s*\bPHP_VERSION_ID\b/i',
function (array $matches): string {
return version_compare($this->phpVersionIdToString((int) $matches[1]), $this->phpVersion, $this->normalizeProjectYamlVersionOperator($matches[2])) ? '1' : '0';
},
$expr
);
if ($expr === null) {
$this->error('Invalid source condition: `' . $condition . '`');
}
$expr = preg_replace_callback(
'/\bPHP_VERSION\b\s*' . $operator . '\s*(' . $versionLiteral . ')/i',
function (array $matches): string {
$version = stripcslashes(($matches[3] ?? '') !== '' ? $matches[3] : $matches[4]);
$this->assertProjectYamlVersionLiteral($version);
return version_compare($this->phpVersion, $version, $this->normalizeProjectYamlVersionOperator($matches[1])) ? '1' : '0';
},
$expr
);
if ($expr === null) {
$this->error('Invalid source condition: `' . $condition . '`');
}
$expr = preg_replace_callback(
'/(' . $versionLiteral . ')\s*' . $operator . '\s*\bPHP_VERSION\b/i',
function (array $matches): string {
$version = stripcslashes($matches[2] !== '' ? $matches[2] : $matches[3]);
$this->assertProjectYamlVersionLiteral($version);
return version_compare($version, $this->phpVersion, $this->normalizeProjectYamlVersionOperator($matches[4])) ? '1' : '0';
},
$expr
);
if ($expr === null) {
$this->error('Invalid source condition: `' . $condition . '`');
}
if (preg_match('/\bPHP_VERSION(?:_ID)?\b/', $expr)) {
$this->error('Unsupported source condition: `' . $condition . '`');
}
return $expr;
}
protected function replaceProjectYamlPhpOsFamilyComparisons(string $expr, string $condition): string
{
$stringLiteral = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'';
$operator = '(==|!=)';
$expr = preg_replace_callback(
'/\bPHP_OS_FAMILY\b\s*' . $operator . '\s*(' . $stringLiteral . ')/i',
function (array $matches): string {
$expected = stripcslashes(($matches[3] ?? '') !== '' ? $matches[3] : $matches[4]);
$this->assertProjectYamlOsFamilyLiteral($expected);
$result = PHP_OS_FAMILY === $expected;
if ($matches[1] === '!=') {
$result = !$result;
}
return $result ? '1' : '0';
},
$expr
);
if ($expr === null) {
$this->error('Invalid source condition: `' . $condition . '`');
}
$expr = preg_replace_callback(
'/(' . $stringLiteral . ')\s*' . $operator . '\s*\bPHP_OS_FAMILY\b/i',
function (array $matches): string {
$expected = stripcslashes(($matches[2] ?? '') !== '' ? $matches[2] : $matches[3]);
$this->assertProjectYamlOsFamilyLiteral($expected);
$result = $expected === PHP_OS_FAMILY;
if ($matches[4] === '!=') {
$result = !$result;
}
return $result ? '1' : '0';
},
$expr
);
if ($expr === null) {
$this->error('Invalid source condition: `' . $condition . '`');
}
if (preg_match('/\bPHP_OS_FAMILY\b/', $expr)) {
$this->error('Unsupported source condition: `' . $condition . '`');
}
return $expr;
}
protected function normalizeProjectYamlVersionOperator(string $operator): string
{
return strtolower($operator);
return $this->getProjectYamlLoader()->evaluateCondition($condition);
}
protected function assertProjectYamlVersionLiteral(string $version): void
protected function getProjectYamlLoader(): ProjectYamlLoader
{
if ($version === '' || !preg_match('/^[0-9A-Za-z_.+\-]+$/', $version)) {
$this->error('Invalid PHP_VERSION literal: `' . $version . '`');
}
}
protected function assertProjectYamlOsFamilyLiteral(string $osFamily): void
{
if (!in_array($osFamily, ['Windows', 'BSD', 'Darwin', 'Solaris', 'Linux', 'Unknown'], true)) {
$this->error('Invalid PHP_OS_FAMILY literal: `' . $osFamily . '`');
}
}
protected function phpVersionIdToString(int $versionId): string
{
if ($versionId < 0) {
$this->error('Invalid PHP_VERSION_ID literal: `' . $versionId . '`');
}
$major = intdiv($versionId, 10000);
$minor = intdiv($versionId % 10000, 100);
$patch = $versionId % 100;
return $major . '.' . $minor . '.' . $patch;
$this->projectYamlLoader ??= new ProjectYamlLoader($this->phpVersion, fn(string $message): never => $this->error($message));
$this->projectYamlLoader->setPhpVersion($this->phpVersion);
return $this->projectYamlLoader;
}
protected function getInternalCeInfo(string $ce): array
@ -2343,13 +2044,13 @@ CODE;
protected function genClassCeList(): void
{
if (empty($this->interfaces) and empty($this->classes)) {
if (empty($this->symbols->interfaces()) and empty($this->symbols->classes())) {
return;
}
$sorter = new StringSort();
foreach ($this->interfaces as $interfaceDef) {
foreach ($this->symbols->interfaces() as $interfaceDef) {
$ce = $this->getClassCe($interfaceDef);
$deps = [];
@ -2371,14 +2072,14 @@ CODE;
$sorter->add($ce, $deps);
}
foreach ($this->classes as $classDef) {
foreach ($this->symbols->classes() as $classDef) {
$ce = $this->getClassCe($classDef);
$deps = [];
$parent = $classDef->extends;
if ($parent) {
// 不存在的父类,说明可能是内置类
$tmpCe = $this->getParentClassCe($classDef);
if (!isset($this->classes[$parent])) {
if (!$this->symbols->hasClass($parent)) {
$sorter->add($tmpCe);
}
$deps[] = $tmpCe;
@ -2388,7 +2089,7 @@ CODE;
if ($implements) {
foreach ($implements as $interface) {
$tmpCe = self::PREFIX . 'class_entry_' . $this->escapeCeName($interface);
if (!isset($this->interfaces[$interface])) {
if (!$this->symbols->hasInterface($interface)) {
$sorter->add($tmpCe);
}
$deps[] = $tmpCe;

Loading…
Cancel
Save