Skip to content

Introduce Best Effort compilation options #17582

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/backend/jvm/GenBCode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class GenBCode extends Phase { self =>

override def description: String = GenBCode.description

override def isRunnable(using Context) = super.isRunnable && !ctx.usedBestEffortTasty

private val superCallsMap = new MutableSymbolMap[Set[ClassSymbol]]
def registerSuperCall(sym: Symbol, calls: ClassSymbol): Unit = {
val old = superCallsMap.getOrElse(sym, Set.empty)
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/backend/sjs/GenSJSIR.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class GenSJSIR extends Phase {
override def description: String = GenSJSIR.description

override def isRunnable(using Context): Boolean =
super.isRunnable && ctx.settings.scalajs.value
super.isRunnable && ctx.settings.scalajs.value && !ctx.usedBestEffortTasty

def run(using Context): Unit =
new JSCodeGen().run()
Expand Down
7 changes: 5 additions & 2 deletions compiler/src/dotty/tools/dotc/Driver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ class Driver {
catch
case ex: FatalError =>
report.error(ex.getMessage.nn) // signals that we should fail compilation.
case ex: Throwable if ctx.usedBestEffortTasty =>
report.bestEffortError(ex, "Some best-effort tasty files were not able to be read.")
throw ex
case ex: TypeError if !runOrNull.enrichedErrorMessage =>
println(runOrNull.enrichErrorMessage(s"${ex.toMessage} while compiling ${files.map(_.path).mkString(", ")}"))
throw ex
Expand Down Expand Up @@ -102,8 +105,8 @@ class Driver {
None
else file.ext match
case FileExtension.Jar => Some(file.path)
case FileExtension.Tasty =>
TastyFileUtil.getClassPath(file) match
case FileExtension.Tasty | FileExtension.Betasty =>
TastyFileUtil.getClassPath(file, ctx.withBestEffortTasty) match
case Some(classpath) => Some(classpath)
case _ =>
report.error(em"Could not load classname from: ${file.path}")
Expand Down
10 changes: 9 additions & 1 deletion compiler/src/dotty/tools/dotc/Run.scala
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,13 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
val profiler = ctx.profiler
var phasesWereAdjusted = false

var forceReachPhaseMaybe =
if (ctx.isBestEffort && phases.exists(_.phaseName == "typer")) Some("typer")
else None

for phase <- allPhases do
doEnterPhase(phase)
val phaseWillRun = phase.isRunnable
val phaseWillRun = phase.isRunnable || forceReachPhaseMaybe.nonEmpty
if phaseWillRun then
Stats.trackTime(s"phase time ms/$phase") {
val start = System.currentTimeMillis
Expand All @@ -344,6 +348,10 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
def printCtx(unit: CompilationUnit) = phase.printingContext(
ctx.fresh.setPhase(phase.next).setCompilationUnit(unit))
lastPrintedTree = printTree(lastPrintedTree)(using printCtx(unit))

if forceReachPhaseMaybe.contains(phase.phaseName) then
forceReachPhaseMaybe = None

report.informTime(s"$phase ", start)
Stats.record(s"total trees at end of $phase", ast.Trees.ntrees)
for (unit <- units)
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/ast/TreeInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -919,12 +919,12 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
else cpy.PackageDef(tree)(pid, slicedStats) :: Nil
case tdef: TypeDef =>
val sym = tdef.symbol
assert(sym.isClass)
assert(sym.isClass || ctx.tolerateErrorsForBestEffort)
if (cls == sym || cls == sym.linkedClass) tdef :: Nil
else Nil
case vdef: ValDef =>
val sym = vdef.symbol
assert(sym.is(Module))
assert(sym.is(Module) || ctx.tolerateErrorsForBestEffort)
if (cls == sym.companionClass || cls == sym.moduleClass) vdef :: Nil
else Nil
case tree =>
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/ast/tpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
case _: RefTree | _: GenericApply | _: Inlined | _: Hole =>
ta.assignType(untpd.Apply(fn, args), fn, args)
case _ =>
assert(ctx.reporter.errorsReported)
assert(ctx.reporter.errorsReported || ctx.tolerateErrorsForBestEffort)
ta.assignType(untpd.Apply(fn, args), fn, args)

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

def Literal(const: Constant)(using Context): Literal =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ case class DirectoryClassPath(dir: JFile) extends JFileDirectoryLookup[BinaryFil
protected def createFileEntry(file: AbstractFile): BinaryFileEntry = BinaryFileEntry(file)

protected def isMatchingFile(f: JFile): Boolean =
f.isTasty || (f.isClass && !f.hasSiblingTasty)
f.isTasty || f.isBestEffortTasty || (f.isClass && !f.hasSiblingTasty)

private[dotty] def classes(inPackage: PackageName): Seq[BinaryFileEntry] = files(inPackage)
}
Expand Down
8 changes: 8 additions & 0 deletions compiler/src/dotty/tools/dotc/classpath/FileUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ object FileUtils {

def hasTastyExtension: Boolean = file.ext.isTasty

def hasBetastyExtension: Boolean = file.ext.isBetasty

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

def isBestEffortTasty: Boolean = !file.isDirectory && hasBetastyExtension

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

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

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

def isBestEffortTasty: Boolean = file.isFile && file.getName.endsWith(SUFFIX_BETASTY)


/**
* Returns if there is an existing sibling `.tasty` file.
*/
Expand All @@ -69,6 +76,7 @@ object FileUtils {
private val SUFFIX_CLASS = ".class"
private val SUFFIX_SCALA = ".scala"
private val SUFFIX_TASTY = ".tasty"
private val SUFFIX_BETASTY = ".betasty"
private val SUFFIX_JAVA = ".java"
private val SUFFIX_SIG = ".sig"

Expand Down
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,9 @@ private sealed trait YSettings:
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 *", "_")
//.withPostSetHook( _ => YprofileEnabled.value = true )

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.")
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.")

// Experimental language features
val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-kind-polymorphism", "Disable kind polymorphism.")
val YexplicitNulls: Setting[Boolean] = BooleanSetting(ForkSetting, "Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.")
Expand Down
18 changes: 18 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Contexts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,21 @@ object Contexts {

/** Is the flexible types option set? */
def flexibleTypes: Boolean = base.settings.YexplicitNulls.value && !base.settings.YnoFlexibleTypes.value

/** Is the best-effort option set? */
def isBestEffort: Boolean = base.settings.YbestEffort.value

/** Is the with-best-effort-tasty option set? */
def withBestEffortTasty: Boolean = base.settings.YwithBestEffortTasty.value

/** Were any best effort tasty dependencies used during compilation? */
def usedBestEffortTasty: Boolean = base.usedBestEffortTasty

/** Confirm that a best effort tasty dependency was used during compilation. */
def setUsedBestEffortTasty(): Unit = base.usedBestEffortTasty = true

/** Is either the best-effort option set or .betasty files were used during compilation? */
def tolerateErrorsForBestEffort = isBestEffort || usedBestEffortTasty

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

/** Was best effort file used during compilation? */
private[core] var usedBestEffortTasty = false

// Types state
/** A table for hash consing unique types */
private[core] val uniques: Uniques = Uniques()
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/core/DenotTransformers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ object DenotTransformers {

/** The transformation method */
def transform(ref: SingleDenotation)(using Context): SingleDenotation

override def isRunnable(using Context) = super.isRunnable && !ctx.usedBestEffortTasty
}

/** A transformer that only transforms the info field of denotations */
Expand Down
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/core/Denotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,8 @@ object Denotations {
ctx.runId >= validFor.runId
|| ctx.settings.YtestPickler.value // mixing test pickler with debug printing can travel back in time
|| ctx.mode.is(Mode.Printing) // no use to be picky when printing error messages
|| symbol.isOneOf(ValidForeverFlags),
|| symbol.isOneOf(ValidForeverFlags)
|| ctx.tolerateErrorsForBestEffort,
s"denotation $this invalid in run ${ctx.runId}. ValidFor: $validFor")
var d: SingleDenotation = this
while ({
Expand Down
18 changes: 11 additions & 7 deletions compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -720,12 +720,16 @@ object SymDenotations {
* TODO: Find a more robust way to characterize self symbols, maybe by
* spending a Flag on them?
Comment on lines 720 to 721
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe a flag has been spent on that since this was written, namely SelfName. Should this whole method become

    final def isSelfSym(using Context): Boolean = is(SelfName)

?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*/
final def isSelfSym(using Context): Boolean = owner.infoOrCompleter match {
case ClassInfo(_, _, _, _, selfInfo) =>
selfInfo == symbol ||
selfInfo.isInstanceOf[Type] && name == nme.WILDCARD
case _ => false
}
final def isSelfSym(using Context): Boolean =
if !ctx.isBestEffort || exists then
owner.infoOrCompleter match {
case ClassInfo(_, _, _, _, selfInfo) =>
selfInfo == symbol ||
selfInfo.isInstanceOf[Type] && name == nme.WILDCARD
case _ => false
}
else false


/** Is this definition contained in `boundary`?
* Same as `ownersIterator contains boundary` but more efficient.
Expand Down Expand Up @@ -2003,7 +2007,7 @@ object SymDenotations {
case p :: parents1 =>
p.classSymbol match {
case pcls: ClassSymbol => builder.addAll(pcls.baseClasses)
case _ => assert(isRefinementClass || p.isError || ctx.mode.is(Mode.Interactive), s"$this has non-class parent: $p")
case _ => assert(isRefinementClass || p.isError || ctx.mode.is(Mode.Interactive) || ctx.tolerateErrorsForBestEffort, s"$this has non-class parent: $p")
}
traverse(parents1)
case nil =>
Expand Down
42 changes: 29 additions & 13 deletions compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import java.nio.channels.ClosedByInterruptException

import scala.util.control.NonFatal

import dotty.tools.dotc.classpath.FileUtils.hasTastyExtension
import dotty.tools.dotc.classpath.FileUtils.{hasTastyExtension, hasBetastyExtension}
import dotty.tools.io.{ ClassPath, ClassRepresentation, AbstractFile }
import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions

Expand All @@ -26,6 +26,7 @@ import parsing.JavaParsers.OutlineJavaParser
import parsing.Parsers.OutlineParser
import dotty.tools.tasty.{TastyHeaderUnpickler, UnpickleException, UnpicklerConfig, TastyVersion}
import dotty.tools.dotc.core.tasty.TastyUnpickler
import dotty.tools.tasty.besteffort.BestEffortTastyHeaderUnpickler

object SymbolLoaders {
import ast.untpd.*
Expand Down Expand Up @@ -198,7 +199,7 @@ object SymbolLoaders {
enterToplevelsFromSource(owner, nameOf(classRep), src)
case (Some(bin), _) =>
val completer =
if bin.hasTastyExtension then ctx.platform.newTastyLoader(bin)
if bin.hasTastyExtension || bin.hasBetastyExtension then ctx.platform.newTastyLoader(bin)
else ctx.platform.newClassLoader(bin)
enterClassAndModule(owner, nameOf(classRep), completer)
}
Expand Down Expand Up @@ -261,7 +262,8 @@ object SymbolLoaders {
(idx + str.TOPLEVEL_SUFFIX.length + 1 != name.length || !name.endsWith(str.TOPLEVEL_SUFFIX))
}

def maybeModuleClass(classRep: ClassRepresentation): Boolean = classRep.name.last == '$'
def maybeModuleClass(classRep: ClassRepresentation): Boolean =
classRep.name.nonEmpty && classRep.name.last == '$'

private def enterClasses(root: SymDenotation, packageName: String, flat: Boolean)(using Context) = {
def isAbsent(classRep: ClassRepresentation) =
Expand Down Expand Up @@ -416,34 +418,45 @@ class ClassfileLoader(val classfile: AbstractFile) extends SymbolLoader {
}

class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader {

val isBestEffortTasty = tastyFile.hasBetastyExtension
private val unpickler: tasty.DottyUnpickler =
handleUnpicklingExceptions:
val tastyBytes = tastyFile.toByteArray
new tasty.DottyUnpickler(tastyFile, tastyBytes) // reads header and name table
new tasty.DottyUnpickler(tastyFile, tastyBytes, isBestEffortTasty) // reads header and name table

val compilationUnitInfo: CompilationUnitInfo | Null = unpickler.compilationUnitInfo

def description(using Context): String = "TASTy file " + tastyFile.toString
def description(using Context): String =
if isBestEffortTasty then "Best Effort TASTy file " + tastyFile.toString
else "TASTy file " + tastyFile.toString

override def doComplete(root: SymDenotation)(using Context): Unit =
handleUnpicklingExceptions:
checkTastyUUID()
val (classRoot, moduleRoot) = rootDenots(root.asClass)
unpickler.enter(roots = Set(classRoot, moduleRoot, moduleRoot.sourceModule))(using ctx.withSource(util.NoSource))
if mayLoadTreesFromTasty then
classRoot.classSymbol.rootTreeOrProvider = unpickler
moduleRoot.classSymbol.rootTreeOrProvider = unpickler
if (!isBestEffortTasty || ctx.withBestEffortTasty) then
val tastyBytes = tastyFile.toByteArray
unpickler.enter(roots = Set(classRoot, moduleRoot, moduleRoot.sourceModule))(using ctx.withSource(util.NoSource))
if mayLoadTreesFromTasty || isBestEffortTasty then
classRoot.classSymbol.rootTreeOrProvider = unpickler
moduleRoot.classSymbol.rootTreeOrProvider = unpickler
if isBestEffortTasty then
checkBeTastyUUID(tastyFile, tastyBytes)
ctx.setUsedBestEffortTasty()
else
checkTastyUUID()
else
report.error(em"Cannot read Best Effort TASTy $tastyFile without the ${ctx.settings.YwithBestEffortTasty.name} option")

private def handleUnpicklingExceptions[T](thunk: =>T): T =
try thunk
catch case e: RuntimeException =>
val tastyType = if (isBestEffortTasty) "Best Effort TASTy" else "TASTy"
val message = e match
case e: UnpickleException =>
s"""TASTy file ${tastyFile.canonicalPath} could not be read, failing with:
s"""$tastyType file ${tastyFile.canonicalPath} could not be read, failing with:
| ${Option(e.getMessage).getOrElse("")}""".stripMargin
case _ =>
s"""TASTy file ${tastyFile.canonicalPath} is broken, reading aborted with ${e.getClass}
s"""$tastyFile file ${tastyFile.canonicalPath} is broken, reading aborted with ${e.getClass}
| ${Option(e.getMessage).getOrElse("")}""".stripMargin
throw IOException(message, e)

Expand All @@ -460,6 +473,9 @@ class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader {
// tasty file compiled by `-Yearly-tasty-output-write` comes from an early output jar.
report.inform(s"No classfiles found for $tastyFile when checking TASTy UUID")

private def checkBeTastyUUID(tastyFile: AbstractFile, tastyBytes: Array[Byte])(using Context): Unit =
new BestEffortTastyHeaderUnpickler(tastyBytes).readHeader()

private def mayLoadTreesFromTasty(using Context): Boolean =
ctx.settings.YretainTrees.value || ctx.settings.fromTasty.value
}
Expand Down
7 changes: 5 additions & 2 deletions compiler/src/dotty/tools/dotc/core/TypeErasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,9 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst
assert(!etp.isInstanceOf[WildcardType] || inSigName, i"Unexpected WildcardType erasure for $tp")
etp

/** Like translucentSuperType, but issue a fatal error if it does not exist. */
/** Like translucentSuperType, but issue a fatal error if it does not exist.
* If using the best-effort option, the fatal error will not be issued.
*/
private def checkedSuperType(tp: TypeProxy)(using Context): Type =
val tp1 = tp.translucentSuperType
if !tp1.exists then
Expand All @@ -756,7 +758,8 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst
MissingType(tycon.prefix, tycon.name)
case _ =>
TypeError(em"Cannot resolve reference to $tp")
throw typeErr
if ctx.isBestEffort then report.error(typeErr.toMessage)
else throw typeErr
tp1

/** Widen term ref, skipping any `()` parameter of an eventual getter. Used to erase a TermRef.
Expand Down
8 changes: 5 additions & 3 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3149,7 +3149,8 @@ object Types extends TypeUtils {
if (ctx.erasedTypes) tref
else cls.info match {
case cinfo: ClassInfo => cinfo.selfType
case _: ErrorType | NoType if ctx.mode.is(Mode.Interactive) => cls.info
case _: ErrorType | NoType
if ctx.mode.is(Mode.Interactive) || ctx.tolerateErrorsForBestEffort => cls.info
// can happen in IDE if `cls` is stale
}

Expand Down Expand Up @@ -3719,8 +3720,9 @@ object Types extends TypeUtils {

def apply(tp1: Type, tp2: Type, soft: Boolean)(using Context): OrType = {
def where = i"in union $tp1 | $tp2"
expectValueTypeOrWildcard(tp1, where)
expectValueTypeOrWildcard(tp2, where)
if !ctx.usedBestEffortTasty then
expectValueTypeOrWildcard(tp1, where)
expectValueTypeOrWildcard(tp2, where)
assertUnerased()
unique(new CachedOrType(tp1, tp2, soft))
}
Expand Down
Loading
Loading