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}