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