Skip to content

fix(parser): prevent multiple Parser instances in binary#22120

Merged
graphite-app[bot] merged 1 commit into
mainfrom
om/05-04-fix_parser_prevent_multiple_parser_instances_in_binary
May 5, 2026
Merged

fix(parser): prevent multiple Parser instances in binary#22120
graphite-app[bot] merged 1 commit into
mainfrom
om/05-04-fix_parser_prevent_multiple_parser_instances_in_binary

Conversation

@overlookmotel

@overlookmotel overlookmotel commented May 4, 2026

Copy link
Copy Markdown
Member

Fixes rolldown/rolldown#9166

The problem

#19637 made Parser generic over ParserConfig. Unfortunately this had a negative impact on Rolldown's binary size - the parser ended up taking ~1.4 MiB of .text in Rolldown's .dylib, up from ~322 KiB before.

Cause is not multiple ParserConfigs in use - Rolldown, and all crate it depends on, only ever uses NoTokensParserConfig.

Cause is that Rust monomorphizes generic functions per consuming crate, so each downstream crate that calls Parser::new(...).parse() ended up with its own private copy of the parser. None of those copies could be deduped by COMDAT (different mangled names) or by fat LTO (slightly different inlining contexts produced bit-distinct bodies). Rolldown ended up with 7 copies.

This PR

This PR solves the problem without changing the external API.

Parser<ParserConfig> API remains as it was, but Parser::parse now routes through non-generic helpers - one per known config - so the parser body lives in oxc_parser's rlib only once and gets shared by every consumer.

The mechanism: parse and parse_expression upcast &self.config to &dyn Any, then dispatch with Any::is::<NoTokensParserConfig>(), Any::is::<TokensParserConfig>(), and Any::downcast_ref::<RuntimeParserConfig>(). Once monomorphized, the casts and branches are all removed by compiler, leaving just a single direct call to one of the non-generic helper functions - no wrapper symbol survives in the binary.

The helpers are #[inline(never)] to stop fat LTO re-inlining the parser body across the rlib boundary, which would defeat the whole thing.

User-defined ParserConfig impls fall through to a generic body in the else branch. Those still monomorphize per consuming crate, but that's the same cost the parser had before, just isolated to the rare custom-config case.

Verification

Have tested that this does actually solve the problem in Rolldown this time:

  • Before: 1.4 MiB oxc_parser .text, 7 copies of parse_statement_list_item, dylib 24.6 MB
  • After: 350 KiB oxc_parser .text, 1 copy, dylib 22.9 MB

Matches the pre-#19637 baseline within ~30 KiB.

Copy link
Copy Markdown
Member Author

How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • 0-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent changes, fast-track this PR to the front of the merge queue

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@codspeed-hq

codspeed-hq Bot commented May 4, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 48 untouched benchmarks
⏩ 3 skipped benchmarks1


Comparing om/05-04-fix_parser_prevent_multiple_parser_instances_in_binary (7f1499e) with main (00f9a9d)2

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (831e8cb) during the generation of this report, so 00f9a9d was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@overlookmotel overlookmotel changed the title fix(parser): prevent multiple Parser instances in binary fix(parser)!: prevent multiple Parser instances in binary May 4, 2026
@overlookmotel overlookmotel changed the title fix(parser)!: prevent multiple Parser instances in binary fix(parser): prevent multiple Parser instances in binary May 4, 2026
@overlookmotel
overlookmotel marked this pull request as ready for review May 4, 2026 23:25
@overlookmotel
overlookmotel requested review from camc314 and Copilot May 4, 2026 23:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses downstream binary size bloat caused by per-crate monomorphization of Parser<C>::parse/parse_expression after Parser became generic over ParserConfig. It does so by routing parsing through non-generic, #[inline(never)] helper functions for the known configs so the parser body is emitted once in oxc_parser and shared by consumers.

Changes:

  • Add Any-based dispatch in Parser<C>::parse and Parser<C>::parse_expression to select one of three non-generic parse helpers (NoTokensParserConfig, TokensParserConfig, RuntimeParserConfig).
  • Introduce non-generic, #[inline(never)] helper functions to keep the heavy parser code from being duplicated across downstream crates (even under fat LTO).
  • Require ParserConfig: 'static to support Any-based dispatch.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
