Skip to main content

vm/
debug_info.rs

1#[derive(Clone, Debug, PartialEq, Eq)]
2pub struct ArgInfo {
3    pub name: String,
4    pub position: u8,
5}
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct DebugFunction {
9    pub name: String,
10    pub args: Vec<ArgInfo>,
11}
12
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct LocalInfo {
15    pub name: String,
16    pub index: u8,
17    pub declared_line: Option<u32>,
18    pub last_line: Option<u32>,
19}
20
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct LineInfo {
23    pub offset: u32,
24    pub line: u32,
25}
26
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct DebugInfo {
29    pub source: Option<String>,
30    pub lines: Vec<LineInfo>,
31    pub functions: Vec<DebugFunction>,
32    pub locals: Vec<LocalInfo>,
33}
34
35impl DebugInfo {
36    pub fn line_for_offset(&self, offset: usize) -> Option<u32> {
37        let offset = offset as u32;
38        if self.lines.is_empty() {
39            return None;
40        }
41        let mut lo = 0;
42        let mut hi = self.lines.len();
43        while lo < hi {
44            let mid = (lo + hi) / 2;
45            if self.lines[mid].offset <= offset {
46                lo = mid + 1;
47            } else {
48                hi = mid;
49            }
50        }
51        if lo == 0 {
52            None
53        } else {
54            Some(self.lines[lo - 1].line)
55        }
56    }
57
58    pub fn offsets_for_line(&self, line: u32) -> Vec<u32> {
59        self.lines
60            .iter()
61            .filter(|info| info.line == line)
62            .map(|info| info.offset)
63            .collect()
64    }
65
66    pub fn source_line(&self, line: u32) -> Option<String> {
67        let source = self.source.as_ref()?;
68        let index = line.checked_sub(1)? as usize;
69        source.lines().nth(index).map(|text| text.to_string())
70    }
71
72    pub fn local_index(&self, name: &str) -> Option<u8> {
73        self.locals
74            .iter()
75            .find(|local| local.name == name)
76            .map(|local| local.index)
77    }
78}
79
80#[derive(Default)]
81pub struct DebugInfoBuilder {
82    source: Option<String>,
83    lines: Vec<LineInfo>,
84    functions: Vec<DebugFunction>,
85    locals: Vec<LocalInfo>,
86    last_offset: Option<u32>,
87}
88
89impl DebugInfoBuilder {
90    pub fn new() -> Self {
91        Self::default()
92    }
93
94    pub fn set_source(&mut self, source: String) {
95        self.source = Some(source);
96    }
97
98    pub fn add_function(&mut self, name: String, args: Vec<String>) {
99        let args = args
100            .into_iter()
101            .enumerate()
102            .map(|(idx, name)| ArgInfo {
103                name,
104                position: idx as u8,
105            })
106            .collect();
107        self.functions.push(DebugFunction { name, args });
108    }
109
110    pub fn add_local(&mut self, name: String, index: u8) {
111        self.add_local_with_range(name, index, None, None);
112    }
113
114    pub fn add_local_with_range(
115        &mut self,
116        name: String,
117        index: u8,
118        declared_line: Option<u32>,
119        last_line: Option<u32>,
120    ) {
121        if let Some(existing) = self.locals.iter_mut().find(|local| local.name == name) {
122            existing.index = index;
123            existing.declared_line = merge_min_line(existing.declared_line, declared_line);
124            existing.last_line = merge_max_line(existing.last_line, last_line);
125            return;
126        }
127        self.locals.push(LocalInfo {
128            name,
129            index,
130            declared_line,
131            last_line,
132        });
133    }
134
135    pub fn mark_line(&mut self, offset: u32, line: u32) {
136        if self.last_offset == Some(offset) {
137            if let Some(last) = self.lines.last_mut()
138                && last.offset == offset
139            {
140                // Keep the most recent line mapping for this offset so non-emitting
141                // statements (e.g., import declarations) do not pin stale lines.
142                last.line = line;
143            }
144            return;
145        }
146        self.lines.push(LineInfo { offset, line });
147        self.last_offset = Some(offset);
148    }
149
150    pub fn finish(self) -> Option<DebugInfo> {
151        if self.source.is_none()
152            && self.lines.is_empty()
153            && self.functions.is_empty()
154            && self.locals.is_empty()
155        {
156            return None;
157        }
158        Some(DebugInfo {
159            source: self.source,
160            lines: self.lines,
161            functions: self.functions,
162            locals: self.locals,
163        })
164    }
165}
166
167fn merge_min_line(current: Option<u32>, incoming: Option<u32>) -> Option<u32> {
168    match (current, incoming) {
169        (Some(lhs), Some(rhs)) => Some(lhs.min(rhs)),
170        (Some(lhs), None) => Some(lhs),
171        (None, Some(rhs)) => Some(rhs),
172        (None, None) => None,
173    }
174}
175
176fn merge_max_line(current: Option<u32>, incoming: Option<u32>) -> Option<u32> {
177    match (current, incoming) {
178        (Some(lhs), Some(rhs)) => Some(lhs.max(rhs)),
179        (Some(lhs), None) => Some(lhs),
180        (None, Some(rhs)) => Some(rhs),
181        (None, None) => None,
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::DebugInfoBuilder;
188
189    #[test]
190    fn add_local_with_range_keeps_distinct_names_on_shared_slot() {
191        let mut builder = DebugInfoBuilder::new();
192        builder.add_local_with_range("a".to_string(), 0, Some(1), Some(1));
193        builder.add_local_with_range("b".to_string(), 0, Some(2), Some(2));
194
195        let debug = builder.finish().expect("debug info should be present");
196        assert_eq!(debug.locals.len(), 2);
197        assert_eq!(debug.locals[0].name, "a");
198        assert_eq!(debug.locals[1].name, "b");
199    }
200}