perf(parser): store Modifiers on stack#20742
Merged
graphite-app[bot] merged 1 commit intomainfrom Mar 26, 2026
Merged
Conversation
This was referenced Mar 25, 2026
This was referenced Mar 25, 2026
Member
Author
34034be to
eb566f2
Compare
Merging this PR will not alter performance
Comparing Footnotes
|
3e043d7 to
c37d9b5
Compare
eb566f2 to
2ac2e86
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Refactors Modifiers (temporary parser-only state for TS/JS modifier keywords) to store modifier presence and keyword start offsets inline on the stack, avoiding arena allocations and duplicate storage while reconstructing Spans on demand.
Changes:
- Replaces
Modifiers<'a>’sOption<Vec<'a, Modifier>>storage with a fixed-size[MaybeUninit<u32>; 15]offsets array plus aModifierKindsbitfield. - Updates parser call sites to use the new
ModifiersAPI (no lifetime,new_single, updatediter/getsemantics). - Adjusts invalid-modifier reporting to preserve source-order diagnostics while iterating modifiers in discriminant order.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tasks/track_memory_allocations/allocs_parser.snap | Updates allocation snapshot numbers consistent with reduced arena usage. |
| crates/oxc_parser/src/modifiers.rs | Implements new stack-based Modifiers representation and updates modifier parsing/validation logic. |
| crates/oxc_parser/src/js/statement.rs | Switches const enum modifier construction to Modifiers::new_single. |
| crates/oxc_parser/src/js/module.rs | Switches export default abstract class modifier construction to Modifiers::new_single. |
| crates/oxc_parser/src/js/function.rs | Updates function parsing signatures to accept &Modifiers (no lifetime). |
| crates/oxc_parser/src/js/object.rs | Updates method-getter/setter parsing signature to accept &Modifiers (no lifetime). |
| crates/oxc_parser/src/js/class.rs | Updates class parsing signatures/usages to accept &Modifiers (no lifetime). |
| crates/oxc_parser/src/ts/statement.rs | Updates TS declaration parsing signatures to accept &Modifiers (no lifetime). |
| crates/oxc_parser/src/ts/types.rs | Updates TS signature parsing signatures to accept &Modifiers (no lifetime). |
c37d9b5 to
23bf3b1
Compare
2ac2e86 to
4fc398f
Compare
23bf3b1 to
126134b
Compare
4fc398f to
24c0f9f
Compare
24c0f9f to
4fc398f
Compare
This was referenced Mar 25, 2026
graphite-app bot
pushed a commit
that referenced
this pull request
Mar 26, 2026
The types for modifiers were confusing. We had `ModifierFlags` type defined with `bitflags!` macro, and `ModifierKind` enum which represents the different possible flags. The two types weren't statically related, and consuming code needed to manually relate the two e.g. pairing usage of `ModifierFlags::DECLARE` with `ModifierKind::Declare`. This is unnecessarily complicated, and error-prone. This PR turns `ModifierFlags` into a wrapper around a `u16` and adds methods to construct, query, and set bits in the set by passing a `ModifierKind` or a set of `ModifierKind`s. Consuming code now only has one type to deal with which is the single source of truth about what modifiers are available - `ModifierKind`. Code is more verbose in some places, but personally I think this more explicit style is clearer. `ModifierFlags::new` is a const function, so even sizeable calls e.g. `ModifierFlags::new([ModifierKind::Public, ModifierKind::Private, ModifierKind::Protected])` is boiled down to a static `u16` at compile time. It compiles down to the same as the `bitflags!` version. This change also enables #20742, which improves the storage of `Modifiers`.
camc314
approved these changes
Mar 26, 2026
Contributor
Merge activity
|
126134b to
4689d0e
Compare
graphite-app bot
pushed a commit
that referenced
this pull request
Mar 26, 2026
`Modifiers` represents a set of 0 or more modifier keywords (`public`, `private`, `static` etc), up to a maximum of 15 possible modifiers.
Previously it was stored as:
```rs
pub struct Modifiers<'a> {
/// Set of `Modifier`s, each containing `Span` of modifier keyword + modifier kind.
modifiers: Option<Vec<'a, Modifier>>,
/// Bitfield of which modifiers are present.
kinds: ModifierKinds,
}
```
This is inefficient in a few ways:
1. `Modifiers` is not part of the AST, only temporary data used during parsing. Using a `Vec<'a, Modifier>` results in this temporary data taking up space in the AST arena, which hangs around unused for the whole life of the AST.
2. `modifiers` `Vec` can contains duplicates. This is pointless because using the same modifier twice is always a syntax error, and this is already handled by `check_modifier`.
3. `Modifier` contains the `Span` of the modifier keyword. The length of modifier keywords is statically known from their kind (e.g. `public` is always 6 bytes). Storing just the start offsets of keywords is sufficient.
This PR switches to a more efficient representation:
```rs
pub struct Modifiers {
/// Start offset for each modifier, indexed by `ModifierKind` discriminant.
offsets: [MaybeUninit<u32>; 15],
/// Bitfield of which modifiers are present.
kinds: ModifierKinds,
}
```
This type contains all its data inline, and can be stored on the stack (64 bytes) - versus previous 32 bytes on stack + 16 bytes for each modifier in arena. Despite the larger footprint on stack, the use of `MaybeUninit` means that constructing an empty `Modifiers` requires less memory ops:
* Now: 2-byte write to set `kinds = ModifierKinds::none()`.
* Previously: 2-byte write for `kinds` + 8-byte write to set `modifiers = None`.
Offsets are only written to `offsets` when a modifier is added. The `kinds` bitfield indicates which entries in `offsets` are present and initialized. All methods check `kinds` before accessing `offsets`, to avoid reading uninitialized memory (which would be UB).
Small positive impact on some benchmarks (+0.2%). To be honest, it's more of a "the principle of the matter" thing - temporary data should not be stored in the arena. This stack was also an experiment in getting Claude to create a stack of PRs unattended from an initial step-by-step plan.
4fc398f to
bc58fe2
Compare
`Modifiers` represents a set of 0 or more modifier keywords (`public`, `private`, `static` etc), up to a maximum of 15 possible modifiers.
Previously it was stored as:
```rs
pub struct Modifiers<'a> {
/// Set of `Modifier`s, each containing `Span` of modifier keyword + modifier kind.
modifiers: Option<Vec<'a, Modifier>>,
/// Bitfield of which modifiers are present.
kinds: ModifierKinds,
}
```
This is inefficient in a few ways:
1. `Modifiers` is not part of the AST, only temporary data used during parsing. Using a `Vec<'a, Modifier>` results in this temporary data taking up space in the AST arena, which hangs around unused for the whole life of the AST.
2. `modifiers` `Vec` can contains duplicates. This is pointless because using the same modifier twice is always a syntax error, and this is already handled by `check_modifier`.
3. `Modifier` contains the `Span` of the modifier keyword. The length of modifier keywords is statically known from their kind (e.g. `public` is always 6 bytes). Storing just the start offsets of keywords is sufficient.
This PR switches to a more efficient representation:
```rs
pub struct Modifiers {
/// Start offset for each modifier, indexed by `ModifierKind` discriminant.
offsets: [MaybeUninit<u32>; 15],
/// Bitfield of which modifiers are present.
kinds: ModifierKinds,
}
```
This type contains all its data inline, and can be stored on the stack (64 bytes) - versus previous 32 bytes on stack + 16 bytes for each modifier in arena. Despite the larger footprint on stack, the use of `MaybeUninit` means that constructing an empty `Modifiers` requires less memory ops:
* Now: 2-byte write to set `kinds = ModifierKinds::none()`.
* Previously: 2-byte write for `kinds` + 8-byte write to set `modifiers = None`.
Offsets are only written to `offsets` when a modifier is added. The `kinds` bitfield indicates which entries in `offsets` are present and initialized. All methods check `kinds` before accessing `offsets`, to avoid reading uninitialized memory (which would be UB).
Small positive impact on some benchmarks (+0.2%). To be honest, it's more of a "the principle of the matter" thing - temporary data should not be stored in the arena. This stack was also an experiment in getting Claude to create a stack of PRs unattended from an initial step-by-step plan.
4689d0e to
511d5e5
Compare
bc58fe2 to
5f9bee5
Compare
Base automatically changed from
om/03-23-perf_parser_add_modifiers_get_method
to
main
March 26, 2026 13:49
This was referenced Mar 26, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Modifiersrepresents a set of 0 or more modifier keywords (public,private,staticetc), up to a maximum of 15 possible modifiers.Previously it was stored as:
This is inefficient in a few ways:
Modifiersis not part of the AST, only temporary data used during parsing. Using aVec<'a, Modifier>results in this temporary data taking up space in the AST arena, which hangs around unused for the whole life of the AST.modifiersVeccan contains duplicates. This is pointless because using the same modifier twice is always a syntax error, and this is already handled bycheck_modifier.Modifiercontains theSpanof the modifier keyword. The length of modifier keywords is statically known from their kind (e.g.publicis always 6 bytes). Storing just the start offsets of keywords is sufficient.This PR switches to a more efficient representation:
This type contains all its data inline, and can be stored on the stack (64 bytes) - versus previous 32 bytes on stack + 16 bytes for each modifier in arena. Despite the larger footprint on stack, the use of
MaybeUninitmeans that constructing an emptyModifiersrequires less memory ops:kinds = ModifierKinds::none().kinds+ 8-byte write to setmodifiers = None.Offsets are only written to
offsetswhen a modifier is added. Thekindsbitfield indicates which entries inoffsetsare present and initialized. All methods checkkindsbefore accessingoffsets, to avoid reading uninitialized memory (which would be UB).Small positive impact on some benchmarks (+0.2%). To be honest, it's more of a "the principle of the matter" thing - temporary data should not be stored in the arena. This stack was also an experiment in getting Claude to create a stack of PRs unattended from an initial step-by-step plan.