Skip to main content

srcmap_scopes/
encode.rs

1//! Encoder for the ECMA-426 scopes proposal.
2//!
3//! Encodes structured `ScopeInfo` into a VLQ-encoded `scopes` string.
4
5use std::collections::HashMap;
6
7use srcmap_codec::{vlq_encode, vlq_encode_unsigned};
8
9use crate::{
10    Binding, GeneratedRange, OriginalScope, ScopeInfo, TAG_GENERATED_RANGE_BINDINGS,
11    TAG_GENERATED_RANGE_CALL_SITE, TAG_GENERATED_RANGE_END, TAG_GENERATED_RANGE_START,
12    TAG_GENERATED_RANGE_SUB_RANGE_BINDINGS, TAG_ORIGINAL_SCOPE_END, TAG_ORIGINAL_SCOPE_START,
13    TAG_ORIGINAL_SCOPE_VARIABLES, resolve_or_add_name,
14};
15
16// ── Encoder ──────────────────────────────────────────────────────
17
18struct ScopesEncoder<'a> {
19    output: Vec<u8>,
20    names: &'a mut Vec<String>,
21    name_map: HashMap<String, u32>,
22    first_item: bool,
23
24    // Original scope relative state
25    os_line: u32,
26    os_col: u32,
27    os_name: i64,
28    os_kind: i64,
29    os_var: i64,
30
31    // Generated range relative state
32    gr_line: u32,
33    gr_col: u32,
34    gr_def: i64,
35}
36
37impl<'a> ScopesEncoder<'a> {
38    fn new(names: &'a mut Vec<String>) -> Self {
39        let name_map: HashMap<String, u32> = names
40            .iter()
41            .enumerate()
42            .map(|(i, n)| (n.clone(), i as u32))
43            .collect();
44
45        Self {
46            output: Vec::with_capacity(256),
47            names,
48            name_map,
49            first_item: true,
50            os_line: 0,
51            os_col: 0,
52            os_name: 0,
53            os_kind: 0,
54            os_var: 0,
55            gr_line: 0,
56            gr_col: 0,
57            gr_def: 0,
58        }
59    }
60
61    #[inline]
62    fn emit_comma(&mut self) {
63        if !self.first_item {
64            self.output.push(b',');
65        }
66        self.first_item = false;
67    }
68
69    #[inline]
70    fn emit_tag(&mut self, tag: u64) {
71        vlq_encode_unsigned(&mut self.output, tag);
72    }
73
74    #[inline]
75    fn emit_unsigned(&mut self, value: u64) {
76        vlq_encode_unsigned(&mut self.output, value);
77    }
78
79    #[inline]
80    fn emit_signed(&mut self, value: i64) {
81        vlq_encode(&mut self.output, value);
82    }
83
84    #[inline]
85    fn name_idx(&mut self, name: &str) -> u32 {
86        resolve_or_add_name(name, self.names, &mut self.name_map)
87    }
88
89    fn encode(mut self, info: &ScopeInfo) -> String {
90        // Phase 1: Encode original scope trees
91        for scope in &info.scopes {
92            match scope {
93                Some(s) => {
94                    // Reset position state for new top-level tree
95                    self.os_line = 0;
96                    self.os_col = 0;
97                    self.encode_original_scope(s);
98                }
99                None => {
100                    // Empty item: emit a comma to mark the absent source
101                    self.emit_comma();
102                }
103            }
104        }
105
106        // Phase 2: Encode generated ranges
107        for range in &info.ranges {
108            self.encode_generated_range(range);
109        }
110
111        // SAFETY: vlq_encode/vlq_encode_unsigned only push bytes from
112        // BASE64_ENCODE (all ASCII), and we only add b',' — all valid UTF-8.
113        debug_assert!(self.output.is_ascii());
114        unsafe { String::from_utf8_unchecked(self.output) }
115    }
116
117    fn encode_original_scope(&mut self, scope: &OriginalScope) {
118        // B item: scope start
119        self.emit_comma();
120        self.emit_tag(TAG_ORIGINAL_SCOPE_START);
121
122        let mut flags: u64 = 0;
123        if scope.name.is_some() {
124            flags |= crate::OS_FLAG_HAS_NAME;
125        }
126        if scope.kind.is_some() {
127            flags |= crate::OS_FLAG_HAS_KIND;
128        }
129        if scope.is_stack_frame {
130            flags |= crate::OS_FLAG_IS_STACK_FRAME;
131        }
132        self.emit_unsigned(flags);
133
134        // Line (relative)
135        let line_delta = scope.start.line - self.os_line;
136        self.emit_unsigned(line_delta as u64);
137        self.os_line = scope.start.line;
138
139        // Column (absolute if line changed, relative if same line)
140        let col = if line_delta != 0 {
141            scope.start.column
142        } else {
143            scope.start.column - self.os_col
144        };
145        self.emit_unsigned(col as u64);
146        self.os_col = scope.start.column;
147
148        // Name (signed relative)
149        if let Some(ref name) = scope.name {
150            let idx = self.name_idx(name) as i64;
151            self.emit_signed(idx - self.os_name);
152            self.os_name = idx;
153        }
154
155        // Kind (signed relative)
156        if let Some(ref kind) = scope.kind {
157            let idx = self.name_idx(kind) as i64;
158            self.emit_signed(idx - self.os_kind);
159            self.os_kind = idx;
160        }
161
162        // D item: variables
163        if !scope.variables.is_empty() {
164            self.emit_comma();
165            self.emit_tag(TAG_ORIGINAL_SCOPE_VARIABLES);
166            for var in &scope.variables {
167                let idx = self.name_idx(var) as i64;
168                self.emit_signed(idx - self.os_var);
169                self.os_var = idx;
170            }
171        }
172
173        // Recursively encode children
174        for child in &scope.children {
175            self.encode_original_scope(child);
176        }
177
178        // C item: scope end
179        self.emit_comma();
180        self.emit_tag(TAG_ORIGINAL_SCOPE_END);
181
182        let line_delta = scope.end.line - self.os_line;
183        self.emit_unsigned(line_delta as u64);
184        self.os_line = scope.end.line;
185
186        let col = if line_delta != 0 {
187            scope.end.column
188        } else {
189            scope.end.column - self.os_col
190        };
191        self.emit_unsigned(col as u64);
192        self.os_col = scope.end.column;
193    }
194
195    fn encode_generated_range(&mut self, range: &GeneratedRange) {
196        // E item: range start
197        self.emit_comma();
198        self.emit_tag(TAG_GENERATED_RANGE_START);
199
200        let line_delta = range.start.line - self.gr_line;
201
202        let mut flags: u64 = 0;
203        if line_delta != 0 {
204            flags |= crate::GR_FLAG_HAS_LINE;
205        }
206        if range.definition.is_some() {
207            flags |= crate::GR_FLAG_HAS_DEFINITION;
208        }
209        if range.is_stack_frame {
210            flags |= crate::GR_FLAG_IS_STACK_FRAME;
211        }
212        if range.is_hidden {
213            flags |= crate::GR_FLAG_IS_HIDDEN;
214        }
215        self.emit_unsigned(flags);
216
217        if line_delta != 0 {
218            self.emit_unsigned(line_delta as u64);
219        }
220        self.gr_line = range.start.line;
221
222        let col = if line_delta != 0 {
223            range.start.column
224        } else {
225            range.start.column - self.gr_col
226        };
227        self.emit_unsigned(col as u64);
228        self.gr_col = range.start.column;
229
230        if let Some(def) = range.definition {
231            let def_val = def as i64;
232            self.emit_signed(def_val - self.gr_def);
233            self.gr_def = def_val;
234        }
235
236        // G item: bindings
237        if !range.bindings.is_empty() {
238            self.emit_comma();
239            self.emit_tag(TAG_GENERATED_RANGE_BINDINGS);
240            for binding in &range.bindings {
241                match binding {
242                    Binding::Expression(expr) => {
243                        let idx = self.name_idx(expr);
244                        self.emit_unsigned(idx as u64 + 1); // 1-based
245                    }
246                    Binding::Unavailable => {
247                        self.emit_unsigned(0);
248                    }
249                    Binding::SubRanges(subs) => {
250                        // G gets the first sub-range's binding
251                        if let Some(first) = subs.first() {
252                            match &first.expression {
253                                Some(expr) => {
254                                    let idx = self.name_idx(expr);
255                                    self.emit_unsigned(idx as u64 + 1);
256                                }
257                                None => {
258                                    self.emit_unsigned(0);
259                                }
260                            }
261                        } else {
262                            self.emit_unsigned(0);
263                        }
264                    }
265                }
266            }
267        }
268
269        // H items: sub-range bindings
270        let mut h_var_idx = 0u64;
271        let mut h_first = true;
272        for (i, binding) in range.bindings.iter().enumerate() {
273            if let Binding::SubRanges(subs) = binding
274                && subs.len() > 1
275            {
276                self.emit_comma();
277                self.emit_tag(TAG_GENERATED_RANGE_SUB_RANGE_BINDINGS);
278
279                // Variable index (relative)
280                let var_delta = i as u64 - if h_first { 0 } else { h_var_idx };
281                self.emit_unsigned(var_delta);
282                h_var_idx = i as u64;
283                h_first = false;
284
285                // Sub-range line/col state (relative to range start)
286                let mut h_line = range.start.line;
287                let mut h_col = range.start.column;
288
289                // Skip first sub-range (that's in G), encode the rest
290                for sub in &subs[1..] {
291                    // Binding (1-based absolute)
292                    match &sub.expression {
293                        Some(expr) => {
294                            let idx = self.name_idx(expr);
295                            self.emit_unsigned(idx as u64 + 1);
296                        }
297                        None => {
298                            self.emit_unsigned(0);
299                        }
300                    }
301
302                    let sub_line_delta = sub.from.line - h_line;
303                    self.emit_unsigned(sub_line_delta as u64);
304                    h_line = sub.from.line;
305
306                    let sub_col = if sub_line_delta != 0 {
307                        sub.from.column
308                    } else {
309                        sub.from.column - h_col
310                    };
311                    self.emit_unsigned(sub_col as u64);
312                    h_col = sub.from.column;
313                }
314            }
315        }
316
317        // I item: call site
318        if let Some(ref cs) = range.call_site {
319            self.emit_comma();
320            self.emit_tag(TAG_GENERATED_RANGE_CALL_SITE);
321            self.emit_unsigned(cs.source_index as u64);
322            self.emit_unsigned(cs.line as u64);
323            self.emit_unsigned(cs.column as u64);
324        }
325
326        // Recursively encode children
327        for child in &range.children {
328            self.encode_generated_range(child);
329        }
330
331        // F item: range end
332        self.emit_comma();
333        self.emit_tag(TAG_GENERATED_RANGE_END);
334
335        let line_delta = range.end.line - self.gr_line;
336        if line_delta != 0 {
337            self.emit_unsigned(line_delta as u64);
338        }
339        self.gr_line = range.end.line;
340
341        let col = if line_delta != 0 {
342            range.end.column
343        } else {
344            range.end.column - self.gr_col
345        };
346        self.emit_unsigned(col as u64);
347        self.gr_col = range.end.column;
348    }
349}
350
351/// Encode scope information into a VLQ-encoded `scopes` string.
352///
353/// New names may be added to the `names` array during encoding.
354pub fn encode_scopes(info: &ScopeInfo, names: &mut Vec<String>) -> String {
355    let encoder = ScopesEncoder::new(names);
356    encoder.encode(info)
357}