Skip to content

Commit 839713b

Browse files
committed
Implement Debugger
1 parent 3d7023b commit 839713b

File tree

6 files changed

+158
-36
lines changed

6 files changed

+158
-36
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
package dotty.tools.debug
22

3-
import scala.util.Using
4-
import scala.io.Source
5-
import java.nio.charset.StandardCharsets
6-
import scala.io.Codec
3+
import com.sun.jdi.Location
74
import dotty.tools.io.JFile
8-
import java.nio.file.Files
95
import dotty.tools.readLines
106

117
/**
@@ -19,38 +15,36 @@ private[debug] object DebugStepAssert:
1915
def parseCheckFile(checkFile: JFile): Seq[DebugStepAssert[?]] =
2016
val sym = "[a-zA-Z0-9$.]+"
2117
val line = "\\d+"
22-
val break = s"break ($sym) ($line)".r
23-
val step = s"step ($sym|$line)".r
24-
val next = s"next ($sym|$line)".r
25-
val comment = "// .*".r
26-
val empty = "\\w*".r
18+
val trailing = s"\\s*(?:\\/\\/.*)?".r // empty or comment
19+
val break = s"break ($sym)$trailing".r
20+
val step = s"step ($sym|$line)$trailing".r
21+
val next = s"next ($sym|$line)$trailing".r
2722
readLines(checkFile).flatMap:
2823
case break(className , lineStr) =>
2924
val line = lineStr.toInt
30-
Some(DebugStepAssert(Break(className, line), checkFrame(className, line)))
25+
Some(DebugStepAssert(Break(className, line), checkClassAndLine(className, line)))
3126
case step(pattern) => Some(DebugStepAssert(Step, checkLineOrMethod(pattern)))
32-
case next(pattern) => Some(DebugStepAssert(Step, checkLineOrMethod(pattern)))
33-
case comment() | empty() => None
27+
case next(pattern) => Some(DebugStepAssert(Next, checkLineOrMethod(pattern)))
28+
case trailing() => None
3429
case invalid => throw new Exception(s"Cannot parse debug step: $invalid")
3530

36-
private def checkFrame(className: String, line: Int)(frame: Frame): Unit =
37-
assert(className.matches(frame.className))
38-
assert(frame.line == line)
31+
private def checkClassAndLine(className: String, line: Int)(location: Location): Unit =
32+
assert(className == location.declaringType.name, s"obtained ${location.declaringType.name}, expected ${className}")
33+
checkLine(line)(location)
3934

40-
private def checkLineOrMethod(pattern: String): Frame => Unit =
35+
private def checkLineOrMethod(pattern: String): Location => Unit =
4136
if "(\\d+)".r.matches(pattern) then checkLine(pattern.toInt) else checkMethod(pattern)
4237

43-
private def checkLine(line: Int)(frame: Frame): Unit = assert(frame.line == line)
38+
private def checkLine(line: Int)(location: Location): Unit =
39+
assert(location.lineNumber == line, s"obtained ${location.lineNumber}, expected $line")
4440

45-
private def checkMethod(method: String)(frame: Frame): Unit =
46-
assert(method.matches(s"${frame.className}.${frame.methodName}"))
41+
private def checkMethod(methodName: String)(location: Location): Unit = assert(methodName == location.method.name)
4742
end DebugStepAssert
4843

4944
private[debug] enum DebugStep[T]:
50-
case Break(className: String, line: Int) extends DebugStep[Frame]
51-
case Step extends DebugStep[Frame]
52-
case Next extends DebugStep[Frame]
45+
case Break(className: String, line: Int) extends DebugStep[Location]
46+
case Step extends DebugStep[Location]
47+
case Next extends DebugStep[Location]
5348

54-
private[debug] case class Frame(className: String, methodName: String, line: Int)
5549

5650

compiler/test/dotty/tools/debug/DebugTests.scala

+39-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package dotty.tools.debug
22

3+
import com.sun.jdi.*
34
import dotty.Properties
45
import dotty.tools.dotc.reporting.TestReporter
56
import dotty.tools.io.JFile
@@ -12,10 +13,9 @@ class DebugTests:
1213
import DebugTests.*
1314
@Test def debug: Unit =
1415
implicit val testGroup: TestGroup = TestGroup("debug")
16+
// compileFile("tests/debug/tailrec.scala", TestConfiguration.defaultOptions).checkDebug()
1517
compileFilesInDir("tests/debug", TestConfiguration.defaultOptions).checkDebug()
1618

17-
end DebugTests
18-
1919
object DebugTests extends ParallelTesting:
2020
def maxDuration = 45.seconds
2121
def numberOfSlaves = Runtime.getRuntime().availableProcessors()
@@ -45,9 +45,16 @@ object DebugTests extends ParallelTesting:
4545
val checkFile = testSource.checkFile.getOrElse(throw new Exception("Missing check file"))
4646
val debugSteps = DebugStepAssert.parseCheckFile(checkFile)
4747
val status = debugMain(testSource.runClassPath): debuggee =>
48-
val debugger = Debugger(debuggee.jdiPort, maxDuration)
49-
try debuggee.launch()
50-
finally debugger.dispose()
48+
val debugger = Debugger(debuggee.jdiPort, maxDuration/* , verbose = true */)
49+
// configure the breakpoints before starting the debuggee
50+
val breakpoints = debugSteps.map(_.step).collect { case b: DebugStep.Break => b }
51+
for b <- breakpoints do debugger.configureBreakpoint(b.className, b.line)
52+
try
53+
debuggee.launch()
54+
playDebugSteps(debugger, debugSteps/* , verbose = true */)
55+
finally
56+
// stop debugger to let debuggee terminate its execution
57+
debugger.dispose()
5158
status match
5259
case Success(output) => ()
5360
case Failure(output) =>
@@ -60,5 +67,32 @@ object DebugTests extends ParallelTesting:
6067
case Timeout =>
6168
echo("failed because test " + testSource.title + " timed out")
6269
failTestSource(testSource, TimeoutFailure(testSource.title))
70+
end verifyDebug
71+
72+
private def playDebugSteps(debugger: Debugger, steps: Seq[DebugStepAssert[?]], verbose: Boolean = false): Unit =
73+
import scala.language.unsafeNulls
74+
75+
var thread: ThreadReference = null
76+
def location = thread.frame(0).location
6377

