Preserve catch-observable property and computed loads during dead-code elimination, and record their root operands as reactive-scope dependencies so throwing reads remain inside `try`.
This prevents memoization from suppressing catch behavior or hoisting potentially throwing property access into cache guards.
Validated with the React Compiler crate suite, NAPI transform suite, and original-vs-compiled runtime cases.
AI assistance was used to investigate and implement this change.
Preserve JSX component classification through React Compiler HIR and codegen so underscore-prefixed components are emitted as identifiers instead of intrinsic string tags.
Update the existing context-variable JSX fixture to cover `_Component`.
Validated with the React Compiler crate suite, NAPI suite, and all six ecosystem reproductions.
AI assistance was used.
React Compiler rebuilt source-derived AST nodes with empty spans, so compiled function bodies had no useful source mappings. Preserve retained HIR and reactive spans during codegen while keeping synthetic memoization scaffolding unmapped.
AI-assisted: implementation used Codex; the author remains responsible for review.
Model tagged-template expressions as function applications during mutation and alias inference. This keeps mutations of returned objects within the memoization scope and preserves the implicit template argument position.
Checked with `cargo test -p oxc_react_compiler` and the transform-react NAPI suite.
AI-assisted.
The discovery walker let a declarator name flow through `forwardRef`/`memo` calls, so `const Foo = forwardRef((props, viewTracker) => ...)` was treated as a named component and rejected by the component param check (a second param must look like a ref). Upstream only names a function from its direct parent (declarator/assignment/property); a function in call arguments is anonymous and is detected through the forwardRef/memo callback branch, which never checks params.
Fixes the two Simorgh files from the ecosystem comparison (`TopStoriesItem`, `LatestMediaItem`): both now compile with output matching upstream, including identical memo cache sizes. Adds a fixture for the pattern; no existing fixture snapshots change.
Created with assistance from Claude.
Compile parenthesized arrow components by preserving declarator name inference through nested parentheses.
Adds a React Compiler fixture for the ecosystem reproduction.
Created with assistance from Codex.
Align ref validation with Babel React Compiler 1.0 by limiting call-site alias-effect checks to values actually passed to the call. This keeps refs captured by returned event handlers valid while preserving diagnostics for refs passed to invoked functions.
Validated with `cargo test -p oxc_react_compiler`, the rebuilt `oxc-transform-react` binding against Outline’s `ContentEditable.tsx`, and `just ready`.
AI-assisted; the contributor remains responsible for review and submission.
Disable exhaustive memo dependency validation by default and align
suppression handling.
AI-assisted.
<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
Fix React Refresh when used with the React Compiler N-API transform by
preserving original hook argument and binding-pattern spans through
compiler codegen.
This prevents refresh signature generation from panicking and keeps
signatures based on the original source.
Validated with `just ready` and the React transform N-API suite.
Generated with AI assistance; reviewed and validated by the contributor.
## Summary
- Groups `Class::super_class` and `Class::super_type_arguments` into `Class::heritage`.
- Introduces `ClassHeritage` containing the superclass expression and its optional TypeScript type arguments.
- Makes superclass type arguments without a superclass expression unrepresentable.
## Breaking Change
The Rust AST changes from:
```rust
pub struct Class<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub r#type: ClassType,
pub decorators: Vec<'a, Decorator<'a>>,
pub id: Option<BindingIdentifier<'a>>,
#[ts]
pub type_parameters: Option<Box<'a, TSTypeParameterDeclaration<'a>>>,
pub super_class: Option<Expression<'a>>,
#[ts]
pub super_type_arguments: Option<Box<'a, TSTypeParameterInstantiation<'a>>>,
#[ts]
pub implements: Vec<'a, TSClassImplements<'a>>,
pub body: Box<'a, ClassBody<'a>>,
#[ts]
pub r#abstract: bool,
#[ts]
pub declare: bool,
pub scope_id: Cell<Option<ScopeId>>,
}
```
to:
```rust
pub struct Class<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub r#type: ClassType,
pub decorators: Vec<'a, Decorator<'a>>,
pub id: Option<BindingIdentifier<'a>>,
#[ts]
pub type_parameters: Option<Box<'a, TSTypeParameterDeclaration<'a>>>,
pub heritage: Option<ClassHeritage<'a>>,
#[ts]
pub implements: Vec<'a, TSClassImplements<'a>>,
pub body: Box<'a, ClassBody<'a>>,
#[ts]
pub r#abstract: bool,
#[ts]
pub declare: bool,
pub scope_id: Cell<Option<ScopeId>>,
}
pub struct ClassHeritage<'a> {
pub expression: Expression<'a>,
#[ts]
pub type_arguments: Option<Box<'a, TSTypeParameterInstantiation<'a>>>,
}
```
## Why?
ECMAScript defines the complete `extends` clause as `ClassHeritage`:
```text
ClassHeritage : extends LeftHandSideExpression
```
TypeScript additionally permits type arguments on the heritage expression:
```ts
class Derived extends Base<T> {}
```
Previously, the AST stored the expression and type arguments independently:
```text
Class {
super_class: Some(Base),
super_type_arguments: Some(<T>),
...
}
```
This allowed an invalid state to be constructed:
```text
Class {
super_class: None,
super_type_arguments: Some(<T>),
...
}
```
Superclass type arguments cannot exist without an `extends` expression.
The parser already treated these fields as a pair. It parsed a class heritage entry and then split its expression and type arguments into two independent fields. Builders and transforms therefore had to preserve their relationship manually.
Grouping them into `ClassHeritage` makes the syntactic dependency explicit and prevents consumers from observing or constructing contradictory class heritage.
## Class heritage forms
A class without an `extends` clause has no heritage:
```js
class Base {}
```
```text
Class {
heritage: None,
...
}
```
A class extending an expression has heritage without type arguments:
```js
class Derived extends Base {}
```
```text
Class {
heritage: Some(ClassHeritage {
expression: Base,
type_arguments: None,
}),
...
}
```
TypeScript type arguments belong to the same heritage structure:
```ts
class Derived extends Base<T> {}
```
```text
Class {
heritage: Some(ClassHeritage {
expression: Base,
type_arguments: Some(<T>),
}),
...
}
```
The heritage expression may be any valid class heritage expression:
```js
class Derived extends mixin(Base) {}
class NullDerived extends null {}
```
## How to migrate
Code that previously accessed the superclass expression directly:
```rust
if let Some(super_class) = &class.super_class {
// ...
}
```
can use the compatibility accessor:
```rust
if let Some(super_class) = class.super_class() {
// ...
}
```
Code that needs both parts should access the grouped heritage:
```rust
if let Some(heritage) = &class.heritage {
let expression = &heritage.expression;
let type_arguments = heritage.type_arguments.as_deref();
}
```
Mutating consumers should update the nested fields:
```rust
if let Some(heritage) = &mut class.heritage {
heritage.type_arguments = None;
}
```
Common migrations:
| Previous representation | New representation |
| --- | --- |
| `class.super_class` | `class.heritage.as_ref().map(\|h\| &h.expression)` or `class.super_class()` |
| `class.super_type_arguments` | `class.heritage.as_ref()?.type_arguments` or `class.super_type_arguments()` |
| `class.super_class.is_some()` | `class.heritage.is_some()` |
| Mutate `class.super_class` | Mutate `class.heritage.expression` |
| Clear `class.super_type_arguments` | Clear `class.heritage.type_arguments` |
| Pass superclass and type arguments separately to a `Class` builder | Construct and pass `ClassHeritage` |
| Clone both fields independently | Clone `class.heritage` |
```ts
superClass: Expression | null;
superTypeArguments?: TSTypeParameterInstantiation | null;
```
No `heritage` property or `ClassHeritage` node is added to the ESTree output.
fixes https://github.com/oxc-project/backlog/issues/216
## Summary
- Removes `VariableDeclarator::kind`.
- Makes `VariableDeclaration::kind` the single source of truth for every declarator in its declaration list.
- Makes contradictory parent and child declaration kinds unrepresentable.
## Breaking Change
The Rust AST changes from:
```rust
pub struct VariableDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub kind: VariableDeclarationKind,
pub declarations: Vec<'a, VariableDeclarator<'a>>,
#[ts]
pub declare: bool,
}
pub struct VariableDeclarator<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
#[estree(skip)]
pub kind: VariableDeclarationKind,
#[estree(via = VariableDeclaratorId)]
pub id: BindingPattern<'a>,
#[ts]
#[estree(skip)]
pub type_annotation: Option<Box<'a, TSTypeAnnotation<'a>>>,
pub init: Option<Expression<'a>>,
#[ts]
pub definite: bool,
}
```
to:
```rust
pub struct VariableDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub kind: VariableDeclarationKind,
pub declarations: Vec<'a, VariableDeclarator<'a>>,
#[ts]
pub declare: bool,
}
pub struct VariableDeclarator<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
#[estree(via = VariableDeclaratorId)]
pub id: BindingPattern<'a>,
#[ts]
#[estree(skip)]
pub type_annotation: Option<Box<'a, TSTypeAnnotation<'a>>>,
pub init: Option<Expression<'a>>,
#[ts]
pub definite: bool,
}
```
## Why?
A variable declaration has one declaration kind shared by every declarator in its declaration list:
```js
const first = 1, second = 2;
```
Previously, the AST stored that kind on both the parent declaration and every child declarator:
```text
VariableDeclaration {
kind: Const,
declarations: [
VariableDeclarator { kind: Const, ... },
VariableDeclarator { kind: Const, ... },
],
}
```
This allowed contradictory ASTs to be constructed:
```text
VariableDeclaration {
kind: Const,
declarations: [
VariableDeclarator { kind: Var, ... },
],
}
```
Different consumers read different copies:
- Code generation used `VariableDeclaration::kind`.
- Semantic binding used `VariableDeclarator::kind`.
- Lint rules, transforms, and minifier passes used a mixture of both.
As a result, one AST could be emitted as `const` while its binding was treated as function-scoped `var`.
Transforms also had to keep the copies synchronized manually. For example, const-to-let normalization updated the parent and then looped through every declarator to update each child. Explicit resource management performed similar paired mutations for `using` declarations.
Keeping the kind only on `VariableDeclaration` removes this synchronization requirement and prevents consumers from observing conflicting declaration semantics.
## Declaration forms
The declaration kind now belongs exclusively to the node that owns the declaration list:
```js
let value;
```
```text
VariableDeclaration {
kind: Let,
declarations: [
VariableDeclarator { id: value, ... },
],
}
```
Multiple declarators share the same parent kind:
```js
const first = 1, second = 2;
```
```text
VariableDeclaration {
kind: Const,
declarations: [
VariableDeclarator { id: first, ... },
VariableDeclarator { id: second, ... },
],
}
```
## How to migrate
Code that previously read the kind from a declarator:
```rust
if declarator.kind.is_const() {
// ...
}
```
should now read it from the containing declaration:
```rust
if declaration.kind.is_const() {
// ...
}
```
Consumers operating through an AST node store should retrieve the parent `VariableDeclaration`:
```rust
let AstKind::VariableDeclaration(declaration) =
nodes.parent_kind(declarator.node_id())
else {
unreachable!();
};
let kind = declaration.kind;
```
Common migrations:
| Previous representation | New representation |
| --- | --- |
| `declarator.kind` | Containing `declaration.kind` |
| Match `VariableDeclarator::kind` during semantic binding | Read the parent `VariableDeclaration` from ancestry |
| Pass `kind` to `VariableDeclarator::new` | Remove the `kind` argument |
| Update parent and every child kind | Update only `VariableDeclaration::kind` |
| Recover kind after detaching a declarator | Capture or pass the parent kind before detaching it |
| Inspect a symbol’s declarator kind in lint rules | Resolve its parent declaration through the node store |
When constructing declarators, use the generated kindless `VariableDeclarator` builders and place them inside a `VariableDeclaration` carrying the required kind.
fixes https://github.com/oxc-project/backlog/issues/237
Preserve Babel's non-node classification for parenthesized object returns so ordinary PascalCase factories are not compiled as React components.
Validated against the React Compiler suite and the Kibana reproduction.
AI-assisted.
Fix React Compiler discovery of `memo` and `forwardRef` callbacks in runtime parameter defaults when the enclosing function is skipped.
Validated against the React Compiler suite and the Kibana reproduction.
AI-assisted.
## Summary
- Introduces `ArrowFunctionBody` to represent concise expression and block bodies explicitly.
- Removes the `ArrowFunctionExpression::expression` boolean.
- Makes invalid arrow body combinations unrepresentable.
## Breaking Change
The Rust AST changes from:
```rust
pub struct ArrowFunctionExpression<'a> {
pub expression: bool,
pub body: Box<'a, FunctionBody<'a>>,
}
```
to:
```rust
pub enum ArrowFunctionBody<'a> {
FunctionBody(Box<'a, FunctionBody<'a>>) = 64,
INHERIT(Expression<'a>),
}
pub struct ArrowFunctionExpression<'a> {
pub body: ArrowFunctionBody<'a>,
}
```
Concise arrow bodies are now stored directly as expressions instead of synthetic `FunctionBody` and `ExpressionStatement` nodes. Block bodies continue to use `FunctionBody`.
## Why?
The previous representation required consumers to maintain this implicit invariant:
```text
expression == true
→ no directives
→ exactly one statement
→ that statement is an ExpressionStatement
```
Invalid combinations could cause panics, skipped work, or fallback behavior. The enum encodes the grammar directly and makes those invalid states unrepresentable.
Concise expressions also now have `ArrowFunctionExpression` as their direct traversal ancestor, without synthetic `FunctionBody` or `ExpressionStatement` nodes.
## How to migrate
Use the arrow body accessors or match on `ArrowFunctionBody` rather than inspecting `expression` and synthetic statements.
Common field migrations:
| Previous representation | New representation |
| --- | --- |
| `arrow.expression` | `arrow.is_expression()` |
| `arrow.get_expression()` | `arrow.get_expression()` |
| `&arrow.body.statements` | `arrow.get_function_body().map(\|body\| &body.statements)` |
| `&mut arrow.body.statements` | `arrow.get_function_body_mut().map(\|body\| &mut body.statements)` |
| Synthetic expression statement in `arrow.body` | Direct `ArrowFunctionBody` expression variant |
| `arrow.body = function_body` | `arrow.body = ArrowFunctionBody::FunctionBody(function_body)` |
| Expression-to-block conversion | Create a `FunctionBody` containing a return statement |
| Block-to-expression conversion | Assign the expression directly to `arrow.body` |
| Builder `(expression, body)` arguments | Pass a single `ArrowFunctionBody` |
closes https://github.com/oxc-project/backlog/issues/211
AST builder methods accepted an `IntoIn<ArenaBox<T>>` for all params where an `ArenaBox<T>` is required. You could pass a plain `T`, and the builder method would automatically allocate space for it, and copy it into the arena.
This was originally intended as a convenience, but in fact it's a footgun - it encourages allocating late, holding types on the stack during other work, which results in register pressure, and churn of copying to/from the stack.
Remove the automatic conversion. All places where we were using auto-allocation are already fixed in previous PRs #25034, #25035, #25036, #25037.
Additional benefits of this change:
1. AST builder methods become less polymorphic which should help with compile times.
2. We can get rid of the `NONE` type, which was a hacky workaround, necessary for passing as params where an `IntoIn<Option<ArenaBox<T>>>` is required. Now these all become plain `None`.
Most of the diff in this PR is just replacing `NONE` with `None` all over the place.
Note: AST builder methods still define params which are `ArenaVec`s as `IntoIn<ArenaVec<T>>`, as this does add genuine value - it allows passing `[]` instead of the much more verbose `ArenaVec::new_in(self)`.
Continuation of #25034.
In React compiler, move some allocations to happen earlier.
When we need an `ArenaBox<T>` for later method calls, it's preferable to allocate it into arena as swiftly as possible, so only hold an 8-byte `ArenaBox` on stack, instead of the whole `T`, which is much larger. This reduces stack and register pressure.
Preserve warning-level React Compiler diagnostics instead of wrapping pipeline bailouts in a synthetic fatal error. This keeps expected incompatible-library and Todo bailouts non-fatal so sibling functions can still compile.
Adds regression coverage and refreshes the affected snapshots. Validated with `just ready`.
AI-assisted: Codex was used to investigate, implement, and validate this change.
## Context
Value sets in the aliasing inference state are `Rc<FxHashSet<ValueId>>`,
shared between states rather than copied (#24117). When two states meet
at a join, the merge still walks every variable and probes one set
against the other looking for a value the current state doesn't have.
Most of the time both sides point at the same `Rc`, so that probe is
comparing a set to itself.
## This PR
Check `Rc::ptr_eq` before probing. If the pointers match there is
nothing to add, so we skip the scan. Both merge paths did this check in
slightly different ways, so I pulled it into one helper. The sets are
never mutated in place, which is what makes the pointer check safe.
### Performance
| workload | main | this PR | delta |
| ----------------------------------------------------- | -------: |
-------: | ------: |
| 3.6k-line control-flow heavy component, instructions | 23.762 B |
22.034 B | -7.3% |
| same file, wall time | 4.55 s | 4.13 s | -9% |
| 16.5k-file real corpus, instructions (1 thread) | 50.135 B | 49.697 B
| -0.87% |
This PR was assisted by Claude Code.
Preserve source-specific labels for aliasing diagnostics while retaining
their shared analysis identity. Previously, interning by place, message,
and help reused the first source location for later effects.
Validated with `just ready` and React ecosystem comparisons.
AI-assisted: Codex was used to implement and validate this change.
The reactive-tree transform passes rebuilt each mutated block by
deep-cloning statements into a fresh vector. With the tree now
arena-backed, those clones (and the abandoned scratch vectors) were
never freed until the per-file arena dropped — a churn tail on top of
the tree's relocation into the arena. This removes it.
## What changed
- `traverse_block` — the shared transform engine used by all 7
reactive-scopes passes — now mutates the block's own arena buffer
directly: `Keep` transforms in place, `Replace` overwrites the slot,
`Remove` is `Vec::remove`, `ReplaceMany` is `Vec::splice`. This drops
(a) the deep `clone_in` of the block prefix on the first structural
change, (b) the scratch `next_block` vector, and (c) the per-statement
placeholder swap. The index bookkeeping (advance on Keep/Replace, hold
on Remove, skip inserted on ReplaceMany) reproduces the exact original
order.
- Merge Pass-3 already `mem::replace`s the block out — so it owns the
statements — but then cloned them back into the result; it now moves
them via `into_iter`.
Behavior is unchanged — snapshot corpus and transform tests remain
byte-identical.
## Memory
Arena bytes across the full 1269-fixture corpus (fresh allocator per
file, after `compile`): **12.97 MB → 12.12 MB (−6.5%)**, recovering most
of the reactive-tree flip's churn tail. The deep `clone_in` of statement
subtrees is replaced by cheap moves (a CPU win on transform-heavy
components, in addition to the allocation drop).
## Stack
- #24664
- #24666
- #24696
- #24700
- #24702
- **this PR** — stacked on top of #24702🤖 This PR was written with the assistance of Claude Code.
The final subtree of the react_compiler HIR-to-arena migration: the
ReactiveFunction tree's own structural vectors and boxes move off the
heap into the compilation arena, so the reactive tree stops straddling
two allocators.
## What changed
- `ReactiveBlock` (`= Vec<ReactiveStatement>`, every block body),
`ReactiveValue::SequenceExpression.instructions`, and
`ReactiveTerminal::Switch.cases` become `oxc_allocator::Vec<'a>`.
- The tree's `Box` fields (`ReactiveStatement::Terminal`, the boxed
`ReactiveValue` sub-expressions, ...) become `oxc_allocator::Box<'a>` —
required so the element types are `!needs_drop` and can live in an arena
`Vec` (`ReactiveStatement` was `Drop` via its std boxes). This completes
the tree's move (Vec + Box) into the arena.
- The transform passes' `std::mem::take` on block bodies (arena `Vec`
has no `Default`) become `std::mem::replace(.., Vec::new_in(alloc))`;
construction sites allocate from `env.allocator`.
- `Transformed::ReplaceMany` deliberately stays a std `Vec` — a
transient replacement list that `traverse_block` drains into the arena
block — so the generic visitor enum and every pass's `ReplaceMany`
construction are untouched.
Behavior is unchanged — snapshot corpus and transform tests remain
byte-identical.
## Memory
Arena bytes across the full 1269-fixture corpus (fresh allocator per
file, after `compile`):
| | total arena used | max single fixture |
|---|---|---|
| before (tree on heap) | 9.93 MB | 228 KB |
| after (tree in arena) | 12.97 MB | 297 KB |
The +3.04 MB (+31%) is a relocation of the tree off the heap — smaller
than the CFG subtree's +82% shift, and no fixture balloons.
## Follow-up (not in this PR)
The reactive-scopes passes rebuild block bodies with a clone-prefix +
take/rebuild pattern; with the arena the abandoned spine isn't freed
until the per-file arena drops (a bounded churn tail, included in the
+31% above). Rewriting `traverse_block` and the merge Pass-3 to
drain/splice the block in place would collapse that churn tail **and**
delete a pre-existing wasteful deep-clone (a CPU win). Deferred to its
own PR because the in-place splice must be verified byte-identical
across the whole corpus.
## Stack
- #24664
- #24666
- #24696
- #24700
- **this PR** — stacked on top of #24700🤖 This PR was written with the assistance of Claude Code.
The largest step of the react_compiler HIR-to-arena migration: moves the
control-flow-graph HIR and the reactive-function tree off the global
heap into the compilation arena, eliminating the per-node
`malloc`/`free` churn (block instruction/phi vectors, predecessor sets,
phi operand maps, instruction-value vectors) that dominated the
compiler's allocation traffic.
## What changed
- `HIR`, `BasicBlock`, `Phi`, `Terminal`, `AliasingEffect`,
`ArrayPattern`, and `AliasingSignature` gain an `'a` lifetime; their
`Vec` fields become `oxc_allocator::Vec<'a>` (block instructions/phis,
terminal effects/cases, all `InstructionValue` vectors, patterns,
aliasing args/captures, function
params/context/directives/aliasing-effects, ...).
- The two per-node insertion-ordered containers move to the arena:
`BasicBlock.preds` → `ArenaOrderedSet<'a, BlockId>` and `Phi.operands` →
`ArenaOrderedMap<'a, BlockId, Place>` — new arena-backed variants
(`oxc_allocator::Vec` + `oxc_allocator::HashMap`) added alongside the
existing std `OrderedMap`/`OrderedSet`, which are kept.
- `HIR.blocks` deliberately stays a std `OrderedMap` (one map per
function, no allocation payoff, and `HirFunction` is never arena-boxed —
it lives in a std `IndexVec`). This also keeps
`get_reverse_postordered_blocks` returning a std map, bounding arena
high-water.
- Arena `Vec` is not `Clone`, so the arena-carrying HIR and
reactive-tree types drop `#[derive(Clone)]` and get hand-written
same-arena `CloneIn` impls; construction/clone sites across ~30 files
thread the allocator from `env.allocator`.
Behavior is unchanged — the snapshot corpus and transform tests remain
byte-identical.
## Memory
Arena bytes used across the full 1269-fixture corpus (fresh allocator
per file, measured after `compile`):
| | total arena used | max single fixture |
|---|---|---|
| before (HIR on heap) | 5.45 MB | 171 KB |
| after (HIR in arena) | 9.93 MB | 228 KB |
The +4.48 MB is the HIR moving from the heap into the arena (offsetting
the corresponding heap allocations). No pathological ballooning:
RPO-heavy fixtures grow ~2-3× at negligible absolute scale, and arenas
free per-file so nothing accumulates across the corpus.
## Stack
- #24664
- #24666
- #24696
- **this PR** — stacked on top of #24696🤖 This PR was written with the assistance of Claude Code.
Continues the react_compiler HIR-to-arena migration by moving the
per-compilation `ReactiveScope` collections off the global heap and into
the compilation arena.
## What changed
- `ReactiveScope::{dependencies, declarations, reassignments, merged}`
and `ReactiveScopeDependency::path` change from `std::vec::Vec` to
`oxc_allocator::Vec<'a>`.
- Arena `Vec` is not `Clone`, so
`ReactiveScope`/`ReactiveScopeDependency` drop `#[derive(Clone)]`.
`ReactiveScopeDependency` and `DependencyPathEntry` get hand-written
same-arena `CloneIn` impls (leaves copy, the arena `Vec` recurses);
`ReactiveScopeDeclaration` becomes `Copy`.
- Construction and clone sites across the reactive-scope and inference
passes now allocate from `env.allocator` (`clone_in` /
`ArenaVec::*_in`); a few dependency-collection helpers take an explicit
`&'a Allocator`.
Behavior is unchanged — the snapshot corpus and transform tests remain
byte-identical.
## Stack
- #24664
- #24666
- **this PR** — stacked on top of #24666🤖 This PR was written with the assistance of Claude Code.
The HIR's insertion-ordered CFG containers were `indexmap`-based:
- `HIR.blocks: FxIndexMap<BlockId, BasicBlock>` — iteration order is
reverse-postorder, which is load-bearing (forward dataflow visits each
block after its predecessors).
- `BasicBlock.preds: FxIndexSet<BlockId>` — predecessor order sets
phi-operand order.
- `Phi.operands: FxIndexMap<BlockId, Place>` — one operand per
predecessor, in that order.
`indexmap` has no arena-allocator variant, which blocks moving the HIR
into an arena. `Vec` and hash maps do (`oxc_allocator::Vec` /
`oxc_allocator::HashMap`), so this replaces those three fields with
`OrderedMap` / `OrderedSet` — ordered containers backed by `Vec<(K, V)>`
plus an `FxHashMap<K, usize>` index. They mirror the subset of the
`IndexMap` / `IndexSet` API the HIR uses
(`get`/`get_mut`/`insert`/`shift_remove`/`retain`/`drain`/`keys`/`values`/`iter[_mut]`/`get_index_of`,
`IntoIterator`, `Index`, `DoubleEndedIterator`), preserving insertion
order and O(1) lookup, so call sites are unchanged.
This is a preparatory step: the three fields are now composed of
arena-convertible primitives, so the eventual arena move is a mechanical
`Vec` -> `Vec<'a>` / `FxHashMap` -> arena `HashMap` swap rather than an
impossible `indexmap` port.
The `SymbolId`-keyed builder maps and `StabilizeBlockIds`' scratch set
stay `FxIndexMap` / `FxIndexSet` — they are pass-local working data, not
HIR CFG structures.
Pure internal change: the fixtures snapshot is byte-identical; new unit
tests cover the `shift_remove` / `retain` / `drain` reindexing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
The `MutateFrozen`/`MutateGlobal`/`Impure` variants of `AliasingEffect`
each held an owned `OxcDiagnostic`. Objects allocated into an oxc arena
are never dropped, so `OxcDiagnostic` — which owns `Cow<'static, str>`
message/help plus a `Labels` vec — is a `Drop` type that blocks moving
the HIR into an arena.
Move the diagnostics into an `Environment`-owned side table and store a
`Copy` `DiagnosticId` in the effect instead:
- `Environment::intern_aliasing_diagnostic` interns a diagnostic and
returns its id, deduplicated on `(place, message, help)` — the same
granularity as the effect interning key (`EffectKey`). Identical errors
still collapse to one stored diagnostic, so `EffectKey` can key on the
id directly instead of cloning the message/help strings.
- The table uses `RefCell` because two effect-building passes
(`compute_signature_for_instruction`,
`compute_effects_for_legacy_signature`) only hold `&Environment`.
Interning is off the hot path — it happens only when an error effect is
produced.
- The range pass resolves the id back to the diagnostic at emit time
(`env.record_diagnostic(env.aliasing_diagnostic(id))`), the point at
which these errors were already surfaced.
`AliasingEffect` no longer carries any `Drop` diagnostic; its remaining
non-arena fields are the `Vec`s, which are the mechanical arena-`Vec`
migration tracked separately.
Pure internal change: the fixtures snapshot is byte-identical.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Move all `Ident`\-hashing code from `oxc_allocator` into `oxc_str`, to sit alongside `Ident`'s code.
This feels like the natural place for this code, and reduces the public API surface - we no longer need to expose `Ident`'s internals (`ident_hash`). Now only `IdentHasher` and `IdentBuildHasher` are exposed.
Only change to code is removing `pack_len_hash`, as it was only used in tests.
A follow-up dead-code sweep of `oxc_react_compiler`. Everything here is
**behavior-preserving** — the removed branch/variant never executed, and
the rest is stale attributes, a pass-through merge, and visibility.
## Dead code removed
- **Never-wired branch in `rename_variables`** —
`rename_variables_with_parent`'s `parent_names` was always `None` (its
only caller passes `None`; it doesn't recurse), so the `Some(..)`
"outlined functions" block and the wrapper split were dead. Inlined into
`rename_variables`.
- **`ApplyArgConfig::Spread`** — never constructed anywhere (only a
defensive match arm); removed the variant, its write-only `kind` field,
`Hole.kind`, and the now-orphaned `ApplyArgSpreadKind` /
`ApplyArgHoleKind` enums. (`ApplyArgConfig` derives only `Debug, Clone`,
so nothing built it via serde either.)
## Stale attributes / redundant machinery
- Delete 4 `#[allow(dead_code)]` markers whose items are actually used
now: `OxcContext::{fbt_operands, record_error}`, `Driver.env`,
`InferredDependency::Local.context`.
- Merge the pure pass-through `Unifier::unify` into `unify_impl`
(identical signature, no accumulator) — a single `unify`.
## Visibility
Tighten 8 items referenced only within their own module to private /
`pub(crate)`: `GlobalRegistry`/`ShapeRegistry::into_inner`,
`post_dominators_of`, `collect_maybe_memo_dependencies`,
`is_always_invalidating_type`, `MEMO_CACHE_SENTINEL`, `next_type_id`,
`validate_context_variable_lvalues_with_errors`.
`14 files changed, +17 / -85`. `cargo clippy` clean; unit + transform
tests pass.
Follow-up cleanup of `globals.rs` (and its two callers) in `oxc_react_compiler`. Removes machinery that no longer earns its keep — no behavior change.
## What
- **Collapse the `install_type_config` split.** `install_type_config_with_errors` wrapped a private `install_type_config_inner` whose `errors: &mut Option<&mut Vec<String>>` was *always* `Some` — the sole caller passed `Some` and the recursion only forwarded it. Vestigial from a non-error variant that no longer exists. Now a single `install_type_config` taking `&mut Vec<String>`.
- **Remove `shrink_global` / `shrink_shape`.** These were `fn(&T<'static>) -> &T<'a> { x }` identity functions; lifetime variance already performs that coercion in `Registry::get`, so they were no-op noise.
- **Tighten visibility.** `build_builtin_shapes` / `build_default_globals` are only used inside the `BASE` initializer, so they no longer need to be `pub`.
Net `3 files changed, 27 insertions(+), 53 deletions(-)`.
`AliasingEffect::Apply` held `Option<Rc<FunctionSignature>>`. Objects allocated into an oxc arena are never dropped, so a refcounted `Rc` — like any `Drop` type — blocks moving the HIR into an arena.
The `Rc` was never shared ownership for its own sake; it was a borrow-detachment device. The signature is a deterministic lookup (`env.get_function_signature(env.types[…])`), read exactly once in the same pass that builds it and ignored by every later pass. The `Rc` existed only to snapshot the registry signature into owned data so `apply_effect` could read its fields while calling `&mut env` effect computations.
Store the callee's `TypeId` instead and resolve the `FunctionSignature` on demand. `TypeId` is `Copy`/non-`Drop` and lifetime-free, so no lifetime is forced onto `AliasingEffect`/`Terminal`/`HIR`. The id is propagated unchanged through the place-substitution remap in `compute_effects_for_aliasing_signature`, preserving the exact signature identity (re-deriving from the *substituted* callee place would not). The read site clones the resolved signature into a local to reproduce the previous borrow detachment. The now-dead `get_function_call_signature` helper is removed.
Pure internal change: the fixtures snapshot is byte-identical.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---
<sub>Stack created with <a href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
The `react_compiler` HIR is an SSA/CFG IR, not an AST clone, so most of it cannot be `Copy` (the aggregate nodes hold `Vec`/`FxIndexMap`/`Box`). But a number of its **leaf** types have all-`Copy` fields and were needlessly deriving only `Clone`.
This derives `Copy` on the 23 leaf HIR types where it is free — `Place`, `Identifier`, `MutableRange`, `LValue`, the pattern/element enums (`ParamPattern`, `SpreadPattern`, `ArrayPatternElement`, `ArrayElement`, `PlaceOrSpread`, `ObjectPropertyOrSpread`, `ObjectProperty`, `ObjectPropertyKey`), the JSX leaves (`JsxTag`, `JsxAttribute`, `BuiltinTag`), `LoweredFunction`, `TemplateQuasi`, `PropertyNameKind`, and the binding/dep leaves (`VariableBinding`, `NonLocalBinding`, `ManualMemoDependencyRoot`, `DependencyPathEntry`) — and drops the now-redundant `.clone()` / lazy-eval calls that clippy flags as a result.
The aggregate nodes are deliberately untouched. This is a pure internal cleanup: the fixtures snapshot is byte-identical.
Net **-151 lines** across 31 files.
Pure refactor.
Shorten code in React compiler by using `NONE` instead of writing out the type long-hand.
e.g. `None::<ArenaBox<TSTypeParameterDeclaration>>` -> `NONE`
Pure refactor. Just shorten code.
#24621 made AST builder methods take arrays as params.
Replace `ArenaVec::from_value_in(v, alloc)` with `[v]` where the `ArenaVec` is passed to an AST builder method.
```rs
let block = Statement::new_block_statement(
SPAN,
ArenaVec::from_value_in(return_stmt, ctx),
ctx,
);
```
\->
```rs
let block = Statement::new_block_statement(SPAN, [return_stmt], ctx);
```
Pure refactor. Just shorten code.
#24621 made AST builder methods take arrays as params.
Replace `ArenaVec::new_in(...)` arguments written inline at AST builder method call sites with the empty array literal `[]`.
```rs
let block = Statement::new_block_statement(SPAN, ArenaVec::new_in(ctx), ctx);
```
\->
```rs
let block = Statement::new_block_statement(SPAN, [], ctx);
```
## Summary
- replace the generic Rust MetaProperty AST node with dedicated ImportMeta and NewTarget nodes
- preserve the ESTree MetaProperty shape and generated public TypeScript types
- update parser, formatter, codegen, lint, transform, minifier, traversal, and React compiler consumers
## Summary
Remove `MetaProperty` AST node, in favour of two separate nodes (`ImportMeta` and `NewTarget`)
## Breaking Change
The Rust AST field changes from:
```rust
pub struct MetaProperty<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub meta: IdentifierName<'a>,
pub property: IdentifierName<'a>,
}
```
to:
```rust
/// /// `import.meta` in `console.log(import.meta);`.
pub struct ImportMeta {
pub node_id: Cell<NodeId>,
pub span: Span,
}
/// `new.target` in `function F() { return new.target; }`.
pub struct NewTarget {
pub node_id: Cell<NodeId>,
pub span: Span,
}
```
## Why?
The current AST could represent shapes such as:
```
foo.bar
```
By constructing the node manually. However this doesn't make sense, and is invalid.
## How to migrate?
1. Replace any `Expression::MetaProperty` matches/checks with the dedicated `Expression::ImportMeta` and `Expression::NewTarget` varients
2. Replace any `new_meta_property` construction with the new `new_import_meta` or `new_new_target` APIs.
3. Inspecting the `meta`/`property` identifier names is no longer required.
Mechanical migration of all 14 stable sort calls in `oxc_react_compiler` to their unstable variants: 8 `.sort_by()` to `.sort_unstable_by()` and 6 `.sort_by_key()` to `.sort_unstable_by_key()`.
Unstable sort avoids the stable sort's temporary allocation and is the repo-wide convention where stability isn't required. Sorted outputs were checked against the full fixture snapshot corpus (`cargo test -p oxc_react_compiler`, insta-based over the upstream fixtures) and are unchanged, so no call site observably relies on stable-sort tie ordering. All sites were migrated; none needed to be kept stable.
Binary size is unchanged (measured at opt-level 3 with fat LTO); this is a code hygiene/consistency change, not a size optimization.
Note for future re-syncs with upstream `react_compiler_oxc`: this is a systematic porting rule — JS `Array.prototype.sort` (stable) becomes Rust `sort_unstable*` unless tie order is observable in output, in which case the stable variant is kept with a comment. Re-apply the rule when re-vendoring.
Authored with Claude Code; reviewed by a human before merge.
Every distinct `Visit`/`VisitMut` implementor monomorphizes the entire generated AST walk; `react_compiler/entrypoint/program.rs` had three such types (`OxcReplaceFnVisitor`, `OxcReplaceWithGatedVisitor`, and the local `Finder`), so the walk was instantiated three times. They are now a single `OxcVisitor` selected by an `OxcVisitMode` enum (`ReplaceFns` / `ReplaceWithGated` / `FindOriginalFn`), so the walk is instantiated once.
Each overridden method dispatches on the mode and behaves exactly like the default walk in the modes that do not use it, preserving traversal order and the early-exit semantics (`remaining == 0`, `done`, `found.is_some()`). `Finder` moves from the read-only `VisitJs` walk onto the shared `VisitMut` walk — a superset traversal; it matches a unique `scope_id`, and function scopes cannot occur in the pruned type grammar (per #24532), so the lookup result is unchanged. The Babel-mirroring notes (`ReplaceFnVisitor` / `ReplaceWithGatedVisitor`) stay on the corresponding modes, and the per-node replacement logic is ported verbatim into free helpers (`ox_replace_function`, `ox_replace_arrow`, `ox_build_gated_const_decl`).
Measured on this crate's release example binary (`--example react_compiler`, workspace release profile with fat LTO): machine code (`__text`) shrinks by 24,112 bytes (~24 KB), file size by 16,592 bytes (~17 KB). The three walk instantiations measured ~34K/~16K/~14K in the symbol-level size analysis of the release napi binary that motivated this change.
Behavior is verified unchanged on the full fixture snapshot corpus (1,736 snapshots, including the `gating__*` fixtures that exercise the gated-replacement and original-function-lookup paths), and the release example binary produces byte-identical output before and after across the gating and general fixtures.
Authored with Claude Code; reviewed by a human before merge.
Each distinct `FxHashMap<K, V>` monomorphizes its own copy of hashbrown; maps keyed by the dense u32 newtype ids become direct-indexed `IndexVec<Id, Option<V>>`s, removing those instantiations and the per-lookup hashing.
### Migration criteria
A map was migrated only when all of these hold:
1. The key is a dense id (`IdentifierId`, `DeclarationId`, `BlockId`) with a sound bound available at construction time: `env.identifiers.len()` (declaration ids share the identifier id index space — `DeclarationId::from_usize(id.index())` at allocation), `env.next_block_id_counter` (+1 for the synthetic exit node in the dominator), or the max block id present in the function body.
2. The map is only accessed by key (`get`/`insert`/`contains_key`); it is never iterated in a way that can reach the output. The one iterated map (the optional-chain temporaries merge) writes into another map with distinct keys, so iteration order is not observable.
3. `Option<V>` slots preserve the exact presence semantics, including "insert twice keeps latest" and `entry().or_insert()` first-wins. Where `map.len()` carried meaning, it was replaced with an explicit counter (`Rewrites::len` for the phi-elimination fixpoint, `BlockIdMappings::next` for sequential id assignment).
4. The map is allocated once per pass, not per inner loop iteration.
### Migrated maps
| File | Map |
| --- | --- |
| `react_compiler_hir/dominator.rs` | `PostDominator::nodes`, `Graph::node_index`, `raw_nodes`, `doms` (kills `FxHashMap<BlockId, BlockId>`, `<BlockId, Node>`, `<BlockId, usize>`) |
| `react_compiler_reactive_scopes/stabilize_block_ids.rs` | block id `mappings` (new `BlockIdMappings` with explicit `next` counter) |
| `react_compiler_optimization/prune_maybe_throws.rs` | `terminal_mapping` |
| `react_compiler_ssa/eliminate_redundant_phi.rs` | `rewrites` (new `Rewrites` with explicit entry count for the fixpoint check) |
| `react_compiler_validation/validate_hooks_usage.rs` | `value_kinds` (`Option<Kind>` is 1 byte) |
| `react_compiler_reactive_scopes/promote_used_temporaries.rs` | `State::pruned`, `inter_state` |
| `react_compiler_reactive_scopes/merge_reactive_scopes_that_invalidate_together.rs` | `last_usage`, `temporaries` |
| `react_compiler_inference/propagate_scope_dependencies_hir.rs` | `temporaries` + `temporaries_read_in_optional` (the crate's most-instantiated map type, ×11), the declaring-scope map in `find_temporaries_used_outside_declaring_scope`, and `DependencyCollectionContext::{declarations, reassignments}` |
This eliminates 11 distinct id-keyed hashbrown instantiations from the crate.
### Deliberately skipped
- `outline_jsx.rs` `globals` and `infer_types.rs` / `name_anonymous_functions.rs` `names`: these create a fresh map per nested function, so a dense vec sized by the env-wide identifier count would allocate O(identifiers) for a handful of entries on every nested function.
- `outline_jsx.rs` `rewrite_instr` and other `EvaluationOrder`-keyed maps: no clean bound at the construction site and keys are sparse.
- `get_assumed_invoked_functions` temporaries in `propagate_scope_dependencies_hir.rs`: allocated per inner function (same per-iteration concern).
- `FxIndexMap` uses: insertion order is semantic there.
- Maps whose iteration feeds diagnostics or output ordering (e.g. `validate_no_derived_computations_in_effects.rs`).
### Measured effect
Release example binary (`examples/react_compiler`, macOS arm64): `__text` (machine code) shrinks 2,767,800 → 2,756,116 bytes (**−11,684 bytes, −0.42%**). The on-disk file size only changes by 16 bytes because the `__TEXT` segment is page-aligned; the machine-code section is the meaningful metric. The example links the full parser/codegen stack, so the relative saving within this crate's own code is larger. The main win is runtime: every migrated lookup is now a bounds-checked array index instead of a hash + probe.
The upstream fixture corpus (1,736 snapshots) is byte-for-byte unchanged, and `cargo clippy -p oxc_react_compiler --all-targets` is clean.
### Note for upstream re-syncs
This intentionally deviates from the upstream TypeScript structure (`Map`-based sidemaps in `PropagateScopeDependenciesHIR.ts`, `Dominator.ts`, etc.). When re-syncing this crate with upstream via `react_compiler_oxc`, please preserve the `IndexVec` representation for these maps.
Authored with Claude Code; reviewed by a human before merge.
The builtin shape/global registries are built exactly once per process inside the `BASE` `LazyLock`; this PR replaces their straight-line construction code with const descriptor tables interpreted by small loops (and marks the builders `#[inline(never)]`/`#[cold]`), removing cold init code that the optimizer was aggressively inlining and unrolling.
**What changed**
- New const tables — `BUILTIN_SHAPE_DEFS`, `STATE_SHAPE_DEFS`, `REACT_HOOK_DEFS`, `TYPED_GLOBAL_OBJECTS` plus `PRIMITIVE_GLOBAL_FNS`/`COLLECTION_CTORS` — encode the repetitive `addFunction`/`addObject`/`addHook` registrations (method signatures, effects, aliasing configs, plain properties). Two small interpreters (`add_method`, `add_object_from_def`) replay them into the registries.
- Descriptors are kept compact so the code-for-data trade stays profitable: a 24-byte `TypeDef` mirror of the `Type` literals and `Option<&'static AliasingSignatureConfig>` instead of a 96-byte inline `Option`.
- Genuinely irregular entries stay imperative rather than being forced into tables: `use`, `useEffectEvent` (function-typed return), the React namespace and `_jsx` functions, `Infinity`/`NaN`, and the `globalThis`/`global` wiring. `build_default_globals`, `UNTYPED_GLOBALS`, `get_reanimated_module_type`, and the `install_type_config` path are untouched apart from the new attributes.
- All one-time builders (`build_builtin_shapes`, `build_default_globals`, `build_react_apis`, `build_typed_globals`, and their helpers) are `#[cold]` + `#[inline(never)]`, so they no longer get expanded into the `LazyLock` initializer and are optimized for size.
**Size impact (measured)**
Comparing `cargo build -p oxc_react_compiler --release --example react_compiler` (fat LTO, arm64 macOS) against the parent commit:
- `__text`: 2,767,800 → 2,739,076 bytes (−28.7 KB of machine code)
- const data (`__DATA_CONST.__const` + `__TEXT.__const`): +9.5 KB (the descriptor tables)
- net content: −19.2 KB; binary file size 3,098,512 → 3,082,352 bytes (−16.2 KB, page-granular)
For reference, a symbol-level size analysis of the release napi binary had attributed ~38 KB to this file's builders (~20 KB `build_builtin_shapes`, ~9 KB `build_typed_globals`).
**Resulting registries are unchanged**
The full upstream fixture snapshot corpus passes with zero snapshot changes, and a structural dump of both base registries (every global name resolved to its full type/signature, every named shape, and both maps' key iteration orders) is byte-identical before and after. The only internal difference is that a few anonymous `<generated_N>` shape ids are minted in upstream property order rather than the port's hoisted-`let` order; those ids are opaque registry keys, and every name-to-signature mapping is identical.
The data tables intentionally mirror the upstream Globals.ts / ObjectShape.ts entry order (and keep the upstream comments) so future re-syncs can diff them directly against the TS source.
Authored with Claude Code; reviewed by a human before merge.
Migrate the transform-pipeline `Visit` implementors to `VisitJs` (#24499): `oxc_transformer` (3), `oxc_minifier` (4), `oxc_react_compiler` (1), plus the parser regex example and the coverage driver. Part of #24317; linter half in #24530. Behavior is unchanged.
Together with #24530: `oxlint` −129.4 KiB (−0.94%) stripped, `oxc_transform` napi −16.2 KiB (−0.46%).
## Why each visitor can switch
A visitor is safe on `VisitJs` when its hooks cannot observe anything inside the pruned type grammar — either the hooked node kinds are value-space-only, or the AST it walks cannot contain type nodes at that pipeline stage.
| Visitor | File | Why equivalent |
|---|---|---|
| `PrivateInExpressionDetector` | `crates/oxc_transformer/src/decorator/legacy/mod.rs` | Hooks `visit_private_in_expression` (+`visit_decorators`); `#x in y` is runtime-only and cannot occur in type grammar, even inside decorator expressions carrying casts. |
| `BindingMover` | `crates/oxc_transformer/src/es2017/async_to_generator.rs` | Its formal-parameter overrides visit only pattern + initializer (`BindingPattern` carries no type field — annotations are sibling fields it never touches); runs in the exit phase after TS strip. |
| `UsedInJSXBindingsCollector` | `crates/oxc_transformer/src/jsx/refresh.rs` | Runs on un-stripped TSX, but its hooks (JSX-shaped calls, `visit_jsx_opening_element`, declarations/imports) cannot fire inside type grammar; its hand-written `visit_ts_type_annotation` skip-stub is deleted — `VisitJs` prunes that natively. |
| `Finder` | `crates/oxc_react_compiler/src/react_compiler/entrypoint/program.rs` | Matches functions by `scope_id`; real JS function scopes never live in pure type grammar, and `VisitJs` still walks the places one could hide (enum initializers, namespace bodies, cast inner expressions). |
| `KeepVar` | `crates/oxc_minifier/src/keep_var.rs` | Hooks `visit_statement`/`visit_variable_declaration`; statements and `var` declarations don't exist in type space, and namespace/enum bodies (which can contain them) are still walked by `VisitJs`. |
| `DirectEvalFlagCheck` | `crates/oxc_minifier/src/peephole/mod.rs` | Debug-only; hooks `visit_call_expression` only — call expressions cannot occur in valid type grammar. |
| `LiveDirectEvalCollector` | `crates/oxc_minifier/src/peephole/mod.rs` | Same single call-expression hook; type-space-immune regardless of input. |
| `TestVisitor` | `crates/oxc_minifier/tests/ecmascript/prop_name.rs` | Test helper parsing `SourceType::default()` (plain JS); single non-recursing `visit_object_expression` hook. |
| `RegularExpressionVisitor` | `crates/oxc_parser/examples/regular_expression.rs` | Finds `RegExpLiteral` / `new RegExp(...)`; neither node kind can occur in type grammar. |
| `CheckASTNodes` | `tasks/coverage/src/driver.rs` | Its only active override is `visit_reg_exp_literal` (regex print idempotency); `RegExpLiteral` cannot occur in type space. (The commented-out `visit_span` full-AST check would need `Visit` if ever revived.) |
Left on `Visit`: the minifier's incremental-scoping bookkeeping (`DropDiff`, `OverPruneCheck`, `LiveRefCollector`) — dropping `let x: Foo = …` must unmark the type-space `Foo` reference, which the `peephole::remove_unused_declaration` tests verify (they fail under `VisitJs`; confirmed and reverted during this work). `ChildScopeCollector` also stays: TS type nodes (mapped/conditional/function types) carry real `scope_id`s it must collect.
---
🤖 This PR was written with the assistance of Claude Code (Claude Fable 5).