warpo 3.1.2 → 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 (60) hide show
  1. package/asconfig.schema.json +10 -2
  2. package/dist/common/leb128.d.ts +5 -0
  3. package/dist/common/leb128.js +17 -0
  4. package/dist/common/leb128.js.map +1 -0
  5. package/dist/debug_server/debugSession.d.ts +31 -2
  6. package/dist/debug_server/debugSession.js +491 -126
  7. package/dist/debug_server/debugSession.js.map +1 -1
  8. package/dist/debug_server/debugTrace.d.ts +2 -0
  9. package/dist/debug_server/debugTrace.js +38 -0
  10. package/dist/debug_server/debugTrace.js.map +1 -0
  11. package/dist/debug_server/debugger.d.ts +6 -2
  12. package/dist/debug_server/debuggerWasmModule.d.ts +7 -0
  13. package/dist/debug_server/debuggerWasmModule.js +162 -17
  14. package/dist/debug_server/debuggerWasmModule.js.map +1 -1
  15. package/dist/debug_server/nodeDebugger.d.ts +18 -2
  16. package/dist/debug_server/nodeDebugger.js +134 -30
  17. package/dist/debug_server/nodeDebugger.js.map +1 -1
  18. package/dist/debug_server/tests/debugSession.test.js +598 -21
  19. package/dist/debug_server/tests/debugSession.test.js.map +1 -1
  20. package/dist/debug_server/tests/testModule/debugger_breakpoint_update.d.ts +1 -0
  21. package/dist/debug_server/tests/testModule/debugger_breakpoint_update.js +10 -0
  22. package/dist/debug_server/tests/testModule/debugger_breakpoint_update.js.map +1 -0
  23. package/dist/debug_server/tests/testModule/debugger_breakpoint_validation.d.ts +1 -0
  24. package/dist/debug_server/tests/testModule/debugger_breakpoint_validation.js +12 -0
  25. package/dist/debug_server/tests/testModule/debugger_breakpoint_validation.js.map +1 -0
  26. package/dist/debug_server/tests/testModule/debugger_function_closure.d.ts +1 -0
  27. package/dist/debug_server/tests/testModule/debugger_function_closure.js +24 -0
  28. package/dist/debug_server/tests/testModule/debugger_function_closure.js.map +1 -0
  29. package/dist/debug_server/tests/testModule/debugger_generic_function.d.ts +1 -0
  30. package/dist/debug_server/tests/testModule/debugger_generic_function.js +10 -0
  31. package/dist/debug_server/tests/testModule/debugger_generic_function.js.map +1 -0
  32. package/dist/debug_server/tests/testModule/debugger_import.case.d.ts +1 -0
  33. package/dist/debug_server/tests/testModule/debugger_import.case.js +9 -0
  34. package/dist/debug_server/tests/testModule/debugger_import.case.js.map +1 -0
  35. package/dist/debug_server/tests/testModule/debugger_import.d.ts +1 -0
  36. package/dist/debug_server/tests/testModule/debugger_import.js +6 -0
  37. package/dist/debug_server/tests/testModule/debugger_import.js.map +1 -0
  38. package/dist/debug_server/tests/testModule/debugger_member_function.d.ts +1 -0
  39. package/dist/debug_server/tests/testModule/debugger_member_function.js +16 -0
  40. package/dist/debug_server/tests/testModule/debugger_member_function.js.map +1 -0
  41. package/dist/debug_server/tests/testModule/debugger_pause.d.ts +1 -0
  42. package/dist/debug_server/tests/testModule/debugger_pause.js +9 -0
  43. package/dist/debug_server/tests/testModule/debugger_pause.js.map +1 -0
  44. package/dist/debug_server/tests/testModule/debugger_stepping.d.ts +3 -0
  45. package/dist/debug_server/tests/testModule/debugger_stepping.js +28 -0
  46. package/dist/debug_server/tests/testModule/debugger_stepping.js.map +1 -0
  47. package/dist/debug_server/tests/testModule/debugger_typed_array.d.ts +1 -0
  48. package/dist/debug_server/tests/testModule/debugger_typed_array.js +52 -0
  49. package/dist/debug_server/tests/testModule/debugger_typed_array.js.map +1 -0
  50. package/dist/debug_server/tests/testModule/debugger_utf8_const_str.d.ts +1 -0
  51. package/dist/debug_server/tests/testModule/debugger_utf8_const_str.js +10 -0
  52. package/dist/debug_server/tests/testModule/debugger_utf8_const_str.js.map +1 -0
  53. package/dist/dwarf/dwarfParser.js +6 -13
  54. package/dist/dwarf/dwarfParser.js.map +1 -1
  55. package/dist/dwarf/functionDebugInfo.d.ts +3 -0
  56. package/dist/dwarf/functionDebugInfo.js +24 -0
  57. package/dist/dwarf/functionDebugInfo.js.map +1 -1
  58. package/dist/test_runner/core/compiler.js +2 -0
  59. package/dist/test_runner/core/compiler.js.map +1 -1
  60. 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";
