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 {
100            scopes,
101            ranges: vec![],
102        });
103    }
104
105    let mut tok = Tokenizer::new(input.as_bytes());
106
107    // Original scope state
108    let mut scopes: Vec<Option<OriginalScope>> = Vec::new();
109    let mut source_idx = 0usize;
110    let mut scope_stack: Vec<BuildingScope> = Vec::new();
111    let mut os_line = 0u32;
112    let mut os_col = 0u32;
113    let mut os_name = 0i64;
114    let mut os_kind = 0i64;
115    let mut os_var = 0i64;
116
117    // Generated range state
118    let mut ranges: Vec<GeneratedRange> = Vec::new();
119    let mut range_stack: Vec<BuildingRange> = Vec::new();
120    let mut gr_line = 0u32;
121    let mut gr_col = 0u32;
122    let mut gr_def = 0i64;
123    let mut h_var_acc: u64 = 0;
124    let mut in_generated_ranges = false;
125
126    while tok.has_next() {
127        // Empty item: no scope info for this source file
128        if tok.at_item_end() {
129            if !in_generated_ranges && source_idx < num_sources && scope_stack.is_empty() {
130                scopes.push(None);
131                source_idx += 1;
132            }
133            tok.skip_comma();
134            continue;
135        }
136
137        let tag = tok.read_unsigned()?;
138
139        match tag {
140            TAG_ORIGINAL_SCOPE_START => {
141                // Reset position state at start of new top-level tree
142                if scope_stack.is_empty() {
143                    os_line = 0;
144                    os_col = 0;
145                }
146
147                let flags = tok.read_unsigned()?;
148
149                let line_delta = tok.read_unsigned()? as u32;
150                os_line += line_delta;
151                let col_raw = tok.read_unsigned()? as u32;
152                os_col = if line_delta != 0 {
153                    col_raw
154                } else {
155                    os_col + col_raw
156                };
157
158                let name = if flags & crate::OS_FLAG_HAS_NAME != 0 {
159                    let d = tok.read_signed()?;
160                    os_name += d;
161                    Some(resolve_name(names, os_name)?)
162                } else {
163                    None
164                };
165
166                let kind = if flags & crate::OS_FLAG_HAS_KIND != 0 {
167                    let d = tok.read_signed()?;
168                    os_kind += d;
169                    Some(resolve_name(names, os_kind)?)
170                } else {
171                    None
172                };
173
174                let is_stack_frame = flags & crate::OS_FLAG_IS_STACK_FRAME != 0;
175
176                scope_stack.push(BuildingScope {
177                    start: Position {
178                        line: os_line,
179                        column: os_col,
180                    },
181                    name,
182                    kind,
183                    is_stack_frame,
184                    variables: Vec::new(),
185                    children: Vec::new(),
186                });
187            }
188
189            TAG_ORIGINAL_SCOPE_END => {
190                if scope_stack.is_empty() {
191                    return Err(ScopesError::UnmatchedScopeEnd);
192                }
193
194                let line_delta = tok.read_unsigned()? as u32;
195                os_line += line_delta;
196                let col_raw = tok.read_unsigned()? as u32;
197                os_col = if line_delta != 0 {
198                    col_raw
199                } else {
200                    os_col + col_raw
201                };
202
203                // Safe: scope_stack.is_empty() checked above
204                let building = scope_stack.pop().expect("non-empty: checked above");
205                let finished = OriginalScope {
206                    start: building.start,
207                    end: Position {
208                        line: os_line,
209                        column: os_col,
210                    },
211                    name: building.name,
212                    kind: building.kind,
213                    is_stack_frame: building.is_stack_frame,
214                    variables: building.variables,
215                    children: building.children,
216                };
217
218                if scope_stack.is_empty() {
219                    scopes.push(Some(finished));
220                    source_idx += 1;
221                } else {
222                    // Safe: just checked !is_empty()
223                    scope_stack
224                        .last_mut()
225                        .expect("non-empty: checked above")
226                        .children
227                        .push(finished);
228                }
229            }
230
231            TAG_ORIGINAL_SCOPE_VARIABLES => {
232                if let Some(current) = scope_stack.last_mut() {
233                    while !tok.at_item_end() {
234                        let d = tok.read_signed()?;
235                        os_var += d;
236                        current.variables.push(resolve_name(names, os_var)?);
237                    }
238                } else {
239                    while !tok.at_item_end() {
240                        let _ = tok.read_signed()?;
241                    }
242                }
243            }
244
245            TAG_GENERATED_RANGE_START => {
246                if !in_generated_ranges {
247                    in_generated_ranges = true;
248                    // Fill remaining source slots
249                    while scopes.len() < num_sources {
250                        scopes.push(None);
251                    }
252                    source_idx = num_sources;
253                }
254
255                let flags = tok.read_unsigned()?;
256
257                let line_delta = if flags & crate::GR_FLAG_HAS_LINE != 0 {
258                    tok.read_unsigned()? as u32
259                } else {
260                    0
261                };
262                gr_line += line_delta;
263
264                let col_raw = tok.read_unsigned()? as u32;
265                gr_col = if line_delta != 0 {
266                    col_raw
267                } else {
268                    gr_col + col_raw
269                };
270
271                let definition = if flags & crate::GR_FLAG_HAS_DEFINITION != 0 {
272                    let d = tok.read_signed()?;
273                    gr_def += d;
274                    Some(gr_def as u32)
275                } else {
276                    None
277                };
278
279                let is_stack_frame = flags & crate::GR_FLAG_IS_STACK_FRAME != 0;
280                let is_hidden = flags & crate::GR_FLAG_IS_HIDDEN != 0;
281
282                // Reset H-tag variable index accumulator for each new range
283                h_var_acc = 0;
284
285                range_stack.push(BuildingRange {
286                    start: Position {
287                        line: gr_line,
288                        column: gr_col,
289                    },
290                    is_stack_frame,
291                    is_hidden,
292                    definition,
293                    call_site: None,
294                    bindings: Vec::new(),
295                    sub_range_bindings: Vec::new(),
296                    children: Vec::new(),
297                });
298            }
299
300            TAG_GENERATED_RANGE_END => {
301                if range_stack.is_empty() {
302                    return Err(ScopesError::UnmatchedRangeEnd);
303                }
304
305                // F tag: 1 VLQ = column only, 2 VLQs = line + column
306                let first = tok.read_unsigned()? as u32;
307                let (line_delta, col_raw) = if !tok.at_item_end() {
308                    let second = tok.read_unsigned()? as u32;
309                    (first, second)
310                } else {
311                    (0, first)
312                };
313                gr_line += line_delta;
314                gr_col = if line_delta != 0 {
315                    col_raw
316                } else {
317                    gr_col + col_raw
318                };
319
320                // Safe: range_stack.is_empty() checked above
321                let building = range_stack.pop().expect("non-empty: checked above");
322
323                // Merge sub-range bindings into final bindings
324                let final_bindings = merge_bindings(
325                    building.bindings,
326                    &building.sub_range_bindings,
327                    building.start,
328                );
329
330                let finished = GeneratedRange {
331                    start: building.start,
332                    end: Position {
333                        line: gr_line,
334                        column: gr_col,
335                    },
336                    is_stack_frame: building.is_stack_frame,
337                    is_hidden: building.is_hidden,
338                    definition: building.definition,
339                    call_site: building.call_site,
340                    bindings: final_bindings,
341                    children: building.children,
342                };
343
344                if range_stack.is_empty() {
345                    ranges.push(finished);
346                } else {
347                    // Safe: just checked !is_empty()
348                    range_stack
349                        .last_mut()
350                        .expect("non-empty: checked above")
351                        .children
352                        .push(finished);
353                }
354            }
355
356            TAG_GENERATED_RANGE_BINDINGS => {
357                if let Some(current) = range_stack.last_mut() {
358                    while !tok.at_item_end() {
359                        let idx = tok.read_unsigned()?;
360                        let binding = match resolve_binding(names, idx)? {
361                            Some(expr) => Binding::Expression(expr),
362                            None => Binding::Unavailable,
363                        };
364                        current.bindings.push(binding);
365                    }
366                } else {
367                    while !tok.at_item_end() {
368                        let _ = tok.read_unsigned()?;
369                    }
370                }
371            }
372
373            TAG_GENERATED_RANGE_SUB_RANGE_BINDINGS => {
374                if let Some(current) = range_stack.last_mut() {
375                    let var_delta = tok.read_unsigned()?;
376                    h_var_acc += var_delta;
377                    let var_idx = h_var_acc as usize;
378
379                    let mut sub_ranges: Vec<SubRangeBinding> = Vec::new();
380                    // Line/column state relative to range start
381                    let mut h_line = current.start.line;
382                    let mut h_col = current.start.column;
383
384                    while !tok.at_item_end() {
385                        let binding_raw = tok.read_unsigned()?;
386                        let line_delta = tok.read_unsigned()? as u32;
387                        h_line += line_delta;
388
389                        let col_raw = tok.read_unsigned()? as u32;
390                        h_col = if line_delta != 0 {
391                            col_raw
392                        } else {
393                            h_col + col_raw
394                        };
395
396                        let expression = resolve_binding(names, binding_raw)?;
397                        sub_ranges.push(SubRangeBinding {
398                            expression,
399                            from: Position {
400                                line: h_line,
401                                column: h_col,
402                            },
403                        });
404                    }
405
406                    current.sub_range_bindings.push((var_idx, sub_ranges));
407                } else {
408                    while !tok.at_item_end() {
409                        let _ = tok.read_unsigned()?;
410                    }
411                }
412            }
413
414            TAG_GENERATED_RANGE_CALL_SITE => {
415                if let Some(current) = range_stack.last_mut() {
416                    let source_index = tok.read_unsigned()? as u32;
417                    let line = tok.read_unsigned()? as u32;
418                    let column = tok.read_unsigned()? as u32;
419                    current.call_site = Some(CallSite {
420                        source_index,
421                        line,
422                        column,
423                    });
424                } else {
425                    while !tok.at_item_end() {
426                        let _ = tok.read_unsigned()?;
427                    }
428                }
429            }
430
431            _ => {
432                // Unknown tag: skip remaining VLQs in this item
433                while !tok.at_item_end() {
434                    let _ = tok.read_unsigned()?;
435                }
436            }
437        }
438
439        tok.skip_comma();
440    }
441
442    if !scope_stack.is_empty() {
443        return Err(ScopesError::UnclosedScope);
444    }
445    if !range_stack.is_empty() {
446        return Err(ScopesError::UnclosedRange);
447    }
448
449    // Fill remaining source slots
450    while scopes.len() < num_sources {
451        scopes.push(None);
452    }
453
454    Ok(ScopeInfo { scopes, ranges })
455}
456
457/// Merge initial bindings from G items with sub-range overrides from H items.
458fn merge_bindings(
459    initial: Vec<Binding>,
460    sub_range_map: &[(usize, Vec<SubRangeBinding>)],
461    range_start: Position,
462) -> Vec<Binding> {
463    if sub_range_map.is_empty() {
464        return initial;
465    }
466
467    let mut result = initial;
468
469    for (var_idx, sub_ranges) in sub_range_map {
470        if *var_idx < result.len() {
471            // Get the initial binding expression to use as first sub-range
472            let initial_expr = match &result[*var_idx] {
473                Binding::Expression(e) => Some(e.clone()),
474                Binding::Unavailable => None,
475                Binding::SubRanges(_) => None, // shouldn't happen
476            };
477
478            let mut all_subs = vec![SubRangeBinding {
479                expression: initial_expr,
480                from: range_start,
481            }];
482            all_subs.extend(sub_ranges.iter().cloned());
483
484            result[*var_idx] = Binding::SubRanges(all_subs);
485        }
486    }
487
488    result
489}