78+
for case step <- steps do
79+
import DebugStep.*
80+
step match
81+
case DebugStepAssert(Break(className, line), assert) =>
82+
// continue if paused
83+
if thread != null then
84+
debugger.continue(thread)
85+
thread = null
86+
thread = debugger.break()
87+
if verbose then println(s"break ${location.declaringType.name} ${location.lineNumber}")
88+
assert(location)
89+
case DebugStepAssert(Next, assert) =>
90+
thread = debugger.next(thread)
91+
if verbose then println(s"next ${location.lineNumber}")
92+
assert(location)
93+
case DebugStepAssert(Step, assert) =>
94+
thread = debugger.step(thread)
95+
if verbose then println(s"step ${location.lineNumber}")
96+
assert(location)
97+
end playDebugSteps
6498
end DebugTest

compiler/test/dotty/tools/debug/Debugger.scala

+98-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,106 @@
11
package dotty.tools.debug
22

33
import com.sun.jdi.*
4-
import scala.jdk.CollectionConverters.*
4+
import com.sun.jdi.event.*
5+
import com.sun.jdi.request.*
6+
7+
import java.lang.ref.Reference
8+
import java.util.concurrent.LinkedBlockingQueue
9+
import java.util.concurrent.TimeUnit
10+
import java.util.concurrent.atomic.AtomicReference
511
import scala.concurrent.duration.Duration
12+
import scala.jdk.CollectionConverters.*
13+
14+
class Debugger(vm: VirtualMachine, maxDuration: Duration, verbose: Boolean = false):
15+
// For some JDI events that we receive, we wait for client actions.
16+
// Example: On a BreakpointEvent, the client may want to inspect frames and variables, before it
17+
// decides to step in or continue.
18+
private val pendingEvents = new LinkedBlockingQueue[Event]()
19+
20+
// Internal event subscriptions, to react to JDI events
21+
// Example: add a Breakpoint on a ClassPrepareEvent
22+
private val eventSubs = new AtomicReference(List.empty[PartialFunction[Event, Unit]])
23+
private val eventListener = startListeningVM()
24+
25+
def configureBreakpoint(className: String, line: Int): Unit =
26+
vm.classesByName(className).asScala.foreach(addBreakpoint(_, line))
27+
// watch class preparation and add breakpoint when the class is prepared
28+
val request = vm.eventRequestManager.createClassPrepareRequest
29+
request.addClassFilter(className)
30+
subscribe:
31+
case e: ClassPrepareEvent if e.request == request => addBreakpoint(e.referenceType, line)
32+
request.enable()
33+
34+
def break(): ThreadReference = receiveEvent { case e: BreakpointEvent => e.thread }
35+
36+
def continue(thread: ThreadReference): Unit = thread.resume()
37+
38+
def next(thread: ThreadReference): ThreadReference =
39+
stepAndWait(thread, StepRequest.STEP_LINE, StepRequest.STEP_OVER)
40+
41+
def step(thread: ThreadReference): ThreadReference =
42+
stepAndWait(thread, StepRequest.STEP_LINE, StepRequest.STEP_INTO)
43+
44+
/** stop listening and disconnect debugger */
45+
def dispose(): Unit =
46+
eventListener.interrupt()
47+
vm.dispose()
48+
49+
private def addBreakpoint(refType: ReferenceType, line: Int): Unit =
50+
for location <- refType.locationsOfLine(line).asScala do
51+
if verbose then println(s"Adding breakpoint in $location")
52+
val breakpoint = vm.eventRequestManager.createBreakpointRequest(location)
53+
// suspend only the thread which triggered the event
54+
breakpoint.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD)
55+
// let's enable the breakpoint and forget about it
56+
// we don't need to store it because we never remove any breakpoint
57+
breakpoint.enable()
58+
59+
private def stepAndWait(thread: ThreadReference, size: Int, depth: Int): ThreadReference =
60+
val request = vm.eventRequestManager.createStepRequest(thread, size, depth)
61+
request.enable()
62+
thread.resume()
63+
// Because our debuggee is mono-threaded, we don't check that `e.request` is our step request.
64+
// Indeed there can be only one step request per thread at a time.
65+
val newThreadRef = receiveEvent { case e: StepEvent => e.thread }
66+
request.disable()
67+
newThreadRef
68+
69+
private def subscribe(f: PartialFunction[Event, Unit]): Unit =
70+
eventSubs.updateAndGet(f :: _)
71+
72+
private def startListeningVM(): Thread =
73+
val thread = Thread: () =>
74+
var isAlive = true
75+
try while isAlive do
76+
val eventSet = vm.eventQueue.remove()
77+
val subscriptions = eventSubs.get
78+
var shouldResume = true
79+
for event <- eventSet.iterator.asScala.toSeq do
80+
if verbose then println(formatEvent(event))
81+
for f <- subscriptions if f.isDefinedAt(event) do f(event)
82+
event match
83+
case e: (BreakpointEvent | StepEvent) =>
84+
shouldResume = false
85+
pendingEvents.put(e)
86+
case _: VMDisconnectEvent => isAlive = false
87+
case _ => ()
88+
if shouldResume then eventSet.resume()
89+
catch case _: InterruptedException => ()
90+
thread.start()
91+
thread
92+
end startListeningVM
93+
94+
private def receiveEvent[T](f: PartialFunction[Event, T]): T =
95+
// poll repeatedly until we get an event that matches the partial function or a timeout
96+
Iterator.continually(pendingEvents.poll(maxDuration.toMillis, TimeUnit.MILLISECONDS))
97+
.collect(f)
98+
.next()
699

7-
class Debugger(vm: VirtualMachine, maxDuration: Duration):
8-
export vm.dispose
100+
private def formatEvent(event: Event): String =
101+
event match
102+
case e: ClassPrepareEvent => s"$e ${e.referenceType}"
103+
case e => e.toString
9104

10105
object Debugger:
11106
// The socket JDI connector

compiler/test/dotty/tools/vulpix/RunnerOrchestration.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ trait RunnerOrchestration {
143143
end debugMain
144144

145145
private def startMain(classPath: String): Future[Status] =
146-
// pass file to running process
146+
// pass classpath to running process
147147
process.stdin.println(classPath)
148148

149149
// Create a future reading the object:

tests/debug/function.check

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ step 10
44
break Test$ 6
55
step 7
66
step 8
7-
next 9
7+
next apply$mcIII$sp // specialized Lambda.apply
88
next 10
99
next 11

tests/debug/tailrec.check

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,5 @@ step 3
66
break Test$ 14
77
step 3
88
step 4
9-
// incorrect debug line
10-
step 6
9+
step 14
1110
step 15

0 commit comments

Comments
 (0)