Skip to content

Commit d1795ef

Browse files
committed
add support for pipeline build of Scala and Java files
- rename '-Yjava-tasty-output' to '-Yearly-tasty-output' because now Scala TASTy will also be written to this destination. - add '-Ypickle-java' alias of '-Yjava-tasty', as expected by Zinc - add '-Ypickle-write' alias of '-Yearly-tasty-output', as expected by Zinc - move ExtractAPI phase to after Pickler, this way we can do it in parallel with generating TASTy bytes. At the end of this phase we write the TASTy to the '-Yearly-tasty-output' destination. Also ensure that ExtractAPI phase runs with '-Yjava-tasty', even if no incremental callback is set (don't extract the API in this case). - test the pipelining with sbt scripted tests, including for inline methods and macros with pipelining - describe semantics with respect to suspensions, introduce -Yno-suspended-units flag for greater control by the user.
1 parent 2842412 commit d1795ef

File tree

67 files changed

+786
-100
lines changed

Some content is hidden

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

67 files changed

+786
-100
lines changed

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,15 @@ class CompilationUnit protected (val source: SourceFile, val info: CompilationUn
9797
// when this unit is unsuspended.
9898
depRecorder.clear()
9999
if !suspended then
100-
if (ctx.settings.XprintSuspension.value)
101-
report.echo(i"suspended: $this")
102-
suspended = true
103-
ctx.run.nn.suspendedUnits += this
104-
if ctx.phase == Phases.inliningPhase then
105-
suspendedAtInliningPhase = true
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
106109
throw CompilationUnit.SuspendException()
107110

108111
private var myAssignmentSpans: Map[Int, List[Span]] | Null = null

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@ class Compiler {
4141
List(new semanticdb.ExtractSemanticDB.ExtractSemanticInfo) :: // Extract info into .semanticdb files
4242
List(new PostTyper) :: // Additional checks and cleanups after type checking
4343
List(new sjs.PrepJSInterop) :: // Additional checks and transformations for Scala.js (Scala.js only)
44-
List(new sbt.ExtractAPI) :: // Sends a representation of the API of classes to sbt via callbacks
4544
List(new SetRootTree) :: // Set the `rootTreeOrProvider` on class symbols
4645
Nil
4746

4847
/** Phases dealing with TASTY tree pickling and unpickling */
4948
protected def picklerPhases: List[List[Phase]] =
5049
List(new Pickler) :: // Generate TASTY info
50+
List(new sbt.ExtractAPI) :: // Sends a representation of the API of classes to sbt via callbacks
5151
List(new Inlining) :: // Inline and execute macros
5252
List(new PostInlining) :: // Add mirror support for inlined code
5353
List(new CheckUnused.PostInlining) :: // Check for unused elements

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ private sealed trait YSettings:
377377
val YprintPos: Setting[Boolean] = BooleanSetting("-Yprint-pos", "Show tree positions.")
378378
val YprintPosSyms: Setting[Boolean] = BooleanSetting("-Yprint-pos-syms", "Show symbol definitions positions.")
379379
val YnoDeepSubtypes: Setting[Boolean] = BooleanSetting("-Yno-deep-subtypes", "Throw an exception on deep subtyping call stacks.")
380+
val YnoSuspendedUnits: Setting[Boolean] = BooleanSetting("-Yno-suspended-units", "Do not suspend units, e.g. when calling a macro defined in the same run. This will error instead of suspending.")
380381
val YnoPatmatOpt: Setting[Boolean] = BooleanSetting("-Yno-patmat-opt", "Disable all pattern matching optimizations.")
381382
val YplainPrinter: Setting[Boolean] = BooleanSetting("-Yplain-printer", "Pretty-print using a plain printer.")
382383
val YprintSyms: Setting[Boolean] = BooleanSetting("-Yprint-syms", "When printing trees print info in symbols instead of corresponding info in trees.")
@@ -438,7 +439,7 @@ private sealed trait YSettings:
438439
val YdebugMacros: Setting[Boolean] = BooleanSetting("-Ydebug-macros", "Show debug info when quote pattern match fails")
439440

440441
// Pipeline compilation options
441-
val YjavaTasty: Setting[Boolean] = BooleanSetting("-Yjava-tasty", "Pickler phase should compute pickles for .java defined symbols for use by build tools")
442-
val YjavaTastyOutput: Setting[AbstractFile] = OutputSetting("-Yjava-tasty-output", "directory|jar", "(Internal use only!) destination for generated .tasty files containing Java type signatures.", NoAbstractFile)
442+
val YjavaTasty: Setting[Boolean] = BooleanSetting("-Yjava-tasty", "Pickler phase should compute pickles for .java defined symbols for use by build tools", aliases = List("-Ypickle-java"))
443+
val YearlyTastyOutput: Setting[AbstractFile] = OutputSetting("-Yearly-tasty-output", "directory|jar", "Destination for generated .tasty files containing possibly outline type signatures.", NoAbstractFile, aliases = List("-Ypickle-write"))
443444
val YallowOutlineFromTasty: Setting[Boolean] = BooleanSetting("-Yallow-outline-from-tasty", "Allow outline TASTy to be loaded with the -from-tasty option.")
444445
end YSettings

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,8 @@ object Settings:
308308
def MultiStringSetting(name: String, helpArg: String, descr: String, default: List[String] = Nil, aliases: List[String] = Nil): Setting[List[String]] =
309309
publish(Setting(name, descr, default, helpArg, aliases = aliases))
310310

311-
def OutputSetting(name: String, helpArg: String, descr: String, default: AbstractFile): Setting[AbstractFile] =
312-
publish(Setting(name, descr, default, helpArg))
311+
def OutputSetting(name: String, helpArg: String, descr: String, default: AbstractFile, aliases: List[String] = Nil): Setting[AbstractFile] =
312+
publish(Setting(name, descr, default, helpArg, aliases = aliases))
313313

314314
def PathSetting(name: String, descr: String, default: String, aliases: List[String] = Nil): Setting[String] =
315315
publish(Setting(name, descr, default, aliases = aliases))

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,7 @@ class Definitions {
10661066
@tu lazy val PublicInBinaryAnnot: ClassSymbol = requiredClass("scala.annotation.publicInBinary")
10671067

10681068
@tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable")
1069+
@tu lazy val JavaAnnotationAnnot: ClassSymbol = requiredClass("java.lang.annotation.Annotation")
10691070

10701071
// Initialization annotations
10711072
@tu lazy val InitModule: Symbol = requiredModule("scala.annotation.init")

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ object Phases {
210210
private var myTyperPhase: Phase = uninitialized
211211
private var myPostTyperPhase: Phase = uninitialized
212212
private var mySbtExtractDependenciesPhase: Phase = uninitialized
213+
private var mySbtExtractAPIPhase: Phase = uninitialized
213214
private var myPicklerPhase: Phase = uninitialized
214215
private var myInliningPhase: Phase = uninitialized
215216
private var myStagingPhase: Phase = uninitialized
@@ -235,6 +236,7 @@ object Phases {
235236
final def typerPhase: Phase = myTyperPhase
236237
final def postTyperPhase: Phase = myPostTyperPhase
237238
final def sbtExtractDependenciesPhase: Phase = mySbtExtractDependenciesPhase
239+
final def sbtExtractAPIPhase: Phase = mySbtExtractAPIPhase
238240
final def picklerPhase: Phase = myPicklerPhase
239241
final def inliningPhase: Phase = myInliningPhase
240242
final def stagingPhase: Phase = myStagingPhase
@@ -263,6 +265,7 @@ object Phases {
263265
myTyperPhase = phaseOfClass(classOf[TyperPhase])
264266
myPostTyperPhase = phaseOfClass(classOf[PostTyper])
265267
mySbtExtractDependenciesPhase = phaseOfClass(classOf[sbt.ExtractDependencies])
268+
mySbtExtractAPIPhase = phaseOfClass(classOf[sbt.ExtractAPI])
266269
myPicklerPhase = phaseOfClass(classOf[Pickler])
267270
myInliningPhase = phaseOfClass(classOf[Inlining])
268271
myStagingPhase = phaseOfClass(classOf[Staging])
@@ -336,19 +339,29 @@ object Phases {
336339
/** skip the phase for a Java compilation unit, may depend on -Yjava-tasty */
337340
def skipIfJava(using Context): Boolean = true
338341

342+
final def isAfterLastJavaPhase(using Context): Boolean =
343+
// With `-Yjava-tasty` nominally the final phase is expected be ExtractAPI,
344+
// otherwise drop Java sources at the end of TyperPhase.
345+
// Checks if the last Java phase is before this phase,
346+
// which always fails if the terminal phase is before lastJavaPhase.
347+
val lastJavaPhase = if ctx.settings.YjavaTasty.value then sbtExtractAPIPhase else typerPhase
348+
lastJavaPhase <= this
349+
339350
/** @pre `isRunnable` returns true */
340351
def run(using Context): Unit
341352

342353
/** @pre `isRunnable` returns true */
343354
def runOn(units: List[CompilationUnit])(using runCtx: Context): List[CompilationUnit] =
344355
val buf = List.newBuilder[CompilationUnit]
345-
// factor out typedAsJava check when not needed
346-
val doSkipJava = ctx.settings.YjavaTasty.value && this <= picklerPhase && skipIfJava
356+
357+
// Test that we are in a state where we need to check if the phase should be skipped for a java file,
358+
// this prevents checking the expensive `unit.typedAsJava` unnecessarily.
359+
val doCheckJava = skipIfJava && !isAfterLastJavaPhase
347360
for unit <- units do
348361
given unitCtx: Context = runCtx.fresh.setPhase(this.start).setCompilationUnit(unit).withRootImports
349362
if ctx.run.enterUnit(unit) then
350363
try
351-
if doSkipJava && unit.typedAsJava then
364+
if doCheckJava && unit.typedAsJava then
352365
()
353366
else
354367
run
@@ -503,6 +516,7 @@ object Phases {
503516
def typerPhase(using Context): Phase = ctx.base.typerPhase
504517
def postTyperPhase(using Context): Phase = ctx.base.postTyperPhase
505518
def sbtExtractDependenciesPhase(using Context): Phase = ctx.base.sbtExtractDependenciesPhase
519+
def sbtExtractAPIPhase(using Context): Phase = ctx.base.sbtExtractAPIPhase
506520
def picklerPhase(using Context): Phase = ctx.base.picklerPhase
507521
def inliningPhase(using Context): Phase = ctx.base.inliningPhase
508522
def stagingPhase(using Context): Phase = ctx.base.stagingPhase

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,8 @@ class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader {
456456
val tastyUUID = unpickler.unpickler.header.uuid
457457
new ClassfileTastyUUIDParser(classfile)(ctx).checkTastyUUID(tastyUUID)
458458
else
459-
// This will be the case in any of our tests that compile with `-Youtput-only-tasty`
459+
// This will be the case in any of our tests that compile with `-Youtput-only-tasty`, or when
460+
// tasty file compiled by `-Yearly-tasty-output-write` comes from an early output jar.
460461
report.inform(s"No classfiles found for $tastyFile when checking TASTy UUID")
461462

462463
private def mayLoadTreesFromTasty(using Context): Boolean =

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,9 @@ class Inliner(val call: tpd.Tree)(using Context):
10421042
for sym <- dependencies do
10431043
if ctx.compilationUnit.source.file == sym.associatedFile then
10441044
report.error(em"Cannot call macro $sym defined in the same source file", call.srcPos)
1045+
else if ctx.settings.YnoSuspendedUnits.value then
1046+
val addendum = ", suspension prevented by -Yno-suspended-units"
1047+
report.error(em"Cannot call macro $sym defined in the same compilation run$addendum", call.srcPos)
10451048
if (suspendable && ctx.settings.XprintSuspension.value)
10461049
report.echo(i"suspension triggered by macro call to ${sym.showLocated} in ${sym.associatedFile}", call.srcPos)
10471050
if suspendable then

compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import StdNames.str
1919
import NameOps.*
2020
import inlines.Inlines
2121
import transform.ValueClasses
22-
import dotty.tools.io.{File, FileExtension, JarArchive}
22+
import transform.Pickler
23+
import dotty.tools.io.{File, FileExtension, JarArchive, ClassfileWriterOps}
2324
import util.{Property, SourceFile}
2425
import java.io.PrintWriter
2526

@@ -51,7 +52,7 @@ class ExtractAPI extends Phase {
5152
override def description: String = ExtractAPI.description
5253

5354
override def isRunnable(using Context): Boolean = {
54-
super.isRunnable && ctx.runZincPhases
55+
super.isRunnable && (ctx.runZincPhases || ctx.settings.YjavaTasty.value)
5556
}
5657

5758
// Check no needed. Does not transform trees
@@ -65,16 +66,74 @@ class ExtractAPI extends Phase {
6566
// after `PostTyper` (unlike `ExtractDependencies`, the simplication to trees
6667
// done by `PostTyper` do not affect this phase because it only cares about
6768
// definitions, and `PostTyper` does not change definitions).
68-
override def runsAfter: Set[String] = Set(transform.PostTyper.name)
69+
override def runsAfter: Set[String] = Set(transform.Pickler.name)
6970

7071
override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] =
72+
val doZincCallback = ctx.runZincPhases
73+
val sigWriter: Option[Pickler.EarlyFileWriter] = ctx.settings.YearlyTastyOutput.value match
74+
case earlyOut if earlyOut.isDirectory && earlyOut.exists =>
75+
Some(Pickler.EarlyFileWriter(ClassfileWriterOps(earlyOut)))
76+
case _ =>
77+
None
7178
val nonLocalClassSymbols = new mutable.HashSet[Symbol]
72-
val ctx0 = ctx.withProperty(NonLocalClassSymbolsInCurrentUnits, Some(nonLocalClassSymbols))
73-
val units0 = super.runOn(units)(using ctx0)
74-
ctx.withIncCallback(recordNonLocalClasses(nonLocalClassSymbols, _))
75-
units0
79+
val units0 =
80+
if doZincCallback then
81+
val ctx0 = ctx.withProperty(NonLocalClassSymbolsInCurrentUnits, Some(nonLocalClassSymbols))
82+
super.runOn(units)(using ctx0)
83+
else
84+
units // still run the phase for the side effects (writing TASTy files to -Yearly-tasty-output)
85+
sigWriter.foreach(writeSigFiles(units0, _))
86+
if doZincCallback then
87+
ctx.withIncCallback(recordNonLocalClasses(nonLocalClassSymbols, _))
88+
if ctx.settings.YjavaTasty.value then
89+
units0.filterNot(_.typedAsJava) // remove java sources, this is the terminal phase when `-Yjava-tasty` is set
90+
else
91+
units0
7692
end runOn
7793

94+
// Why we only write to early output in the first run?
95+
// ===================================================
96+
// TL;DR the point of pipeline compilation is to start downstream projects early,
97+
// so we don't want to wait for suspended units to be compiled.
98+
//
99+
// But why is it safe to ignore suspended units?
100+
// If this project contains a transparent macro that is called in the same project,
101+
// the compilation unit of that call will be suspended (if the macro implementation
102+
// is also in this project), causing a second run.
103+
// However before we do that run, we will have already requested sbt to begin
104+
// early downstream compilation. This means that the suspended definitions will not
105+
// be visible in *early* downstream compilation.
106+
//
107+
// However, sbt will by default prevent downstream compilation happening in this scenario,
108+
// due to the existence of macro definitions. So we are protected from failure if user tries
109+
// to use the suspended definitions.
110+
//
111+
// Additionally, it is recommended for the user to move macro implementations to another project
112+
// if they want to force early output. In this scenario the suspensions will no longer occur, so now
113+
// they will become visible in the early-output.
114+
//
115+
// See `sbt-test/pipelining/pipelining-scala-macro` and `sbt-test/pipelining/pipelining-scala-macro-force`
116+
// for examples of this in action.
117+
//
118+
// Therefore we only need to write to early output in the first run. We also provide the option
119+
// to diagnose suspensions with the `-Yno-suspended-units` flag.
120+
private def writeSigFiles(units: List[CompilationUnit], writer: Pickler.EarlyFileWriter)(using Context): Unit = {
121+
try
122+
for
123+
unit <- units
124+
(cls, pickled) <- unit.pickled
125+
if cls.isDefinedInCurrentRun
126+
do
127+
val binaryName = cls.binaryClassName.replace('.', java.io.File.separatorChar).nn
128+
val binaryClassName = if (cls.is(Module)) binaryName.stripSuffix(str.MODULE_SUFFIX).nn else binaryName
129+
writer.writeTasty(binaryClassName, pickled())
130+
finally
131+
writer.close()
132+
if ctx.settings.verbose.value then
133+
report.echo("[sig files written]")
134+
end try
135+
}
136+
78137
private def recordNonLocalClasses(nonLocalClassSymbols: mutable.HashSet[Symbol], cb: interfaces.IncrementalCallback)(using Context): Unit =
79138
for cls <- nonLocalClassSymbols do
80139
val sourceFile = cls.source

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

Lines changed: 8 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class Pickler extends Phase {
4646

4747
// No need to repickle trees coming from TASTY
4848
override def isRunnable(using Context): Boolean =
49-
super.isRunnable && (!ctx.settings.fromTasty.value || ctx.settings.YjavaTasty.value)
49+
super.isRunnable && !ctx.settings.fromTasty.value
5050

5151
// when `-Yjava-tasty` is set we actually want to run this phase on Java sources
5252
override def skipIfJava(using Context): Boolean = false
@@ -83,9 +83,7 @@ class Pickler extends Phase {
8383

8484
private val executor = Executor[Array[Byte]]()
8585

86-
private def useExecutor(using Context) =
87-
Pickler.ParallelPickling && !ctx.settings.YtestPickler.value &&
88-
!ctx.settings.YjavaTasty.value // disable parallel pickling when `-Yjava-tasty` is set (internal testing only)
86+
private def useExecutor(using Context) = Pickler.ParallelPickling && !ctx.settings.YtestPickler.value
8987

9088
private def printerContext(isOutline: Boolean)(using Context): Context =
9189
if isOutline then ctx.fresh.setPrinterFn(OutlinePrinter(_))
@@ -182,22 +180,13 @@ class Pickler extends Phase {
182180
}
183181

184182
override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] = {
185-
val sigWriter: Option[Pickler.EarlyFileWriter] = ctx.settings.YjavaTastyOutput.value match
186-
case jar: JarArchive if jar.exists =>
187-
Some(Pickler.EarlyFileWriter(ClassfileWriterOps(jar)))
188-
case _ =>
189-
None
190-
val units0 =
191-
if ctx.settings.fromTasty.value then
192-
// we still run the phase for the side effect of writing the pipeline tasty files
193-
units
183+
val result =
184+
if useExecutor then
185+
executor.start()
186+
try super.runOn(units)
187+
finally executor.close()
194188
else
195-
if useExecutor then
196-
executor.start()
197-
try super.runOn(units)
198-
finally executor.close()
199-
else
200-
super.runOn(units)
189+
super.runOn(units)
201190
if ctx.settings.YtestPickler.value then
202191
val ctx2 = ctx.fresh
203192
.setSetting(ctx.settings.YreadComments, true)
@@ -208,34 +197,9 @@ class Pickler extends Phase {
208197
.setReporter(new ThrowingReporter(ctx.reporter))
209198
.addMode(Mode.ReadPositions)
210199
)
211-
val result =
212-
if ctx.settings.YjavaTasty.value then
213-
sigWriter.foreach(writeJavaSigFiles(units0, _))
214-
units0.filterNot(_.typedAsJava) // remove java sources, this is the terminal phase when `-Yjava-tasty` is set
215-
else
216-
units0
217200
result
218201
}
219202

220-
private def writeJavaSigFiles(units: List[CompilationUnit], writer: Pickler.EarlyFileWriter)(using Context): Unit = {
221-
var count = 0
222-
try
223-
for
224-
unit <- units if unit.typedAsJava
225-
(cls, pickled) <- unit.pickled
226-
if cls.isDefinedInCurrentRun
227-
do
228-
val binaryName = cls.binaryClassName.replace('.', java.io.File.separatorChar).nn
229-
val binaryClassName = if (cls.is(Module)) binaryName.stripSuffix(str.MODULE_SUFFIX).nn else binaryName
230-
writer.writeTasty(binaryClassName, pickled())
231-
count += 1
232-
finally
233-
writer.close()
234-
if ctx.settings.verbose.value then
235-
report.echo(s"[$count java sig files written]")
236-
end try
237-
}
238-
239203
private def testUnpickler(using Context): Unit =
240204
pickling.println(i"testing unpickler at run ${ctx.runId}")
241205
ctx.initialize()

0 commit comments

Comments
 (0)