Skip to main content

encode_scopes

Function encode_scopes 

Source
pub fn encode_scopes(info: &ScopeInfo, names: &mut Vec<String>) -> String
Expand description

Encode scope information into a VLQ-encoded scopes string.

New names may be added to the names array during encoding.

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