Skip to content

Commit 95a8a9c

Browse files
authored
Introduce Best Effort compilation options (#17582)
This PR introduces 2 new experimental options: `-Ybest-effort` and `-Ywith-best-effort-tasty`. It also introduces the Best Effort TASTy format (BETASTy), a TASTy aligned file format able to hold some errored trees. Behaviour of the options and the format is documented as part of this PR in the `best-effort-compilation.md` docs file. `-Ybest-effort` allows to create .betasty files in a failing compilation, while `-Ywith-best-effort-tasty` allows to read them in subsequent compilations, so that we can get information needed for IDEs from broken modules, and modules depending on them. It is worth noting that these compilation depending on Betty will never reach transform phases, and will never produce any artifacts other then betasty and semanticdb. My overall aim was to be able to handle as many cases, with little as little maintainance necessary as possible. This is for example why pretty much the only required phases are Parser and Typer - they are enough for, as far as I know, all necessary metals completions and I did not find any issues with setting their products (best effort tasty files) as dependencies. Requiring, for example, PostTyper, would require for the errored trees to be able to somehow pass through that phase, meaning a large investment from me into working the existing known error cases through there (muddling the codebase in the process) and possibly from the maintainers working on both Typer (to be able to produce „correct” error trees) and PostTyper (to be able to consume „correct” errored trees), which would obviously make the entire initiative dreadful. This is also why any tests are able to be put into a blacklist file, in case something changes and a neg test will not pass, or a new test will be added as part of issue fix that does not play well with the best-effort features.
1 parent b93c337 commit 95a8a9c

File tree

61 files changed

+1202
-233
lines changed

Some content is hidden

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

61 files changed

+1202
-233
lines changed

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

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

2222
override def description: String = GenBCode.description
2323

24+
override def isRunnable(using Context) = super.isRunnable && !ctx.usedBestEffortTasty
25+
2426
private val superCallsMap = new MutableSymbolMap[Set[ClassSymbol]]
2527
def registerSuperCall(sym: Symbol, calls: ClassSymbol): Unit = {
2628
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.usedBestEffortTasty
1616

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

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ 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.usedBestEffortTasty =>
43+
report.bestEffortError(ex, "Some best-effort tasty files were not able to be read.")
44+
throw ex
4245
case ex: TypeError if !runOrNull.enrichedErrorMessage =>
4346
println(runOrNull.enrichErrorMessage(s"${ex.toMessage} while compiling ${files.map(_.path).mkString(", ")}"))
4447
throw ex
@@ -102,8 +105,8 @@ class Driver {
102105
None
103106
else file.ext match
104107
case FileExtension.Jar => Some(file.path)
105-
case FileExtension.Tasty =>
106-
TastyFileUtil.getClassPath(file) match
108+
case FileExtension.Tasty | FileExtension.Betasty =>
109+
TastyFileUtil.getClassPath(file, ctx.withBestEffortTasty) match
107110
case Some(classpath) => Some(classpath)
108111
case _ =>
109112
report.error(em"Could not load classname from: ${file.path}")

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,9 +329,13 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
329329
val profiler = ctx.profiler
330330
var phasesWereAdjusted = false
331331

332+
var forceReachPhaseMaybe =
333+
if (ctx.isBestEffort && phases.exists(_.phaseName == "typer")) Some("typer")
334+
else None
335+
332336
for phase <- allPhases do
333337
doEnterPhase(phase)
334-
val phaseWillRun = phase.isRunnable
338+
val phaseWillRun = phase.isRunnable || forceReachPhaseMaybe.nonEmpty
335339
if phaseWillRun then
336340
Stats.trackTime(s"phase time ms/$phase") {
337341
val start = System.currentTimeMillis
@@ -344,6 +348,10 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
344348
def printCtx(unit: CompilationUnit) = phase.printingContext(
345349
ctx.fresh.setPhase(phase.next).setCompilationUnit(unit))
346350
lastPrintedTree = printTree(lastPrintedTree)(using printCtx(unit))
351+
352+
if forceReachPhaseMaybe.contains(phase.phaseName) then
353+
forceReachPhaseMaybe = None
354+
347355
report.informTime(s"$phase ", start)
348356
Stats.record(s"total trees at end of $phase", ast.Trees.ntrees)
349357
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
@@ -919,12 +919,12 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
919919
else cpy.PackageDef(tree)(pid, slicedStats) :: Nil
920920
case tdef: TypeDef =>
921921
val sym = tdef.symbol
922-
assert(sym.isClass)
922+
assert(sym.isClass || ctx.tolerateErrorsForBestEffort)
923923
if (cls == sym || cls == sym.linkedClass) tdef :: Nil
924924
else Nil
925925
case vdef: ValDef =>
926926
val sym = vdef.symbol
927-
assert(sym.is(Module))
927+
assert(sym.is(Module) || ctx.tolerateErrorsForBestEffort)
928928
if (cls == sym.companionClass || cls == sym.moduleClass) vdef :: Nil
929929
else Nil
930930
case tree =>

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

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

5353
def TypeApply(fn: Tree, args: List[Tree])(using Context): TypeApply = fn match
@@ -56,7 +56,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
5656
case _: RefTree | _: GenericApply =>
5757
ta.assignType(untpd.TypeApply(fn, args), fn, args)
5858
case _ =>
59-
assert(ctx.reporter.errorsReported, s"unexpected tree for type application: $fn")
59+
assert(ctx.reporter.errorsReported || ctx.tolerateErrorsForBestEffort, s"unexpected tree for type application: $fn")
6060
ta.assignType(untpd.TypeApply(fn, args), fn, args)
6161

6262
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
@@ -285,7 +285,7 @@ case class DirectoryClassPath(dir: JFile) extends JFileDirectoryLookup[BinaryFil
285285
protected def createFileEntry(file: AbstractFile): BinaryFileEntry = BinaryFileEntry(file)
286286

287287
protected def isMatchingFile(f: JFile): Boolean =
288-
f.isTasty || (f.isClass && !f.hasSiblingTasty)
288+
f.isTasty || f.isBestEffortTasty || (f.isClass && !f.hasSiblingTasty)
289289

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

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ object FileUtils {
2323

2424
def hasTastyExtension: Boolean = file.ext.isTasty
2525

26+
def hasBetastyExtension: Boolean = file.ext.isBetasty
27+
2628
def isTasty: Boolean = !file.isDirectory && hasTastyExtension
2729

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

3034
def isScalaOrJavaSource: Boolean = !file.isDirectory && file.ext.isScalaOrJava
@@ -55,6 +59,9 @@ object FileUtils {
5559

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

62+
def isBestEffortTasty: Boolean = file.isFile && file.getName.endsWith(SUFFIX_BETASTY)
63+
64+
5865
/**
5966
* Returns if there is an existing sibling `.tasty` file.
6067
*/
@@ -69,6 +76,7 @@ object FileUtils {
6976
private val SUFFIX_CLASS = ".class"
7077
private val SUFFIX_SCALA = ".scala"
7178
private val SUFFIX_TASTY = ".tasty"
79+
private val SUFFIX_BETASTY = ".betasty"
7280
private val SUFFIX_JAVA = ".java"
7381
private val SUFFIX_SIG = ".sig"
7482

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,9 @@ private sealed trait YSettings:
414414
val YprofileRunGcBetweenPhases: Setting[List[String]] = PhasesSetting(ForkSetting, "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 *", "_")
415415
//.withPostSetHook( _ => YprofileEnabled.value = true )
416416

417+
val YbestEffort: Setting[Boolean] = BooleanSetting(ForkSetting, "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.")
418+
val YwithBestEffortTasty: Setting[Boolean] = BooleanSetting(ForkSetting, "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.")
419+
417420
// Experimental language features
418421
val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-kind-polymorphism", "Disable kind polymorphism.")
419422
val YexplicitNulls: Setting[Boolean] = BooleanSetting(ForkSetting, "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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,21 @@ object Contexts {
474474

475475
/** Is the flexible types option set? */
476476
def flexibleTypes: Boolean = base.settings.YexplicitNulls.value && !base.settings.YnoFlexibleTypes.value
477+
478+
/** Is the best-effort option set? */
479+
def isBestEffort: Boolean = base.settings.YbestEffort.value
480+
481+
/** Is the with-best-effort-tasty option set? */
482+
def withBestEffortTasty: Boolean = base.settings.YwithBestEffortTasty.value
483+
484+
/** Were any best effort tasty dependencies used during compilation? */
485+
def usedBestEffortTasty: Boolean = base.usedBestEffortTasty
486+
487+
/** Confirm that a best effort tasty dependency was used during compilation. */
488+
def setUsedBestEffortTasty(): Unit = base.usedBestEffortTasty = true
489+
490+
/** Is either the best-effort option set or .betasty files were used during compilation? */
491+
def tolerateErrorsForBestEffort = isBestEffort || usedBestEffortTasty
477492

478493
/** A fresh clone of this context embedded in this context. */
479494
def fresh: FreshContext = freshOver(this)
@@ -960,6 +975,9 @@ object Contexts {
960975
val sources: util.HashMap[AbstractFile, SourceFile] = util.HashMap[AbstractFile, SourceFile]()
961976
val files: util.HashMap[TermName, AbstractFile] = util.HashMap()
962977

978+
/** Was best effort file used during compilation? */
979+
private[core] var usedBestEffortTasty = false
980+
963981
// Types state
964982
/** A table for hash consing unique types */
965983
private[core] val uniques: Uniques = Uniques()

0 commit comments

Comments
 (0)