Skip to content

Commit 5475c2b

Browse files
committed
Introduce best-effort compilation for IDEs
2 new experimental options are introduces for the compiler: `-Ybest-effort` and `-Ywith-best-effort-tasty`. A related Best Effort TASTy (.betasty) format, a TASTy aligned file format able to hold some errored trees was also added. Behaviour of the options and the format is documented as part of this commit in the `best-effort-compilation.md` docs file. `-Ybest-effort` is used to produce `.betasty` files in the `<output>/META-INF/best-effort`. `-Ywith-best-effort-tasty` allows to use them during compilation, limiting it to the frontend phases if such file is used. If any .betasty is used, transparent inline macros also cease to be expanded by the compiler. Since best-effort compilation can fail (e.g. due to cyclic reference errors which sometimes are not able to be pickled or unpickled), the crashes caused by it are wrapped into an additional descriptive error message in the aim to fail more gracefully (and not pollute our issue tracker with known problems). The feature is tested in two ways: * with a set of pairs of dependent projects, one of which is meant to produce .betasty by using `-Ybest-effort`, and the other tries to consume it using `-Ywith-best-effort-tasty`. * by reusing the compiler nonbootstrapped neg tests, first by running them with `-Ybest-effort` option, and then by running read-tasty tests on the produced betasty files to thest best-effort tastt unpickling Additionally, `-Ywith-best-effort-tasty` allows to print `.betasty` via `-print-tasty`.
1 parent f61026d commit 5475c2b

Some content is hidden

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

56 files changed

