hermes_parser/cursor.rs
1//! The lexer's scan cursor. This is the one place the port uses `unsafe`
2//! (decision "B"): a raw `*const u8` cursor over the source buffer for parity
3//! with the C++ lexer's pointer arithmetic. The buffer is held as an
4//! `Rc<SourceBuffer>` (stable heap address; kept alive for the cursor's life),
5//! and every public method converts to/from a byte offset, so nothing `unsafe`
6//! escapes this module. The buffer is NUL-terminated, so `peek_at` one past the
7//! last real byte reads the terminating 0 (in-bounds).
8//!
9//! # Safety invariants
10//! - `start`, `cur`, and `end` all point into the single contiguous allocation
11//! owned by `buffer` (the `Rc<SourceBuffer>` keeps it alive for `self`'s life,
12//! and `SourceBuffer`'s storage is a `Vec<u8>` whose data pointer is stable
13//! while the buffer is alive — it is never mutated).
14//! - `start` is the first byte, `end` points at the trailing NUL (index
15//! `bytes().len()`), so `[start, end]` is in-bounds and `end` itself is a
16//! valid, readable byte (the NUL).
17//! - Byte offsets are `u32` (matching the front end's `SMLoc`), so source
18//! buffers are assumed to be smaller than 4 GiB.
19//! - `cur` is always kept within `[start, end]` by the public methods. The C++
20//! lexer dereferences `*curCharPtr_` at `end` (reading the NUL) and uses
21//! bounded lookahead (`curCharPtr_[1]`, ...) only after seeing a non-NUL byte,
22//! which is also in-bounds because of the terminator. We preserve that
23//! contract: `peek_at(n)` is only used by callers respecting the same
24//! lookahead invariant.
25#![allow(unsafe_code)]
26
27use hermes_support::buffer::SourceBuffer;
28use std::rc::Rc;
29
30/// A raw-pointer scan cursor over a NUL-terminated source buffer.
31pub struct Cursor {
32 buffer: Rc<SourceBuffer>,
33 start: *const u8,
34 cur: *const u8,
35 /// Points at the terminating NUL (index `bytes().len()`).
36 end: *const u8,
37}
38
39impl Cursor {
40 /// Create a cursor positioned at the start of `buffer`.
41 pub fn new(buffer: Rc<SourceBuffer>) -> Cursor {
42 // `bytes()` excludes the NUL; `raw()` includes it. We need the NUL to be
43 // addressable, so base pointers on the NUL-terminated storage.
44 let raw = buffer.raw(); // includes trailing NUL
45 let n = buffer.bytes().len(); // logical length (without NUL)
46 let start = raw.as_ptr();
47 // SAFETY: `raw` is a contiguous slice of length `n + 1` with a trailing
48 // NUL at index `n`, so `start.add(n)` is in-bounds (one-past-the-last
49 // real byte = the NUL byte, which is itself readable).
50 let end = unsafe { start.add(n) };
51 Cursor {
52 buffer,
53 start,
54 cur: start,
55 end,
56 }
57 }
58
59 /// The current byte offset from the start of the buffer.
60 #[inline]
61 pub fn offset(&self) -> u32 {
62 // SAFETY: `cur` is within `[start, end]` (same allocation), so the
63 // distance is a valid, non-negative `isize` that fits in `u32`.
64 (unsafe { self.cur.offset_from(self.start) }) as u32
65 }
66
67 /// True once the cursor has reached the terminating NUL.
68 #[inline]
69 pub fn at_end(&self) -> bool {
70 self.cur >= self.end
71 }
72
73 /// Byte at the cursor (or the NUL terminator at end).
74 #[inline]
75 pub fn peek(&self) -> u8 {
76 // SAFETY: `cur` is within `[start, end]`, and `end` (the NUL) is
77 // readable, so the dereference is in-bounds.
78 unsafe { *self.cur }
79 }
80
81 /// Byte `n` ahead of the cursor. Only valid while the bytes in between were
82 /// non-NUL (the C++ lookahead invariant); reading the terminating NUL is
83 /// always in-bounds.
84 #[inline]
85 pub fn peek_at(&self, n: usize) -> u8 {
86 // SAFETY: callers respect the lexer lookahead invariant: `peek_at(n)` is
87 // only used after the preceding bytes were observed non-NUL, so
88 // `cur.add(n)` stays within `[start, end]` and is readable.
89 unsafe { *self.cur.add(n) }
90 }
91
92 /// Advance the cursor by `n` bytes.
93 #[inline]
94 pub fn advance(&mut self, n: usize) {
95 // SAFETY: callers only advance within the buffer (bounded by the NUL
96 // terminator the lexer stops at), keeping `cur` within `[start, end]`.
97 unsafe {
98 self.cur = self.cur.add(n);
99 }
100 }
101
102 /// Seek to an absolute byte offset.
103 #[inline]
104 pub fn seek(&mut self, offset: u32) {
105 // SAFETY: `offset` is a valid byte offset into the buffer (<= the NUL's
106 // index), so `start.add(offset)` is within `[start, end]`.
107 unsafe {
108 self.cur = self.start.add(offset as usize);
109 }
110 }
111
112 /// Move the cursor to EOF (the terminating NUL). Port of `forceEOF`.
113 #[inline]
114 pub fn seek_end(&mut self) {
115 self.cur = self.end;
116 }
117
118 /// Bytes in `[from_offset, current offset)`.
119 #[inline]
120 pub fn slice_from(&self, from_offset: u32) -> &[u8] {
121 &self.buffer.raw()[from_offset as usize..self.offset() as usize]
122 }
123
124 /// Bytes in `[from_offset, to_offset)`.
125 #[inline]
126 pub fn slice(&self, from_offset: u32, to_offset: u32) -> &[u8] {
127 &self.buffer.raw()[from_offset as usize..to_offset as usize]
128 }
129
130 /// The NUL-terminated raw bytes of the underlying buffer.
131 #[inline]
132 pub fn raw(&self) -> &[u8] {
133 self.buffer.raw()
134 }
135
136 /// Decode the (non-ASCII) UTF-8 char at the cursor WITHOUT advancing.
137 /// Port of `JSLexer::_peekUTF8` (JSLexer.h:1159-1167): it decodes with
138 /// surrogates disallowed and swallows any errors. Returns
139 /// `(code_point, offset_after)`, where `offset_after` is the byte offset of
140 /// the next character.
141 ///
142 /// This stays in `cursor.rs` to keep the raw-pointer parity confined here,
143 /// but it actually drives the safe `utf8::decode_utf8` over `raw()` at a
144 /// copied byte offset (no new `unsafe`).
145 pub fn peek_utf8(&self) -> (u32, u32) {
146 let bytes = self.raw();
147 let mut i = self.offset() as usize;
148 let cp = crate::utf8::decode_utf8::<false>(bytes, &mut i, |_| {});
149 (cp, i as u32)
150 }
151
152 /// The underlying buffer (cloning the `Rc` is cheap).
153 pub fn buffer(&self) -> &Rc<SourceBuffer> {
154 &self.buffer
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use hermes_support::buffer::SourceBuffer;
162 use std::rc::Rc;
163
164 fn cur(s: &str) -> Cursor {
165 Cursor::new(Rc::new(SourceBuffer::from_str("t", s)))
166 }
167
168 #[test]
169 fn basic() {
170 let mut c = cur("ab");
171 assert_eq!(c.offset(), 0);
172 assert_eq!(c.peek(), b'a');
173 assert_eq!(c.peek_at(1), b'b');
174 assert_eq!(c.peek_at(2), 0); // NUL terminator (in-bounds, always present)
175 assert!(!c.at_end());
176 c.advance(1);
177 assert_eq!(c.offset(), 1);
178 assert_eq!(c.peek(), b'b');
179 c.advance(1);
180 assert_eq!(c.peek(), 0);
181 assert!(c.at_end());
182 }
183
184 #[test]
185 fn peek_utf8_no_advance() {
186 let c = cur("\u{4e2d}x"); // 中 = e4 b8 ad
187 let (cp, next) = c.peek_utf8();
188 assert_eq!(cp, 0x4E2D);
189 assert_eq!(next, 3); // offset of 'x'
190 assert_eq!(c.offset(), 0); // cursor did not move
191 }
192
193 #[test]
194 fn slicing_and_seek() {
195 let mut c = cur("hello");
196 c.advance(2);
197 assert_eq!(c.slice_from(0), b"he"); // bytes [0, offset)
198 c.seek(4);
199 assert_eq!(c.offset(), 4);
200 assert_eq!(c.peek(), b'o');
201 }
202}