@@ -33,17 +58,21 @@ export class WarpoDebugSession extends LoggingDebugSession {
33
58
  static threadId = 1;
34
59
  breakpointId = 0;
35
60
  requestedBreakpointsBySource = new Map();
36
- pendingBreakpointUpdatesBySource = new Map();
61
+ pendingBreakpointSources = new Set();
37
62
  variableContainers = new Map();
38
63
  pausedStackFramesById = new Map();
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,41 +129,76 @@ 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
- this.pendingBreakpointUpdatesBySource.set(sourcePath, [...bps]);
110
- const breakpoints = bps.map((bp) => {
111
- const dbp = new Breakpoint(bp.verified, bp.line);
112
- dbp.id = bp.id;
113
- return dbp;
114
- });
159
+ this.pendingBreakpointSources.clear();
160
+ for (const pendingSourcePath of this.requestedBreakpointsBySource.keys()) {
161
+ this.pendingBreakpointSources.add(pendingSourcePath);
162
+ }
163
+ const breakpoints = bps.map((bp) => this.toDebugProtocolBreakpoint(bp));
115
164
  response.body = { breakpoints };
116
- this.sendResponse(response);
117
- this.requestBreakpointUpdate();
165
+ if (this.runtime?.isPaused() && this.loadedModule) {
166
+ this.requestBreakpointUpdate(() => this.sendResponse(response));
167
+ }
168
+ else {
169
+ this.sendResponse(response);
170
+ this.requestBreakpointUpdate();
171
+ }
118
172
  }
119
173
  configurationDoneRequest(response, _args) {
174
+ this.configurationDone = true;
120
175
  super.configurationDoneRequest(response, _args);
121
176
  this.logAllBreakpoints();
177
+ this.maybeStartRuntime();
122
178
  }
