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:
Boshen committed 2026-06-07 12:12:57 +00:00
1 parent d16f456ce9
commit 883dfc1355
2 files changed
+20 -21

No files matched your search

+16 -17
View File
@@ -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