Skip to content

Commit a5a57c8

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 9d375cf commit a5a57c8

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

+825
-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 time ms/$phase") {
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
@@ -893,12 +893,12 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
893893
else cpy.PackageDef(tree)(pid, slicedStats) :: Nil
894894
case tdef: TypeDef =>
895895
val sym = tdef.symbol
896-
assert(sym.isClass)
896+
if !ctx.isBestEffort then assert(sym.isClass)
897897
if (cls == sym || cls == sym.linkedClass) tdef :: Nil
898898
else Nil
899899
case vdef: ValDef =>
900900
val sym = vdef.symbol
901-
assert(sym.is(Module))
901+
if !ctx.isBestEffort then assert(sym.is(Module))
902902
if (cls == sym.companionClass || cls == sym.moduleClass) vdef :: Nil
903903
else Nil
904904
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
@@ -288,7 +288,7 @@ case class DirectoryClassPath(dir: JFile) extends JFileDirectoryLookup[ClassFile
288288

289289
protected def createFileEntry(file: AbstractFile): ClassFileEntryImpl = ClassFileEntryImpl(file)
290290
protected def isMatchingFile(f: JFile): Boolean =
291-
f.isTasty || (f.isClass && f.classToTasty.isEmpty)
291+
f.isTasty || f.isBestEffortTasty || (f.isClass && f.classToTasty.isEmpty)
292292

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

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ object FileUtils {
5454

5555
def isTasty: Boolean = file.isFile && file.getName.endsWith(SUFFIX_TASTY)
5656

57+
def isBestEffortTasty: Boolean = file.isFile && file.getName.endsWith(SUFFIX_BETASTY)
58+
5759
/** Returns the tasty file associated with this class file */
5860
def classToTasty: Option[JFile] =
5961
assert(file.isClass, s"non-class: $file")
@@ -66,6 +68,7 @@ object FileUtils {
6668
private val SUFFIX_CLASS = ".class"
6769
private val SUFFIX_SCALA = ".scala"
6870
private val SUFFIX_TASTY = ".tasty"
71+
private val SUFFIX_BETASTY = ".betasty"
6972
private val SUFFIX_JAVA = ".java"
7073
private val SUFFIX_SIG = ".sig"
7174

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,9 @@ private sealed trait YSettings:
379379
//.withPostSetHook( _ => YprofileEnabled.value = true )
380380
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 *", "_")
381381
//.withPostSetHook( _ => YprofileEnabled.value = true )
382+
383+
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.", "")
384+
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.")
382385

383386
// Experimental language features
384387
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
@@ -463,6 +463,18 @@ object Contexts {
463463
/** Is the explicit nulls option set? */
464464
def explicitNulls: Boolean = base.settings.YexplicitNulls.value
465465

466+
/** Is best-effort-dir option set? */
467+
def isBestEffort: Boolean = base.settings.YbestEffortDir.value.nonEmpty
468+
469+
/** Is the from-best-effort-tasty option set to true? */
470+
def withBestEffortTasty: Boolean = base.settings.YwithBestEffortTasty.value
471+
472+
/** Were any best effort tasty dependencies used during compilation? */
473+
def usesBestEffortTasty: Boolean = base.usedBestEffortTasty
474+
475+
/** Confirm that a best effort tasty dependency was used during compilation. */
476+
def setUsesBestEffortTasty(): Unit = base.usedBestEffortTasty = true
477+
466478
/** A fresh clone of this context embedded in this context. */
467479
def fresh: FreshContext = freshOver(this)
468480

@@ -949,6 +961,9 @@ object Contexts {
949961
val sources: util.HashMap[AbstractFile, SourceFile] = util.HashMap[AbstractFile, SourceFile]()
950962
val files: util.HashMap[TermName, AbstractFile] = util.HashMap()
951963

964+
/** Was best effort file used during compilation? */
965+
private[core] var usedBestEffortTasty = false
966+
952967
// Types state
953968
/** A table for hash consing unique types */
954969
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
@@ -717,7 +717,8 @@ object Denotations {
717717
ctx.runId >= validFor.runId
718718
|| ctx.settings.YtestPickler.value // mixing test pickler with debug printing can travel back in time
719719
|| ctx.mode.is(Mode.Printing) // no use to be picky when printing error messages
720-
|| symbol.isOneOf(ValidForeverFlags),
720+
|| symbol.isOneOf(ValidForeverFlags)
721+
|| ctx.isBestEffort,
721722
s"denotation $this invalid in run ${ctx.runId}. ValidFor: $validFor")
722723
var d: SingleDenotation = this
723724
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.
@@ -1990,7 +1994,7 @@ object SymDenotations {
19901994
case p :: parents1 =>
19911995
p.classSymbol match {
19921996
case pcls: ClassSymbol => builder.addAll(pcls.baseClasses)
1993-
case _ => assert(isRefinementClass || p.isError || ctx.mode.is(Mode.Interactive), s"$this has non-class parent: $p")
1997+
case _ => assert(isRefinementClass || p.isError || ctx.mode.is(Mode.Interactive) || ctx.isBestEffort || ctx.usesBestEffortTasty, s"$this has non-class parent: $p")
19941998
}
19951999
traverse(parents1)
19962000
case nil =>

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

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions
1414
import Contexts._, Symbols._, Flags._, SymDenotations._, Types._, Scopes._, Names._
1515
import NameOps._
1616
import StdNames._
17-
import classfile.{ClassfileParser, ClassfileTastyUUIDParser}
17+
import classfile.{ClassfileParser, ClassfileTastyUUIDParser, BestEffortTastyParser}
1818
import Decorators._
1919

2020
import util.Stats
@@ -261,7 +261,8 @@ object SymbolLoaders {
261261
(idx + str.TOPLEVEL_SUFFIX.length + 1 != name.length || !name.endsWith(str.TOPLEVEL_SUFFIX))
262262
}
263263

264-
def maybeModuleClass(classRep: ClassRepresentation): Boolean = classRep.name.last == '$'
264+
def maybeModuleClass(classRep: ClassRepresentation): Boolean =
265+
classRep.name.nonEmpty && classRep.name.last == '$'
265266

266267
private def enterClasses(root: SymDenotation, packageName: String, flat: Boolean)(using Context) = {
267268
def isAbsent(classRep: ClassRepresentation) =
@@ -410,8 +411,23 @@ class ClassfileLoader(val classfile: AbstractFile) extends SymbolLoader {
410411

411412
override def doComplete(root: SymDenotation)(using Context): Unit =
412413
val (classRoot, moduleRoot) = rootDenots(root.asClass)
413-
val classfileParser = new ClassfileParser(classfile, classRoot, moduleRoot)(ctx)
414-
classfileParser.run()
414+
val isBestEffortTasty = classfile.name.endsWith(".betasty")
415+
val result =
416+
if isBestEffortTasty then // TODO move to TastyLoader
417+
new BestEffortTastyParser(classfile, classRoot, moduleRoot)(ctx).run()
418+
else
419+
new ClassfileParser(classfile, classRoot, moduleRoot)(ctx).run()
420+
421+
if (mayLoadTreesFromTasty || (isBestEffortTasty && ctx.withBestEffortTasty))
422+
result match {
423+
case Some(unpickler: tasty.DottyUnpickler) =>
424+
classRoot.classSymbol.rootTreeOrProvider = unpickler
425+
moduleRoot.classSymbol.rootTreeOrProvider = unpickler
426+
case _ =>
427+
}
428+
429+
private def mayLoadTreesFromTasty(using Context): Boolean =
430+
ctx.settings.YretainTrees.value || ctx.settings.fromTasty.value
415431
}
416432

417433
class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader {

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -754,12 +754,13 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst
754754
private def checkedSuperType(tp: TypeProxy)(using Context): Type =
755755
val tp1 = tp.translucentSuperType
756756
if !tp1.exists then
757-
val msg = tp.typeConstructor match
758-
case tycon: TypeRef =>
759-
MissingType(tycon.prefix, tycon.name).toMessage.message
760-
case _ =>
761-
i"Cannot resolve reference to $tp"
762-
throw FatalError(msg)
757+
val msg = tp.typeConstructor match
758+
case tycon: TypeRef =>
759+
MissingType(tycon.prefix, tycon.name).toMessage.message
760+
case _ =>
761+
i"Cannot resolve reference to $tp"
762+
if ctx.isBestEffort then report.error(msg)
763+
else throw FatalError(msg)
763764
tp1
764765

765766
/** 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
@@ -729,6 +729,7 @@ object Types {
729729
* the type of the member as seen from given prefix `pre`.
730730
*/
731731
final def findMember(name: Name, pre: Type, required: FlagSet = EmptyFlags, excluded: FlagSet = EmptyFlags)(using Context): Denotation = {
732+
// println("findMember " + name + " " + pre + " " + required)
732733
@tailrec def go(tp: Type): Denotation = tp match {
733734
case tp: TermRef =>
734735
go (tp.underlying match {
@@ -3574,8 +3575,8 @@ object Types {
35743575

35753576
def apply(tp1: Type, tp2: Type, soft: Boolean)(using Context): OrType = {
35763577
def where = i"in union $tp1 | $tp2"
3577-
expectValueTypeOrWildcard(tp1, where)
3578-
expectValueTypeOrWildcard(tp2, where)
3578+
if (!ctx.usesBestEffortTasty) expectValueTypeOrWildcard(tp1, where)
3579+
if (!ctx.usesBestEffortTasty) expectValueTypeOrWildcard(tp2, where)
35793580
assertUnerased()
35803581
unique(new CachedOrType(tp1, tp2, soft))
35813582
}
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

0 commit comments

Comments
 (0)