123
179
  launchRequest(response, args) {
124
180
  void this.doLaunchRequest(response, args);
125
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
+ }
126
202
  async doLaunchRequest(response, args) {
127
203
  try {
128
204
  this.debugSessionLogging = args.debugSessionLogging ?? false;
@@ -134,6 +210,7 @@ export class WarpoDebugSession extends LoggingDebugSession {
134
210
  return;
135
211
  }
136
212
  const wasmFilePath = path.resolve(program);
213
+ configureTrace(args.debugSessionLogFile, cwd);
137
214
  this.log(`Launch requested for: ${program}`);
138
215
  if (launchType === "wasm file" || launchType === "unittest") {
139
216
  const runtimeName = args.runtime ?? "node";
@@ -148,6 +225,7 @@ export class WarpoDebugSession extends LoggingDebugSession {
148
225
  runtime.onModuleLoad = (wasmModule) => {
149
226
  this.loadedModule = wasmModule;
150
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}`);
151
229
  this.sendEvent(new LoadedSourceEvent("new", {
152
230
  name: path.basename(wasmModule.wasmFilePath),
153
231
  path: wasmModule.wasmFilePath,
@@ -156,16 +234,7 @@ export class WarpoDebugSession extends LoggingDebugSession {
156
234
  name: path.basename(wasmModule.sourceMapFilePath),
157
235
  path: wasmModule.sourceMapFilePath,
158
236
  }));
159
- this.applyPendingBreakpointUpdates(runtime, () => {
160
- if (runtime.isPaused()) {
161
- void runtime.resume().catch((error) => {
162
- const message = error instanceof Error ? error.message : "unknown error";
163
- this.log(`Failed to resume after module load: ${message}`);
164
- });
165
- return;
166
- }
167
- runtime.finishModuleLoad();
168
- });
237
+ this.applyPendingBreakpointUpdates(runtime, () => this.maybeStartRuntime());
169
238
  };
170
239
  runtime.onPause = (info) => {
171
240
  this.handleRuntimePause(runtime, info);
@@ -180,7 +249,6 @@ export class WarpoDebugSession extends LoggingDebugSession {
180
249
  this.log(message);
181
250
  };
182
251
  this.disposeLoadedModule();
183
- this.runtime?.dispose();
184
252
  this.runtime = runtime;
185
253
  await runtime.launch(launchType === "unittest"
186
254
  ? {
@@ -228,9 +296,52 @@ export class WarpoDebugSession extends LoggingDebugSession {
228
296
  continueRequest(response, _args) {
229
297
  this.doContinueRequest(response);
230
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
+ }
231
333
  disconnectRequest(response, _args) {
232
- this.runtime?.dispose();
334
+ void this.doDisconnectRequest(response);
335
+ }
336
+ async doDisconnectRequest(response) {
337
+ const runtime = this.runtime;
233
338
  this.runtime = undefined;
339
+ try {
340
+ await runtime?.dispose();
341
+ }
342
+ catch (error) {
343
+ this.log(`Failed to dispose runtime: ${formatUnknownError(error)}`);
344
+ }
234
345
  this.resetRuntimeState();
235
346
  this.debugSessionLogging = false;
236
347
  this.dispose();
@@ -293,6 +404,16 @@ export class WarpoDebugSession extends LoggingDebugSession {
293
404
  response.body = { variables: await this.decodeObjectAtAddress(variableContainer.address) };
294
405
  break;
295
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
+ }
296
417
  case "map-entry": {
297
418
  response.body = {
298
419
  variables: [variableContainer.key, variableContainer.value].map((variable) => this.toDebugProtocolVariable(variable)),
@@ -358,24 +479,78 @@ export class WarpoDebugSession extends LoggingDebugSession {
358
479
  }
359
480
  const sourceVariables = sourceVariableScope.variables;
360
481
  const rawWasmValues = await this.resolveRawWasmValues(sourceVariables, runtimeVariablesByIndex, runtime);
361
- const closureTupleValuesByLevel = await this.readClosureTupleChain(sourceVariableScope.rootClosureEnvLocalIndex, runtimeVariablesByIndex);
362
- const scopeTupleLevel = sourceVariableScope.tupleLevel ?? 0;
363
- const rawLocalValues = sourceVariables.map((variable) => {
364
- if (variable.kind === "closure") {
365
- const elementIndex = variable.fieldOffset / SMALL_TUPLE_SLOT_SIZE;
366
- const levelValues = closureTupleValuesByLevel.get(scopeTupleLevel);
367
- const tupleRawValue = levelValues?.get(elementIndex);
368
- if (tupleRawValue === undefined) {
369
- 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;
370
503
  }
371
- return { name: variable.name, value: tupleRawValue, typeName: variable.typeName };
504
+ const parentAddress = Number(parentTuple.value);
505
+ if (!Number.isInteger(parentAddress) || parentAddress <= 0) {
506
+ break;
507
+ }
508
+ currentAddress = parentAddress;
509
+ tupleLevel++;
372
510
  }
373
- const wasmRawValue = rawWasmValues.get(variable);
374
- assert(wasmRawValue !== undefined);
375
- 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);
376
553
  });
377
- const variables = await Promise.all(rawLocalValues.map((variable) => this.toDebugSessionVariableValue(variable.name, variable.typeName, variable.value)));
378
- return variables.map((variable) => this.toDebugProtocolVariable(variable));
379
554
  }
380
555
  async decodeObjectAtAddress(address) {
381
556
  const fields = await this.resolveObjectFields(address);
@@ -400,39 +575,16 @@ export class WarpoDebugSession extends LoggingDebugSession {
400
575
  }
401
576
  return rawWasmValues;
402
577
  }
403
- async readClosureTupleChain(closureEnvLocalIndex, runtimeVariablesByIndex) {
404
- const closureTupleValuesByLevel = new Map();
578
+ resolveClosureTupleAddress(closureEnvLocalIndex, runtimeVariablesByIndex) {
405
579
  if (closureEnvLocalIndex === undefined) {
406
- return closureTupleValuesByLevel;
580
+ return undefined;
407
581
  }
408
582
  const closureEnvVariable = runtimeVariablesByIndex.get(closureEnvLocalIndex);
409
583
  if (closureEnvVariable === undefined) {
410
- return closureTupleValuesByLevel;
411
- }
412
- let address = Number(closureEnvVariable.value);
413
- if (!Number.isInteger(address) || address <= 0) {
414
- return closureTupleValuesByLevel;
415
- }
416
- let level = 0;
417
- while (true) {
418
- const tupleValues = await this.readTupleElementValues(address);
419
- const levelValues = new Map();
420
- for (const [index, value] of tupleValues.entries()) {
421
- levelValues.set(index, value.value);
422
- }
423
- closureTupleValuesByLevel.set(level, levelValues);
424
- const parentAddress = levelValues.get(0);
425
- if (parentAddress === undefined) {
426
- break;
427
- }
428
- const parentAddressNumber = Number(parentAddress);
429
- if (!Number.isInteger(parentAddressNumber) || parentAddressNumber <= 0) {
430
- break;
431
- }
432
- address = parentAddressNumber;
433
- level++;
584
+ return undefined;
434
585
  }
435
- return closureTupleValuesByLevel;
586
+ const address = Number(closureEnvVariable.value);
587
+ return Number.isInteger(address) && address > 0 ? address : undefined;
436
588
  }
437
589
  resolvePausedStackFrame(frameId) {
438
590
  return this.pausedStackFramesById.get(frameId);
@@ -444,7 +596,7 @@ export class WarpoDebugSession extends LoggingDebugSession {
444
596
  if (typeName !== undefined) {
445
597
  const objectAddress = this.parseObjectAddress(typeName, value);
446
598
  if (objectAddress) {
447
- if (typeName === AS_STRING_CLASS_NAME) {
599
+ if (isDisplayableStringClass(typeName)) {
448
600
  const stringValue = await this.decodeStringAtAddress(typeName, objectAddress);
449
601
  return this.createClassVariable(name, typeName, objectAddress, stringValue);
450
602
  }
@@ -479,12 +631,18 @@ export class WarpoDebugSession extends LoggingDebugSession {
479
631
  case "map-entry": {
480
632
  return "";
481
633
  }
634
+ case "synthetic": {
635
+ return variable.value;
636
+ }
482
637
  }
483
638
  }
484
639
  getVariableReference(variable, isExpandableClass) {
485
640
  if (isExpandableClass && variable.kind === "class") {
486
641
  return this.toObjectVariablesReference(variable.address);
487
642
  }
643
+ if (variable.kind === "synthetic") {
644
+ return variable.container === undefined ? 0 : this.createVariableContainer(variable.container);
645
+ }
488
646
  if (variable.kind === "map-entry") {
489
647
  return this.createVariableContainer({ kind: "map-entry", key: variable.key, value: variable.value });
490
648
  }
@@ -494,7 +652,7 @@ export class WarpoDebugSession extends LoggingDebugSession {
494
652
  return { kind: "class", name, typeName, address, displayValue };
495
653
  }
496
654
  parseObjectAddress(typeName, value) {
497
- if (!this.loadedModule?.getClassLayout(typeName)) {
655
+ if (typeName !== UTF8_CONST_STR_CLASS_NAME && !this.loadedModule?.getClassLayout(typeName)) {
498
656
  return undefined;
499
657
  }
500
658
  const address = Number(value);
@@ -591,9 +749,16 @@ export class WarpoDebugSession extends LoggingDebugSession {
591
749
  return [];
592
750
  }
593
751
  const view = new DataView(memory.buffer, memory.byteOffset, memory.byteLength);
752
+ if (classLayout.builtinKind === BuiltinContainerKind.Function) {
753
+ return this.decodeFunctionElements(view, classLayout);
754
+ }
594
755
  if (classLayout.builtinKind === BuiltinContainerKind.ArrayBuffer) {
595
756
  return this.decodeArrayBufferElements(view);
596
757
  }
758
+ const typedArrayElementType = TYPED_ARRAY_ELEMENT_TYPES.get(classLayout.name);
759
+ if (typedArrayElementType !== undefined) {
760
+ return this.decodeTypedArrayElements(address, view, classLayout, typedArrayElementType);
761
+ }
597
762
  if (classLayout.builtinKind === BuiltinContainerKind.Array) {
598
763
  return this.decodeArrayElements(address, view, classLayout);
599
764
  }
@@ -608,10 +773,49 @@ export class WarpoDebugSession extends LoggingDebugSession {
608
773
  }
609
774
  return Promise.all(classLayout.fields.map((field) => this.decodeFieldVariable(view, field)));
610
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
+ }
611
814
  async resolveRuntimeObjectPayloadSize(address, classLayout) {
612
815
  if (classLayout.builtinKind !== BuiltinContainerKind.SmallTuple &&
613
816
  classLayout.builtinKind !== BuiltinContainerKind.StaticArray &&
614
- classLayout.builtinKind !== BuiltinContainerKind.ArrayBuffer) {
817
+ classLayout.builtinKind !== BuiltinContainerKind.ArrayBuffer &&
818
+ !TYPED_ARRAY_ELEMENT_TYPES.has(classLayout.name)) {
615
819
  return classLayout.byteSize;
616
820
  }
617
821
  const sizeMemory = await this.runtime?.readWasmMemory(address + OBJECT_RTSIZE_OFFSET, OBJECT_RTSIZE_SIZE);
@@ -651,7 +855,7 @@ export class WarpoDebugSession extends LoggingDebugSession {
651
855
  this.log(`Error: cannot expand tuple reference at ${elementAddress}: runtime class layout is missing`);
652
856
  return { kind: "basic", name, value: elementAddress.toString(), typeName: undefined };
653
857
  }
654
- if (classLayout.name === AS_STRING_CLASS_NAME) {
858
+ if (isDisplayableStringClass(classLayout.name)) {
655
859
  const stringValue = await this.decodeStringAtAddress(classLayout.name, elementAddress);
656
860
  return this.createClassVariable(name, classLayout.name, elementAddress, stringValue);
657
861
  }
@@ -691,6 +895,32 @@ export class WarpoDebugSession extends LoggingDebugSession {
691
895
  }
692
896
  return elements;
693
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
+ }
694
924
  async decodeStaticArrayElements(address, view, classLayout) {
695
925
  assert.equal(classLayout.builtinKind, BuiltinContainerKind.StaticArray);
696
926
  const elementTypeName = classLayout.templateType;
@@ -849,7 +1079,7 @@ export class WarpoDebugSession extends LoggingDebugSession {
849
1079
  const value = this.decodeFieldValue(view, field);
850
1080
  const address = field.isReference ? this.parseObjectAddress(field.typeName, value) : undefined;
851
1081
  if (address) {
852
- if (field.typeName === AS_STRING_CLASS_NAME) {
1082
+ if (isDisplayableStringClass(field.typeName)) {
853
1083
  const stringValue = await this.decodeStringAtAddress(field.typeName, address);
854
1084
  return this.createClassVariable(field.name, field.typeName, address, stringValue);
855
1085
  }
@@ -931,7 +1161,7 @@ export class WarpoDebugSession extends LoggingDebugSession {
931
1161
  }
932
1162
  }
933
1163
  async decodeStringAtAddress(typeName, address) {
934
- assert.equal(typeName, AS_STRING_CLASS_NAME);
1164
+ assert.ok(isDisplayableStringClass(typeName));
935
1165
  if (!this.runtime) {
936
1166
  this.log(`Error: cannot decode string at ${address}: runtime is not active`);
937
1167
  return "<unavailable>";
@@ -948,7 +1178,8 @@ export class WarpoDebugSession extends LoggingDebugSession {
948
1178
  this.log(`Error: cannot decode string at ${address}: string payload memory is unavailable`);
949
1179
  return "<unavailable>";
950
1180
  }
951
- 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));
952
1183
  }
953
1184
  doContinueRequest(response) {
954
1185
  const runtime = this.runtime;
@@ -957,13 +1188,107 @@ export class WarpoDebugSession extends LoggingDebugSession {
957
1188
  return;
958
1189
  }
959
1190
  response.body = { allThreadsContinued: true };
960
- this.sendResponse(response);
961
1191
  if (this.stoppedForTrap) {
1192
+ this.sendResponse(response);
962
1193
  this.log("Continue requested from wasm trap; terminating debug session");
963
- this.disposeRuntimeAndTerminate(runtime);
1194
+ void this.disposeRuntimeAndTerminate(runtime);
964
1195
  return;
965
1196
  }
1197
+ this.cancelStep();
966
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;
967
1292
  }
968
1293
  async resumeRuntime(runtime) {
969
1294
  try {
@@ -984,21 +1309,33 @@ export class WarpoDebugSession extends LoggingDebugSession {
984
1309
  if (this.runtime !== runtime) {
985
1310
  return;
986
1311
  }
987
- this.disposeRuntimeAndTerminate(runtime);
1312
+ void this.disposeRuntimeAndTerminate(runtime);
988
1313
  }
989
- disposeRuntimeAndTerminate(runtime) {
990
- runtime.dispose();
1314
+ async disposeRuntimeAndTerminate(runtime) {
991
1315
  if (this.runtime === runtime) {
992
1316
  this.runtime = undefined;
993
1317
  }
1318
+ try {
1319
+ await runtime.dispose();
1320
+ }
1321
+ catch (error) {
1322
+ this.log(`Failed to dispose runtime: ${formatUnknownError(error)}`);
1323
+ }
994
1324
  this.resetRuntimeState();
995
1325
  this.sendEvent(new TerminatedEvent());
996
1326
  }
997
1327
  resetRuntimeState() {
998
1328
  this.disposeLoadedModule();
999
- this.pendingBreakpointUpdatesBySource.clear();
1329
+ this.pendingBreakpointSources.clear();
1000
1330
  this.stoppedWasmBytecodeOffset = undefined;
1001
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
+ }
1002
1339
  }
1003
1340
  logAllBreakpoints() {
1004
1341
  this.log("=== Registered Breakpoints ===");
@@ -1009,63 +1346,91 @@ export class WarpoDebugSession extends LoggingDebugSession {
1009
1346
  }
1010
1347
  this.log("==============================");
1011
1348
  }
1012
- requestBreakpointUpdate() {
1013
- if (!this.runtime || !this.loadedModule || this.pendingBreakpointUpdatesBySource.size === 0) {
1349
+ toDebugProtocolBreakpoint(breakpoint) {
1350
+ const dbp = new Breakpoint(breakpoint.verified, breakpoint.line);
1351
+ dbp.id = breakpoint.id;
1352
+ return dbp;
1353
+ }
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}`);
1356
+ if (!this.runtime || !this.loadedModule || this.pendingBreakpointSources.size === 0) {
1357
+ this.log("Breakpoint update skipped: runtime, module, or pending source is unavailable");
1358
+ onComplete?.();
1014
1359
  return;
1015
1360
  }
1016
1361
  if (this.runtime.isPaused()) {
1017
- this.applyPendingBreakpointUpdates(this.runtime);
1362
+ this.applyPendingBreakpointUpdates(this.runtime, onComplete);
1018
1363
  return;
1019
1364
  }
1020
- for (const sourcePath of this.pendingBreakpointUpdatesBySource.keys()) {
1021
- if (this.loadedModule.hasSource(sourcePath)) {
1022
- this.runtime.pause();
1023
- return;
1024
- }
1365
+ this.runtime.pause();
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;
1025
1378
  }
1379
+ runtime.finishModuleLoad();
1026
1380
  }
1027
1381
  applyPendingBreakpointUpdates(runtime, onComplete) {
1028
1382
  const wasmModule = this.loadedModule;
1029
1383
  if (!wasmModule) {
1384
+ this.log("Breakpoint update skipped: no loaded wasm module");
1030
1385
  onComplete?.();
1031
1386
  return;
1032
1387
  }
1033
- for (const [sourcePath, breakpoints] of this.pendingBreakpointUpdatesBySource) {
1034
- if (breakpoints.length === 0) {
1035
- this.pendingBreakpointUpdatesBySource.delete(sourcePath);
1036
- continue;
1037
- }
1038
- if (!wasmModule.hasSource(sourcePath)) {
1039
- continue;
1040
- }
1041
- const breakpoint = breakpoints.shift();
1042
- if (!breakpoint) {
1043
- this.pendingBreakpointUpdatesBySource.delete(sourcePath);
1044
- continue;
1045
- }
1046
- if (breakpoints.length === 0) {
1047
- this.pendingBreakpointUpdatesBySource.delete(sourcePath);
1048
- }
1049
- const breakpointLocation = wasmModule.resolveBreakpointLocation(breakpoint);
1050
- if (!breakpointLocation) {
1051
- this.log(`No source-map match for breakpoint ${path.basename(breakpoint.source)}:${breakpoint.line}`);
1052
- this.applyPendingBreakpointUpdates(runtime, onComplete);
1053
- return;
1054
- }
1055
- this.log(`Resolved breakpoint ${path.basename(breakpoint.source)}:${breakpoint.line} -> ${breakpointLocation.wasmBytecodeOffset}`);
1056
- runtime.setWasmBreakpoint(wasmModule, breakpointLocation.wasmBytecodeOffset, {
1057
- onSuccess: () => {
1058
- this.log(`Breakpoint installed at ${breakpointLocation.wasmBytecodeOffset}`);
1059
- this.applyPendingBreakpointUpdates(runtime, onComplete);
1060
- },
1061
- onError: (error) => {
1062
- this.log(`Failed to set breakpoint ${path.basename(breakpoint.source)}:${breakpoint.line} at ${breakpointLocation.wasmBytecodeOffset}: ${error.message}`);
1063
- this.applyPendingBreakpointUpdates(runtime, onComplete);
1064
- },
1065
- });
1388
+ if (this.pendingBreakpointSources.size === 0) {
1389
+ this.log("Breakpoint update skipped: no pending breakpoint sources");
1390
+ onComplete?.();
1066
1391
  return;
1067
1392
  }
1068
- onComplete?.();
1393
+ this.log(`Applying breakpoint updates: requestedSources=${this.requestedBreakpointsBySource.size}, pendingSources=${this.pendingBreakpointSources.size}`);
1394
+ this.pendingBreakpointSources.clear();
1395
+ const wasmBytecodeOffsets = this.collectBreakpointOffsets(wasmModule);
1396
+ runtime.setWasmBreakpoints(wasmModule, Array.from(wasmBytecodeOffsets).toSorted((left, right) => left - right), {
1397
+ onSuccess: () => {
1398
+ this.log(`Breakpoint set installed: ${wasmBytecodeOffsets.size} locations, offsets=${Array.from(wasmBytecodeOffsets)
1399
+ .toSorted((left, right) => left - right)
1400
+ .join(",") || "none"}`);
1401
+ onComplete?.();
1402
+ },
1403
+ onError: (error) => {
1404
+ this.log(`Failed to update breakpoints: ${error.message}`);
1405
+ onComplete?.();
1406
+ },
1407
+ });
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
+ }
1069
1434
  }
1070
1435
  disposeLoadedModule() {
1071
1436
  this.clearVariableContainers();