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