crates/oxc_parser/src/lib.rs Adds Any-based dispatch in entry points and non-generic, #[inline(never)] helpers to reduce downstream code duplication.
crates/oxc_parser/src/config.rs Updates ParserConfig trait bound to Default + 'static and documents why.

Comment thread crates/oxc_parser/src/config.rs

@camc314 camc314 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i hate this haha.

nice work!

@camc314 camc314 added A-parser Area - Parser 0-merge Merge with Graphite Merge Queue labels May 5, 2026

camc314 commented May 5, 2026

Copy link
Copy Markdown
Contributor

Merge activity

Fixes rolldown/rolldown#9166

### The problem

#19637 made `Parser` generic over `ParserConfig`. Unfortunately this had a negative impact on Rolldown's binary size - the parser ended up taking ~1.4 MiB of `.text` in Rolldown's `.dylib`, up from ~322 KiB before.

Cause is not multiple `ParserConfig`s in use - Rolldown, and all crate it depends on, only ever uses `NoTokensParserConfig`.

Cause is that Rust monomorphizes generic functions per consuming crate, so each downstream crate that calls `Parser::new(...).parse()` ended up with its own private copy of the parser. None of those copies could be deduped by COMDAT (different mangled names) or by fat LTO (slightly different inlining contexts produced bit-distinct bodies). Rolldown ended up with 7 copies.

### This PR

This PR solves the problem without changing the external API.

`Parser<ParserConfig>` API remains as it was, but `Parser::parse` now routes through non-generic helpers - one per known config - so the parser body lives in `oxc_parser`'s rlib only once and gets shared by every consumer.

The mechanism: `parse` and `parse_expression` upcast `&self.config` to `&dyn Any`, then dispatch with `Any::is::<NoTokensParserConfig>()`, `Any::is::<TokensParserConfig>()`, and `Any::downcast_ref::<RuntimeParserConfig>()`. Once monomorphized, the casts and branches are all removed by compiler, leaving just a single direct call to one of the non-generic helper functions - no wrapper symbol survives in the binary.

The helpers are `#[inline(never)]` to stop fat LTO re-inlining the parser body across the rlib boundary, which would defeat the whole thing.

User-defined `ParserConfig` impls fall through to a generic body in the `else` branch. Those still monomorphize per consuming crate, but that's the same cost the parser had before, just isolated to the rare custom-config case.

### Verification

Have tested that this _does_ actually solve the problem in Rolldown this time:

- Before: 1.4 MiB `oxc_parser` `.text`, 7 copies of `parse_statement_list_item`, dylib 24.6 MB
- After: 350 KiB `oxc_parser` `.text`, 1 copy, dylib 22.9 MB

Matches the pre-#19637 baseline within ~30 KiB.
@graphite-app
graphite-app Bot force-pushed the om/05-04-fix_parser_prevent_multiple_parser_instances_in_binary branch from 7f1499e to 81e834c Compare May 5, 2026 08:20
@graphite-app
graphite-app Bot merged commit 81e834c into main May 5, 2026
28 checks passed
@graphite-app graphite-app Bot removed the 0-merge Merge with Graphite Merge Queue label May 5, 2026
@graphite-app
graphite-app Bot deleted the om/05-04-fix_parser_prevent_multiple_parser_instances_in_binary branch May 5, 2026 08:25
@overlookmotel

Copy link
Copy Markdown
Member Author

i hate this haha.

Yeah, it feels wrong, and not scalable at all. Hopefully we can find a better way. But I thought this was the least disruptive solution for now.

@camc314

camc314 commented May 7, 2026

Copy link
Copy Markdown
Contributor

i hate this haha.

Yeah, it feels wrong, and not scalable at all. Hopefully we can find a better way. But I thought this was the least disruptive solution for now.

maybe worth making an issue in the rust repo, or perhaps reaching out on their discord (or i think they maybe use tulip) to see if this can be addressed in a better way on their end. (they may well just say this is expected behaviour, but I struggled to find docs for this). It would be nice to get it all written down somewhere for anyone else to reference in future encountering the same problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-parser Area - Parser

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Rolldown Binary Size Increased

3 participants