warpo 3.1.1 → 3.1.3

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 (189) hide show
  1. package/asconfig.schema.json +10 -2
  2. package/dist/debug_server/debugPath.d.ts +1 -0
  3. package/dist/debug_server/debugPath.js +10 -0
  4. package/dist/debug_server/debugPath.js.map +1 -0
  5. package/dist/debug_server/debugSession.d.ts +61 -3
  6. package/dist/debug_server/debugSession.js +919 -80
  7. package/dist/debug_server/debugSession.js.map +1 -1
  8. package/dist/debug_server/debugger.d.ts +30 -2
  9. package/dist/debug_server/debuggerWasmModule.d.ts +59 -3
  10. package/dist/debug_server/debuggerWasmModule.js +234 -14
  11. package/dist/debug_server/debuggerWasmModule.js.map +1 -1
  12. package/dist/debug_server/nodeDebugger.d.ts +42 -3
  13. package/dist/debug_server/nodeDebugger.js +648 -59
  14. package/dist/debug_server/nodeDebugger.js.map +1 -1
  15. package/dist/debug_server/tests/debugSession.test.js +1154 -26
  16. package/dist/debug_server/tests/debugSession.test.js.map +1 -1
  17. package/dist/debug_server/tests/launcher.js +1 -0
  18. package/dist/debug_server/tests/launcher.js.map +1 -1
  19. package/dist/debug_server/tests/testModule/debugger_array.d.ts +1 -0
  20. package/dist/debug_server/tests/testModule/debugger_array.js +9 -0
  21. package/dist/debug_server/tests/testModule/debugger_array.js.map +1 -0
  22. package/dist/debug_server/tests/testModule/debugger_array_buffer.d.ts +1 -0
  23. package/dist/debug_server/tests/testModule/debugger_array_buffer.js +12 -0
  24. package/dist/debug_server/tests/testModule/debugger_array_buffer.js.map +1 -0
  25. package/dist/debug_server/tests/testModule/debugger_basic.d.ts +1 -0
  26. package/dist/debug_server/tests/testModule/debugger_basic.js +9 -1
  27. package/dist/debug_server/tests/testModule/debugger_basic.js.map +1 -1
  28. package/dist/debug_server/tests/testModule/debugger_breakpoint_update.d.ts +1 -0
  29. package/dist/debug_server/tests/testModule/debugger_breakpoint_update.js +10 -0
  30. package/dist/debug_server/tests/testModule/debugger_breakpoint_update.js.map +1 -0
  31. package/dist/debug_server/tests/testModule/debugger_callee.d.ts +10 -0
  32. package/dist/debug_server/tests/testModule/debugger_callee.js +106 -0
  33. package/dist/debug_server/tests/testModule/debugger_callee.js.map +1 -0
  34. package/dist/debug_server/tests/testModule/debugger_caller.d.ts +1 -0
  35. package/dist/debug_server/tests/testModule/debugger_caller.js +7 -0
  36. package/dist/debug_server/tests/testModule/debugger_caller.js.map +1 -0
  37. package/dist/debug_server/tests/testModule/debugger_class.d.ts +1 -0
  38. package/dist/debug_server/tests/testModule/debugger_class.js +39 -0
  39. package/dist/debug_server/tests/testModule/debugger_class.js.map +1 -0
  40. package/dist/debug_server/tests/testModule/debugger_class_array.d.ts +1 -0
  41. package/dist/debug_server/tests/testModule/debugger_class_array.js +15 -0
  42. package/dist/debug_server/tests/testModule/debugger_class_array.js.map +1 -0
  43. package/dist/debug_server/tests/testModule/debugger_closure.d.ts +1 -0
  44. package/dist/debug_server/tests/testModule/debugger_closure.js +48 -0
  45. package/dist/debug_server/tests/testModule/debugger_closure.js.map +1 -0
  46. package/dist/debug_server/tests/testModule/debugger_closure_if.d.ts +1 -0
  47. package/dist/debug_server/tests/testModule/debugger_closure_if.js +16 -0
  48. package/dist/debug_server/tests/testModule/debugger_closure_if.js.map +1 -0
  49. package/dist/debug_server/tests/testModule/debugger_generic_function.d.ts +1 -0
  50. package/dist/debug_server/tests/testModule/debugger_generic_function.js +10 -0
  51. package/dist/debug_server/tests/testModule/debugger_generic_function.js.map +1 -0
  52. package/dist/debug_server/tests/testModule/debugger_global.d.ts +1 -0
  53. package/dist/debug_server/tests/testModule/debugger_global.js +11 -0
  54. package/dist/debug_server/tests/testModule/debugger_global.js.map +1 -0
  55. package/dist/debug_server/tests/testModule/debugger_map.d.ts +1 -0
  56. package/dist/debug_server/tests/testModule/debugger_map.js +22 -0
  57. package/dist/debug_server/tests/testModule/debugger_map.js.map +1 -0
  58. package/dist/debug_server/tests/testModule/debugger_member_function.d.ts +1 -0
  59. package/dist/debug_server/tests/testModule/debugger_member_function.js +16 -0
  60. package/dist/debug_server/tests/testModule/debugger_member_function.js.map +1 -0
  61. package/dist/debug_server/tests/testModule/debugger_scopes.d.ts +1 -0
  62. package/dist/debug_server/tests/testModule/debugger_scopes.js +22 -0
  63. package/dist/debug_server/tests/testModule/debugger_scopes.js.map +1 -0
  64. package/dist/debug_server/tests/testModule/debugger_set.d.ts +1 -0
  65. package/dist/debug_server/tests/testModule/debugger_set.js +23 -0
  66. package/dist/debug_server/tests/testModule/debugger_set.js.map +1 -0
  67. package/dist/debug_server/tests/testModule/debugger_static_array.d.ts +1 -0
  68. package/dist/debug_server/tests/testModule/debugger_static_array.js +21 -0
  69. package/dist/debug_server/tests/testModule/debugger_static_array.js.map +1 -0
  70. package/dist/debug_server/tests/testModule/debugger_string.d.ts +1 -0
  71. package/dist/debug_server/tests/testModule/debugger_string.js +16 -0
  72. package/dist/debug_server/tests/testModule/debugger_string.js.map +1 -0
  73. package/dist/debug_server/tests/testModule/debugger_trap.d.ts +1 -0
  74. package/dist/debug_server/tests/testModule/debugger_trap.js +9 -0
  75. package/dist/debug_server/tests/testModule/debugger_trap.js.map +1 -0
  76. package/dist/debug_server/tests/testModule/debugger_tuple.d.ts +1 -0
  77. package/dist/debug_server/tests/testModule/debugger_tuple.js +15 -0
  78. package/dist/debug_server/tests/testModule/debugger_tuple.js.map +1 -0
  79. package/dist/debug_server/tests/testModule/import_failure.d.ts +1 -0
  80. package/dist/debug_server/tests/testModule/import_failure.js +4 -0
  81. package/dist/debug_server/tests/testModule/import_failure.js.map +1 -0
  82. package/dist/debug_server/tests/testModule/unittest_debug.case.js +10 -0
  83. package/dist/debug_server/tests/testModule/unittest_debug.case.js.map +1 -0
  84. package/dist/debug_server/tests/testModule/unittest_debug.d.ts +1 -0
  85. package/dist/debug_server/tests/testModule/unittest_debug.js +6 -0
  86. package/dist/debug_server/tests/testModule/unittest_debug.js.map +1 -0
  87. package/dist/debug_server/wasmEntry.js +32 -12
  88. package/dist/debug_server/wasmEntry.js.map +1 -1
  89. package/dist/dwarf/classDebugInfo.d.ts +45 -0
  90. package/dist/dwarf/classDebugInfo.js +292 -0
  91. package/dist/dwarf/classDebugInfo.js.map +1 -0
  92. package/dist/{heap_analyzer/src → dwarf}/dwarfParser.js +2 -0
  93. package/dist/dwarf/dwarfParser.js.map +1 -0
  94. package/dist/dwarf/functionDebugInfo.d.ts +63 -0
  95. package/dist/dwarf/functionDebugInfo.js +377 -0
  96. package/dist/dwarf/functionDebugInfo.js.map +1 -0
  97. package/dist/dwarf/tests/fixtures/functionDebugInfo.d.ts +3 -0
  98. package/dist/dwarf/tests/fixtures/functionDebugInfo.js +24 -0
  99. package/dist/dwarf/tests/fixtures/functionDebugInfo.js.map +1 -0
  100. package/dist/dwarf/tests/functionDebugInfo.test.d.ts +1 -0
  101. package/dist/dwarf/tests/functionDebugInfo.test.js +102 -0
  102. package/dist/dwarf/tests/functionDebugInfo.test.js.map +1 -0
  103. package/dist/heap_analyzer/cli.js +1 -1
  104. package/dist/heap_analyzer/cli.js.map +1 -1
  105. package/dist/heap_analyzer/src/constants.js +2 -1
  106. package/dist/heap_analyzer/src/constants.js.map +1 -1
  107. package/dist/heap_analyzer/src/debugInfoResolver.d.ts +4 -15
  108. package/dist/heap_analyzer/src/debugInfoResolver.js +20 -231
  109. package/dist/heap_analyzer/src/debugInfoResolver.js.map +1 -1
  110. package/dist/heap_analyzer/src/dominator.js +4 -1
  111. package/dist/heap_analyzer/src/dominator.js.map +1 -1
  112. package/dist/heap_analyzer/src/referenceScanner.js +7 -7
  113. package/dist/heap_analyzer/src/referenceScanner.js.map +1 -1
  114. package/dist/heap_analyzer/src/retainedSize.js +2 -0
  115. package/dist/heap_analyzer/src/retainedSize.js.map +1 -1
  116. package/dist/heap_analyzer/src/snapshot.js +1 -1
  117. package/dist/heap_analyzer/src/snapshot.js.map +1 -1
  118. package/dist/heap_analyzer/src/{tlsf.js → tlsfBlockWalker.js} +4 -3
  119. package/dist/heap_analyzer/src/tlsfBlockWalker.js.map +1 -0
  120. package/dist/heap_analyzer/src/types.d.ts +3 -32
  121. package/dist/heap_analyzer/src/types.js +1 -8
  122. package/dist/heap_analyzer/src/types.js.map +1 -1
  123. package/dist/heap_analyzer/tests/debugInfoResolver.test.js +1 -1
  124. package/dist/heap_analyzer/tests/debugInfoResolver.test.js.map +1 -1
  125. package/dist/heap_analyzer/tests/dwarfParser.test.js +57 -40
  126. package/dist/heap_analyzer/tests/dwarfParser.test.js.map +1 -1
  127. package/dist/heap_analyzer/tests/roots.test.js +1 -1
  128. package/dist/heap_analyzer/tests/roots.test.js.map +1 -1
  129. package/dist/heap_analyzer/tests/snapshot.test.js +4 -4
  130. package/dist/heap_analyzer/tests/testHelper.js +4 -7
  131. package/dist/heap_analyzer/tests/testHelper.js.map +1 -1
  132. package/dist/heap_analyzer/tests/tlsfBlockWalker.test.d.ts +1 -0
  133. package/dist/heap_analyzer/tests/{tlsf.test.js → tlsfBlockWalker.test.js} +2 -2
  134. package/dist/heap_analyzer/tests/tlsfBlockWalker.test.js.map +1 -0
  135. package/dist/heap_analyzer/tests/wasmExecutor.d.ts +2 -2
  136. package/dist/heap_analyzer/tests/wasmExecutor.js +5 -5
  137. package/dist/heap_analyzer/tests/wasmExecutor.js.map +1 -1
  138. package/dist/runtime/objectLayout.d.ts +10 -0
  139. package/dist/runtime/objectLayout.js +13 -0
  140. package/dist/runtime/objectLayout.js.map +1 -0
  141. package/dist/scripts/lib.d.ts +1 -0
  142. package/dist/scripts/lib.js +86 -22
  143. package/dist/scripts/lib.js.map +1 -1
  144. package/dist/test_runner/cli.d.ts +0 -1
  145. package/dist/test_runner/cli.js +37 -19
  146. package/dist/test_runner/cli.js.map +1 -1
  147. package/dist/test_runner/config.d.ts +3 -0
  148. package/dist/test_runner/config.js +8 -0
  149. package/dist/test_runner/config.js.map +1 -0
  150. package/dist/test_runner/core/analyze.js +12 -3
  151. package/dist/test_runner/core/analyze.js.map +1 -1
  152. package/dist/test_runner/core/compile.js +1 -4
  153. package/dist/test_runner/core/compile.js.map +1 -1
  154. package/dist/test_runner/core/compiler.js +2 -0
  155. package/dist/test_runner/core/compiler.js.map +1 -1
  156. package/dist/test_runner/core/execute.js +20 -3
  157. package/dist/test_runner/core/execute.js.map +1 -1
  158. package/dist/test_runner/executionResult.js +10 -8
  159. package/dist/test_runner/executionResult.js.map +1 -1
  160. package/dist/test_runner/generator/table-generator/index.js +4 -2
  161. package/dist/test_runner/generator/table-generator/index.js.map +1 -1
  162. package/dist/test_runner/parser/index.js +6 -5
  163. package/dist/test_runner/parser/index.js.map +1 -1
  164. package/dist/test_runner/parser/singleFileAnalysis.js +7 -4
  165. package/dist/test_runner/parser/singleFileAnalysis.js.map +1 -1
  166. package/dist/test_runner/parser/singleFunctionAnalysis.js +1 -1
  167. package/dist/test_runner/parser/singleFunctionAnalysis.js.map +1 -1
  168. package/dist/test_runner/utils/errorTraceHandler.js +2 -1
  169. package/dist/test_runner/utils/errorTraceHandler.js.map +1 -1
  170. package/dist/test_runner/utils/index.js +9 -2
  171. package/dist/test_runner/utils/index.js.map +1 -1
  172. package/dist/test_runner/utils/interface.js +2 -1
  173. package/dist/test_runner/utils/interface.js.map +1 -1
  174. package/dist/test_runner/utils/pathResolver.js +9 -7
  175. package/dist/test_runner/utils/pathResolver.js.map +1 -1
  176. package/dist/test_runner/utils/wasm.js +16 -8
  177. package/dist/test_runner/utils/wasm.js.map +1 -1
  178. package/dist/trace_visualizer.d.ts +2 -0
  179. package/dist/trace_visualizer.js +18 -0
  180. package/dist/trace_visualizer.js.map +1 -0
  181. package/dist/warpo.js +2 -1
  182. package/dist/warpo.js.map +1 -1
  183. package/package.json +9 -5
  184. package/dist/heap_analyzer/src/dwarfParser.js.map +0 -1
  185. package/dist/heap_analyzer/src/tlsf.js.map +0 -1
  186. package/dist/heap_analyzer/tests/tlsf.test.js.map +0 -1
  187. /package/dist/{heap_analyzer/tests/tlsf.test.d.ts → debug_server/tests/testModule/unittest_debug.case.d.ts} +0 -0
  188. /package/dist/{heap_analyzer/src → dwarf}/dwarfParser.d.ts +0 -0
  189. /package/dist/heap_analyzer/src/{tlsf.d.ts → tlsfBlockWalker.d.ts} +0 -0
