Skip to content

Commit

Permalink
allow assignments in runtime evaluator
Browse files Browse the repository at this point in the history
  • Loading branch information
iusildra committed Jul 31, 2023
1 parent afa80fb commit cc7afbe
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class RuntimeDefaultEvaluator(val frame: JdiFrame, implicit val logger: Logger)
case array: ArrayElemTree => evaluateArrayElement(array)
case branching: IfTree => evaluateIf(branching)
case staticMethod: StaticMethodTree => invokeStatic(staticMethod)
case assign: AssignTree => evaluateAssign(assign)
case UnitTree => Safe(JdiValue(frame.thread.virtualMachine.mirrorOfVoid, frame.thread))
}

Expand Down Expand Up @@ -118,6 +119,26 @@ class RuntimeDefaultEvaluator(val frame: JdiFrame, implicit val logger: Logger)
predicate <- eval(tree.p).flatMap(_.unboxIfPrimitive).flatMap(_.toBoolean)
value <- if (predicate) eval(tree.thenp) else eval(tree.elsep)
} yield value

/* -------------------------------------------------------------------------- */
/* Assign evaluation */
/* -------------------------------------------------------------------------- */
def evaluateAssign(tree: AssignTree): Safe[JdiValue] = {
eval(tree.rhs)
.flatMap { rhsValue =>
tree.lhs match {
case field: InstanceFieldTree =>
eval(field.qual).flatMap { qualValue =>
Safe(qualValue.asObject.reference.setValue(field.field, rhsValue.value))
}
case field: StaticFieldTree => Safe(field.on.setValue(field.field, rhsValue.value))
case localVar: LocalVarTree =>
val localVarRef = frame.variableByName(localVar.name)
Safe(localVarRef.map(frame.setVariable(_, rhsValue)).get)
}
}
.map(_ => JdiValue(frame.thread.virtualMachine.mirrorOfVoid, frame.thread))
}
}

object RuntimeDefaultEvaluator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class RuntimeDefaultValidator(val frame: JdiFrame, val sourceLookUp: SourceLookU
case branch: Term.If => validateIf(branch)
case instance: Term.New => validateNew(instance)
case block: Term.Block => validateBlock(block)
case assign: Term.Assign => validateAssign(assign)
case _ => Recoverable("Expression not supported at runtime")
}

Expand Down Expand Up @@ -205,13 +206,6 @@ class RuntimeDefaultValidator(val frame: JdiFrame, val sourceLookUp: SourceLookU
result <- validateApply(intermediate, args)
} yield result

/*
* validateIndirectApply MUST be called before methodTreeByNameAndArgs
* That's because at runtime, a inner module is accessible as a 0-arg method,
* and if its associated class has not attribute either, when calling the
* constructor of the class, methodTreeByNameAndArgs would return its companion
* object instead. Look at the test about multiple layers for an example
*/
def findMethod(
tree: RuntimeTree,
name: String,
Expand Down Expand Up @@ -280,15 +274,14 @@ class RuntimeDefaultValidator(val frame: JdiFrame, val sourceLookUp: SourceLookU
}

/* -------------------------------------------------------------------------- */
/* Looking for $outer */
/* Outer validation */
/* -------------------------------------------------------------------------- */
def validateOuter(tree: RuntimeTree): Validation[RuntimeEvaluableTree] =
outerLookup(tree)

/* -------------------------------------------------------------------------- */
/* Flow control validation */
/* -------------------------------------------------------------------------- */

def validateIf(tree: Term.If): Validation[RuntimeEvaluableTree] = {
lazy val objType = loadClass("java.lang.Object").get
for {
Expand All @@ -304,6 +297,39 @@ class RuntimeDefaultValidator(val frame: JdiFrame, val sourceLookUp: SourceLookU
)
} yield ifTree
}

/* -------------------------------------------------------------------------- */
/* Assign validation */
/* -------------------------------------------------------------------------- */
def isMutable(tree: RuntimeEvaluableTree): Boolean =
tree match {
case field: FieldTree => !field.immutable
case localVar: LocalVarTree => true
case _ => false
}

def validateAssign(
tree: Term.Assign,
localVarValidation: String => Validation[RuntimeEvaluableTree] = localVarTreeByName,
fieldValidation: (Validation[RuntimeTree], String) => Validation[RuntimeEvaluableTree] = fieldTreeByName
): Validation[RuntimeEvaluableTree] = {
val lhs = tree.lhs match {
case select: Term.Select =>
fieldValidation(validateWithClass(select.qual), select.name.value)
case name: Term.Name =>
localVarValidation(name.value).orElse(fieldValidation(thisTree, name.value))
case _ => Recoverable("Unsupported assignment")
}

for {
lhsValue <- lhs
if isMutable(lhsValue)
rhs <- validate(tree.rhs)
if isAssignableFrom(rhs.`type`, lhsValue.`type`)
unit = frame.thread.virtualMachine().mirrorOfVoid().`type`()
assign <- AssignTree(lhsValue, rhs, unit)
} yield assign
}
}