+1033
-215
lines changed

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

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class GenBCode extends Phase { self =>
1717

1818
override def description: String = GenBCode.description
1919

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

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

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ class Driver {
3939
catch
4040
case ex: FatalError =>
4141
report.error(ex.getMessage.nn) // signals that we should fail compilation.
42+
case ex: Throwable if ctx.usesBestEffortTasty =>
43+
report.bestEffortError(ex, "Some best-effort tasty files were not able to be read.")
4244
case ex: TypeError if !runOrNull.enrichedErrorMessage =>
4345
println(runOrNull.enrichErrorMessage(s"${ex.toMessage} while compiling ${files.map(_.path).mkString(", ")}"))
4446
throw ex
@@ -100,8 +102,8 @@ class Driver {
100102
None
101103
else file.extension match
102104
case "jar" => Some(file.path)
103-
case "tasty" =>
104-
TastyFileUtil.getClassPath(file) match
105+
case "tasty" | "betasty" =>
106+
TastyFileUtil.getClassPath(file, ctx.withBestEffortTasty) match
105107
case Some(classpath) => Some(classpath)
106108
case _ =>
107109
report.error(em"Could not load classname from: ${file.path}")

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

+12-1
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
297297
ctx.settings.Yskip.value, ctx.settings.YstopBefore.value, stopAfter, ctx.settings.Ycheck.value)
298298
ctx.base.usePhases(phases)
299299

300+
var forceReachPhaseMaybe =
301+
if (ctx.isBestEffort && phases.exists(_.phaseName == "typer")) Some("typer")
302+
else None
303+
300304
if ctx.settings.YnoDoubleBindings.value then
301305
ctx.base.checkNoDoubleBindings = true
302306

@@ -307,7 +311,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
307311

308312
for phase <- allPhases do
309313
doEnterPhase(phase)
310-
val phaseWillRun = phase.isRunnable
314+
val phaseWillRun = phase.isRunnable || forceReachPhaseMaybe.nonEmpty
311315
if phaseWillRun then
312316
Stats.trackTime(s"phase time ms/$phase") {
313317
val start = System.currentTimeMillis
@@ -320,6 +324,13 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
320324
def printCtx(unit: CompilationUnit) = phase.printingContext(
321325
ctx.fresh.setPhase(phase.next).setCompilationUnit(unit))
322326
lastPrintedTree = printTree(lastPrintedTree)(using printCtx(unit))
327+
328+
forceReachPhaseMaybe match {
329+
case Some(forceReachPhase) if phase.phaseName == forceReachPhase =>
330+
forceReachPhaseMaybe = None
331+
case _ =>
332+
}
333+
323334
report.informTime(s"$phase ", start)
324335
Stats.record(s"total trees at end of $phase", ast.Trees.ntrees)
325336
for (unit <- units)

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -929,12 +929,12 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
929929
else cpy.PackageDef(tree)(pid, slicedStats) :: Nil
930930
case tdef: TypeDef =>
931931
val sym = tdef.symbol
932-
assert(sym.isClass)
932+
if !ctx.isBestEffort then assert(sym.isClass)
933933
if (cls == sym || cls == sym.linkedClass) tdef :: Nil
934934
else Nil
935935
case vdef: ValDef =>
936936
val sym = vdef.symbol
937-
assert(sym.is(Module))
937+
if !ctx.isBestEffort then assert(sym.is(Module))
938938
if (cls == sym.companionClass || cls == sym.moduleClass) vdef :: Nil
939939
else Nil
940940
case tree =>

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
4949
case _: RefTree | _: GenericApply | _: Inlined | _: Hole =>
5050
ta.assignType(untpd.Apply(fn, args), fn, args)
5151
case _ =>
52-
assert(ctx.reporter.errorsReported)
52+
assert(ctx.isBestEffort || ctx.usesBestEffortTasty || ctx.reporter.errorsReported)
5353
ta.assignType(untpd.Apply(fn, args), fn, args)
5454

5555
def TypeApply(fn: Tree, args: List[Tree])(using Context): TypeApply = fn match
@@ -58,7 +58,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
5858
case _: RefTree | _: GenericApply =>
5959
ta.assignType(untpd.TypeApply(fn, args), fn, args)
6060
case _ =>
61-
assert(ctx.reporter.errorsReported, s"unexpected tree for type application: $fn")
61+
assert(ctx.isBestEffort || ctx.usesBestEffortTasty || ctx.reporter.errorsReported, s"unexpected tree for type application: $fn")
6262
ta.assignType(untpd.TypeApply(fn, args), fn, args)
6363

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

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

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

+7
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ object FileUtils {
2424

2525
def hasTastyExtension: Boolean = file.hasExtension("tasty")
2626

27+
def hasBetastyExtension: Boolean = file.hasExtension("betasty")
28+
2729
def isTasty: Boolean = !file.isDirectory && hasTastyExtension
2830

31+
def isBestEffortTasty: Boolean = !file.isDirectory && hasBetastyExtension
32+
2933
def isScalaBinary: Boolean = file.isClass || file.isTasty
3034

3135
def isScalaOrJavaSource: Boolean = !file.isDirectory && (file.hasExtension("scala") || file.hasExtension("java"))
@@ -54,6 +58,8 @@ object FileUtils {
5458

5559
def isTasty: Boolean = file.isFile && file.getName.endsWith(SUFFIX_TASTY)
5660

61+
def isBestEffortTasty: Boolean = file.isFile && file.getName.endsWith(SUFFIX_BETASTY)
62+
5763
/** Returns the tasty file associated with this class file */
5864
def classToTasty: Option[JFile] =
5965
assert(file.isClass, s"non-class: $file")
@@ -66,6 +72,7 @@ object FileUtils {
6672
private val SUFFIX_CLASS = ".class"
6773
private val SUFFIX_SCALA = ".scala"
6874
private val SUFFIX_TASTY = ".tasty"
75+
private val SUFFIX_BETASTY = ".betasty"
6976
private val SUFFIX_JAVA = ".java"
7077
private val SUFFIX_SIG = ".sig"
7178

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

+3
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,9 @@ private sealed trait YSettings:
406406
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 *", "_")
407407
//.withPostSetHook( _ => YprofileEnabled.value = true )
408408

409+
val YbestEffort: Setting[Boolean] = BooleanSetting("-Ybest-effort", "Enable best-effort compilation attempting to produce betasty to the META-INF/best-effort directory, regardless of errors, as part of the pickler phase.")
410+
val YwithBestEffortTasty: Setting[Boolean] = BooleanSetting("-Ywith-best-effort-tasty", "Allow to compile using best-effort tasty files. If such file is used, the compiler will stop after the pickler phase.")
411+
409412
// Experimental language features
410413
val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting("-Yno-kind-polymorphism", "Disable kind polymorphism.")
411414
val YexplicitNulls: Setting[Boolean] = BooleanSetting("-Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.")

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

+15
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,18 @@ object Contexts {
472472
/** Is the explicit nulls option set? */
473473
def explicitNulls: Boolean = base.settings.YexplicitNulls.value
474474

475+
/** Is best-effort-dir option set? */
476+
def isBestEffort: Boolean = base.settings.YbestEffort.value
477+
478+
/** Is the from-best-effort-tasty option set to true? */
479+
def withBestEffortTasty: Boolean = base.settings.YwithBestEffortTasty.value
480+
481+
/** Were any best effort tasty dependencies used during compilation? */
482+
def usesBestEffortTasty: Boolean = base.usedBestEffortTasty
483+
484+
/** Confirm that a best effort tasty dependency was used during compilation. */
485+
def setUsesBestEffortTasty(): Unit = base.usedBestEffortTasty = true
486+
475487
/** A fresh clone of this context embedded in this context. */
476488
def fresh: FreshContext = freshOver(this)
477489

@@ -959,6 +971,9 @@ object Contexts {
959971
val sources: util.HashMap[AbstractFile, SourceFile] = util.HashMap[AbstractFile, SourceFile]()
960972
val files: util.HashMap[TermName, AbstractFile] = util.HashMap()
961973

974+
/** Was best effort file used during compilation? */
975+
private[core] var usedBestEffortTasty = false
976+
962977
// Types state
963978
/** A table for hash consing unique types */
964979
private[core] val uniques: Uniques = Uniques()

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

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,8 @@ object Denotations {
719719
ctx.runId >= validFor.runId
720720
|| ctx.settings.YtestPickler.value // mixing test pickler with debug printing can travel back in time
721721
|| ctx.mode.is(Mode.Printing) // no use to be picky when printing error messages
722-
|| symbol.isOneOf(ValidForeverFlags),
722+
|| symbol.isOneOf(ValidForeverFlags)
723+
|| ctx.isBestEffort || ctx.usesBestEffortTasty,
723724
s"denotation $this invalid in run ${ctx.runId}. ValidFor: $validFor")
724725
var d: SingleDenotation = this
725726
while ({

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

+11-7
Original file line numberDiff line numberDiff line change
@@ -716,12 +716,16 @@ object SymDenotations {
716716
* TODO: Find a more robust way to characterize self symbols, maybe by
717717
* spending a Flag on them?
718718
*/
719-
final def isSelfSym(using Context): Boolean = owner.infoOrCompleter match {
720-
case ClassInfo(_, _, _, _, selfInfo) =>
721-
selfInfo == symbol ||
722-
selfInfo.isInstanceOf[Type] && name == nme.WILDCARD
723-
case _ => false
724-
}
719+
final def isSelfSym(using Context): Boolean =
720+
if !ctx.isBestEffort || exists then
721+
owner.infoOrCompleter match {
722+
case ClassInfo(_, _, _, _, selfInfo) =>
723+
selfInfo == symbol ||
724+
selfInfo.isInstanceOf[Type] && name == nme.WILDCARD
725+
case _ => false
726+
}
727+
else false
728+
725729

726730
/** Is this definition contained in `boundary`?
727731
* Same as `ownersIterator contains boundary` but more efficient.
@@ -1981,7 +1985,7 @@ object SymDenotations {
19811985
case p :: parents1 =>
19821986
p.classSymbol match {
19831987
case pcls: ClassSymbol => builder.addAll(pcls.baseClasses)
1984-
case _ => assert(isRefinementClass || p.isError || ctx.mode.is(Mode.Interactive), s"$this has non-class parent: $p")
1988+
case _ => assert(isRefinementClass || p.isError || ctx.mode.is(Mode.Interactive) || ctx.isBestEffort || ctx.usesBestEffortTasty, s"$this has non-class parent: $p")
19851989
}
19861990
traverse(parents1)
19871991
case nil =>

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

+29-13
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import java.nio.channels.ClosedByInterruptException
77

88
import scala.util.control.NonFatal
99

10-
import dotty.tools.dotc.classpath.FileUtils.isTasty
10+
import dotty.tools.dotc.classpath.FileUtils.{isTasty, isBestEffortTasty}
1111
import dotty.tools.io.{ ClassPath, ClassRepresentation, AbstractFile }
1212
import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions
1313

@@ -26,6 +26,7 @@ import parsing.JavaParsers.OutlineJavaParser
2626
import parsing.Parsers.OutlineParser
2727
import dotty.tools.tasty.{TastyHeaderUnpickler, UnpickleException, UnpicklerConfig}
2828
import dotty.tools.dotc.core.tasty.TastyUnpickler
29+
import dotty.tools.tasty.besteffort.BestEffortTastyHeaderUnpickler
2930

3031

3132
object SymbolLoaders {
@@ -199,7 +200,7 @@ object SymbolLoaders {
199200
enterToplevelsFromSource(owner, nameOf(classRep), src)
200201
case (Some(bin), _) =>
201202
val completer =
202-
if bin.isTasty then ctx.platform.newTastyLoader(bin)
203+
if bin.isTasty || bin.isBestEffortTasty then ctx.platform.newTastyLoader(bin)
203204
else ctx.platform.newClassLoader(bin)
204205
enterClassAndModule(owner, nameOf(classRep), completer)
205206
}
@@ -262,7 +263,8 @@ object SymbolLoaders {
262263
(idx + str.TOPLEVEL_SUFFIX.length + 1 != name.length || !name.endsWith(str.TOPLEVEL_SUFFIX))
263264
}
264265

265-
def maybeModuleClass(classRep: ClassRepresentation): Boolean = classRep.name.last == '$'
266+
def maybeModuleClass(classRep: ClassRepresentation): Boolean =
267+
classRep.name.nonEmpty && classRep.name.last == '$'
266268

267269
private def enterClasses(root: SymDenotation, packageName: String, flat: Boolean)(using Context) = {
268270
def isAbsent(classRep: ClassRepresentation) =
@@ -419,25 +421,36 @@ class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader {
419421

420422
override def sourceFileOrNull: AbstractFile | Null = tastyFile
421423

422-
def description(using Context): String = "TASTy file " + tastyFile.toString
424+
def description(using Context): String =
425+
if tastyFile.extension == ".betasty" then "Best Effort TASTy file " + tastyFile.toString
426+
else "TASTy file " + tastyFile.toString
423427

424428
override def doComplete(root: SymDenotation)(using Context): Unit =
429+
val isBestEffortTasty = tastyFile.name.endsWith(".betasty")
425430
try
426431
val (classRoot, moduleRoot) = rootDenots(root.asClass)
427-
val tastyBytes = tastyFile.toByteArray
428-
val unpickler = new tasty.DottyUnpickler(tastyBytes)
429-
unpickler.enter(roots = Set(classRoot, moduleRoot, moduleRoot.sourceModule))(using ctx.withSource(util.NoSource))
430-
if mayLoadTreesFromTasty then
431-
classRoot.classSymbol.rootTreeOrProvider = unpickler
432-
moduleRoot.classSymbol.rootTreeOrProvider = unpickler
433-
checkTastyUUID(tastyFile, tastyBytes)
432+
if (!isBestEffortTasty || ctx.withBestEffortTasty) then
433+
val tastyBytes = tastyFile.toByteArray
434+
val unpickler = new tasty.DottyUnpickler(tastyBytes, isBestEffortTasty = isBestEffortTasty)
435+
unpickler.enter(roots = Set(classRoot, moduleRoot, moduleRoot.sourceModule))(using ctx.withSource(util.NoSource))
436+
if mayLoadTreesFromTasty || (isBestEffortTasty && ctx.withBestEffortTasty) then
437+
classRoot.classSymbol.rootTreeOrProvider = unpickler
438+
moduleRoot.classSymbol.rootTreeOrProvider = unpickler
439+
if isBestEffortTasty then
440+
checkBeTastyUUID(tastyFile, tastyBytes)
441+
ctx.setUsesBestEffortTasty()
442+
else
443+
checkTastyUUID(tastyFile, tastyBytes)
444+
else
445+
report.error(em"Best Effort TASTy $tastyFile file could not be read.")
434446
catch case e: RuntimeException =>
447+
val tastyType = if (isBestEffortTasty) "Best Effort TASTy" else "TASTy"
435448
val message = e match
436449
case e: UnpickleException =>
437-
i"""TASTy file ${tastyFile.canonicalPath} could not be read, failing with:
450+
i"""$tastyType file ${tastyFile.canonicalPath} could not be read, failing with:
438451
| ${Option(e.getMessage).getOrElse("")}"""
439452
case _ =>
440-
i"""TASTy file ${tastyFile.canonicalPath} is broken, reading aborted with ${e.getClass}
453+
i"""$tastyFile file ${tastyFile.canonicalPath} is broken, reading aborted with ${e.getClass}
441454
| ${Option(e.getMessage).getOrElse("")}"""
442455
if (ctx.debug) e.printStackTrace()
443456
throw IOException(message)
@@ -454,6 +467,9 @@ class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader {
454467
// This will be the case in any of our tests that compile with `-Youtput-only-tasty`
455468
report.inform(s"No classfiles found for $tastyFile when checking TASTy UUID")
456469

470+
private def checkBeTastyUUID(tastyFile: AbstractFile, tastyBytes: Array[Byte])(using Context): Unit =
471+
new BestEffortTastyHeaderUnpickler(tastyBytes).readHeader()
472+
457473
private def mayLoadTreesFromTasty(using Context): Boolean =
458474
ctx.settings.YretainTrees.value || ctx.settings.fromTasty.value
459475
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -758,7 +758,8 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst
758758
MissingType(tycon.prefix, tycon.name).toMessage.message
759759
case _ =>
760760
i"Cannot resolve reference to $tp"
761-
throw FatalError(msg)
761+
if ctx.isBestEffort then report.error(msg)
762+
else throw FatalError(msg)
762763
tp1
763764

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

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -3079,7 +3079,8 @@ object Types {
30793079
if (ctx.erasedTypes) tref
30803080
else cls.info match {
30813081
case cinfo: ClassInfo => cinfo.selfType
3082-
case _: ErrorType | NoType if ctx.mode.is(Mode.Interactive) => cls.info
3082+
case _: ErrorType | NoType
3083+
if ctx.mode.is(Mode.Interactive) || ctx.isBestEffort || ctx.usesBestEffortTasty => cls.info
30833084
// can happen in IDE if `cls` is stale
30843085
}
30853086

@@ -3604,8 +3605,8 @@ object Types {
36043605

36053606
def apply(tp1: Type, tp2: Type, soft: Boolean)(using Context): OrType = {
36063607
def where = i"in union $tp1 | $tp2"
3607-
expectValueTypeOrWildcard(tp1, where)
3608-
expectValueTypeOrWildcard(tp2, where)
3608+
if (!ctx.usesBestEffortTasty) expectValueTypeOrWildcard(tp1, where)
3609+
if (!ctx.usesBestEffortTasty) expectValueTypeOrWildcard(tp2, where)
36093610
assertUnerased()
36103611
unique(new CachedOrType(tp1, tp2, soft))
36113612
}

0 commit comments

Comments
 (0)