Skip to content

Commit 2b09680

Browse files
authored
Detect macro dependencies that are missing from the classloader (#20139)
So the situation is basically that `DFiant` and `html.scala` projects do not work "out of the box" with pipelining, and will need to tune their builds if they want some pipelining. However, the compiler reports an error that is not helpful to the user, so in this PR we report a better one. Previously, it was assumed that a missing class (that is valid in current run) during macro evaluation was due to the symbol being defined in the same project. If this condition is met, then compilation is suspended. This assumption breaks when the symbol comes from the classpath, but without a corresponding class file, leading a situation where the same file is always suspended, until it is the only one left, leading to the "cyclic macro dependencies" error. In this case we should assume that the class file will never become available because class path entries are supposed to be immutable. Therefore we should not suspend in this case. This commit therefore detects this situation. Instead of suspending the unit, the compiler aborts the macro expansion, reporting an error that the user will have to deal with - likely by changing the build definition/ In the end, users will see a more actionable error. Note that sbt already automatically disables pipelining on projects that define macros, but this is not useful if the macro itself depends on upstream projects that do not define macros. This is probably a hard problem to detect automatically - so this is good compromise. We also fix `-Xprint-suspension`, which appeared to swallow a lot of diagnostic information. Also make `-Yno-suspended-units` behave better. fixes #20119
2 parents 9d990fb + ab91dfe commit 2b09680

File tree

22 files changed

+227
-37
lines changed

22 files changed

+227
-37
lines changed

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

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,22 +90,19 @@ class CompilationUnit protected (val source: SourceFile, val info: CompilationUn
9090
/** Suspends the compilation unit by thowing a SuspendException
9191
* and recording the suspended compilation unit
9292
*/
93-
def suspend()(using Context): Nothing =
93+
def suspend(hint: => String)(using Context): Nothing =
9494
assert(isSuspendable)
9595
// Clear references to symbols that may become stale. No need to call
9696
// `depRecorder.sendToZinc()` since all compilation phases will be rerun
9797
// when this unit is unsuspended.
9898
depRecorder.clear()
9999
if !suspended then
100-
if ctx.settings.YnoSuspendedUnits.value then
101-
report.error(i"Compilation unit suspended $this (-Yno-suspended-units is set)")
102-
else
103-
if (ctx.settings.XprintSuspension.value)
104-
report.echo(i"suspended: $this")
105-
suspended = true
106-
ctx.run.nn.suspendedUnits += this
107-
if ctx.phase == Phases.inliningPhase then
108-
suspendedAtInliningPhase = true
100+
suspended = true
101+
ctx.run.nn.suspendedUnits += this
102+
if ctx.settings.XprintSuspension.value then
103+
ctx.run.nn.suspendedHints += (this -> hint)
104+
if ctx.phase == Phases.inliningPhase then
105+
suspendedAtInliningPhase = true
109106
throw CompilationUnit.SuspendException()
110107

111108
private var myAssignmentSpans: Map[Int, List[Span]] | Null = null
@@ -123,7 +120,7 @@ class CompilationUnit protected (val source: SourceFile, val info: CompilationUn
123120

124121
override def isJava: Boolean = false
125122

126-
override def suspend()(using Context): Nothing =
123+
override def suspend(hint: => String)(using Context): Nothing =
127124
throw CompilationUnit.SuspendException()
128125

129126
override def assignmentSpans(using Context): Map[Int, List[Span]] = Map.empty

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ class Driver {
5252
if !ctx.reporter.errorsReported && run.suspendedUnits.nonEmpty then
5353
val suspendedUnits = run.suspendedUnits.toList
5454
if (ctx.settings.XprintSuspension.value)
55+
val suspendedHints = run.suspendedHints.toList
5556
report.echo(i"compiling suspended $suspendedUnits%, %")
57+
for (unit, hint) <- suspendedHints do
58+
report.echo(s" $unit: $hint")
5659
val run1 = compiler.newRun
5760
run1.compileSuspendedUnits(suspendedUnits)
5861
finish(compiler, run1)(using MacroClassLoader.init(ctx.fresh))

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
130130
myUnits = us
131131

132132
var suspendedUnits: mutable.ListBuffer[CompilationUnit] = mutable.ListBuffer()
133+
var suspendedHints: mutable.Map[CompilationUnit, String] = mutable.HashMap()
133134

134135
def checkSuspendedUnits(newUnits: List[CompilationUnit])(using Context): Unit =
135136
if newUnits.isEmpty && suspendedUnits.nonEmpty && !ctx.reporter.errorsReported then

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,10 @@ object Symbols extends SymUtils {
165165
final def isDefinedInSource(using Context): Boolean =
166166
span.exists && isValidInCurrentRun && associatedFileMatches(!_.isScalaBinary)
167167

168+
/** Is this symbol valid in the current run, but comes from the classpath? */
169+
final def isDefinedInBinary(using Context): Boolean =
170+
isValidInCurrentRun && associatedFileMatches(_.isScalaBinary)
171+
168172
/** Is symbol valid in current run? */
169173
final def isValidInCurrentRun(using Context): Boolean =
170174
(lastDenot.validFor.runId == ctx.runId || stillValid(lastDenot)) &&

compiler/src/dotty/tools/dotc/inlines/Inliner.scala

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,22 +1058,30 @@ class Inliner(val call: tpd.Tree)(using Context):
10581058
}
10591059
}
10601060

1061-
private def expandMacro(body: Tree, splicePos: SrcPos)(using Context) = {
1061+
private def expandMacro(body: Tree, splicePos: SrcPos)(using Context): Tree = {
10621062
assert(level == 0)
10631063
val inlinedFrom = enclosingInlineds.last
10641064
val dependencies = macroDependencies(body)(using spliceContext)
10651065
val suspendable = ctx.compilationUnit.isSuspendable
1066+
val printSuspensions = ctx.settings.XprintSuspension.value
10661067
if dependencies.nonEmpty && !ctx.reporter.errorsReported then
1068+
val hints: mutable.ListBuffer[String] | Null =
1069+
if printSuspensions then mutable.ListBuffer.empty[String] else null
10671070
for sym <- dependencies do
10681071
if ctx.compilationUnit.source.file == sym.associatedFile then
10691072
report.error(em"Cannot call macro $sym defined in the same source file", call.srcPos)
10701073
else if ctx.settings.YnoSuspendedUnits.value then
10711074
val addendum = ", suspension prevented by -Yno-suspended-units"
10721075
report.error(em"Cannot call macro $sym defined in the same compilation run$addendum", call.srcPos)
1073-
if (suspendable && ctx.settings.XprintSuspension.value)
1074-
report.echo(i"suspension triggered by macro call to ${sym.showLocated} in ${sym.associatedFile}", call.srcPos)
1076+
if suspendable && printSuspensions then
1077+
hints.nn += i"suspension triggered by macro call to ${sym.showLocated} in ${sym.associatedFile}"
10751078
if suspendable then
1076-
ctx.compilationUnit.suspend() // this throws a SuspendException
1079+
if ctx.settings.YnoSuspendedUnits.value then
1080+
return ref(defn.Predef_undefined)
1081+
.withType(ErrorType(em"could not expand macro, suspended units are disabled by -Yno-suspended-units"))
1082+
.withSpan(splicePos.span)
1083+
else
1084+
ctx.compilationUnit.suspend(hints.nn.toList.mkString(", ")) // this throws a SuspendException
10771085

10781086
val evaluatedSplice = inContext(quoted.MacroExpansion.context(inlinedFrom)) {
10791087
Splicer.splice(body, splicePos, inlinedFrom.srcPos, MacroClassLoader.fromContext)

compiler/src/dotty/tools/dotc/quoted/Interpreter.scala

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,8 @@ class Interpreter(pos: SrcPos, classLoader0: ClassLoader)(using Context):
166166
val inst =
167167
try loadModule(moduleClass)
168168
catch
169-
case MissingClassDefinedInCurrentRun(sym) =>
170-
suspendOnMissing(sym, pos)
169+
case MissingClassValidInCurrentRun(sym, origin) =>
170+
suspendOnMissing(sym, origin, pos)
171171
val clazz = inst.getClass
172172
val name = fn.name.asTermName
173173
val method = getMethod(clazz, name, paramsSig(fn))
@@ -213,8 +213,8 @@ class Interpreter(pos: SrcPos, classLoader0: ClassLoader)(using Context):
213213
private def loadClass(name: String): Class[?] =
214214
try classLoader.loadClass(name)
215215
catch
216-
case MissingClassDefinedInCurrentRun(sym) =>
217-
suspendOnMissing(sym, pos)
216+
case MissingClassValidInCurrentRun(sym, origin) =>
217+
suspendOnMissing(sym, origin, pos)
218218

219219

220220
private def getMethod(clazz: Class[?], name: Name, paramClasses: List[Class[?]]): JLRMethod =
@@ -223,8 +223,8 @@ class Interpreter(pos: SrcPos, classLoader0: ClassLoader)(using Context):
223223
case _: NoSuchMethodException =>
224224
val msg = em"Could not find method ${clazz.getCanonicalName}.$name with parameters ($paramClasses%, %)"
225225
throw new StopInterpretation(msg, pos)
226-
case MissingClassDefinedInCurrentRun(sym) =>
227-
suspendOnMissing(sym, pos)
226+
case MissingClassValidInCurrentRun(sym, origin) =>
227+
suspendOnMissing(sym, origin, pos)
228228
}
229229

230230
private def stopIfRuntimeException[T](thunk: => T, method: JLRMethod): T =
@@ -242,8 +242,8 @@ class Interpreter(pos: SrcPos, classLoader0: ClassLoader)(using Context):
242242
ex.getTargetException match {
243243
case ex: scala.quoted.runtime.StopMacroExpansion =>
244244
throw ex
245-
case MissingClassDefinedInCurrentRun(sym) =>
246-
suspendOnMissing(sym, pos)
245+
case MissingClassValidInCurrentRun(sym, origin) =>
246+
suspendOnMissing(sym, origin, pos)
247247
case targetException =>
248248
val sw = new StringWriter()
249249
sw.write("Exception occurred while executing macro expansion.\n")
@@ -348,8 +348,11 @@ object Interpreter:
348348
}
349349
end Call
350350

351-
object MissingClassDefinedInCurrentRun {
352-
def unapply(targetException: Throwable)(using Context): Option[Symbol] = {
351+
enum ClassOrigin:
352+
case Classpath, Source
353+
354+
object MissingClassValidInCurrentRun {
355+
def unapply(targetException: Throwable)(using Context): Option[(Symbol, ClassOrigin)] = {
353356
if !ctx.compilationUnit.isSuspendable then None
354357
else targetException match
355358
case _: NoClassDefFoundError | _: ClassNotFoundException =>
@@ -358,16 +361,34 @@ object Interpreter:
358361
else
359362
val className = message.replace('/', '.')
360363
val sym =
361-
if className.endsWith(str.MODULE_SUFFIX) then staticRef(className.toTermName).symbol.moduleClass
362-
else staticRef(className.toTypeName).symbol
363-
// If the symbol does not a a position we assume that it came from the current run and it has an error
364-
if sym.isDefinedInCurrentRun || (sym.exists && !sym.srcPos.span.exists) then Some(sym)
365-
else None
364+
if className.endsWith(str.MODULE_SUFFIX) then
365+
staticRef(className.stripSuffix(str.MODULE_SUFFIX).toTermName).symbol.moduleClass
366+
else
367+
staticRef(className.toTypeName).symbol
368+
if sym.isDefinedInBinary then
369+
// i.e. the associated file is `.tasty`, if the macro classloader is not able to find the class,
370+
// possibly it indicates that it comes from a pipeline-compiled dependency.
371+
Some((sym, ClassOrigin.Classpath))
372+
else if sym.isDefinedInCurrentRun || (sym.exists && !sym.srcPos.span.exists) then
373+
// If the symbol does not a a position we assume that it came from the current run and it has an error
374+
Some((sym, ClassOrigin.Source))
375+
else
376+
None
366377
case _ => None
367378
}
368379
}
369380

370-
def suspendOnMissing(sym: Symbol, pos: SrcPos)(using Context): Nothing =
371-
if ctx.settings.XprintSuspension.value then
372-
report.echo(i"suspension triggered by a dependency on $sym", pos)
373-
ctx.compilationUnit.suspend() // this throws a SuspendException
381+
def suspendOnMissing(sym: Symbol, origin: ClassOrigin, pos: SrcPos)(using Context): Nothing =
382+
if origin == ClassOrigin.Classpath then
383+
throw StopInterpretation(
384+
em"""Macro code depends on ${sym.showLocated} found on the classpath, but could not be loaded while evaluating the macro.
385+
| This is likely because class files could not be found in the classpath entry for the symbol.
386+
|
387+
| A possible cause is if the origin of this symbol was built with pipelined compilation;
388+
| in which case, this problem may go away by disabling pipelining for that origin.
389+
|
390+
| $sym is defined in file ${sym.associatedFile}""", pos)
391+
else if ctx.settings.YnoSuspendedUnits.value then
392+
throw StopInterpretation(em"suspension triggered by a dependency on missing ${sym.showLocated} not allowed with -Yno-suspended-units", pos)
393+
else
394+
ctx.compilationUnit.suspend(i"suspension triggered by a dependency on missing ${sym.showLocated}") // this throws a SuspendException

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ class MacroAnnotations(phase: IdentityDenotTransformer):
107107
if !ctx.reporter.hasErrors then
108108
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)
109109
List(tree)
110-
case Interpreter.MissingClassDefinedInCurrentRun(sym) =>
111-
Interpreter.suspendOnMissing(sym, annot.tree)
110+
case Interpreter.MissingClassValidInCurrentRun(sym, origin) =>
111+
Interpreter.suspendOnMissing(sym, origin, annot.tree)
112112
case NonFatal(ex) =>
113113
val stack0 = ex.getStackTrace.takeWhile(_.getClassName != "dotty.tools.dotc.transform.MacroAnnotations")
114114
val stack = stack0.take(1 + stack0.lastIndexWhere(_.getMethodName == "transform"))

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1678,7 +1678,7 @@ class Namer { typer: Typer =>
16781678

16791679
final override def complete(denot: SymDenotation)(using Context): Unit =
16801680
denot.resetFlag(Touched) // allow one more completion
1681-
ctx.compilationUnit.suspend()
1681+
ctx.compilationUnit.suspend(i"reset $denot")
16821682
}
16831683

16841684
/** Typecheck `tree` during completion using `typed`, and remember result in TypedAhead map */
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
ThisBuild / usePipelining := true
2+
3+
// m defines a macro depending on b.B, it also tries to use the macro in the same project,
4+
// which will succeed even though B.class is not available when running the macro,
5+
// because compilation can suspend until B is available.
6+
lazy val m = project.in(file("m"))
7+
.settings(
8+
scalacOptions += "-Ycheck:all",
9+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package a
2+
3+
class A(val i: Int)

0 commit comments

Comments
 (0)