@@ -1,30 +1,75 @@
1
1
  // Copyright (C) 2025 wasm-ecosystem
2
2
  // SPDX-License-Identifier: Apache-2.0
3
- import { LoggingDebugSession, InitializedEvent, Thread, Breakpoint, LoadedSourceEvent, StoppedEvent, ContinuedEvent, logger, } from "@vscode/debugadapter";
3
+ import { LoggingDebugSession, InitializedEvent, Thread, Breakpoint, Source, Scope, StackFrame, LoadedSourceEvent, OutputEvent, StoppedEvent, ContinuedEvent, TerminatedEvent, logger, } from "@vscode/debugadapter";
4
+ import assert from "node:assert/strict";
5
+ import { existsSync } from "node:fs";
4
6
  import * as path from "node:path";
7
+ import { BuiltinContainerKind, resolveArrayElementSize, } from "../dwarf/classDebugInfo.js";
8
+ import { ARRAY_DATA_START_OFFSET, ARRAY_LENGTH_OFFSET, AS_STRING_CLASS_NAME, OBJECT_RTID_OFFSET, OBJECT_RTID_SIZE, OBJECT_RTSIZE_OFFSET, OBJECT_RTSIZE_SIZE, SMALL_TUPLE_BITMAP_SIZE, SMALL_TUPLE_SLOT_SIZE, } from "../runtime/objectLayout.js";
9
+ import { normalizeDebugPath } from "./debugPath.js";
5
10
  import { NodeDebugger } from "./nodeDebugger.js";
