Skip to content

Commit af582f1

Browse files
committed
Introduce and implement ExpressionEvaluator
1 parent 8da89e4 commit af582f1

File tree

3 files changed

+202
-4
lines changed

3 files changed

+202
-4
lines changed

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ object DebugTests extends ParallelTesting:
4444
else
4545
val checkFile = testSource.checkFile.getOrElse(throw new Exception("Missing check file"))
4646
val debugSteps = DebugStepAssert.parseCheckFile(checkFile)
47+
val expressionEvaluator = ExpressionEvaluator(testSource.sourceFiles, testSource.flags, testSource.runClassPath, testSource.outDir)
4748
val status = debugMain(testSource.runClassPath): debuggee =>
48-
val debugger = Debugger(debuggee.jdiPort, maxDuration/* , verbose = true */)
49+
val debugger = Debugger(debuggee.jdiPort, expressionEvaluator, maxDuration/* , verbose = true */)
4950
// configure the breakpoints before starting the debuggee
5051
val breakpoints = debugSteps.map(_.step).collect { case b: DebugStep.Break => b }
5152
for b <- breakpoints do debugger.configureBreakpoint(b.className, b.line)
@@ -72,6 +73,11 @@ object DebugTests extends ParallelTesting:
7273
private def playDebugSteps(debugger: Debugger, steps: Seq[DebugStepAssert[?]], verbose: Boolean = false): Unit =
7374
import scala.language.unsafeNulls
7475

76+
/** The DebugTests can only debug one thread at a time. It cannot handle breakpoints in concurrent threads.
77+
* When thread is null, it means the JVM is running and no thread is waiting to be resumed.
78+
* If thread is not null, it is waiting to be resumed by calling continue, step or next.
79+
* While the thread is paused, it can be used for evaluation.
80+
*/
7581
var thread: ThreadReference = null
7682
def location = thread.frame(0).location
7783

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import java.util.concurrent.atomic.AtomicReference
1111
import scala.concurrent.duration.Duration
1212
import scala.jdk.CollectionConverters.*
1313

