Skip to main content

decode_scopes

Function decode_scopes 

Source
pub fn decode_scopes(
    input: &str,
    names: &[String],
    num_sources: usize,
) -> Result<ScopeInfo, ScopesError>
Expand description

Decode a scopes string into structured scope information.

  • input: the VLQ-encoded scopes string from the source map
  • names: the names array from the source map (for resolving indices). Must contain all names referenced by the encoded string, or ScopesError::InvalidNameIndex will be returned.
  • num_sources: number of source files (length of sources array)
Examples found in repository?
examples/debug_scopes.rs (line 161)
42fn main() {
43    // -----------------------------------------------------------------------
44    // 1. Build the original scope tree (what the author wrote)
45    // -----------------------------------------------------------------------
46    //
47    // ECMA-426 original scopes form a tree per source file. Each scope has:
48    //   - start/end positions (0-based line and column)
49    //   - an optional name (for named functions, classes, etc.)
50    //   - an optional kind ("global", "module", "function", "block", "class")
51    //   - is_stack_frame: true for function-like scopes that appear in call stacks
52    //   - variables: names declared in this scope (parameters, let/const/var)
53    //   - children: nested scopes
54    //
55    // Scopes are indexed by a pre-order traversal counter ("definition index"):
56    //   - definition 0: the module scope (root)
57    //   - definition 1: the `add` function scope (first child)
58
59    let add_scope = OriginalScope {
60        start: Position { line: 1, column: 0 },
61        end: Position { line: 3, column: 1 },
62        name: Some("add".to_string()),
63        kind: Some("function".to_string()),
64        is_stack_frame: true,
65        variables: vec!["a".to_string(), "b".to_string()],
66        children: vec![],
67    };
68
69    let module_scope = OriginalScope {
70        start: Position { line: 0, column: 0 },
71        end: Position { line: 5, column: 27 },
72        name: None,
73        kind: Some("module".to_string()),
74        is_stack_frame: false,
75        variables: vec!["result".to_string()],
76        children: vec![add_scope],
77    };
78
79    // -----------------------------------------------------------------------
80    // 2. Build the generated ranges (what the bundler produced)
81    // -----------------------------------------------------------------------
82    //
83    // Generated ranges describe regions of the output code and how they map
84    // back to original scopes. Key fields:
85    //
86    //   - definition: index into the pre-order list of all original scopes,
87    //     linking this range to its corresponding original scope
88    //   - call_site: if this range is an inlined function body, the location
89    //     in original source where the call happened
90    //   - bindings: one entry per variable in the referenced original scope,
91    //     telling the debugger what JS expression to evaluate for each variable
92    //   - is_stack_frame: true if this range should appear in synthetic stacks
93    //   - is_hidden: true if the debugger should skip over this range entirely
94
95    let inlined_range = GeneratedRange {
96        start: Position { line: 1, column: 0 },
97        end: Position { line: 3, column: 22 },
98        is_stack_frame: true,
99        is_hidden: false,
100        // definition=1 points to the `add` function scope (pre-order index 1)
101        definition: Some(1),
102        // The call site is where `add(10, 32)` was called in the original source.
103        // The debugger uses this to reconstruct a synthetic call stack:
104        //   add @ math.ts:2:2  (current position in the inlined body)
105        //   <module> @ math.ts:5:14  (the call site)
106        call_site: Some(CallSite { source_index: 0, line: 5, column: 14 }),
107        // Bindings map the original scope's variables to generated expressions.
108        // The `add` scope has variables ["a", "b"] (in that order), so:
109        //   bindings[0] = Expression("_a")  → original `a` is `_a` in generated code
110        //   bindings[1] = Expression("_b")  → original `b` is `_b` in generated code
111        bindings: vec![
112            Binding::Expression("_a".to_string()),
113            Binding::Expression("_b".to_string()),
114        ],
115        children: vec![],
116    };
117
118    let wrapper_range = GeneratedRange {
119        start: Position { line: 0, column: 0 },
120        end: Position { line: 4, column: 1 },
121        is_stack_frame: false,
122        is_hidden: false,
123        // definition=0 points to the module scope (pre-order index 0)
124        definition: Some(0),
125        call_site: None,
126        // The module scope has variables ["result"], and in the generated code
127        // the variable keeps its name, so we bind it to "result".
128        bindings: vec![Binding::Expression("result".to_string())],
129        children: vec![inlined_range],
130    };
131
132    // -----------------------------------------------------------------------
133    // 3. Assemble ScopeInfo and encode
134    // -----------------------------------------------------------------------
135    //
136    // ScopeInfo combines original scope trees (one per source file) with the
137    // generated ranges. The `scopes` vec is indexed by source index — None
138    // means no scope info for that source file.
139
140    let scope_info = ScopeInfo { scopes: vec![Some(module_scope)], ranges: vec![wrapper_range] };
141
142    // Encoding produces a compact VLQ string (stored in the source map's
143    // "scopes" field) and populates the names array with any new name strings.
144    let mut names: Vec<String> = vec![];
145    let encoded = encode_scopes(&scope_info, &mut names);
146
147    println!("=== ECMA-426 Scopes Roundtrip ===\n");
148    println!("Encoded scopes: {encoded:?}");
149    println!("Names array:    {names:?}\n");
150
151    assert!(!encoded.is_empty(), "encoded string must not be empty");
152    assert!(!names.is_empty(), "names array must contain scope/variable names");
153
154    // -----------------------------------------------------------------------
155    // 4. Decode back and verify roundtrip
156    // -----------------------------------------------------------------------
157    //
158    // decode_scopes takes the encoded string, the names array, and the number
159    // of source files (so it knows how many original scope trees to expect).
160
161    let decoded = decode_scopes(&encoded, &names, 1).expect("decoding must succeed");
162
163    // Verify the original scope tree roundtrips correctly
164    assert_eq!(decoded.scopes.len(), 1, "must have exactly one source file's scopes");
165
166    let root_scope = decoded.scopes[0].as_ref().expect("source 0 must have scope info");
167
168    assert_eq!(root_scope.kind.as_deref(), Some("module"));
169    assert!(!root_scope.is_stack_frame, "module scope is not a stack frame");
170    assert_eq!(root_scope.variables, vec!["result"]);
171    assert_eq!(root_scope.start, Position { line: 0, column: 0 });
172    assert_eq!(root_scope.end, Position { line: 5, column: 27 });
173
174    println!("Original scope tree (source 0):");
175    println!("  Root: kind={:?}, variables={:?}", root_scope.kind, root_scope.variables);
176
177    assert_eq!(root_scope.children.len(), 1, "module has one child scope");
178
179    let func_scope = &root_scope.children[0];
180    assert_eq!(func_scope.name.as_deref(), Some("add"));
181    assert_eq!(func_scope.kind.as_deref(), Some("function"));
182    assert!(func_scope.is_stack_frame, "function scope is a stack frame");
183    assert_eq!(func_scope.variables, vec!["a", "b"]);
184    assert_eq!(func_scope.start, Position { line: 1, column: 0 });
185    assert_eq!(func_scope.end, Position { line: 3, column: 1 });
186
187    println!(
188        "  Child: name={:?}, kind={:?}, variables={:?}",
189        func_scope.name, func_scope.kind, func_scope.variables
190    );
191
192    // Verify the generated ranges roundtrip correctly
193    assert_eq!(decoded.ranges.len(), 1, "must have one top-level generated range");
194
195    let wrapper = &decoded.ranges[0];
196    assert_eq!(wrapper.definition, Some(0));
197    assert!(!wrapper.is_stack_frame);
198    assert!(!wrapper.is_hidden);
199    assert!(wrapper.call_site.is_none());
200    assert_eq!(wrapper.bindings, vec![Binding::Expression("result".to_string())]);
201
202    println!("\nGenerated ranges:");
203    println!(
204        "  Wrapper: lines {}-{}, definition={:?}, bindings={:?}",
205        wrapper.start.line, wrapper.end.line, wrapper.definition, wrapper.bindings
206    );
207
208    assert_eq!(wrapper.children.len(), 1, "wrapper has one child range");
209
210    let inlined = &wrapper.children[0];
211    assert_eq!(inlined.definition, Some(1));
212    assert!(inlined.is_stack_frame, "inlined range is a stack frame");
213    assert!(!inlined.is_hidden);
214    assert_eq!(inlined.call_site, Some(CallSite { source_index: 0, line: 5, column: 14 }));
215    assert_eq!(
216        inlined.bindings,
217        vec![Binding::Expression("_a".to_string()), Binding::Expression("_b".to_string()),]
218    );
219
220    println!(
221        "  Inlined: lines {}-{}, definition={:?}, call_site={:?}, bindings={:?}",
222        inlined.start.line,
223        inlined.end.line,
224        inlined.definition,
225        inlined.call_site,
226        inlined.bindings
227    );
228
229    // Full structural equality check
230    assert_eq!(decoded, scope_info, "decoded scope info must match the original");
231
232    println!("\nRoundtrip verified: decoded structure matches original.\n");
233
234    // -----------------------------------------------------------------------
235    // 5. Look up original scopes by definition index
236    // -----------------------------------------------------------------------
237    //
238    // A debugger hits a breakpoint in generated code and finds a generated
239    // range with definition=1. It needs to find the corresponding original
240    // scope to know the function name, parameter names, etc.
241
242    println!("--- Definition index lookups ---\n");
243
244    let scope_0 = decoded.original_scope_for_definition(0).expect("definition 0 must exist");
245    assert_eq!(scope_0.kind.as_deref(), Some("module"));
246    println!("  definition 0: kind={:?}, name={:?}", scope_0.kind, scope_0.name);
247
248    let scope_1 = decoded.original_scope_for_definition(1).expect("definition 1 must exist");
249    assert_eq!(scope_1.name.as_deref(), Some("add"));
250    assert_eq!(scope_1.variables, vec!["a", "b"]);
251    println!("  definition 1: kind={:?}, name={:?}", scope_1.kind, scope_1.name);
252
253    // Out-of-bounds definition index returns None
254    assert!(
255        decoded.original_scope_for_definition(99).is_none(),
256        "non-existent definition must return None"
257    );
258    println!("  definition 99: None (out of bounds)");
259
260    // -----------------------------------------------------------------------
261    // 6. Demonstrate the Unavailable binding variant
262    // -----------------------------------------------------------------------
263    //
264    // Sometimes a variable is optimized out entirely. The debugger should
265    // show it as "unavailable" rather than silently omitting it.
266
267    println!("\n--- Unavailable binding ---\n");
268
269    let optimized_info = ScopeInfo {
270        scopes: vec![Some(OriginalScope {
271            start: Position { line: 0, column: 0 },
272            end: Position { line: 3, column: 1 },
273            name: Some("compute".to_string()),
274            kind: Some("function".to_string()),
275            is_stack_frame: true,
276            variables: vec!["x".to_string(), "y".to_string()],
277            children: vec![],
278        })],
279        ranges: vec![GeneratedRange {
280            start: Position { line: 0, column: 0 },
281            end: Position { line: 1, column: 0 },
282            is_stack_frame: true,
283            is_hidden: false,
284            definition: Some(0),
285            call_site: None,
286            // x is available as "_x", but y was optimized out
287            bindings: vec![Binding::Expression("_x".to_string()), Binding::Unavailable],
288            children: vec![],
289        }],
290    };
291
292    let mut opt_names: Vec<String> = vec![];
293    let opt_encoded = encode_scopes(&optimized_info, &mut opt_names);
294    let opt_decoded = decode_scopes(&opt_encoded, &opt_names, 1).expect("decoding must succeed");
295
296    assert_eq!(opt_decoded, optimized_info);
297    println!("  Bindings: {:?}", opt_decoded.ranges[0].bindings);
298    println!("  Variable 'x' -> Expression(\"_x\"), variable 'y' -> Unavailable");
299
300    // -----------------------------------------------------------------------
301    // 7. Error handling for invalid input
302    // -----------------------------------------------------------------------
303    //
304    // decode_scopes returns ScopesError for malformed input. This is useful
305    // for tools that validate source maps.
306
307    println!("\n--- Error handling ---\n");
308
309    // Empty encoded string is valid (no scopes, no ranges)
310    let empty_result = decode_scopes("", &[], 0);
311    assert!(empty_result.is_ok(), "empty input is valid");
312    println!("  Empty input: ok (no scopes, no ranges)");
313
314    // Invalid VLQ data: 'z' is not a valid base64 character for VLQ
315    let bad_vlq = decode_scopes("!!!", &[], 1);
316    assert!(bad_vlq.is_err(), "invalid VLQ must produce an error");
317    println!("  Invalid VLQ (\"!!!\"): {}", bad_vlq.unwrap_err());
318
319    println!("\nAll assertions passed.");
320}