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.
Prevents the internal `oxc_react_compiler` and
`oxc_transform_react_napi` Rust crates from being published while
including the `oxc-transform-react` npm package in workspace releases.
Validated with `taplo lint`, Cargo metadata, and `just ready`.
AI assistance was used for this change.
## 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.
Adds explicit includes for `oxc_react_compiler` to omit 2.2MB of files
(49% of total) under `/tests` and `/fixtures` from the published
package. I included the examples directory, but that can also be
excluded if desired
[The full list of removed files are included in this text
file](https://github.com/user-attachments/files/30380492/oxc_react_removed.txt).
Today I learned there is a size limit in GitHub PR descriptions
For the record, I checked all other published crates in the workspace,
and none of them included any extraneous files 😃
## 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.
Closes the migration tracked in #23043.
The new builder API (methods on the AST types themselves) has been in place for a while, all Oxc crates are migrated, and every crate which uses `AstBuilder` already enables `disable_old_builder` in its tests. The module docs said "after a few weeks, we will remove `AstBuilder`'s own methods entirely" — this does that.
## What's removed
- `crates/oxc_ast/src/generated/ast_builder.rs` and `builder/methods.rs` — `AstBuilder`'s own node and primitive constructors.
- `AstBuilderGenerator`, which produced the former.
- The `disable_old_builder` Cargo feature, plus the dev-dependency hacks it required across 33 manifests. Eight crates depended on `oxc_ast` *purely* to turn the feature on; those deps and their `cargo-shear` ignore entries are gone too.
## One thing worth reviewing
`AstBuilderGenerator` owned the `#[builder(default)]` / `#[builder(skip)]` attr registration that `BuilderMethodsGenerator` only *reads* — its doc comment said as much. Deleting it naively would have silently dropped `skip`/`default` handling for the 12 AST types using those attrs, so `attrs()` and `parse_attr()` move to `BuilderMethodsGenerator`.
The evidence this was faithful: after regenerating, `builder_methods.rs` and every other generated file are **byte-identical**. The only generated change in the diff is the deletion.
## Breaking changes
- `AstBuilder`'s own methods are gone. Use the AST type methods: `Statement::new_expression_statement(span, expr, &builder)` instead of `builder.statement_expression(span, expr)`.
- `AstBuilder` is no longer `Copy`/`Clone`, and its `allocator` field is private — use the `allocator` method from `GetAllocator`.
- `AstBuilder` and `NONE` are no longer re-exported from the crate root; import them from `oxc_ast::builder`. (The `disable_old_builder` feature already gated these off, so nothing in-tree used the root path.)
- The `disable_old_builder` feature no longer exists — remove it from any manifest that sets it.
A migration note covering all of the above is in `crates/oxc_ast/src/builder/mod.rs`.
---
Implemented with AI assistance (Claude Code); design, review, and verification driven by the author per the repo AI-usage policy.
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`