unreachable FQCN => 'php-native' | 'unsupported' */ public array $categories = []; /** @var array> FQCN => reasons for demotion */ public array $reasons = []; /** @var list classes writing static props with no filed disposition */ public array $unfiledStaticState = []; public function __construct( private readonly SourceParser $sources, private readonly ReachabilityClosure $closure, private readonly Lists $lists, /** @var array reachable FQCN => preliminary category */ private readonly array $reachableCategories, ) { } public function run(): void { $universe = []; foreach ($this->sources->classes as $fqcn => $info) { if (isset($this->closure->reachable[$fqcn]) || $this->lists->isTwoWorld($fqcn)) { continue; } if (isset($this->lists->overrides[$fqcn])) { // The override fixes this class's category; it does not // participate in the fixed point. continue; } $universe[$fqcn] = $info; } foreach ($universe as $fqcn => $info) { $this->categories[$fqcn] = 'php-native'; if ($info->writesOwnStaticProps) { $disposition = $this->lists->staticDispositions[$fqcn] ?? null; if ($disposition === null) { $this->unfiledStaticState[] = $fqcn; $this->demote($fqcn, 'mutable static state with no filed disposition'); } elseif ($disposition === 'needs-sync') { $this->demote($fqcn, 'mutable static state shared with the Rust side (needs-sync)'); } } } do { $changed = false; foreach ($universe as $fqcn => $info) { if ($this->categories[$fqcn] === 'unsupported') { continue; } $bad = $this->firstBadRef($info); if ($bad !== null) { $this->demote($fqcn, $bad); $changed = true; } } } while ($changed); } private function firstBadRef(ClassInfo $info): ?string { foreach (array_unique($info->newRefs) as $ref) { $problem = $this->checkTarget($info, $ref, true); if ($problem !== null) { return $problem; } } foreach (array_unique($info->staticRefs) as $ref) { $problem = $this->checkTarget($info, $ref, false); if ($problem !== null) { return $problem; } } return null; } private function checkTarget(ClassInfo $info, string $ref, bool $isNew): ?string { if ($ref === $info->fqcn) { return null; } $category = $this->lists->overrides[$ref]['category'] ?? $this->reachableCategories[$ref] ?? null; if ($category !== null) { if ($isNew && $category === 'rust-proxy') { return "constructs proxied service $ref (dual instantiation unresolved)"; } return null; } if ($this->lists->isTwoWorld($ref)) { return null; } if (isset($this->sources->classes[$ref])) { if (($this->categories[$ref] ?? 'unsupported') === 'unsupported') { return ($isNew ? 'constructs' : 'statically references') . " unsupported type $ref"; } return null; } if (VendorPackages::lookup($ref) !== null) { // Vendor packages ship as real PHP in the child process // regardless of their bridging category. return null; } if (!str_contains($ref, '\\')) { // Global namespace: PHP builtin. return null; } return ($isNew ? 'constructs' : 'statically references') . " unknown type $ref"; } private function demote(string $fqcn, string $reason): void { $this->categories[$fqcn] = 'unsupported'; $this->reasons[$fqcn][] = $reason; } }