Store code generation AST paths in a shared trie (#98581)
### What?
Store the AST paths that code generation uses to locate nodes in a
per-module trie, referenced by a 4-byte id, instead of a
`Vec<AstParentKind>` per path.
### Why?
Ast paths can share long prefixes leading to quadratic behavior (in
pathological cases), even in non pathological cases there can be
redundancy.
Peak memory on a repro of 180 files, each with a 1200-term chain:
| | peak | compile |
|---|---|---|
| before | 24.0 GB | 19.1 / 17.4 / 16.5s |
| after | 4.0 GB | 13.7 / 14.2 / 14.8s |
And on an ordinary app (`vercel-site`, 5,669 files; 5 cold runs +
warmup):
| | baseline | this branch |
|---|---|---|
| user time | 321.00 s ±0.8% | 321.30 s ±0.8% |
| MaxRSS | 14,984 MB ±0.4% | 14,755 MB ±0.4% |
### How?
The trie is built lazily, interning only the paths code generation asks
for, so it stays sparse. A builder holds the child index during analysis
and is frozen into an immutable trie stored beside the code gens.
`ApplyVisitors` previously sorted the full paths and binary searched
them at every level; it now descends the trie alongside the AST walk.
Consumers that manipulated paths structurally walk it instead —
`EsmBinding` steps to a parent rather than popping, and `removal.rs` and
`code_gen::path_to` trim from the end without allocating.
Interning was profiled separately. Sorting paths first, embedding the
child map in each node, resuming interning from the previous path, and
moving `Effect`'s paths into a second trie were all measured and
rejected: each was slower, needed more memory, or was inside the noise.
Interning is not a hot path.
Ideally we would also intern the paths stored in the temporary `Effect`
enum, but this is a pure time/memory tradeoff. We can massively reduce
pressure on the arena in exchange for a 10% perf regression. Not worth
it.
<!-- NEXT_JS_LLM -->
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>