Skip to main content

quickjs_oxide/engine/code/
debug.rs

1//! Typed source locations and bytecode-to-source metadata.
2//!
3//! QuickJS deliberately does not use Unicode-scalar columns for its debug
4//! tables.  Only LF advances the line number and every UTF-8 lead byte advances
5//! the column.  Keeping that rule in one locator prevents the lexer diagnostic
6//! coordinates (which have different ECMAScript line-terminator semantics)
7//! from leaking into runtime debug metadata.
8
9use crate::source::LineColumn;
10
11/// Runtime-wide policy for the currently represented, JS-observable portion
12/// of QuickJS `JS_SetStripInfo`.
13///
14/// The selected mode is sampled when a function is compiled. Existing
15/// bytecode keeps the debug payload it was published with.
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
17pub enum DebugInfoMode {
18    /// Keep filename, PC positions and exact authored function source.
19    #[default]
20    Full,
21    /// Keep filename and PC positions but omit authored function source.
22    StripSource,
23    /// Omit function source and location metadata from the bytecode payload.
24    StripDebug,
25}
26
27/// One absolute typed entry in a bytecode PC-to-source table.
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
29pub struct Pc2LineEntry {
30    pub pc: u32,
31    pub position: LineColumn,
32}
33
34/// Debug position of a function definition and marked bytecode instructions.
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct Pc2LineTable {
37    pub definition: LineColumn,
38    pub entries: Box<[Pc2LineEntry]>,
39}
40
41impl Pc2LineTable {
42    #[must_use]
43    pub fn new(definition: LineColumn, entries: impl Into<Box<[Pc2LineEntry]>>) -> Self {
44        Self {
45            definition,
46            entries: entries.into(),
47        }
48    }
49
50    /// Match QuickJS `find_line_num`: the last entry whose PC is no greater
51    /// than the queried PC wins. A missing current PC uses the definition site.
52    #[cfg(test)]
53    #[must_use]
54    pub fn lookup(&self, pc: Option<u32>) -> LineColumn {
55        let Some(pc) = pc else {
56            return self.definition;
57        };
58        self.entries
59            .iter()
60            .take_while(|entry| entry.pc <= pc)
61            .last()
62            .map_or(self.definition, |entry| entry.position)
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::{LineColumn, Pc2LineEntry, Pc2LineTable};
69    use crate::source::DebugMetadataError;
70    use crate::source::{QuickJsSourceLocator, SourceOffset};
71
72    #[test]
73    fn quickjs_locator_only_treats_lf_as_a_newline() {
74        let source = "a\rb\r\nc\u{2028}d\u{2029}e\nf";
75        let locator = QuickJsSourceLocator::new(source);
76
77        assert_eq!(locator.locate_byte_offset(0), Ok(LineColumn::new(0, 0)));
78        assert_eq!(
79            locator.locate_byte_offset(source.find('b').unwrap()),
80            Ok(LineColumn::new(0, 2))
81        );
82        assert_eq!(
83            locator.locate_byte_offset(source.find('c').unwrap()),
84            Ok(LineColumn::new(1, 0))
85        );
86        assert_eq!(
87            locator.locate_byte_offset(source.find('d').unwrap()),
88            Ok(LineColumn::new(1, 2))
89        );
90        assert_eq!(
91            locator.locate_byte_offset(source.find('e').unwrap()),
92            Ok(LineColumn::new(1, 4))
93        );
94        assert_eq!(
95            locator.locate_byte_offset(source.find('f').unwrap()),
96            Ok(LineColumn::new(2, 0))
97        );
98    }
99
100    #[test]
101    fn quickjs_locator_counts_utf8_lead_bytes_not_raw_bytes() {
102        let source = "é中x";
103        let locator = QuickJsSourceLocator::new(source);
104        assert_eq!(
105            locator.locate_byte_offset(source.find('x').unwrap()),
106            Ok(LineColumn::new(0, 2))
107        );
108        assert_eq!(
109            locator.locate_byte_offset(1),
110            Err(DebugMetadataError::OffsetNotUtf8Boundary)
111        );
112    }
113
114    #[test]
115    fn quickjs_raw_locator_preserves_cesu8_and_malformed_byte_columns() {
116        let canonical = QuickJsSourceLocator::from_bytes(b"/*\xf0\x9f\x98\x80*/@");
117        let cesu8 = QuickJsSourceLocator::from_bytes(b"/*\xed\xa0\xbd\xed\xb8\x80*/@");
118        let continuation = QuickJsSourceLocator::from_bytes(b"/*\x80*/@");
119        let invalid_lead = QuickJsSourceLocator::from_bytes(b"/*\xff*/@");
120
121        assert_eq!(canonical.locate_byte_offset(8), Ok(LineColumn::new(0, 5)));
122        assert_eq!(cesu8.locate_byte_offset(10), Ok(LineColumn::new(0, 6)));
123        assert_eq!(
124            continuation.locate_byte_offset(5),
125            Ok(LineColumn::new(0, 4))
126        );
127        assert_eq!(
128            invalid_lead.locate_byte_offset(5),
129            Ok(LineColumn::new(0, 5))
130        );
131    }
132
133    #[test]
134    fn pc_lookup_uses_the_last_entry_at_or_before_the_pc() {
135        let table = Pc2LineTable::new(
136            LineColumn::new(1, 2),
137            vec![
138                Pc2LineEntry {
139                    pc: 1,
140                    position: LineColumn::new(3, 4),
141                },
142                Pc2LineEntry {
143                    pc: 5,
144                    position: LineColumn::new(6, 7),
145                },
146            ],
147        );
148        assert_eq!(table.lookup(None), LineColumn::new(1, 2));
149        assert_eq!(table.lookup(Some(0)), LineColumn::new(1, 2));
150        assert_eq!(table.lookup(Some(4)), LineColumn::new(3, 4));
151        assert_eq!(table.lookup(Some(5)), LineColumn::new(6, 7));
152        assert_eq!(SourceOffset::try_from_usize(7).unwrap().get(), 7);
153    }
154}