Skip to content

Handle macro annotation suspends and crashes #16509

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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