Skip to content

Commit 83cf8b7

Browse files
committed
add support for pipeline build of Scala and Java files
- add '-Ypickle-java' and '-Ypickle-write' flags 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. - 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 889077a commit 83cf8b7

File tree

64 files changed

+749
-91
lines changed

Some content is hidden

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

64 files changed

+749
-91
lines changed

Diff for: compiler/src/dotty/tools/dotc/CompilationUnit.scala

+9-6
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,15 @@ class CompilationUnit protected (val source: SourceFile, val info: CompilationUn
9494
// when this unit is unsuspended.
9595
depRecorder.clear()
9696
if !suspended then
97-
if (ctx.settings.XprintSuspension.value)
98-
report.echo(i"suspended: $this")
99-
suspended = true
100-
ctx.run.nn.suspendedUnits += this
101-
if ctx.phase == Phases.inliningPhase then
102-
suspendedAtInliningPhase = true
97+
if ctx.settings.YnoSuspendedUnits.value then
98+
report.error(i"Compilation unit suspended $this (-Yno-suspended-units is set)")
99+
else
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
103106
throw CompilationUnit.SuspendException()
104107

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

Diff for: compiler/src/dotty/tools/dotc/Compiler.scala

+1-1
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

Diff for: compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ private sealed trait YSettings:
374374
val YprintPos: Setting[Boolean] = BooleanSetting("-Yprint-pos", "Show tree positions.")
375375
val YprintPosSyms: Setting[Boolean] = BooleanSetting("-Yprint-pos-syms", "Show symbol definitions positions.")
376376
val YnoDeepSubtypes: Setting[Boolean] = BooleanSetting("-Yno-deep-subtypes", "Throw an exception on deep subtyping call stacks.")
377+
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.")
377378
val YnoPatmatOpt: Setting[Boolean] = BooleanSetting("-Yno-patmat-opt", "Disable all pattern matching optimizations.")
378379
val YplainPrinter: Setting[Boolean] = BooleanSetting("-Yplain-printer", "Pretty-print using a plain printer.")
379380
val YprintSyms: Setting[Boolean] = BooleanSetting("-Yprint-syms", "When printing trees print info in symbols instead of corresponding info in trees.")
@@ -435,7 +436,7 @@ private sealed trait YSettings:
435436
val YdebugMacros: Setting[Boolean] = BooleanSetting("-Ydebug-macros", "Show debug info when quote pattern match fails")
436437

437438
// Pipeline compilation options
438-
val YjavaTasty: Setting[Boolean] = BooleanSetting("-Yjava-tasty", "Pickler phase should compute pickles for .java defined symbols for use by build tools")
439-
val YjavaTastyOutput: Setting[AbstractFile] = OutputSetting("-Yjava-tasty-output", "directory|jar", "(Internal use only!) destination for generated .tasty files containing Java type signatures.", NoAbstractFile)
439+
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"))
440+
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"))
440441
val YallowOutlineFromTasty: Setting[Boolean] = BooleanSetting("-Yallow-outline-from-tasty", "Allow outline TASTy to be loaded with the -from-tasty option.")
441442
end YSettings

Diff for: compiler/src/dotty/tools/dotc/config/Settings.scala

+2-2
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))

Diff for: compiler/src/dotty/tools/dotc/core/Definitions.scala

