Skip to main content

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}