Skip to content

Commit

Permalink
Handle macro annotation suspends and crashes
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolasstucki committed Dec 13, 2022
1 parent 01a5dd4 commit 371ffef
Show file tree
Hide file tree
Showing 13 changed files with 119 additions and 34 deletions.
60 changes: 29 additions & 31 deletions compiler/src/dotty/tools/dotc/quoted/Interpreter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,8 @@ class Interpreter(pos: SrcPos, classLoader: ClassLoader)(using Context):
(inst, inst.getClass)
}
catch
case MissingClassDefinedInCurrentRun(sym) if ctx.compilationUnit.isSuspendable =>
if (ctx.settings.XprintSuspension.value)
report.echo(i"suspension triggered by a dependency on $sym", pos)
ctx.compilationUnit.suspend() // this throws a SuspendException
case MissingClassDefinedInCurrentRun(sym) =>
suspendOnMissing(sym, pos)

val name = fn.name.asTermName
val method = getMethod(clazz, name, paramsSig(fn))
Expand Down Expand Up @@ -214,23 +212,19 @@ class Interpreter(pos: SrcPos, classLoader: ClassLoader)(using Context):

private def loadClass(name: String): Class[?] =
try classLoader.loadClass(name)
catch {
case MissingClassDefinedInCurrentRun(sym) if ctx.compilationUnit.isSuspendable =>
if (ctx.settings.XprintSuspension.value)
report.echo(i"suspension triggered by a dependency on $sym", pos)
ctx.compilationUnit.suspend() // this throws a SuspendException
}
catch
case MissingClassDefinedInCurrentRun(sym) =>
suspendOnMissing(sym, pos)


private def getMethod(clazz: Class[?], name: Name, paramClasses: List[Class[?]]): JLRMethod =
try clazz.getMethod(name.toString, paramClasses: _*)
catch {
case _: NoSuchMethodException =>
val msg = em"Could not find method ${clazz.getCanonicalName}.$name with parameters ($paramClasses%, %)"
throw new StopInterpretation(msg, pos)
case MissingClassDefinedInCurrentRun(sym) if ctx.compilationUnit.isSuspendable =>
if (ctx.settings.XprintSuspension.value)
report.echo(i"suspension triggered by a dependency on $sym", pos)
ctx.compilationUnit.suspend() // this throws a SuspendException
case MissingClassDefinedInCurrentRun(sym) =>
suspendOnMissing(sym, pos)
}

private def stopIfRuntimeException[T](thunk: => T, method: JLRMethod): T =
Expand All @@ -248,10 +242,8 @@ class Interpreter(pos: SrcPos, classLoader: ClassLoader)(using Context):
ex.getTargetException match {
case ex: scala.quoted.runtime.StopMacroExpansion =>
throw ex
case MissingClassDefinedInCurrentRun(sym) if ctx.compilationUnit.isSuspendable =>
if (ctx.settings.XprintSuspension.value)
report.echo(i"suspension triggered by a dependency on $sym", pos)
ctx.compilationUnit.suspend() // this throws a SuspendException
case MissingClassDefinedInCurrentRun(sym) =>
suspendOnMissing(sym, pos)
case targetException =>
val sw = new StringWriter()
sw.write("Exception occurred while executing macro expansion.\n")
Expand All @@ -268,19 +260,6 @@ class Interpreter(pos: SrcPos, classLoader: ClassLoader)(using Context):
}
}

private object MissingClassDefinedInCurrentRun {
def unapply(targetException: Throwable)(using Context): Option[Symbol] = {
targetException match
case _: NoClassDefFoundError | _: ClassNotFoundException =>
val className = targetException.getMessage
if className eq null then None
else
val sym = staticRef(className.toTypeName).symbol
if (sym.isDefinedInCurrentRun) Some(sym) else None
case _ => None
}
}

/** List of classes of the parameters of the signature of `sym` */
private def paramsSig(sym: Symbol): List[Class[?]] = {
def paramClass(param: Type): Class[?] = {
Expand Down Expand Up @@ -364,3 +343,22 @@ object Interpreter:
}
}
end Call

object MissingClassDefinedInCurrentRun {
def unapply(targetException: Throwable)(using Context): Option[Symbol] = {
if !ctx.compilationUnit.isSuspendable then None
else targetException match
case _: NoClassDefFoundError | _: ClassNotFoundException =>
val className = targetException.getMessage
if className eq null then None
else
val sym = staticRef(className.toTypeName).symbol
if (sym.isDefinedInCurrentRun) Some(sym) else None
case _ => None
}
}

def suspendOnMissing(sym: Symbol, pos: SrcPos)(using Context): Nothing =
if ctx.settings.XprintSuspension.value then
report.echo(i"suspension triggered by a dependency on $sym", pos)
ctx.compilationUnit.suspend() // this throws a SuspendException
30 changes: 29 additions & 1 deletion compiler/src/dotty/tools/dotc/transform/MacroAnnotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ import dotty.tools.dotc.core.DenotTransformers.DenotTransformer
import dotty.tools.dotc.core.Flags.*
import dotty.tools.dotc.core.MacroClassLoader
import dotty.tools.dotc.core.Symbols.*
import dotty.tools.dotc.core.Types._
import dotty.tools.dotc.quoted.*
import dotty.tools.dotc.util.SrcPos
import scala.quoted.runtime.impl.{QuotesImpl, SpliceScope}

import scala.quoted.Quotes
import scala.util.control.NonFatal

import java.lang.reflect.InvocationTargetException

class MacroAnnotations(thisPhase: DenotTransformer):
import tpd.*
Expand Down Expand Up @@ -53,7 +57,31 @@ class MacroAnnotations(thisPhase: DenotTransformer):
debug.println(i"Expanding macro annotation: ${annot}")

// Interpret call to `new myAnnot(..).transform(using <Quotes>)(<tree>)`
val transformedTrees = callMacro(macroInterpreter, tree, annot)
val transformedTrees =
try callMacro(macroInterpreter, tree, annot)
catch
// TODO: Replace this case when scala.annaotaion.MacroAnnotation is no longer experimental and reflectiveSelectable is not used
// Replace this case with the nested cases.
case ex0: InvocationTargetException =>
ex0.getCause match
case ex: scala.quoted.runtime.StopMacroExpansion =>
if !ctx.reporter.hasErrors then
report.error("Macro expansion was aborted by the macro without any errors reported. Macros should issue errors to end-users when aborting a macro expansion with StopMacroExpansion.", annot.tree)
List(tree)
case Interpreter.MissingClassDefinedInCurrentRun(sym) =>
Interpreter.suspendOnMissing(sym, annot.tree)
case NonFatal(ex) =>
val stack0 = ex.getStackTrace.takeWhile(_.getClassName != "dotty.tools.dotc.transform.MacroAnnotations")
val stack = stack0.take(1 + stack0.lastIndexWhere(_.getMethodName == "transform"))
val msg =
em"""Failed to evaluate macro.
| Caused by ${ex.getClass}: ${if (ex.getMessage == null) "" else ex.getMessage}
| ${stack.mkString("\n ")}
|"""
report.error(msg, annot.tree)
List(tree)
case _ =>
throw ex0
transformedTrees.span(_.symbol != tree.symbol) match
case (prefixed, newTree :: suffixed) =>
allTrees ++= prefixed
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/transform/Splicer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ object Splicer {
throw ex
case ex: scala.quoted.runtime.StopMacroExpansion =>
if !ctx.reporter.hasErrors then
report.error("Macro expansion was aborted by the macro without any errors reported. Macros should issue errors to end-users to facilitate debugging when aborting a macro expansion.", splicePos)
report.error("Macro expansion was aborted by the macro without any errors reported. Macros should issue errors to end-users when aborting a macro expansion with StopMacroExpansion.", splicePos)
// errors have been emitted
EmptyTree
case ex: StopInterpretation =>
Expand Down
8 changes: 8 additions & 0 deletions tests/neg-macros/annot-crash.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

-- Error: tests/neg-macros/annot-crash/Test_2.scala:1:0 ----------------------------------------------------------------
1 |@crash // error
|^^^^^^
|Failed to evaluate macro.
| Caused by class scala.NotImplementedError: an implementation is missing
| scala.Predef$.$qmark$qmark$qmark(Predef.scala:344)
| crash.transform(Macro_1.scala:7)
8 changes: 8 additions & 0 deletions tests/neg-macros/annot-crash/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import scala.annotation.{experimental, MacroAnnotation}
import scala.quoted._

@experimental
class crash extends MacroAnnotation {
def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
???
}
2 changes: 2 additions & 0 deletions tests/neg-macros/annot-crash/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@crash // error
def test = ()
5 changes: 5 additions & 0 deletions tests/neg-macros/annot-ill-abort.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

-- Error: tests/neg-macros/annot-ill-abort/Test_2.scala:1:0 ------------------------------------------------------------
1 |@crash // error
|^^^^^^
|Macro expansion was aborted by the macro without any errors reported. Macros should issue errors to end-users when aborting a macro expansion with StopMacroExpansion.
8 changes: 8 additions & 0 deletions tests/neg-macros/annot-ill-abort/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import scala.annotation.{experimental, MacroAnnotation}
import scala.quoted._

@experimental
class crash extends MacroAnnotation {
def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
throw new scala.quoted.runtime.StopMacroExpansion
}
2 changes: 2 additions & 0 deletions tests/neg-macros/annot-ill-abort/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@crash // error
def test = ()
12 changes: 12 additions & 0 deletions tests/neg-macros/annot-suspend-cycle.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- [E129] Potential Issue Warning: tests/neg-macros/annot-suspend-cycle/Macro.scala:7:4 --------------------------------
7 | new Foo
| ^^^^^^^
| A pure expression does nothing in statement position; you may be omitting necessary parentheses
|
| longer explanation available when compiling with `-explain`
Cyclic macro dependencies in tests/neg-macros/annot-suspend-cycle/Test.scala.
Compilation stopped since no further progress can be made.

To fix this, place macros in one set of files and their callers in another.

Compiling with -Xprint-suspension gives more information.
9 changes: 9 additions & 0 deletions tests/neg-macros/annot-suspend-cycle/Macro.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import scala.annotation.{experimental, MacroAnnotation}
import scala.quoted._

@experimental
class cycle extends MacroAnnotation {
def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
new Foo
List(tree)
}
5 changes: 5 additions & 0 deletions tests/neg-macros/annot-suspend-cycle/Test.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// nopos-error
class Foo

@cycle
def test = ()
2 changes: 1 addition & 1 deletion tests/neg-macros/ill-abort.check
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-- Error: tests/neg-macros/ill-abort/quoted_2.scala:1:15 ---------------------------------------------------------------
1 |def test = fail() // error
| ^^^^^^
|Macro expansion was aborted by the macro without any errors reported. Macros should issue errors to end-users to facilitate debugging when aborting a macro expansion.
|Macro expansion was aborted by the macro without any errors reported. Macros should issue errors to end-users when aborting a macro expansion with StopMacroExpansion.
|---------------------------------------------------------------------------------------------------------------------
|Inline stack trace
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Expand Down

0 comments on commit 371ffef

Please sign in to comment.