object RuntimeDefaultValidator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ protected[internal] object RuntimeEvaluatorExtractors {
case mt: NestedModuleTree => unapply(mt.init.qual)
case ft: InstanceFieldTree => unapply(ft.qual)
case IfTree(p, t, f, _) => unapply(p) || unapply(t) || unapply(f)
case AssignTree(lhs, rhs, _) => unapply(lhs) || unapply(rhs)
case _: MethodTree | _: NewInstanceTree => true
case _: LiteralTree | _: LocalVarTree | _: PreEvaluatedTree | _: ThisTree | UnitTree => false
case _: StaticFieldTree | _: ClassTree | _: TopLevelModuleTree => false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ class RuntimePreEvaluationValidator(
}
case tree => tree
}

// ? We mustn't pre-evaluate the lhs of an assignment, because we need its 'declaration' and not its value
override def validateAssign(
tree: Term.Assign,
localVarValidation: String => Validation[RuntimeEvaluableTree] = localVarTreeByName,
fieldValidation: (Validation[RuntimeTree], String) => Validation[RuntimeEvaluableTree] = fieldTreeByName
): Validation[RuntimeEvaluableTree] =
super.validateAssign(tree, super.localVarTreeByName, super.fieldTreeByName)
}

object RuntimePreEvaluationValidator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ sealed trait MethodTree extends RuntimeEvaluableTree {
def args: Seq[RuntimeEvaluableTree]
}

sealed trait FieldTree extends RuntimeEvaluableTree {
sealed trait FieldTree extends AssignableTree {
def field: Field
def immutable: Boolean = field.isFinal
}

sealed trait AssignableTree extends RuntimeEvaluableTree

/* -------------------------------------------------------------------------- */
/* Simple trees */
/* -------------------------------------------------------------------------- */
Expand Down Expand Up @@ -61,7 +64,7 @@ object LiteralTree {
case class LocalVarTree(
name: String,
`type`: Type
) extends RuntimeEvaluableTree {
) extends AssignableTree {
override def prettyPrint(depth: Int): String = {
val indent = "\t" * (depth + 1)
s"""|LocalVarTree(
Expand Down Expand Up @@ -345,3 +348,29 @@ object IfTree {
}
}
}

/* -------------------------------------------------------------------------- */
/* Assign tree */
/* -------------------------------------------------------------------------- */
case class AssignTree(
lhs: AssignableTree,
rhs: RuntimeEvaluableTree,
`type`: Type
) extends RuntimeEvaluableTree {
override def prettyPrint(depth: Int): String = {
val indent = "\t" * (depth + 1)
s"""|AssignTree(
|${indent}lhs= ${lhs.prettyPrint(depth + 1)},
|${indent}rhs= ${rhs.prettyPrint(depth + 1)},
|${indent}t= ${`type`}
|${indent.dropRight(1)})""".stripMargin
}
}

object AssignTree {
def apply(lhs: RuntimeEvaluableTree, rhs: RuntimeEvaluableTree, tpe: Type): Validation[AssignTree] =
lhs match {
case lhs: AssignableTree => Valid(AssignTree(lhs, rhs, tpe))
case _ => CompilerRecoverable("Left hand side of an assignment must be assignable")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,56 @@ abstract class RuntimeEvaluatorTests(val scalaVersion: ScalaVersion) extends Deb
Evaluation.success("bar.Baz(42).Bizz.x", 43)
)
}

test("should assign values to local var and fields") {
val source =
"""|package example
|
|class A {
| var a = 0
| var l = new B
| def f = this
|}
|
|class B
|class C extends B {
| var c = 41
|}
|
|object Main {
| var b = 0
| var c = 0
| def main(args: Array[String]): Unit = {
| val a = new A
| var b = 0
| var bc: B = new C
| println("ok")
| }
|}
|""".stripMargin
implicit val debuggee: TestingDebuggee = TestingDebuggee.mainClass(source, "example.Main", scalaVersion)
def assignSuccess(expr: String) = Evaluation.success(expr)(res => res == "<void value>")
check(
Breakpoint(21),
assignSuccess("a.a = 42"),
Evaluation.success("a.a", 42),
assignSuccess("a.f.a = 64"),
Evaluation.success("a.f.a", 64),
assignSuccess("b = 42"),
Evaluation.success("Main.b", 0),
Evaluation.successOrIgnore("b", 42, true), // ? works in a debug session but not in tests ???
assignSuccess("c = 42"),
Evaluation.success("c", 42),
assignSuccess("a.l = new C"),
Evaluation.success("a.l", ObjectRef("C")),
assignSuccess("bc.c = 42"),
Evaluation.success("bc.c", 42),
Evaluation.failed("a.a = \"str\""),
Evaluation.failed("a.f.a = \"str\""),
Evaluation.failed("b = \"str\""),
Evaluation.failed("Main.b = \"str\"")
)
}
}

/* -------------------------------------------------------------------------- */
Expand Down

0 comments on commit cc7afbe

Please sign in to comment.