Skip to content

Commit 9193c44

Browse files
Merge pull request #7324 from dotty-staging/add-suspend
Fix #7138: Suspend callers of macros compiled in the same run
2 parents 169e1db + 4f59892 commit 9193c44

File tree

78 files changed

+540
-120
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+540
-120
lines changed

compiler/src/dotty/tools/backend/jvm/GenBCode.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ class GenBCode extends Phase {
5555
try super.runOn(units)
5656
finally myOutput match {
5757
case jar: JarArchive =>
58+
if (ctx.run.suspendedUnits.nonEmpty)
59+
// If we close the jar the next run will not be able to write on the jar.
60+
// But if we do not close it we cannot use it as part of the macro classpath of the suspended files.
61+
ctx.error("Can not suspend and output to a jar at the same time. See suspension with -Xprint-suspension.")
5862
jar.close()
5963
case _ =>
6064
}

compiler/src/dotty/tools/dotc/CompilationUnit.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import dotty.tools.dotc.core.SymDenotations.ClassDenotation
1010
import dotty.tools.dotc.core.Symbols._
1111
import dotty.tools.dotc.transform.SymUtils._
1212
import util.{NoSource, SourceFile}
13+
import core.Decorators._
1314

1415
class CompilationUnit protected (val source: SourceFile) {
1516

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

3233
/** A structure containing a temporary map for generating inline accessors */
3334
val inlineAccessors: InlineAccessors = new InlineAccessors
35+
36+
var suspended: Boolean = false
37+
38+
def suspend()(given ctx: Context): Nothing =
39+
if !suspended then
40+
if (ctx.settings.XprintSuspension.value)
41+
ctx.echo(i"suspended: $this")
42+
suspended = true
43+
ctx.run.suspendedUnits += this
44+
throw CompilationUnit.SuspendException()
3445
}
3546

3647
object CompilationUnit {
3748

49+
class SuspendException extends Exception
50+
3851
/** Make a compilation unit for top class `clsd` with the contents of the `unpickled` tree */
3952
def apply(clsd: ClassDenotation, unpickled: Tree, forceTrees: Boolean)(implicit ctx: Context): CompilationUnit =
4053
apply(new SourceFile(clsd.symbol.associatedFile, Array.empty[Char]), unpickled, forceTrees)

compiler/src/dotty/tools/dotc/Driver.scala

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import core.{MacroClassLoader, Mode, TypeError}
1010
import dotty.tools.dotc.ast.Positioned
1111
import dotty.tools.io.File
1212
import reporting._
13+
import core.Decorators._
1314

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

3132
protected def doCompile(compiler: Compiler, fileNames: List[String])(implicit ctx: Context): Reporter =
3233
if (fileNames.nonEmpty)
33-
try {
34+
try
3435
val run = compiler.newRun
3536
run.compile(fileNames)
36-
run.printSummary()
37-
}
38-
catch {
37+
38+
def finish(run: Run): Unit =
39+
run.printSummary()
40+
if !ctx.reporter.errorsReported && run.suspendedUnits.nonEmpty then
41+
val suspendedUnits = run.suspendedUnits.toList
42+
if (ctx.settings.XprintSuspension.value)
43+
ctx.echo(i"compiling suspended $suspendedUnits%, %")
44+
val run1 = compiler.newRun
45+
for unit <- suspendedUnits do unit.suspended = false
46+
run1.compileUnits(suspendedUnits)
47+
finish(run1)
48+
49+
finish(run)
50+
catch
3951
case ex: FatalError =>
4052
ctx.error(ex.getMessage) // signals that we should fail compilation.
41-
ctx.reporter
4253
case ex: TypeError =>
4354
println(s"${ex.toMessage} while compiling ${fileNames.mkString(", ")}")
4455
throw ex
4556
case ex: Throwable =>
4657
println(s"$ex while compiling ${fileNames.mkString(", ")}")
4758
throw ex
48-
}
49-
else ctx.reporter
59+
ctx.reporter
60+
end doCompile
5061

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

compiler/src/dotty/tools/dotc/Run.scala

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,20 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
7878
*/
7979
def units: List[CompilationUnit] = myUnits
8080

81+
var suspendedUnits: mutable.ListBuffer[CompilationUnit] = mutable.ListBuffer()
82+
8183
private def units_=(us: List[CompilationUnit]): Unit =
8284
myUnits = us
8385

84-
/** The files currently being compiled, this may return different results over time.
85-
* These files do not have to be source files since it's possible to compile
86-
* from TASTY.
87-
*/
86+
/** The files currently being compiled (active or suspended).
87+
* This may return different results over time.
88+
* These files do not have to be source files since it's possible to compile
89+
* from TASTY.
90+
*/
8891
def files: Set[AbstractFile] = {
8992
if (myUnits ne myUnitsCached) {
9093
myUnitsCached = myUnits
91-
myFiles = myUnits.map(_.source.file).toSet
94+
myFiles = (myUnits ++ suspendedUnits).map(_.source.file).toSet
9295
}
9396
myFiles
9497
}
@@ -247,11 +250,10 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
247250
}
248251

249252
/** Print summary; return # of errors encountered */
250-
def printSummary(): Reporter = {
253+
def printSummary(): Unit = {
251254
printMaxConstraint()
252255
val r = ctx.reporter
253256
r.printSummary
254-
r
255257
}
256258

257259
override def reset(): Unit = {

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class ScalaSettings extends Settings.SettingGroup {
7777
val XprintDiff: Setting[Boolean] = BooleanSetting("-Xprint-diff", "Print changed parts of the tree since last print.")
7878
val XprintDiffDel: Setting[Boolean] = BooleanSetting("-Xprint-diff-del", "Print changed parts of the tree since last print including deleted parts.")
7979
val XprintInline: Setting[Boolean] = BooleanSetting("-Xprint-inline", "Show where inlined code comes from")
80+
val XprintSuspension: Setting[Boolean] = BooleanSetting("-Xprint-suspension", "Show when code is suspended until macros are compiled")
8081
val Xprompt: Setting[Boolean] = BooleanSetting("-Xprompt", "Display a prompt after each error (debugging option).")
8182
val XnoValueClasses: Setting[Boolean] = BooleanSetting("-Xno-value-classes", "Do not use value classes. Helps debugging.")
8283
val XreplLineWidth: Setting[Int] = IntSetting("-Xrepl-line-width", "Maximal number of columns per line for REPL output", 390)
@@ -200,7 +201,7 @@ class ScalaSettings extends Settings.SettingGroup {
200201
"The source repository of your project",
201202
""
202203
)
203-
204+
204205
val projectLogo: Setting[String] = StringSetting(
205206
"-project-logo",
206207
"project logo filename",

compiler/src/dotty/tools/dotc/core/MacroClassLoader.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ object MacroClassLoader {
2121

2222
private def makeMacroClassLoader(implicit ctx: Context): ClassLoader = trace("new macro class loader") {
2323
val urls = ctx.settings.classpath.value.split(java.io.File.pathSeparatorChar).map(cp => java.nio.file.Paths.get(cp).toUri.toURL)
24-
new java.net.URLClassLoader(urls, getClass.getClassLoader)
24+
val out = ctx.settings.outputDir.value.jpath.toUri.toURL // to find classes in case of suspended compilation
25+
new java.net.URLClassLoader(urls :+ out, getClass.getClassLoader)
2526
}
2627
}

compiler/src/dotty/tools/dotc/interactive/InteractiveDriver.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ class InteractiveDriver(val settings: List[String]) extends Driver {
155155

156156
run.compileSources(List(source))
157157
run.printSummary()
158-
val unit = ctx.run.units.head
158+
val unit = if ctx.run.units.nonEmpty then ctx.run.units.head else ctx.run.suspendedUnits.head
159159
val t = unit.tpdTree
160160
cleanup(t)
161161
myOpenedTrees(uri) = topLevelTrees(t, source)

compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ trait MessageRendering {
104104
}
105105

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

compiler/src/dotty/tools/dotc/transform/Splicer.scala

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ object Splicer {
4747
interpretedExpr.fold(tree)(macroClosure => PickledQuotes.quotedExprToTree(macroClosure(QuoteContext())))
4848
}
4949
catch {
50+
case ex: CompilationUnit.SuspendException =>
51+
throw ex
5052
case ex: StopInterpretation =>
5153
ctx.error(ex.msg, ex.pos)
5254
EmptyTree
@@ -322,20 +324,18 @@ object Splicer {
322324
try classLoader.loadClass(name)
323325
catch {
324326
case _: ClassNotFoundException =>
325-
val msg = s"Could not find class $name in classpath$extraMsg"
327+
val msg = s"Could not find class $name in classpath"
326328
throw new StopInterpretation(msg, pos)
327329
}
328330

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

337-
private def extraMsg = ". The most common reason for that is that you apply macros in the compilation run that defines them"
338-
339339
private def stopIfRuntimeException[T](thunk: => T, method: Method): T =
340340
try thunk
341341
catch {
@@ -348,21 +348,38 @@ object Splicer {
348348
sw.write("\n")
349349
throw new StopInterpretation(sw.toString, pos)
350350
case ex: InvocationTargetException =>
351-
val sw = new StringWriter()
352-
sw.write("Exception occurred while executing macro expansion.\n")
353-
val targetException = ex.getTargetException
354-
if (!ctx.settings.Ydebug.value) {
355-
val end = targetException.getStackTrace.lastIndexWhere { x =>
356-
x.getClassName == method.getDeclaringClass.getCanonicalName && x.getMethodName == method.getName
357-
}
358-
val shortStackTrace = targetException.getStackTrace.take(end + 1)
359-
targetException.setStackTrace(shortStackTrace)
351+
ex.getTargetException match {
352+
case MissingClassDefinedInCurrentRun(sym) =>
353+
if (ctx.settings.XprintSuspension.value)
354+
ctx.echo(i"suspension triggered by a dependency on $sym", pos)
355+
ctx.compilationUnit.suspend() // this throws a SuspendException
356+
case targetException =>
357+
val sw = new StringWriter()
358+
sw.write("Exception occurred while executing macro expansion.\n")
359+
if (!ctx.settings.Ydebug.value) {
360+
val end = targetException.getStackTrace.lastIndexWhere { x =>
361+
x.getClassName == method.getDeclaringClass.getCanonicalName && x.getMethodName == method.getName
362+
}
363+
val shortStackTrace = targetException.getStackTrace.take(end + 1)
364+
targetException.setStackTrace(shortStackTrace)
365+
}
366+
targetException.printStackTrace(new PrintWriter(sw))
367+
sw.write("\n")
368+
throw new StopInterpretation(sw.toString, pos)
360369
}
361-
targetException.printStackTrace(new PrintWriter(sw))
362-
sw.write("\n")
363-
throw new StopInterpretation(sw.toString, pos)
364370
}
365371

372+
private object MissingClassDefinedInCurrentRun {
373+
def unapply(targetException: NoClassDefFoundError)(given ctx: Context): Option[Symbol] = {
374+
val className = targetException.getMessage
375+
if (className eq null) None
376+
else {
377+
val sym = ctx.base.staticRef(className.toTypeName).symbol
378+
if (sym.isDefinedInCurrentRun) Some(sym) else None
379+
}
380+
}
381+
}
382+
366383
/** List of classes of the parameters of the signature of `sym` */
367384
private def paramsSig(sym: Symbol): List[Class[?]] = {
368385
def paramClass(param: Type): Class[?] = {

compiler/src/dotty/tools/dotc/typer/FrontEnd.scala

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import core._
66
import Phases._
77
import Contexts._
88
import Symbols._
9+
import Decorators._
910
import dotty.tools.dotc.parsing.JavaParsers.JavaParser
1011
import parsing.Parsers.Parser
1112
import config.Config
@@ -71,11 +72,15 @@ class FrontEnd extends Phase {
7172
}
7273

7374
def typeCheck(implicit ctx: Context): Unit = monitor("typechecking") {
74-
val unit = ctx.compilationUnit
75-
unit.tpdTree = ctx.typer.typedExpr(unit.untpdTree)
76-
typr.println("typed: " + unit.source)
77-
record("retained untyped trees", unit.untpdTree.treeSize)
78-
record("retained typed trees after typer", unit.tpdTree.treeSize)
75+
try
76+
val unit = ctx.compilationUnit
77+
if !unit.suspended then
78+
unit.tpdTree = ctx.typer.typedExpr(unit.untpdTree)
79+
typr.println("typed: " + unit.source)
80+
record("retained untyped trees", unit.untpdTree.treeSize)
81+
record("retained typed trees after typer", unit.tpdTree.treeSize)
82+
catch
83+
case ex: CompilationUnit.SuspendException =>
7984
}
8085

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

8893
protected def discardAfterTyper(unit: CompilationUnit)(implicit ctx: Context): Boolean =
89-
unit.isJava
94+
unit.isJava || unit.suspended
9095

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

109114
unitContexts.foreach(typeCheck(_))
110115
record("total trees after typer", ast.Trees.ntrees)
111-
unitContexts.map(_.compilationUnit).filterNot(discardAfterTyper)
116+
val newUnits = unitContexts.map(_.compilationUnit).filterNot(discardAfterTyper)
117+
val suspendedUnits = ctx.run.suspendedUnits
118+
if newUnits.isEmpty && suspendedUnits.nonEmpty && !ctx.reporter.errorsReported then
119+
val where =
120+
if suspendedUnits.size == 1 then i"in ${suspendedUnits.head}."
121+
else i"""among
122+
|
123+
| ${suspendedUnits.toList}%, %
124+
|"""
125+
val enableXprintSuspensionHint =
126+
if (ctx.settings.XprintSuspension.value) ""
127+
else "\n\nCompiling with -Xprint-suspension gives more information."
128+
ctx.error(em"""Cyclic macro dependencies $where
129+
|Compilation stopped since no further progress can be made.
130+
|
131+
|To fix this, place macros in one set of files and their callers in another.$enableXprintSuspensionHint""")
132+
newUnits
112133
}
113134

114135
def run(implicit ctx: Context): Unit = unsupported("run")

0 commit comments

Comments
 (0)