Skip to content

fix(autoload): keep WeakMap/WeakReference when only Composer-autoloaded files use them - #2288

Draft
phcorp wants to merge 2 commits into
illegalstudio:mainfrom
phcorp:fix/autoloaded-weakmap-prelude
Draft

phcorp wants to merge 2 commits into
illegalstudio:mainfrom
phcorp:fix/autoloaded-weakmap-prelude

Conversation

@phcorp

@phcorp phcorp commented Nov 8, 2026 •

Copy link
Copy Markdown

Summary

The core prelude decides whether to declare WeakMap and WeakReference by looking only at the entry program. That decision runs before the Composer autoload pass. So when only an autoloaded class names one of them, the class is pruned and the autoloaded file fails to compile:

error[src/Registry.php:5:5]: Unknown type: WeakMap

revolt/event-loop hits this (Internal\DriverSuspension, Internal\AbstractDriver, FiberLocal), so no program built on Revolt compiles today.

With this PR, the existing autoload::prelude_probe, which already does this for PDO, mysqli, curl, hash, fileinfo and others, also detects WeakMap and WeakReference in the autoload closure. core_prelude::inject_if_used then keeps them.

Repro

composer.json: {"autoload":{"psr-4":{"App\\":"src/"}}}

<?php // src/Registry.php
namespace App;
final class Registry {
    private ?\WeakReference $last = null;
    public function __construct(private \WeakMap $seen = new \WeakMap()) {}
    public function remember(object $k, string $v): void { $this->seen[$k] = $v; $this->last = \WeakReference::create($k); }
    public function count(): int { return \count($this->seen); }
}
<?php // main.php: never names WeakMap / WeakReference
$r = new App\Registry(); $a = new stdClass(); $r->remember($a, "a"); echo $r->count(), "\n";
output
main (145426b) error[src/Registry.php:5:5]: Unknown type: WeakMap
this branch compiles, prints 1
php 8.5.12 1

How it works

flowchart TD
    A[parse entry + resolve includes] --> B[autoload::Registry::build<br/>composer.json PSR-4 / classmap / files]
    B --> C["autoload-prelude-probe<br/>(disposable copy)"]
    C --> C1[name_resolver::resolve on a clone]
    C1 --> C2[autoload::run: load the whole class closure]
    C2 --> C3["run every PROBED_GROUPS detector<br/>… + weak_map, weak_reference"]
    C3 --> C4[restore span / anon-class counters]
    C4 --> D{"autoloaded(group)"}
    D --> E["preludes: pdo, mysqli, tz, image, hash, tokenizer, …"]
    D --> F["core_prelude::inject_if_used(ast,<br/>autoloaded(weak_map), autoloaded(weak_reference))"]
    F --> G[name resolution]
    G --> H["real autoload::run<br/>(class files now see WeakMap/WeakReference)"]
    H --> I[type check → EIR → codegen]
Loading
  • Two probe groups are added to PROBED_GROUPS:
    ("weak_map", crate::core_prelude::program_uses_weak_map),
    ("weak_reference", crate::core_prelude::program_uses_weak_reference),
  • core_prelude::program_uses_weak_map / program_uses_weak_reference apply the entry-program test (usage.classes / usage.literals) to the autoload closure.
  • inject_if_used takes the two probed flags. The rest of the selection is unchanged: WeakMap still implies WeakReference, and introspection or dynamic construction still keeps both.
  • pipeline.rs passes autoloaded("weak_map") and autoloaded("weak_reference"). Test frontends without autoload pass false, false.
  • Docs: the probe's detector lists in docs/compiling/compilation-pipeline.md, docs/internals/how-elephc-works.md and docs/internals/architecture.md now name every group. Before this PR they also left out tokenizer, fileinfo and web-environment.

Compile-time cost. Each new detector adds one prelude_prune::usage::collect walk over the closure. I timed a generated PSR-4 project with 200 autoloaded classes (12 methods each), using a debug build and --timings. The autoload-prelude-probe phase took 1.66 s on main and 1.00 s on the branch, out of about 410 s total in both cases. The difference is within noise, so there is no measurable cost.

Tests

Test Guards
probe_detects_weak_references_in_autoloaded_classes The probe reports weak_map for a promoted \WeakMap property and for a new \WeakMap() body, and weak_reference for a ?\WeakReference property
test_weak_map_composer_autoload_activation End to end through the production CLI: a Composer project whose entry never names either class compiles, runs and prints 1|other (same as php 8.5). It fails on main with Unknown type: WeakMap

Runs on c9eb795:

  • cargo test -p elephc --lib prelude_probe: 5 passed.
  • cargo test --test codegen_tests weak: 78 passed.
  • cargo test --test error_tests weak: 3 passed.
  • cargo build: no new warnings.
  • git diff --check: clean.

Note

CI status: this PR comes from a fork, so the CI, PDO Live Databases and PHP extension host SDK workflows are waiting for maintainer approval (action_required). The focused tests above ran locally on macOS arm64, and the codegen tests were also compared with PHP 8.5.12. The change has no target-specific code.

Limits

The probe is still an allow-list. The same bug affects other usage-gated preludes that are not probed. Each case below was measured on this branch with a PSR-4 class that uses the surface and an entry file that never names it. Each one compiles fine when the entry names the surface, and php 8.5 runs each one:

Surface in an autoloaded class elephc (this branch) php 8.5
new \Random\Randomizer(new \Random\Engine\Mt19937(42)) Undefined class: Random\Engine\Mt19937 1
\mt_srand(42); \mt_rand(1, 100) Undefined function: mt_srand 43
new \BcMath\Number("1.5") Undefined class: BcMath\Number 3.75
\DateTimeZone::listIdentifiers(...) Undefined function: __elephc_list_identifiers 1
\gmp_add(...) EIR: call to unknown function __elephc_gmp_add 123…891

Follow-ups

  • Add probe groups for random, bcmath, gmp and list_id. For random, bcmath and gmp this also needs the bridge-archive and link selection to see the probed flag, so it is more than a one-line PROBED_GROUPS entry. That is why it is left out of this PR.
  • Longer term, consider deriving the probe list from the prelude inventory (each usage-gated prelude exposes its detector) so a new prelude cannot silently skip the probe.
  • Merge order: independent of fix(checker): defer enum case ->value/->name member defaults until enum cases exist #2291.

@github-actions github-actions Bot added area:eir Touches EIR definitions, lowering, validation, or passes. area:optimizer Touches AST or EIR optimization passes. area:resolver Touches include, namespace, name, or autoload resolution. size:m Medium-sized pull request. type:fix Corrects broken or incompatible behavior. labels Nov 8, 2026
The probe now also detects WeakMap/WeakReference; the pipeline docs also
omitted the existing tokenizer, fileinfo and web-environment groups.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:eir Touches EIR definitions, lowering, validation, or passes. area:optimizer Touches AST or EIR optimization passes. area:resolver Touches include, namespace, name, or autoload resolution. size:m Medium-sized pull request. type:fix Corrects broken or incompatible behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant