Skip to content

Commit

Permalink
Merge pull request #648 from adpi2/unwrap-invocation-exception
Browse files Browse the repository at this point in the history
Unwrap invocation exception in runtime evaluation
  • Loading branch information
adpi2 committed Feb 8, 2024
2 parents f1d864f + 2dbcaf8 commit 49e962b
Show file tree
Hide file tree
Showing 13 changed files with 72 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ private[internal] class EvaluationProvider(
.invoke(methodName, methodSignature, wrappedArgs)
.recover {
// if invocation throws an exception, we return that exception as the result
case MethodInvocationFailed(msg, Some(exception)) => exception
case RuntimeException(msg, Some(exception)) => exception
}
.map(_.value)
}
Expand Down Expand Up @@ -170,7 +170,7 @@ private[internal] class EvaluationProvider(
}

private def evaluate(expression: PreparedExpression, frame: JdiFrame): Try[Value] = evaluationBlock {
expression match {
val result = expression match {
case logMessage: PlainLogMessage => MessageLogger.log(logMessage, frame)
case expr: RuntimeExpression => runtimeEvaluator.evaluate(expr, frame)
case expr: CompiledExpression =>
Expand All @@ -179,6 +179,8 @@ private[internal] class EvaluationProvider(
compiledExpression <- scalaEvaluator.evaluate(expr)
} yield compiledExpression
}
// if evaluation throws an exception, we return that exception as the result
result.recover { case RuntimeException(_, Some(exception)) => exception.value }
}

private def completeFuture[T](result: Try[T], thread: ThreadReference): CompletableFuture[T] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ class JdiArray(arrayRef: ArrayReference, thread: ThreadReference) extends JdiObj

def getValues: Seq[JdiValue] = arrayRef.getValues.asScala.toSeq.map(JdiValue(_, thread))

def getValue(i: Int): JdiValue = JdiValue(arrayRef.getValue(i), thread)
def getValue(i: Int): Safe[JdiValue] =
Safe(JdiValue(arrayRef.getValue(i), thread)).recoverWith { case e: IndexOutOfBoundsException =>
Safe.failed(RuntimeException(e.getMessage, None))
}
}

