warpo 3.1.3 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/common/leb128.d.ts +5 -0
  2. package/dist/common/leb128.js +17 -0
  3. package/dist/common/leb128.js.map +1 -0
  4. package/dist/debug_server/debugSession.d.ts +30 -1
  5. package/dist/debug_server/debugSession.js +462 -93
  6. package/dist/debug_server/debugSession.js.map +1 -1
  7. package/dist/debug_server/debugTrace.d.ts +2 -0
  8. package/dist/debug_server/debugTrace.js +38 -0
  9. package/dist/debug_server/debugTrace.js.map +1 -0
  10. package/dist/debug_server/debugger.d.ts +5 -1
  11. package/dist/debug_server/debuggerWasmModule.d.ts +6 -0
  12. package/dist/debug_server/debuggerWasmModule.js +135 -2
  13. package/dist/debug_server/debuggerWasmModule.js.map +1 -1
  14. package/dist/debug_server/nodeDebugger.d.ts +12 -1
  15. package/dist/debug_server/nodeDebugger.js +74 -28
  16. package/dist/debug_server/nodeDebugger.js.map +1 -1
  17. package/dist/debug_server/tests/debugSession.test.js +460 -22
  18. package/dist/debug_server/tests/debugSession.test.js.map +1 -1
  19. package/dist/debug_server/tests/testModule/debugger_breakpoint_validation.d.ts +1 -0
  20. package/dist/debug_server/tests/testModule/debugger_breakpoint_validation.js +12 -0
  21. package/dist/debug_server/tests/testModule/debugger_breakpoint_validation.js.map +1 -0
  22. package/dist/debug_server/tests/testModule/debugger_function_closure.d.ts +1 -0
  23. package/dist/debug_server/tests/testModule/debugger_function_closure.js +24 -0
  24. package/dist/debug_server/tests/testModule/debugger_function_closure.js.map +1 -0
  25. package/dist/debug_server/tests/testModule/debugger_import.case.d.ts +1 -0
  26. package/dist/debug_server/tests/testModule/debugger_import.case.js +9 -0
  27. package/dist/debug_server/tests/testModule/debugger_import.case.js.map +1 -0
  28. package/dist/debug_server/tests/testModule/debugger_import.d.ts +1 -0
  29. package/dist/debug_server/tests/testModule/debugger_import.js +6 -0
  30. package/dist/debug_server/tests/testModule/debugger_import.js.map +1 -0
  31. package/dist/debug_server/tests/testModule/debugger_pause.d.ts +1 -0
  32. package/dist/debug_server/tests/testModule/debugger_pause.js +9 -0
  33. package/dist/debug_server/tests/testModule/debugger_pause.js.map +1 -0
  34. package/dist/debug_server/tests/testModule/debugger_stepping.d.ts +3 -0
  35. package/dist/debug_server/tests/testModule/debugger_stepping.js +28 -0
  36. package/dist/debug_server/tests/testModule/debugger_stepping.js.map +1 -0
  37. package/dist/debug_server/tests/testModule/debugger_typed_array.d.ts +1 -0
  38. package/dist/debug_server/tests/testModule/debugger_typed_array.js +52 -0
  39. package/dist/debug_server/tests/testModule/debugger_typed_array.js.map +1 -0
  40. package/dist/debug_server/tests/testModule/debugger_utf8_const_str.d.ts +1 -0
  41. package/dist/debug_server/tests/testModule/debugger_utf8_const_str.js +10 -0
  42. package/dist/debug_server/tests/testModule/debugger_utf8_const_str.js.map +1 -0
  43. package/dist/dwarf/dwarfParser.js +6 -13
  44. package/dist/dwarf/dwarfParser.js.map +1 -1
  45. package/dist/dwarf/functionDebugInfo.d.ts +3 -0
  46. package/dist/dwarf/functionDebugInfo.js +24 -0
  47. package/dist/dwarf/functionDebugInfo.js.map +1 -1
  48. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  // Copyright (C) 2025 wasm-ecosystem
2
2
  // SPDX-License-Identifier: Apache-2.0
3
- import { LoggingDebugSession, InitializedEvent, Thread, Breakpoint, Source, Scope, StackFrame, LoadedSourceEvent, OutputEvent, StoppedEvent, ContinuedEvent, TerminatedEvent, logger, } from "@vscode/debugadapter";
3
+ import { LoggingDebugSession, InitializedEvent, Thread, Breakpoint, BreakpointEvent, Source, Scope, StackFrame, LoadedSourceEvent, OutputEvent, StoppedEvent, ContinuedEvent, TerminatedEvent, logger, } from "@vscode/debugadapter";
4
4
  import assert from "node:assert/strict";
5
5
  import { existsSync } from "node:fs";
6
6
  import * as path from "node:path";
