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