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