object JdiArray {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ private[internal] class JdiClass(
for {
_ <- prepareMethod(ctr)
instance <- Safe(cls.newInstance(thread, ctr, args.map(_.value).asJava, ObjectReference.INVOKE_SINGLE_THREADED))
.recoverWith(wrapInvocationException(thread))
.recoverWith(wrapInvocationException)
} yield JdiObject(instance, thread)

// Load the argument types of the method to avoid ClassNotLoadedException
Expand Down Expand Up @@ -64,7 +64,7 @@ private[internal] class JdiClass(
def invokeStatic(method: Method, args: Seq[JdiValue]): Safe[JdiValue] =
Safe(cls.invokeMethod(thread, method, args.map(_.value).asJava, ObjectReference.INVOKE_SINGLE_THREADED))
.map(JdiValue(_, thread))
.recoverWith(wrapInvocationException(thread))
.recoverWith(wrapInvocationException)
}

object JdiClass {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,9 @@ private[evaluator] class JdiObject(
val reference: ObjectReference,
thread: ThreadReference
) extends JdiValue(reference, thread) {
def getField(field: Field): JdiValue =
JdiValue(reference.getValue(field), thread)
def getField(field: Field): JdiValue = JdiValue(reference.getValue(field), thread)

def getField(name: String): JdiValue = {
val field = reference.referenceType.fieldByName(name)
JdiValue(reference.getValue(field), thread)
}
def getField(name: String): JdiValue = getField(reference.referenceType.fieldByName(name))

def invoke(methodName: String, args: Seq[JdiValue]): Safe[JdiValue] = {
val m = reference.referenceType.methodsByName(methodName).get(0)
Expand All @@ -29,24 +25,22 @@ private[evaluator] class JdiObject(
def classObject: JdiClass = JdiClass(reference.referenceType, thread)
def classLoader: JdiClassLoader = JdiClassLoader(reference.referenceType.classLoader, thread)

def invoke(method: Method, args: Seq[JdiValue]): Safe[JdiValue] = {
def invoke(method: Method, args: Seq[JdiValue]): Safe[JdiValue] =
Safe(reference.invokeMethod(thread, method, args.map(_.value).asJava, ObjectReference.INVOKE_SINGLE_THREADED))
.map(JdiValue(_, thread))
.recoverWith(wrapInvocationException(thread))
}
.recoverWith(wrapInvocationException)

protected def wrapInvocationException(thread: ThreadReference): PartialFunction[Throwable, Safe[Nothing]] = {
protected val wrapInvocationException: PartialFunction[Throwable, Safe[Nothing]] = {
case invocationException: InvocationException =>
for {
exception <- Safe(invocationException.exception).map(JdiObject(_, thread))
message <- exception.invoke("toString", List()).map(_.asString.stringValue).recover { case _ => "" }
} yield throw new MethodInvocationFailed(message, Some(exception))
} yield throw new RuntimeException(message, Some(exception))
}

// we use a Seq instead of a Map because the ScalaEvaluator rely on the order of the fields
def fields: Seq[(String, JdiValue)] =
reference.referenceType.fields.asScala.toSeq
.map(f => (f.name, JdiValue(reference.getValue(f), thread)))
reference.referenceType.fields.asScala.toSeq.map(f => (f.name, getField(f)))
}

private[internal] object JdiObject {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ class RuntimeEvaluation(frame: JdiFrame, logger: Logger) {
for {
array <- eval(tree.array)
index <- eval(tree.index).flatMap(_.unboxIfPrimitive).flatMap(_.toInt)
} yield array.asArray.getValue(index)
value <- array.asArray.getValue(index)
} yield value

private def evaluateIf(tree: If): Safe[JdiValue] =
for {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ch.epfl.scala.debugadapter.internal.evaluator

private[internal] case class RuntimeException(
message: String,
remoteException: Option[JdiObject]
) extends Exception(message) {
override def toString: String = s"RuntimeException: $message"
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,50 +132,44 @@ object RuntimePrimitiveOps {
fractional: Fractional[T]
): Safe[JdiValue] = {
import Fractional.Implicits._
val result: Option[AnyVal] = this match {
case Plus => Some(x + y)
case Minus => Some(x - y)
case Times => Some(x * y)
case Div if y != 0 => Some(x / y)
case Modulo if y != 0 =>
x match {
case d: Double => Some(d % y.asInstanceOf[Double])
case f: Float => Some(f % y.asInstanceOf[Float])
}
case Div | Modulo => None
case Less => Some(fractional.lt(x, y))
case LessOrEqual => Some(fractional.lteq(x, y))
case Greater => Some(fractional.gt(x, y))
case GreaterOrEqual => Some(fractional.gteq(x, y))
}

result match {
case Some(value) => Safe.successful(clsLoader.mirrorOfAnyVal(value))
case None => Safe.failed(MethodInvocationFailed("Division by zero", None))
}
Safe(this)
.map {
case Plus => x + y
case Minus => x - y
case Times => x * y
case Div => x / y
case Modulo =>
x match {
case d: Double => d % y.asInstanceOf[Double]
case f: Float => f % y.asInstanceOf[Float]
}
case Less => fractional.lt(x, y)
case LessOrEqual => fractional.lteq(x, y)
case Greater => fractional.gt(x, y)
case GreaterOrEqual => fractional.gteq(x, y)
}
.map(clsLoader.mirrorOfAnyVal)
.recoverWith { case e: ArithmeticException => Safe.failed(RuntimeException(e.getMessage, None)) }
}

private def computeIntegral[T <: AnyVal](x: T, y: T, clsLoader: JdiClassLoader)(implicit
integral: Integral[T]
): Safe[JdiValue] = {
import Integral.Implicits._
val result: Option[AnyVal] = this match {
case Plus => Some(x + y)
case Minus => Some(x - y)
case Times => Some(x * y)
case Div if y != 0 => Some(x / y)
case Modulo if y != 0 => Some(x % y)
case Div | Modulo => None
case Less => Some(integral.lt(x, y))
case LessOrEqual => Some(integral.lteq(x, y))
case Greater => Some(integral.gt(x, y))
case GreaterOrEqual => Some(integral.gteq(x, y))
}

result match {
case Some(value) => Safe.successful(clsLoader.mirrorOfAnyVal(value))
case None => Safe.failed(MethodInvocationFailed("Division by zero", None))
}
Safe(this)
.map {
case Plus => x + y
case Minus => x - y
case Times => x * y
case Div => x / y
case Modulo => x % y
case Less => integral.lt(x, y)
case LessOrEqual => integral.lteq(x, y)
case Greater => integral.gt(x, y)
case GreaterOrEqual => integral.gteq(x, y)
}
.map(clsLoader.mirrorOfAnyVal)
.recoverWith { case e: ArithmeticException => Safe.failed(RuntimeException(e.getMessage, None)) }
}

def evaluate(lhs: JdiValue, rhs: JdiValue, loader: JdiClassLoader) = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,8 @@ private[internal] class ScalaEvaluator(
evaluatedValue.getResult
}

private def evaluateExpression(expressionInstance: JdiObject): Safe[JdiValue] = {
expressionInstance
.invoke("evaluate", List())
.recover {
// if evaluation throws an exception, we return that exception as the result
case MethodInvocationFailed(msg, Some(exception)) => exception
}
}
private def evaluateExpression(expressionInstance: JdiObject): Safe[JdiValue] =
expressionInstance.invoke("evaluate", List())

/**
* In order to load the previously compiled Expression class, we need to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ final class ExpressionCompilerBridge {
) ++ options :+ sourceFile.toString

val command = new CompilerCommand(args, errorConsumer.accept(_))
val reporter = new StoreReporter() // cannot fix because of Scala 2.10
val reporter = new StoreReporter() // cannot fix because of Scala 2.12
val global = new ExpressionGlobal(
command.settings,
reporter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ class ExpressionContext(
val expressionTermName: TermName = termName(uniqueName.toLowerCase.toString)
val expressionClassName: TypeName = typeName(uniqueName)

var expressionSymbol: TermSymbol = _
var expressionSymbol: TermSymbol = null
// all classes and def in the chain of owners of the expression from local to global
// we store them to resolve the captured variables
var classOwners: Seq[ClassSymbol] = _
var classOwners: Seq[ClassSymbol] = null
var capturingMethod: Option[TermSymbol] = None

def store(exprSym: Symbol)(using Context): Unit =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import dotty.tools.dotc.transform.MacroTransform
import dotty.tools.dotc.core.Phases.*
import dotty.tools.dotc.report
import dotty.tools.dotc.util.SrcPos
import scala.annotation.nowarn

class ExtractExpression(using exprCtx: ExpressionContext) extends MacroTransform with DenotTransformer:
override def phaseName: String = ExtractExpression.name
Expand All @@ -39,7 +40,7 @@ class ExtractExpression(using exprCtx: ExpressionContext) extends MacroTransform

override protected def newTransformer(using Context): Transformer =
new Transformer:
var expressionTree: Tree = _
var expressionTree: Tree = null
override def transform(tree: Tree)(using Context): Tree =
tree match
case PackageDef(pid, stats) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ch.epfl.scala.debugadapter.internal
import ch.epfl.scala.debugadapter.testfmk.*
import ch.epfl.scala.debugadapter.ScalaVersion
import ch.epfl.scala.debugadapter.DebugConfig
import java.{util => ju}

class Scala212RuntimeEvaluatorTests extends RuntimeEvaluatorTests(ScalaVersion.`2.12`) {
test("Should access to wrapping 'object' methods") {
Expand Down Expand Up @@ -887,7 +888,7 @@ abstract class RuntimeEvaluatorTests(val scalaVersion: ScalaVersion) extends Deb
Evaluation.success("f1.foo_v2_apply.bar(42)", "hello 42"),
Evaluation.success("inner(42).x", 42),
Evaluation.success("list(0).toString()", "0"),
Evaluation.failed("list(1)", "java.lang.IndexOutOfBoundsException: 1")
Evaluation.success("list(1)", new IndexOutOfBoundsException("1"))
)
)
}
Expand Down Expand Up @@ -920,7 +921,7 @@ abstract class RuntimeEvaluatorTests(val scalaVersion: ScalaVersion) extends Deb
Evaluation.success("arr(by)", 3),
Evaluation.success("arr(new Integer(2))", 3),
Evaluation.success("arr(new Character('\u0000'))", 1),
Evaluation.failed("arr(3)", "java.lang.IndexOutOfBoundsException"),
Evaluation.failed("arr(3)", "Invalid array range: 3 to 3."),
Evaluation.success("test(arr)", "1,2,3"),
Evaluation.failed(
"test(Array(Test(1), Test(2), Test(3)))",
Expand All @@ -939,13 +940,12 @@ abstract class RuntimeEvaluatorTests(val scalaVersion: ScalaVersion) extends Deb
Evaluation.success("list(2).toString", "3"),
Evaluation.success("list(new Integer(2)).toString", "3"),
Evaluation.successOrIgnore("list(new Character('\u0000')).toString", "1", true),
Evaluation.failed("list(3)", "java.lang.IndexOutOfBoundsException"),
Evaluation.success("list(3)", new IndexOutOfBoundsException("3")),
Evaluation.success("map(1)", "one"),
Evaluation.success("map(2)", "two"),
Evaluation.success("map(new Integer(2))", "two"),
Evaluation.successOrIgnore("map(new Character('\u0000'))", "one", true),
// todo distinguish evaluation exception from MethodInvocationFailed
Evaluation.failed("map(3)", "key not found: 3"),
Evaluation.success("map(3)", new ju.NoSuchElementException("key not found: 3")),
Evaluation.success("set(1)", true),
Evaluation.success("set(2)", true),
Evaluation.success("set(new Integer(2))", true),
Expand All @@ -955,7 +955,7 @@ abstract class RuntimeEvaluatorTests(val scalaVersion: ScalaVersion) extends Deb
Evaluation.success("seq(2).toString", "3"),
Evaluation.success("seq(new Integer(2)).toString", "3"),
Evaluation.successOrIgnore("seq(new Character('\u0000')).toString", "1", true),
Evaluation.failed("seq(3)", "java.lang.IndexOutOfBoundsException: 3"),
Evaluation.success("seq(3)", new IndexOutOfBoundsException("3")),
Evaluation.success("vector(0).toString", "1"),
Evaluation.success("vector(2).toString", "3"),
Evaluation.success("vector(new Integer(2)).toString", "3"),
Expand Down

0 comments on commit 49e962b

Please sign in to comment.