Skip to content

Commit 20232d9

Browse files
committed
Fix breakpoints in loops & recursive functions
Currently, when continuing from a breakpoint, the debugger needs to remove it and restore the original instruction before resuming. The conventionally expected behaviour is for the original breakpoint to be preserved. For that, we don't fully resume, but only step to the next instruction, restore the original breakpoint, and then finally resume again. This still doesn't account for branching instructions, in the same way that the current instruction stepping does not. To be resolved in a future PR, as that requires precise instruction control flow analysis.
1 parent 297236b commit 20232d9

File tree

5 files changed

+81
-7
lines changed

5 files changed

+81
-7
lines changed

Sources/WasmKit/Execution/Debugger.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,12 +209,12 @@
209209
case .token:
210210
try self.execution.runTokenThreaded(sp: &sp, pc: &pc, md: &md, ms: &ms)
211211
}
212-
} catch is Execution.EndOfExecution {
212+
} catch let end as Execution.EndOfExecution {
213213
// The module successfully executed till the "end of execution" instruction.
214-
let type = self.store.engine.funcTypeInterner.resolve(currentFunction.type)
214+
let type = self.entrypointFunction.type
215215
self.state = .entrypointReturned(
216216
type.results.enumerated().map { (i, type) in
217-
sp[VReg(i)].cast(to: type)
217+
end.sp[VReg(i)].cast(to: type)
218218
}
219219
)
220220
}
@@ -256,6 +256,21 @@
256256
try self.run()
257257
}
258258

259+
/// Resumes the module instantiated by the debugger stopped at a breakpoint. The breakpoint from which
260+
/// the debugger resumes is preserved. If the module is current not stopped at a breakpoint, this function
261+
/// returns immediately.
262+
package mutating func runPreservingCurrentBreakpoint() throws {
263+
guard case .stoppedAtBreakpoint(let breakpoint) = self.state else {
264+
return
265+
}
266+
267+
// TODO: analyze actual instruction branching to set the breakpoint correctly.
268+
try self.enableBreakpoint(address: breakpoint.wasmPc + 1)
269+
try self.run()
270+
try self.enableBreakpoint(address: breakpoint.wasmPc)
271+
try self.run()
272+
}
273+
259274
package func getLocal(frameIndex: UInt, localIndex: UInt) throws -> UInt64 {
260275
guard case .stoppedAtBreakpoint(let breakpoint) = self.state else {
261276
throw Error.notStoppedAtBreakpoint

Sources/WasmKit/Execution/Execution.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,9 @@ extension Execution {
410410
}
411411

412412
/// An ``Error`` thrown when the execution normally ends.
413-
struct EndOfExecution: Error {}
413+
struct EndOfExecution: Error, @unchecked Sendable {
414+
let sp: Sp
415+
}
414416

415417
/// An ``Error`` thrown when a breakpoint is triggered.
416418
struct Breakpoint: Error, @unchecked Sendable {

Sources/WasmKit/Execution/Instructions/Control.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ extension Execution {
5252
}
5353

5454
mutating func endOfExecution(sp: inout Sp, pc: Pc) throws -> (Pc, CodeSlot) {
55-
throw EndOfExecution()
55+
throw EndOfExecution(sp: sp)
5656
}
5757

5858
@inline(__always)

Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,13 +268,13 @@
268268
case .step:
269269
try self.debugger.step()
270270
case .continue:
271-
try self.debugger.run()
271+
try self.debugger.runPreservingCurrentBreakpoint()
272272
}
273273

274274
responseKind = try self.currentThreadStopInfo
275275

276276
case .continue:
277-
try self.debugger.run()
277+
try self.debugger.runPreservingCurrentBreakpoint()
278278

279279
responseKind = try self.currentThreadStopInfo
280280

Tests/WasmKitTests/DebuggerTests.swift

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,28 @@
6161
)
6262
"""
6363

64+
private let factorialWAT = """
65+
(module
66+
(func (export "_start") (result i64)
67+
(i64.const 3)
68+
(call $factorial)
69+
)
70+
71+
(func $factorial (param $arg i64) (result i64)
72+
(if (result i64)
73+
(i64.eqz (local.get $arg))
74+
(then (i64.const 1))
75+
(else
76+
(i64.mul
77+
(local.get $arg)
78+
(call $factorial
79+
(i64.sub
80+
(local.get $arg)
81+
(i64.const 1)
82+
))))))
83+
)
84+
"""
85+
6486
@Suite
6587
struct DebuggerTests {
6688
@Test
@@ -145,6 +167,41 @@
145167
let secondLocal = try debugger.getLocal(frameIndex: 0, localIndex: 1)
146168
#expect(secondLocal == 24)
147169
}
170+
171+
@Test
172+
func runPreservingCurrentBreakpoint() throws {
173+
let store = Store(engine: Engine())
174+
let bytes = try wat2wasm(factorialWAT)
175+
let module = try parseWasm(bytes: bytes)
176+
var debugger = try Debugger(module: module, store: store, imports: [:])
177+
178+
_ = try debugger.enableBreakpoint(
179+
module: module,
180+
function: 1,
181+
// if 1 byte + i64.const 2 bytes + i64.eqz 1 byte + local.set 4 bytes
182+
offsetWithinFunction: 8
183+
)
184+
185+
try debugger.run()
186+
var local = try debugger.getLocal(frameIndex: 0, localIndex: 0)
187+
#expect(local == 3)
188+
try debugger.runPreservingCurrentBreakpoint()
189+
190+
local = try debugger.getLocal(frameIndex: 0, localIndex: 0)
191+
#expect(local == 2)
192+
try debugger.runPreservingCurrentBreakpoint()
193+
194+
local = try debugger.getLocal(frameIndex: 0, localIndex: 0)
195+
#expect(local == 1)
196+
try debugger.runPreservingCurrentBreakpoint()
197+
198+
guard case .entrypointReturned(let values) = debugger.state else {
199+
Issue.record()
200+
return
201+
}
202+
203+
#expect(values == [.i64(6)])
204+
}
148205
}
149206

150207
#endif

0 commit comments

Comments
 (0)