Skip to main content

srcmap_scopes/
decode.rs

1//! Decoder for the ECMA-426 scopes proposal.
2//!
3//! Parses a VLQ-encoded `scopes` string into structured `ScopeInfo`.
4
5use srcmap_codec::{vlq_decode, vlq_decode_unsigned};
6
7use crate::{
8    Binding, CallSite, GeneratedRange, OriginalScope, Position, ScopeInfo, ScopesError,
9    SubRangeBinding, TAG_GENERATED_RANGE_BINDINGS, TAG_GENERATED_RANGE_CALL_SITE,
10    TAG_GENERATED_RANGE_END, TAG_GENERATED_RANGE_START, TAG_GENERATED_RANGE_SUB_RANGE_BINDINGS,
11    TAG_ORIGINAL_SCOPE_END, TAG_ORIGINAL_SCOPE_START, TAG_ORIGINAL_SCOPE_VARIABLES,
12    resolve_binding, resolve_name,
13};
14
15// ── Tokenizer ────────────────────────────────────────────────────
16
17struct Tokenizer<'a> {
18    input: &'a [u8],
19    pos: usize,
20}
21
22impl<'a> Tokenizer<'a> {
23    fn new(input: &'a [u8]) -> Self {
24        Self { input, pos: 0 }
25    }
26
27    fn has_next(&self) -> bool {
28        self.pos < self.input.len()
29    }
30
31    /// Check if we're at the end of the current item (comma or end of input).
32    fn at_item_end(&self) -> bool {
33        self.pos >= self.input.len() || self.input[self.pos] == b','
34    }
35
36    /// Skip a comma separator (if present).
37    fn skip_comma(&mut self) {
38        if self.pos < self.input.len() && self.input[self.pos] == b',' {
39            self.pos += 1;
40        }
41    }
42
43    fn read_unsigned(&mut self) -> Result<u64, ScopesError> {
44        let (val, consumed) = vlq_decode_unsigned(self.input, self.pos)?;
45        self.pos += consumed;
46        Ok(val)
47    }
48
49    fn read_signed(&mut self) -> Result<i64, ScopesError> {
50        let (val, consumed) = vlq_decode(self.input, self.pos)?;
51        self.pos += consumed;
52        Ok(val)
53    }
54}
55
56// ── Building types ───────────────────────────────────────────────
57
58struct BuildingScope {
59    start: Position,
60    name: Option<String>,
61    kind: Option<String>,
62    is_stack_frame: bool,
63    variables: Vec<String>,
64    children: Vec<OriginalScope>,
65}
66
67struct BuildingRange {
68    start: Position,
69    is_stack_frame: bool,
70    is_hidden: bool,
71    definition: Option<usize>,
72    call_site: Option<CallSite>,
73    bindings: Vec<Binding>,
74    sub_range_bindings: Vec<(usize, Vec<SubRangeBinding>)>,
75    children: Vec<GeneratedRange>,
76}
77
78// ── Decode ───────────────────────────────────────────────────────
79
80/// Decode a `scopes` string into structured scope information.
81///
82/// - `input`: the VLQ-encoded scopes string from the source map
83/// - `names`: the `names` array from the source map (for resolving indices)
84/// - `num_sources`: number of source files (length of `sources` array)
85pub fn decode_scopes(
86    input: &str,
87    names: &[String],
88    num_sources: usize,
89) -> Result<ScopeInfo, ScopesError> {
90    if input.is_empty() {
91        let scopes = vec![None; num_sources];
92        return Ok(ScopeInfo {
93            scopes,
94            ranges: vec![],
95        });
96    }
97
98    let mut tok = Tokenizer::new(input.as_bytes());
99
100    // Original scope state
101    let mut scopes: Vec<Option<OriginalScope>> = Vec::new();
102    let mut source_idx = 0usize;
103    let mut scope_stack: Vec<BuildingScope> = Vec::new();
104    let mut os_line = 0u32;
105    let mut os_col = 0u32;
106    let mut os_name = 0i64;
107    let mut os_kind = 0i64;
108    let mut os_var = 0i64;
109
110    // Generated range state
111    let mut ranges: Vec<GeneratedRange> = Vec::new();
112    let mut range_stack: Vec<BuildingRange> = Vec::new();
113    let mut gr_line = 0u32;
114    let mut gr_col = 0u32;
115    let mut gr_def = 0i64;
116    let mut in_generated_ranges = false;
117
118    while tok.has_next() {
119        // Empty item: no scope info for this source file
120        if tok.at_item_end() {
121            if !in_generated_ranges && source_idx < num_sources && scope_stack.is_empty() {
122                scopes.push(None);
123                source_idx += 1;
124            }
125            tok.skip_comma();
126            continue;
127        }
128
129        let tag = tok.read_unsigned()?;
130
131        match tag {
132            TAG_ORIGINAL_SCOPE_START => {
133                // Reset position state at start of new top-level tree
134                if scope_stack.is_empty() {
135                    os_line = 0;
136                    os_col = 0;
137                }
138
139                let flags = tok.read_unsigned()?;
140
141                let line_delta = tok.read_unsigned()? as u32;
142                os_line += line_delta;
143                let col_raw = tok.read_unsigned()? as u32;
144                os_col = if line_delta != 0 {
145                    col_raw
146                } else {
147                    os_col + col_raw
148                };
149
150                let name = if flags & crate::OS_FLAG_HAS_NAME != 0 {
151                    let d = tok.read_signed()?;
152                    os_name += d;
153                    Some(resolve_name(names, os_name)?)
154                } else {
155                    None
156                };
157
158                let kind = if flags & crate::OS_FLAG_HAS_KIND != 0 {
159                    let d = tok.read_signed()?;
160                    os_kind += d;
161                    Some(resolve_name(names, os_kind)?)
162                } else {
163                    None
164                };
165
166                let is_stack_frame = flags & crate::OS_FLAG_IS_STACK_FRAME != 0;
167
168                scope_stack.push(BuildingScope {
169                    start: Position {
170                        line: os_line,
171                        column: os_col,
172                    },
173                    name,
174                    kind,
175                    is_stack_frame,
176                    variables: Vec::new(),
177                    children: Vec::new(),
178                });
179            }
180
181            TAG_ORIGINAL_SCOPE_END => {
182                if scope_stack.is_empty() {
183                    return Err(ScopesError::UnmatchedScopeEnd);
184                }
185
186                let line_delta = tok.read_unsigned()? as u32;
187                os_line += line_delta;
188                let col_raw = tok.read_unsigned()? as u32;
189                os_col = if line_delta != 0 {
190                    col_raw
191                } else {
192                    os_col + col_raw
193                };
194
195                let building = scope_stack.pop().unwrap();
196                let finished = OriginalScope {
197                    start: building.start,
198                    end: Position {
199                        line: os_line,
200                        column: os_col,
201                    },
202                    name: building.name,
203                    kind: building.kind,
204                    is_stack_frame: building.is_stack_frame,
205                    variables: building.variables,
206                    children: building.children,
207                };
208
209                if scope_stack.is_empty() {
210                    scopes.push(Some(finished));
211                    source_idx += 1;
212                    // Position resets at next tree start (handled above)
213                } else {
214                    scope_stack.last_mut().unwrap().children.push(finished);
215                }
216            }
217
218            TAG_ORIGINAL_SCOPE_VARIABLES => {
219                if let Some(current) = scope_stack.last_mut() {
220                    while !tok.at_item_end() {
221                        let d = tok.read_signed()?;
222                        os_var += d;
223                        current.variables.push(resolve_name(names, os_var)?);
224                    }
225                }
226            }
227
228            TAG_GENERATED_RANGE_START => {
229                if !in_generated_ranges {
230                    in_generated_ranges = true;
231                    // Fill remaining source slots
232                    while scopes.len() < num_sources {
233                        scopes.push(None);
234                    }
235                    source_idx = num_sources;
236                }
237
238                let flags = tok.read_unsigned()?;
239
240                let line_delta = if flags & crate::GR_FLAG_HAS_LINE != 0 {
241                    tok.read_unsigned()? as u32
242                } else {
243                    0
244                };
245                gr_line += line_delta;
246
247                let col_raw = tok.read_unsigned()? as u32;
248                gr_col = if line_delta != 0 {
249                    col_raw
250                } else {
251                    gr_col + col_raw
252                };
253
254                let definition = if flags & crate::GR_FLAG_HAS_DEFINITION != 0 {
255                    let d = tok.read_signed()?;
256                    gr_def += d;
257                    Some(gr_def as usize)
258                } else {
259                    None
260                };
261
262                let is_stack_frame = flags & crate::GR_FLAG_IS_STACK_FRAME != 0;
263                let is_hidden = flags & crate::GR_FLAG_IS_HIDDEN != 0;
264
265                range_stack.push(BuildingRange {
266                    start: Position {
267                        line: gr_line,
268                        column: gr_col,
269                    },
270                    is_stack_frame,
271                    is_hidden,
272                    definition,
273                    call_site: None,
274                    bindings: Vec::new(),
275                    sub_range_bindings: Vec::new(),
276                    children: Vec::new(),
277                });
278            }
279
280            TAG_GENERATED_RANGE_END => {
281                if range_stack.is_empty() {
282                    return Err(ScopesError::UnmatchedRangeEnd);
283                }
284
285                // F tag: 1 VLQ = column only, 2 VLQs = line + column
286                let first = tok.read_unsigned()? as u32;
287                let (line_delta, col_raw) = if !tok.at_item_end() {
288                    let second = tok.read_unsigned()? as u32;
289                    (first, second)
290                } else {
291                    (0, first)
292                };
293                gr_line += line_delta;
294                gr_col = if line_delta != 0 {
295                    col_raw
296                } else {
297                    gr_col + col_raw
298                };
299
300                let building = range_stack.pop().unwrap();
301
302                // Merge sub-range bindings into final bindings
303                let final_bindings = merge_bindings(
304                    building.bindings,
305                    &building.sub_range_bindings,
306                    building.start,
307                );
308
309                let finished = GeneratedRange {
310                    start: building.start,
311                    end: Position {
312                        line: gr_line,
313                        column: gr_col,
314                    },
315                    is_stack_frame: building.is_stack_frame,
316                    is_hidden: building.is_hidden,
317                    definition: building.definition,
318                    call_site: building.call_site,
319                    bindings: final_bindings,
320                    children: building.children,
321                };
322
323                if range_stack.is_empty() {
324                    ranges.push(finished);
325                } else {
326                    range_stack.last_mut().unwrap().children.push(finished);
327                }
328            }
329
330            TAG_GENERATED_RANGE_BINDINGS => {
331                if let Some(current) = range_stack.last_mut() {
332                    while !tok.at_item_end() {
333                        let idx = tok.read_unsigned()?;
334                        let binding = match resolve_binding(names, idx)? {
335                            Some(expr) => Binding::Expression(expr),
336                            None => Binding::Unavailable,
337                        };
338                        current.bindings.push(binding);
339                    }
340                }
341            }
342
343            TAG_GENERATED_RANGE_SUB_RANGE_BINDINGS => {
344                if let Some(current) = range_stack.last_mut() {
345                    let var_idx = tok.read_unsigned()? as usize;
346
347                    let mut sub_ranges: Vec<SubRangeBinding> = Vec::new();
348                    // Line/column state relative to range start
349                    let mut h_line = current.start.line;
350                    let mut h_col = current.start.column;
351
352                    while !tok.at_item_end() {
353                        let binding_raw = tok.read_unsigned()?;
354                        let line_delta = tok.read_unsigned()? as u32;
355                        h_line += line_delta;
356
357                        let col_raw = tok.read_unsigned()? as u32;
358                        h_col = if line_delta != 0 {
359                            col_raw
360                        } else {
361                            h_col + col_raw
362                        };
363
364                        let expression = resolve_binding(names, binding_raw)?;
365                        sub_ranges.push(SubRangeBinding {
366                            expression,
367                            from: Position {
368                                line: h_line,
369                                column: h_col,
370                            },
371                        });
372                    }
373
374                    current.sub_range_bindings.push((var_idx, sub_ranges));
375                }
376            }
377
378            TAG_GENERATED_RANGE_CALL_SITE => {
379                if let Some(current) = range_stack.last_mut() {
380                    let source_index = tok.read_unsigned()? as u32;
381                    let line = tok.read_unsigned()? as u32;
382                    let column = tok.read_unsigned()? as u32;
383                    current.call_site = Some(CallSite {
384                        source_index,
385                        line,
386                        column,
387                    });
388                }
389            }
390
391            _ => {
392                // Unknown tag: skip remaining VLQs in this item
393                while !tok.at_item_end() {
394                    let _ = tok.read_unsigned()?;
395                }
396            }
397        }
398
399        tok.skip_comma();
400    }
401
402    if !scope_stack.is_empty() {
403        return Err(ScopesError::UnclosedScope);
404    }
405    if !range_stack.is_empty() {
406        return Err(ScopesError::UnclosedRange);
407    }
408
409    // Fill remaining source slots
410    while scopes.len() < num_sources {
411        scopes.push(None);
412    }
413
414    Ok(ScopeInfo { scopes, ranges })
415}
416
417/// Merge initial bindings from G items with sub-range overrides from H items.
418fn merge_bindings(
419    initial: Vec<Binding>,
420    sub_range_map: &[(usize, Vec<SubRangeBinding>)],
421    range_start: Position,
422) -> Vec<Binding> {
423    if sub_range_map.is_empty() {
424        return initial;
425    }
426
427    let mut result = initial;
428
429    for (var_idx, sub_ranges) in sub_range_map {
430        if *var_idx < result.len() {
431            // Get the initial binding expression to use as first sub-range
432            let initial_expr = match &result[*var_idx] {
433                Binding::Expression(e) => Some(e.clone()),
434                Binding::Unavailable => None,
435                Binding::SubRanges(_) => None, // shouldn't happen
436            };
437
438            let mut all_subs = vec![SubRangeBinding {
439                expression: initial_expr,
440                from: range_start,
441            }];
442            all_subs.extend(sub_ranges.iter().cloned());
443
444            result[*var_idx] = Binding::SubRanges(all_subs);
445        }
446    }
447
448    result
449}