Skip to main content

hermes_parser/lexer/
state.rs

1//! Self-contained lexer-state surface: the `unsafeSet*` helpers, `SavePoint`
2//! (save/restore for backtracking), `isCurrentTokenADirective`, and
3//! `rescanRBraceInTemplateLiteral`.
4//!
5//! These `impl<'a> JSLexer<'a>` methods live in a child module of `lexer`, so
6//! they can access the private fields of `JSLexer` declared in `lexer/mod.rs`.
7//!
8//! Ported from `include/hermes/Parser/JSLexer.h` (`SavePoint`, the `unsafeSet*`
9//! helpers) and `lib/Parser/JSLexer.cpp:911-1035`.
10
11use hermes_atom_table::AtomBytes;
12use hermes_support::diag::Subsystem;
13use hermes_support::location::{SMLoc, SMRange};
14
15use crate::token_kinds::TokenKind;
16use crate::utf8::{
17    is_utf8_start, match_unicode_line_terminator_offset1,
18    UTF8_LINE_TERMINATOR_CHAR0,
19};
20use hermes_unicode::is_unicode_only_space;
21
22use super::JSLexer;
23
24impl<'a> JSLexer<'a> {
25    /// Set the end location of the previous token. Port of
26    /// `setPrevTokenEndLoc`.
27    pub fn set_prev_token_end_loc(&mut self, loc: SMLoc) {
28        self.prev_token_end = loc;
29    }
30
31    /// Set the current token kind to `kind` without any checks and seek to
32    /// `loc`. Should only be used for save-point use-cases. Port of
33    /// `unsafeSetPunctuator` (JSLexer.h:1049-1054). (The C++ name says "unsafe"
34    /// but the Rust port involves no `unsafe` keyword — all `unsafe` lives in
35    /// `cursor.rs`.)
36    pub(crate) fn unsafe_set_punctuator(
37        &mut self,
38        kind: TokenKind,
39        loc: SMLoc,
40        range: SMRange,
41    ) {
42        debug_assert!(kind.is_punctuator(), "must set a punctuator");
43        self.token.set_punctuator(kind);
44        self.token.set_range(range);
45        self.seek(loc);
46    }
47
48    /// Set the current token to an identifier without any checks and seek to
49    /// `loc`. Should only be used for save-point use-cases. Port of
50    /// `unsafeSetIdentifier` (JSLexer.h:1059-1063).
51    pub(crate) fn unsafe_set_identifier(
52        &mut self,
53        ident: AtomBytes,
54        loc: SMLoc,
55        range: SMRange,
56    ) {
57        self.token.set_identifier(ident);
58        self.token.set_range(range);
59        self.seek(loc);
60    }
61
62    /// Set the current token to a reserved word without any checks and seek to
63    /// `loc`. Should only be used for save-point use-cases. Port of
64    /// `unsafeSetReservedWord` (JSLexer.h:1068-1072).
65    pub(crate) fn unsafe_set_reserved_word(
66        &mut self,
67        kind: TokenKind,
68        loc: SMLoc,
69        range: SMRange,
70    ) {
71        let ident = self.res_word_ident(kind);
72        self.token.set_res_word(kind, ident);
73        self.token.set_range(range);
74        self.seek(loc);
75    }
76
77    /// Store state of the lexer and allow rescanning from that point. Port of
78    /// `JSLexer::SavePoint::SavePoint` (JSLexer.h:778-794). Can only save state
79    /// when the current token is a punctuator, `identifier`, or `rw_extends`.
80    ///
81    /// DEVIATION: the C++ `SavePoint` is an RAII-style object holding a
82    /// `JSLexer *`. Rust cannot hold a `&mut JSLexer` across an `advance` call
83    /// (which also needs `&mut JSLexer`), so we model it as a plain value
84    /// snapshot whose `restore(&mut JSLexer)` re-applies the saved state.
85    pub fn save_point(&self) -> SavePoint {
86        let kind = self.token.kind();
87        debug_assert!(
88            kind.is_punctuator()
89                || kind == TokenKind::identifier
90                || kind == TokenKind::rw_extends,
91            "SavePoint can only be used for punctuators, identifier or `extends` keyword"
92        );
93        SavePoint {
94            kind,
95            // Saved identifier, None if kind != identifier.
96            ident: if kind == TokenKind::identifier {
97                Some(self.token.get_identifier())
98            } else {
99                None
100            },
101            loc: self.cur_loc(),
102            range: self.token.source_range(),
103            prev_token_end: self.prev_token_end,
104            comment_storage_size: self.get_stored_comments().len(),
105            token_storage_size: self.get_stored_tokens().len(),
106        }
107    }
108
109    /// Check whether the current token is a directive, in other words is it a
110    /// string literal without escapes or new line continuations, followed by
111    /// either new line, semicolon or right brace. This doesn't move the input
112    /// pointer, so the optional semicolon, brace or the new line will be
113    /// consumed normally by the next `advance` call. Port of
114    /// `isCurrentTokenADirective` (JSLexer.cpp:911-1021).
115    ///
116    /// \return true if the token can be interpreted as a directive.
117    pub fn is_current_token_a_directive(&mut self) -> bool {
118        if self.token.kind() != TokenKind::string_literal {
119            return false;
120        }
121
122        // A directive is a string literal (the current token, directly behind
123        // the cursor), followed by a semicolon, new line, or eof that we will
124        // now try to find. There can also be comments. So, we loop, consuming
125        // whitespace until we encounter:
126        // - EOF. Don't consume it and succeed.
127        // - Semicolon. Don't consume it and succeed.
128        // - Right brace. Don't consume it and succeed.
129        // - A new line. Don't consume it and succeed.
130        // - A line comment. It implies a new line. Don't consume it and succeed.
131        // - A block comment. Consume it and continue.
132        // - Anything else. We consume nothing and fail.
133        //
134        // DEVIATION: the C++ scans with a local `ptr` for the simple cases and
135        // calls `skipBlockComment(ptr)` (which returns the new ptr) only for
136        // block comments. Our `skip_block_comment` mutates the cursor + newline
137        // flag, so we scan from a local offset, only moving the real cursor for
138        // the block-comment case, and restore the cursor offset (and the newline
139        // flag, which the block-comment scan may set) before returning so the
140        // caller's next `advance` starts where it left off.
141        let saved_offset = self.cursor.offset();
142        let saved_newline = self.new_line_before_current_token;
143        // Clone the buffer Rc so the byte view does not borrow `self`; the
144        // block-comment arm needs `&mut self`, and the buffer bytes are stable.
145        let buffer = self.cursor.buffer().clone();
146        let raw = buffer.raw();
147        let mut ptr = saved_offset as usize;
148
149        let result = loop {
150            debug_assert!(
151                ptr < raw.len(),
152                "lexing past end of input"
153            );
154
155            match raw[ptr] {
156                0 => {
157                    // EOF? (the trailing NUL is at index raw.len() - 1)
158                    if ptr == raw.len() - 1 {
159                        break true;
160                    }
161                    // We encountered a stray 0 character.
162                    break false;
163                }
164
165                b';' | b'}' => break true,
166
167                b'\r' | b'\n' => break true,
168
169                // Line separator 
 UTF8 encoded is      : e2 80 a8
170                // Paragraph separator 
 UTF8 encoded is : e2 80 a9
171                UTF8_LINE_TERMINATOR_CHAR0 => {
172                    if match_unicode_line_terminator_offset1(&raw[ptr..]) {
173                        break true;
174                    }
175                    break false;
176                }
177
178                // \v \f : skip whitespace.
179                0x0b | 0x0c => {
180                    ptr += 1;
181                    continue;
182                }
183
184                // \t and space: spaces frequently come in groups, so use a
185                // tight inner loop to skip.
186                b'\t' | b' ' => {
187                    loop {
188                        ptr += 1;
189                        if raw[ptr] != b'\t' && raw[ptr] != b' ' {
190                            break;
191                        }
192                    }
193                    continue;
194                }
195
196                // No-break space   is UTF8 encoded as: c2 a0
197                0xc2 => {
198                    if raw[ptr + 1] == 0xa0 {
199                        ptr += 2;
200                        continue;
201                    } else {
202                        // Fall through to the default (unicode-space) handling.
203                        if let Some(next) = directive_unicode_space(raw, ptr) {
204                            ptr = next;
205                            continue;
206                        }
207                        break false;
208                    }
209                }
210
211                // Byte-order mark  is encoded as: ef bb bf
212                0xef => {
213                    if raw[ptr + 1] == 0xbb && raw[ptr + 2] == 0xbf {
214                        ptr += 3;
215                        continue;
216                    } else {
217                        if let Some(next) = directive_unicode_space(raw, ptr) {
218                            ptr = next;
219                            continue;
220                        }
221                        break false;
222                    }
223                }
224
225                b'/' => {
226                    if raw[ptr + 1] == b'/' {
227                        // Line comment? It implies a new line, so we are good.
228                        break true;
229                    } else if raw[ptr + 1] == b'*' {
230                        // Block comment. Consume it (with messages suppressed
231                        // and comment storage saved/restored) and continue.
232                        let saved_comment_len = self.comment_storage.len();
233                        let saved_suppressed = self.sm.suppressed_messages();
234                        self.sm
235                            .set_suppressed_messages(Some(Subsystem::Unspecified));
236                        // Drive `skip_block_comment` from `ptr`; it mutates the
237                        // cursor (and may set the newline flag), so seek there
238                        // first and read the new offset back into `ptr`.
239                        self.cursor.seek(ptr as u32);
240                        self.skip_block_comment();
241                        ptr = self.cursor.offset() as usize;
242                        self.sm.set_suppressed_messages(saved_suppressed);
243                        if self.store_comments {
244                            self.comment_storage.truncate(saved_comment_len);
245                        }
246                        // Re-borrow `raw` after the mutable calls above by
247                        // looping; the buffer bytes are stable.
248                        continue;
249                    } else {
250                        break false;
251                    }
252                }
253
254                // Handle all other characters: if it is a unicode space, skip
255                // it. Otherwise we have failed.
256                _ => {
257                    if let Some(next) = directive_unicode_space(raw, ptr) {
258                        ptr = next;
259                        continue;
260                    }
261                    break false;
262                }
263            }
264        };
265
266        // Restore the cursor and the newline flag so the caller is unaffected.
267        self.cursor.seek(saved_offset);
268        self.new_line_before_current_token = saved_newline;
269        result
270    }
271
272    /// Rescan the `}` token as a TemplateMiddle or TemplateTail. Should be
273    /// called in the middle of parsing a template literal. Port of
274    /// `rescanRBraceInTemplateLiteral` (JSLexer.cpp:1023-1035).
275    pub fn rescan_rbrace_in_template_literal(&mut self) -> &crate::token::Token {
276        debug_assert!(
277            self.token.kind() == TokenKind::r_brace,
278            "need }} to rescan"
279        );
280        // Back the cursor up one, to the `}`.
281        let back = self.cursor.offset() - 1;
282        self.cursor.seek(back);
283        // Undo the storage for the '}'.
284        if self.store_tokens {
285            self.token_storage.pop();
286        }
287        debug_assert!(
288            self.cursor.peek() == b'}',
289            "non-}} was scanned as r_brace"
290        );
291        // Set the token start to the `}` and scan the `}`-start template path.
292        let start = self.cur_loc();
293        self.token.set_start(start);
294        self.scan_template_literal();
295        self.finish_token();
296        &self.token
297    }
298}
299
300/// Store state of the lexer and allow rescanning from that point. Port of the
301/// C++ `JSLexer::SavePoint` (JSLexer.h:751-821).
302///
303/// DEVIATION: the C++ `SavePoint` holds a `JSLexer *` and exposes `restore()`.
304/// In Rust a save point cannot hold a `&mut JSLexer` across an intervening
305/// `advance` (which also borrows the lexer mutably), so it is a plain value
306/// snapshot and `restore` takes `&mut JSLexer`.
307pub struct SavePoint {
308    /// Saved token kind: a punctuator, `identifier`, or `rw_extends`.
309    kind: TokenKind,
310
311    /// Saved identifier, None if `kind != identifier`.
312    ident: Option<AtomBytes>,
313
314    /// Saved cursor location (port of `loc_`, i.e. `curCharPtr_`).
315    loc: SMLoc,
316
317    /// Saved token range from the lexer.
318    range: SMRange,
319
320    /// Saved previous token end location from the lexer.
321    prev_token_end: SMLoc,
322
323    /// Saved size of comment storage within the lexer. If we restore this save
324    /// point, comments past this index should be removed from the lexer.
325    comment_storage_size: usize,
326
327    /// Stored token storage size. If we backtrack, we must also delete the
328    /// previously stored tokens.
329    token_storage_size: usize,
330}
331
332impl SavePoint {
333    /// Restore the state of `lexer` to the originally saved state. Port of
334    /// `JSLexer::SavePoint::restore` (JSLexer.h:797-820).
335    pub fn restore(self, lexer: &mut JSLexer) {
336        if self.kind == TokenKind::identifier {
337            lexer.unsafe_set_identifier(self.ident.unwrap(), self.loc, self.range);
338        } else if self.kind == TokenKind::rw_extends {
339            lexer.unsafe_set_reserved_word(self.kind, self.loc, self.range);
340        } else {
341            lexer.unsafe_set_punctuator(self.kind, self.loc, self.range);
342        }
343
344        lexer.prev_token_end = self.prev_token_end;
345
346        // Deliberately mirror C++: tokens are gated on `getStoreTokens()`
347        // while comments are gated on the `storeComments_` field directly.
348        if lexer.store_comments
349            && self.comment_storage_size < lexer.comment_storage.len()
350        {
351            lexer.comment_storage.truncate(self.comment_storage_size);
352        }
353
354        if lexer.get_store_tokens() {
355            lexer.token_storage.truncate(self.token_storage_size);
356        }
357    }
358}
359
360/// If the byte sequence at `raw[ptr..]` begins a unicode-only space, return the
361/// offset just past it; otherwise `None`. Mirrors the `default` arm of
362/// `isCurrentTokenADirective` (JSLexer.cpp:1008-1018): only multi-byte UTF-8
363/// starts are considered (ASCII spaces are handled by their own arms).
364fn directive_unicode_space(raw: &[u8], ptr: usize) -> Option<usize> {
365    if is_utf8_start(raw[ptr]) {
366        let mut i = ptr;
367        let cp = crate::utf8::decode_utf8::<false>(raw, &mut i, |_| {});
368        if is_unicode_only_space(cp) {
369            return Some(i);
370        }
371    }
372    None
373}