Skip to content

Commit 1590d2b

Browse files
committed
Introduce best-effort compilation for IDEs
2 new experimental options are introduces for the compiler: `-Ybest-effort-dir` and `-Ywith-best-effort-tasty`. A related Best Effort TASTy format, a TASTy aligned file format able to hold some errored trees. Behaviour of the options and the format is documented as part of this commit in the `best-effort-compilation.md` docs file.
1 parent 62479c9 commit 1590d2b

Some content is hidden

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

41 files changed

+817
-163
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ class GenBCode extends Phase { self =>
1616

1717
override def description: String = GenBCode.description
1818

19+
override def isRunnable(using Context) = super.isRunnable && !ctx.usesBestEffortTasty
20+
1921
private val superCallsMap = new MutableSymbolMap[Set[ClassSymbol]]
2022
def registerSuperCall(sym: Symbol, calls: ClassSymbol): Unit = {
2123
val old = superCallsMap.getOrElse(sym, Set.empty)

compiler/src/dotty/tools/backend/sjs/GenSJSIR.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class GenSJSIR extends Phase {
1212
override def description: String = GenSJSIR.description
1313

1414
override def isRunnable(using Context): Boolean =
15-
super.isRunnable && ctx.settings.scalajs.value
15+
super.isRunnable && ctx.settings.scalajs.value && !ctx.usesBestEffortTasty
1616

1717
def run(using Context): Unit =
1818
new JSCodeGen().run()

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ class Driver {
100100
None
101101
else file.extension match
102102
case "jar" => Some(file.path)
103-
case "tasty" =>
104-
TastyFileUtil.getClassPath(file) match
103+
case "tasty" | "betasty" =>
104+
TastyFileUtil.getClassPath(file, ctx.withBestEffortTasty) match
105105
case Some(classpath) => Some(classpath)
106106
case _ =>
107107
report.error(em"Could not load classname from: ${file.path}")

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,13 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
225225
if (ctx.settings.YtestPickler.value) List("pickler")
226226
else ctx.settings.YstopAfter.value
227227

228+
var forceReachPhaseMaybe =
229+
if (ctx.isBestEffort) Some("typer")
230+
else None
231+
232+
var reachedSemanticDB = false
233+
var reachedPickler = false
234+
228235
val pluginPlan = ctx.base.addPluginPhases(ctx.base.phasePlan)
229236
val phases = ctx.base.fusePhases(pluginPlan,
230237
ctx.settings.Yskip.value, ctx.settings.YstopBefore.value, stopAfter, ctx.settings.Ycheck.value)
@@ -239,7 +246,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
239246
var phasesWereAdjusted = false
240247

241248
for (phase <- ctx.base.allPhases)
242-
if (phase.isRunnable)
249+
if (phase.isRunnable || forceReachPhaseMaybe.nonEmpty)
243250
Stats.trackTime(s"$phase ms ") {
244251
val start = System.currentTimeMillis
245252
val profileBefore = profiler.beforePhase(phase)
@@ -249,6 +256,24 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
249256
for (unit <- units)
250257
lastPrintedTree =
251258
printTree(lastPrintedTree)(using ctx.fresh.setPhase(phase.next).setCompilationUnit(unit))
259+
260+
forceReachPhaseMaybe match {
261+
case Some(forceReachPhase) if phase.phaseName == forceReachPhase =>
262+
forceReachPhaseMaybe = None
263+
case _ =>
264+
}
265+
266+
if phase.phaseName == "extractSemanticDB" then reachedSemanticDB = true
267+
if phase.phaseName == "pickler" then reachedPickler = true
268+
269+
if !reachedSemanticDB && forceReachPhaseMaybe.isEmpty && ctx.reporter.hasErrors && ctx.isBestEffort then
270+
ctx.base.allPhases.find(_.phaseName == "extractSemanticDB").foreach(_.runOn(units))
271+
reachedSemanticDB = true
272+
273+
if !reachedPickler && forceReachPhaseMaybe.isEmpty && ctx.reporter.hasErrors && ctx.isBestEffort then
274+
ctx.base.allPhases.find(_.phaseName == "pickler").foreach(_.runOn(units))
275+
reachedPickler = true
276+
252277
report.informTime(s"$phase ", start)
253278
Stats.record(s"total trees at end of $phase", ast.Trees.ntrees)
254279
for (unit <- units)

compiler/src/dotty/tools/dotc/ast/TreeInfo.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -886,12 +886,12 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
886886
else cpy.PackageDef(tree)(pid, slicedStats) :: Nil
887887
case tdef: TypeDef =>
888888
val sym = tdef.symbol
889-
assert(sym.isClass)
889+
if !ctx.isBestEffort then assert(sym.isClass)
890890
if (cls == sym || cls == sym.linkedClass) tdef :: Nil
891891
else Nil
892892
case vdef: ValDef =>
893893
val sym = vdef.symbol
894-
assert(sym.is(Module))
894+
if !ctx.isBestEffort then assert(sym.is(Module))
895895
if (cls == sym.companionClass || cls == sym.moduleClass) vdef :: Nil
896896
else Nil
897897
case tree =>

compiler/src/dotty/tools/dotc/ast/tpd.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
4848
case _: RefTree | _: GenericApply | _: Inlined | _: Hole =>
4949
ta.assignType(untpd.Apply(fn, args), fn, args)
5050
case _ =>
51-
assert(ctx.reporter.errorsReported)
51+
assert(ctx.isBestEffort || ctx.usesBestEffortTasty || ctx.reporter.errorsReported)
5252
ta.assignType(untpd.Apply(fn, args), fn, args)
5353

5454
def TypeApply(fn: Tree, args: List[Tree])(using Context): TypeApply = fn match
@@ -57,7 +57,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
5757
case _: RefTree | _: GenericApply =>
5858
ta.assignType(untpd.TypeApply(fn, args), fn, args)
5959
case _ =>
60-
assert(ctx.reporter.errorsReported)
60+
assert(ctx.isBestEffort || ctx.usesBestEffortTasty || ctx.reporter.errorsReported)
6161
ta.assignType(untpd.TypeApply(fn, args), fn, args)
6262

6363
def Literal(const: Constant)(using Context): Literal =

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ case class DirectoryClassPath(dir: JFile) extends JFileDirectoryLookup[ClassFile
286286
}
287287

288288
protected def createFileEntry(file: AbstractFile): ClassFileEntryImpl = ClassFileEntryImpl(file)
289-
protected def isMatchingFile(f: JFile): Boolean = f.isClass
289+
protected def isMatchingFile(f: JFile): Boolean = f.isClass || f.isBestEffortTasty
290290

291291
private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] = files(inPackage)
292292
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ object FileUtils {
3737

3838
def isClass: Boolean = file.isFile && file.getName.endsWith(".class") && !file.getName.endsWith("$class.class")
3939
// FIXME: drop last condition when we stop being compatible with Scala 2.11
40+
41+
def isTasty: Boolean = file.isFile && file.getName.endsWith(".tasty")
42+
43+
def isBestEffortTasty: Boolean = file.isFile && file.getName.endsWith(".betasty")
4044
}
4145

4246
private val SUFFIX_CLASS = ".class"

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,9 @@ private sealed trait YSettings:
376376
//.withPostSetHook( _ => YprofileEnabled.value = true )
377377
val YprofileRunGcBetweenPhases: Setting[List[String]] = PhasesSetting("-Yprofile-run-gc", "Run a GC between phases - this allows heap size to be accurate at the expense of more time. Specify a list of phases, or *", "_")
378378
//.withPostSetHook( _ => YprofileEnabled.value = true )
379+
380+
val YbestEffortDir: Setting[String] = StringSetting("-Ybest-effort-dir", "dir", "Enable best-effort compilation attempting to produce tasty in case of failure to specified directory, as part of the pickler phase.", "")
381+
val YwithBestEffortTasty: Setting[Boolean] = BooleanSetting("-Ywith-best-effort-tasty", "Allow to compile from a best effort tasty files. If such file is used, the compiler will stop after the pickler phase.")
379382

380383
// Experimental language features
381384
val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting("-Yno-kind-polymorphism", "Disable kind polymorphism.")

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,18 @@ object Contexts {
453453
/** Is the explicit nulls option set? */
454454
def explicitNulls: Boolean = base.settings.YexplicitNulls.value
455455

456+
/** Is best-effort-dir option set? */
457+
def isBestEffort: Boolean = base.settings.YbestEffortDir.value.nonEmpty
458+
459+
/** Is the from-best-effort-tasty option set to true? */
460+
def withBestEffortTasty: Boolean = base.settings.YwithBestEffortTasty.value
461+
462+
/** Were any best effort tasty dependencies used during compilation? */
463+
def usesBestEffortTasty: Boolean = base.usedBestEffortTasty
464+
465+
/** Confirm that a best effort tasty dependency was used during compilation. */
466+
def setUsesBestEffortTasty(): Unit = base.usedBestEffortTasty = true
467+
456468
/** A fresh clone of this context embedded in this context. */
457469
def fresh: FreshContext = freshOver(this)
458470

@@ -939,6 +951,9 @@ object Contexts {
939951
val sources: util.HashMap[AbstractFile, SourceFile] = util.HashMap[AbstractFile, SourceFile]()
940952
val files: util.HashMap[TermName, AbstractFile] = util.HashMap()
941953

954+
/** Was best effort file used during compilation? */
955+
private[core] var usedBestEffortTasty = false
956+
942957
// Types state
943958
/** A table for hash consing unique types */
944959
private[core] val uniques: Uniques = Uniques()

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ object DenotTransformers {
2828

2929
/** The transformation method */
3030
def transform(ref: SingleDenotation)(using Context): SingleDenotation
31+
32+
override def isRunnable(using Context) = super.isRunnable && !ctx.usesBestEffortTasty
3133
}
3234

3335
/** A transformer that only transforms the info field of denotations */

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,8 @@ object Denotations {
713713
ctx.runId >= validFor.runId
714714
|| ctx.settings.YtestPickler.value // mixing test pickler with debug printing can travel back in time
715715
|| ctx.mode.is(Mode.Printing) // no use to be picky when printing error messages
716-
|| symbol.isOneOf(ValidForeverFlags),
716+
|| symbol.isOneOf(ValidForeverFlags)
717+
|| ctx.isBestEffort,
717718
s"denotation $this invalid in run ${ctx.runId}. ValidFor: $validFor")
718719
var d: SingleDenotation = this
719720
while ({

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -711,12 +711,16 @@ object SymDenotations {
711711
* TODO: Find a more robust way to characterize self symbols, maybe by
712712
* spending a Flag on them?
713713
*/
714-
final def isSelfSym(using Context): Boolean = owner.infoOrCompleter match {
715-
case ClassInfo(_, _, _, _, selfInfo) =>
716-
selfInfo == symbol ||
717-
selfInfo.isInstanceOf[Type] && name == nme.WILDCARD
718-
case _ => false
719-
}
714+
final def isSelfSym(using Context): Boolean =
715+
if !ctx.isBestEffort || exists then
716+
owner.infoOrCompleter match {
717+
case ClassInfo(_, _, _, _, selfInfo) =>
718+
selfInfo == symbol ||
719+
selfInfo.isInstanceOf[Type] && name == nme.WILDCARD
720+
case _ => false
721+
}
722+
else false
723+
720724

721725
/** Is this definition contained in `boundary`?
722726
* Same as `ownersIterator contains boundary` but more efficient.
@@ -1986,7 +1990,7 @@ object SymDenotations {
19861990
case p :: parents1 =>
19871991
p.classSymbol match {
19881992
case pcls: ClassSymbol => builder.addAll(pcls.baseClasses)
1989-
case _ => assert(isRefinementClass || p.isError || ctx.mode.is(Mode.Interactive), s"$this has non-class parent: $p")
1993+
case _ => assert(isRefinementClass || p.isError || ctx.mode.is(Mode.Interactive) || ctx.isBestEffort || ctx.usesBestEffortTasty, s"$this has non-class parent: $p")
19901994
}
19911995
traverse(parents1)
19921996
case nil =>

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Contexts._, Symbols._, Flags._, SymDenotations._, Types._, Scopes._, Name
1414
import NameOps._
1515
import StdNames._
1616
import classfile.ClassfileParser
17+
import classfile.BestEffortTastyParser
1718
import Decorators._
1819

1920
import util.Stats
@@ -256,7 +257,8 @@ object SymbolLoaders {
256257
(idx + str.TOPLEVEL_SUFFIX.length + 1 != name.length || !name.endsWith(str.TOPLEVEL_SUFFIX))
257258
}
258259

259-
def maybeModuleClass(classRep: ClassRepresentation): Boolean = classRep.name.last == '$'
260+
def maybeModuleClass(classRep: ClassRepresentation): Boolean =
261+
classRep.name.nonEmpty && classRep.name.last == '$'
260262

261263
private def enterClasses(root: SymDenotation, packageName: String, flat: Boolean)(using Context) = {
262264
def isAbsent(classRep: ClassRepresentation) =
@@ -408,9 +410,14 @@ class ClassfileLoader(val classfile: AbstractFile) extends SymbolLoader {
408410

409411
def load(root: SymDenotation)(using Context): Unit = {
410412
val (classRoot, moduleRoot) = rootDenots(root.asClass)
411-
val classfileParser = new ClassfileParser(classfile, classRoot, moduleRoot)(ctx)
412-
val result = classfileParser.run()
413-
if (mayLoadTreesFromTasty)
413+
val isBestEffortTasty = classfile.name.endsWith(".betasty")
414+
val result =
415+
if isBestEffortTasty then
416+
new BestEffortTastyParser(classfile, classRoot, moduleRoot)(ctx).run()
417+
else
418+
new ClassfileParser(classfile, classRoot, moduleRoot)(ctx).run()
419+
420+
if (mayLoadTreesFromTasty || (isBestEffortTasty && ctx.withBestEffortTasty))
414421
result match {
415422
case Some(unpickler: tasty.DottyUnpickler) =>
416423
classRoot.classSymbol.rootTreeOrProvider = unpickler

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -709,12 +709,13 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst
709709
private def checkedSuperType(tp: TypeProxy)(using Context): Type =
710710
val tp1 = tp.translucentSuperType
711711
if !tp1.exists then
712-
val msg = tp.typeConstructor match
713-
case tycon: TypeRef =>
714-
MissingType(tycon.prefix, tycon.name).toMessage.message
715-
case _ =>
716-
i"Cannot resolve reference to $tp"
717-
throw FatalError(msg)
712+
val msg = tp.typeConstructor match
713+
case tycon: TypeRef =>
714+
MissingType(tycon.prefix, tycon.name).toMessage.message
715+
case _ =>
716+
i"Cannot resolve reference to $tp"
717+
if ctx.isBestEffort then report.error(msg)
718+
else throw FatalError(msg)
718719
tp1
719720

720721
/** Widen term ref, skipping any `()` parameter of an eventual getter. Used to erase a TermRef.

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,7 @@ object Types {
699699
* the type of the member as seen from given prefix `pre`.
700700
*/
701701
final def findMember(name: Name, pre: Type, required: FlagSet = EmptyFlags, excluded: FlagSet = EmptyFlags)(using Context): Denotation = {
702+
// println("findMember " + name + " " + pre + " " + required)
702703
@tailrec def go(tp: Type): Denotation = tp match {
703704
case tp: TermRef =>
704705
go (tp.underlying match {
@@ -3511,8 +3512,8 @@ object Types {
35113512

35123513
def apply(tp1: Type, tp2: Type, soft: Boolean)(using Context): OrType = {
35133514
def where = i"in union $tp1 | $tp2"
3514-
expectValueTypeOrWildcard(tp1, where)
3515-
expectValueTypeOrWildcard(tp2, where)
3515+
if (!ctx.usesBestEffortTasty) expectValueTypeOrWildcard(tp1, where)
3516+
if (!ctx.usesBestEffortTasty) expectValueTypeOrWildcard(tp2, where)
35163517
assertUnerased()
35173518
unique(new CachedOrType(tp1, tp2, soft))
35183519
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package dotty.tools.dotc.core.classfile
2+
3+
import java.io.ByteArrayOutputStream
4+
import dotty.tools.io
5+
import dotty.tools.io._
6+
import dotty.tools.dotc.core.Contexts.Context
7+
import dotty.tools.dotc.core.tasty.DottyUnpickler
8+
import dotty.tools.dotc.core.classfile.ClassfileParser.Embedded
9+
import dotty.tools.dotc.core.SymDenotations.ClassDenotation
10+
11+
class BestEffortTastyParser(
12+
bestEffortTastyFile: AbstractFile,
13+
classRoot: ClassDenotation,
14+
moduleRoot: ClassDenotation
15+
)(ictx: Context):
16+
17+
private def readFile(file: AbstractFile): Array[Byte] =
18+
file match
19+
case zipEntry: io.ZipArchive#Entry => // We are in a jar
20+
val stream = file.input
21+
try
22+
val tastyOutStream = new ByteArrayOutputStream()
23+
val buffer = new Array[Byte](1024)
24+
var read = stream.read(buffer, 0, buffer.length)
25+
while read != -1 do
26+
tastyOutStream.write(buffer, 0, read)
27+
read = stream.read(buffer, 0, buffer.length)
28+
29+
tastyOutStream.flush()
30+
tastyOutStream.toByteArray.nn
31+
finally
32+
stream.close()
33+
case _ =>
34+
file.toByteArray
35+
36+
private def unpickleTASTY(bytes: Array[Byte])(using ctx: Context): Some[Embedded] = {
37+
ctx.setUsesBestEffortTasty()
38+
val unpickler = new DottyUnpickler(bytes, withBestEffortTasty = true)
39+
unpickler.enter(roots = Set(classRoot, moduleRoot, moduleRoot.sourceModule))(using ctx.withSource(dotty.tools.dotc.util.NoSource))
40+
Some(unpickler)
41+
}
42+
43+
def run()(using Context): Option[Embedded] =
44+
val bytes = readFile(bestEffortTastyFile)
45+
if bytes.nonEmpty then unpickleTASTY(bytes)
46+
else None
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package dotty.tools.dotc
2+
package core
3+
package tasty
4+
5+
import scala.language.unsafeNulls
6+
import java.nio.file.{Path as JPath, Files as JFiles}
7+
import java.nio.channels.ClosedByInterruptException
8+
import java.io.DataOutputStream
9+
import dotty.tools.io.{File, PlainFile}
10+
import dotty.tools.dotc.core.Contexts.Context
11+
12+
object BestEffortTastyWriter:
13+
14+
def write(dir: JPath, units: List[CompilationUnit])(using Context): Unit =
15+
if JFiles.exists(dir) then JFiles.createDirectories(dir)
16+
17+
units.foreach { unit =>
18+
unit.pickled.foreach { (clz, binary) =>
19+
val parts = clz.fullName.mangledString.split('.')
20+
val outPath = outputPath(parts.toList, dir)
21+
val outTastyFile = new PlainFile(new File(outPath))
22+
val outstream = new DataOutputStream(outTastyFile.bufferedOutput)
23+
try outstream.write(binary())
24+
catch case ex: ClosedByInterruptException =>
25+
try
26+
outTastyFile.delete() // don't leave an empty or half-written tastyfile around after an interrupt
27+
catch
28+
case _: Throwable =>
29+
throw ex
30+
finally outstream.close()
31+
}
32+
}
33+
34+
def outputPath(parts: List[String], acc: JPath): JPath =
35+
parts match
36+
case Nil => throw new Exception("Invalid class name")
37+
case last :: Nil =>
38+
val name = last.stripSuffix("$")
39+
acc.resolve(s"$name.betasty")
40+
case pkg :: tail =>
41+
val next = acc.resolve(pkg)
42+
if !JFiles.exists(next) then JFiles.createDirectory(next)
43+
outputPath(tail, next)

0 commit comments

Comments
 (0)