From c903f5c0e1e0bffd347f81ed7ba8def1e3416ecd Mon Sep 17 00:00:00 2001 From: Boshen Date: Thu, 29 Oct 2026 16:30:15 +0800 Subject: [PATCH] perf(parser): specialize known-token parsing (#28286) Callers already establish these token kinds, so specialized parsing avoids repeating classification while retaining contextual identifier checks. AI-assisted implementation. --- crates/oxc_parser/src/js/arrow.rs | 8 +++- crates/oxc_parser/src/js/binding.rs | 4 ++ crates/oxc_parser/src/js/class.rs | 6 +-- crates/oxc_parser/src/js/expression.rs | 47 +++++++++++++++++-- crates/oxc_parser/src/js/function.rs | 3 ++ crates/oxc_parser/src/js/module.rs | 18 +++---- crates/oxc_parser/src/js/object.rs | 9 +++- crates/oxc_parser/src/jsx/mod.rs | 2 +- crates/oxc_parser/src/ts/statement.rs | 6 +-- crates/oxc_parser/src/ts/types.rs | 10 ++-- .../misc/pass/async-identifier-fast-paths.js | 3 ++ tasks/coverage/snapshots/codegen_misc.snap | 4 +- tasks/coverage/snapshots/formatter_misc.snap | 4 +- tasks/coverage/snapshots/lexer_misc.snap | 4 +- tasks/coverage/snapshots/parser_misc.snap | 4 +- tasks/coverage/snapshots/semantic_misc.snap | 4 +- .../coverage/snapshots/transformer_misc.snap | 4 +- 17 files changed, 102 insertions(+), 38 deletions(-) create mode 100644 tasks/coverage/misc/pass/async-identifier-fast-paths.js diff --git a/crates/oxc_parser/src/js/arrow.rs b/crates/oxc_parser/src/js/arrow.rs index 604c9bdf59..2d20ae8d61 100644 --- a/crates/oxc_parser/src/js/arrow.rs +++ b/crates/oxc_parser/src/js/arrow.rs @@ -37,7 +37,13 @@ impl<'a, C: Config> ParserImpl<'a, C> { if self.at(Kind::Async) && self.is_un_parenthesized_async_arrow_function_worker() { let start = self.cur_start(); self.bump_any(); // bump `async` - let expr = self.parse_binary_expression_or_higher(Precedence::Comma); + // Lookahead established a single binding identifier followed by `=>`. + // An ordinary identifier cannot extend into a member, call, or binary expression here. + let expr = if self.at(Kind::Ident) { + self.parse_plain_identifier_expression() + } else { + self.parse_binary_expression_or_higher(Precedence::Comma) + }; let Expression::Identifier(ident) = &expr else { return self.unexpected(); }; diff --git a/crates/oxc_parser/src/js/binding.rs b/crates/oxc_parser/src/js/binding.rs index 6c946f6d81..52088e9ed0 100644 --- a/crates/oxc_parser/src/js/binding.rs +++ b/crates/oxc_parser/src/js/binding.rs @@ -24,6 +24,10 @@ impl<'a, C: Config> ParserImpl<'a, C> { pub(crate) fn parse_binding_pattern(&mut self) -> BindingPattern<'a> { match self.cur_kind() { + Kind::Ident => { + let ident = self.parse_plain_binding_identifier(); + BindingPattern::BindingIdentifier(self.alloc(ident)) + } Kind::LCurly => self.parse_object_binding_pattern(), Kind::LBrack => self.parse_array_binding_pattern(), _ => self.parse_binding_pattern_identifier(), diff --git a/crates/oxc_parser/src/js/class.rs b/crates/oxc_parser/src/js/class.rs index d08f46a9bd..7e8763582b 100644 --- a/crates/oxc_parser/src/js/class.rs +++ b/crates/oxc_parser/src/js/class.rs @@ -77,7 +77,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { && !(self.at(Kind::Implements) && self.lexer.peek_token().kind().is_identifier_or_keyword()) { - Some(self.parse_binding_identifier()) + Some(self.parse_known_binding_identifier()) } else { None }; @@ -602,14 +602,14 @@ impl<'a, C: Config> ParserImpl<'a, C> { fn parse_constructor_name(&mut self) -> Option> { if self.at(Kind::Constructor) { - let ident = self.parse_identifier_name(); + let ident = self.parse_known_identifier_name(); return Some(PropertyKey::StaticIdentifier(self.alloc(ident))); } if self.at(Kind::Str) && self.cur_js_string() == "constructor" && self.lexer.peek_token().kind() == Kind::LParen { - let string_literal = self.parse_literal_string(); + let string_literal = self.parse_known_literal_string(); return Some(PropertyKey::StringLiteral(self.alloc(string_literal))); } None diff --git a/crates/oxc_parser/src/js/expression.rs b/crates/oxc_parser/src/js/expression.rs index abf0ee4650..58059e11b2 100644 --- a/crates/oxc_parser/src/js/expression.rs +++ b/crates/oxc_parser/src/js/expression.rs @@ -81,6 +81,14 @@ impl<'a, C: Config> ParserImpl<'a, C> { Expression::Identifier(self.alloc(ident)) } + #[expect(clippy::inline_always)] + #[inline(always)] + pub(super) fn parse_plain_identifier_expression(&mut self) -> Expression<'a> { + debug_assert!(self.at(Kind::Ident)); + let (span, name) = self.parse_identifier_kind(Kind::Ident); + Expression::Identifier(self.alloc(IdentifierReference::new(span, name, self))) + } + pub(crate) fn parse_identifier_reference(&mut self) -> IdentifierReference<'a> { // allow `await` and `yield`, let semantic analysis report error let kind = self.cur_kind(); @@ -111,7 +119,25 @@ impl<'a, C: Config> ParserImpl<'a, C> { self.unexpected() }; } - self.check_identifier(cur, self.ctx); + self.parse_known_binding_identifier() + } + + /// Parse a token already known to be a `BindingIdentifier`. + /// Membership alone does not rule out the `Await` and `Yield` early errors. + #[inline] + pub(crate) fn parse_known_binding_identifier(&mut self) -> BindingIdentifier<'a> { + let kind = self.cur_kind(); + debug_assert!(kind.is_binding_identifier()); + self.check_identifier(kind, self.ctx); + let (span, name) = self.parse_identifier_kind(Kind::Ident); + BindingIdentifier::new(span, name, self) + } + + /// Ordinary identifiers cannot trigger the parser's `Await`/`Yield` checks. + /// Strict-mode binding restrictions are still checked by semantic analysis. + #[inline] + pub(crate) fn parse_plain_binding_identifier(&mut self) -> BindingIdentifier<'a> { + debug_assert!(self.at(Kind::Ident)); let (span, name) = self.parse_identifier_kind(Kind::Ident); BindingIdentifier::new(span, name, self) } @@ -130,6 +156,13 @@ impl<'a, C: Config> ParserImpl<'a, C> { if !self.cur_kind().is_identifier_name() { return self.unexpected(); } + self.parse_known_identifier_name() + } + + /// Parse an `IdentifierName` whose token kind has already been checked. + #[inline] + pub(crate) fn parse_known_identifier_name(&mut self) -> IdentifierName<'a> { + debug_assert!(self.cur_kind().is_identifier_name()); let (span, name) = self.parse_identifier_kind(Kind::Ident); IdentifierName::new(span, name, self) } @@ -240,7 +273,8 @@ impl<'a, C: Config> ParserImpl<'a, C> { let kind = self.cur_kind(); match kind { - Kind::Ident => self.parse_identifier_expression(), // fast path, keywords are checked at the end + // Fast path: `Ident` is already classified as a valid identifier reference. + Kind::Ident => self.parse_plain_identifier_expression(), // ArrayLiteral Kind::LBrack => self.parse_array_expression(), // ObjectLiteral @@ -351,7 +385,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { let kind = self.cur_kind(); match kind { Kind::Str => { - let lit = self.parse_literal_string(); + let lit = self.parse_known_literal_string(); Expression::StringLiteral(self.alloc(lit)) } Kind::True | Kind::False => Expression::BooleanLiteral(self.parse_literal_boolean()), @@ -503,6 +537,13 @@ impl<'a, C: Config> ParserImpl<'a, C> { if !self.at(Kind::Str) { return self.unexpected(); } + self.parse_known_literal_string() + } + + /// Parse a string literal whose `Str` token kind has already been checked. + #[inline] + pub(crate) fn parse_known_literal_string(&mut self) -> StringLiteral<'a> { + debug_assert!(self.at(Kind::Str)); let span = self.cur_token().span(); let raw = Str::from(self.cur_src()); let value = self.cur_js_string(); diff --git a/crates/oxc_parser/src/js/function.rs b/crates/oxc_parser/src/js/function.rs index 8ad9179bab..02cb9f8952 100644 --- a/crates/oxc_parser/src/js/function.rs +++ b/crates/oxc_parser/src/js/function.rs @@ -530,6 +530,9 @@ impl<'a, C: Config> ParserImpl<'a, C> { generator: bool, ) -> Option> { let kind = self.cur_kind(); + if kind == Kind::Ident { + return Some(self.parse_plain_binding_identifier()); + } if kind.is_binding_identifier() { let mut ctx = self.ctx; if func_kind.is_expression() { diff --git a/crates/oxc_parser/src/js/module.rs b/crates/oxc_parser/src/js/module.rs index 4d51f22c86..a4f4947477 100644 --- a/crates/oxc_parser/src/js/module.rs +++ b/crates/oxc_parser/src/js/module.rs @@ -79,7 +79,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { let mut identifier_after_import: Option> = if self.cur_kind().is_binding_identifier() { // `import something ...` - Some(self.parse_binding_identifier()) + Some(self.parse_known_binding_identifier()) } else { // `import ...` None @@ -115,7 +115,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { } else if kind.is_binding_identifier() { // `import type something ...` let token = self.cur_token(); - let identifier_after_type = self.parse_binding_identifier(); + let identifier_after_type = self.parse_known_binding_identifier(); if token.kind() == Kind::From && self.at(Kind::Str) { // `import type from 'source'` if token.escaped() { @@ -167,7 +167,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { token_after_import.span(), )); // Parse the identifier as the default specifier to continue recovery - identifier_after_import = Some(self.parse_binding_identifier()); + identifier_after_import = Some(self.parse_known_binding_identifier()); phase = Some(ImportPhase::Defer); } // else: `import defer from 'source'` - defer is the binding name, no phase @@ -175,7 +175,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { if self.cur_kind().is_binding_identifier() { // `import source something ...` let token = self.cur_token(); - let identifier_after_source = self.parse_binding_identifier(); + let identifier_after_source = self.parse_known_binding_identifier(); if token.kind() == Kind::From { // `import source from ...` if self.at(Kind::From) { @@ -402,7 +402,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { fn parse_import_attribute(&mut self) -> ImportAttribute<'a> { let start = self.cur_start(); let key = match self.cur_kind() { - Kind::Str => ImportAttributeKey::StringLiteral(self.parse_literal_string()), + Kind::Str => ImportAttributeKey::StringLiteral(self.parse_known_literal_string()), _ => ImportAttributeKey::Identifier(self.parse_identifier_name()), }; self.expect(Kind::Colon); @@ -411,7 +411,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { self.cur_token().span(), )); } - let value = self.parse_literal_string(); + let value = self.parse_known_literal_string(); ImportAttribute::new(self.end_span(start), key, value, self) } @@ -878,10 +878,10 @@ impl<'a, C: Config> ParserImpl<'a, C> { // import { type as as as } from "mod"; - isTypeOnly: true, name: as, propertyName: as if self.at(Kind::As) { // { type as ...? } - let first_as = self.parse_identifier_name(); + let first_as = self.parse_known_identifier_name(); if self.at(Kind::As) { // { type as as ...? } - let second_as = self.parse_identifier_name(); + let second_as = self.parse_known_identifier_name(); if self.can_parse_module_export_name() { // { type as as something } // { type as as "something" } @@ -994,7 +994,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { pub(crate) fn parse_module_export_name(&mut self) -> ModuleExportName<'a> { match self.cur_kind() { Kind::Str => { - let literal = self.parse_literal_string(); + let literal = self.parse_known_literal_string(); // ModuleExportName : StringLiteral // It is a Syntax Error if IsStringWellFormedUnicode(the SV of StringLiteral) is false. // The error is fatal so that no AST ever carries an ill-formed export name. diff --git a/crates/oxc_parser/src/js/object.rs b/crates/oxc_parser/src/js/object.rs index 93068455bc..0978b5f5a4 100644 --- a/crates/oxc_parser/src/js/object.rs +++ b/crates/oxc_parser/src/js/object.rs @@ -154,7 +154,14 @@ impl<'a, C: Config> ParserImpl<'a, C> { pub(crate) fn parse_property_name(&mut self) -> (PropertyKey<'a>, bool) { let mut computed = false; let key = match self.cur_kind() { - Kind::Str => PropertyKey::from(self.parse_literal_expression()), + Kind::Ident => { + let ident = self.parse_known_identifier_name(); + PropertyKey::StaticIdentifier(self.alloc(ident)) + } + Kind::Str => { + let literal = self.parse_known_literal_string(); + PropertyKey::StringLiteral(self.alloc(literal)) + } kind if kind.is_number() => PropertyKey::from(self.parse_literal_expression()), // { [foo]() {} } Kind::LBrack => { diff --git a/crates/oxc_parser/src/jsx/mod.rs b/crates/oxc_parser/src/jsx/mod.rs index 0e60e98b3f..5e19b28cb8 100644 --- a/crates/oxc_parser/src/jsx/mod.rs +++ b/crates/oxc_parser/src/jsx/mod.rs @@ -481,7 +481,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { fn parse_jsx_attribute_value(&mut self) -> JSXAttributeValue<'a> { match self.cur_kind() { Kind::Str => { - let str_lit = self.parse_literal_string(); + let str_lit = self.parse_known_literal_string(); JSXAttributeValue::StringLiteral(self.alloc(str_lit)) } Kind::LCurly => { diff --git a/crates/oxc_parser/src/ts/statement.rs b/crates/oxc_parser/src/ts/statement.rs index cd6321224a..8ab2d3281d 100644 --- a/crates/oxc_parser/src/ts/statement.rs +++ b/crates/oxc_parser/src/ts/statement.rs @@ -72,7 +72,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { fn parse_ts_enum_member_name(&mut self) -> TSEnumMemberName<'a> { match self.cur_kind() { Kind::Str => { - let literal = self.parse_literal_string(); + let literal = self.parse_known_literal_string(); TSEnumMemberName::String(self.alloc(literal)) } Kind::LBrack => match self.parse_computed_property_name() { @@ -459,7 +459,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { start: u32, modifiers: &Modifiers, ) -> ArenaBox<'a, TSExternalModuleDeclaration<'a>> { - let id = self.parse_literal_string(); + let id = self.parse_known_literal_string(); if !self.ctx.has_ambient() { self.error(diagnostics::quoted_module_name_only_allowed_in_ambient_module(id.span())); } @@ -715,7 +715,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { && self.cur_kind().is_binding_identifier() { // `import type something ...` - identifier = self.parse_binding_identifier(); + identifier = self.parse_known_binding_identifier(); import_kind = ImportOrExportKind::Type; } self.parse_ts_import_equals_declaration(import_kind, identifier, start) diff --git a/crates/oxc_parser/src/ts/types.rs b/crates/oxc_parser/src/ts/types.rs index e3d956d347..76babc8c9d 100644 --- a/crates/oxc_parser/src/ts/types.rs +++ b/crates/oxc_parser/src/ts/types.rs @@ -799,7 +799,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { let parameter_name = if self.at(Kind::This) { TSTypePredicateName::This(self.parse_this_type_node()) } else { - let ident_name = self.parse_identifier_name(); + let ident_name = self.parse_known_identifier_name(); TSTypePredicateName::Identifier(self.alloc(ident_name)) }; let mut type_annotation = None; @@ -1005,7 +1005,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { && self.lookahead(Self::is_next_token_colon_or_question_colon) { let member_start = self.cur_start(); - let label = self.parse_identifier_name(); + let label = self.parse_known_identifier_name(); let optional = self.eat(Kind::Question); self.expect(Kind::Colon); let type_start = self.cur_start(); @@ -1106,7 +1106,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { self.expect(Kind::LParen); let source = if self.at(Kind::Str) { - self.parse_literal_string() + self.parse_known_literal_string() } else { // `StringLiteral` is the only valid type for `TSImportType`'s `source` field. So this is an error. // Fallback to parsing a `TSType`, to obtain a good span for the diagnostic. @@ -1241,7 +1241,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { (key, value, true) } else { let key = if self.at(Kind::Str) { - let string_literal = self.parse_literal_string(); + let string_literal = self.parse_known_literal_string(); PropertyKey::StringLiteral(self.alloc(string_literal)) } else { let ident = self.parse_identifier_name(); @@ -1317,7 +1317,7 @@ impl<'a, C: Config> ParserImpl<'a, C> { let parameter_name = if self.at(Kind::This) { TSTypePredicateName::This(self.parse_this_type_node()) } else { - let ident_name = self.parse_identifier_name(); + let ident_name = self.parse_known_identifier_name(); TSTypePredicateName::Identifier(self.alloc(ident_name)) }; self.bump_any(); // bump `is` diff --git a/tasks/coverage/misc/pass/async-identifier-fast-paths.js b/tasks/coverage/misc/pass/async-identifier-fast-paths.js new file mode 100644 index 0000000000..4a0206667d --- /dev/null +++ b/tasks/coverage/misc/pass/async-identifier-fast-paths.js @@ -0,0 +1,3 @@ +const plain = async foo => ({ foo, "key": foo }); +const contextual = async get => ({ get, "key": get }); +const escaped = async \u0066oo => ({ foo, "key": \u0066oo }); diff --git a/tasks/coverage/snapshots/codegen_misc.snap b/tasks/coverage/snapshots/codegen_misc.snap index 35f18bed42..6302943e05 100644 --- a/tasks/coverage/snapshots/codegen_misc.snap +++ b/tasks/coverage/snapshots/codegen_misc.snap @@ -1,3 +1,3 @@ codegen_misc Summary: -AST Parsed : 117/117 (100.00%) -Positive Passed: 117/117 (100.00%) +AST Parsed : 118/118 (100.00%) +Positive Passed: 118/118 (100.00%) diff --git a/tasks/coverage/snapshots/formatter_misc.snap b/tasks/coverage/snapshots/formatter_misc.snap index 68fb647891..1aeef96a95 100644 --- a/tasks/coverage/snapshots/formatter_misc.snap +++ b/tasks/coverage/snapshots/formatter_misc.snap @@ -1,3 +1,3 @@ formatter_misc Summary: -AST Parsed : 117/117 (100.00%) -Positive Passed: 117/117 (100.00%) +AST Parsed : 118/118 (100.00%) +Positive Passed: 118/118 (100.00%) diff --git a/tasks/coverage/snapshots/lexer_misc.snap b/tasks/coverage/snapshots/lexer_misc.snap index 34a7837e6b..1d0c9fa28f 100644 --- a/tasks/coverage/snapshots/lexer_misc.snap +++ b/tasks/coverage/snapshots/lexer_misc.snap @@ -1,3 +1,3 @@ lexer_misc Summary: -AST Parsed : 372/372 (100.00%) -Positive Passed: 372/372 (100.00%) +AST Parsed : 373/373 (100.00%) +Positive Passed: 373/373 (100.00%) diff --git a/tasks/coverage/snapshots/parser_misc.snap b/tasks/coverage/snapshots/parser_misc.snap index 03c9ff6242..0191607100 100644 --- a/tasks/coverage/snapshots/parser_misc.snap +++ b/tasks/coverage/snapshots/parser_misc.snap @@ -1,6 +1,6 @@ parser_misc Summary: -AST Parsed : 117/117 (100.00%) -Positive Passed: 117/117 (100.00%) +AST Parsed : 118/118 (100.00%) +Positive Passed: 118/118 (100.00%) Negative Passed: 257/257 (100.00%) × TS(1275): 'accessor' modifier can only appear on a property declaration. diff --git a/tasks/coverage/snapshots/semantic_misc.snap b/tasks/coverage/snapshots/semantic_misc.snap index ed1fa84f36..35188280f3 100644 --- a/tasks/coverage/snapshots/semantic_misc.snap +++ b/tasks/coverage/snapshots/semantic_misc.snap @@ -1,6 +1,6 @@ semantic_misc Summary: -AST Parsed : 117/117 (100.00%) -Positive Passed: 111/117 (94.87%) +AST Parsed : 118/118 (100.00%) +Positive Passed: 112/118 (94.92%) semantic Error: tasks/coverage/misc/pass/declare-let-private.ts Bindings mismatch: after transform: ScopeId(0): ["private"] diff --git a/tasks/coverage/snapshots/transformer_misc.snap b/tasks/coverage/snapshots/transformer_misc.snap index 5e1acaa010..6bdf80ec73 100644 --- a/tasks/coverage/snapshots/transformer_misc.snap +++ b/tasks/coverage/snapshots/transformer_misc.snap @@ -1,3 +1,3 @@ transformer_misc Summary: -AST Parsed : 117/117 (100.00%) -Positive Passed: 117/117 (100.00%) +AST Parsed : 118/118 (100.00%) +Positive Passed: 118/118 (100.00%)