Skip to content

Fix #7138: Suspend callers of macros compiled in the same run #7324

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
merged 31 commits into from
Oct 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f8cf9fd
Suspend callers of macros compiled in the same run
odersky Sep 26, 2019
068af6a
Make macroDependencies compilation-order independent
odersky Sep 27, 2019
4e8ab2a
Track splice calls instead of inline calls
odersky Sep 27, 2019
3a13d29
Fix tests
odersky Sep 27, 2019
81a29f2
Add `outermost` utility method to SourcePosition.
odersky Sep 27, 2019
38784ab
Add debug output to track down problem compiling ScalaTest
odersky Sep 27, 2019
58d7f5e
Add -Xprint-suspension option to print info about suspensions
odersky Sep 27, 2019
ce557d6
Compile scalatest with -Xprint-suspension
odersky Sep 27, 2019
b2d2796
Don't count vals as macro dependencies
odersky Sep 27, 2019
440e295
Move tests to pos-macros
nicolasstucki Sep 28, 2019
463aa7b
Fix error message
nicolasstucki Sep 28, 2019
cd0a9ce
Fix reflect-inline test
nicolasstucki Sep 28, 2019
9e387f8
Revert "Don't count vals as macro dependencies"
nicolasstucki Sep 28, 2019
7ebdcf0
Add minimization of previous scalatest stripMargin issue
nicolasstucki Sep 28, 2019
c7b9326
Suspend macros when class not found during macro expansion
nicolasstucki Sep 30, 2019
6d5ef8a
Improve error message
nicolasstucki Sep 30, 2019
1babee2
Don't suspend if errors were reported.
odersky Sep 30, 2019
ff9c07c
Identify classes defined in current run
nicolasstucki Sep 30, 2019
3a8c56f
Remove outdated comment
nicolasstucki Sep 30, 2019
56f3b2c
Handle suspended units in InteractiveDriver
nicolasstucki Sep 30, 2019
e80aefd
Test for legitimate NoClassDefFoundError in Splicer
nicolasstucki Oct 2, 2019
14bf2c6
Add regression tests
nicolasstucki Oct 2, 2019
e1c8fdf
Add output as macro classpath for suspended compilation
nicolasstucki Oct 2, 2019
9134d3f
Add error when suspension has jar output
nicolasstucki Oct 2, 2019
a4bdb4f
Add supspension to macro docs
nicolasstucki Oct 3, 2019
ca57ff9
Test incremental compilation with suspension
nicolasstucki Oct 4, 2019
4c353c6
Typos and specify run vs compile time
nicolasstucki Oct 7, 2019
0c33e64
Add test on incremental and suspended compilation
nicolasstucki Oct 7, 2019
5d863fb
Fix check file
liufengyun Oct 19, 2019
8457680
Fix trailing space in error message
liufengyun Oct 19, 2019
4f59892
Fix CI: remove trailing spaces in check files
liufengyun Oct 19, 2019
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
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/backend/jvm/GenBCode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ class GenBCode extends Phase {
try super.runOn(units)
finally myOutput match {
case jar: JarArchive =>
if (ctx.run.suspendedUnits.nonEmpty)
// If we close the jar the next run will not be able to write on the jar.
// But if we do not close it we cannot use it as part of the macro classpath of the suspended files.
ctx.error("Can not suspend and output to a jar at the same time. See suspension with -Xprint-suspension.")
jar.close()
case _ =>
}
Expand Down
13 changes: 13 additions & 0 deletions compiler/src/dotty/tools/dotc/CompilationUnit.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import dotty.tools.dotc.core.SymDenotations.ClassDenotation
import dotty.tools.dotc.core.Symbols._
import dotty.tools.dotc.transform.SymUtils._
import util.{NoSource, SourceFile}
import core.Decorators._

class CompilationUnit protected (val source: SourceFile) {

Expand All @@ -31,10 +32,22 @@ class CompilationUnit protected (val source: SourceFile) {

/** A structure containing a temporary map for generating inline accessors */
val inlineAccessors: InlineAccessors = new InlineAccessors

var suspended: Boolean = false

def suspend()(given ctx: Context): Nothing =
if !suspended then
if (ctx.settings.XprintSuspension.value)
ctx.echo(i"suspended: $this")
suspended = true
ctx.run.suspendedUnits += this
throw CompilationUnit.SuspendException()
}

object CompilationUnit {

class SuspendException extends Exception

/** Make a compilation unit for top class `clsd` with the contents of the `unpickled` tree */
def apply(clsd: ClassDenotation, unpickled: Tree, forceTrees: Boolean)(implicit ctx: Context): CompilationUnit =
apply(new SourceFile(clsd.symbol.associatedFile, Array.empty[Char]), unpickled, forceTrees)
Expand Down
25 changes: 18 additions & 7 deletions compiler/src/dotty/tools/dotc/Driver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import core.{MacroClassLoader, Mode, TypeError}
import dotty.tools.dotc.ast.Positioned
import dotty.tools.io.File
import reporting._
import core.Decorators._

import scala.util.control.NonFatal
import fromtasty.{TASTYCompiler, TastyFileUtil}
Expand All @@ -30,23 +31,33 @@ class Driver {

protected def doCompile(compiler: Compiler, fileNames: List[String])(implicit ctx: Context): Reporter =
if (fileNames.nonEmpty)
try {
try
val run = compiler.newRun
run.compile(fileNames)
run.printSummary()
}
catch {

def finish(run: Run): Unit =
run.printSummary()
if !ctx.reporter.errorsReported && run.suspendedUnits.nonEmpty then
val suspendedUnits = run.suspendedUnits.toList
if (ctx.settings.XprintSuspension.value)
ctx.echo(i"compiling suspended $suspendedUnits%, %")
val run1 = compiler.newRun
for unit <- suspendedUnits do unit.suspended = false
run1.compileUnits(suspendedUnits)
finish(run1)

finish(run)
catch
case ex: FatalError =>
ctx.error(ex.getMessage) // signals that we should fail compilation.
ctx.reporter
case ex: TypeError =>
println(s"${ex.toMessage} while compiling ${fileNames.mkString(", ")}")
throw ex
case ex: Throwable =>
println(s"$ex while compiling ${fileNames.mkString(", ")}")
throw ex
}
else ctx.reporter
ctx.reporter
end doCompile

protected def initCtx: Context = (new ContextBase).initialCtx

Expand Down
16 changes: 9 additions & 7 deletions compiler/src/dotty/tools/dotc/Run.scala
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,20 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
*/
def units: List[CompilationUnit] = myUnits

var suspendedUnits: mutable.ListBuffer[CompilationUnit] = mutable.ListBuffer()

private def units_=(us: List[CompilationUnit]): Unit =
myUnits = us

/** The files currently being compiled, this may return different results over time.
* These files do not have to be source files since it's possible to compile
* from TASTY.
*/
/** The files currently being compiled (active or suspended).
* This may return different results over time.
* These files do not have to be source files since it's possible to compile
* from TASTY.
*/
def files: Set[AbstractFile] = {
if (myUnits ne myUnitsCached) {
myUnitsCached = myUnits
myFiles = myUnits.map(_.source.file).toSet
myFiles = (myUnits ++ suspendedUnits).map(_.source.file).toSet
}
myFiles
}
Expand Down Expand Up @@ -247,11 +250,10 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
}

/** Print summary; return # of errors encountered */
def printSummary(): Reporter = {
def printSummary(): Unit = {
printMaxConstraint()
val r = ctx.reporter
r.printSummary
r
}

override def reset(): Unit = {
Expand Down
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class ScalaSettings extends Settings.SettingGroup {
val XprintDiff: Setting[Boolean] = BooleanSetting("-Xprint-diff", "Print changed parts of the tree since last print.")
val XprintDiffDel: Setting[Boolean] = BooleanSetting("-Xprint-diff-del", "Print changed parts of the tree since last print including deleted parts.")
val XprintInline: Setting[Boolean] = BooleanSetting("-Xprint-inline", "Show where inlined code comes from")
val XprintSuspension: Setting[Boolean] = BooleanSetting("-Xprint-suspension", "Show when code is suspended until macros are compiled")
val Xprompt: Setting[Boolean] = BooleanSetting("-Xprompt", "Display a prompt after each error (debugging option).")
val XnoValueClasses: Setting[Boolean] = BooleanSetting("-Xno-value-classes", "Do not use value classes. Helps debugging.")
val XreplLineWidth: Setting[Int] = IntSetting("-Xrepl-line-width", "Maximal number of columns per line for REPL output", 390)
Expand Down Expand Up @@ -200,7 +201,7 @@ class ScalaSettings extends Settings.SettingGroup {
"The source repository of your project",
""
)

val projectLogo: Setting[String] = StringSetting(
"-project-logo",
"project logo filename",
Expand Down
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/core/MacroClassLoader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ object MacroClassLoader {

private def makeMacroClassLoader(implicit ctx: Context): ClassLoader = trace("new macro class loader") {
val urls = ctx.settings.classpath.value.split(java.io.File.pathSeparatorChar).map(cp => java.nio.file.Paths.get(cp).toUri.toURL)
new java.net.URLClassLoader(urls, getClass.getClassLoader)
val out = ctx.settings.outputDir.value.jpath.toUri.toURL // to find classes in case of suspended compilation
new java.net.URLClassLoader(urls :+ out, getClass.getClassLoader)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class InteractiveDriver(val settings: List[String]) extends Driver {

run.compileSources(List(source))
run.printSummary()
val unit = ctx.run.units.head
val unit = if ctx.run.units.nonEmpty then ctx.run.units.head else ctx.run.suspendedUnits.head
val t = unit.tpdTree
cleanup(t)
myOpenedTrees(uri) = topLevelTrees(t, source)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ trait MessageRendering {
}

msg.linesIterator
.map { line => " " * (offset - 1) + "|" + padding + line}
.map { line => " " * (offset - 1) + "|" + (if line.isEmpty then "" else padding + line) }
.mkString(EOL)
}

Expand Down
49 changes: 33 additions & 16 deletions compiler/src/dotty/tools/dotc/transform/Splicer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ object Splicer {
interpretedExpr.fold(tree)(macroClosure => PickledQuotes.quotedExprToTree(macroClosure(QuoteContext())))
}
catch {
case ex: CompilationUnit.SuspendException =>
throw ex
case ex: StopInterpretation =>
ctx.error(ex.msg, ex.pos)
EmptyTree
Expand Down Expand Up @@ -322,20 +324,18 @@ object Splicer {
try classLoader.loadClass(name)
catch {
case _: ClassNotFoundException =>
val msg = s"Could not find class $name in classpath$extraMsg"
val msg = s"Could not find class $name in classpath"
throw new StopInterpretation(msg, pos)
}

private def getMethod(clazz: Class[?], name: Name, paramClasses: List[Class[?]]): Method =
try clazz.getMethod(name.toString, paramClasses: _*)
catch {
case _: NoSuchMethodException =>
val msg = em"Could not find method ${clazz.getCanonicalName}.$name with parameters ($paramClasses%, %)$extraMsg"
val msg = em"Could not find method ${clazz.getCanonicalName}.$name with parameters ($paramClasses%, %)"
throw new StopInterpretation(msg, pos)
}

private def extraMsg = ". The most common reason for that is that you apply macros in the compilation run that defines them"

private def stopIfRuntimeException[T](thunk: => T, method: Method): T =
try thunk
catch {
Expand All @@ -348,21 +348,38 @@ object Splicer {
sw.write("\n")
throw new StopInterpretation(sw.toString, pos)
case ex: InvocationTargetException =>
val sw = new StringWriter()
sw.write("Exception occurred while executing macro expansion.\n")
val targetException = ex.getTargetException
if (!ctx.settings.Ydebug.value) {
val end = targetException.getStackTrace.lastIndexWhere { x =>
x.getClassName == method.getDeclaringClass.getCanonicalName && x.getMethodName == method.getName
}
val shortStackTrace = targetException.getStackTrace.take(end + 1)
targetException.setStackTrace(shortStackTrace)
ex.getTargetException match {
case MissingClassDefinedInCurrentRun(sym) =>
if (ctx.settings.XprintSuspension.value)
ctx.echo(i"suspension triggered by a dependency on $sym", pos)
ctx.compilationUnit.suspend() // this throws a SuspendException
case targetException =>
val sw = new StringWriter()
sw.write("Exception occurred while executing macro expansion.\n")
if (!ctx.settings.Ydebug.value) {
val end = targetException.getStackTrace.lastIndexWhere { x =>
x.getClassName == method.getDeclaringClass.getCanonicalName && x.getMethodName == method.getName
}
val shortStackTrace = targetException.getStackTrace.take(end + 1)
targetException.setStackTrace(shortStackTrace)
}
targetException.printStackTrace(new PrintWriter(sw))
sw.write("\n")
throw new StopInterpretation(sw.toString, pos)
}
targetException.printStackTrace(new PrintWriter(sw))
sw.write("\n")
throw new StopInterpretation(sw.toString, pos)
}

private object MissingClassDefinedInCurrentRun {
def unapply(targetException: NoClassDefFoundError)(given ctx: Context): Option[Symbol] = {
val className = targetException.getMessage
if (className eq null) None
else {
val sym = ctx.base.staticRef(className.toTypeName).symbol
if (sym.isDefinedInCurrentRun) Some(sym) else 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
37 changes: 29 additions & 8 deletions compiler/src/dotty/tools/dotc/typer/FrontEnd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import core._
import Phases._
import Contexts._
import Symbols._
import Decorators._
import dotty.tools.dotc.parsing.JavaParsers.JavaParser
import parsing.Parsers.Parser
import config.Config
Expand Down Expand Up @@ -71,11 +72,15 @@ class FrontEnd extends Phase {
}

def typeCheck(implicit ctx: Context): Unit = monitor("typechecking") {
val unit = ctx.compilationUnit
unit.tpdTree = ctx.typer.typedExpr(unit.untpdTree)
typr.println("typed: " + unit.source)
record("retained untyped trees", unit.untpdTree.treeSize)
record("retained typed trees after typer", unit.tpdTree.treeSize)
try
val unit = ctx.compilationUnit
if !unit.suspended then
unit.tpdTree = ctx.typer.typedExpr(unit.untpdTree)
typr.println("typed: " + unit.source)
record("retained untyped trees", unit.untpdTree.treeSize)
record("retained typed trees after typer", unit.tpdTree.treeSize)
catch
case ex: CompilationUnit.SuspendException =>
}

private def firstTopLevelDef(trees: List[tpd.Tree])(implicit ctx: Context): Symbol = trees match {
Expand All @@ -86,14 +91,14 @@ class FrontEnd extends Phase {
}

protected def discardAfterTyper(unit: CompilationUnit)(implicit ctx: Context): Boolean =
unit.isJava
unit.isJava || unit.suspended

override def runOn(units: List[CompilationUnit])(implicit ctx: Context): List[CompilationUnit] = {
val unitContexts = for (unit <- units) yield {
ctx.inform(s"compiling ${unit.source}")
ctx.fresh.setCompilationUnit(unit)
}
unitContexts foreach (parse(_))
unitContexts.foreach(parse(_))
record("parsedTrees", ast.Trees.ntrees)
remaining = unitContexts
while (remaining.nonEmpty) {
Expand All @@ -108,7 +113,23 @@ class FrontEnd extends Phase {

unitContexts.foreach(typeCheck(_))
record("total trees after typer", ast.Trees.ntrees)
unitContexts.map(_.compilationUnit).filterNot(discardAfterTyper)
val newUnits = unitContexts.map(_.compilationUnit).filterNot(discardAfterTyper)
val suspendedUnits = ctx.run.suspendedUnits
if newUnits.isEmpty && suspendedUnits.nonEmpty && !ctx.reporter.errorsReported then
val where =
if suspendedUnits.size == 1 then i"in ${suspendedUnits.head}."
else i"""among
|
| ${suspendedUnits.toList}%, %
|"""
val enableXprintSuspensionHint =
if (ctx.settings.XprintSuspension.value) ""
else "\n\nCompiling with -Xprint-suspension gives more information."
ctx.error(em"""Cyclic macro dependencies $where
|Compilation stopped since no further progress can be made.
|
|To fix this, place macros in one set of files and their callers in another.$enableXprintSuspensionHint""")
newUnits
}

def run(implicit ctx: Context): Unit = unsupported("run")
Expand Down
Loading