hermes_parser/lexer/lookahead.rs
1//! Parser-facing lookahead helpers for the JS lexer.
2//!
3//! These `impl<'a> JSLexer<'a>` methods live in a child module of `lexer`, so
4//! they can access the private fields of `JSLexer` declared in `lexer/mod.rs`.
5//!
6//! Ported from `lib/Parser/JSLexer.cpp`:
7//! - `optimisticSkipWhitespace` (`:117-132`)
8//! - `lookahead1` (`:1038-1095`)
9//! - `lookahead2` (`:1100-1154`)
10//! - `isLetFollowedByDeclStart` (`:134-176`)
11//! - `isUsingFollowedByIdentifier` (`:178-204`)
12//! - `isAwaitUsingFollowedByIdentifier` (`:206-253`)
13//!
14//! The C++ `template <bool RequireNoNewLine>` is preserved as the const generic
15//! `REQUIRE_NO_NEWLINE` (so each specialization monomorphizes like the C++
16//! template); the parser `Keywords` dependency is replaced by passing the needed
17//! pre-interned atom (`ident_using`). The C++ `make_scope_exit` restore +
18//! `SaveAndSuppressMessages` become explicit save/restore.
19
20use hermes_atom_table::AtomBytes;
21use hermes_support::diag::Subsystem;
22
23use hermes_unicode::{is_ascii_identifier_continue, is_ascii_identifier_start};
24
25use crate::token_kinds::TokenKind;
26
27use super::{GrammarContext, JSLexer};
28
29impl<'a> JSLexer<'a> {
30 /// Skip ` `/`\t`/`\v`/`\f` from the cursor (advancing it) and return the
31 /// next non-whitespace byte (or `\0` at EOF). Does NOT skip newlines or
32 /// comments. Port of `optimisticSkipWhitespace` (JSLexer.cpp:117-132).
33 pub(crate) fn optimistic_skip_whitespace(&mut self) -> u8 {
34 loop {
35 let cur = self.cursor.peek();
36 if cur == 0 {
37 return 0;
38 }
39 match cur {
40 b' ' | b'\t' | 0x0b | 0x0c => {
41 self.cursor.advance(1);
42 continue;
43 }
44 _ => return cur,
45 }
46 }
47 }
48
49 /// Look ahead one token, restoring the lexer state afterwards (unless the
50 /// next token matches `expected`, in which case the lookahead is consumed).
51 /// Port of `lookahead1` (JSLexer.cpp:1038-1095).
52 ///
53 /// The C++ `template <bool RequireNoNewLine>` is the const generic
54 /// `REQUIRE_NO_NEWLINE`.
55 pub fn lookahead1<const REQUIRE_NO_NEWLINE: bool>(
56 &mut self,
57 expected: Option<TokenKind>,
58 ) -> Option<TokenKind> {
59 // We support TokenKind::question here because of Flow's render types.
60 // `renders?` is not a token itself (as making it a token would be bad
61 // for identifier parsing performance). When we are parsing something
62 // like (renders?: number) => string and the cursor is under the `?`, we
63 // need to perform a lookahead to see if the next token is a colon, in
64 // which case this is a function parameter, and if not then parse as a
65 // render type.
66 debug_assert!(
67 self.token.kind() == TokenKind::identifier
68 || self.token.is_res_word()
69 || self.token.kind() == TokenKind::question,
70 "unsupported current token"
71 );
72 let saved_kind = self.token.kind();
73 let saved_ident: Option<AtomBytes> = if saved_kind
74 == TokenKind::identifier
75 || self.token.is_res_word()
76 {
77 Some(self.token.get_res_word_or_identifier())
78 } else {
79 None
80 };
81 let start = self.token.start_loc();
82 let end = self.token.end_loc();
83 let cur = self.cur_loc();
84 let saved_suppressed = self.sm.suppressed_messages();
85 self.sm.set_suppressed_messages(Some(Subsystem::Unspecified));
86
87 // Remove any comments that were stored during the lookahead.
88 let saved_comment_storage_size = self.comment_storage.len();
89
90 self.advance(GrammarContext::AllowRegExp);
91 let mut kind = Some(self.token.kind());
92 if REQUIRE_NO_NEWLINE && self.is_new_line_before_current_token() {
93 // Disregard anything after LineTerminator.
94 kind = None;
95 } else if expected == kind {
96 // Do not move the cursor back.
97 // NOTE: the C++ `make_scope_exit` still fires on this early return, so
98 // it truncates comment storage here too (and we restore the suppression
99 // state the RAII guard would have restored).
100 if self.store_comments {
101 self.comment_storage.truncate(saved_comment_storage_size);
102 }
103 self.sm.set_suppressed_messages(saved_suppressed);
104 return kind;
105 }
106
107 self.token.set_start(start);
108 self.token.set_end(end);
109 if saved_kind == TokenKind::identifier {
110 self.token.set_identifier(saved_ident.unwrap());
111 } else if saved_kind == TokenKind::question {
112 self.token.set_punctuator(TokenKind::question);
113 } else {
114 self.token.set_res_word(saved_kind, saved_ident.unwrap());
115 }
116 self.seek(cur);
117
118 // Undo the storage for the token we just advanced to.
119 if self.store_tokens {
120 self.token_storage.pop();
121 }
122 if self.store_comments {
123 self.comment_storage.truncate(saved_comment_storage_size);
124 }
125
126 self.sm.set_suppressed_messages(saved_suppressed);
127 kind
128 }
129
130 /// Look ahead two tokens: if the next token is `expected_ident`, return the
131 /// kind of the token after it; otherwise `None`. ALWAYS restores the lexer
132 /// state. Port of `lookahead2` (JSLexer.cpp:1100-1154).
133 ///
134 /// The C++ `template <bool RequireNoNewLine>` is the const generic
135 /// `REQUIRE_NO_NEWLINE`; the C++ single `make_scope_exit` that always
136 /// restores becomes a computed result followed by an unconditional restore.
137 pub fn lookahead2<const REQUIRE_NO_NEWLINE: bool>(
138 &mut self,
139 expected_ident: AtomBytes,
140 ) -> Option<TokenKind> {
141 debug_assert!(
142 self.token.kind() == TokenKind::identifier
143 || self.token.is_res_word(),
144 "unsupported current token"
145 );
146 let saved_ident = self.token.get_res_word_or_identifier();
147 let saved_kind = self.token.kind();
148 let start = self.token.start_loc();
149 let end = self.token.end_loc();
150 let cur = self.cur_loc();
151 let saved_suppressed = self.sm.suppressed_messages();
152 self.sm.set_suppressed_messages(Some(Subsystem::Unspecified));
153
154 // Remove any comments/tokens that were stored during the lookahead.
155 let saved_comment_storage_size = self.comment_storage.len();
156 let saved_token_storage_size =
157 if self.store_tokens { self.token_storage.len() } else { 0 };
158
159 // Compute the result; the C++ scope_exit restores unconditionally, so
160 // we capture the result and restore at the end of the function.
161 let result = self.lookahead2_impl::<REQUIRE_NO_NEWLINE>(expected_ident);
162
163 // Restore (mirror of the C++ `make_scope_exit`).
164 if self.store_comments {
165 self.comment_storage.truncate(saved_comment_storage_size);
166 }
167 // Undo the storage for the tokens we advanced to.
168 if self.store_tokens {
169 self.token_storage.truncate(saved_token_storage_size);
170 }
171 // Restore the original token.
172 self.token.set_start(start);
173 self.token.set_end(end);
174 if saved_kind == TokenKind::identifier {
175 self.token.set_identifier(saved_ident);
176 } else {
177 self.token.set_res_word(saved_kind, saved_ident);
178 }
179 self.seek(cur);
180
181 self.sm.set_suppressed_messages(saved_suppressed);
182 result
183 }
184
185 /// The body of `lookahead2` that advances past two tokens and computes the
186 /// result; the surrounding `lookahead2` performs the unconditional restore.
187 fn lookahead2_impl<const REQUIRE_NO_NEWLINE: bool>(
188 &mut self,
189 expected_ident: AtomBytes,
190 ) -> Option<TokenKind> {
191 self.advance(GrammarContext::AllowRegExp);
192 if REQUIRE_NO_NEWLINE && self.is_new_line_before_current_token() {
193 return None;
194 }
195
196 // If the next token isn't the expected identifier, bail.
197 if self.token.kind() != TokenKind::identifier
198 || self.token.get_identifier() != expected_ident
199 {
200 return None;
201 }
202
203 // Advance to the token we're looking ahead to.
204 self.advance(GrammarContext::AllowRegExp);
205 if REQUIRE_NO_NEWLINE && self.is_new_line_before_current_token() {
206 return None;
207 }
208
209 Some(self.token.kind())
210 }
211
212 /// \return true if the `let` keyword (the current token) is followed by the
213 /// start of a declaration. Port of `isLetFollowedByDeclStart`
214 /// (JSLexer.cpp:134-176).
215 pub fn is_let_followed_by_decl_start(&mut self) -> bool {
216 debug_assert!(
217 self.token.kind() == TokenKind::identifier
218 && self.strtab.bytes(self.token.get_identifier()) == b"let",
219 "current token must be the `let` identifier"
220 );
221
222 // Unlike `is_using_*`, this does NOT save/restore the cursor around the
223 // whitespace skip (matching the C++): the fast paths only peek, and the
224 // slow path delegates to `lookahead1`, which restores the cursor itself.
225 let cur_char = self.optimistic_skip_whitespace();
226
227 // Fast path.
228 // If the next character is a '{', then this is a let declaration.
229 // If the next character is a '[', then this is a let declaration.
230 if cur_char == b'{' || cur_char == b'[' {
231 return true;
232 }
233
234 // Fast path.
235 // If the next character starts an ASCII identifier,
236 // then this is a declaration.
237 // Don't check for UTF-8 here to avoid having to read a codepoint
238 // or determine Unicode letter value membership.
239 if is_ascii_identifier_start(cur_char as u32) {
240 // If the next characters are 'in', this may result in 'in' or
241 // 'instanceof'. So we'd actually have to run a lookahead.
242 if !(cur_char == b'i' && self.cursor.peek_at(1) == b'n') {
243 return true;
244 }
245 }
246
247 // Slow path.
248 // There might be comments, newlines, UTF-8 identifiers, etc.
249 // If there's a next token and it's an identifier, '[', '{', then this
250 // is a declaration. Otherwise, it's not.
251 // Pass RequireNoNewLine=false because
252 // let
253 // x = 3;
254 // is supposed to parse as a let declaration of x, no ASI here.
255 // https://262.ecma-international.org/14.0/#prod-LexicalBinding
256 let next_token_kind = self.lookahead1::<false>(None);
257 matches!(
258 next_token_kind,
259 Some(TokenKind::identifier)
260 | Some(TokenKind::l_brace)
261 | Some(TokenKind::l_square)
262 )
263 }
264
265 /// \return true if the `using` keyword (the current token) is followed by an
266 /// identifier with no intervening line terminator. Port of
267 /// `isUsingFollowedByIdentifier` (JSLexer.cpp:178-204).
268 ///
269 /// DEVIATION: the C++ takes the `Keywords &kw` and asserts the current token
270 /// is `kw.identUsing`; we keep that as a `debug_assert` against the interned
271 /// identifier bytes.
272 pub fn is_using_followed_by_identifier(&mut self) -> bool {
273 debug_assert!(
274 self.token.kind() == TokenKind::identifier
275 && self.strtab.bytes(self.token.get_identifier()) == b"using",
276 "current token must be the `using` identifier"
277 );
278 // Checking for:
279 // using [no LineTerminator here] Identifier
280 // ^
281
282 let saved_ptr = self.cursor.offset();
283 let cur_char = self.optimistic_skip_whitespace();
284 self.cursor.seek(saved_ptr);
285
286 // Check for newline - if present, this is not a using declaration.
287 if cur_char == b'\r' || cur_char == b'\n' {
288 return false;
289 }
290
291 // Fast path: next char starts an ASCII identifier.
292 if is_ascii_identifier_start(cur_char as u32) {
293 return true;
294 }
295
296 // Slow path: use lookahead with RequireNoNewLine=true.
297 let next_token_kind = self.lookahead1::<true>(None);
298 next_token_kind == Some(TokenKind::identifier)
299 }
300
301 /// \return true if `await` (the current token) is followed by `using` and
302 /// then an identifier, with no intervening line terminators. Port of
303 /// `isAwaitUsingFollowedByIdentifier` (JSLexer.cpp:206-253).
304 ///
305 /// DEVIATION: the C++ takes `Keywords &kw`; the `kw.identUsing` atom is
306 /// passed in as `ident_using`.
307 pub fn is_await_using_followed_by_identifier(
308 &mut self,
309 ident_using: AtomBytes,
310 ) -> bool {
311 debug_assert!(
312 self.token.kind() == TokenKind::identifier
313 && self.strtab.bytes(self.token.get_identifier()) == b"await",
314 "current token must be the `await` identifier"
315 );
316 // Checking for:
317 // await [no LineTerminator here] using [no LineTerminator here] Identifier
318 // ^
319
320 let saved_ptr = self.cursor.offset();
321
322 // Skip whitespace after 'await' (no newlines allowed).
323 let mut cur_char = self.optimistic_skip_whitespace();
324
325 // Check for newline.
326 if cur_char == b'\r' || cur_char == b'\n' {
327 self.cursor.seek(saved_ptr);
328 return false;
329 }
330
331 // Fast path: check if next chars are 'using' followed by whitespace
332 // and an ASCII identifier.
333 // Note that we can just check character by character because the buffer
334 // is null-terminated.
335 if cur_char == b'u'
336 && self.cursor.peek_at(1) == b's'
337 && self.cursor.peek_at(2) == b'i'
338 && self.cursor.peek_at(3) == b'n'
339 && self.cursor.peek_at(4) == b'g'
340 && !is_ascii_identifier_continue(self.cursor.peek_at(5) as u32)
341 {
342 self.cursor.advance(5);
343 cur_char = self.optimistic_skip_whitespace();
344
345 self.cursor.seek(saved_ptr);
346
347 // Check for newline between 'using' and identifier.
348 if cur_char == b'\r' || cur_char == b'\n' {
349 return false;
350 }
351
352 if is_ascii_identifier_start(cur_char as u32) {
353 return true;
354 }
355 }
356
357 // Slow path.
358 // There might be comments, newlines, UTF-8 identifiers, etc.
359 self.cursor.seek(saved_ptr);
360 let opt_next = self.lookahead2::<true>(ident_using);
361 opt_next == Some(TokenKind::identifier)
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use hermes_atom_table::AtomTable;
369 use hermes_support::manager::SourceErrorManager;
370
371 #[test]
372 fn lookahead1_basic() {
373 // current token must be identifier/resword/question. lookahead1 peeks
374 // the next token and restores state unless it matches `expected`.
375 let mut sm = SourceErrorManager::new();
376 let id = sm.add_buffer("t", "async function");
377 let tab = AtomTable::new();
378 let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
379 lex.advance(GrammarContext::AllowDiv); // 'async' (identifier)
380 // peek: next is 'function' (rw_function), no newline.
381 assert_eq!(lex.lookahead1::<true>(None), Some(TokenKind::rw_function));
382 // state restored: current token still 'async', next advance is 'function'
383 assert_eq!(lex.token().kind(), TokenKind::identifier);
384 assert_eq!(
385 lex.advance(GrammarContext::AllowDiv).kind(),
386 TokenKind::rw_function
387 );
388 }
389
390 #[test]
391 fn lookahead1_consume_truncates_comments() {
392 // With store_comments on, a comment collected during a CONSUMED lookahead
393 // (expected matched) must be rolled back from comment storage, matching the
394 // C++ make_scope_exit which fires on the early return too.
395 let mut sm = SourceErrorManager::new();
396 let id = sm.add_buffer("t", "a /*c*/ b");
397 let tab = AtomTable::new();
398 let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
399 lex.set_store_comments(true);
400 lex.advance(GrammarContext::AllowDiv); // 'a'
401 assert_eq!(lex.get_stored_comments().len(), 0);
402 // Consume the lookahead (next token 'b' is an identifier, which matches).
403 assert_eq!(
404 lex.lookahead1::<true>(Some(TokenKind::identifier)),
405 Some(TokenKind::identifier)
406 );
407 // The block comment scanned during the consumed lookahead is rolled back.
408 assert_eq!(lex.get_stored_comments().len(), 0);
409 }
410
411 #[test]
412 fn lookahead1_newline_and_expected() {
413 let mut sm = SourceErrorManager::new();
414 let id = sm.add_buffer("t", "async\nx");
415 let tab = AtomTable::new();
416 let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
417 lex.advance(GrammarContext::AllowDiv); // 'async'
418 // RequireNoNewLine=true and there IS a newline -> None
419 assert_eq!(lex.lookahead1::<true>(None), None);
420
421 // when expectedToken matches, the cursor is NOT moved back (consumes
422 // the lookahead):
423 let mut sm2 = SourceErrorManager::new();
424 let id2 = sm2.add_buffer("t2", "a b");
425 let tab2 = AtomTable::new();
426 let mut lex2 =
427 JSLexer::new(id2, &mut sm2, &tab2, GrammarContext::AllowDiv);
428 lex2.advance(GrammarContext::AllowDiv); // 'a'
429 assert_eq!(
430 lex2.lookahead1::<true>(Some(TokenKind::identifier)),
431 Some(TokenKind::identifier)
432 );
433 assert_eq!(lex2.token().kind(), TokenKind::identifier); // now 'b' (consumed)
434 }
435
436 #[test]
437 fn lookahead2_basic() {
438 // lookahead2(expected_ident): skip the next token IF it's
439 // `expected_ident`, return the kind of the token after it. Always
440 // restores state.
441 let mut sm = SourceErrorManager::new();
442 let id = sm.add_buffer("t", "await using x");
443 let tab = AtomTable::new();
444 let mut lex = JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
445 lex.advance(GrammarContext::AllowDiv); // 'await'
446 let using = tab.atom_bytes(b"using");
447 // next is 'using' (matches), the one after is 'x' (identifier).
448 assert_eq!(lex.lookahead2::<true>(using), Some(TokenKind::identifier));
449 // state restored to 'await'
450 assert_eq!(lex.token().kind(), TokenKind::identifier);
451 assert_eq!(
452 lex.token().get_res_word_or_identifier(),
453 tab.atom_bytes(b"await")
454 );
455 }
456
457 #[test]
458 fn let_decl_start() {
459 fn islet(src: &str) -> bool {
460 let mut sm = SourceErrorManager::new();
461 let id = sm.add_buffer("t", src);
462 let tab = AtomTable::new();
463 let mut lex =
464 JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
465 lex.advance(GrammarContext::AllowDiv); // 'let'
466 lex.is_let_followed_by_decl_start()
467 }
468 assert!(islet("let x"));
469 assert!(islet("let {a}"));
470 assert!(islet("let [a]"));
471 assert!(islet("let\nx")); // no ASI: still a declaration
472 assert!(!islet("let in")); // 'let in ...' is not a decl ('in' operator)
473 assert!(!islet("let = 3")); // 'let' as identifier
474 }
475
476 #[test]
477 fn using_decls() {
478 fn isusing(src: &str) -> bool {
479 let mut sm = SourceErrorManager::new();
480 let id = sm.add_buffer("t", src);
481 let tab = AtomTable::new();
482 let mut lex =
483 JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
484 lex.advance(GrammarContext::AllowDiv); // 'using'
485 lex.is_using_followed_by_identifier()
486 }
487 assert!(isusing("using x"));
488 assert!(!isusing("using\nx")); // newline -> not a using decl
489 assert!(!isusing("using = 1")); // 'using' as identifier
490
491 fn isawait(src: &str) -> bool {
492 let mut sm = SourceErrorManager::new();
493 let id = sm.add_buffer("t", src);
494 let tab = AtomTable::new();
495 let mut lex =
496 JSLexer::new(id, &mut sm, &tab, GrammarContext::AllowDiv);
497 lex.advance(GrammarContext::AllowDiv); // 'await'
498 let using = tab.atom_bytes(b"using");
499 lex.is_await_using_followed_by_identifier(using)
500 }
501 assert!(isawait("await using x"));
502 assert!(!isawait("await using\nx"));
503 assert!(!isawait("await x"));
504 }
505}