1use std::error::Error;
4use std::fmt;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub struct SourceOffset(u32);
9
10impl SourceOffset {
11 pub fn try_from_usize(value: usize) -> Result<Self, DebugMetadataError> {
13 u32::try_from(value)
14 .map(Self)
15 .map_err(|_| DebugMetadataError::SourceTooLarge)
16 }
17
18 #[must_use]
20 pub const fn get(self) -> u32 {
21 self.0
22 }
23
24 #[must_use]
26 pub const fn as_usize(self) -> usize {
27 self.0 as usize
28 }
29}
30
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
33pub struct LineColumn {
34 pub line: u32,
35 pub column: u32,
36}
37
38impl LineColumn {
39 #[must_use]
40 pub const fn new(line: u32, column: u32) -> Self {
41 Self { line, column }
42 }
43
44 #[must_use]
46 pub const fn one_based(self) -> Option<(u32, u32)> {
47 match (self.line.checked_add(1), self.column.checked_add(1)) {
48 (Some(line), Some(column)) => Some((line, column)),
49 _ => None,
50 }
51 }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum DebugMetadataError {
57 SourceTooLarge,
58 OffsetOutOfBounds,
59 OffsetNotUtf8Boundary,
60 LineOrColumnOverflow,
61}
62
63impl fmt::Display for DebugMetadataError {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 f.write_str(match self {
66 Self::SourceTooLarge => "source is too large for QuickJS debug metadata",
67 Self::OffsetOutOfBounds => "source offset is out of bounds",
68 Self::OffsetNotUtf8Boundary => "source offset is not a UTF-8 boundary",
69 Self::LineOrColumnOverflow => "QuickJS debug line or column overflowed",
70 })
71 }
72}
73
74impl Error for DebugMetadataError {}
75
76#[derive(Clone, Copy, Debug)]
81pub struct QuickJsSourceLocator<'source> {
82 source: &'source [u8],
83 utf8: Option<&'source str>,
84}
85
86impl<'source> QuickJsSourceLocator<'source> {
87 #[must_use]
88 pub const fn new(source: &'source str) -> Self {
89 Self {
90 source: source.as_bytes(),
91 utf8: Some(source),
92 }
93 }
94
95 #[must_use]
96 pub const fn from_bytes(source: &'source [u8]) -> Self {
97 Self { source, utf8: None }
98 }
99
100 pub fn locate(self, offset: SourceOffset) -> Result<LineColumn, DebugMetadataError> {
101 self.locate_byte_offset(offset.as_usize())
102 }
103
104 pub fn locate_byte_offset(self, byte_offset: usize) -> Result<LineColumn, DebugMetadataError> {
105 if byte_offset > self.source.len() {
106 return Err(DebugMetadataError::OffsetOutOfBounds);
107 }
108 if self
109 .utf8
110 .is_some_and(|source| !source.is_char_boundary(byte_offset))
111 {
112 return Err(DebugMetadataError::OffsetNotUtf8Boundary);
113 }
114
115 advance_position(LineColumn::default(), &self.source[..byte_offset])
116 }
117
118 pub fn index(self) -> Result<QuickJsSourceIndex<'source>, DebugMetadataError> {
121 let mut checkpoints = Vec::with_capacity(self.source.len() / SOURCE_CHECKPOINT_BYTES + 1);
122 let mut position = LineColumn::default();
123 checkpoints.push(position);
124 for chunk in self.source.as_chunks::<SOURCE_CHECKPOINT_BYTES>().0 {
125 position = advance_position(position, chunk)?;
126 checkpoints.push(position);
127 }
128 Ok(QuickJsSourceIndex {
129 locator: self,
130 checkpoints,
131 })
132 }
133}
134
135const SOURCE_CHECKPOINT_BYTES: usize = 256;
136
137#[derive(Debug)]
142pub struct QuickJsSourceIndex<'source> {
143 locator: QuickJsSourceLocator<'source>,
144 checkpoints: Vec<LineColumn>,
145}
146
147impl<'source> QuickJsSourceIndex<'source> {
148 pub fn locate(&self, offset: SourceOffset) -> Result<LineColumn, DebugMetadataError> {
149 self.locate_byte_offset(offset.as_usize())
150 }
151
152 pub fn locate_byte_offset(&self, byte_offset: usize) -> Result<LineColumn, DebugMetadataError> {
153 self.validate_byte_offset(byte_offset)?;
154 self.locate_validated_byte_offset(byte_offset)
155 }
156
157 pub(crate) fn cursor(&self) -> QuickJsSourceCursor<'_, 'source> {
159 QuickJsSourceCursor {
160 index: self,
161 previous: None,
162 }
163 }
164
165 pub(crate) fn locate_all(
170 &self,
171 offsets: impl IntoIterator<Item = SourceOffset>,
172 ) -> QuickJsLocatedOffsets<'_, 'source> {
173 let len = self.locator.source.len();
174 let mut words = vec![0_u64; len / 64 + 1];
175 for offset in offsets {
176 let offset = offset.as_usize();
177 if offset <= len {
178 words[offset / 64] |= 1 << (offset % 64);
179 }
180 }
181 let mut cursor = self.cursor();
182 let mut ranks = Vec::with_capacity(words.len());
183 let mut positions = Vec::new();
184 let mut errors = Vec::new();
185 for (word_index, &word) in words.iter().enumerate() {
186 ranks.push(positions.len());
187 let mut bits = word;
188 while bits != 0 {
189 let offset = word_index * 64 + bits.trailing_zeros() as usize;
190 bits &= bits - 1;
191 let at = positions.len();
192 positions.push(cursor.locate_byte_offset(offset).unwrap_or_else(|error| {
193 errors.push((at, error));
194 LineColumn::default()
195 }));
196 }
197 }
198 QuickJsLocatedOffsets {
199 index: self,
200 words,
201 ranks,
202 positions,
203 errors,
204 }
205 }
206
207 fn validate_byte_offset(&self, byte_offset: usize) -> Result<(), DebugMetadataError> {
208 if byte_offset > self.locator.source.len() {
209 return Err(DebugMetadataError::OffsetOutOfBounds);
210 }
211 if self
212 .locator
213 .utf8
214 .is_some_and(|source| !source.is_char_boundary(byte_offset))
215 {
216 return Err(DebugMetadataError::OffsetNotUtf8Boundary);
217 }
218 Ok(())
219 }
220
221 fn locate_validated_byte_offset(
222 &self,
223 byte_offset: usize,
224 ) -> Result<LineColumn, DebugMetadataError> {
225 let checkpoint = byte_offset / SOURCE_CHECKPOINT_BYTES;
226 advance_position(
227 self.checkpoints[checkpoint],
228 &self.locator.source[checkpoint * SOURCE_CHECKPOINT_BYTES..byte_offset],
229 )
230 }
231}
232
233pub(crate) struct QuickJsSourceCursor<'index, 'source> {
238 index: &'index QuickJsSourceIndex<'source>,
239 previous: Option<(usize, LineColumn)>,
240}
241
242impl QuickJsSourceCursor<'_, '_> {
243 fn locate_byte_offset(&mut self, byte_offset: usize) -> Result<LineColumn, DebugMetadataError> {
244 self.index.validate_byte_offset(byte_offset)?;
245 let position = match self.previous {
246 Some((previous_offset, position))
247 if previous_offset <= byte_offset
248 && previous_offset / SOURCE_CHECKPOINT_BYTES
249 == byte_offset / SOURCE_CHECKPOINT_BYTES =>
250 {
251 advance_position(
252 position,
253 &self.index.locator.source[previous_offset..byte_offset],
254 )?
255 }
256 _ => self.index.locate_validated_byte_offset(byte_offset)?,
257 };
258 self.previous = Some((byte_offset, position));
259 Ok(position)
260 }
261}
262
263pub(crate) struct QuickJsLocatedOffsets<'index, 'source> {
266 index: &'index QuickJsSourceIndex<'source>,
267 words: Vec<u64>,
269 ranks: Vec<usize>,
271 positions: Vec<LineColumn>,
273 errors: Vec<(usize, DebugMetadataError)>,
275}
276
277impl QuickJsLocatedOffsets<'_, '_> {
278 pub(crate) fn locate(&self, offset: SourceOffset) -> Result<LineColumn, DebugMetadataError> {
279 let byte_offset = offset.as_usize();
280 let bit = 1_u64 << (byte_offset % 64);
281 let Some(&word) = self
282 .words
283 .get(byte_offset / 64)
284 .filter(|&&word| word & bit != 0)
285 else {
286 return self.index.locate(offset);
287 };
288 let at = self.ranks[byte_offset / 64] + (word & (bit - 1)).count_ones() as usize;
289 match self.errors.binary_search_by_key(&at, |&(at, _)| at) {
290 Ok(error) => Err(self.errors[error].1),
291 Err(_) => Ok(self.positions[at]),
292 }
293 }
294}
295
296fn advance_position(
298 mut position: LineColumn,
299 bytes: &[u8],
300) -> Result<LineColumn, DebugMetadataError> {
301 for &byte in bytes {
302 if byte == b'\n' {
303 position.line = position
304 .line
305 .checked_add(1)
306 .ok_or(DebugMetadataError::LineOrColumnOverflow)?;
307 position.column = 0;
308 } else if byte & 0xc0 != 0x80 {
309 position.column = position
310 .column
311 .checked_add(1)
312 .ok_or(DebugMetadataError::LineOrColumnOverflow)?;
313 }
314 }
315 Ok(position)
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn indexed_coordinates_match_scans_at_every_raw_byte_and_utf8_boundary() {
324 let raw = [b'a', 0x80, 0xff, b'\r', b'\n', 0xe2, 0x80, 0xa8].repeat(100);
325 let locator = QuickJsSourceLocator::from_bytes(&raw);
326 let indexed = locator.index().unwrap();
327 for offset in (0..=raw.len() + 1).rev() {
328 assert_eq!(
329 indexed.locate_byte_offset(offset),
330 locator.locate_byte_offset(offset)
331 );
332 }
333 let text = ("é\r\n𝄞\u{2028}".to_owned() + &"x".repeat(600)).repeat(3);
334 let locator = QuickJsSourceLocator::new(&text);
335 let indexed = locator.index().unwrap();
336 for offset in 0..=text.len() + 1 {
337 assert_eq!(
338 indexed.locate_byte_offset(offset),
339 locator.locate_byte_offset(offset)
340 );
341 }
342 let empty = QuickJsSourceLocator::from_bytes(b"").index().unwrap();
343 assert_eq!(empty.locate_byte_offset(0), Ok(LineColumn::new(0, 0)));
344 }
345 #[test]
346 fn cursor_matches_index_and_scan_for_forward_backward_and_invalid_offsets() {
347 let raw = [b'a', 0x80, 0xff, b'\r', b'\n', 0xe2, 0x80, 0xa8].repeat(100);
348 let text = ("é\r\n𝄞\u{2028}".to_owned() + &"x".repeat(600)).repeat(3);
349 for locator in [
350 QuickJsSourceLocator::from_bytes(&raw),
351 QuickJsSourceLocator::new(&text),
352 QuickJsSourceLocator::from_bytes(b""),
353 ] {
354 let index = locator.index().unwrap();
355 let mut cursor = index.cursor();
356 let length = locator.source.len();
357 let offsets = (0..=length + 1)
358 .chain((0..=length + 1).rev())
359 .chain((0..=length).flat_map(|offset| {
360 [offset, usize::MAX, offset, length.saturating_sub(offset)]
361 }));
362 for offset in offsets {
363 let previous = cursor.previous;
364 let expected = index.locate_byte_offset(offset);
365 assert_eq!(expected, locator.locate_byte_offset(offset));
366 assert_eq!(
367 cursor.locate_byte_offset(offset),
368 expected,
369 "offset={offset}"
370 );
371 if expected.is_err() {
372 assert_eq!(cursor.previous, previous);
373 }
374 }
375 }
376 }
377
378 #[test]
379 fn cursor_preserves_line_and_column_overflow_and_last_success() {
380 for (bytes, position) in [
383 (&b"\nx"[..], LineColumn::new(u32::MAX, 0)),
384 (&b"x\n"[..], LineColumn::new(0, u32::MAX)),
385 ] {
386 let index = QuickJsSourceIndex {
387 locator: QuickJsSourceLocator::from_bytes(bytes),
388 checkpoints: vec![position],
389 };
390 let mut cursor = index.cursor();
391 assert_eq!(cursor.locate_byte_offset(0), Ok(position));
392 for offset in [1, 2, 0, usize::MAX, 1] {
393 let previous = cursor.previous;
394 let expected = index.locate_byte_offset(offset);
395 assert_eq!(cursor.locate_byte_offset(offset), expected);
396 if expected.is_err() {
397 assert_eq!(cursor.previous, previous);
398 }
399 }
400 }
401 }
402}