11
+ import { loadConfig } from "../test_runner/config.js";
12
+ function formatUnknownError(error) {
13
+ if (error instanceof Error) {
14
+ return error.message;
15
+ }
16
+ if (typeof error === "string") {
17
+ return error;
18
+ }
19
+ return "unknown error";
20
+ }
21
+ async function resolveUnitTestWasmPath(cwd) {
22
+ const configPath = path.join(cwd, "as-test.config.js");
23
+ let outputFolder = "coverage";
24
+ if (existsSync(configPath)) {
25
+ const config = await loadConfig(configPath);
26
+ if (typeof config.output === "string") {
27
+ outputFolder = config.output;
28
+ }
29
+ }
30
+ return path.resolve(cwd, outputFolder, "test.instrumented.wasm");
31
+ }
6
32
  export class WarpoDebugSession extends LoggingDebugSession {
7
33
  static threadId = 1;
8
34
  breakpointId = 0;
9
35
  requestedBreakpointsBySource = new Map();
10
- pendingBreakpointUpdatesBySource = new Map();
36
+ pendingBreakpointSources = new Set();
37
+ variableContainers = new Map();
38
+ pausedStackFramesById = new Map();
39
+ nextVariablesReference = 1;
11
40
  loadedModule;
12
41
  runtime;
42
+ stoppedWasmBytecodeOffset;
43
+ stoppedForTrap = false;
44
+ debugSessionLogging = false;
13
45
  log(msg) {
14
46
  logger.log(msg);
15
- }
16
- normalizeSourcePath(filePath) {
17
- if (filePath === "") {
18
- return "";
47
+ if (this.debugSessionLogging) {
48
+ this.sendEvent(new OutputEvent(`${msg}\n`, "stdout"));
19
49
  }
20
- return path.resolve(filePath).replaceAll("\\", "/");
21
50
  }
22
51
  sendStoppedEvent(runtime, info) {
23
52
  this.log(`[${runtime.name}] Paused: ${info.reason}`);
24
- const reason = info.reason === "breakpoint" ? "breakpoint" : "pause";
53
+ this.clearVariableContainers();
54
+ let reason;
55
+ if (info.reason === "breakpoint") {
56
+ reason = "breakpoint";
57
+ }
58
+ else if (info.reason === "exception") {
59
+ reason = "exception";
60
+ }
61
+ else {
62
+ reason = "pause";
63
+ }
64
+ this.log(`Sending stopped event: reason=${reason}, threadId=${WarpoDebugSession.threadId}`);
25
65
  this.sendEvent(new StoppedEvent(reason, WarpoDebugSession.threadId, info.reason));
26
66
  }
27
67
  handleRuntimePause(runtime, info) {
68
+ this.stoppedWasmBytecodeOffset = info.wasmBytecodeOffset;
69
+ this.stoppedForTrap = info.reason === "exception";
70
+ if (info.reason === "other" && !this.loadedModule) {
71
+ return;
72
+ }
28
73
  this.applyPendingBreakpointUpdates(runtime, () => {
29
74
  if (info.reason === "other") {
30
75
  void runtime.resume().catch((error) => {
@@ -42,11 +87,12 @@ export class WarpoDebugSession extends LoggingDebugSession {
42
87
  response.body.supportsSetVariable = false;
43
88
  response.body.supportsBreakpointLocationsRequest = false;
44
89
  response.body.supportsLoadedSourcesRequest = true;
90
+ response.body.supportsEvaluateForHovers = false;
45
91
  this.sendResponse(response);
46
92
  this.sendEvent(new InitializedEvent());
47
93
  }
48
94
  setBreakPointsRequest(response, args) {
49
- const sourcePath = this.normalizeSourcePath(args.source.path || "");
95
+ const sourcePath = normalizeDebugPath(args.source.path || "");
50
96
  const clientLines = args.breakpoints || [];
51
97
  const bps = clientLines.map((bp) => {
52
98
  const id = ++this.breakpointId;
@@ -60,15 +106,23 @@ export class WarpoDebugSession extends LoggingDebugSession {
60
106
  return info;
61
107
  });
62
108
  this.requestedBreakpointsBySource.set(sourcePath, bps);
63
- this.pendingBreakpointUpdatesBySource.set(sourcePath, [...bps]);
109
+ this.pendingBreakpointSources.clear();
110
+ for (const pendingSourcePath of this.requestedBreakpointsBySource.keys()) {
111
+ this.pendingBreakpointSources.add(pendingSourcePath);
112
+ }
64
113
  const breakpoints = bps.map((bp) => {
65
114
  const dbp = new Breakpoint(bp.verified, bp.line);
66
115
  dbp.id = bp.id;
67
116
  return dbp;
68
117
  });
69
118
  response.body = { breakpoints };
70
- this.sendResponse(response);
71
- this.requestBreakpointUpdate();
119
+ if (this.runtime?.isPaused() && this.loadedModule) {
120
+ this.requestBreakpointUpdate(() => this.sendResponse(response));
121
+ }
122
+ else {
123
+ this.sendResponse(response);
124
+ this.requestBreakpointUpdate();
125
+ }
72
126
  }
73
127
  configurationDoneRequest(response, _args) {
74
128
  super.configurationDoneRequest(response, _args);
@@ -79,9 +133,17 @@ export class WarpoDebugSession extends LoggingDebugSession {
79
133
  }
80
134
  async doLaunchRequest(response, args) {
81
135
  try {
82
- this.log(`Launch requested for: ${args.program}`);
136
+ this.debugSessionLogging = args.debugSessionLogging ?? false;
83
137
  const launchType = args.launchType ?? "wasm file";
84
- if (launchType === "wasm file") {
138
+ const cwd = args.cwd ?? process.cwd();
139
+ const program = launchType === "unittest" ? await resolveUnitTestWasmPath(cwd) : args.program;
140
+ if (!program) {
141
+ this.sendErrorResponse(response, 1, "No program specified for launch");
142
+ return;
143
+ }
144
+ const wasmFilePath = path.resolve(program);
145
+ this.log(`Launch requested for: ${program}`);
146
+ if (launchType === "wasm file" || launchType === "unittest") {
85
147
  const runtimeName = args.runtime ?? "node";
86
148
  let runtime;
87
149
  if (runtimeName === "node") {
@@ -91,34 +153,60 @@ export class WarpoDebugSession extends LoggingDebugSession {
91
153
  this.sendErrorResponse(response, 1, `Unknown runtime "${runtimeName}"`);
92
154
  return;
93
155
  }
94
- runtime.onModuleLoad = (module) => {
95
- this.loadedModule = module;
96
- this.log(`Wasm module loaded: ${module.wasmFilePath} (reported as ${module.url}, scriptId: ${module.scriptId})`);
156
+ runtime.onModuleLoad = (wasmModule) => {
157
+ this.loadedModule = wasmModule;
158
+ this.log(`Wasm module loaded: ${wasmModule.wasmFilePath} (reported as ${wasmModule.url}, scriptId: ${wasmModule.scriptId})`);
97
159
  this.sendEvent(new LoadedSourceEvent("new", {
98
- name: path.basename(module.wasmFilePath),
99
- path: module.wasmFilePath,
160
+ name: path.basename(wasmModule.wasmFilePath),
161
+ path: wasmModule.wasmFilePath,
100
162
  }));
101
163
  this.sendEvent(new LoadedSourceEvent("new", {
102
- name: path.basename(module.sourceMapFilePath),
103
- path: module.sourceMapFilePath,
164
+ name: path.basename(wasmModule.sourceMapFilePath),
165
+ path: wasmModule.sourceMapFilePath,
104
166
  }));
105
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
+ }
106
175
  runtime.finishModuleLoad();
107
176
  });
108
177
  };
109
178
  runtime.onPause = (info) => {
110
179
  this.handleRuntimePause(runtime, info);
111
180
  };
181
+ runtime.onRuntimeError = (message) => {
182
+ this.sendEvent(new OutputEvent(`${message}\n`, "stderr"));
183
+ };
184
+ runtime.onRuntimeExit = () => {
185
+ this.handleRuntimeExit(runtime);
186
+ };
187
+ runtime.onLog = (message) => {
188
+ this.log(message);
189
+ };
112
190
  this.disposeLoadedModule();
113
191
  this.runtime?.dispose();
114
192
  this.runtime = runtime;
115
- await runtime.launch({
116
- wasmFilePath: path.resolve(args.program),
117
- entryFunctionName: args.entryFunctionName ?? "main",
118
- args: args.args ?? [],
119
- });
193
+ await runtime.launch(launchType === "unittest"
194
+ ? {
195
+ wasmFilePath,
196
+ cwd,
197
+ warpoPath: args.warpoPath,
198
+ }
199
+ : {
200
+ wasmFilePath,
201
+ entryFunctionName: args.entryFunctionName ?? "main",
202
+ args: args.args ?? [],
203
+ });
120
204
  this.log(`[${runtime.name}] Runtime launched`);
121
205
  }
206
+ else {
207
+ this.sendErrorResponse(response, 1, `Unknown launch type "${launchType}"`);
208
+ return;
209
+ }
122
210
  this.sendResponse(response);
123
211
  }
124
212
  catch (error) {
@@ -132,32 +220,793 @@ export class WarpoDebugSession extends LoggingDebugSession {
132
220
  };
133
221
  this.sendResponse(response);
134
222
  }
223
+ stackTraceRequest(response, args) {
224
+ this.doStackTraceRequest(response, args);
225
+ }
226
+ scopesRequest(response, args) {
227
+ this.doScopesRequest(response, args);
228
+ }
229
+ variablesRequest(response, args) {
230
+ void this.doVariablesRequest(response, args).catch((error) => {
231
+ const message = formatUnknownError(error);
232
+ this.log(`Variables request failed: ${message}`);
233
+ this.sendErrorResponse(response, 1, `Variables request failed: ${message}`);
234
+ });
235
+ }
135
236
  continueRequest(response, _args) {
136
- void this.doContinueRequest(response);
237
+ this.doContinueRequest(response);
137
238
  }
138
239
  disconnectRequest(response, _args) {
139
- this.disposeLoadedModule();
140
- this.pendingBreakpointUpdatesBySource.clear();
141
240
  this.runtime?.dispose();
142
241
  this.runtime = undefined;
242
+ this.resetRuntimeState();
243
+ this.debugSessionLogging = false;
244
+ this.dispose();
143
245
  this.log("Debug session ended.");
144
246
  this.sendResponse(response);
145
247
  }
146
- async doContinueRequest(response) {
248
+ doStackTraceRequest(response, args) {
249
+ const runtime = this.runtime;
250
+ const frames = runtime ? runtime.getPausedWasmFrames() : [];
251
+ this.log(`StackTrace request: threadId=${args.threadId}, startFrame=${args.startFrame ?? 0}, levels=${args.levels ?? "all"}, runtimeFrames=${frames.length}`);
252
+ this.pausedStackFramesById.clear();
253
+ const stackFrames = frames.map((frame, index) => {
254
+ const id = index + 1;
255
+ this.pausedStackFramesById.set(id, { ...frame, frameIndex: index });
256
+ const sourceLocation = this.resolveSourceLocation(frame.wasmBytecodeOffset);
257
+ const sourceLabel = sourceLocation
258
+ ? `${path.basename(sourceLocation.sourcePath)}:${sourceLocation.sourceLine}`
259
+ : "unknown";
260
+ this.log(` frame ${id}: index=${index}, name=${frame.functionName || "wasm"}, offset=${frame.wasmBytecodeOffset ?? "unknown"}, source=${sourceLabel}`);
261
+ const source = sourceLocation
262
+ ? new Source(path.basename(sourceLocation.sourcePath), sourceLocation.sourcePath)
263
+ : undefined;
264
+ return new StackFrame(id, frame.functionName || "wasm", source, sourceLocation?.sourceLine ?? 0, 1);
265
+ });
266
+ const startFrame = args.startFrame ?? 0;
267
+ const requestedStackFrames = args.levels === undefined
268
+ ? stackFrames.slice(startFrame)
269
+ : stackFrames.slice(startFrame, startFrame + args.levels);
270
+ response.body = {
271
+ stackFrames: requestedStackFrames,
272
+ totalFrames: stackFrames.length,
273
+ };
274
+ this.log(`StackTrace response: returned=${requestedStackFrames.length}, total=${stackFrames.length}`);
275
+ this.sendResponse(response);
276
+ }
277
+ resolveSourceLocation(wasmBytecodeOffset) {
278
+ const wasmModule = this.loadedModule;
279
+ if (!wasmModule || wasmBytecodeOffset === undefined) {
280
+ return undefined;
281
+ }
282
+ return wasmModule.resolveSourceLocation(wasmBytecodeOffset);
283
+ }
284
+ doScopesRequest(response, args) {
285
+ this.log(`Scopes request: frameId=${args.frameId}`);
286
+ const scopes = this.resolveLocalScopes(args.frameId);
287
+ response.body = { scopes };
288
+ this.sendResponse(response);
289
+ }
290
+ async doVariablesRequest(response, args) {
291
+ const variableContainer = this.variableContainers.get(args.variablesReference);
292
+ switch (variableContainer?.kind) {
293
+ case "locals": {
294
+ this.log(`Variables request: locals for frameId=${variableContainer.frameId}, scopeIndex=${variableContainer.scopeIndex}`);
295
+ response.body = {
296
+ variables: await this.resolveLocalScopeVariables(variableContainer.frameId, variableContainer.scopeIndex),
297
+ };
298
+ break;
299
+ }
300
+ case "object": {
301
+ response.body = { variables: await this.decodeObjectAtAddress(variableContainer.address) };
302
+ break;
303
+ }
304
+ case "map-entry": {
305
+ response.body = {
306
+ variables: [variableContainer.key, variableContainer.value].map((variable) => this.toDebugProtocolVariable(variable)),
307
+ };
308
+ break;
309
+ }
310
+ default: {
311
+ this.log(`Warning: unknown variables reference ${args.variablesReference}`);
312
+ response.body = { variables: [] };
313
+ break;
314
+ }
315
+ }
316
+ this.sendResponse(response);
317
+ }
318
+ resolveLocalScopes(frameId) {
319
+ const runtimeFrame = this.resolvePausedStackFrame(frameId);
320
+ const wasmModule = this.loadedModule;
321
+ if (!runtimeFrame || !wasmModule) {
322
+ this.log(`Scopes unavailable: frameId=${frameId}, hasFrame=${runtimeFrame !== undefined}, hasModule=${wasmModule !== undefined}`);
323
+ return [];
324
+ }
325
+ const wasmBytecodeOffset = runtimeFrame.wasmBytecodeOffset;
326
+ if (wasmBytecodeOffset === undefined) {
327
+ this.log(`Error: scopes unavailable: frame ${frameId} has no wasm bytecode offset`);
328
+ return [];
329
+ }
330
+ const sourceVariableScopes = wasmModule.getVariableScopesAtBytecodeOffset(wasmBytecodeOffset);
331
+ if (!sourceVariableScopes || sourceVariableScopes.length === 0) {
332
+ this.log(`Scopes response: frameId=${frameId}, no scopes`);
333
+ return [];
334
+ }
335
+ this.log(`Scopes response: frameId=${frameId}, scopes=${sourceVariableScopes.map((scope) => scope.name).join(", ")}`);
336
+ return sourceVariableScopes.map((scope, index) => new Scope(scope.name, this.createVariableContainer({ kind: "locals", frameId, scopeIndex: index }), false));
337
+ }
338
+ async resolveLocalScopeVariables(frameId, scopeIndex) {
339
+ const runtimeFrame = this.resolvePausedStackFrame(frameId);
340
+ const wasmModule = this.loadedModule;
341
+ const runtime = this.runtime;
342
+ if (!runtimeFrame || !wasmModule || !runtime) {
343
+ this.log(`Local variables unavailable: frameId=${frameId}, hasFrame=${runtimeFrame !== undefined}, hasModule=${wasmModule !== undefined}, hasRuntime=${runtime !== undefined}`);
344
+ return [];
345
+ }
346
+ const wasmBytecodeOffset = runtimeFrame.wasmBytecodeOffset;
347
+ if (wasmBytecodeOffset === undefined) {
348
+ this.log(`Error: local variables unavailable: frame ${frameId} has no wasm bytecode offset`);
349
+ return [];
350
+ }
351
+ const sourceVariableScopes = wasmModule.getVariableScopesAtBytecodeOffset(wasmBytecodeOffset);
352
+ if (!sourceVariableScopes) {
353
+ this.log(`Error: local variables unavailable: no source variables at wasm bytecode offset ${wasmBytecodeOffset}`);
354
+ return [];
355
+ }
356
+ const sourceVariableScope = sourceVariableScopes[scopeIndex];
357
+ if (!sourceVariableScope) {
358
+ this.log(`Error: local variables unavailable: no scope at index ${scopeIndex}`);
359
+ return [];
360
+ }
361
+ const runtimeVariables = await runtime.getPausedWasmFrameVariables(runtimeFrame.frameIndex);
362
+ this.log(`Local variables resolved: frameId=${frameId}, frameIndex=${runtimeFrame.frameIndex}, offset=${wasmBytecodeOffset}, scopeIndex=${scopeIndex}, scope=${sourceVariableScope.name}, runtimeVariables=${runtimeVariables.length}`);
363
+ const runtimeVariablesByIndex = new Map();
364
+ for (const variable of runtimeVariables) {
365
+ runtimeVariablesByIndex.set(variable.localIndex, variable);
366
+ }
367
+ const sourceVariables = sourceVariableScope.variables;
368
+ 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 };
378
+ }
379
+ return { name: variable.name, value: tupleRawValue, typeName: variable.typeName };
380
+ }
381
+ const wasmRawValue = rawWasmValues.get(variable);
382
+ assert(wasmRawValue !== undefined);
383
+ return { name: variable.name, value: wasmRawValue, typeName: variable.typeName };
384
+ });
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
+ }
388
+ async decodeObjectAtAddress(address) {
389
+ const fields = await this.resolveObjectFields(address);
390
+ return fields.map((variable) => this.toDebugProtocolVariable(variable));
391
+ }
392
+ async resolveRawWasmValues(sourceVariables, runtimeVariablesByIndex, runtime) {
393
+ const rawWasmValues = new Map();
394
+ let runtimeGlobalsByIndex;
395
+ for (const variable of sourceVariables) {
396
+ if (variable.kind === "wasm-local") {
397
+ const runtimeVariable = runtimeVariablesByIndex.get(variable.localIndex);
398
+ assert(runtimeVariable !== undefined);
399
+ rawWasmValues.set(variable, runtimeVariable.value);
400
+ }
401
+ else if (variable.kind === "wasm-global") {
402
+ if (runtimeGlobalsByIndex === undefined) {
403
+ const pausedWasmGlobalVariables = await runtime.getPausedWasmGlobalVariables();
404
+ runtimeGlobalsByIndex = new Map(pausedWasmGlobalVariables.map((runtimeVariable) => [runtimeVariable.globalIndex, runtimeVariable.value]));
405
+ }
406
+ rawWasmValues.set(variable, runtimeGlobalsByIndex.get(variable.globalIndex) ?? "<unavailable>");
407
+ }
408
+ }
409
+ return rawWasmValues;
410
+ }
411
+ async readClosureTupleChain(closureEnvLocalIndex, runtimeVariablesByIndex) {
412
+ const closureTupleValuesByLevel = new Map();
413
+ if (closureEnvLocalIndex === undefined) {
414
+ return closureTupleValuesByLevel;
415
+ }
416
+ const closureEnvVariable = runtimeVariablesByIndex.get(closureEnvLocalIndex);
417
+ 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++;
442
+ }
443
+ return closureTupleValuesByLevel;
444
+ }
445
+ resolvePausedStackFrame(frameId) {
446
+ return this.pausedStackFramesById.get(frameId);
447
+ }
448
+ async toDebugSessionVariable(variable, name = variable.name, typeName = variable.type) {
449
+ return this.toDebugSessionVariableValue(name, typeName, variable.value);
450
+ }
451
+ async toDebugSessionVariableValue(name, typeName, value) {
452
+ if (typeName !== undefined) {
453
+ const objectAddress = this.parseObjectAddress(typeName, value);
454
+ if (objectAddress) {
455
+ if (typeName === AS_STRING_CLASS_NAME) {
456
+ const stringValue = await this.decodeStringAtAddress(typeName, objectAddress);
457
+ return this.createClassVariable(name, typeName, objectAddress, stringValue);
458
+ }
459
+ return this.createClassVariable(name, typeName, objectAddress);
460
+ }
461
+ }
462
+ return {
463
+ kind: "basic",
464
+ name,
465
+ value,
466
+ typeName,
467
+ };
468
+ }
469
+ toDebugProtocolVariable(variable, options = {}) {
470
+ const isExpandableClass = variable.kind === "class" && variable.displayValue === undefined;
471
+ const showClassNameInName = options.showClassNameInName ?? true;
472
+ return {
473
+ name: isExpandableClass && showClassNameInName ? `${variable.name}: ${variable.typeName}` : variable.name,
474
+ value: this.getVariableDisplayValue(variable),
475
+ type: variable.kind === "map-entry" ? undefined : variable.typeName,
476
+ variablesReference: this.getVariableReference(variable, isExpandableClass),
477
+ };
478
+ }
479
+ getVariableDisplayValue(variable) {
480
+ switch (variable.kind) {
481
+ case "basic": {
482
+ return variable.value;
483
+ }
484
+ case "class": {
485
+ return variable.displayValue ?? "";
486
+ }
487
+ case "map-entry": {
488
+ return "";
489
+ }
490
+ }
491
+ }
492
+ getVariableReference(variable, isExpandableClass) {
493
+ if (isExpandableClass && variable.kind === "class") {
494
+ return this.toObjectVariablesReference(variable.address);
495
+ }
496
+ if (variable.kind === "map-entry") {
497
+ return this.createVariableContainer({ kind: "map-entry", key: variable.key, value: variable.value });
498
+ }
499
+ return 0;
500
+ }
501
+ createClassVariable(name, typeName, address, displayValue) {
502
+ return { kind: "class", name, typeName, address, displayValue };
503
+ }
504
+ parseObjectAddress(typeName, value) {
505
+ if (!this.loadedModule?.getClassLayout(typeName)) {
506
+ return undefined;
507
+ }
508
+ const address = Number(value);
509
+ return Number.isInteger(address) && address > 0 ? address : undefined;
510
+ }
511
+ toObjectVariablesReference(address) {
512
+ return this.createVariableContainer({ kind: "object", address });
513
+ }
514
+ createVariableContainer(variableContainer) {
515
+ const variablesReference = this.nextVariablesReference++;
516
+ this.variableContainers.set(variablesReference, variableContainer);
517
+ return variablesReference;
518
+ }
519
+ clearVariableContainers() {
520
+ this.variableContainers.clear();
521
+ this.pausedStackFramesById.clear();
522
+ }
523
+ async readTupleElementValues(address, classLayout) {
524
+ if (!this.runtime) {
525
+ this.log(`Warning: cannot expand tuple at ${address}: runtime is not active`);
526
+ return [];
527
+ }
528
+ const tupleClassLayout = classLayout ?? (await this.resolveRuntimeClassLayout(address));
529
+ if (!tupleClassLayout) {
530
+ this.log(`Warning: cannot expand tuple at ${address}: runtime class layout is missing`);
531
+ return [];
532
+ }
533
+ if (tupleClassLayout.builtinKind !== BuiltinContainerKind.SmallTuple) {
534
+ this.log(`Warning: cannot expand tuple at ${address}: runtime class layout is not tuple`);
535
+ return [];
536
+ }
537
+ const payloadSize = await this.resolveRuntimeObjectPayloadSize(address, tupleClassLayout);
538
+ if (payloadSize === undefined) {
539
+ return [];
540
+ }
541
+ const memory = await this.runtime.readWasmMemory(address, payloadSize);
542
+ if (!memory) {
543
+ this.log(`Warning: cannot expand tuple at ${address}: tuple memory is unavailable`);
544
+ return [];
545
+ }
546
+ const view = new DataView(memory.buffer, memory.byteOffset, memory.byteLength);
547
+ assert.equal(tupleClassLayout.builtinKind, BuiltinContainerKind.SmallTuple);
548
+ if (view.byteLength <= SMALL_TUPLE_BITMAP_SIZE) {
549
+ return [];
550
+ }
551
+ const elementAreaSize = view.byteLength - SMALL_TUPLE_BITMAP_SIZE;
552
+ if (elementAreaSize % SMALL_TUPLE_SLOT_SIZE !== 0) {
553
+ this.log(`Error: cannot expand tuple at ${address}: tuple element area is not slot aligned`);
554
+ return [];
555
+ }
556
+ const bitmap = view.getBigUint64(elementAreaSize, true);
557
+ const values = [];
558
+ for (let index = 0; index < elementAreaSize / SMALL_TUPLE_SLOT_SIZE; index++) {
559
+ const offset = index * SMALL_TUPLE_SLOT_SIZE;
560
+ const isReference = (bitmap & (1n << BigInt(index))) !== 0n;
561
+ const value = isReference
562
+ ? this.decodeTupleReferenceValue(view, offset)
563
+ : this.decodeTupleBasicValue(view, offset);
564
+ values.push({ value, isReference });
565
+ }
566
+ return values;
567
+ }
568
+ async decodeTupleElements(address, classLayout) {
569
+ const values = await this.readTupleElementValues(address, classLayout);
570
+ return Promise.all(values.map((value, index) => value.isReference
571
+ ? this.decodeTupleReferenceElement(index.toString(), value.value)
572
+ : this.toDebugSessionVariableValue(index.toString(), "usize", value.value)));
573
+ }
574
+ async resolveObjectFields(address) {
575
+ const wasmModule = this.loadedModule;
576
+ if (!wasmModule) {
577
+ this.log(`Warning: cannot expand object at ${address}: wasm module is not loaded`);
578
+ return [];
579
+ }
580
+ if (!this.runtime) {
581
+ this.log(`Warning: cannot expand object at ${address}: runtime is not active`);
582
+ return [];
583
+ }
584
+ const classLayout = await this.resolveRuntimeClassLayout(address);
585
+ if (!classLayout) {
586
+ this.log(`Warning: cannot expand object at ${address}: runtime class layout is missing`);
587
+ return [];
588
+ }
589
+ if (classLayout.builtinKind === BuiltinContainerKind.SmallTuple) {
590
+ return this.decodeTupleElements(address, classLayout);
591
+ }
592
+ const payloadSize = await this.resolveRuntimeObjectPayloadSize(address, classLayout);
593
+ if (payloadSize === undefined) {
594
+ return [];
595
+ }
596
+ const memory = await this.runtime.readWasmMemory(address, payloadSize);
597
+ if (!memory) {
598
+ this.log(`Warning: cannot expand object at ${address}: object memory is unavailable`);
599
+ return [];
600
+ }
601
+ const view = new DataView(memory.buffer, memory.byteOffset, memory.byteLength);
602
+ if (classLayout.builtinKind === BuiltinContainerKind.ArrayBuffer) {
603
+ return this.decodeArrayBufferElements(view);
604
+ }
605
+ if (classLayout.builtinKind === BuiltinContainerKind.Array) {
606
+ return this.decodeArrayElements(address, view, classLayout);
607
+ }
608
+ if (classLayout.builtinKind === BuiltinContainerKind.StaticArray) {
609
+ return this.decodeStaticArrayElements(address, view, classLayout);
610
+ }
611
+ if (this.isSetClassLayout(classLayout)) {
612
+ return this.decodeSetElements(address, view, classLayout);
613
+ }
614
+ if (this.isMapClassLayout(classLayout)) {
615
+ return this.decodeMapElements(address, view, classLayout);
616
+ }
617
+ return Promise.all(classLayout.fields.map((field) => this.decodeFieldVariable(view, field)));
618
+ }
619
+ async resolveRuntimeObjectPayloadSize(address, classLayout) {
620
+ if (classLayout.builtinKind !== BuiltinContainerKind.SmallTuple &&
621
+ classLayout.builtinKind !== BuiltinContainerKind.StaticArray &&
622
+ classLayout.builtinKind !== BuiltinContainerKind.ArrayBuffer) {
623
+ return classLayout.byteSize;
624
+ }
625
+ const sizeMemory = await this.runtime?.readWasmMemory(address + OBJECT_RTSIZE_OFFSET, OBJECT_RTSIZE_SIZE);
626
+ if (!sizeMemory) {
627
+ this.log(`Warning: cannot read object size at ${address + OBJECT_RTSIZE_OFFSET}`);
628
+ return undefined;
629
+ }
630
+ const sizeView = new DataView(sizeMemory.buffer, sizeMemory.byteOffset, sizeMemory.byteLength);
631
+ return sizeView.getUint32(0, true);
632
+ }
633
+ decodeTupleReferenceValue(view, offset) {
634
+ if (offset + SMALL_TUPLE_SLOT_SIZE > view.byteLength) {
635
+ return "<unavailable>";
636
+ }
637
+ const elementAddress = view.getUint32(offset, true);
638
+ return elementAddress === 0 ? "null" : elementAddress.toString();
639
+ }
640
+ decodeTupleBasicValue(view, offset) {
641
+ return this.decodeFieldValue(view, {
642
+ name: "",
643
+ typeName: "usize",
644
+ offset,
645
+ size: SMALL_TUPLE_SLOT_SIZE,
646
+ isReference: false,
647
+ });
648
+ }
649
+ async decodeTupleReferenceElement(name, value) {
650
+ const elementAddress = Number(value);
651
+ if (elementAddress === 0) {
652
+ return { kind: "basic", name, value: "null", typeName: undefined };
653
+ }
654
+ if (!Number.isInteger(elementAddress) || elementAddress <= 0) {
655
+ return { kind: "basic", name, value, typeName: undefined };
656
+ }
657
+ const classLayout = await this.resolveRuntimeClassLayout(elementAddress);
658
+ if (!classLayout) {
659
+ this.log(`Error: cannot expand tuple reference at ${elementAddress}: runtime class layout is missing`);
660
+ return { kind: "basic", name, value: elementAddress.toString(), typeName: undefined };
661
+ }
662
+ if (classLayout.name === AS_STRING_CLASS_NAME) {
663
+ const stringValue = await this.decodeStringAtAddress(classLayout.name, elementAddress);
664
+ return this.createClassVariable(name, classLayout.name, elementAddress, stringValue);
665
+ }
666
+ return this.createClassVariable(name, classLayout.name, elementAddress);
667
+ }
668
+ async decodeArrayElements(address, view, classLayout) {
669
+ assert.equal(classLayout.builtinKind, BuiltinContainerKind.Array);
670
+ const elementTypeName = classLayout.templateType;
671
+ if (elementTypeName === undefined) {
672
+ this.log(`Error: cannot expand array at ${address}: element type is unavailable`);
673
+ return [];
674
+ }
675
+ if (ARRAY_LENGTH_OFFSET + 4 > view.byteLength) {
676
+ this.log(`Error: cannot expand array at ${address}: array header is unavailable`);
677
+ return [];
678
+ }
679
+ const dataStart = view.getUint32(ARRAY_DATA_START_OFFSET, true);
680
+ const length = view.getUint32(ARRAY_LENGTH_OFFSET, true);
681
+ const elementSize = resolveArrayElementSize(elementTypeName, classLayout.templateTypeIsReference === true);
682
+ const elementsMemory = await this.runtime?.readWasmMemory(dataStart, length * elementSize);
683
+ if (!elementsMemory) {
684
+ this.log(`Error: cannot expand array at ${address}: array elements memory is unavailable`);
685
+ return [];
686
+ }
687
+ const elementsView = new DataView(elementsMemory.buffer, elementsMemory.byteOffset, elementsMemory.byteLength);
688
+ return this.decodeArrayElementVariables(elementsView, length, elementTypeName, elementSize, classLayout.templateTypeIsReference === true);
689
+ }
690
+ decodeArrayBufferElements(view) {
691
+ const elements = [];
692
+ for (let index = 0; index < view.byteLength; index++) {
693
+ elements.push({
694
+ kind: "basic",
695
+ name: index.toString(),
696
+ value: view.getUint8(index).toString(),
697
+ typeName: "u8",
698
+ });
699
+ }
700
+ return elements;
701
+ }
702
+ async decodeStaticArrayElements(address, view, classLayout) {
703
+ assert.equal(classLayout.builtinKind, BuiltinContainerKind.StaticArray);
704
+ const elementTypeName = classLayout.templateType;
705
+ if (elementTypeName === undefined) {
706
+ this.log(`Error: cannot expand static array at ${address}: element type is unavailable`);
707
+ return [];
708
+ }
709
+ const elementSize = resolveArrayElementSize(elementTypeName, classLayout.templateTypeIsReference === true);
710
+ if (view.byteLength % elementSize !== 0) {
711
+ this.log(`Error: cannot expand static array at ${address}: payload size is not element aligned`);
712
+ return [];
713
+ }
714
+ return this.decodeArrayElementVariables(view, view.byteLength / elementSize, elementTypeName, elementSize, classLayout.templateTypeIsReference === true);
715
+ }
716
+ async decodeArrayElementVariables(view, length, elementTypeName, elementSize, isReference) {
717
+ const elementVariables = [];
718
+ for (let index = 0; index < length; index++) {
719
+ elementVariables.push(this.decodeFieldVariable(view, {
720
+ name: index.toString(),
721
+ typeName: elementTypeName,
722
+ offset: index * elementSize,
723
+ size: elementSize,
724
+ isReference,
725
+ }));
726
+ }
727
+ return Promise.all(elementVariables);
728
+ }
729
+ async decodeSetElements(address, view, classLayout) {
730
+ const elementTypeName = classLayout.templateType;
731
+ if (elementTypeName === undefined) {
732
+ this.log(`Error: cannot expand set at ${address}: element type is unavailable`);
733
+ return [];
734
+ }
735
+ const entryLayout = classLayout.entryLayout;
736
+ if (entryLayout === undefined) {
737
+ this.log(`Error: cannot expand set at ${address}: entry layout is unavailable`);
738
+ return [];
739
+ }
740
+ const entriesView = await this.readMapOrSetEntries(address, view, entryLayout.size, "set");
741
+ if (!entriesView) {
742
+ return [];
743
+ }
744
+ const entriesOffset = view.getInt32(16, true);
745
+ if (entriesOffset <= 0) {
746
+ return [];
747
+ }
748
+ const elementSize = resolveArrayElementSize(elementTypeName, classLayout.templateTypeIsReference === true);
749
+ const elements = [];
750
+ for (let entryIndex = 0; entryIndex < entriesOffset; entryIndex++) {
751
+ const entryBase = entryIndex * entryLayout.size;
752
+ const taggedNextOffset = entryBase + entryLayout.size - 4;
753
+ if (taggedNextOffset + 4 > entriesView.byteLength) {
754
+ break;
755
+ }
756
+ const taggedNext = entriesView.getUint32(taggedNextOffset, true);
757
+ if (taggedNext & 1) {
758
+ continue;
759
+ }
760
+ elements.push(this.decodeFieldVariable(entriesView, {
761
+ name: elements.length.toString(),
762
+ typeName: elementTypeName,
763
+ offset: entryBase,
764
+ size: elementSize,
765
+ isReference: classLayout.templateTypeIsReference === true,
766
+ }));
767
+ }
768
+ return Promise.all(elements);
769
+ }
770
+ async decodeMapElements(address, view, classLayout) {
771
+ const entryLayout = classLayout.entryLayout;
772
+ if (entryLayout === undefined) {
773
+ this.log(`Error: cannot expand map at ${address}: entry layout is unavailable`);
774
+ return [];
775
+ }
776
+ const entryClass = this.getMapOrSetEntryClassLayout(classLayout.name);
777
+ const keyField = entryClass?.fields.find((field) => field.name === "key");
778
+ const valueField = entryClass?.fields.find((field) => field.name === "value");
779
+ if (!keyField || !valueField) {
780
+ this.log(`Error: cannot expand map at ${address}: key/value entry fields are unavailable`);
781
+ return [];
782
+ }
783
+ const entriesView = await this.readMapOrSetEntries(address, view, entryLayout.size, "map");
784
+ if (!entriesView) {
785
+ return [];
786
+ }
787
+ const entriesOffset = view.getInt32(16, true);
788
+ const elements = [];
789
+ for (let entryIndex = 0; entryIndex < entriesOffset; entryIndex++) {
790
+ const entryBase = entryIndex * entryLayout.size;
791
+ const taggedNextOffset = entryBase + entryLayout.size - 4;
792
+ if (taggedNextOffset + 4 > entriesView.byteLength) {
793
+ break;
794
+ }
795
+ const taggedNext = entriesView.getUint32(taggedNextOffset, true);
796
+ if (taggedNext & 1) {
797
+ continue;
798
+ }
799
+ elements.push(this.decodeMapEntry(entriesView, elements.length.toString(), entryBase, keyField, valueField));
800
+ }
801
+ return Promise.all(elements);
802
+ }
803
+ isSetClassLayout(classLayout) {
804
+ return classLayout.name.startsWith("~lib/set/Set<");
805
+ }
806
+ isMapClassLayout(classLayout) {
807
+ return classLayout.name.startsWith("~lib/map/Map<");
808
+ }
809
+ getMapOrSetEntryClassLayout(className) {
810
+ if (className.startsWith("~lib/set/Set<")) {
811
+ return this.loadedModule?.getInternalClassLayout(className.replace("~lib/set/Set<", "~lib/set/SetEntry<"));
812
+ }
813
+ if (className.startsWith("~lib/map/Map<")) {
814
+ return this.loadedModule?.getInternalClassLayout(className.replace("~lib/map/Map<", "~lib/map/MapEntry<"));
815
+ }
816
+ return undefined;
817
+ }
818
+ async decodeMapEntry(entriesView, name, entryBase, keyField, valueField) {
819
+ const [key, value] = await Promise.all([
820
+ this.decodeFieldVariable(entriesView, { ...keyField, name: "key", offset: entryBase + keyField.offset }),
821
+ this.decodeFieldVariable(entriesView, { ...valueField, name: "value", offset: entryBase + valueField.offset }),
822
+ ]);
823
+ return { kind: "map-entry", name, key, value };
824
+ }
825
+ async readMapOrSetEntries(address, view, entrySize, containerKind) {
826
+ if (view.byteLength < 20) {
827
+ this.log(`Error: cannot expand ${containerKind} at ${address}: ${containerKind} header is unavailable`);
828
+ return undefined;
829
+ }
830
+ const entriesPtr = view.getUint32(8, true);
831
+ const entriesOffset = view.getInt32(16, true);
832
+ if (entriesPtr === 0 || entriesOffset <= 0) {
833
+ return new DataView(new ArrayBuffer(0));
834
+ }
835
+ const entriesMemory = await this.runtime?.readWasmMemory(entriesPtr, entriesOffset * entrySize);
836
+ if (!entriesMemory) {
837
+ this.log(`Error: cannot expand ${containerKind} at ${address}: ${containerKind} entries memory is unavailable`);
838
+ return undefined;
839
+ }
840
+ return new DataView(entriesMemory.buffer, entriesMemory.byteOffset, entriesMemory.byteLength);
841
+ }
842
+ async resolveRuntimeClassLayout(address) {
843
+ const header = await this.runtime?.readWasmMemory(address + OBJECT_RTID_OFFSET, OBJECT_RTID_SIZE);
844
+ if (!header) {
845
+ this.log(`Warning: cannot read object RTID at ${address + OBJECT_RTID_OFFSET}`);
846
+ return undefined;
847
+ }
848
+ const headerView = new DataView(header.buffer, header.byteOffset, header.byteLength);
849
+ const rtid = headerView.getUint32(0, true);
850
+ const classLayout = this.loadedModule?.getClassLayoutByRtid(rtid);
851
+ if (!classLayout) {
852
+ this.log(`Warning: cannot resolve class layout for RTID ${rtid}`);
853
+ }
854
+ return classLayout;
855
+ }
856
+ async decodeFieldVariable(view, field) {
857
+ const value = this.decodeFieldValue(view, field);
858
+ const address = field.isReference ? this.parseObjectAddress(field.typeName, value) : undefined;
859
+ if (address) {
860
+ if (field.typeName === AS_STRING_CLASS_NAME) {
861
+ const stringValue = await this.decodeStringAtAddress(field.typeName, address);
862
+ return this.createClassVariable(field.name, field.typeName, address, stringValue);
863
+ }
864
+ return this.createClassVariable(field.name, field.typeName, address);
865
+ }
866
+ return {
867
+ kind: "basic",
868
+ name: field.name,
869
+ value,
870
+ typeName: field.typeName,
871
+ };
872
+ }
873
+ decodeFieldValue(view, field) {
874
+ if (field.offset + field.size > view.byteLength) {
875
+ return "<unavailable>";
876
+ }
877
+ if (field.isReference) {
878
+ const address = view.getUint32(field.offset, true);
879
+ return address === 0 ? "null" : address.toString();
880
+ }
881
+ switch (field.typeName) {
882
+ case "bool": {
883
+ return String(view.getUint8(field.offset) !== 0);
884
+ }
885
+ case "i8": {
886
+ return view.getInt8(field.offset).toString();
887
+ }
888
+ case "u8": {
889
+ return view.getUint8(field.offset).toString();
890
+ }
891
+ case "i16": {
892
+ return view.getInt16(field.offset, true).toString();
893
+ }
894
+ case "u16": {
895
+ return view.getUint16(field.offset, true).toString();
896
+ }
897
+ case "i32":
898
+ case "isize": {
899
+ return view.getInt32(field.offset, true).toString();
900
+ }
901
+ case "u32":
902
+ case "usize": {
903
+ return view.getUint32(field.offset, true).toString();
904
+ }
905
+ case "i64": {
906
+ return view.getBigInt64(field.offset, true).toString();
907
+ }
908
+ case "u64": {
909
+ return view.getBigUint64(field.offset, true).toString();
910
+ }
911
+ case "f32": {
912
+ return view.getFloat32(field.offset, true).toString();
913
+ }
914
+ case "f64": {
915
+ return view.getFloat64(field.offset, true).toString();
916
+ }
917
+ default: {
918
+ return this.decodeIntegerBySize(view, field);
919
+ }
920
+ }
921
+ }
922
+ decodeIntegerBySize(view, field) {
923
+ switch (field.size) {
924
+ case 1: {
925
+ return view.getUint8(field.offset).toString();
926
+ }
927
+ case 2: {
928
+ return view.getUint16(field.offset, true).toString();
929
+ }
930
+ case 4: {
931
+ return view.getUint32(field.offset, true).toString();
932
+ }
933
+ case 8: {
934
+ return view.getBigUint64(field.offset, true).toString();
935
+ }
936
+ default: {
937
+ return "<unavailable>";
938
+ }
939
+ }
940
+ }
941
+ async decodeStringAtAddress(typeName, address) {
942
+ assert.equal(typeName, AS_STRING_CLASS_NAME);
147
943
  if (!this.runtime) {
944
+ this.log(`Error: cannot decode string at ${address}: runtime is not active`);
945
+ return "<unavailable>";
946
+ }
947
+ const sizeMemory = await this.runtime.readWasmMemory(address + OBJECT_RTSIZE_OFFSET, OBJECT_RTSIZE_SIZE);
948
+ if (!sizeMemory) {
949
+ this.log(`Error: cannot decode string at ${address}: string size memory is unavailable`);
950
+ return "<unavailable>";
951
+ }
952
+ const sizeView = new DataView(sizeMemory.buffer, sizeMemory.byteOffset, sizeMemory.byteLength);
953
+ const byteLength = sizeView.getUint32(0, true);
954
+ const stringMemory = await this.runtime.readWasmMemory(address, byteLength);
955
+ if (!stringMemory) {
956
+ this.log(`Error: cannot decode string at ${address}: string payload memory is unavailable`);
957
+ return "<unavailable>";
958
+ }
959
+ return JSON.stringify(Buffer.from(stringMemory).toString("utf16le"));
960
+ }
961
+ doContinueRequest(response) {
962
+ const runtime = this.runtime;
963
+ if (!runtime) {
148
964
  this.sendErrorResponse(response, 1, "No active runtime");
149
965
  return;
150
966
  }
967
+ response.body = { allThreadsContinued: true };
968
+ this.sendResponse(response);
969
+ if (this.stoppedForTrap) {
970
+ this.log("Continue requested from wasm trap; terminating debug session");
971
+ this.disposeRuntimeAndTerminate(runtime);
972
+ return;
973
+ }
974
+ void this.resumeRuntime(runtime);
975
+ }
976
+ async resumeRuntime(runtime) {
151
977
  try {
152
- await this.runtime.resume();
153
- response.body = { allThreadsContinued: true };
154
- this.sendResponse(response);
155
- this.sendEvent(new ContinuedEvent(WarpoDebugSession.threadId, true));
978
+ await runtime.resume();
156
979
  }
157
980
  catch (error) {
158
- const message = error instanceof Error ? error.message : String(error);
159
- this.sendErrorResponse(response, 1, `Continue failed: ${message}`);
981
+ this.log(`Continue failed: ${formatUnknownError(error)}`);
982
+ return;
983
+ }
984
+ if (this.runtime !== runtime) {
985
+ return;
986
+ }
987
+ this.clearVariableContainers();
988
+ this.stoppedWasmBytecodeOffset = undefined;
989
+ this.sendEvent(new ContinuedEvent(WarpoDebugSession.threadId, true));
990
+ }
991
+ handleRuntimeExit(runtime) {
992
+ if (this.runtime !== runtime) {
993
+ return;
994
+ }
995
+ this.disposeRuntimeAndTerminate(runtime);
996
+ }
997
+ disposeRuntimeAndTerminate(runtime) {
998
+ runtime.dispose();
999
+ if (this.runtime === runtime) {
1000
+ this.runtime = undefined;
160
1001
  }
1002
+ this.resetRuntimeState();
1003
+ this.sendEvent(new TerminatedEvent());
1004
+ }
1005
+ resetRuntimeState() {
1006
+ this.disposeLoadedModule();
1007
+ this.pendingBreakpointSources.clear();
1008
+ this.stoppedWasmBytecodeOffset = undefined;
1009
+ this.stoppedForTrap = false;
161
1010
  }
162
1011
  logAllBreakpoints() {
163
1012
  this.log("=== Registered Breakpoints ===");
@@ -168,64 +1017,54 @@ export class WarpoDebugSession extends LoggingDebugSession {
168
1017
  }
169
1018
  this.log("==============================");
170
1019
  }
171
- requestBreakpointUpdate() {
172
- if (!this.runtime || !this.loadedModule || this.pendingBreakpointUpdatesBySource.size === 0) {
1020
+ requestBreakpointUpdate(onComplete) {
1021
+ if (!this.runtime || !this.loadedModule || this.pendingBreakpointSources.size === 0) {
1022
+ onComplete?.();
173
1023
  return;
174
1024
  }
175
1025
  if (this.runtime.isPaused()) {
176
- this.applyPendingBreakpointUpdates(this.runtime);
1026
+ this.applyPendingBreakpointUpdates(this.runtime, onComplete);
177
1027
  return;
178
1028
  }
179
- for (const sourcePath of this.pendingBreakpointUpdatesBySource.keys()) {
180
- if (this.loadedModule.hasSource(sourcePath)) {
181
- this.runtime.pause();
182
- return;
183
- }
184
- }
1029
+ this.runtime.pause();
185
1030
  }
186
1031
  applyPendingBreakpointUpdates(runtime, onComplete) {
187
- const module = this.loadedModule;
188
- if (!module) {
1032
+ const wasmModule = this.loadedModule;
1033
+ if (!wasmModule) {
189
1034
  onComplete?.();
190
1035
  return;
191
1036
  }
192
- for (const [sourcePath, breakpoints] of this.pendingBreakpointUpdatesBySource) {
193
- if (breakpoints.length === 0) {
194
- this.pendingBreakpointUpdatesBySource.delete(sourcePath);
195
- continue;
196
- }
197
- if (!module.hasSource(sourcePath)) {
198
- continue;
199
- }
200
- const breakpoint = breakpoints.shift();
201
- if (!breakpoint) {
202
- this.pendingBreakpointUpdatesBySource.delete(sourcePath);
203
- continue;
204
- }
205
- if (breakpoints.length === 0) {
206
- this.pendingBreakpointUpdatesBySource.delete(sourcePath);
207
- }
208
- const bytecodeOffset = module.findBytecodeOffset(breakpoint.source, breakpoint.line);
209
- if (bytecodeOffset === undefined) {
210
- this.log(`No source-map match for breakpoint ${path.basename(breakpoint.source)}:${breakpoint.line}`);
211
- this.applyPendingBreakpointUpdates(runtime, onComplete);
212
- return;
213
- }
214
- this.log(`Resolved breakpoint ${path.basename(breakpoint.source)}:${breakpoint.line} -> bytecode offset ${bytecodeOffset}`);
215
- runtime.setWasmBreakpoint(module, bytecodeOffset, {
216
- onSuccess: () => {
217
- this.applyPendingBreakpointUpdates(runtime, onComplete);
218
- },
219
- onError: (error) => {
220
- this.log(`Failed to set breakpoint ${path.basename(breakpoint.source)}:${breakpoint.line}: ${error.message}`);
221
- this.applyPendingBreakpointUpdates(runtime, onComplete);
222
- },
223
- });
1037
+ if (this.pendingBreakpointSources.size === 0) {
1038
+ onComplete?.();
224
1039
  return;
225
1040
  }
226
- onComplete?.();
1041
+ 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
+ }
1055
+ runtime.setWasmBreakpoints(wasmModule, Array.from(wasmBytecodeOffsets).toSorted((left, right) => left - right), {
1056
+ onSuccess: () => {
1057
+ this.log(`Breakpoint set installed: ${wasmBytecodeOffsets.size} locations`);
1058
+ onComplete?.();
1059
+ },
1060
+ onError: (error) => {
1061
+ this.log(`Failed to update breakpoints: ${error.message}`);
1062
+ onComplete?.();
1063
+ },
1064
+ });
227
1065
  }
228
1066
  disposeLoadedModule() {
1067
+ this.clearVariableContainers();
229
1068
  this.loadedModule?.dispose();
230
1069
  this.loadedModule = undefined;
231
1070
  }