mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
perf(parser): skip parse_call_expression_rest when no call follows (#23063)
## What In `parse_lhs_expression_or_higher`, skip `parse_call_expression_rest` unless the current token is `(` or `?.`, and inline the single-use `parse_member_expression_or_higher` so the `LeftHandSideExpression` parse reads top-to-bottom (`primary` → `member_expression` → `lhs`). ## Why `parse_member_expression_rest` already consumes every `MemberExpression` continuation (`.`, `?.`, `[]`, tagged template, TS `!` / `<T>`). The only ways to extend a fully-parsed `MemberExpression` into an LHS are `Arguments` (`(`) or an `OptionalChain` (`?.`) — the `.`/`[]`/template extensions live under `CallExpression`, so they only apply after a call. Previously `parse_lhs_expression_or_higher` always called `parse_call_expression_rest`, whose loop re-runs `parse_member_expression_rest` (the #1 hotspot per profiling) on every primary before checking for `?.`/`(`. So every leaf expression paid for a redundant member-rest scan plus call-rest overhead. A `sample` profile showed the expression LHS chain at ~50% of parse time. ## Correctness Behavior-preserving. When the current token is neither `(` nor `?.`, `parse_call_expression_rest` is a no-op. `a<T>()` is unaffected (`<T>` consumed in member-rest, leaving `cur == (`). The inlined helper had a single caller and identical `span`. estree (AST + spans + tokens) byte-identical to `main`; `allocs_parser.snap` byte-identical (pure work reduction, no memory effect). ## Reference typescript-go (and TS's own JS parser) use the same shape with the same redundant member-rest re-entry — the reference leaves this unoptimized. Grammar excerpt + `parser.go` cross-reference in the PR comment. ## Benchmark Interleaved A/B, two saved release binaries, 2.5 MB expression-dense file, 12 alternating reps: - `main`: ~22.6 ms/parse (median) - this branch: ~19.7 ms/parse (median) ≈ **13% faster on expression-heavy code**; every branch run beat every main run. Neutral on mixed workloads (saving is per-primary).
This commit is contained in:
2 files changed
+20
-21
No files matched your search
@@ -728,8 +728,22 @@ impl<'a, C: Config> ParserImpl<'a, C> {
|
||||
pub(crate) fn parse_lhs_expression_or_higher(&mut self) -> Expression<'a> {
|
||||
let span = self.start_span();
|
||||
let mut in_optional_chain = false;
|
||||
let lhs = self.parse_member_expression_or_higher(&mut in_optional_chain);
|
||||
let lhs = self.parse_call_expression_rest(span, lhs, &mut in_optional_chain);
|
||||
// `MemberExpression`
|
||||
let primary = self.parse_primary_expression();
|
||||
let member_expression = self.parse_member_expression_rest(
|
||||
span,
|
||||
primary,
|
||||
&mut in_optional_chain,
|
||||
/* allow_optional_chain */ true,
|
||||
);
|
||||
// A fully-parsed `MemberExpression` only extends into a `LeftHandSideExpression` via
|
||||
// `Arguments` (`(`) or an `OptionalChain` (`?.`); see <https://tc39.es/ecma262/#sec-left-hand-side-expressions>.
|
||||
// So skip `parse_call_expression_rest` (and its redundant member-rest re-scan) otherwise.
|
||||
let lhs = if matches!(self.cur_kind(), Kind::LParen | Kind::QuestionDot) {
|
||||
self.parse_call_expression_rest(span, member_expression, &mut in_optional_chain)
|
||||
} else {
|
||||
member_expression
|
||||
};
|
||||
if !in_optional_chain {
|
||||
return lhs;
|
||||
}
|
||||
@@ -763,21 +777,6 @@ impl<'a, C: Config> ParserImpl<'a, C> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Section 13.3 Member Expression
|
||||
fn parse_member_expression_or_higher(
|
||||
&mut self,
|
||||
in_optional_chain: &mut bool,
|
||||
) -> Expression<'a> {
|
||||
let span = self.start_span();
|
||||
let lhs = self.parse_primary_expression();
|
||||
self.parse_member_expression_rest(
|
||||
span,
|
||||
lhs,
|
||||
in_optional_chain,
|
||||
/* allow_optional_chain */ true,
|
||||
)
|
||||
}
|
||||
|
||||
/// Section 13.3 Super Call
|
||||
fn parse_super(&mut self) -> Expression<'a> {
|
||||
let span = self.start_span();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
File | File size || Sys allocs | Sys reallocs || Arena allocs | Arena reallocs
|
||||
---------------------------------------------------------------------------------------------------------------------------
|
||||
checker.ts | 2.92 MB || 5303 | 10 || 265746 | 22860
|
||||
checker.ts | 2.92 MB || 2725 | 10 || 264366 | 22860
|
||||
|
||||
App.tsx | 415.34 kB || 632 | 13 || 44517 | 3538
|
||||
App.tsx | 415.34 kB || 520 | 13 || 44464 | 3537
|
||||
|
||||
RadixUIAdoptionSection.jsx | 2.52 kB || 1 | 0 || 367 | 66
|
||||
|
||||
@@ -10,7 +10,7 @@ pdf.mjs | 567.30 kB || 518 | 67 |
|
||||
|
||||
antd.js | 6.69 MB || 5392 | 221 || 528577 | 55355
|
||||
|
||||
binder.ts | 193.08 kB || 281 | 0 || 16699 | 1476
|
||||
binder.ts | 193.08 kB || 141 | 0 || 16620 | 1476
|
||||
|
||||
kitchen-sink.tsx | 732.90 kB || 5027 | 160 || 139162 | 11919
|
||||
kitchen-sink.tsx | 732.90 kB || 2991 | 160 || 138059 | 11905
|
||||
|
||||
Reference in new issue
Block a user