Skip to content

Commit a7f00e2

Browse files
authored
Add support for Pipelined builds (#18880)
This includes support for a single pass pipelined build, compatible with sbt's `ThisBuild/usePipelining`, - adds `-Ypickle-java` and `-Ypickle-write` flags, expected by Zinc when pipelining is enabled in sbt. - when `-Ypickle-write <directory|jar>` is set, then write tasty from pickler to that output, (building upon #19074 support for Java signatures in TASTy files). - call `apiPhaseCompleted` and `dependencyPhaseCompleted` callbacks, which will activate early downstream compilation - calls `generatedNonLocalClass` callbacks early, which enables Zinc to run the incremental algorithm before starting downstream compilation (including checking for macro definitions). generally this can be reviewed commit-by-commit, as they each do an isolated feature. As well as many tests in the `sbt-test/pipelining` directory, this has also been tested locally on `akka/akka-http`, `apache/incubator-pekko`, `lichess-org/lila`, `scalacenter/scaladex`, `typelevel/fs2`, `typelevel/http4s`, `typelevel/cats`, `slick/slick`. This PR sets the ground work for an optional 2-pass compile (reusing the `OUTLINEattr`), which should use a faster frontend (skipping rhs when possible) before producing tasty signatures fixes #19743
2 parents fd2a03e + c19b67e commit a7f00e2

File tree

118 files changed

+1193
-307
lines changed

Some content is hidden

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

118 files changed

+1193
-307
lines changed

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,17 +125,16 @@ class CodeGen(val int: DottyBackendInterface, val primitives: DottyPrimitives)(
125125

126126
// Creates a callback that will be evaluated in PostProcessor after creating a file
127127
private def onFileCreated(cls: ClassNode, claszSymbol: Symbol, sourceFile: util.SourceFile)(using Context): AbstractFile => Unit = {
128-
val (fullClassName, isLocal) = atPhase(sbtExtractDependenciesPhase) {
129-
(ExtractDependencies.classNameAsString(claszSymbol), claszSymbol.isLocal)
128+
val isLocal = atPhase(sbtExtractDependenciesPhase) {
129+
claszSymbol.isLocal
130130
}
131131
clsFile => {
132132
val className = cls.name.replace('/', '.')
133133
if (ctx.compilerCallback != null)
134134
ctx.compilerCallback.onClassGenerated(sourceFile, convertAbstractFile(clsFile), className)
135135

136-
ctx.withIncCallback: cb =>
137-
if (isLocal) cb.generatedLocalClass(sourceFile, clsFile.jpath)
138-
else cb.generatedNonLocalClass(sourceFile, clsFile.jpath, className, fullClassName)
136+
if isLocal then
137+
ctx.withIncCallback(_.generatedLocalClass(sourceFile, clsFile.jpath))
139138
}
140139
}
141140

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

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,16 @@ class CompilationUnit protected (val source: SourceFile, val info: CompilationUn
2828
var tpdTree: tpd.Tree = tpd.EmptyTree
2929

3030
/** Is this the compilation unit of a Java file */
31-
def isJava: Boolean = source.file.name.endsWith(".java")
31+
def isJava: Boolean = source.file.ext.isJava
3232

3333
/** Is this the compilation unit of a Java file, or TASTy derived from a Java file */
34-
def typedAsJava = isJava || {
35-
val infoNN = info
36-
infoNN != null && infoNN.tastyInfo.exists(_.attributes.isJava)
37-
}
34+
def typedAsJava =
35+
val ext = source.file.ext
36+
ext.isJavaOrTasty && (ext.isJava || tastyInfo.exists(_.attributes.isJava))
37+
38+
def tastyInfo: Option[TastyInfo] =
39+
val local = info
40+
if local == null then None else local.tastyInfo
3841

3942

4043
/** The source version for this unit, as determined by a language import */
@@ -94,12 +97,15 @@ class CompilationUnit protected (val source: SourceFile, val info: CompilationUn
9497
// when this unit is unsuspended.
9598
depRecorder.clear()
9699
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
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
103109
throw CompilationUnit.SuspendException()
104110

105111
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/Driver.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import core.Comments.{ContextDoc, ContextDocstrings}
66
import core.Contexts.*
77
import core.{MacroClassLoader, TypeError}
88
import dotty.tools.dotc.ast.Positioned
9-
import dotty.tools.io.AbstractFile
9+
import dotty.tools.io.{AbstractFile, FileExtension}
1010
import reporting.*
1111
import core.Decorators.*
1212
import config.Feature
@@ -97,9 +97,9 @@ class Driver {
9797
if !file.exists then
9898
report.error(em"File does not exist: ${file.path}")
9999
None
100-
else file.extension match
101-
case "jar" => Some(file.path)
102-
case "tasty" =>
100+
else file.ext match
101+
case FileExtension.Jar => Some(file.path)
102+
case FileExtension.Tasty =>
103103
TastyFileUtil.getClassPath(file) match
104104
case Some(classpath) => Some(classpath)
105105
case _ =>

compiler/src/dotty/tools/dotc/classpath/AggregateClassPath.scala

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,25 +33,6 @@ case class AggregateClassPath(aggregates: Seq[ClassPath]) extends ClassPath {
3333
packageIndex.getOrElseUpdate(pkg.dottedString, aggregates.filter(_.hasPackage(pkg)))
3434
}
3535

36-
override def findClass(className: String): Option[ClassRepresentation] = {
37-
val (pkg, _) = PackageNameUtils.separatePkgAndClassNames(className)
38-
39-
def findEntry(isSource: Boolean): Option[ClassRepresentation] =
40-
aggregatesForPackage(PackageName(pkg)).iterator.map(_.findClass(className)).collectFirst {
41-
case Some(s: SourceFileEntry) if isSource => s
42-
case Some(s: BinaryFileEntry) if !isSource => s
43-
}
44-
45-
val classEntry = findEntry(isSource = false)
46-
val sourceEntry = findEntry(isSource = true)
47-
48-
(classEntry, sourceEntry) match {
49-
case (Some(c: BinaryFileEntry), Some(s: SourceFileEntry)) => Some(BinaryAndSourceFilesEntry(c, s))
50-
case (c @ Some(_), _) => c
51-
case (_, s) => s
52-
}
53-
}
54-
5536
override def asURLs: Seq[URL] = aggregates.flatMap(_.asURLs)
5637

5738
override def asClassPathStrings: Seq[String] = aggregates.map(_.asClassPathString).distinct

compiler/src/dotty/tools/dotc/classpath/ClassPath.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package dotty.tools.dotc.classpath
66
import dotty.tools.dotc.classpath.FileUtils.isTasty
77
import dotty.tools.io.AbstractFile
88
import dotty.tools.io.ClassRepresentation
9+
import dotty.tools.io.FileExtension
910

1011
case class ClassPathEntries(packages: scala.collection.Seq[PackageEntry], classesAndSources: scala.collection.Seq[ClassRepresentation]) {
1112
def toTuple: (scala.collection.Seq[PackageEntry], scala.collection.Seq[ClassRepresentation]) = (packages, classesAndSources)
@@ -52,7 +53,7 @@ sealed trait BinaryFileEntry extends ClassRepresentation {
5253
object BinaryFileEntry {
5354
def apply(file: AbstractFile): BinaryFileEntry =
5455
if file.isTasty then
55-
if file.resolveSiblingWithExtension("class") != null then TastyWithClassFileEntry(file)
56+
if file.resolveSiblingWithExtension(FileExtension.Class) != null then TastyWithClassFileEntry(file)
5657
else StandaloneTastyFileEntry(file)
5758
else
5859
ClassFileEntry(file)

compiler/src/dotty/tools/dotc/classpath/DirectoryClassPath.scala

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -274,23 +274,18 @@ final class CtSymClassPath(ctSym: java.nio.file.Path, release: Int) extends Clas
274274
}
275275

276276
case class DirectoryClassPath(dir: JFile) extends JFileDirectoryLookup[BinaryFileEntry] with NoSourcePaths {
277-
override def findClass(className: String): Option[ClassRepresentation] =
278-
findClassFile(className).map(BinaryFileEntry(_))
279277

280278
def findClassFile(className: String): Option[AbstractFile] = {
281279
val relativePath = FileUtils.dirPath(className)
282-
val tastyFile = new JFile(dir, relativePath + ".tasty")
283-
if tastyFile.exists then Some(tastyFile.toPath.toPlainFile)
284-
else
285-
val classFile = new JFile(dir, relativePath + ".class")
286-
if classFile.exists then Some(classFile.toPath.toPlainFile)
287-
else None
280+
val classFile = new JFile(dir, relativePath + ".class")
281+
if classFile.exists then Some(classFile.toPath.toPlainFile)
282+
else None
288283
}
289284

290285
protected def createFileEntry(file: AbstractFile): BinaryFileEntry = BinaryFileEntry(file)
291286

292287
protected def isMatchingFile(f: JFile): Boolean =
293-
f.isTasty || (f.isClass && f.classToTasty.isEmpty)
288+
f.isTasty || (f.isClass && !f.hasSiblingTasty)
294289

295290
private[dotty] def classes(inPackage: PackageName): Seq[BinaryFileEntry] = files(inPackage)
296291
}
@@ -301,16 +296,5 @@ case class DirectorySourcePath(dir: JFile) extends JFileDirectoryLookup[SourceFi
301296
protected def createFileEntry(file: AbstractFile): SourceFileEntry = SourceFileEntry(file)
302297
protected def isMatchingFile(f: JFile): Boolean = endsScalaOrJava(f.getName)
303298

304-
override def findClass(className: String): Option[ClassRepresentation] = findSourceFile(className).map(SourceFileEntry(_))
305-
306-
private def findSourceFile(className: String): Option[AbstractFile] = {
307-
val relativePath = FileUtils.dirPath(className)
308-
val sourceFile = LazyList("scala", "java")
309-
.map(ext => new JFile(dir, relativePath + "." + ext))
310-
.collectFirst { case file if file.exists() => file }
311-
312-
sourceFile.map(_.toPath.toPlainFile)
313-
}
314-
315299
private[dotty] def sources(inPackage: PackageName): Seq[SourceFileEntry] = files(inPackage)
316300
}

compiler/src/dotty/tools/dotc/classpath/FileUtils.scala

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,49 +17,52 @@ object FileUtils {
1717
extension (file: AbstractFile) {
1818
def isPackage: Boolean = file.isDirectory && mayBeValidPackage(file.name)
1919

20-
def isClass: Boolean = !file.isDirectory && hasClassExtension && !file.name.endsWith("$class.class")
21-
// FIXME: drop last condition when we stop being compatible with Scala 2.11
20+
def isClass: Boolean = !file.isDirectory && hasClassExtension
2221

23-
def hasClassExtension: Boolean = file.hasExtension("class")
22+
def hasClassExtension: Boolean = file.ext.isClass
2423

25-
def hasTastyExtension: Boolean = file.hasExtension("tasty")
24+
def hasTastyExtension: Boolean = file.ext.isTasty
2625

2726
def isTasty: Boolean = !file.isDirectory && hasTastyExtension
2827

2928
def isScalaBinary: Boolean = file.isClass || file.isTasty
3029

31-
def isScalaOrJavaSource: Boolean = !file.isDirectory && (file.hasExtension("scala") || file.hasExtension("java"))
30+
def isScalaOrJavaSource: Boolean = !file.isDirectory && file.ext.isScalaOrJava
3231

3332
// TODO do we need to check also other files using ZipMagicNumber like in scala.tools.nsc.io.Jar.isJarOrZip?
34-
def isJarOrZip: Boolean = file.hasExtension("jar") || file.hasExtension("zip")
33+
def isJarOrZip: Boolean = file.ext.isJarOrZip
3534

3635
/**
3736
* Safe method returning a sequence containing one URL representing this file, when underlying file exists,
3837
* and returning given default value in other case
3938
*/
4039
def toURLs(default: => Seq[URL] = Seq.empty): Seq[URL] = if (file.file == null) default else Seq(file.toURL)
4140

42-
/** Returns the tasty file associated with this class file */
43-
def classToTasty: Option[AbstractFile] =
44-
assert(file.isClass, s"non-class: $file")
45-
val tastyName = classNameToTasty(file.name)
46-
Option(file.resolveSibling(tastyName))
41+
/**
42+
* Returns if there is an existing sibling `.tasty` file.
43+
*/
44+
def hasSiblingTasty: Boolean =
45+
assert(file.hasClassExtension, s"non-class: $file")
46+
file.resolveSibling(classNameToTasty(file.name)) != null
4747
}
4848

4949
extension (file: JFile) {
5050
def isPackage: Boolean = file.isDirectory && mayBeValidPackage(file.getName)
5151

52-
def isClass: Boolean = file.isFile && file.getName.endsWith(SUFFIX_CLASS) && !file.getName.endsWith("$class.class")
53-
// FIXME: drop last condition when we stop being compatible with Scala 2.11
52+
def isClass: Boolean = file.isFile && hasClassExtension
53+
54+
def hasClassExtension: Boolean = file.getName.endsWith(SUFFIX_CLASS)
5455

5556
def isTasty: Boolean = file.isFile && file.getName.endsWith(SUFFIX_TASTY)
5657

57-
/** Returns the tasty file associated with this class file */
58-
def classToTasty: Option[JFile] =
59-
assert(file.isClass, s"non-class: $file")
60-
val tastyName = classNameToTasty(file.getName.stripSuffix(".class"))
61-
val tastyPath = file.toPath.resolveSibling(tastyName)
62-
if java.nio.file.Files.exists(tastyPath) then Some(tastyPath.toFile) else None
58+
/**
59+
* Returns if there is an existing sibling `.tasty` file.
60+
*/
61+
def hasSiblingTasty: Boolean =
62+
assert(file.hasClassExtension, s"non-class: $file")
63+
val path = file.toPath
64+
val tastyPath = path.resolveSibling(classNameToTasty(file.getName))
65+
java.nio.file.Files.exists(tastyPath)
6366

6467
}
6568

compiler/src/dotty/tools/dotc/classpath/VirtualDirectoryClassPath.scala

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,18 @@ case class VirtualDirectoryClassPath(dir: VirtualDirectory) extends ClassPath wi
3838
def asURLs: Seq[URL] = Seq(new URI(dir.name).toURL)
3939
def asClassPathStrings: Seq[String] = Seq(dir.path)
4040

41-
override def findClass(className: String): Option[ClassRepresentation] =
42-
findClassFile(className).map(BinaryFileEntry(_))
43-
4441
def findClassFile(className: String): Option[AbstractFile] = {
4542
val pathSeq = FileUtils.dirPath(className).split(java.io.File.separator)
4643
val parentDir = lookupPath(dir)(pathSeq.init.toSeq, directory = true)
47-
if parentDir == null then return None
44+
if parentDir == null then None
4845
else
49-
Option(lookupPath(parentDir)(pathSeq.last + ".tasty" :: Nil, directory = false))
50-
.orElse(Option(lookupPath(parentDir)(pathSeq.last + ".class" :: Nil, directory = false)))
46+
Option(lookupPath(parentDir)(pathSeq.last + ".class" :: Nil, directory = false))
5147
}
5248

5349
private[dotty] def classes(inPackage: PackageName): Seq[BinaryFileEntry] = files(inPackage)
5450

5551
protected def createFileEntry(file: AbstractFile): BinaryFileEntry = BinaryFileEntry(file)
5652

5753
protected def isMatchingFile(f: AbstractFile): Boolean =
58-
f.isTasty || (f.isClass && f.classToTasty.isEmpty)
54+
f.isTasty || (f.isClass && !f.hasSiblingTasty)
5955
}

compiler/src/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactory.scala

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,15 @@ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory {
4545
with NoSourcePaths {
4646

4747
override def findClassFile(className: String): Option[AbstractFile] =
48-
findClass(className).map(_.file)
49-
50-
// This method is performance sensitive as it is used by SBT's ExtractDependencies phase.
51-
override def findClass(className: String): Option[BinaryFileEntry] = {
5248
val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className)
53-
val binaries = files(PackageName(pkg), simpleClassName + ".tasty", simpleClassName + ".class")
54-
binaries.find(_.file.isTasty).orElse(binaries.find(_.file.isClass))
55-
}
49+
file(PackageName(pkg), simpleClassName + ".class").map(_.file)
5650

5751
override private[dotty] def classes(inPackage: PackageName): Seq[BinaryFileEntry] = files(inPackage)
5852

5953
override protected def createFileEntry(file: FileZipArchive#Entry): BinaryFileEntry = BinaryFileEntry(file)
6054

6155
override protected def isRequiredFileType(file: AbstractFile): Boolean =
62-
file.isTasty || (file.isClass && file.classToTasty.isEmpty)
56+
file.isTasty || (file.isClass && !file.hasSiblingTasty)
6357
}
6458

6559
/**

compiler/src/dotty/tools/dotc/classpath/ZipArchiveFileLookup.scala

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,6 @@ trait ZipArchiveFileLookup[FileEntryType <: ClassRepresentation] extends Efficie
4343
}
4444
yield createFileEntry(entry)
4545

46-
protected def files(inPackage: PackageName, names: String*): Seq[FileEntryType] =
47-
for {
48-
dirEntry <- findDirEntry(inPackage).toSeq
49-
name <- names
50-
entry <- Option(dirEntry.lookupName(name, directory = false))
51-
if isRequiredFileType(entry)
52-
}
53-
yield createFileEntry(entry)
54-
5546
protected def file(inPackage: PackageName, name: String): Option[FileEntryType] =
5647
for {
5748
dirEntry <- findDirEntry(inPackage)

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ object ScalaSettings extends ScalaSettings
3131

3232
// Kept as seperate type to avoid breaking backward compatibility
3333
abstract class ScalaSettings extends SettingGroup, AllScalaSettings:
34-
val settingsByCategory: Map[SettingCategory, List[Setting[_]]] =
34+
val settingsByCategory: Map[SettingCategory, List[Setting[_]]] =
3535
allSettings.groupBy(_.category)
3636
.view.mapValues(_.toList).toMap
3737
.withDefaultValue(Nil)
@@ -43,7 +43,7 @@ abstract class ScalaSettings extends SettingGroup, AllScalaSettings:
4343
val verboseSettings: List[Setting[_]] = settingsByCategory(VerboseSetting).sortBy(_.name)
4444
val settingsByAliases: Map[String, Setting[_]] = allSettings.flatMap(s => s.aliases.map(_ -> s)).toMap
4545

46-
46+
4747
trait AllScalaSettings extends CommonScalaSettings, PluginSettings, VerboseSettings, WarningSettings, XSettings, YSettings:
4848
self: SettingGroup =>
4949

@@ -380,6 +380,7 @@ private sealed trait YSettings:
380380
val YprintPos: Setting[Boolean] = BooleanSetting(ForkSetting, "Yprint-pos", "Show tree positions.")
381381
val YprintPosSyms: Setting[Boolean] = BooleanSetting(ForkSetting, "Yprint-pos-syms", "Show symbol definitions positions.")
382382
val YnoDeepSubtypes: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-deep-subtypes", "Throw an exception on deep subtyping call stacks.")
383+
val YnoSuspendedUnits: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-suspended-units", "Do not suspend units, e.g. when calling a macro defined in the same run. This will error instead of suspending.")
383384
val YnoPatmatOpt: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-patmat-opt", "Disable all pattern matching optimizations.")
384385
val YplainPrinter: Setting[Boolean] = BooleanSetting(ForkSetting, "Yplain-printer", "Pretty-print using a plain printer.")
385386
val YprintSyms: Setting[Boolean] = BooleanSetting(ForkSetting, "Yprint-syms", "When printing trees print info in symbols instead of corresponding info in trees.")
@@ -439,7 +440,7 @@ private sealed trait YSettings:
439440
val YdebugMacros: Setting[Boolean] = BooleanSetting(ForkSetting, "Ydebug-macros", "Show debug info when quote pattern match fails")
440441

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

0 commit comments

Comments
 (0)