Skip to content

Commit 6c7c9b4

Browse files
committed
Fix Scala 3 evaluation in value class
1 parent ceafa7c commit 6c7c9b4

File tree

9 files changed

+45
-72
lines changed

9 files changed

+45
-72
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package ch.epfl.scala.debugadapter.internal.evaluator
2+
3+
object JdiNull extends JdiObject(null, null)

modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/ScalaEvaluator.scala

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ private[internal] class ScalaEvaluator(
5353
(names, values) <- extractValuesAndNames(classLoader)
5454
namesArray <- classLoader.createArray("java.lang.String", names)
5555
valuesArray <- classLoader.createArray("java.lang.Object", values)
56-
args = List(namesArray, valuesArray)
56+
thisObject = frame.thisObject.getOrElse(JdiNull)
57+
args = List(thisObject, namesArray, valuesArray)
5758
expressionInstance <- createExpressionInstance(classLoader, classDir, className, args)
5859
evaluatedValue <- evaluateExpression(expressionInstance)
5960
_ <- updateVariables(valuesArray)
@@ -94,10 +95,7 @@ private[internal] class ScalaEvaluator(
9495
private def extractValuesAndNames(classLoader: JdiClassLoader): Safe[(Seq[JdiString], Seq[JdiValue])] = {
9596
def extractVariablesFromFrame(): Safe[(Seq[JdiString], Seq[JdiValue])] = {
9697
val localVariables = frame.variablesAndValues().map { case (variable, value) => (variable.name, value) }
97-
// Exclude the this object if there already is a local $this variable
98-
// The Scala compiler uses `$this` in the extension methods of AnyVal classes
99-
val thisObject = frame.thisObject.filter(_ => !localVariables.contains("$this")).map("$this".->)
100-
(localVariables ++ thisObject)
98+
localVariables
10199
.map { case (name, value) =>
102100
for {
103101
name <- classLoader.mirrorOf(name)
@@ -109,26 +107,22 @@ private[internal] class ScalaEvaluator(
109107
.map(xs => (xs.map(_._1), xs.map(_._2)))
110108
}
111109
// Only useful in Scala 2
112-
def extractFields(thisObject: JdiObject): Safe[(Seq[JdiString], Seq[JdiValue])] = {
113-
val fields = thisObject.fields
114-
val names = fields.map(_._1).map(classLoader.mirrorOf).traverse
115-
val values = fields.map(_._2).map(value => classLoader.boxIfPrimitive(value)).traverse
110+
def extractThisAndFields(thisObject: JdiObject): Safe[(Seq[JdiString], Seq[JdiValue])] = {
111+
val thisAndFieds = ("$this", thisObject) +: thisObject.fields
112+
val names = thisAndFieds.map(_._1).map(classLoader.mirrorOf).traverse
113+
val values = thisAndFieds.map(_._2).map(value => classLoader.boxIfPrimitive(value)).traverse
116114
Safe.join(names, values)
117115
}
118116

119117
for {
120118
(variableNames, variableValues) <- extractVariablesFromFrame()
121-
// Currently we only need to load the fields for the Scala 2
122-
// expression evaluator.
123-
// It is dangerous because local values can shadow fields
124-
// TODO: adapt Scala 2 expression compiler
119+
// Currently we need to load the this object and the fields for the Scala 2 expression evaluator.
120+
// It is dangerous because local values can shadow them
121+
// TODO: rewrite Scala 2 expression compiler based on Scala 3
125122
(fieldNames, fieldValues) <- frame.thisObject
126123
.filter(_ => compiler.scalaVersion.isScala2)
127-
.map(extractFields)
124+
.map(extractThisAndFields)
128125
.getOrElse(Safe((Nil, Nil)))
129-
// If `this` is a value class then the breakpoint must be in
130-
// a generated method of its companion object which takes
131-
// the erased value`$this` as argument.
132126
} yield {
133127
val names = variableNames ++ fieldNames
134128
val values = variableValues ++ fieldValues

modules/expression-compiler/src/main/scala-2/scala/tools/nsc/InsertExpression.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class InsertExpression(override val global: ExpressionGlobal) extends Transform
2323
*/
2424
private class Inserter extends Transformer {
2525
private val expressionClassSource =
26-
s"""class $expressionClassName(names: Array[String], values: Array[Object]) {
26+
s"""class $expressionClassName(thisObject: Any, names: Array[String], values: Array[Object]) {
2727
| val valuesByName = names.reverse.map(_.asInstanceOf[String]).zip(values.reverse).toMap
2828
|
2929
| def evaluate() = {

modules/expression-compiler/src/main/scala-3.3/dotty/tools/dotc/ExpressionReporter.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ package dotty.tools.dotc
33
import dotty.tools.dotc.core.Contexts.*
44
import dotty.tools.dotc.reporting.AbstractReporter
55
import dotty.tools.dotc.reporting.Diagnostic
6+
import scala.util.matching.Regex
67

78
class ExpressionReporter(reportError: String => Unit) extends AbstractReporter:
89
override def doReport(dia: Diagnostic)(using Context): Unit =
9-
// println(messageAndPos(dia))
10+
// When printing trees, we trim what comes after 'def evaluate'
11+
// val message = messageAndPos(dia)
12+
// val shortMessage = message.split(Regex.quote("\u001b[33mdef\u001b[0m \u001b[36mgetLocalValue\u001b[0m"))(0)
13+
// println(shortMessage)
1014
dia match
1115
case error: Diagnostic.Error =>
1216
val newPos = error.pos.source.positionInUltimateSource(error.pos)

modules/expression-compiler/src/main/scala-3/dotty/tools/dotc/ExpressionCompilerBridge.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class ExpressionCompilerBridge:
2929
classPath,
3030
"-Yskip:pureStats"
3131
// Debugging: Print the tree after phases of the debugger
32-
// "-Vprint:typer,extract-expression,resolve-reflect-eval",
32+
// "-Vprint:elimByName,extract-expression,resolve-reflect-eval",
3333
) ++ options :+ sourceFile.toString
3434
val exprCtx =
3535
ExpressionContext(expressionClassName, line, expression, localVariables.asScala.toSet, pckg, testMode)

modules/expression-compiler/src/main/scala-3/dotty/tools/dotc/evaluation/InsertExpression.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,15 @@ class InsertExpression(using exprCtx: ExpressionContext) extends Phase:
3030
override def isCheckable: Boolean = false
3131

3232
private val evaluationClassSource =
33-
s"""|class ${exprCtx.expressionClassName}(names: Array[String], values: Array[Any]):
33+
s"""|class ${exprCtx.expressionClassName}(thisObject: Any, names: Array[String], values: Array[Any]):
3434
| import java.lang.reflect.InvocationTargetException
3535
| val classLoader = getClass.getClassLoader.nn
3636
|
3737
| def evaluate(): Any =
3838
| ()
3939
|
40+
| def getThisObject(): Any = thisObject
41+
|
4042
| def getLocalValue(name: String): Any =
4143
| val idx = names.indexOf(name)
4244
| if idx == -1 then throw new NoSuchElementException(name)

modules/expression-compiler/src/main/scala-3/dotty/tools/dotc/evaluation/ResolveReflectEval.scala

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,9 @@ class ResolveReflectEval(using exprCtx: ExpressionContext) extends MiniPhase:
4242
val args = argsTree.asInstanceOf[JavaSeqLiteral].elems
4343
val gen = new Gen(reflectEval)
4444
tree.attachment(EvaluationStrategy) match
45-
case EvaluationStrategy.This(cls) =>
46-
gen.getLocalValue("$this")
47-
case EvaluationStrategy.LocalOuter(cls) =>
48-
gen.getLocalValue("$outer")
49-
case EvaluationStrategy.Outer(outerCls) =>
50-
gen.getOuter(qualifier, outerCls)
45+
case EvaluationStrategy.This(cls) => gen.getThisObject
46+
case EvaluationStrategy.LocalOuter(cls) => gen.getLocalValue("$outer")
47+
case EvaluationStrategy.Outer(outerCls) => gen.getOuter(qualifier, outerCls)
5148
case EvaluationStrategy.LocalValue(variable, isByName) =>
5249
val variableName = JavaEncoding.encode(variable.name)
5350
val rawLocalValue = gen.getLocalValue(variableName)
@@ -109,8 +106,7 @@ class ResolveReflectEval(using exprCtx: ExpressionContext) extends MiniPhase:
109106
val typeSymbol = variable.info.typeSymbol
110107
val elemField = typeSymbol.info.decl(termName("elem")).symbol
111108
gen.setField(capture, elemField.asTerm, value)
112-
case EvaluationStrategy.StaticObject(obj) =>
113-
gen.getStaticObject(obj)
109+
case EvaluationStrategy.StaticObject(obj) => gen.getStaticObject(obj)
114110
case EvaluationStrategy.Field(field, isByName) =>
115111
// if the field is lazy, if it is private in a value class or a trait
116112
// then we must call the getter method
@@ -125,15 +121,12 @@ class ResolveReflectEval(using exprCtx: ExpressionContext) extends MiniPhase:
125121
val arg = gen.unboxIfValueClass(field, args.head)
126122
if field.owner.is(Trait) then gen.callMethod(qualifier, field.setter.asTerm, List(arg))
127123
else gen.setField(qualifier, field, arg)
128-
case EvaluationStrategy.MethodCall(method) =>
129-
gen.callMethod(qualifier, method, args)
130-
case EvaluationStrategy.ConstructorCall(ctr, cls) =>
131-
gen.callConstructor(qualifier, ctr, args)
124+
case EvaluationStrategy.MethodCall(method) => gen.callMethod(qualifier, method, args)
125+
case EvaluationStrategy.ConstructorCall(ctr, cls) => gen.callConstructor(qualifier, ctr, args)
132126
case _ => super.transform(tree)
133127

134128
private def isReflectEval(symbol: Symbol)(using Context): Boolean =
135-
symbol.name == termName("reflectEval") &&
136-
symbol.owner == exprCtx.expressionClass
129+
symbol.name == termName("reflectEval") && symbol.owner == exprCtx.expressionClass
137130

138131
class Gen(reflectEval: Apply)(using Context):
139132
private val expressionThis = reflectEval.fun.asInstanceOf[Select].qualifier
@@ -172,6 +165,9 @@ class ResolveReflectEval(using exprCtx: ExpressionContext) extends MiniPhase:
172165
val unboxMethod = ValueClasses.valueClassUnbox(cls).asTerm
173166
callMethod(tree, unboxMethod, Nil)
174167

168+
def getThisObject: Tree =
169+
Apply(Select(expressionThis, termName("getThisObject")), List.empty)
170+
175171
def getLocalValue(name: String): Tree =
176172
Apply(
177173
Select(expressionThis, termName("getLocalValue")),
@@ -323,21 +319,19 @@ class ResolveReflectEval(using exprCtx: ExpressionContext) extends MiniPhase:
323319

324320
private def capturedValue(sym: Symbol, originalName: TermName): Option[Tree] =
325321
val encodedName = JavaEncoding.encode(originalName)
326-
if exprCtx.classOwners.contains(sym)
327-
then capturedByClass(sym.asClass, originalName)
322+
if exprCtx.classOwners.contains(sym) then capturedByClass(sym.asClass, originalName)
323+
else if exprCtx.localVariables.contains(encodedName) then Some(getLocalValue(encodedName))
328324
else
329-
// if the captured value is not a local variables
330-
// then it must have been captured by the outer method
331-
if exprCtx.localVariables.contains(encodedName)
332-
then Some(getLocalValue(encodedName))
333-
else exprCtx.capturingMethod.flatMap(getMethodCapture(_, originalName))
325+
// if the captured value is not a local variables
326+
// then it must have been captured by the outer method
327+
exprCtx.capturingMethod.flatMap(getMethodCapture(_, originalName))
334328

335329
private def capturedByClass(cls: ClassSymbol, originalName: TermName): Option[Tree] =
336330
val target = exprCtx.classOwners.indexOf(cls)
337331
val qualifier = exprCtx.classOwners
338332
.drop(1)
339333
.take(target)
340-
.foldLeft(getLocalValue("$this"))((q, cls) => getOuter(q, cls))
334+
.foldLeft(getThisObject)((q, cls) => getOuter(q, cls))
341335
getClassCapture(qualifier, originalName, cls)
342336

343337
object ResolveReflectEval:

modules/expression-compiler/src/test/scala-3.3/ch/epfl/scala/debugadapter/ExpressionCompilerDebug.scala

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,30 +34,6 @@ class ExpressionCompilerDebug extends munit.FunSuite:
3434
evaluate(6, "msg", localVariables = Set("msg$1"))
3535
}
3636

37-
test("local method in value class".only) {
38-
val source =
39-
"""|package example
40-
|
41-
|class A(val self: String) extends AnyVal {
42-
| def m(size: Int): String = {
43-
| def m(mul: Int): String = {
44-
| self.take(size) * mul
45-
| }
46-
| m(2)
47-
| }
48-
|}
49-
|
50-
|object Main {
51-
| def main(args: Array[String]): Unit = {
52-
| val a = new A("foo")
53-
| println(a.m(2))
54-
| }
55-
|}
56-
|""".stripMargin
57-
implicit val debuggee: TestingDebuggee = TestingDebuggee.mainClass(source, "example.Main", scalaVersion)
58-
evaluate(8, "this.m(2)", localVariables = Set("$this"))
59-
}
60-
6137
private def evaluate(line: Int, expression: String, localVariables: Set[String] = Set.empty)(using
6238
debuggee: TestingDebuggee
6339
): Unit =

modules/tests/src/test/scala/ch/epfl/scala/debugadapter/ScalaEvaluationTests.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ abstract class ScalaEvaluationTests(scalaVersion: ScalaVersion) extends DebugTes
551551
Breakpoint(11),
552552
DebugStepAssert.inParallel(
553553
// B captures the local value x1
554-
Evaluation.successOrIgnore("new B", isScala2)(result => assert(result.startsWith("A$B$1@"))),
554+
Evaluation.successOrIgnore("new B", ObjectRef("A$B$1"), isScala2),
555555
// x1 is captured by B
556556
Evaluation.successOrIgnore("x1", "x1", isScala2),
557557
Evaluation.success("x2", "x2"),
@@ -1363,10 +1363,10 @@ abstract class ScalaEvaluationTests(scalaVersion: ScalaVersion) extends DebugTes
13631363
),
13641364
Breakpoint(6),
13651365
DebugStepAssert.inParallel(
1366-
Evaluation.success("self", "foo"),
1367-
Evaluation.success("size", 2),
1368-
// Evaluation.success("m(1)", "fo"),
1369-
Evaluation.success("this.m(1)", "ff")
1366+
Evaluation.successOrIgnore("self", "foo", isScala2),
1367+
Evaluation.successOrIgnore("size", 2, isScala2),
1368+
Evaluation.successOrIgnore("m(1)", "fo", isScala2),
1369+
Evaluation.successOrIgnore("this.m(1)", "ff", isScala2)
13701370
)
13711371
)
13721372
}

0 commit comments

Comments
 (0)