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 mapnames: thenamesarray from the source map (for resolving indices). Must contain all names referenced by the encoded string, orScopesError::InvalidNameIndexwill be returned.num_sources: number of source files (length ofsourcesarray)
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}