Root cause
ParserImpl::try_parse in crates/oxc_parser/src/cursor.rs signals "rewind me" by setting self.fatal_error = Some(...) via self.unexpected() / set_unexpected() (error_handler.rs:38, 81).
Building that signal costs 2–3 global allocations that are immediately dropped when try_parse rewinds:
// error_handler.rs: set_unexpected → set_fatal_error
let error = diagnostics::unexpected_token(self.cur_token().span());
// diagnostics.rs
OxcDiagnostic::error("Unexpected token").with_label(span)
// oxc_diagnostics/src/lib.rs: OxcDiagnostic::error
Self { inner: Box::new(OxcDiagnosticInner { … }) } // 1 sys alloc
// .with_label(span) pushes to a Vec<LabeledSpan> // +1-2 sys allocs
On rewind the FatalError is restored to the checkpoint's value (usually None), so the Box<OxcDiagnosticInner> and the labels Vec are dropped. The entire heap allocation exists only as a "rewind me" flag.
Evidence
#21532 converted 5 try_parse sites where the discriminator is a cheap peek, eliminating the failure path altogether. Allocation snapshot (tasks/track_memory_allocations/allocs_parser.snap):
| File |
Sys allocs (before → after) |
Δ |
checker.ts |
9672 → 7080 |
−2592 |
binder.ts |
530 → 394 |
−136 |
cal.com.tsx |
1083 → 833 |
−250 |
antd.js |
7132 → 7102 |
−30 |
pdf.mjs |
703 → 695 |
−8 |
TS-heavy files dominate the win because the paths that took the failure branch (infer T extends …, primitive-keyword-vs-reference, x is Foo predicate, soft-keyword modifiers, constructor string key) only fire on TypeScript input.
Suggested fix
Make the throwaway-diagnostic path lazy. set_unexpected could set a cheap sentinel (e.g. self.fatal_error = Some(FatalError::Sentinel)) and only materialize the OxcDiagnostic when fatal_error survives to the top-level parser exit — i.e. when it will actually be reported. That removes the heap cost from every try_parse failure, including the cases where try_parse is genuinely the right tool and can't be converted to lookahead.
Root cause
ParserImpl::try_parseincrates/oxc_parser/src/cursor.rssignals "rewind me" by settingself.fatal_error = Some(...)viaself.unexpected()/set_unexpected()(error_handler.rs:38, 81).Building that signal costs 2–3 global allocations that are immediately dropped when
try_parserewinds:On rewind the
FatalErroris restored to the checkpoint's value (usuallyNone), so theBox<OxcDiagnosticInner>and the labelsVecare dropped. The entire heap allocation exists only as a "rewind me" flag.Evidence
#21532 converted 5
try_parsesites where the discriminator is a cheap peek, eliminating the failure path altogether. Allocation snapshot (tasks/track_memory_allocations/allocs_parser.snap):checker.tsbinder.tscal.com.tsxantd.jspdf.mjsTS-heavy files dominate the win because the paths that took the failure branch (
infer T extends …, primitive-keyword-vs-reference,x is Foopredicate, soft-keyword modifiers, constructor string key) only fire on TypeScript input.Suggested fix
Make the throwaway-diagnostic path lazy.
set_unexpectedcould set a cheap sentinel (e.g.self.fatal_error = Some(FatalError::Sentinel)) and only materialize theOxcDiagnosticwhenfatal_errorsurvives to the top-level parser exit — i.e. when it will actually be reported. That removes the heap cost from everytry_parsefailure, including the cases wheretry_parseis genuinely the right tool and can't be converted tolookahead.