leaysgur 788af22210 perf(formatter): keep AstNodes on the stack, and only chain links in the arena (#27963)
Stop allocating every `AstNode` wrapper in the arena, following the approach of #21931.

### What changes

- `AstNode<'parent, 'a, T> { inner, parent, following_span_start }` is `Copy` and has no `&Allocator`.
  - `'parent` is the lifetime of the parent `AstNode` that `parent` refers to.
  - Generated child accessors return an owned `AstNode` that borrows its parent.
  - The parent's `write` keeps its children on its stack frame.
  - The unsafe lifetime extension (`transmute_self`) is gone.
- The arena-backed `as_ast_nodes()` is replaced by a generated owned `AstNodeKind` (`as_kind()`).
  - `NeedsParentheses for AstNode<Expression>` is generated as one direct match.

### Sites that are not a plain recursive descent

A loop that walks down a chain cannot keep each node on the stack, as the next node refers to it as its parent.
#21931 hit this at about 10 sites.

- Chain walks (binary-like flattening, member chains, arrow chains, assignment-like, nested namespaces) keep their loops.
  - Only the links that the walk continues past go into the arena, through `AstNode::in_arena` / `AstNodeKind::in_arena`.
  - The rule is documented once on `AstNode`.
- Checks that never ask for a parent read the AST directly, with no wrapper.
  - Left sides: one table, `LeftSide::of`, which `left_span_of`, `leftmost` and the `AstNode` walk `left_of` derive from.
  - The shape checks of assignment-like chains.
- The binary-like split and the flattened logical expression are loops too, so long chains need no more stack than on `main`.

### Compared to #21931 (Jim's PoC)

- Enum nodes resolve through the generated `as_kind()`, so call sites keep the `match` shape of `as_ast_nodes()`.
  - PoC rewrote each of them by hand, as a `match` on `inner` with `with_inner`.
- The arena is used only through `in_arena`, and only for the links a chain walk continues past.
  - PoC put an `f.allocator().alloc(..)` at each site that broke, kept the recursion, and allocated at the entry of a flatten.
- Checks that never ask for a parent use no wrapper.
  - PoC allocated a wrapper at each step of e.g. `get_innermost_expression`.
- `ImportExpression::arguments` takes the allocator, instead of leaving `to_arguments` as a TODO.

### Alternatives tried

Both follow directions suggested in #21931.

- #27958: no arena at all
  - As suggested "replace loop with recursive function call".
  - The chain walks recurse, and a walk that needs the whole chain keeps it in a `StackList` on the stack, consumed through continuation callbacks.
  - The recursion uses stack in proportion to the chain length: with 2 MiB workers, `return a.b().b()…` overflows at 4000 calls, where `main` formats 32000.
  - So it needs a workaround, e.g. 8 MiB `oxfmt` workers, which still leaves the napi API and the LSP on default stacks.
  - About 0.5 points faster than this PR on CodSpeed, but more code.
- #27959: no parent pointer
  - As suggested "Alternative approach".
  - Ancestors on a `NonEmptyStack` like `Traverse`, and `following_span_start` computed on demand.
  - 2-4% more instructions than #27958, quadratic on long chains, and the ancestry is only checked in debug builds.
2026-10-21 00:52:40 +00:00
…



Oxc logo


MIT licensed Build Status Code Coverage CodSpeed Badge

Discord chat Playground Website

⚓ Oxc

/oʊ ɛks siː/

The Oxidation Compiler is a collection of high-performance tools for JavaScript and TypeScript written in Rust.

Oxc is part of VoidZero's vision for a unified, high-performance toolchain for JavaScript. It powers Rolldown (Vite's bundler) and enables the next generation of ultra-fast development tools that work seamlessly together.

For more information, check out our website at oxc.rs.

* Oxidation is the chemical process that creates rust

🙋 Who's using Oxc?

Rolldown and Nuxt use Oxc for parsing. Rolldown also uses Oxc for transformation and minification. Nova, swc-node, and knip use oxc_resolver for module resolution. Preact, Shopify, ByteDance, and Shopee use oxlint for linting.

See more projects using Oxc →

🔧 Lint or Format a Codebase

  • Lint: Oxlint — npx oxlint@latest
  • Format: Oxfmt — npx oxfmt@latest

🧰 Build Tooling on Top of Oxc

  • Parse JavaScript and TypeScript: Parser
  • Transform TypeScript, JSX, and modern JavaScript: Transformer
  • Minify JavaScript for production builds: Minifier
  • Resolve modules for JavaScript and TypeScript: Resolver

✍️ Contribute

Check out some of the good first issues or ask us on Discord.

See CONTRIBUTING.md for guidance, or read the complete contributing guide on our website →

If you are unable to contribute by code, you can still participate by:

📚 Other Resources

Sponsored By

Our sponsors

📖 License

Oxc is free and open-source software licensed under the MIT License.

Thank you to namespace.so for powering our CI/CD pipelines with fast, free macOS and Linux runners.

Oxc ports or copies code from other open source projects, their licenses are listed in Third-party library licenses.

Languages
Rust 80.6%
JavaScript 12.3%
TypeScript 7%