@@ -9,6 +9,14 @@ import { ARRAY_DATA_START_OFFSET, ARRAY_LENGTH_OFFSET, AS_STRING_CLASS_NAME, OBJ
9
9
  import { normalizeDebugPath } from "./debugPath.js";
10
10
  import { NodeDebugger } from "./nodeDebugger.js";
11
11
  import { loadConfig } from "../test_runner/config.js";
12
+ import { configureTrace, trace } from "./debugTrace.js";
13
+ var StepMode;
14
+ (function (StepMode) {
15
+ StepMode[StepMode["None"] = 0] = "None";
16
+ StepMode[StepMode["Into"] = 1] = "Into";
17
+ StepMode[StepMode["Over"] = 2] = "Over";
18
+ StepMode[StepMode["Out"] = 3] = "Out";
19
+ })(StepMode || (StepMode = {}));
12
20
  function formatUnknownError(error) {
13
21
  if (error instanceof Error) {
14
22
  return error.message;
@@ -18,6 +26,23 @@ function formatUnknownError(error) {
18
26
  }
19
27
  return "unknown error";
20
28
  }
29
+ const UTF8_CONST_STR_CLASS_NAME = "~lib/warpo/utf8/const_str/ConstStr";
30
+ function isDisplayableStringClass(typeName) {
31
+ return typeName === AS_STRING_CLASS_NAME || typeName === UTF8_CONST_STR_CLASS_NAME;
32
+ }
33
+ const TYPED_ARRAY_ELEMENT_TYPES = new Map([
34
+ ["~lib/typedarray/Int8Array", "i8"],
35
+ ["~lib/typedarray/Uint8Array", "u8"],
36
+ ["~lib/typedarray/Uint8ClampedArray", "u8"],
37
+ ["~lib/typedarray/Int16Array", "i16"],
38
+ ["~lib/typedarray/Uint16Array", "u16"],
39
+ ["~lib/typedarray/Int32Array", "i32"],
40
+ ["~lib/typedarray/Uint32Array", "u32"],
41
+ ["~lib/typedarray/Int64Array", "i64"],
42
+ ["~lib/typedarray/Uint64Array", "u64"],
43
+ ["~lib/typedarray/Float32Array", "f32"],
44
+ ["~lib/typedarray/Float64Array", "f64"],
45
+ ]);
21
46
  async function resolveUnitTestWasmPath(cwd) {
22
47
  const configPath = path.join(cwd, "as-test.config.js");
23
48
  let outputFolder = "coverage";
@@ -39,11 +64,15 @@ export class WarpoDebugSession extends LoggingDebugSession {
39
64
  nextVariablesReference = 1;
40
65
  loadedModule;
41
66
  runtime;
67
+ configurationDone = false;
42
68
  stoppedWasmBytecodeOffset;
43
69
  stoppedForTrap = false;
70
+ stepMode = StepMode.None;
71
+ stepStartSourceLocation;
44
72
  debugSessionLogging = false;
45
73
  log(msg) {
46
74
  logger.log(msg);
75
+ trace("debug-session", msg);
47
76
  if (this.debugSessionLogging) {
48
77
  this.sendEvent(new OutputEvent(`${msg}\n`, "stdout"));
49
78
  }
@@ -72,12 +101,24 @@ export class WarpoDebugSession extends LoggingDebugSession {
72
101
  }
73
102
  this.applyPendingBreakpointUpdates(runtime, () => {
74
103
  if (info.reason === "other") {
104
+ if (this.stepMode !== StepMode.None) {
105
+ void this.handleStepPause(runtime, info);
106
+ return;
107
+ }
75
108
  void runtime.resume().catch((error) => {
76
109
  const message = error instanceof Error ? error.message : "unknown error";
77
110
  this.log(`Failed to resume after breakpoint update: ${message}`);
78
111
  });
79
112
  return;
80
113
  }
114
+ if (info.reason === "breakpoint" || info.reason === "exception") {
115
+ this.handleNonStepPause(runtime, info);
116
+ return;
117
+ }
118
+ if (this.stepMode !== StepMode.None) {
119
+ void this.handleStepPause(runtime, info);
120
+ return;
121
+ }
81
122
  this.sendStoppedEvent(runtime, info);
82
123
  });
83
124
  }
@@ -88,33 +129,38 @@ export class WarpoDebugSession extends LoggingDebugSession {
88
129
  response.body.supportsBreakpointLocationsRequest = false;
89
130
  response.body.supportsLoadedSourcesRequest = true;
90
131
  response.body.supportsEvaluateForHovers = false;
132
+ response.body.supportsTerminateRequest = true;
133
+ response.body.supportsRestartRequest = true;
91
134
  this.sendResponse(response);
92
135
  this.sendEvent(new InitializedEvent());
93
136
  }
94
137
  setBreakPointsRequest(response, args) {
95
138
  const sourcePath = normalizeDebugPath(args.source.path || "");
96
139
  const clientLines = args.breakpoints || [];
97
- const bps = clientLines.map((bp) => {
140
+ const wasmModule = this.loadedModule;
141
+ this.log(`SetBreakpoints request: source=${sourcePath}, lines=${clientLines.map((breakpoint) => breakpoint.line).join(",") || "none"}`);
142
+ const bps = [];
143
+ for (const bp of clientLines) {
98
144
  const id = ++this.breakpointId;
99
- const info = {
145
+ bps.push({
100
146
  id,
101
147
  line: bp.line,
102
- verified: true,
148
+ verified: false,
103
149
  source: sourcePath,
104
- };
150
+ });
105
151
  this.log(`Breakpoint set: ${path.basename(sourcePath)}:${bp.line}`);
106
- return info;
107
- });
152
+ }
153
+ if (wasmModule !== undefined) {
154
+ for (const breakpoint of bps) {
155
+ breakpoint.verified = wasmModule.resolveBreakpointLocations(breakpoint).length > 0;
156
+ }
157
+ }
108
158
  this.requestedBreakpointsBySource.set(sourcePath, bps);
109
159
  this.pendingBreakpointSources.clear();
110
160
  for (const pendingSourcePath of this.requestedBreakpointsBySource.keys()) {
111
161
  this.pendingBreakpointSources.add(pendingSourcePath);
112
162
  }
113
- const breakpoints = bps.map((bp) => {
114
- const dbp = new Breakpoint(bp.verified, bp.line);
115
- dbp.id = bp.id;
116
- return dbp;
117
- });
163
+ const breakpoints = bps.map((bp) => this.toDebugProtocolBreakpoint(bp));
118
164
  response.body = { breakpoints };
119
165
  if (this.runtime?.isPaused() && this.loadedModule) {
120
166
  this.requestBreakpointUpdate(() => this.sendResponse(response));
@@ -125,12 +171,34 @@ export class WarpoDebugSession extends LoggingDebugSession {
125
171
  }
126
172
  }
127
173
  configurationDoneRequest(response, _args) {
174
+ this.configurationDone = true;
128
175
  super.configurationDoneRequest(response, _args);
129
176
  this.logAllBreakpoints();
177
+ this.maybeStartRuntime();
130
178
  }
131
179
  launchRequest(response, args) {
132
180
  void this.doLaunchRequest(response, args);
133
181
  }
182
+ restartRequest(response, _args) {
183
+ const runtime = this.runtime;
184
+ if (!runtime) {
185
+ this.sendErrorResponse(response, 1, "No active runtime to restart");
186
+ return;
187
+ }
188
+ this.log("Restart requested");
189
+ this.resetRuntimeState();
190
+ this.queueAllBreakpointUpdates();
191
+ void this.doRestartRequest(response, runtime);
192
+ }
193
+ async doRestartRequest(response, runtime) {
194
+ try {
195
+ await runtime.restart();
196
+ this.sendResponse(response);
197
+ }
198
+ catch (error) {
199
+ this.sendErrorResponse(response, 1, `Restart failed: ${formatUnknownError(error)}`);
200
+ }
201
+ }
134
202
  async doLaunchRequest(response, args) {
135
203
  try {
136
204
  this.debugSessionLogging = args.debugSessionLogging ?? false;
@@ -142,6 +210,7 @@ export class WarpoDebugSession extends LoggingDebugSession {
142
210
  return;
143
211
  }
144
212
  const wasmFilePath = path.resolve(program);
213
+ configureTrace(args.debugSessionLogFile, cwd);
145
214
  this.log(`Launch requested for: ${program}`);
146
215
  if (launchType === "wasm file" || launchType === "unittest") {
147
216
  const runtimeName = args.runtime ?? "node";
@@ -156,6 +225,7 @@ export class WarpoDebugSession extends LoggingDebugSession {
156
225
  runtime.onModuleLoad = (wasmModule) => {
157
226
  this.loadedModule = wasmModule;
158
227
  this.log(`Wasm module loaded: ${wasmModule.wasmFilePath} (reported as ${wasmModule.url}, scriptId: ${wasmModule.scriptId})`);
228
+ this.log(`Module load breakpoint state: requestedSources=${this.requestedBreakpointsBySource.size}, pendingSources=${this.pendingBreakpointSources.size}`);
159
229
  this.sendEvent(new LoadedSourceEvent("new", {
160
230
  name: path.basename(wasmModule.wasmFilePath),
161
231
  path: wasmModule.wasmFilePath,
@@ -164,16 +234,7 @@ export class WarpoDebugSession extends LoggingDebugSession {
164
234
  name: path.basename(wasmModule.sourceMapFilePath),
165
235
  path: wasmModule.sourceMapFilePath,
166
236
  }));
167
- this.applyPendingBreakpointUpdates(runtime, () => {
168
- if (runtime.isPaused()) {
169
- void runtime.resume().catch((error) => {
170
- const message = error instanceof Error ? error.message : "unknown error";
171
- this.log(`Failed to resume after module load: ${message}`);
172
- });
173
- return;
174
- }
175
- runtime.finishModuleLoad();
176
- });
237
+ this.applyPendingBreakpointUpdates(runtime, () => this.maybeStartRuntime());
177
238
  };
178
239
  runtime.onPause = (info) => {
179
240
  this.handleRuntimePause(runtime, info);
@@ -188,7 +249,6 @@ export class WarpoDebugSession extends LoggingDebugSession {
188
249
  this.log(message);
189
250
  };
190
251
  this.disposeLoadedModule();
191
- this.runtime?.dispose();
192
252
  this.runtime = runtime;
193
253
  await runtime.launch(launchType === "unittest"
194
254
  ? {
@@ -236,9 +296,52 @@ export class WarpoDebugSession extends LoggingDebugSession {
236
296
  continueRequest(response, _args) {
237
297
  this.doContinueRequest(response);
238
298
  }
299
+ stepInRequest(response, _args) {
300
+ this.doStepRequest(response, StepMode.Into);
301
+ }
302
+ nextRequest(response, _args) {
303
+ this.doStepRequest(response, StepMode.Over);
304
+ }
305
+ pauseRequest(response, _args) {
306
+ const runtime = this.runtime;
307
+ if (!runtime) {
308
+ this.sendErrorResponse(response, 1, "No active runtime");
309
+ return;
310
+ }
311
+ if (runtime.isPaused()) {
312
+ this.sendResponse(response);
313
+ return;
314
+ }
315
+ this.cancelStep();
316
+ // StepMode.Into is used as a sentinel to reuse the step-pause handling path; no actual step-in is performed.
317
+ this.stepMode = StepMode.Into;
318
+ this.stepStartSourceLocation = undefined;
319
+ this.clearVariableContainers();
320
+ runtime.pause();
321
+ this.sendResponse(response);
322
+ }
323
+ terminateRequest(response, _args) {
324
+ const runtime = this.runtime;
325
+ if (runtime) {
326
+ void this.disposeRuntimeAndTerminate(runtime);
327
+ }
328
+ this.sendResponse(response);
329
+ }
330
+ stepOutRequest(response, _args) {
331
+ this.doStepRequest(response, StepMode.Out);
332
+ }
239
333
  disconnectRequest(response, _args) {
240
- this.runtime?.dispose();
334
+ void this.doDisconnectRequest(response);
335
+ }
336
+ async doDisconnectRequest(response) {
337
+ const runtime = this.runtime;
241
338
  this.runtime = undefined;
339
+ try {
340
+ await runtime?.dispose();
341
+ }
342
+ catch (error) {
343
+ this.log(`Failed to dispose runtime: ${formatUnknownError(error)}`);
344
+ }
242
345
  this.resetRuntimeState();
243
346
  this.debugSessionLogging = false;
244
347
  this.dispose();
@@ -301,6 +404,16 @@ export class WarpoDebugSession extends LoggingDebugSession {
301
404
  response.body = { variables: await this.decodeObjectAtAddress(variableContainer.address) };
302
405
  break;
303
406
  }
407
+ case "variables": {
408
+ response.body = { variables: variableContainer.variables };
409
+ break;
410
+ }
411
+ case "closure-scopes": {
412
+ response.body = {
413
+ variables: await this.resolveClosureScopeVariables(variableContainer.functionName, variableContainer.envAddress, variableContainer.scopeLevels),
414
+ };
415
+ break;
416
+ }
304
417
  case "map-entry": {
305
418
  response.body = {
306
419
  variables: [variableContainer.key, variableContainer.value].map((variable) => this.toDebugProtocolVariable(variable)),
@@ -366,24 +479,78 @@ export class WarpoDebugSession extends LoggingDebugSession {
366
479
  }
367
480
  const sourceVariables = sourceVariableScope.variables;
368
481
  const rawWasmValues = await this.resolveRawWasmValues(sourceVariables, runtimeVariablesByIndex, runtime);
369
- const closureTupleValuesByLevel = await this.readClosureTupleChain(sourceVariableScope.rootClosureEnvLocalIndex, runtimeVariablesByIndex);
370
- const scopeTupleLevel = sourceVariableScope.tupleLevel ?? 0;
371
- const rawLocalValues = sourceVariables.map((variable) => {
372
- if (variable.kind === "closure") {
373
- const elementIndex = variable.fieldOffset / SMALL_TUPLE_SLOT_SIZE;
374
- const levelValues = closureTupleValuesByLevel.get(scopeTupleLevel);
375
- const tupleRawValue = levelValues?.get(elementIndex);
376
- if (tupleRawValue === undefined) {
377
- return { name: variable.name, value: "<unavailable>", typeName: variable.typeName };
482
+ const firstTupleAddress = this.resolveClosureTupleAddress(sourceVariableScope.rootClosureEnvLocalIndex, runtimeVariablesByIndex);
483
+ const decodedScopes = await this.decodeScopeChainVariables([sourceVariableScope], firstTupleAddress, rawWasmValues);
484
+ return decodedScopes[0] ?? [];
485
+ }
486
+ async decodeScopeChainVariables(scopeLevels, firstTupleAddress, rawWasmValues) {
487
+ const closureTupleValuesByLevel = new Map();
488
+ const visitedAddresses = new Set();
489
+ if (firstTupleAddress !== undefined && Number.isInteger(firstTupleAddress) && firstTupleAddress > 0) {
490
+ let currentAddress = firstTupleAddress;
491
+ let tupleLevel = 0;
492
+ while (!visitedAddresses.has(currentAddress)) {
493
+ visitedAddresses.add(currentAddress);
494
+ const tupleValues = await this.readTupleElementValues(currentAddress);
495
+ const levelValues = new Map();
496
+ for (const [index, value] of tupleValues.entries()) {
497
+ levelValues.set(index, value.value);
498
+ }
499
+ closureTupleValuesByLevel.set(tupleLevel, levelValues);
500
+ const parentTuple = tupleValues[0];
501
+ if (!parentTuple) {
502
+ break;
503
+ }
504
+ const parentAddress = Number(parentTuple.value);
505
+ if (!Number.isInteger(parentAddress) || parentAddress <= 0) {
506
+ break;
378
507
  }
379
- return { name: variable.name, value: tupleRawValue, typeName: variable.typeName };
508
+ currentAddress = parentAddress;
509
+ tupleLevel++;
380
510
  }
381
- const wasmRawValue = rawWasmValues.get(variable);
382
- assert(wasmRawValue !== undefined);
383
- return { name: variable.name, value: wasmRawValue, typeName: variable.typeName };
511
+ }
512
+ return Promise.all(scopeLevels.map(async (scopeLevel) => {
513
+ const rawLocalValues = scopeLevel.variables.map((variable) => {
514
+ if (variable.kind === "closure") {
515
+ const elementIndex = variable.fieldOffset / SMALL_TUPLE_SLOT_SIZE;
516
+ const levelValues = closureTupleValuesByLevel.get(scopeLevel.tupleLevel ?? 0);
517
+ const tupleRawValue = levelValues?.get(elementIndex);
518
+ if (tupleRawValue === undefined) {
519
+ return { name: variable.name, value: "<unavailable>", typeName: variable.typeName };
520
+ }
521
+ return { name: variable.name, value: tupleRawValue, typeName: variable.typeName };
522
+ }
523
+ const wasmRawValue = rawWasmValues.get(variable);
524
+ return {
525
+ name: variable.name,
526
+ value: wasmRawValue ?? "<unavailable>",
527
+ typeName: variable.typeName,
528
+ };
529
+ });
530
+ const variables = await Promise.all(rawLocalValues.map((variable) => this.toDebugSessionVariableValue(variable.name, variable.typeName, variable.value)));
531
+ return variables.map((variable) => this.toDebugProtocolVariable(variable));
532
+ }));
533
+ }
534
+ async resolveClosureScopeVariables(functionName, envAddress, scopeLevels) {
535
+ const wasmModule = this.loadedModule;
536
+ if (!wasmModule) {
537
+ this.log(`Warning: cannot expand closure scopes for ${functionName}: wasm module is not loaded`);
538
+ return [];
539
+ }
540
+ if (!scopeLevels) {
541
+ this.log(`Warning: cannot expand closure scopes for ${functionName}: DWARF function is unavailable`);
542
+ return [];
543
+ }
544
+ const decodedScopeVariables = await this.decodeScopeChainVariables(scopeLevels, envAddress, new Map());
545
+ return scopeLevels.map((scopeLevel, index) => {
546
+ const variable = {
547
+ kind: "synthetic",
548
+ name: index.toString(),
549
+ value: scopeLevel.name,
550
+ container: { kind: "variables", variables: decodedScopeVariables[index] ?? [] },
551
+ };
552
+ return this.toDebugProtocolVariable(variable);
384
553
  });
385
- const variables = await Promise.all(rawLocalValues.map((variable) => this.toDebugSessionVariableValue(variable.name, variable.typeName, variable.value)));
386
- return variables.map((variable) => this.toDebugProtocolVariable(variable));
387
554
  }
388
555
  async decodeObjectAtAddress(address) {
389
556
  const fields = await this.resolveObjectFields(address);
@@ -408,39 +575,16 @@ export class WarpoDebugSession extends LoggingDebugSession {
408
575
  }
409
576
  return rawWasmValues;
410
577
  }
411
- async readClosureTupleChain(closureEnvLocalIndex, runtimeVariablesByIndex) {
412
- const closureTupleValuesByLevel = new Map();
578
+ resolveClosureTupleAddress(closureEnvLocalIndex, runtimeVariablesByIndex) {
413
579
  if (closureEnvLocalIndex === undefined) {
414
- return closureTupleValuesByLevel;
580
+ return undefined;
415
581
  }
416
582
  const closureEnvVariable = runtimeVariablesByIndex.get(closureEnvLocalIndex);
417
583
  if (closureEnvVariable === undefined) {
418
- return closureTupleValuesByLevel;
419
- }
420
- let address = Number(closureEnvVariable.value);
421
- if (!Number.isInteger(address) || address <= 0) {
422
- return closureTupleValuesByLevel;
423
- }
424
- let level = 0;
425
- while (true) {
426
- const tupleValues = await this.readTupleElementValues(address);
427
- const levelValues = new Map();
428
- for (const [index, value] of tupleValues.entries()) {
429
- levelValues.set(index, value.value);
430
- }
431
- closureTupleValuesByLevel.set(level, levelValues);
432
- const parentAddress = levelValues.get(0);
433
- if (parentAddress === undefined) {
434
- break;
435
- }
436
- const parentAddressNumber = Number(parentAddress);
437
- if (!Number.isInteger(parentAddressNumber) || parentAddressNumber <= 0) {
438
- break;
439
- }
440
- address = parentAddressNumber;
441
- level++;
584
+ return undefined;
442
585
  }
443
- return closureTupleValuesByLevel;
586
+ const address = Number(closureEnvVariable.value);
587
+ return Number.isInteger(address) && address > 0 ? address : undefined;
444
588
  }
445
589
  resolvePausedStackFrame(frameId) {
446
590
  return this.pausedStackFramesById.get(frameId);
@@ -452,7 +596,7 @@ export class WarpoDebugSession extends LoggingDebugSession {
452
596
  if (typeName !== undefined) {
453
597
  const objectAddress = this.parseObjectAddress(typeName, value);
454
598
  if (objectAddress) {
455
- if (typeName === AS_STRING_CLASS_NAME) {
599
+ if (isDisplayableStringClass(typeName)) {
456
600
  const stringValue = await this.decodeStringAtAddress(typeName, objectAddress);
457
601
  return this.createClassVariable(name, typeName, objectAddress, stringValue);
458
602
  }
@@ -487,12 +631,18 @@ export class WarpoDebugSession extends LoggingDebugSession {
487
631
  case "map-entry": {
488
632
  return "";
489
633
  }
634
+ case "synthetic": {
635
+ return variable.value;
636
+ }
490
637
  }
491
638
  }
492
639
  getVariableReference(variable, isExpandableClass) {
493
640
  if (isExpandableClass && variable.kind === "class") {
494
641
  return this.toObjectVariablesReference(variable.address);
495
642
  }
643
+ if (variable.kind === "synthetic") {
644
+ return variable.container === undefined ? 0 : this.createVariableContainer(variable.container);
645
+ }
496
646
  if (variable.kind === "map-entry") {
497
647
  return this.createVariableContainer({ kind: "map-entry", key: variable.key, value: variable.value });
498
648
  }
@@ -502,7 +652,7 @@ export class WarpoDebugSession extends LoggingDebugSession {
502
652
  return { kind: "class", name, typeName, address, displayValue };
503
653
  }
504
654
  parseObjectAddress(typeName, value) {
505
- if (!this.loadedModule?.getClassLayout(typeName)) {
655
+ if (typeName !== UTF8_CONST_STR_CLASS_NAME && !this.loadedModule?.getClassLayout(typeName)) {
506
656
  return undefined;
507
657
  }
508
658
  const address = Number(value);
@@ -599,9 +749,16 @@ export class WarpoDebugSession extends LoggingDebugSession {
599
749
  return [];
600
750
  }
601
751
  const view = new DataView(memory.buffer, memory.byteOffset, memory.byteLength);
752
+ if (classLayout.builtinKind === BuiltinContainerKind.Function) {
753
+ return this.decodeFunctionElements(view, classLayout);
754
+ }
602
755
  if (classLayout.builtinKind === BuiltinContainerKind.ArrayBuffer) {
603
756
  return this.decodeArrayBufferElements(view);
604
757
  }
758
+ const typedArrayElementType = TYPED_ARRAY_ELEMENT_TYPES.get(classLayout.name);
759
+ if (typedArrayElementType !== undefined) {
760
+ return this.decodeTypedArrayElements(address, view, classLayout, typedArrayElementType);
761
+ }
605
762
  if (classLayout.builtinKind === BuiltinContainerKind.Array) {
606
763
  return this.decodeArrayElements(address, view, classLayout);
607
764
  }
@@ -616,10 +773,49 @@ export class WarpoDebugSession extends LoggingDebugSession {
616
773
  }
617
774
  return Promise.all(classLayout.fields.map((field) => this.decodeFieldVariable(view, field)));
618
775
  }
776
+ decodeFunctionElements(view, classLayout) {
777
+ const indexField = classLayout.fields.find((field) => field.name === "_index" || field.name === "index");
778
+ let functionName;
779
+ if (indexField) {
780
+ const indexValue = this.decodeFieldValue(view, indexField);
781
+ const functionIndex = Number(indexValue);
782
+ if (Number.isInteger(functionIndex) && functionIndex >= 0) {
783
+ functionName = this.loadedModule?.getFunctionNameByTableIndex(functionIndex);
784
+ }
785
+ }
786
+ else {
787
+ this.log(`Warning: cannot expand function: index field is unavailable`);
788
+ }
789
+ const envField = classLayout.fields.find((field) => field.name === "_env" || field.name === "env");
790
+ let envAddress;
791
+ if (envField) {
792
+ const envValue = Number(this.decodeFieldValue(view, envField));
793
+ if (Number.isInteger(envValue) && envValue > 0) {
794
+ envAddress = envValue;
795
+ }
796
+ }
797
+ else {
798
+ this.log(`Warning: cannot expand function: env field is unavailable`);
799
+ }
800
+ const closureScopes = functionName === undefined ? undefined : this.loadedModule?.getClosureVariableScopesByFunctionName(functionName);
801
+ return [
802
+ { kind: "basic", name: "name", value: functionName ?? "<unavailable>", typeName: "string" },
803
+ {
804
+ kind: "synthetic",
805
+ name: "[[scopes]]",
806
+ value: `Scopes[${closureScopes?.length ?? 0}]`,
807
+ typeName: "Scopes",
808
+ container: functionName === undefined || envAddress === undefined
809
+ ? undefined
810
+ : { kind: "closure-scopes", functionName, envAddress, scopeLevels: closureScopes },
811
+ },
812
+ ];
813
+ }
619
814
  async resolveRuntimeObjectPayloadSize(address, classLayout) {
620
815
  if (classLayout.builtinKind !== BuiltinContainerKind.SmallTuple &&
621
816
  classLayout.builtinKind !== BuiltinContainerKind.StaticArray &&
622
- classLayout.builtinKind !== BuiltinContainerKind.ArrayBuffer) {
817
+ classLayout.builtinKind !== BuiltinContainerKind.ArrayBuffer &&
818
+ !TYPED_ARRAY_ELEMENT_TYPES.has(classLayout.name)) {
623
819
  return classLayout.byteSize;
624
820
  }
625
821
  const sizeMemory = await this.runtime?.readWasmMemory(address + OBJECT_RTSIZE_OFFSET, OBJECT_RTSIZE_SIZE);
@@ -659,7 +855,7 @@ export class WarpoDebugSession extends LoggingDebugSession {
659
855
  this.log(`Error: cannot expand tuple reference at ${elementAddress}: runtime class layout is missing`);
660
856
  return { kind: "basic", name, value: elementAddress.toString(), typeName: undefined };
661
857
  }
662
- if (classLayout.name === AS_STRING_CLASS_NAME) {
858
+ if (isDisplayableStringClass(classLayout.name)) {
663
859
  const stringValue = await this.decodeStringAtAddress(classLayout.name, elementAddress);
664
860
  return this.createClassVariable(name, classLayout.name, elementAddress, stringValue);
665
861
  }
@@ -699,6 +895,32 @@ export class WarpoDebugSession extends LoggingDebugSession {
699
895
  }
700
896
  return elements;
701
897
  }
898
+ async decodeTypedArrayElements(address, view, classLayout, elementTypeName) {
899
+ const dataStartField = classLayout.fields.find((field) => field.name === "dataStart");
900
+ const byteLengthField = classLayout.fields.find((field) => field.name === "byteLength");
901
+ if (!dataStartField || !byteLengthField) {
902
+ this.log(`Error: cannot expand typed array at ${address}: typed array header is unavailable`);
903
+ return [];
904
+ }
905
+ const dataStart = Number(this.decodeFieldValue(view, dataStartField));
906
+ const byteLength = Number(this.decodeFieldValue(view, byteLengthField));
907
+ const elementSize = resolveArrayElementSize(elementTypeName, false);
908
+ if (!Number.isInteger(dataStart) ||
909
+ dataStart < 0 ||
910
+ !Number.isInteger(byteLength) ||
911
+ byteLength < 0 ||
912
+ byteLength % elementSize !== 0) {
913
+ this.log(`Error: cannot expand typed array at ${address}: typed array data is invalid`);
914
+ return [];
915
+ }
916
+ const elementsMemory = await this.runtime?.readWasmMemory(dataStart, byteLength);
917
+ if (!elementsMemory) {
918
+ this.log(`Error: cannot expand typed array at ${address}: typed array data is unavailable`);
919
+ return [];
920
+ }
921
+ const elementsView = new DataView(elementsMemory.buffer, elementsMemory.byteOffset, elementsMemory.byteLength);
922
+ return this.decodeArrayElementVariables(elementsView, byteLength / elementSize, elementTypeName, elementSize, false);
923
+ }
702
924
  async decodeStaticArrayElements(address, view, classLayout) {
703
925
  assert.equal(classLayout.builtinKind, BuiltinContainerKind.StaticArray);
704
926
  const elementTypeName = classLayout.templateType;
@@ -857,7 +1079,7 @@ export class WarpoDebugSession extends LoggingDebugSession {
857
1079
  const value = this.decodeFieldValue(view, field);
858
1080
  const address = field.isReference ? this.parseObjectAddress(field.typeName, value) : undefined;
859
1081
  if (address) {
860
- if (field.typeName === AS_STRING_CLASS_NAME) {
1082
+ if (isDisplayableStringClass(field.typeName)) {
861
1083
  const stringValue = await this.decodeStringAtAddress(field.typeName, address);
862
1084
  return this.createClassVariable(field.name, field.typeName, address, stringValue);
863
1085
  }
@@ -939,7 +1161,7 @@ export class WarpoDebugSession extends LoggingDebugSession {
939
1161
  }
940
1162
  }
941
1163
  async decodeStringAtAddress(typeName, address) {
942
- assert.equal(typeName, AS_STRING_CLASS_NAME);
1164
+ assert.ok(isDisplayableStringClass(typeName));
943
1165
  if (!this.runtime) {
944
1166
  this.log(`Error: cannot decode string at ${address}: runtime is not active`);
945
1167
  return "<unavailable>";
@@ -956,7 +1178,8 @@ export class WarpoDebugSession extends LoggingDebugSession {
956
1178
  this.log(`Error: cannot decode string at ${address}: string payload memory is unavailable`);
957
1179
  return "<unavailable>";
958
1180
  }
959
- return JSON.stringify(Buffer.from(stringMemory).toString("utf16le"));
1181
+ const encoding = typeName === UTF8_CONST_STR_CLASS_NAME ? "utf8" : "utf16le";
1182
+ return JSON.stringify(Buffer.from(stringMemory).toString(encoding));
960
1183
  }
961
1184
  doContinueRequest(response) {
962
1185
  const runtime = this.runtime;
@@ -965,13 +1188,107 @@ export class WarpoDebugSession extends LoggingDebugSession {
965
1188
  return;
966
1189
  }
967
1190
  response.body = { allThreadsContinued: true };
968
- this.sendResponse(response);
969
1191
  if (this.stoppedForTrap) {
1192
+ this.sendResponse(response);
970
1193
  this.log("Continue requested from wasm trap; terminating debug session");
971
- this.disposeRuntimeAndTerminate(runtime);
1194
+ void this.disposeRuntimeAndTerminate(runtime);
972
1195
  return;
973
1196
  }
1197
+ this.cancelStep();
974
1198
  void this.resumeRuntime(runtime);
1199
+ this.sendResponse(response);
1200
+ }
1201
+ doStepRequest(response, mode) {
1202
+ const runtime = this.runtime;
1203
+ if (!runtime) {
1204
+ this.sendErrorResponse(response, 1, "No active runtime");
1205
+ return;
1206
+ }
1207
+ if (!runtime.isPaused()) {
1208
+ this.sendErrorResponse(response, 1, "Runtime is not paused");
1209
+ return;
1210
+ }
1211
+ if (this.stoppedForTrap) {
1212
+ this.sendResponse(response);
1213
+ this.log("Step requested from wasm trap; terminating debug session");
1214
+ void this.disposeRuntimeAndTerminate(runtime);
1215
+ return;
1216
+ }
1217
+ this.stepMode = mode;
1218
+ this.stepStartSourceLocation = this.resolveSourceLocation(this.stoppedWasmBytecodeOffset);
1219
+ this.clearVariableContainers();
1220
+ this.sendResponse(response);
1221
+ this.sendEvent(new ContinuedEvent(WarpoDebugSession.threadId, true));
1222
+ void this.advanceStep(runtime).catch((error) => {
1223
+ this.handleStepFailure(error);
1224
+ });
1225
+ }
1226
+ async handleStepPause(runtime, info) {
1227
+ try {
1228
+ if (this.hasReachedNextSourceLine(info.wasmBytecodeOffset)) {
1229
+ this.completeStep(runtime, info);
1230
+ return;
1231
+ }
1232
+ await this.advanceStep(runtime);
1233
+ }
1234
+ catch (error) {
1235
+ this.handleStepFailure(error);
1236
+ }
1237
+ }
1238
+ handleNonStepPause(runtime, info) {
1239
+ this.cancelStep();
1240
+ if (this.runtime !== runtime) {
1241
+ return;
1242
+ }
1243
+ this.sendStoppedEvent(runtime, info);
1244
+ }
1245
+ hasReachedNextSourceLine(wasmBytecodeOffset) {
1246
+ const sourceLocation = this.resolveSourceLocation(wasmBytecodeOffset);
1247
+ if (!sourceLocation) {
1248
+ return false;
1249
+ }
1250
+ const startSourceLocation = this.stepStartSourceLocation;
1251
+ return (startSourceLocation === undefined ||
1252
+ sourceLocation.sourcePath !== startSourceLocation.sourcePath ||
1253
+ sourceLocation.sourceLine !== startSourceLocation.sourceLine);
1254
+ }
1255
+ async advanceStep(runtime) {
1256
+ if (this.runtime !== runtime || this.stepMode === StepMode.None) {
1257
+ return;
1258
+ }
1259
+ const wasmModule = this.loadedModule;
1260
+ const wasmBytecodeOffset = this.stoppedWasmBytecodeOffset;
1261
+ if (this.stepMode === StepMode.Out) {
1262
+ this.stepMode = StepMode.Into;
1263
+ await runtime.stepOut();
1264
+ }
1265
+ else if (this.stepMode === StepMode.Over &&
1266
+ wasmModule &&
1267
+ wasmBytecodeOffset !== undefined &&
1268
+ wasmModule.getNextBytecodeOffsetAfterCall(wasmBytecodeOffset) !== undefined) {
1269
+ await runtime.stepOver();
1270
+ }
1271
+ else {
1272
+ // Source-level stepping advances through runtime primitives until the source location changes. Calls use the
1273
+ // runtime's call-aware operation because their following bytecode may be a structural `end`, not an executable instruction.
1274
+ await runtime.stepInstruction();
1275
+ }
1276
+ }
1277
+ completeStep(runtime, info) {
1278
+ this.cancelStep();
1279
+ if (this.runtime !== runtime) {
1280
+ return;
1281
+ }
1282
+ this.clearVariableContainers();
1283
+ this.sendStoppedEvent(runtime, info);
1284
+ }
1285
+ handleStepFailure(error) {
1286
+ this.log(`Step failed: ${formatUnknownError(error)}`);
1287
+ this.cancelStep();
1288
+ }
1289
+ cancelStep() {
1290
+ this.stepMode = StepMode.None;
1291
+ this.stepStartSourceLocation = undefined;
975
1292
  }
976
1293
  async resumeRuntime(runtime) {
977
1294
  try {
@@ -992,13 +1309,18 @@ export class WarpoDebugSession extends LoggingDebugSession {
992
1309
  if (this.runtime !== runtime) {
993
1310
  return;
994
1311
  }
995
- this.disposeRuntimeAndTerminate(runtime);
1312
+ void this.disposeRuntimeAndTerminate(runtime);
996
1313
  }
997
- disposeRuntimeAndTerminate(runtime) {
998
- runtime.dispose();
1314
+ async disposeRuntimeAndTerminate(runtime) {
999
1315
  if (this.runtime === runtime) {
1000
1316
  this.runtime = undefined;
1001
1317
  }
1318
+ try {
1319
+ await runtime.dispose();
1320
+ }
1321
+ catch (error) {
1322
+ this.log(`Failed to dispose runtime: ${formatUnknownError(error)}`);
1323
+ }
1002
1324
  this.resetRuntimeState();
1003
1325
  this.sendEvent(new TerminatedEvent());
1004
1326
  }
@@ -1007,6 +1329,13 @@ export class WarpoDebugSession extends LoggingDebugSession {
1007
1329
  this.pendingBreakpointSources.clear();
1008
1330
  this.stoppedWasmBytecodeOffset = undefined;
1009
1331
  this.stoppedForTrap = false;
1332
+ this.stepMode = StepMode.None;
1333
+ this.stepStartSourceLocation = undefined;
1334
+ }
1335
+ queueAllBreakpointUpdates() {
1336
+ for (const sourcePath of this.requestedBreakpointsBySource.keys()) {
1337
+ this.pendingBreakpointSources.add(sourcePath);
1338
+ }
1010
1339
  }
1011
1340
  logAllBreakpoints() {
1012
1341
  this.log("=== Registered Breakpoints ===");
@@ -1017,8 +1346,15 @@ export class WarpoDebugSession extends LoggingDebugSession {
1017
1346
  }
1018
1347
  this.log("==============================");
1019
1348
  }
1349
+ toDebugProtocolBreakpoint(breakpoint) {
1350
+ const dbp = new Breakpoint(breakpoint.verified, breakpoint.line);
1351
+ dbp.id = breakpoint.id;
1352
+ return dbp;
1353
+ }
1020
1354
  requestBreakpointUpdate(onComplete) {
1355
+ this.log(`Breakpoint update requested: hasRuntime=${this.runtime !== undefined}, hasModule=${this.loadedModule !== undefined}, pendingSources=${this.pendingBreakpointSources.size}, runtimePaused=${this.runtime?.isPaused() ?? false}`);
1021
1356
  if (!this.runtime || !this.loadedModule || this.pendingBreakpointSources.size === 0) {
1357
+ this.log("Breakpoint update skipped: runtime, module, or pending source is unavailable");
1022
1358
  onComplete?.();
1023
1359
  return;
1024
1360
  }
@@ -1028,33 +1364,40 @@ export class WarpoDebugSession extends LoggingDebugSession {
1028
1364
  }
1029
1365
  this.runtime.pause();
1030
1366
  }
1367
+ maybeStartRuntime() {
1368
+ const runtime = this.runtime;
1369
+ if (!this.configurationDone || !runtime || !this.loadedModule) {
1370
+ return;
1371
+ }
1372
+ if (runtime.isPaused()) {
1373
+ void runtime.resume().catch((error) => {
1374
+ const message = error instanceof Error ? error.message : "unknown error";
1375
+ this.log(`Failed to resume after configuration: ${message}`);
1376
+ });
1377
+ return;
1378
+ }
1379
+ runtime.finishModuleLoad();
1380
+ }
1031
1381
  applyPendingBreakpointUpdates(runtime, onComplete) {
1032
1382
  const wasmModule = this.loadedModule;
1033
1383
  if (!wasmModule) {
1384
+ this.log("Breakpoint update skipped: no loaded wasm module");
1034
1385
  onComplete?.();
1035
1386
  return;
1036
1387
  }
1037
1388
  if (this.pendingBreakpointSources.size === 0) {
1389
+ this.log("Breakpoint update skipped: no pending breakpoint sources");
1038
1390
  onComplete?.();
1039
1391
  return;
1040
1392
  }
1393
+ this.log(`Applying breakpoint updates: requestedSources=${this.requestedBreakpointsBySource.size}, pendingSources=${this.pendingBreakpointSources.size}`);
1041
1394
  this.pendingBreakpointSources.clear();
1042
- const wasmBytecodeOffsets = new Set();
1043
- for (const [sourcePath, breakpoints] of this.requestedBreakpointsBySource) {
1044
- for (const breakpoint of breakpoints) {
1045
- const breakpointLocations = wasmModule.resolveBreakpointLocations(breakpoint);
1046
- if (breakpointLocations.length === 0) {
1047
- this.log(`No source-map match for breakpoint ${path.basename(sourcePath)}:${breakpoint.line}`);
1048
- }
1049
- for (const location of breakpointLocations) {
1050
- wasmBytecodeOffsets.add(location.wasmBytecodeOffset);
1051
- this.log(`Resolved breakpoint ${path.basename(sourcePath)}:${breakpoint.line} -> ${location.wasmBytecodeOffset}`);
1052
- }
1053
- }
1054
- }
1395
+ const wasmBytecodeOffsets = this.collectBreakpointOffsets(wasmModule);
1055
1396
  runtime.setWasmBreakpoints(wasmModule, Array.from(wasmBytecodeOffsets).toSorted((left, right) => left - right), {
1056
1397
  onSuccess: () => {
1057
- this.log(`Breakpoint set installed: ${wasmBytecodeOffsets.size} locations`);
1398
+ this.log(`Breakpoint set installed: ${wasmBytecodeOffsets.size} locations, offsets=${Array.from(wasmBytecodeOffsets)
1399
+ .toSorted((left, right) => left - right)
1400
+ .join(",") || "none"}`);
1058
1401
  onComplete?.();
1059
1402
  },
1060
1403
  onError: (error) => {
@@ -1063,6 +1406,32 @@ export class WarpoDebugSession extends LoggingDebugSession {
1063
1406
  },
1064
1407
  });
1065
1408
  }
1409
+ collectBreakpointOffsets(wasmModule) {
1410
+ const wasmBytecodeOffsets = new Set();
1411
+ for (const [sourcePath, breakpoints] of this.requestedBreakpointsBySource) {
1412
+ for (const breakpoint of breakpoints) {
1413
+ this.collectBreakpointLocations(wasmModule, sourcePath, breakpoint, wasmBytecodeOffsets);
1414
+ }
1415
+ }
1416
+ return wasmBytecodeOffsets;
1417
+ }
1418
+ collectBreakpointLocations(wasmModule, sourcePath, breakpoint, wasmBytecodeOffsets) {
1419
+ const sourceMapHasSource = wasmModule.hasSource(sourcePath);
1420
+ this.log(`Resolving breakpoint: source=${sourcePath}, line=${breakpoint.line}, sourceMapHasSource=${sourceMapHasSource}`);
1421
+ const breakpointLocations = wasmModule.resolveBreakpointLocations(breakpoint);
1422
+ if (breakpointLocations.length > 0 && !breakpoint.verified) {
1423
+ breakpoint.verified = true;
1424
+ this.sendEvent(new BreakpointEvent("changed", this.toDebugProtocolBreakpoint(breakpoint)));
1425
+ }
1426
+ if (breakpointLocations.length === 0) {
1427
+ this.log(`Source-map lookup failed for breakpoint ${path.basename(sourcePath)}:${breakpoint.line}: ${sourceMapHasSource ? "source present but line has no mapping" : "source is absent"}`);
1428
+ }
1429
+ this.log(`Breakpoint resolution result: source=${sourcePath}, line=${breakpoint.line}, locations=${breakpointLocations.length}, offsets=${breakpointLocations.map((location) => location.wasmBytecodeOffset).join(",") || "none"}`);
1430
+ for (const location of breakpointLocations) {
1431
+ wasmBytecodeOffsets.add(location.wasmBytecodeOffset);
1432
+ this.log(`Resolved breakpoint ${path.basename(sourcePath)}:${breakpoint.line} -> ${location.wasmBytecodeOffset}`);
1433
+ }
1434
+ }
1066
1435
  disposeLoadedModule() {
1067
1436
  this.clearVariableContainers();
1068
1437
  this.loadedModule?.dispose();