+1
Original file line numberDiff line numberDiff line change
@@ -1062,6 +1062,7 @@ class Definitions {
10621062
@tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName")
10631063

10641064
@tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable")
1065+
@tu lazy val JavaAnnotationAnnot: ClassSymbol = requiredClass("java.lang.annotation.Annotation")
10651066

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

Diff for: compiler/src/dotty/tools/dotc/core/Phases.scala

+5-1
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])
@@ -343,7 +346,7 @@ object Phases {
343346
def runOn(units: List[CompilationUnit])(using runCtx: Context): List[CompilationUnit] =
344347
val buf = List.newBuilder[CompilationUnit]
345348
// factor out typedAsJava check when not needed
346-
val doSkipJava = ctx.settings.YjavaTasty.value && this <= picklerPhase && skipIfJava
349+
val doSkipJava = ctx.settings.YjavaTasty.value && this <= sbtExtractAPIPhase && skipIfJava
347350
for unit <- units do
348351
given unitCtx: Context = runCtx.fresh.setPhase(this.start).setCompilationUnit(unit).withRootImports
349352
if ctx.run.enterUnit(unit) then
@@ -503,6 +506,7 @@ object Phases {
503506
def typerPhase(using Context): Phase = ctx.base.typerPhase
504507
def postTyperPhase(using Context): Phase = ctx.base.postTyperPhase
505508
def sbtExtractDependenciesPhase(using Context): Phase = ctx.base.sbtExtractDependenciesPhase
509+
def sbtExtractAPIPhase(using Context): Phase = ctx.base.sbtExtractAPIPhase
506510
def picklerPhase(using Context): Phase = ctx.base.picklerPhase
507511
def inliningPhase(using Context): Phase = ctx.base.inliningPhase
508512
def stagingPhase(using Context): Phase = ctx.base.stagingPhase

Diff for: compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,8 @@ class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader {
467467
val tastyUUID = unpickler.unpickler.header.uuid
468468
new ClassfileTastyUUIDParser(classfile)(ctx).checkTastyUUID(tastyUUID)
469469
else
470-
// This will be the case in any of our tests that compile with `-Youtput-only-tasty`
470+
// This will be the case in any of our tests that compile with `-Youtput-only-tasty`, or when
471+
// tasty file compiled by `-Yearly-tasty-output-write` comes from an early output jar.
471472
report.inform(s"No classfiles found for $tastyFile when checking TASTy UUID")
472473

473474
private def mayLoadTreesFromTasty(using Context): Boolean =

Diff for: compiler/src/dotty/tools/dotc/inlines/Inliner.scala

+3
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

Diff for: compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala

+56-3
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

@@ -65,16 +66,68 @@ 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 sigWriter: Option[Pickler.EarlyFileWriter] = ctx.settings.YearlyTastyOutput.value match
73+
case earlyOut if earlyOut.isDirectory && earlyOut.exists =>
74+
Some(Pickler.EarlyFileWriter(ClassfileWriterOps(earlyOut)))
75+
case _ =>
76+
None
7177
val nonLocalClassSymbols = new mutable.HashSet[Symbol]
7278
val ctx0 = ctx.withProperty(NonLocalClassSymbolsInCurrentUnits, Some(nonLocalClassSymbols))
7379
val units0 = super.runOn(units)(using ctx0)
80+
sigWriter.foreach(writeSigFiles(units0, _))
7481
ctx.withIncCallback(recordNonLocalClasses(nonLocalClassSymbols, _))
75-
units0
82+
if ctx.settings.YjavaTasty.value then
83+
units0.filterNot(_.typedAsJava) // remove java sources, this is the terminal phase when `-Yjava-tasty` is set
84+
else
85+
units0
7686
end runOn
7787

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

Diff for: compiler/src/dotty/tools/dotc/transform/Pickler.scala

+8-44
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class Pickler extends Phase {
4545

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

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

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

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

8987
override def run(using Context): Unit = {
9088
val unit = ctx.compilationUnit
@@ -172,22 +170,13 @@ class Pickler extends Phase {
172170
}
173171

174172
override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] = {
175-
val sigWriter: Option[Pickler.EarlyFileWriter] = ctx.settings.YjavaTastyOutput.value match
176-
case jar: JarArchive if jar.exists =>
177-
Some(Pickler.EarlyFileWriter(ClassfileWriterOps(jar)))
178-
case _ =>
179-
None
180-
val units0 =
181-
if ctx.settings.fromTasty.value then
182-
// we still run the phase for the side effect of writing the pipeline tasty files
183-
units
173+
val result =
174+
if useExecutor then
175+
executor.start()
176+
try super.runOn(units)
177+
finally executor.close()
184178
else
185-
if useExecutor then
186-
executor.start()
187-
try super.runOn(units)
188-
finally executor.close()
189-
else
190-
super.runOn(units)
179+
super.runOn(units)
191180
if ctx.settings.YtestPickler.value then
192181
val ctx2 = ctx.fresh
193182
.setSetting(ctx.settings.YreadComments, true)
@@ -198,34 +187,9 @@ class Pickler extends Phase {
198187
.setReporter(new ThrowingReporter(ctx.reporter))
199188
.addMode(Mode.ReadPositions)
200189
)
201-
val result =
202-
if ctx.settings.YjavaTasty.value then
203-
sigWriter.foreach(writeJavaSigFiles(units0, _))
204-
units0.filterNot(_.typedAsJava) // remove java sources, this is the terminal phase when `-Yjava-tasty` is set
205-
else
206-
units0
207190
result
208191
}
209192

210-
private def writeJavaSigFiles(units: List[CompilationUnit], writer: Pickler.EarlyFileWriter)(using Context): Unit = {
211-
var count = 0
212-
try
213-
for
214-
unit <- units if unit.typedAsJava
215-
(cls, pickled) <- unit.pickled
216-
if cls.isDefinedInCurrentRun
217-
do
218-
val binaryName = cls.binaryClassName.replace('.', java.io.File.separatorChar).nn
219-
val binaryClassName = if (cls.is(Module)) binaryName.stripSuffix(str.MODULE_SUFFIX).nn else binaryName
220-
writer.writeTasty(binaryClassName, pickled())
221-
count += 1
222-
finally
223-
writer.close()
224-
if ctx.settings.verbose.value then
225-
report.echo(s"[$count java sig files written]")
226-
end try
227-
}
228-
229193
private def testUnpickler(using Context): Unit =
230194
pickling.println(i"testing unpickler at run ${ctx.runId}")
231195
ctx.initialize()

0 commit comments

Comments
 (0)