14-
class Debugger(vm: VirtualMachine, maxDuration: Duration, verbose: Boolean = false):
14+
class Debugger(vm: VirtualMachine, evaluator: ExpressionEvaluator, maxDuration: Duration, verbose: Boolean):
1515
// For some JDI events that we receive, we wait for client actions.
1616
// Example: On a BreakpointEvent, the client may want to inspect frames and variables, before it
1717
// decides to step in or continue.
@@ -41,6 +41,9 @@ class Debugger(vm: VirtualMachine, maxDuration: Duration, verbose: Boolean = fal
4141
def step(thread: ThreadReference): ThreadReference =
4242
stepAndWait(thread, StepRequest.STEP_LINE, StepRequest.STEP_INTO)
4343

44+
def evaluate(expression: String, thread: ThreadReference): Either[String, String] =
45+
evaluator.evaluate(expression, thread)
46+
4447
/** stop listening and disconnect debugger */
4548
def dispose(): Unit =
4649
eventListener.interrupt()
@@ -110,11 +113,11 @@ object Debugger:
110113
.find(_.getClass.getName == "com.sun.tools.jdi.SocketAttachingConnector")
111114
.get
112115

113-
def apply(jdiPort: Int, maxDuration: Duration): Debugger =
116+
def apply(jdiPort: Int, expressionEvaluator: ExpressionEvaluator, maxDuration: Duration, verbose: Boolean = false): Debugger =
114117
val arguments = connector.defaultArguments()
115118
arguments.get("hostname").setValue("localhost")
116119
arguments.get("port").setValue(jdiPort.toString)
117120
arguments.get("timeout").setValue(maxDuration.toMillis.toString)
118121
val vm = connector.attach(arguments)
119-
new Debugger(vm, maxDuration)
122+
new Debugger(vm, expressionEvaluator, maxDuration, verbose)
120123

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package dotty.tools.debug
2+
3+
import com.sun.jdi.*
4+
import dotty.tools.io.*
5+
import dotty.tools.vulpix.TestFlags
6+
7+
import scala.jdk.CollectionConverters.*
8+
9+
class ExpressionEvaluator(
10+
sources: Map[String, JPath],
11+
options: Array[String],
12+
classPath: String,
13+
outputDir: JPath
14+
):
15+
private val compiler = ExpressionCompilerBridge()
16+
private var uniqueID: Int = 1
17+
18+
/** returns the value of the evaluated expression or compiler errors */
19+
def evaluate(expression: String, thread: ThreadReference): Either[String, String] =
20+
// We evaluate the expression at the top frame of the stack
21+
val frame = thread.frame(0)
22+
23+
// Extract everything from the frame now because, as soon as we start using the thread
24+
// for remote execution, the frame becomes invalid
25+
val localVariables = frame.visibleVariables.asScala.toSeq
26+
val values = localVariables.map(frame.getValue)
27+
val location = frame.location
28+
val thisRef = frame.thisObject // null in a static context
29+
30+
for expressionClassName <- compile(expression, location, localVariables) yield
31+
// we don't need to create a new classloader because we compiled the expression class
32+
// in the same outputDir as main classes
33+
val classLoader = location.declaringType.classLoader
34+
val expressionClass = thread.loadClass(classLoader, expressionClassName)
35+
36+
val nameArray = thread.createArray(
37+
"java.lang.String",
38+
localVariables.map(v => thread.virtualMachine.mirrorOf(v.name))
39+
)
40+
val valueArray = thread.createArray("java.lang.Object", values.map(thread.boxIfPrimitive))
41+
val args = Seq(thisRef, nameArray, valueArray)
42+
43+
val exprRef = thread.newInstance(expressionClass, args)
44+
val outputValue =
45+
thread.invoke[ObjectReference](exprRef, "evaluate", "()Ljava/lang/Object;", Seq.empty)
46+
// TODO update mutated local variables
47+
thread.invoke[StringReference](outputValue, "toString", "()Ljava/lang/String;", Seq.empty)
48+
.value
49+
50+
/** compiles the expression and returns the new expression class name to load, or compiler errors */
51+
private def compile(
52+
expression: String,
53+
location: Location,
54+
localVariables: Seq[LocalVariable]
55+
): Either[String, String] =
56+
// We assume there is no 2 files with the same name
57+
val sourceFile = sources(location.sourceName)
58+
val packageName = getPackageName(location.declaringType)
59+
val outputClassName = getUniqueClassName()
60+
val errorBuilder = StringBuilder()
61+
val config = ExpressionCompilerConfig(
62+
packageName = packageName,
63+
outputClassName = outputClassName,
64+
breakpointLine = location.lineNumber,
65+
expression = expression,
66+
localVariables = localVariables.toSet.map(_.name).asJava,
67+
errorReporter = errorMsg => errorBuilder.append(errorMsg),
68+
testMode = true
69+
)
70+
val success = compiler.run(outputDir, classPath, options, sourceFile, config)
71+
val fullyQualifiedClassName =
72+
if packageName.isEmpty then outputClassName else s"$packageName.$outputClassName"
73+
if success then Right(fullyQualifiedClassName) else Left(errorBuilder.toString)
74+
end compile
75+
76+
private def getPackageName(tpe: ReferenceType): String =
77+
tpe.name.split('.').dropRight(1).mkString(".")
78+
79+
private def getUniqueClassName(): String =
80+
val id = uniqueID
81+
uniqueID += 1
82+
"Expression" + id
83+
84+
extension (thread: ThreadReference)
85+
private def boxIfPrimitive(value: Value): ObjectReference =
86+
value match
87+
case value: PrimitiveValue => box(value)
88+
case ref: ObjectReference => ref
89+
90+
private def box(value: PrimitiveValue): ObjectReference =
91+
val (className, sig) = value match
92+
case _: BooleanValue => ("java.lang.Boolean", "(Ljava/lang/String;)Ljava/lang/Boolean;")
93+
case _: ByteValue => ("java.lang.Byte", "(Ljava/lang/String;)Ljava/lang/Byte;")
94+
case _: CharValue => ("java.lang.Character", "(C)Ljava/lang/Character;")
95+
case _: DoubleValue => ("java.lang.Double", "(Ljava/lang/String;)Ljava/lang/Double;")
96+
case _: FloatValue => ("java.lang.Float", "(Ljava/lang/String;)Ljava/lang/Float;")
97+
case _: IntegerValue => ("java.lang.Integer", "(Ljava/lang/String;)Ljava/lang/Integer;")
98+
case _: LongValue => ("java.lang.Long", "(Ljava/lang/String;)Ljava/lang/Long;")
99+
case _: ShortValue => ("java.lang.Short", "(Ljava/lang/String;)Ljava/lang/Short;")
100+
val cls = getClass(className)
101+
val args = value match
102+
case c: CharValue => Seq(c)
103+
case value => Seq(mirrorOf(value.toString))
104+
invokeStatic(cls, "valueOf", sig, args)
105+
106+
private def createArray(arrayType: String, values: Seq[Value]): ArrayReference =
107+
val arrayClassObject = getClass(arrayType).classObject
108+
val reflectArrayClass = getClass("java.lang.reflect.Array")
109+
val args = Seq(arrayClassObject, mirrorOf(values.size))
110+
val sig = "(Ljava/lang/Class;I)Ljava/lang/Object;"
111+
val arrayRef = invokeStatic[ArrayReference](reflectArrayClass, "newInstance", sig, args)
112+
arrayRef.setValues(values.asJava)
113+
arrayRef
114+
115+
/** Get the remote class if it is already loaded. Otherwise you should use loadClass. */
116+
private def getClass(className: String): ClassType =
117+
thread.virtualMachine.classesByName(className).get(0).asInstanceOf[ClassType]
118+
119+
private def loadClass(classLoader: ClassLoaderReference, className: String): ClassType =
120+
// Calling classLoader.loadClass would create useless class object which throws
121+
// ClassNotPreparedException. We use java.lang.Class.forName instead.
122+
val classClass = getClass("java.lang.Class")
123+
val args = Seq(mirrorOf(className), mirrorOf(true), classLoader)
124+
val sig = "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;"
125+
invokeStatic[ClassObjectReference](classClass, "forName", sig, args)
126+
.reflectedType
127+
.asInstanceOf[ClassType]
128+
129+
private def invokeStatic[T <: Value](
130+
cls: ClassType,
131+
methodName: String,
132+
sig: String,
133+
args: Seq[Value]
134+
): T =
135+
val method = cls.methodsByName(methodName, sig).get(0)
136+
remotely:
137+
cls.invokeMethod(thread, method, args.asJava, ObjectReference.INVOKE_SINGLE_THREADED)
138+
139+
// we assume there is a single constructor, otherwise we need to add sig as parameter
140+
private def newInstance(cls: ClassType, args: Seq[Value]): ObjectReference =
141+
val constructor = cls.methodsByName("<init>").get(0)
142+
remotely:
143+
cls.newInstance(thread, constructor, args.asJava, ObjectReference.INVOKE_SINGLE_THREADED)
144+
145+
private def invoke[T <: Value](
146+
ref: ObjectReference,
147+
methodName: String,
148+
sig: String,
149+
args: Seq[Value]
150+
): T =
151+
val method = ref.referenceType.methodsByName(methodName, sig).get(0)
152+
remotely:
153+
ref.invokeMethod(thread, method, args.asJava, ObjectReference.INVOKE_SINGLE_THREADED)
154+
155+
/** wrapper for safe remote execution:
156+
* - it catches InvocationException to extract the message of the remote exception
157+
* - it disables GC on the returned
158+
*/
159+
private def remotely[T <: Value](value: => Value): T =
160+
val res =
161+
try value
162+
catch case invocationException: InvocationException =>
163+
val sig = "()Ljava/lang/String;"
164+
val message =
165+
invoke[StringReference](invocationException.exception, "toString", sig, List())
166+
throw new Exception(message.value, invocationException)
167+
// Prevent object created by the debugger to be garbage collected
168+
// In theory we should enable collection later to avoid memory leak
169+
res match
170+
case ref: ObjectReference => ref.disableCollection()
171+
case _ =>
172+
res.asInstanceOf[T]
173+
174+
private def mirrorOf(value: String): StringReference = thread.virtualMachine.mirrorOf(value)
175+
private def mirrorOf(value: Int): IntegerValue = thread.virtualMachine.mirrorOf(value)
176+
private def mirrorOf(value: Boolean): BooleanValue = thread.virtualMachine.mirrorOf(value)
177+
end extension
178+
end ExpressionEvaluator
179+
180+
object ExpressionEvaluator:
181+
def apply(
182+
sources: Array[JFile],
183+
flags: TestFlags,
184+
classPath: String,
185+
outputDir: JFile
186+
): ExpressionEvaluator =
187+
val sourceMap = sources.map(s => s.getName -> s.toPath).toMap
188+
val filteredOptions = flags.options.filterNot(_ == "-Ycheck:all")
189+
new ExpressionEvaluator(sourceMap, filteredOptions, classPath, outputDir.toPath)

0 commit comments

Comments
 (0)