Skip to main content

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}