diff --git a/compiler/src/dotty/tools/backend/jvm/GenBCode.scala b/compiler/src/dotty/tools/backend/jvm/GenBCode.scala
index d9f413a5d5ab..a616241d9a3e 100644
--- a/compiler/src/dotty/tools/backend/jvm/GenBCode.scala
+++ b/compiler/src/dotty/tools/backend/jvm/GenBCode.scala
@@ -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)
diff --git a/compiler/src/dotty/tools/backend/sjs/GenSJSIR.scala b/compiler/src/dotty/tools/backend/sjs/GenSJSIR.scala
index 2c5a6639dc8b..fbb9042affe7 100644
--- a/compiler/src/dotty/tools/backend/sjs/GenSJSIR.scala
+++ b/compiler/src/dotty/tools/backend/sjs/GenSJSIR.scala
@@ -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()
diff --git a/compiler/src/dotty/tools/dotc/Driver.scala b/compiler/src/dotty/tools/dotc/Driver.scala
index f2f104d1c387..580c0eae1810 100644
--- a/compiler/src/dotty/tools/dotc/Driver.scala
+++ b/compiler/src/dotty/tools/dotc/Driver.scala
@@ -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
@@ -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}")
diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala
index 02a0618bb6e9..64e216a39b2a 100644
--- a/compiler/src/dotty/tools/dotc/Run.scala
+++ b/compiler/src/dotty/tools/dotc/Run.scala
@@ -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
@@ -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)
diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala
index a1bba544cc06..34c87eedb081 100644
--- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala
+++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala
@@ -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 =>
diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala
index 13abfae0166c..1716385410e6 100644
--- a/compiler/src/dotty/tools/dotc/ast/tpd.scala
+++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala
@@ -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
@@ -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 =
diff --git a/compiler/src/dotty/tools/dotc/classpath/DirectoryClassPath.scala b/compiler/src/dotty/tools/dotc/classpath/DirectoryClassPath.scala
index 252f046ab548..aed5be45cb0d 100644
--- a/compiler/src/dotty/tools/dotc/classpath/DirectoryClassPath.scala
+++ b/compiler/src/dotty/tools/dotc/classpath/DirectoryClassPath.scala
@@ -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)
}
diff --git a/compiler/src/dotty/tools/dotc/classpath/FileUtils.scala b/compiler/src/dotty/tools/dotc/classpath/FileUtils.scala
index 030b0b61044a..4fe57a722780 100644
--- a/compiler/src/dotty/tools/dotc/classpath/FileUtils.scala
+++ b/compiler/src/dotty/tools/dotc/classpath/FileUtils.scala
@@ -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
@@ -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.
*/
@@ -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"
diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
index fc7e61c8ec71..db867f394297 100644
--- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
+++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
@@ -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.")
diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala
index ab6fda68a09e..230fffdf80ba 100644
--- a/compiler/src/dotty/tools/dotc/core/Contexts.scala
+++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala
@@ -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)
@@ -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()
diff --git a/compiler/src/dotty/tools/dotc/core/DenotTransformers.scala b/compiler/src/dotty/tools/dotc/core/DenotTransformers.scala
index 59982fb99b5f..451561c1b84d 100644
--- a/compiler/src/dotty/tools/dotc/core/DenotTransformers.scala
+++ b/compiler/src/dotty/tools/dotc/core/DenotTransformers.scala
@@ -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 */
diff --git a/compiler/src/dotty/tools/dotc/core/Denotations.scala b/compiler/src/dotty/tools/dotc/core/Denotations.scala
index 8610d2095655..2418aba1978b 100644
--- a/compiler/src/dotty/tools/dotc/core/Denotations.scala
+++ b/compiler/src/dotty/tools/dotc/core/Denotations.scala
@@ -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 ({
diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala
index bfaaf78883ae..f01d2faf86c4 100644
--- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala
+++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala
@@ -720,12 +720,16 @@ object SymDenotations {
* TODO: Find a more robust way to characterize self symbols, maybe by
* spending a Flag on them?
*/
- 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.
@@ -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 =>
diff --git a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala
index d85708024ec6..fdc1ba9697d0 100644
--- a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala
+++ b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala
@@ -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
@@ -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.*
@@ -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)
}
@@ -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) =
@@ -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)
@@ -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
}
diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala
index 48fb1bab2da1..96912c4f7637 100644
--- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala
+++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala
@@ -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
@@ -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.
diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala
index e5cdd3b0613d..cd5fd83a0198 100644
--- a/compiler/src/dotty/tools/dotc/core/Types.scala
+++ b/compiler/src/dotty/tools/dotc/core/Types.scala
@@ -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
}
@@ -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))
}
diff --git a/compiler/src/dotty/tools/dotc/core/tasty/BestEffortTastyWriter.scala b/compiler/src/dotty/tools/dotc/core/tasty/BestEffortTastyWriter.scala
new file mode 100644
index 000000000000..9cdfb042b8fb
--- /dev/null
+++ b/compiler/src/dotty/tools/dotc/core/tasty/BestEffortTastyWriter.scala
@@ -0,0 +1,43 @@
+package dotty.tools.dotc
+package core
+package tasty
+
+import scala.language.unsafeNulls
+import java.nio.file.{Path as JPath, Files as JFiles}
+import java.nio.channels.ClosedByInterruptException
+import java.io.DataOutputStream
+import dotty.tools.io.{File, PlainFile}
+import dotty.tools.dotc.core.Contexts.Context
+
+object BestEffortTastyWriter:
+
+ def write(dir: JPath, units: List[CompilationUnit])(using Context): Unit =
+ if JFiles.exists(dir) then JFiles.createDirectories(dir)
+
+ units.foreach { unit =>
+ unit.pickled.foreach { (clz, binary) =>
+ val parts = clz.fullName.mangledString.split('.')
+ val outPath = outputPath(parts.toList, dir)
+ val outTastyFile = new PlainFile(new File(outPath))
+ val outstream = new DataOutputStream(outTastyFile.bufferedOutput)
+ try outstream.write(binary())
+ catch case ex: ClosedByInterruptException =>
+ try
+ outTastyFile.delete() // don't leave an empty or half-written tastyfile around after an interrupt
+ catch
+ case _: Throwable =>
+ throw ex
+ finally outstream.close()
+ }
+ }
+
+ def outputPath(parts: List[String], acc: JPath): JPath =
+ parts match
+ case Nil => throw new Exception("Invalid class name")
+ case last :: Nil =>
+ val name = last.stripSuffix("$")
+ acc.resolve(s"$name.betasty")
+ case pkg :: tail =>
+ val next = acc.resolve(pkg)
+ if !JFiles.exists(next) then JFiles.createDirectory(next)
+ outputPath(tail, next)
diff --git a/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala
index 4f083b09b015..3605a6cc9515 100644
--- a/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala
+++ b/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala
@@ -23,10 +23,14 @@ object DottyUnpickler {
/** Exception thrown if classfile is corrupted */
class BadSignature(msg: String) extends RuntimeException(msg)
- class TreeSectionUnpickler(compilationUnitInfo: CompilationUnitInfo, posUnpickler: Option[PositionUnpickler], commentUnpickler: Option[CommentUnpickler])
- extends SectionUnpickler[TreeUnpickler](ASTsSection) {
+ class TreeSectionUnpickler(
+ compilationUnitInfo: CompilationUnitInfo,
+ posUnpickler: Option[PositionUnpickler],
+ commentUnpickler: Option[CommentUnpickler],
+ isBestEffortTasty: Boolean
+ ) extends SectionUnpickler[TreeUnpickler](ASTsSection) {
def unpickle(reader: TastyReader, nameAtRef: NameTable): TreeUnpickler =
- new TreeUnpickler(reader, nameAtRef, compilationUnitInfo, posUnpickler, commentUnpickler)
+ new TreeUnpickler(reader, nameAtRef, compilationUnitInfo, posUnpickler, commentUnpickler, isBestEffortTasty)
}
class PositionsSectionUnpickler extends SectionUnpickler[PositionUnpickler](PositionsSection) {
@@ -46,15 +50,21 @@ object DottyUnpickler {
}
/** A class for unpickling Tasty trees and symbols.
- * @param tastyFile tasty file from which we unpickle (used for CompilationUnitInfo)
- * @param bytes the bytearray containing the Tasty file from which we unpickle
- * @param mode the tasty file contains package (TopLevel), an expression (Term) or a type (TypeTree)
+ * @param tastyFile tasty file from which we unpickle (used for CompilationUnitInfo)
+ * @param bytes the bytearray containing the Tasty file from which we unpickle
+ * @param isBestEffortTasty specifies whether file should be unpickled as a Best Effort TASTy
+ * @param mode the tasty file contains package (TopLevel), an expression (Term) or a type (TypeTree)
*/
-class DottyUnpickler(tastyFile: AbstractFile, bytes: Array[Byte], mode: UnpickleMode = UnpickleMode.TopLevel) extends ClassfileParser.Embedded with tpd.TreeProvider {
+class DottyUnpickler(
+ tastyFile: AbstractFile,
+ bytes: Array[Byte],
+ isBestEffortTasty: Boolean,
+ mode: UnpickleMode = UnpickleMode.TopLevel
+) extends ClassfileParser.Embedded with tpd.TreeProvider {
import tpd.*
import DottyUnpickler.*
- val unpickler: TastyUnpickler = new TastyUnpickler(bytes)
+ val unpickler: TastyUnpickler = new TastyUnpickler(bytes, isBestEffortTasty)
val tastyAttributes: Attributes =
unpickler.unpickle(new AttributesSectionUnpickler)
@@ -67,7 +77,7 @@ class DottyUnpickler(tastyFile: AbstractFile, bytes: Array[Byte], mode: Unpickle
private val posUnpicklerOpt = unpickler.unpickle(new PositionsSectionUnpickler)
private val commentUnpicklerOpt = unpickler.unpickle(new CommentsSectionUnpickler)
- private val treeUnpickler = unpickler.unpickle(treeSectionUnpickler(posUnpicklerOpt, commentUnpicklerOpt)).get
+ private val treeUnpickler = unpickler.unpickle(treeSectionUnpickler(posUnpicklerOpt, commentUnpicklerOpt, isBestEffortTasty)).get
/** Enter all toplevel classes and objects into their scopes
* @param roots a set of SymDenotations that should be overwritten by unpickling
@@ -78,8 +88,9 @@ class DottyUnpickler(tastyFile: AbstractFile, bytes: Array[Byte], mode: Unpickle
protected def treeSectionUnpickler(
posUnpicklerOpt: Option[PositionUnpickler],
commentUnpicklerOpt: Option[CommentUnpickler],
+ withBestEffortTasty: Boolean
): TreeSectionUnpickler =
- new TreeSectionUnpickler(compilationUnitInfo, posUnpicklerOpt, commentUnpicklerOpt)
+ new TreeSectionUnpickler(compilationUnitInfo, posUnpicklerOpt, commentUnpicklerOpt, withBestEffortTasty)
protected def computeRootTrees(using Context): List[Tree] = treeUnpickler.unpickle(mode)
diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyAnsiiPrinter.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyAnsiiPrinter.scala
index a3d8cedacb4a..3755b6e8b4b6 100644
--- a/compiler/src/dotty/tools/dotc/core/tasty/TastyAnsiiPrinter.scala
+++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyAnsiiPrinter.scala
@@ -2,9 +2,9 @@ package dotty.tools.dotc
package core
package tasty
-class TastyAnsiiPrinter(bytes: Array[Byte], testPickler: Boolean) extends TastyPrinter(bytes, testPickler) {
+class TastyAnsiiPrinter(bytes: Array[Byte], isBestEffortTasty: Boolean, testPickler: Boolean) extends TastyPrinter(bytes, isBestEffortTasty, testPickler) {
- def this(bytes: Array[Byte]) = this(bytes, testPickler = false)
+ def this(bytes: Array[Byte]) = this(bytes, isBestEffortTasty = false, testPickler = false)
override protected def nameStr(str: String): String = Console.MAGENTA + str + Console.RESET
override protected def treeStr(str: String): String = Console.YELLOW + str + Console.RESET
diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyClassName.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyClassName.scala
index 0a7068b65445..f9d8e10cf16a 100644
--- a/compiler/src/dotty/tools/dotc/core/tasty/TastyClassName.scala
+++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyClassName.scala
@@ -12,9 +12,9 @@ import TastyUnpickler.*
import dotty.tools.tasty.TastyFormat.ASTsSection
/** Reads the package and class name of the class contained in this TASTy */
-class TastyClassName(bytes: Array[Byte]) {
+class TastyClassName(bytes: Array[Byte], isBestEffortTasty: Boolean = false) {
- val unpickler: TastyUnpickler = new TastyUnpickler(bytes)
+ val unpickler: TastyUnpickler = new TastyUnpickler(bytes, isBestEffortTasty)
import unpickler.{nameAtRef, unpickle}
/** Returns a tuple with the package and class names */
diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyHTMLPrinter.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyHTMLPrinter.scala
index b234705413ae..b9cba2e09937 100644
--- a/compiler/src/dotty/tools/dotc/core/tasty/TastyHTMLPrinter.scala
+++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyHTMLPrinter.scala
@@ -2,7 +2,7 @@ package dotty.tools.dotc
package core
package tasty
-class TastyHTMLPrinter(bytes: Array[Byte]) extends TastyPrinter(bytes) {
+class TastyHTMLPrinter(bytes: Array[Byte]) extends TastyPrinter(bytes, isBestEffortTasty = false, testPickler = false) {
override protected def nameStr(str: String): String = s"$str"
override protected def treeStr(str: String): String = s"$str"
override protected def lengthStr(str: String): String = s"$str"
diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyPickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyPickler.scala
index 214f7a5f6702..e35ed5bb2466 100644
--- a/compiler/src/dotty/tools/dotc/core/tasty/TastyPickler.scala
+++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyPickler.scala
@@ -6,6 +6,7 @@ package tasty
import scala.language.unsafeNulls
import dotty.tools.tasty.{TastyBuffer, TastyFormat, TastyHash}
+import dotty.tools.tasty.besteffort.BestEffortTastyFormat
import TastyFormat.*
import TastyBuffer.*
@@ -16,7 +17,7 @@ import Decorators.*
object TastyPickler:
private val versionString = s"Scala ${config.Properties.simpleVersionString}"
-class TastyPickler(val rootCls: ClassSymbol) {
+class TastyPickler(val rootCls: ClassSymbol, isBestEffortTasty: Boolean) {
private val sections = new mutable.ArrayBuffer[(NameRef, TastyBuffer)]
@@ -42,10 +43,12 @@ class TastyPickler(val rootCls: ClassSymbol) {
val uuidHi: Long = otherSectionHashes.fold(0L)(_ ^ _)
val headerBuffer = {
- val buf = new TastyBuffer(header.length + TastyPickler.versionString.length + 32)
- for (ch <- header) buf.writeByte(ch.toByte)
+ val fileHeader = if isBestEffortTasty then BestEffortTastyFormat.bestEffortHeader else header
+ val buf = new TastyBuffer(fileHeader.length + TastyPickler.versionString.length + 32)
+ for (ch <- fileHeader) buf.writeByte(ch.toByte)
buf.writeNat(MajorVersion)
buf.writeNat(MinorVersion)
+ if isBestEffortTasty then buf.writeNat(BestEffortTastyFormat.PatchVersion)
buf.writeNat(ExperimentalVersion)
buf.writeUtf8(TastyPickler.versionString)
buf.writeUncompressedLong(uuidLow)
diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala
index 6850d87d1f4d..72f6895f122c 100644
--- a/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala
+++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala
@@ -23,34 +23,37 @@ import dotty.tools.dotc.classpath.FileUtils.hasTastyExtension
object TastyPrinter:
def showContents(bytes: Array[Byte], noColor: Boolean): String =
- showContents(bytes, noColor, testPickler = false)
+ showContents(bytes, noColor, isBestEffortTasty = false, testPickler = false)
- def showContents(bytes: Array[Byte], noColor: Boolean, testPickler: Boolean = false): String =
+ def showContents(bytes: Array[Byte], noColor: Boolean, isBestEffortTasty: Boolean, testPickler: Boolean = false): String =
val printer =
- if noColor then new TastyPrinter(bytes, testPickler)
- else new TastyAnsiiPrinter(bytes, testPickler)
+ if noColor then new TastyPrinter(bytes, isBestEffortTasty, testPickler)
+ else new TastyAnsiiPrinter(bytes, isBestEffortTasty, testPickler)
printer.showContents()
def main(args: Array[String]): Unit = {
// TODO: Decouple CliCommand from Context and use CliCommand.distill?
+ val betastyOpt = "-Ywith-best-effort-tasty"
val lineWidth = 80
val line = "-" * lineWidth
val noColor = args.contains("-color:never")
+ val allowBetasty = args.contains(betastyOpt)
var printLastLine = false
- def printTasty(fileName: String, bytes: Array[Byte]): Unit =
+ def printTasty(fileName: String, bytes: Array[Byte], isBestEffortTasty: Boolean): Unit =
println(line)
println(fileName)
println(line)
- println(showContents(bytes, noColor))
+ println(showContents(bytes, noColor, isBestEffortTasty, testPickler = false))
println()
printLastLine = true
for arg <- args do
if arg == "-color:never" then () // skip
+ else if arg == betastyOpt then () // skip
else if arg.startsWith("-") then println(s"bad option '$arg' was ignored")
- else if arg.endsWith(".tasty") then
+ else if arg.endsWith(".tasty") || (allowBetasty && arg.endsWith(".betasty")) then
val path = Paths.get(arg)
if Files.exists(path) then
- printTasty(arg, Files.readAllBytes(path).nn)
+ printTasty(arg, Files.readAllBytes(path).nn, arg.endsWith(".betasty"))
else
println("File not found: " + arg)
System.exit(1)
@@ -58,7 +61,7 @@ object TastyPrinter:
val jar = JarArchive.open(Path(arg), create = false)
try
for file <- jar.iterator() if file.hasTastyExtension do
- printTasty(s"$arg ${file.path}", file.toByteArray)
+ printTasty(s"$arg ${file.path}", file.toByteArray, isBestEffortTasty = false)
finally jar.close()
else
println(s"Not a '.tasty' or '.jar' file: $arg")
@@ -68,11 +71,11 @@ object TastyPrinter:
println(line)
}
-class TastyPrinter(bytes: Array[Byte], val testPickler: Boolean) {
+class TastyPrinter(bytes: Array[Byte], isBestEffortTasty: Boolean, val testPickler: Boolean) {
- def this(bytes: Array[Byte]) = this(bytes, testPickler = false)
+ def this(bytes: Array[Byte]) = this(bytes, isBestEffortTasty = false, testPickler = false)
- class TastyPrinterUnpickler extends TastyUnpickler(bytes) {
+ class TastyPrinterUnpickler extends TastyUnpickler(bytes, isBestEffortTasty) {
var namesStart: Addr = uninitialized
var namesEnd: Addr = uninitialized
override def readNames() = {
@@ -130,7 +133,7 @@ class TastyPrinter(bytes: Array[Byte], val testPickler: Boolean) {
})
class TreeSectionUnpickler(sb: StringBuilder) extends PrinterSectionUnpickler[Unit](ASTsSection) {
- import dotty.tools.tasty.TastyFormat.*
+ import dotty.tools.tasty.besteffort.BestEffortTastyFormat.* // superset on TastyFormat
def unpickle0(reader: TastyReader)(using refs: NameRefs): Unit = {
import reader.*
var indent = 0
diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala
index 6fe648ee98d3..f034f03298b1 100644
--- a/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala
+++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala
@@ -2,9 +2,12 @@ package dotty.tools.dotc
package core
package tasty
+import java.util.UUID
import scala.language.unsafeNulls
import dotty.tools.tasty.{TastyFormat, TastyVersion, TastyBuffer, TastyReader, TastyHeaderUnpickler, UnpicklerConfig}
+import dotty.tools.tasty.besteffort.{BestEffortTastyHeader, BestEffortTastyHeaderUnpickler}
+
import TastyFormat.NameTags.*, TastyFormat.nameTagToString
import TastyBuffer.NameRef
@@ -14,6 +17,18 @@ import NameKinds.*
import dotty.tools.tasty.TastyHeader
import dotty.tools.tasty.TastyBuffer.Addr
+case class CommonTastyHeader(
+ uuid: UUID,
+ majorVersion: Int,
+ minorVersion: Int,
+ experimentalVersion: Int,
+ toolingVersion: String
+):
+ def this(h: TastyHeader) =
+ this(h.uuid, h.majorVersion, h.minorVersion, h.experimentalVersion, h.toolingVersion)
+ def this(h: BestEffortTastyHeader) =
+ this(h.uuid, h.majorVersion, h.minorVersion, h.experimentalVersion, h.toolingVersion)
+
object TastyUnpickler {
abstract class SectionUnpickler[R](val name: String) {
@@ -63,10 +78,11 @@ object TastyUnpickler {
import TastyUnpickler.*
-class TastyUnpickler(protected val reader: TastyReader) {
+class TastyUnpickler(protected val reader: TastyReader, isBestEffortTasty: Boolean) {
import reader.*
- def this(bytes: Array[Byte]) = this(new TastyReader(bytes))
+ def this(bytes: Array[Byte]) = this(new TastyReader(bytes), false)
+ def this(bytes: Array[Byte], isBestEffortTasty: Boolean) = this(new TastyReader(bytes), isBestEffortTasty)
private val sectionReader = new mutable.HashMap[String, TastyReader]
val nameAtRef: NameTable = new NameTable
@@ -123,8 +139,11 @@ class TastyUnpickler(protected val reader: TastyReader) {
result
}
- val header: TastyHeader =
- new TastyHeaderUnpickler(scala3CompilerConfig, reader).readFullHeader()
+ val header: CommonTastyHeader =
+ if isBestEffortTasty then
+ new CommonTastyHeader(new BestEffortTastyHeaderUnpickler(scala3CompilerConfig, reader).readFullHeader())
+ else
+ new CommonTastyHeader(new TastyHeaderUnpickler(reader).readFullHeader())
def readNames(): Unit =
until(readEnd()) { nameAtRef.add(readNameContents()) }
diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala
index 0a8669292a74..be616b9054ae 100644
--- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala
+++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala
@@ -6,6 +6,7 @@ package tasty
import scala.language.unsafeNulls
import dotty.tools.tasty.TastyFormat.*
+import dotty.tools.tasty.besteffort.BestEffortTastyFormat.ERRORtype
import dotty.tools.tasty.TastyBuffer.*
import ast.Trees.*
@@ -65,6 +66,17 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) {
fillRef(lengthAddr, currentAddr, relative = true)
}
+ /** There are certain expectations with code which is naturally able to reach
+ * pickling phase as opposed to one that uses best-effort compilation features.
+ * When pickling betasty files, we do some custom checks, in case those
+ * expectations cannot be fulfilled, and if so, then we can try to do something
+ * else (usually pickle an ERRORtype).
+ * For regular non best-effort compilation (without -Ybest-effort with thrown errors
+ * and without using .betasty on classpath), this will always return true.
+ */
+ private inline def passesConditionForErroringBestEffortCode(condition: => Boolean)(using Context): Boolean =
+ !((ctx.isBestEffort && ctx.reporter.errorsReported) || ctx.usedBestEffortTasty) || condition
+
def addrOfSym(sym: Symbol): Option[Addr] =
symRefs.get(sym)
@@ -295,9 +307,13 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) {
else if tpe.isImplicitMethod then mods |= Implicit
pickleMethodic(METHODtype, tpe, mods)
case tpe: ParamRef =>
- assert(pickleParamRef(tpe), s"orphan parameter reference: $tpe")
+ val pickled = pickleParamRef(tpe)
+ if !ctx.isBestEffort then assert(pickled, s"orphan parameter reference: $tpe")
+ else if !pickled then pickleErrorType()
case tpe: LazyRef =>
pickleType(tpe.ref)
+ case _ if ctx.isBestEffort =>
+ pickleErrorType()
}
def pickleMethodic(tag: Int, tpe: LambdaType, mods: FlagSet)(using Context): Unit = {
@@ -321,8 +337,13 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) {
pickled
}
+ def pickleErrorType(): Unit = {
+ writeByte(ERRORtype)
+ }
+
def pickleTpt(tpt: Tree)(using Context): Unit =
- pickleTree(tpt)
+ if passesConditionForErroringBestEffortCode(tpt.isType) then pickleTree(tpt)
+ else pickleErrorType()
def pickleTreeUnlessEmpty(tree: Tree)(using Context): Unit = {
if (!tree.isEmpty) pickleTree(tree)
@@ -336,39 +357,45 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) {
def pickleDef(tag: Int, mdef: MemberDef, tpt: Tree, rhs: Tree = EmptyTree, pickleParams: => Unit = ())(using Context): Unit = {
val sym = mdef.symbol
- assert(symRefs(sym) == NoAddr, sym)
- registerDef(sym)
- writeByte(tag)
- val addr = currentAddr
- try
- withLength {
- pickleName(sym.name)
- pickleParams
- tpt match {
- case _: Template | _: Hole => pickleTree(tpt)
- case _ if tpt.isType => pickleTpt(tpt)
+ def isDefSymPreRegisteredAndTreeHasCorrectStructure() =
+ symRefs.get(sym) == Some(NoAddr) && // check if symbol id preregistered (with the preRegister method)
+ !(tag == TYPEDEF && tpt.isInstanceOf[Template] && !tpt.symbol.exists) // in case this is a TEMPLATE, check if we are able to pickle it
+
+ if passesConditionForErroringBestEffortCode(isDefSymPreRegisteredAndTreeHasCorrectStructure()) then
+ assert(symRefs(sym) == NoAddr, sym)
+ registerDef(sym)
+ writeByte(tag)
+ val addr = currentAddr
+ try
+ withLength {
+ pickleName(sym.name)
+ pickleParams
+ tpt match {
+ case _: Template | _: Hole => pickleTree(tpt)
+ case _ if tpt.isType => pickleTpt(tpt)
+ case _ if ctx.isBestEffort => pickleErrorType()
+ }
+ if isOutlinePickle && sym.isTerm && isJavaPickle then
+ // TODO: if we introduce outline typing for Scala definitions
+ // then we will need to update the check here
+ pickleElidedUnlessEmpty(rhs, tpt.tpe)
+ else
+ pickleTreeUnlessEmpty(rhs)
+ pickleModifiers(sym, mdef)
}
- if isOutlinePickle && sym.isTerm && isJavaPickle then
- // TODO: if we introduce outline typing for Scala definitions
- // then we will need to update the check here
- pickleElidedUnlessEmpty(rhs, tpt.tpe)
- else
- pickleTreeUnlessEmpty(rhs)
- pickleModifiers(sym, mdef)
- }
- catch
- case ex: Throwable =>
- if !ctx.settings.YnoDecodeStacktraces.value
- && handleRecursive.underlyingStackOverflowOrNull(ex) != null then
- throw StackSizeExceeded(mdef)
- else
- throw ex
- if sym.is(Method) && sym.owner.isClass then
- profile.recordMethodSize(sym, (currentAddr.index - addr.index) max 1, mdef.span)
- for docCtx <- ctx.docCtx do
- val comment = docCtx.docstrings.lookup(sym)
- if comment != null then
- docStrings(mdef) = comment
+ catch
+ case ex: Throwable =>
+ if !ctx.settings.YnoDecodeStacktraces.value
+ && handleRecursive.underlyingStackOverflowOrNull(ex) != null then
+ throw StackSizeExceeded(mdef)
+ else
+ throw ex
+ if sym.is(Method) && sym.owner.isClass then
+ profile.recordMethodSize(sym, (currentAddr.index - addr.index) max 1, mdef.span)
+ for docCtx <- ctx.docCtx do
+ val comment = docCtx.docstrings.lookup(sym)
+ if comment != null then
+ docStrings(mdef) = comment
}
def pickleParam(tree: Tree)(using Context): Unit = {
@@ -398,15 +425,17 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) {
else
try tree match {
case Ident(name) =>
- tree.tpe match {
- case tp: TermRef if name != nme.WILDCARD =>
- // wildcards are pattern bound, need to be preserved as ids.
- pickleType(tp)
- case tp =>
- writeByte(if (tree.isType) IDENTtpt else IDENT)
- pickleName(name)
- pickleType(tp)
- }
+ if passesConditionForErroringBestEffortCode(tree.hasType) then
+ tree.tpe match {
+ case tp: TermRef if name != nme.WILDCARD =>
+ // wildcards are pattern bound, need to be preserved as ids.
+ pickleType(tp)
+ case tp =>
+ writeByte(if (tree.isType) IDENTtpt else IDENT)
+ pickleName(name)
+ pickleType(tp)
+ }
+ else pickleErrorType()
case This(qual) =>
// This may be needed when pickling a `This` inside a capture set. See #19662 and #19859.
// In this case, we pickle the tree as null.asInstanceOf[tree.tpe].
@@ -422,6 +451,8 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) {
case ThisType(tref) =>
writeByte(QUALTHIS)
pickleTree(qual.withType(tref))
+ case _: ErrorType if ctx.isBestEffort =>
+ pickleTree(qual)
case _ => pickleCapturedThis
case Select(qual, name) =>
name match {
@@ -434,25 +465,31 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) {
pickleType(tp)
}
case _ =>
- val sig = tree.tpe.signature
- var ename = tree.symbol.targetName
- val selectFromQualifier =
- name.isTypeName
- || qual.isInstanceOf[Hole] // holes have no symbol
- || sig == Signature.NotAMethod // no overload resolution necessary
- || !tree.denot.symbol.exists // polymorphic function type
- || tree.denot.asSingleDenotation.isRefinedMethod // refined methods have no defining class symbol
- if selectFromQualifier then
+ if passesConditionForErroringBestEffortCode(tree.hasType) then
+ val sig = tree.tpe.signature
+ var ename = tree.symbol.targetName
+ val selectFromQualifier =
+ name.isTypeName
+ || qual.isInstanceOf[Hole] // holes have no symbol
+ || sig == Signature.NotAMethod // no overload resolution necessary
+ || !tree.denot.symbol.exists // polymorphic function type
+ || tree.denot.asSingleDenotation.isRefinedMethod // refined methods have no defining class symbol
+ if selectFromQualifier then
+ writeByte(if name.isTypeName then SELECTtpt else SELECT)
+ pickleNameAndSig(name, sig, ename)
+ pickleTree(qual)
+ else // select from owner
+ writeByte(SELECTin)
+ withLength {
+ pickleNameAndSig(name, tree.symbol.signature, ename)
+ pickleTree(qual)
+ pickleType(tree.symbol.owner.typeRef)
+ }
+ else
writeByte(if name.isTypeName then SELECTtpt else SELECT)
- pickleNameAndSig(name, sig, ename)
+ val ename = tree.symbol.targetName
+ pickleNameAndSig(name, Signature.NotAMethod, ename)
pickleTree(qual)
- else // select from owner
- writeByte(SELECTin)
- withLength {
- pickleNameAndSig(name, tree.symbol.signature, ename)
- pickleTree(qual)
- pickleType(tree.symbol.owner.typeRef)
- }
}
case Apply(fun, args) =>
if (fun.symbol eq defn.throwMethod) {
@@ -480,12 +517,14 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) {
args.foreach(pickleTpt)
}
case Literal(const1) =>
- pickleConstant {
- tree.tpe match {
- case ConstantType(const2) => const2
- case _ => const1
+ if passesConditionForErroringBestEffortCode(tree.hasType) then
+ pickleConstant {
+ tree.tpe match {
+ case ConstantType(const2) => const2
+ case _ => const1
+ }
}
- }
+ else pickleConstant(const1)
case Super(qual, mix) =>
writeByte(SUPER)
withLength {
@@ -657,19 +696,22 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) {
writeByte(PACKAGE)
withLength { pickleType(pid.tpe); pickleStats(stats) }
case tree: TypeTree =>
- pickleType(tree.tpe)
+ if passesConditionForErroringBestEffortCode(tree.hasType) then pickleType(tree.tpe)
+ else pickleErrorType()
case SingletonTypeTree(ref) =>
writeByte(SINGLETONtpt)
pickleTree(ref)
case RefinedTypeTree(parent, refinements) =>
if (refinements.isEmpty) pickleTree(parent)
else {
- val refineCls = refinements.head.symbol.owner.asClass
- registerDef(refineCls)
- pickledTypes(refineCls.typeRef) = currentAddr
- writeByte(REFINEDtpt)
- refinements.foreach(preRegister)
- withLength { pickleTree(parent); refinements.foreach(pickleTree) }
+ if passesConditionForErroringBestEffortCode(refinements.head.symbol.exists) then
+ val refineCls = refinements.head.symbol.owner.asClass
+ registerDef(refineCls)
+ pickledTypes(refineCls.typeRef) = currentAddr
+ writeByte(REFINEDtpt)
+ refinements.foreach(preRegister)
+ withLength { pickleTree(parent); refinements.foreach(pickleTree) }
+ else pickleErrorType()
}
case AppliedTypeTree(tycon, args) =>
writeByte(APPLIEDtpt)
@@ -735,10 +777,13 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) {
pickleTree(arg)
}
}
+ case other if ctx.isBestEffort =>
+ pickleErrorType()
}
catch {
case ex: TypeError =>
report.error(ex.toMessage, tree.srcPos.focus)
+ pickleErrorType()
case ex: AssertionError =>
println(i"error when pickling tree $tree")
throw ex
@@ -840,6 +885,7 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) {
// a different toplevel class, it is impossible to pickle a reference to it.
// Such annotations will be reconstituted when unpickling the child class.
// See tests/pickling/i3149.scala
+ case _ if ctx.isBestEffort && !ann.symbol.denot.isError => true
case _ =>
ann.symbol == defn.BodyAnnot // inline bodies are reconstituted automatically when unpickling
}
diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala
index 073edb536151..45bd58e3c7c1 100644
--- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala
+++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala
@@ -42,6 +42,7 @@ import scala.collection.mutable
import config.Printers.pickling
import dotty.tools.tasty.TastyFormat.*
+import dotty.tools.tasty.besteffort.BestEffortTastyFormat.ERRORtype
import scala.annotation.constructorOnly
import scala.annotation.internal.sharable
@@ -53,12 +54,14 @@ import scala.compiletime.uninitialized
* @param posUnpicklerOpt the unpickler for positions, if it exists
* @param commentUnpicklerOpt the unpickler for comments, if it exists
* @param attributeUnpicklerOpt the unpickler for attributes, if it exists
+ * @param isBestEffortTasty decides whether to unpickle as a Best Effort TASTy
*/
class TreeUnpickler(reader: TastyReader,
nameAtRef: NameTable,
compilationUnitInfo: CompilationUnitInfo,
posUnpicklerOpt: Option[PositionUnpickler],
- commentUnpicklerOpt: Option[CommentUnpickler]) {
+ commentUnpicklerOpt: Option[CommentUnpickler],
+ isBestEffortTasty: Boolean = false) {
import TreeUnpickler.*
import tpd.*
@@ -444,6 +447,9 @@ class TreeUnpickler(reader: TastyReader,
}
case FLEXIBLEtype =>
FlexibleType(readType())
+ case _ if isBestEffortTasty =>
+ goto(end)
+ new PreviousErrorType
}
assert(currentAddr == end, s"$start $currentAddr $end ${astTagToString(tag)}")
result
@@ -491,6 +497,9 @@ class TreeUnpickler(reader: TastyReader,
typeAtAddr.getOrElseUpdate(ref, forkAt(ref).readType())
case BYNAMEtype =>
ExprType(readType())
+ case ERRORtype =>
+ if isBestEffortTasty then new PreviousErrorType
+ else throw new Error(s"Illegal ERRORtype in non Best Effort TASTy file")
case _ =>
ConstantType(readConstant(tag))
}
@@ -946,6 +955,7 @@ class TreeUnpickler(reader: TastyReader,
val rhs = readTpt()(using localCtx)
sym.info = new NoCompleter:
+ override def complete(denot: SymDenotation)(using Context): Unit = if !isBestEffortTasty then unsupported("complete")
override def completerTypeParams(sym: Symbol)(using Context) =
rhs.tpe.typeParams
@@ -1021,8 +1031,14 @@ class TreeUnpickler(reader: TastyReader,
case nu: New =>
try nu.tpe
finally goto(end)
+ case other if isBestEffortTasty =>
+ try other.tpe
+ finally goto(end)
case SHAREDterm =>
forkAt(readAddr()).readParentType()
+ case SELECT if isBestEffortTasty =>
+ goto(readEnd())
+ new PreviousErrorType
/** Read template parents
* @param withArgs if false, only read enough of parent trees to determine their type
@@ -1246,6 +1262,7 @@ class TreeUnpickler(reader: TastyReader,
case path: TermRef => ref(path)
case path: ThisType => untpd.This(untpd.EmptyTypeIdent).withType(path)
case path: ConstantType => Literal(path.value)
+ case path: ErrorType if isBestEffortTasty => TypeTree(path)
}
}
diff --git a/compiler/src/dotty/tools/dotc/fromtasty/TASTYRun.scala b/compiler/src/dotty/tools/dotc/fromtasty/TASTYRun.scala
index 8ad9afb7d512..d01f60571601 100644
--- a/compiler/src/dotty/tools/dotc/fromtasty/TASTYRun.scala
+++ b/compiler/src/dotty/tools/dotc/fromtasty/TASTYRun.scala
@@ -27,6 +27,8 @@ class TASTYRun(comp: Compiler, ictx: Context) extends Run(comp, ictx) {
.map(e => e.stripSuffix(".tasty").replace("/", "."))
.toList
case FileExtension.Tasty => TastyFileUtil.getClassName(file)
+ case FileExtension.Betasty if ctx.withBestEffortTasty =>
+ TastyFileUtil.getClassName(file, withBestEffortTasty = true)
case _ =>
report.error(em"File extension is not `tasty` or `jar`: ${file.path}")
Nil
diff --git a/compiler/src/dotty/tools/dotc/fromtasty/TastyFileUtil.scala b/compiler/src/dotty/tools/dotc/fromtasty/TastyFileUtil.scala
index d3a9550c4491..b1277accc621 100644
--- a/compiler/src/dotty/tools/dotc/fromtasty/TastyFileUtil.scala
+++ b/compiler/src/dotty/tools/dotc/fromtasty/TastyFileUtil.scala
@@ -7,6 +7,7 @@ import dotty.tools.dotc.core.tasty.TastyClassName
import dotty.tools.dotc.core.StdNames.nme.EMPTY_PACKAGE
import dotty.tools.io.AbstractFile
import dotty.tools.dotc.classpath.FileUtils.hasTastyExtension
+import dotty.tools.dotc.classpath.FileUtils.hasBetastyExtension
object TastyFileUtil {
/** Get the class path of a tasty file
@@ -18,9 +19,10 @@ object TastyFileUtil {
* ```
* then `getClassName("./out/foo/Foo.tasty") returns `Some("./out")`
*/
- def getClassPath(file: AbstractFile): Option[String] =
- getClassName(file).map { className =>
- val classInPath = className.replace(".", java.io.File.separator) + ".tasty"
+ def getClassPath(file: AbstractFile, fromBestEffortTasty: Boolean = false): Option[String] =
+ getClassName(file, fromBestEffortTasty).map { className =>
+ val extension = if (fromBestEffortTasty) then ".betasty" else ".tasty"
+ val classInPath = className.replace(".", java.io.File.separator) + extension
file.path.replace(classInPath, "")
}
@@ -33,11 +35,11 @@ object TastyFileUtil {
* ```
* then `getClassName("./out/foo/Foo.tasty") returns `Some("foo.Foo")`
*/
- def getClassName(file: AbstractFile): Option[String] = {
+ def getClassName(file: AbstractFile, withBestEffortTasty: Boolean = false): Option[String] = {
assert(file.exists)
- assert(file.hasTastyExtension)
+ assert(file.hasTastyExtension || (withBestEffortTasty && file.hasBetastyExtension))
val bytes = file.toByteArray
- val names = new TastyClassName(bytes).readName()
+ val names = new TastyClassName(bytes, file.hasBetastyExtension).readName()
names.map { case (packageName, className) =>
val fullName = packageName match {
case EMPTY_PACKAGE => s"${className.lastPart}"
diff --git a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala
index 1a884f2bd10b..629bc2ed3b16 100644
--- a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala
+++ b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala
@@ -834,7 +834,7 @@ class Inliner(val call: tpd.Tree)(using Context):
override def typedSplice(tree: untpd.Splice, pt: Type)(using Context): Tree =
super.typedSplice(tree, pt) match
- case tree1 @ Splice(expr) if level == 0 && !hasInliningErrors =>
+ case tree1 @ Splice(expr) if level == 0 && !hasInliningErrors && !ctx.usedBestEffortTasty =>
val expanded = expandMacro(expr, tree1.srcPos)
transform.TreeChecker.checkMacroGeneratedTree(tree1, expanded)
typedExpr(expanded) // Inline calls and constant fold code generated by the macro
diff --git a/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala b/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala
index 8ebd1f6973f2..6d6e2ff01ad4 100644
--- a/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala
+++ b/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala
@@ -217,7 +217,7 @@ object PickledQuotes {
/** Pickle tree into it's TASTY bytes s*/
private def pickle(tree: Tree)(using Context): Array[Byte] = {
quotePickling.println(i"**** pickling quote of\n$tree")
- val pickler = new TastyPickler(defn.RootClass)
+ val pickler = new TastyPickler(defn.RootClass, isBestEffortTasty = false)
val treePkl = new TreePickler(pickler, Attributes.empty)
treePkl.pickle(tree :: Nil)
treePkl.compactify()
@@ -229,7 +229,7 @@ object PickledQuotes {
positionWarnings.foreach(report.warning(_))
val pickled = pickler.assembleParts()
- quotePickling.println(s"**** pickled quote\n${TastyPrinter.showContents(pickled, ctx.settings.color.value == "never")}")
+ quotePickling.println(s"**** pickled quote\n${TastyPrinter.showContents(pickled, ctx.settings.color.value == "never", isBestEffortTasty = false)}")
pickled
}
@@ -266,10 +266,10 @@ object PickledQuotes {
inContext(unpicklingContext) {
- quotePickling.println(s"**** unpickling quote from TASTY\n${TastyPrinter.showContents(bytes, ctx.settings.color.value == "never")}")
+ quotePickling.println(s"**** unpickling quote from TASTY\n${TastyPrinter.showContents(bytes, ctx.settings.color.value == "never", isBestEffortTasty = false)}")
val mode = if (isType) UnpickleMode.TypeTree else UnpickleMode.Term
- val unpickler = new DottyUnpickler(NoAbstractFile, bytes, mode)
+ val unpickler = new DottyUnpickler(NoAbstractFile, bytes, isBestEffortTasty = false, mode)
unpickler.enter(Set.empty)
val tree = unpickler.tree
diff --git a/compiler/src/dotty/tools/dotc/report.scala b/compiler/src/dotty/tools/dotc/report.scala
index a63b6569fefe..10b0023992fe 100644
--- a/compiler/src/dotty/tools/dotc/report.scala
+++ b/compiler/src/dotty/tools/dotc/report.scala
@@ -81,6 +81,22 @@ object report:
if ctx.settings.YdebugError.value then Thread.dumpStack()
if ctx.settings.YdebugTypeError.value then ex.printStackTrace()
+ def bestEffortError(ex: Throwable, msg: String)(using Context): Unit =
+ val stackTrace =
+ Option(ex.getStackTrace()).map { st =>
+ if st.nn.isEmpty then ""
+ else s"Stack trace: \n ${st.nn.mkString("\n ")}".stripMargin
+ }.getOrElse("")
+ // Build tools and dotty's test framework may check precisely for
+ // "Unsuccessful best-effort compilation." error text.
+ val fullMsg =
+ em"""Unsuccessful best-effort compilation.
+ |${msg}
+ |Cause:
+ | ${ex.toString.replace("\n", "\n ")}
+ |${stackTrace}"""
+ ctx.reporter.report(new Error(fullMsg, NoSourcePosition))
+
def errorOrMigrationWarning(msg: Message, pos: SrcPos, migrationVersion: MigrationVersion)(using Context): Unit =
if sourceVersion.isAtLeast(migrationVersion.errorFrom) then
if !sourceVersion.isMigrating then error(msg, pos)
diff --git a/compiler/src/dotty/tools/dotc/semanticdb/ExtractSemanticDB.scala b/compiler/src/dotty/tools/dotc/semanticdb/ExtractSemanticDB.scala
index 77eef4564bbf..357202229e50 100644
--- a/compiler/src/dotty/tools/dotc/semanticdb/ExtractSemanticDB.scala
+++ b/compiler/src/dotty/tools/dotc/semanticdb/ExtractSemanticDB.scala
@@ -56,7 +56,7 @@ class ExtractSemanticDB private (phaseMode: ExtractSemanticDB.PhaseMode) extends
override def isRunnable(using Context) =
import ExtractSemanticDB.{semanticdbTarget, outputDirectory}
def writesToOutputJar = semanticdbTarget.isEmpty && outputDirectory.isInstanceOf[JarArchive]
- super.isRunnable && ctx.settings.Xsemanticdb.value && !writesToOutputJar
+ (super.isRunnable || ctx.isBestEffort) && ctx.settings.Xsemanticdb.value && !writesToOutputJar
// Check not needed since it does not transform trees
override def isCheckable: Boolean = false
diff --git a/compiler/src/dotty/tools/dotc/transform/MacroTransform.scala b/compiler/src/dotty/tools/dotc/transform/MacroTransform.scala
index 887a962f7a65..137fbf4f837c 100644
--- a/compiler/src/dotty/tools/dotc/transform/MacroTransform.scala
+++ b/compiler/src/dotty/tools/dotc/transform/MacroTransform.scala
@@ -13,6 +13,8 @@ abstract class MacroTransform extends Phase {
import ast.tpd.*
+ override def isRunnable(using Context) = super.isRunnable && !ctx.usedBestEffortTasty
+
override def run(using Context): Unit = {
val unit = ctx.compilationUnit
unit.tpdTree = atPhase(transformPhase)(newTransformer.transform(unit.tpdTree))
diff --git a/compiler/src/dotty/tools/dotc/transform/MegaPhase.scala b/compiler/src/dotty/tools/dotc/transform/MegaPhase.scala
index 252babe7058f..86acd009fd09 100644
--- a/compiler/src/dotty/tools/dotc/transform/MegaPhase.scala
+++ b/compiler/src/dotty/tools/dotc/transform/MegaPhase.scala
@@ -136,6 +136,8 @@ object MegaPhase {
override def run(using Context): Unit =
singletonGroup.run
+
+ override def isRunnable(using Context): Boolean = super.isRunnable && !ctx.usedBestEffortTasty
}
}
import MegaPhase.*
@@ -164,6 +166,8 @@ class MegaPhase(val miniPhases: Array[MiniPhase]) extends Phase {
relaxedTypingCache
}
+ override def isRunnable(using Context): Boolean = super.isRunnable && !ctx.usedBestEffortTasty
+
private val cpy: TypedTreeCopier = cpyBetweenPhases
/** Transform node using all phases in this group that have idxInGroup >= start */
diff --git a/compiler/src/dotty/tools/dotc/transform/Pickler.scala b/compiler/src/dotty/tools/dotc/transform/Pickler.scala
index 6fe687072828..8bb396ca4081 100644
--- a/compiler/src/dotty/tools/dotc/transform/Pickler.scala
+++ b/compiler/src/dotty/tools/dotc/transform/Pickler.scala
@@ -32,6 +32,7 @@ import dotty.tools.dotc.sbt.asyncZincPhasesCompleted
import scala.concurrent.ExecutionContext
import scala.util.control.NonFatal
import java.util.concurrent.atomic.AtomicBoolean
+import java.nio.file.Files
object Pickler {
val name: String = "pickler"
@@ -191,7 +192,11 @@ class Pickler extends Phase {
// No need to repickle trees coming from TASTY, however in the case that we need to write TASTy to early-output,
// then we need to run this phase to send the tasty from compilation units to the early-output.
override def isRunnable(using Context): Boolean =
- super.isRunnable && (!ctx.settings.fromTasty.value || doAsyncTasty)
+ (super.isRunnable || ctx.isBestEffort)
+ && (!ctx.settings.fromTasty.value || doAsyncTasty)
+ && (!ctx.usedBestEffortTasty || ctx.isBestEffort)
+ // we do not want to pickle `.betasty` if do not plan to actually create the
+ // betasty file (as signified by the -Ybest-effort option)
// when `-Yjava-tasty` is set we actually want to run this phase on Java sources
override def skipIfJava(using Context): Boolean = false
@@ -238,7 +243,8 @@ class Pickler extends Phase {
private val executor = Executor[Array[Byte]]()
- private def useExecutor(using Context) = Pickler.ParallelPickling && !ctx.settings.YtestPickler.value
+ private def useExecutor(using Context) =
+ Pickler.ParallelPickling && !ctx.isBestEffort && !ctx.settings.YtestPickler.value
private def printerContext(isOutline: Boolean)(using Context): Context =
if isOutline then ctx.fresh.setPrinterFn(OutlinePrinter(_))
@@ -257,6 +263,7 @@ class Pickler extends Phase {
override def run(using Context): Unit = {
val unit = ctx.compilationUnit
+ val isBestEffort = ctx.reporter.errorsReported || ctx.usedBestEffortTasty
pickling.println(i"unpickling in run ${ctx.runId}")
if ctx.settings.fromTasty.value then
@@ -292,9 +299,16 @@ class Pickler extends Phase {
isOutline = isOutline
)
- val pickler = new TastyPickler(cls)
+ val pickler = new TastyPickler(cls, isBestEffortTasty = isBestEffort)
val treePkl = new TreePickler(pickler, attributes)
- treePkl.pickle(tree :: Nil)
+ val successful =
+ try
+ treePkl.pickle(tree :: Nil)
+ true
+ catch
+ case NonFatal(ex) if ctx.isBestEffort =>
+ report.bestEffortError(ex, "Some best-effort tasty files will not be generated.")
+ false
Profile.current.recordTasty(treePkl.buf.length)
val positionWarnings = new mutable.ListBuffer[Message]()
@@ -329,7 +343,7 @@ class Pickler extends Phase {
// println(i"rawBytes = \n$rawBytes%\n%") // DEBUG
if ctx.settings.YprintTasty.value || pickling != noPrinter then
println(i"**** pickled info of $cls")
- println(TastyPrinter.showContents(pickled, ctx.settings.color.value == "never"))
+ println(TastyPrinter.showContents(pickled, ctx.settings.color.value == "never", isBestEffortTasty = false))
println(i"**** end of pickled info of $cls")
if fastDoAsyncTasty then
@@ -339,26 +353,27 @@ class Pickler extends Phase {
}
}
- /** A function that returns the pickled bytes. Depending on `Pickler.ParallelPickling`
- * either computes the pickled data in a future or eagerly before constructing the
- * function value.
- */
- val demandPickled: () => Array[Byte] =
- if useExecutor then
- val futurePickled = executor.schedule(computePickled)
- () =>
- try futurePickled.force.get
- finally reportPositionWarnings()
- else
- val pickled = computePickled()
- reportPositionWarnings()
- if ctx.settings.YtestPickler.value then
- pickledBytes(cls) = (unit, pickled)
- if ctx.settings.YtestPicklerCheck.value then
- printedTasty(cls) = TastyPrinter.showContents(pickled, noColor = true, testPickler = true)
- () => pickled
-
- unit.pickled += (cls -> demandPickled)
+ if successful then
+ /** A function that returns the pickled bytes. Depending on `Pickler.ParallelPickling`
+ * either computes the pickled data in a future or eagerly before constructing the
+ * function value.
+ */
+ val demandPickled: () => Array[Byte] =
+ if useExecutor then
+ val futurePickled = executor.schedule(computePickled)
+ () =>
+ try futurePickled.force.get
+ finally reportPositionWarnings()
+ else
+ val pickled = computePickled()
+ reportPositionWarnings()
+ if ctx.settings.YtestPickler.value then
+ pickledBytes(cls) = (unit, pickled)
+ if ctx.settings.YtestPicklerCheck.value then
+ printedTasty(cls) = TastyPrinter.showContents(pickled, noColor = true, isBestEffortTasty = false, testPickler = true)
+ () => pickled
+
+ unit.pickled += (cls -> demandPickled)
end for
}
@@ -396,6 +411,13 @@ class Pickler extends Phase {
.setReporter(new ThrowingReporter(ctx.reporter))
.addMode(Mode.ReadPositions)
)
+ if ctx.isBestEffort then
+ val outpath =
+ ctx.settings.outputDir.value.jpath.toAbsolutePath.nn.normalize.nn
+ .resolve("META-INF").nn
+ .resolve("best-effort")
+ Files.createDirectories(outpath)
+ BestEffortTastyWriter.write(outpath.nn, result)
result
}
@@ -405,7 +427,7 @@ class Pickler extends Phase {
val resolveCheck = ctx.settings.YtestPicklerCheck.value
val unpicklers =
for ((cls, (unit, bytes)) <- pickledBytes) yield {
- val unpickler = new DottyUnpickler(unit.source.file, bytes)
+ val unpickler = new DottyUnpickler(unit.source.file, bytes, isBestEffortTasty = false)
unpickler.enter(roots = Set.empty)
val optCheck =
if resolveCheck then
diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala
index 612bd22ef19d..9d0150f49a1f 100644
--- a/compiler/src/dotty/tools/dotc/typer/Typer.scala
+++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala
@@ -4322,6 +4322,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
* tree that went unreported. A scenario where this happens is i1802.scala.
*/
def ensureReported(tp: Type) = tp match {
+ case err: PreviousErrorType if ctx.usedBestEffortTasty => // do nothing if error was already reported in previous compilation
case err: ErrorType if !ctx.reporter.errorsReported => report.error(err.msg, tree.srcPos)
case _ =>
}
diff --git a/compiler/src/dotty/tools/io/FileExtension.scala b/compiler/src/dotty/tools/io/FileExtension.scala
index 9d239477aed3..3aeef5b902ce 100644
--- a/compiler/src/dotty/tools/io/FileExtension.scala
+++ b/compiler/src/dotty/tools/io/FileExtension.scala
@@ -5,6 +5,7 @@ import dotty.tools.dotc.util.EnumFlags.FlagSet
enum FileExtension(val toLowerCase: String):
case Tasty extends FileExtension("tasty")
+ case Betasty extends FileExtension("betasty")
case Class extends FileExtension("class")
case Jar extends FileExtension("jar")
case Scala extends FileExtension("scala")
@@ -24,6 +25,8 @@ enum FileExtension(val toLowerCase: String):
/** represents `".tasty"` */
def isTasty = this == Tasty
+ /** represents `".betasty"` */
+ def isBetasty = this == Betasty
/** represents `".class"` */
def isClass = this == Class
/** represents `".scala"` */
@@ -60,6 +63,7 @@ object FileExtension:
case "java" => Java
case "zip" => Zip
case "inc" => Inc
+ case "betasty" => Betasty
case _ => slowLookup(s)
// slower than initialLookup, keep in sync with initialLookup
@@ -72,6 +76,7 @@ object FileExtension:
else if s.equalsIgnoreCase("java") then Java
else if s.equalsIgnoreCase("zip") then Zip
else if s.equalsIgnoreCase("inc") then Inc
+ else if s.equalsIgnoreCase("betasty") then Betasty
else External(s)
def from(s: String): FileExtension =
diff --git a/compiler/src/dotty/tools/tasty/besteffort/BestEffortTastyFormat.scala b/compiler/src/dotty/tools/tasty/besteffort/BestEffortTastyFormat.scala
new file mode 100644
index 000000000000..99a24ce5f346
--- /dev/null
+++ b/compiler/src/dotty/tools/tasty/besteffort/BestEffortTastyFormat.scala
@@ -0,0 +1,45 @@
+package dotty.tools.tasty.besteffort
+
+import dotty.tools.tasty.TastyFormat
+
+/*************************************************************************************
+Best Effort TASTy (.betasty) format extends the TASTy grammar with additional
+terminal symbols and productions. Grammar notation is kept from the regular TASTy.
+However, the lowercase prefixes describing the semantics (but otherwise not affecting
+the grammar) may not always hold.
+
+The following are the added terminal Symbols to the grammar:
+ * `ERRORtype` - representing an error from a previous compilation
+
+The following are the added productions to the grammar:
+
+ Standard-Section: "ASTs"
+```none
+ Type = ERRORtype
+ Path = ERRORtype
+```
+**************************************************************************************/
+object BestEffortTastyFormat {
+ export TastyFormat.{astTagToString => _, *}
+
+ /** First four bytes of a best effort TASTy file, used instead of the regular header.
+ * Signifies that the TASTy can only be consumed by the compiler in the best effort mode.
+ * Other than that, versioning works as usual, disallowing Best Effort Tasty from older minor versions.
+ */
+ final val bestEffortHeader: Array[Int] = Array(0x5C, 0xA1, 0xAB, 0x20)
+
+ /** Natural number. Along with MajorVersion, MinorVersion and ExperimentalVersion
+ * numbers specifies the Best Effort TASTy format. For now, Best Effort TASTy holds
+ * no compatibility guarantees, making this a reserved space for when this would have
+ * to be changed.
+ */
+ final val PatchVersion: Int = 0
+
+ // added AST tag - Best Effort TASTy only
+ final val ERRORtype = 50
+
+ def astTagToString(tag: Int) = tag match {
+ case ERRORtype => "ERRORtype"
+ case _ => TastyFormat.astTagToString(tag)
+ }
+}
diff --git a/compiler/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala b/compiler/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala
new file mode 100644
index 000000000000..4325f55be4a7
--- /dev/null
+++ b/compiler/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala
@@ -0,0 +1,175 @@
+package dotty.tools.tasty.besteffort
+
+import java.util.UUID
+
+import BestEffortTastyFormat.{MajorVersion, MinorVersion, ExperimentalVersion, bestEffortHeader, header}
+import dotty.tools.tasty.{UnpicklerConfig, TastyHeaderUnpickler, TastyReader, UnpickleException, TastyFormat, TastyVersion}
+
+/**
+ * The Best Effort Tasty Header consists of six fields:
+ * - uuid
+ * - contains a hash of the sections of the Best Effort TASTy file
+ * - majorVersion
+ * - matching the TASTy format version that last broke backwards compatibility
+ * - minorVersion
+ * - matching the TASTy format version that last broke forward compatibility
+ * - patchVersion
+ * - specyfing the best effort TASTy version. Currently unused, kept as a reserved space.
+ * Empty if it was serialized as a regular TASTy file with reagular tasty header.
+ * - experimentalVersion
+ * - 0 for final compiler version
+ * - positive for between minor versions and forward compatibility
+ * is broken since the previous stable version.
+ * - toolingVersion
+ * - arbitrary string representing the tooling that produced the Best Effort TASTy
+ */
+sealed abstract case class BestEffortTastyHeader(
+ uuid: UUID,
+ majorVersion: Int,
+ minorVersion: Int,
+ patchVersion: Option[Int],
+ experimentalVersion: Int,
+ toolingVersion: String
+)
+
+class BestEffortTastyHeaderUnpickler(config: UnpicklerConfig, reader: TastyReader) {
+ import BestEffortTastyHeaderUnpickler._
+ import reader._
+
+ def this(reader: TastyReader) = this(UnpicklerConfig.generic, reader)
+ def this(bytes: Array[Byte]) = this(new TastyReader(bytes))
+
+ def readHeader(): UUID =
+ readFullHeader().uuid
+
+ def readFullHeader(): BestEffortTastyHeader = {
+ val hasBestEffortHeader = {
+ val readHeader = (for (i <- 0 until header.length) yield readByte()).toArray
+
+ if (readHeader.sameElements(header)) false
+ else if (readHeader.sameElements(bestEffortHeader)) true
+ else throw new UnpickleException("not a TASTy or Best Effort TASTy file")
+ }
+
+ val fileMajor = readNat()
+ val fileMinor = readNat()
+ val filePatch =
+ if hasBestEffortHeader then Some(readNat())
+ else None
+ val fileExperimental = readNat()
+ val toolingVersion = {
+ val length = readNat()
+ val start = currentAddr
+ val end = start + length
+ goto(end)
+ new String(bytes, start.index, length)
+ }
+
+ checkValidVersion(fileMajor, fileMinor, fileExperimental, toolingVersion, config)
+
+ val uuid = new UUID(readUncompressedLong(), readUncompressedLong())
+ new BestEffortTastyHeader(uuid, fileMajor, fileMinor, filePatch, fileExperimental, toolingVersion) {}
+ }
+
+ private def check(cond: Boolean, msg: => String): Unit = {
+ if (!cond) throw new UnpickleException(msg)
+ }
+}
+
+// Copy pasted from dotty.tools.tasty.TastyHeaderUnpickler
+// Since that library has strong compatibility guarantees, we do not want
+// to add any more methods just to support an experimental feature
+// (like best-effort compilation options).
+object BestEffortTastyHeaderUnpickler {
+
+ private def check(cond: Boolean, msg: => String): Unit = {
+ if (!cond) throw new UnpickleException(msg)
+ }
+
+ private def checkValidVersion(fileMajor: Int, fileMinor: Int, fileExperimental: Int, toolingVersion: String, config: UnpicklerConfig) = {
+ val toolMajor: Int = config.majorVersion
+ val toolMinor: Int = config.minorVersion
+ val toolExperimental: Int = config.experimentalVersion
+ val validVersion = TastyFormat.isVersionCompatible(
+ fileMajor = fileMajor,
+ fileMinor = fileMinor,
+ fileExperimental = fileExperimental,
+ compilerMajor = toolMajor,
+ compilerMinor = toolMinor,
+ compilerExperimental = toolExperimental
+ )
+ check(validVersion, {
+ // failure means that the TASTy file cannot be read, therefore it is either:
+ // - backwards incompatible major, in which case the library should be recompiled by the minimum stable minor
+ // version supported by this compiler
+ // - any experimental in an older minor, in which case the library should be recompiled by the stable
+ // compiler in the same minor.
+ // - older experimental in the same minor, in which case the compiler is also experimental, and the library
+ // should be recompiled by the current compiler
+ // - forward incompatible, in which case the compiler must be upgraded to the same version as the file.
+ val fileVersion = TastyVersion(fileMajor, fileMinor, fileExperimental)
+ val toolVersion = TastyVersion(toolMajor, toolMinor, toolExperimental)
+
+ val compat = Compatibility.failReason(file = fileVersion, read = toolVersion)
+
+ val what = if (compat < 0) "Backward" else "Forward"
+ val signature = signatureString(fileVersion, toolVersion, what, tool = Some(toolingVersion))
+ val fix = (
+ if (compat < 0) {
+ val newCompiler =
+ if (compat == Compatibility.BackwardIncompatibleMajor) toolVersion.minStable
+ else if (compat == Compatibility.BackwardIncompatibleExperimental) fileVersion.nextStable
+ else toolVersion // recompile the experimental library with the current experimental compiler
+ recompileFix(newCompiler, config)
+ }
+ else upgradeFix(fileVersion, config)
+ )
+ signature + fix + tastyAddendum
+ })
+ }
+
+ private def signatureString(
+ fileVersion: TastyVersion, toolVersion: TastyVersion, what: String, tool: Option[String]) = {
+ val optProducedBy = tool.fold("")(t => s", produced by $t")
+ s"""$what incompatible TASTy file has version ${fileVersion.show}$optProducedBy,
+ | expected ${toolVersion.validRange}.
+ |""".stripMargin
+ }
+
+ private def recompileFix(producerVersion: TastyVersion, config: UnpicklerConfig) = {
+ val addendum = config.recompileAdditionalInfo
+ val newTool = config.upgradedProducerTool(producerVersion)
+ s""" The source of this file should be recompiled by $newTool.$addendum""".stripMargin
+ }
+
+ private def upgradeFix(fileVersion: TastyVersion, config: UnpicklerConfig) = {
+ val addendum = config.upgradeAdditionalInfo(fileVersion)
+ val newTool = config.upgradedReaderTool(fileVersion)
+ s""" To read this ${fileVersion.kind} file, use $newTool.$addendum""".stripMargin
+ }
+
+ private def tastyAddendum: String = """
+ | Please refer to the documentation for information on TASTy versioning:
+ | https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin
+
+ private object Compatibility {
+ final val BackwardIncompatibleMajor = -3
+ final val BackwardIncompatibleExperimental = -2
+ final val ExperimentalRecompile = -1
+ final val ExperimentalUpgrade = 1
+ final val ForwardIncompatible = 2
+
+ /** Given that file can't be read, extract the reason */
+ def failReason(file: TastyVersion, read: TastyVersion): Int =
+ if (file.major == read.major && file.minor == read.minor && file.isExperimental && read.isExperimental) {
+ if (file.experimental < read.experimental) ExperimentalRecompile // recompile library as compiler is too new
+ else ExperimentalUpgrade // they should upgrade compiler as library is too new
+ }
+ else if (file.major < read.major)
+ BackwardIncompatibleMajor // pre 3.0.0
+ else if (file.isExperimental && file.major == read.major && file.minor <= read.minor)
+ // e.g. 3.4.0 reading 3.4.0-RC1-NIGHTLY, or 3.3.0 reading 3.0.2-RC1-NIGHTLY
+ BackwardIncompatibleExperimental
+ else ForwardIncompatible
+ }
+}
diff --git a/compiler/test/dotc/neg-best-effort-pickling.blacklist b/compiler/test/dotc/neg-best-effort-pickling.blacklist
new file mode 100644
index 000000000000..ff02be107a8a
--- /dev/null
+++ b/compiler/test/dotc/neg-best-effort-pickling.blacklist
@@ -0,0 +1,19 @@
+export-in-extension.scala
+i12456.scala
+i8623.scala
+i1642.scala
+i16696.scala
+constructor-proxy-values.scala
+i9328.scala
+i15414.scala
+i6796.scala
+i14013.scala
+toplevel-cyclic
+curried-dependent-ift.scala
+i17121.scala
+illegal-match-types.scala
+i13780-1.scala
+
+# semantic db generation fails in the first compilation
+i1642.scala
+i15158.scala
diff --git a/compiler/test/dotc/neg-best-effort-unpickling.blacklist b/compiler/test/dotc/neg-best-effort-unpickling.blacklist
new file mode 100644
index 000000000000..1e22d919f25a
--- /dev/null
+++ b/compiler/test/dotc/neg-best-effort-unpickling.blacklist
@@ -0,0 +1,17 @@
+# cyclic reference crashes
+i4368.scala
+i827.scala
+cycles.scala
+i5332.scala
+i4369c.scala
+i1806.scala
+i0091-infpaths.scala
+exports.scala
+i14834.scala
+
+# other type related crashes
+i4653.scala
+overrideClass.scala
+
+# repeating on a top level type definition
+i18750.scala
diff --git a/compiler/test/dotty/tools/TestSources.scala b/compiler/test/dotty/tools/TestSources.scala
index a288e49c5eb9..b2133b2fb182 100644
--- a/compiler/test/dotty/tools/TestSources.scala
+++ b/compiler/test/dotty/tools/TestSources.scala
@@ -64,6 +64,14 @@ object TestSources {
if Properties.usingScalaLibraryTasty then loadList(patmatExhaustivityScala2LibraryTastyBlacklistFile)
else Nil
+ // neg best effort tests lists
+
+ def negBestEffortPicklingBlacklistFile: String = "compiler/test/dotc/neg-best-effort-pickling.blacklist"
+ def negBestEffortUnpicklingBlacklistFile: String = "compiler/test/dotc/neg-best-effort-unpickling.blacklist"
+
+ def negBestEffortPicklingBlacklisted: List[String] = loadList(negBestEffortPicklingBlacklistFile)
+ def negBestEffortUnpicklingBlacklisted: List[String] = loadList(negBestEffortUnpicklingBlacklistFile)
+
// load lists
private def loadList(path: String): List[String] = {
diff --git a/compiler/test/dotty/tools/dotc/BestEffortCompilationTests.scala b/compiler/test/dotty/tools/dotc/BestEffortCompilationTests.scala
new file mode 100644
index 000000000000..681c92f266d2
--- /dev/null
+++ b/compiler/test/dotty/tools/dotc/BestEffortCompilationTests.scala
@@ -0,0 +1,59 @@
+package dotty
+package tools
+package dotc
+
+import scala.concurrent.duration._
+import dotty.tools.vulpix._
+import org.junit.{ Test, AfterClass }
+import reporting.TestReporter
+import java.io.{File => JFile}
+
+import scala.language.unsafeNulls
+
+class BestEffortCompilationTests {
+ import ParallelTesting._
+ import vulpix.TestConfiguration._
+ import BestEffortCompilationTests._
+ import CompilationTest.aggregateTests
+
+ // Since TASTy and beTASTy files are read in a lazy manner (only when referenced by the source .scala file)
+ // we test by using the "-from-tasty" option. This guarantees that the tasty files will be read
+ // (and that the Best Effort TASTy reader will be tested), but we unfortunately skip the useful
+ // interactions a tree derived from beTASTy could have with other frontend phases.
+ @Test def negTestFromBestEffortTasty: Unit = {
+ // Can be reproduced with
+ // > sbt
+ // > scalac --Ybest-effort -Xsemanticdb
+ // > scalac --from-tasty -Ywith-best-effort-tasty META_INF/best-effort/
+
+ implicit val testGroup: TestGroup = TestGroup("negTestFromBestEffortTasty")
+ compileBestEffortTastyInDir(s"tests${JFile.separator}neg", bestEffortBaselineOptions,
+ picklingFilter = FileFilter.exclude(TestSources.negBestEffortPicklingBlacklisted),
+ unpicklingFilter = FileFilter.exclude(TestSources.negBestEffortUnpicklingBlacklisted)
+ ).checkNoCrash()
+ }
+
+ // Tests an actual use case of this compilation mode, where symbol definitions of the downstream
+ // projects depend on the best effort tasty files generated with the Best Effort dir option
+ @Test def bestEffortIntergrationTest: Unit = {
+ implicit val testGroup: TestGroup = TestGroup("bestEffortIntegrationTests")
+ compileBestEffortIntegration(s"tests${JFile.separator}best-effort", bestEffortBaselineOptions)
+ .noCrashWithCompilingDependencies()
+ }
+}
+
+object BestEffortCompilationTests extends ParallelTesting {
+ def maxDuration = 45.seconds
+ def numberOfSlaves = Runtime.getRuntime.availableProcessors()
+ def safeMode = Properties.testsSafeMode
+ def isInteractive = SummaryReport.isInteractive
+ def testFilter = Properties.testsFilter
+ def updateCheckFiles: Boolean = Properties.testsUpdateCheckfile
+ def failedTests = TestReporter.lastRunFailedTests
+
+ implicit val summaryReport: SummaryReporting = new SummaryReport
+ @AfterClass def tearDown(): Unit = {
+ super.cleanup()
+ summaryReport.echoSummary()
+ }
+}
diff --git a/compiler/test/dotty/tools/dotc/core/tasty/CommentPicklingTest.scala b/compiler/test/dotty/tools/dotc/core/tasty/CommentPicklingTest.scala
index db58ff36ac42..11406070ce7a 100644
--- a/compiler/test/dotty/tools/dotc/core/tasty/CommentPicklingTest.scala
+++ b/compiler/test/dotty/tools/dotc/core/tasty/CommentPicklingTest.scala
@@ -117,7 +117,7 @@ class CommentPicklingTest {
implicit val ctx: Context = setup(args, initCtx).map(_._2).getOrElse(initCtx)
ctx.initialize()
val trees = files.flatMap { f =>
- val unpickler = new DottyUnpickler(AbstractFile.getFile(f.jpath), f.toByteArray())
+ val unpickler = new DottyUnpickler(AbstractFile.getFile(f.jpath), f.toByteArray(), isBestEffortTasty = false)
unpickler.enter(roots = Set.empty)
unpickler.rootTrees(using ctx)
}
diff --git a/compiler/test/dotty/tools/dotc/core/tasty/PathPicklingTest.scala b/compiler/test/dotty/tools/dotc/core/tasty/PathPicklingTest.scala
index 66463e3ff66c..326a2dc87b2a 100644
--- a/compiler/test/dotty/tools/dotc/core/tasty/PathPicklingTest.scala
+++ b/compiler/test/dotty/tools/dotc/core/tasty/PathPicklingTest.scala
@@ -48,7 +48,7 @@ class PathPicklingTest {
val jar = JarArchive.open(Path(s"$out/out.jar"), create = false)
try
for file <- jar.iterator() if file.name.endsWith(".tasty") do
- sb.append(TastyPrinter.showContents(file.toByteArray, noColor = true))
+ sb.append(TastyPrinter.showContents(file.toByteArray, noColor = true, isBestEffortTasty = false))
finally jar.close()
sb.toString()
diff --git a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala
index e9975ed25b6d..880a3bd1cc53 100644
--- a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala
+++ b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala
@@ -158,6 +158,12 @@ trait ParallelTesting extends RunnerOrchestration { self =>
}
}
+ private sealed trait FromTastyCompilationMode
+ private case object NotFromTasty extends FromTastyCompilationMode
+ private case object FromTasty extends FromTastyCompilationMode
+ private case object FromBestEffortTasty extends FromTastyCompilationMode
+ private case class WithBestEffortTasty(bestEffortDir: JFile) extends FromTastyCompilationMode
+
/** A group of files that may all be compiled together, with the same flags
* and output directory
*/
@@ -166,7 +172,7 @@ trait ParallelTesting extends RunnerOrchestration { self =>
files: Array[JFile],
flags: TestFlags,
outDir: JFile,
- fromTasty: Boolean = false,
+ fromTasty: FromTastyCompilationMode = NotFromTasty,
decompilation: Boolean = false
) extends TestSource {
def sourceFiles: Array[JFile] = files.filter(isSourceFile)
@@ -225,9 +231,11 @@ trait ParallelTesting extends RunnerOrchestration { self =>
private final def compileTestSource(testSource: TestSource): Try[List[TestReporter]] =
Try(testSource match {
case testSource @ JointCompilationSource(name, files, flags, outDir, fromTasty, decompilation) =>
- val reporter =
- if (fromTasty) compileFromTasty(flags, outDir)
- else compile(testSource.sourceFiles, flags, outDir)
+ val reporter = fromTasty match
+ case NotFromTasty => compile(testSource.sourceFiles, flags, outDir)
+ case FromTasty => compileFromTasty(flags, outDir)
+ case FromBestEffortTasty => compileFromBestEffortTasty(flags, outDir)
+ case WithBestEffortTasty(bestEffortDir) => compileWithBestEffortTasty(testSource.sourceFiles, bestEffortDir, flags, outDir)
List(reporter)
case testSource @ SeparateCompilationSource(_, dir, flags, outDir) =>
@@ -665,6 +673,31 @@ trait ParallelTesting extends RunnerOrchestration { self =>
reporter
+ protected def compileFromBestEffortTasty(flags0: TestFlags, targetDir: JFile): TestReporter = {
+ val classes = flattenFiles(targetDir).filter(isBestEffortTastyFile).map(_.toString)
+ val flags = flags0 and "-from-tasty" and "-Ywith-best-effort-tasty"
+ val reporter = mkReporter
+ val driver = new Driver
+
+ driver.process(flags.all ++ classes, reporter = reporter)
+
+ reporter
+ }
+
+ protected def compileWithBestEffortTasty(files0: Array[JFile], bestEffortDir: JFile, flags0: TestFlags, targetDir: JFile): TestReporter = {
+ val flags = flags0
+ .and("-Ywith-best-effort-tasty")
+ .and("-d", targetDir.getPath)
+ val reporter = mkReporter
+ val driver = new Driver
+
+ val args = Array("-classpath", flags.defaultClassPath + JFile.pathSeparator + bestEffortDir.toString) ++ flags.options
+
+ driver.process(args ++ files0.map(_.toString), reporter = reporter)
+
+ reporter
+ }
+
protected def compileFromTasty(flags0: TestFlags, targetDir: JFile): TestReporter = {
val tastyOutput = new JFile(targetDir.getPath + "_from-tasty")
tastyOutput.mkdir()
@@ -988,6 +1021,22 @@ trait ParallelTesting extends RunnerOrchestration { self =>
override def maybeFailureMessage(testSource: TestSource, reporters: Seq[TestReporter]): Option[String] = None
}
+ private final class NoBestEffortErrorsTest(testSources: List[TestSource], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting)
+ extends Test(testSources, times, threadLimit, suppressAllOutput) {
+ override def suppressErrors = true
+ override def maybeFailureMessage(testSource: TestSource, reporters: Seq[TestReporter]): Option[String] =
+ val unsucceffulBestEffortErrorMsg = "Unsuccessful best-effort compilation."
+ val failedBestEffortCompilation: Seq[TestReporter] =
+ reporters.collect{
+ case testReporter if testReporter.errors.exists(_.msg.message.startsWith(unsucceffulBestEffortErrorMsg)) =>
+ testReporter
+ }
+ if !failedBestEffortCompilation.isEmpty then
+ Some(failedBestEffortCompilation.flatMap(_.consoleOutput.split("\n")).mkString("\n"))
+ else
+ None
+ }
+
/** The `CompilationTest` is the main interface to `ParallelTesting`, it
* can be instantiated via one of the following methods:
@@ -1127,12 +1176,28 @@ trait ParallelTesting extends RunnerOrchestration { self =>
def checkWarnings()(implicit summaryReport: SummaryReporting): this.type =
checkPass(new WarnTest(targets, times, threadLimit, shouldFail || shouldSuppressOutput), "Warn")
+ /** Creates a "neg" test run, which makes sure that each test manages successful
+ * best-effort compilation, without any errors related to pickling/unpickling
+ * of betasty files.
+ */
+ def checkNoBestEffortError()(implicit summaryReport: SummaryReporting): this.type = {
+ val test = new NoBestEffortErrorsTest(targets, times, threadLimit, shouldFail || shouldSuppressOutput).executeTestSuite()
+
+ cleanup()
+
+ if (test.didFail) {
+ fail("Best-effort test should not have shown a \"Unsuccessful best-effort compilation\" error, but did")
+ }
+
+ this
+ }
+
/** Creates a "neg" test run, which makes sure that each test generates the
* correct number of errors at the correct positions. It also makes sure
* that none of these tests crashes the compiler.
*/
def checkExpectedErrors()(implicit summaryReport: SummaryReporting): this.type =
- val test = new NegTest(targets, times, threadLimit, shouldFail || shouldSuppressOutput).executeTestSuite()
+ val test = new NegTest(targets, times, threadLimit, shouldSuppressOutput).executeTestSuite()
cleanup()
@@ -1504,7 +1569,7 @@ trait ParallelTesting extends RunnerOrchestration { self =>
flags: TestFlags,
outDir: JFile,
fromTasty: Boolean = false,
- ) extends JointCompilationSource(name, Array(file), flags, outDir, fromTasty) {
+ ) extends JointCompilationSource(name, Array(file), flags, outDir, if (fromTasty) FromTasty else NotFromTasty) {
override def buildInstructions(errors: Int, warnings: Int): String = {
val runOrPos = if (file.getPath.startsWith(s"tests${JFile.separator}run${JFile.separator}")) "run" else "pos"
@@ -1538,6 +1603,147 @@ trait ParallelTesting extends RunnerOrchestration { self =>
)
}
+ /** A two step compilation test for best effort compilation pickling and unpickling.
+ *
+ * First, erroring neg test files are compiled with the `-Ybest-effort` option.
+ * If successful, then the produced Best Effort TASTy is re-compiled with
+ * '-Ywith-best-effort-tasty' to test the TastyReader for Best Effort TASTy.
+ */
+ def compileBestEffortTastyInDir(f: String, flags: TestFlags, picklingFilter: FileFilter, unpicklingFilter: FileFilter)(
+ implicit testGroup: TestGroup): BestEffortCompilationTest = {
+ val bestEffortFlag = "-Ybest-effort"
+ val semanticDbFlag = "-Xsemanticdb"
+ assert(!flags.options.contains(bestEffortFlag), "Best effort compilation flag should not be added manually")
+
+ val outDir = defaultOutputDir + testGroup + JFile.separator
+ val sourceDir = new JFile(f)
+ checkRequirements(f, sourceDir, outDir)
+
+ val (dirsStep1, filteredPicklingFiles) = compilationTargets(sourceDir, picklingFilter)
+ val (dirsStep2, filteredUnpicklingFiles) = compilationTargets(sourceDir, unpicklingFilter)
+
+ class BestEffortCompilation(
+ name: String,
+ file: JFile,
+ flags: TestFlags,
+ outputDir: JFile
+ ) extends JointCompilationSource(name, Array(file), flags.and(bestEffortFlag).and(semanticDbFlag), outputDir) {
+ override def buildInstructions(errors: Int, warnings: Int): String = {
+ s"""|
+ |Test '$title' compiled with a compiler crash,
+ |the test can be reproduced by running:
+ |
+ | sbt "scalac $bestEffortFlag $semanticDbFlag $file"
+ |
+ |These tests can be disabled by adding `${file.getName}` to `compiler${JFile.separator}test${JFile.separator}dotc${JFile.separator}neg-best-effort-pickling.blacklist`
+ |""".stripMargin
+ }
+ }
+
+ class CompilationFromBestEffortTasty(
+ name: String,
+ file: JFile,
+ flags: TestFlags,
+ bestEffortDir: JFile,
+ ) extends JointCompilationSource(name, Array(file), flags, bestEffortDir, fromTasty = FromBestEffortTasty) {
+
+ override def buildInstructions(errors: Int, warnings: Int): String = {
+ def beTastyFiles(file: JFile): Array[JFile] =
+ file.listFiles.flatMap { innerFile =>
+ if (innerFile.isDirectory) beTastyFiles(innerFile)
+ else if (isBestEffortTastyFile(innerFile)) Array(innerFile)
+ else Array.empty[JFile]
+ }
+ val beTastyFilesString = beTastyFiles(bestEffortDir).mkString(" ")
+ s"""|
+ |Test '$title' compiled with a compiler crash,
+ |the test can be reproduced by running:
+ |
+ | sbt "scalac -Ybest-effort $file"
+ | sbt "scalac --from-tasty -Ywith-best-effort-tasty $beTastyFilesString"
+ |
+ |These tests can be disabled by adding `${file.getName}` to `compiler${JFile.separator}test${JFile.separator}dotc${JFile.separator}neg-best-effort-unpickling.blacklist`
+ |
+ |""".stripMargin
+ }
+ }
+
+ val (bestEffortTargets, targetAndBestEffortDirs) =
+ filteredPicklingFiles.map { f =>
+ val outputDir = createOutputDirsForFile(f, sourceDir, outDir)
+ val bestEffortDir = new JFile(outputDir, s"META-INF${JFile.separator}best-effort")
+ (
+ BestEffortCompilation(testGroup.name, f, flags, outputDir),
+ (f, bestEffortDir)
+ )
+ }.unzip
+ val (_, bestEffortDirs) = targetAndBestEffortDirs.unzip
+ val fileToBestEffortDirMap = targetAndBestEffortDirs.toMap
+
+ val picklingSet = filteredPicklingFiles.toSet
+ val fromTastyTargets =
+ filteredUnpicklingFiles.filter(picklingSet.contains(_)).map { f =>
+ val bestEffortDir = fileToBestEffortDirMap(f)
+ new CompilationFromBestEffortTasty(testGroup.name, f, flags, bestEffortDir)
+ }
+
+ new BestEffortCompilationTest(
+ new CompilationTest(bestEffortTargets).keepOutput,
+ new CompilationTest(fromTastyTargets).keepOutput,
+ bestEffortDirs,
+ shouldDelete = true
+ )
+ }
+
+ /** A two step integration test for best effort compilation.
+ *
+ * Directories found in the directory `f` represent separate tests and must contain
+ * the 'err' and 'main' directories. First the (erroring) contents of the 'err'
+ * directory are compiled with the `Ybest-effort` option.
+ * Then, are the contents of 'main' are compiled with the previous best effort directory
+ * on the classpath using the option `-Ywith-best-effort-tasty`.
+ */
+ def compileBestEffortIntegration(f: String, flags: TestFlags)(implicit testGroup: TestGroup) = {
+ val bestEffortFlag = "-Ybest-effort"
+ val semanticDbFlag = "-Xsemanticdb"
+ val withBetastyFlag = "-Ywith-best-effort-tasty"
+ val sourceDir = new JFile(f)
+ val dirs = sourceDir.listFiles.toList
+ assert(dirs.forall(_.isDirectory), s"All files in $f have to be directories.")
+
+ val (step1Targets, step2Targets, bestEffortDirs) = dirs.map { dir =>
+ val step1SourceDir = new JFile(dir, "err")
+ val step2SourceDir = new JFile(dir, "main")
+
+ val step1SourceFiles = step1SourceDir.listFiles
+ val step2SourceFiles = step2SourceDir.listFiles
+
+ val outDir = defaultOutputDir + testGroup + JFile.separator + dir.getName().toString + JFile.separator
+
+ val step1OutDir = createOutputDirsForDir(step1SourceDir, step1SourceDir, outDir)
+ val step2OutDir = createOutputDirsForDir(step2SourceDir, step2SourceDir, outDir)
+
+ val step1Compilation = JointCompilationSource(
+ testGroup.name, step1SourceFiles, flags.and(bestEffortFlag).and(semanticDbFlag), step1OutDir, fromTasty = NotFromTasty
+ )
+
+ val bestEffortDir = new JFile(step1OutDir, s"META-INF${JFile.separator}best-effort")
+
+ val step2Compilation = JointCompilationSource(
+ testGroup.name, step2SourceFiles, flags.and(withBetastyFlag).and(semanticDbFlag), step2OutDir, fromTasty = WithBestEffortTasty(bestEffortDir)
+ )
+ (step1Compilation, step2Compilation, bestEffortDir)
+ }.unzip3
+
+ BestEffortCompilationTest(
+ new CompilationTest(step1Targets).keepOutput,
+ new CompilationTest(step2Targets).keepOutput,
+ bestEffortDirs,
+ true
+ )
+ }
+
+
class TastyCompilationTest(step1: CompilationTest, step2: CompilationTest, shouldDelete: Boolean)(implicit testGroup: TestGroup) {
def keepOutput: TastyCompilationTest =
@@ -1564,6 +1770,35 @@ trait ParallelTesting extends RunnerOrchestration { self =>
}
}
+ class BestEffortCompilationTest(step1: CompilationTest, step2: CompilationTest, bestEffortDirs: List[JFile], shouldDelete: Boolean)(implicit testGroup: TestGroup) {
+
+ def checkNoCrash()(implicit summaryReport: SummaryReporting): this.type = {
+ step1.checkNoBestEffortError() // Compile all files to generate the class files with best effort tasty
+ step2.checkNoBestEffortError() // Compile with best effort tasty
+
+ if (shouldDelete) {
+ CompilationTest.aggregateTests(step1, step2).delete()
+ def delete(file: JFile): Unit = {
+ if (file.isDirectory) file.listFiles.foreach(delete)
+ try Files.delete(file.toPath)
+ catch {
+ case _: NoSuchFileException => // already deleted, everything's fine
+ }
+ }
+ bestEffortDirs.foreach(t => delete(t))
+ }
+
+ this
+ }
+
+ def noCrashWithCompilingDependencies()(implicit summaryReport: SummaryReporting): this.type = {
+ step1.checkNoBestEffortError() // Compile all files to generate the class files with best effort tasty
+ step2.checkCompile() // Compile with best effort tasty
+
+ this
+ }
+ }
+
/** This function behaves similar to `compileFilesInDir` but it ignores
* sub-directories and as such, does **not** perform separate compilation
* tests.
@@ -1601,4 +1836,7 @@ object ParallelTesting {
def isTastyFile(f: JFile): Boolean =
f.getName.endsWith(".tasty")
+ def isBestEffortTastyFile(f: JFile): Boolean =
+ f.getName.endsWith(".betasty")
+
}
diff --git a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala
index f5540304da89..086d590fbfc7 100644
--- a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala
+++ b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala
@@ -69,6 +69,7 @@ object TestConfiguration {
val noYcheckCommonOptions = Array("-indent") ++ checkOptions ++ noCheckOptions
val defaultOptions = TestFlags(basicClasspath, commonOptions)
val noYcheckOptions = TestFlags(basicClasspath, noYcheckCommonOptions)
+ val bestEffortBaselineOptions = TestFlags(basicClasspath, noCheckOptions)
val unindentOptions = TestFlags(basicClasspath, Array("-no-indent") ++ checkOptions ++ noCheckOptions ++ yCheckOptions)
val withCompilerOptions =
defaultOptions.withClasspath(withCompilerClasspath).withRunClasspath(withCompilerClasspath)
diff --git a/docs/_docs/internals/best-effort-compilation.md b/docs/_docs/internals/best-effort-compilation.md
new file mode 100644
index 000000000000..2fed951c3fd8
--- /dev/null
+++ b/docs/_docs/internals/best-effort-compilation.md
@@ -0,0 +1,88 @@
+---
+layout: doc-page
+title: Best Effort Compilation
+---
+
+Best-effort compilation is a compilation mode introduced with the aim of improving IDE integration. It allows to generate
+tasty-like artifacts and semanticdb files in erroring programs.
+
+It is composed of two experimental compiler options:
+* `-Ybest-effort` produces Best Effort TASTy (`.betasty`) files to the `META-INF/best-effort` directory
+* `-Ywith-best-effort-tasty` allows to read Best Effort TASTy files, and if such file is read from the classpath then
+limits compilation to the frontend phases
+
+This feature aims to force through to the typer phase regardless of errors, and then serialize tasty-like files
+obtained from the error trees into the best effort directory (`META-INF/best-effort`) and also serialize semanticdb as normal.
+
+The exact execution pattern is as follows:
+
+```none
+Parser
+ │
+ │ regardless of errors
+ ˅
+TyperPhase ────────────────────────────────────┐
+ │ │
+ │ │
+ │ with errors │ no errors
+ │ │
+ │ ˅
+ │ Every following frontend pass until semanticdb.ExtractSemanticDB (interrupted in the case of errors)
+ │ │
+ │ │ regardless of errors
+ ˅ ˅
+semanticdb.ExtractSemanticDB ──────────────────┐
+ │ │
+ │ with errors │ no errors
+ │ │
+ │ ˅
+ │ Every following frontend pass until Pickler (interrupted in the case of errors)
+ │ │
+ │ │ regardless of errors
+ ˅ ˅
+Pickler (with added printing of best effort tasty to the best effort target directory)
+ │ │
+ │ with errors │ no errors
+ ˅ ˅
+End compilation Execute latter passes
+```
+
+This is because the IDE is able to retrieve useful info even when skipping phases like PostTyper.
+
+This execution structure where we skip phases depending on the errors found is motivated by the desire
+to avoid additionally handling errored trees in as many phases as possible, therefore also decreasing
+maintenance load. This way phases like PostTyper do not have to be continually adjusted to handle trees
+with errors from typer and usually the IDE is able to retrieve enough information with just the typer phase.
+
+An unfortunate consequence of this structure is the fact that we lose access to phases allowing for incremental
+compilation, which is something that could be adressed in the future.
+
+`-Ywith-best-effort-tasty` option allows reading Best Effort TASTy files from classpath. If such file is read, then
+the compiler is disallowed from proceeding to any non-frontend phase. This is to be used either in combination with
+`-Ybest-effort` option to produce Best Effort TASTy using failing dependencies, or in the Presentation Compiler
+to access symbols derived from failing projects.
+
+## Best Effort TASTy format
+
+The Best Effort TASTy (`.betasty`) format is a file format produced by the compiler when the `-Ybest-effort` option
+is used. It is characterised by a different header and an addition of the `ERRORtype` type, which represents errored types in
+the compiler. The Best Effort TASTy format also extends the regular TASTy grammar to allow the handling of as
+large amount of incorrect trees produced by the compiler as possible. The format is defined as part of the
+`dotty.tools.besteffort.BestEffortTastyFormat` object.
+
+Since currently the format holds an experimental status, no compatibility rules are defined for now, and the specification
+may change between the patch compiler versions, if need be.
+
+For performance reasons, if no errors are detected in the frontend phases, a betasty file mey be serialized in the format of
+regular TASTy file, characterized by the use of Tasty header instead of Best Effort TASTy header in the `.betasty` file.
+
+## Testing
+
+The testing procedure reuses the `tests/neg` negative tests that are usually meant to produce errors. First they are compiled
+with the `-Ybest-effort` option (testing the TreePickler for errored trees), then later, the tree is reconstructed using
+the previously created Best Effort TASTy, with `-Yread-tasty` and `-Ywith-best-effort-tasty` options. This is to test the
+TreeUnpickler for those Best Effort TASTy files.
+
+One of the goals of this feature is to keep the maintainance cost low, and to not let this feature hinder the pace of the
+overall development of the compiler. Because of that, the tests can be freely disabled in `compiler/neg-best-effort.blacklist`
+(testing TreePickler) and `compiler/neg-best-effort-from-tasty.blacklist` (testing TreeUnpickler).
diff --git a/docs/sidebar.yml b/docs/sidebar.yml
index 65d7ac2f9ee4..a0011b026cef 100644
--- a/docs/sidebar.yml
+++ b/docs/sidebar.yml
@@ -216,5 +216,6 @@ subsection:
- page: internals/debug-macros.md
- page: internals/gadts.md
- page: internals/coverage.md
+ - page: internals/best-effort-compilation.md
- page: release-notes-0.1.2.md
hidden: true
diff --git a/presentation-compiler/src/main/dotty/tools/pc/TastyUtils.scala b/presentation-compiler/src/main/dotty/tools/pc/TastyUtils.scala
index 747f104cfede..62a947aeb50b 100644
--- a/presentation-compiler/src/main/dotty/tools/pc/TastyUtils.scala
+++ b/presentation-compiler/src/main/dotty/tools/pc/TastyUtils.scala
@@ -21,7 +21,7 @@ object TastyUtils:
private def normalTasty(tastyURI: URI): String =
val tastyBytes = Files.readAllBytes(Paths.get(tastyURI))
- new TastyPrinter(tastyBytes.nn).showContents()
+ new TastyPrinter(tastyBytes.nn, isBestEffortTasty = false, testPickler = false).showContents()
private def htmlTasty(
tastyURI: URI,
diff --git a/project/Build.scala b/project/Build.scala
index 69441d0aaa01..cfffda810f75 100644
--- a/project/Build.scala
+++ b/project/Build.scala
@@ -394,6 +394,7 @@ object Build {
"-skip-by-id:scala.runtime.MatchCase",
"-skip-by-id:dotty.tools.tasty",
"-skip-by-id:dotty.tools.tasty.util",
+ "-skip-by-id:dotty.tools.tasty.besteffort",
"-project-footer", s"Copyright (c) 2002-$currentYear, LAMP/EPFL",
"-author",
"-groups",
@@ -2287,6 +2288,7 @@ object ScaladocConfigs {
"scala.runtime.MatchCase",
"dotty.tools.tasty",
"dotty.tools.tasty.util",
+ "dotty.tools.tasty.besteffort"
))
def projectFooter = ProjectFooter(s"Copyright (c) 2002-$currentYear, LAMP/EPFL")
def defaultTemplate = DefaultTemplate("static-site-main")
diff --git a/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala b/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala
index a51541192321..78c5c0ba72b9 100644
--- a/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala
+++ b/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala
@@ -103,7 +103,7 @@ class TastyHeaderUnpickler(config: UnpicklerConfig, reader: TastyReader) {
val fileVersion = TastyVersion(fileMajor, fileMinor, 0)
val toolVersion = TastyVersion(toolMajor, toolMinor, toolExperimental)
val signature = signatureString(fileVersion, toolVersion, what = "Backward", tool = None)
- val fix = recompileFix(toolVersion.minStable)
+ val fix = recompileFix(toolVersion.minStable, config)
throw new UnpickleException(signature + fix + tastyAddendum)
}
else {
@@ -117,43 +117,7 @@ class TastyHeaderUnpickler(config: UnpicklerConfig, reader: TastyReader) {
new String(bytes, start.index, length)
}
- val validVersion = TastyFormat.isVersionCompatible(
- fileMajor = fileMajor,
- fileMinor = fileMinor,
- fileExperimental = fileExperimental,
- compilerMajor = toolMajor,
- compilerMinor = toolMinor,
- compilerExperimental = toolExperimental
- )
-
- check(validVersion, {
- // failure means that the TASTy file cannot be read, therefore it is either:
- // - backwards incompatible major, in which case the library should be recompiled by the minimum stable minor
- // version supported by this compiler
- // - any experimental in an older minor, in which case the library should be recompiled by the stable
- // compiler in the same minor.
- // - older experimental in the same minor, in which case the compiler is also experimental, and the library
- // should be recompiled by the current compiler
- // - forward incompatible, in which case the compiler must be upgraded to the same version as the file.
- val fileVersion = TastyVersion(fileMajor, fileMinor, fileExperimental)
- val toolVersion = TastyVersion(toolMajor, toolMinor, toolExperimental)
-
- val compat = Compatibility.failReason(file = fileVersion, read = toolVersion)
-
- val what = if (compat < 0) "Backward" else "Forward"
- val signature = signatureString(fileVersion, toolVersion, what, tool = Some(toolingVersion))
- val fix = (
- if (compat < 0) {
- val newCompiler =
- if (compat == Compatibility.BackwardIncompatibleMajor) toolVersion.minStable
- else if (compat == Compatibility.BackwardIncompatibleExperimental) fileVersion.nextStable
- else toolVersion // recompile the experimental library with the current experimental compiler
- recompileFix(newCompiler)
- }
- else upgradeFix(fileVersion)
- )
- signature + fix + tastyAddendum
- })
+ checkValidVersion(fileMajor, fileMinor, fileExperimental, toolingVersion, config)
val uuid = new UUID(readUncompressedLong(), readUncompressedLong())
new TastyHeader(uuid, fileMajor, fileMinor, fileExperimental, toolingVersion) {}
@@ -161,11 +125,56 @@ class TastyHeaderUnpickler(config: UnpicklerConfig, reader: TastyReader) {
}
def isAtEnd: Boolean = reader.isAtEnd
+}
+
+object TastyHeaderUnpickler {
private def check(cond: Boolean, msg: => String): Unit = {
if (!cond) throw new UnpickleException(msg)
}
+ private def checkValidVersion(fileMajor: Int, fileMinor: Int, fileExperimental: Int, toolingVersion: String, config: UnpicklerConfig) = {
+ val toolMajor: Int = config.majorVersion
+ val toolMinor: Int = config.minorVersion
+ val toolExperimental: Int = config.experimentalVersion
+ val validVersion = TastyFormat.isVersionCompatible(
+ fileMajor = fileMajor,
+ fileMinor = fileMinor,
+ fileExperimental = fileExperimental,
+ compilerMajor = toolMajor,
+ compilerMinor = toolMinor,
+ compilerExperimental = toolExperimental
+ )
+ check(validVersion, {
+ // failure means that the TASTy file cannot be read, therefore it is either:
+ // - backwards incompatible major, in which case the library should be recompiled by the minimum stable minor
+ // version supported by this compiler
+ // - any experimental in an older minor, in which case the library should be recompiled by the stable
+ // compiler in the same minor.
+ // - older experimental in the same minor, in which case the compiler is also experimental, and the library
+ // should be recompiled by the current compiler
+ // - forward incompatible, in which case the compiler must be upgraded to the same version as the file.
+ val fileVersion = TastyVersion(fileMajor, fileMinor, fileExperimental)
+ val toolVersion = TastyVersion(toolMajor, toolMinor, toolExperimental)
+
+ val compat = Compatibility.failReason(file = fileVersion, read = toolVersion)
+
+ val what = if (compat < 0) "Backward" else "Forward"
+ val signature = signatureString(fileVersion, toolVersion, what, tool = Some(toolingVersion))
+ val fix = (
+ if (compat < 0) {
+ val newCompiler =
+ if (compat == Compatibility.BackwardIncompatibleMajor) toolVersion.minStable
+ else if (compat == Compatibility.BackwardIncompatibleExperimental) fileVersion.nextStable
+ else toolVersion // recompile the experimental library with the current experimental compiler
+ recompileFix(newCompiler, config)
+ }
+ else upgradeFix(fileVersion, config)
+ )
+ signature + fix + tastyAddendum
+ })
+ }
+
private def signatureString(
fileVersion: TastyVersion, toolVersion: TastyVersion, what: String, tool: Option[String]) = {
val optProducedBy = tool.fold("")(t => s", produced by $t")
@@ -174,13 +183,13 @@ class TastyHeaderUnpickler(config: UnpicklerConfig, reader: TastyReader) {
|""".stripMargin
}
- private def recompileFix(producerVersion: TastyVersion) = {
+ private def recompileFix(producerVersion: TastyVersion, config: UnpicklerConfig) = {
val addendum = config.recompileAdditionalInfo
val newTool = config.upgradedProducerTool(producerVersion)
s""" The source of this file should be recompiled by $newTool.$addendum""".stripMargin
}
- private def upgradeFix(fileVersion: TastyVersion) = {
+ private def upgradeFix(fileVersion: TastyVersion, config: UnpicklerConfig) = {
val addendum = config.upgradeAdditionalInfo(fileVersion)
val newTool = config.upgradedReaderTool(fileVersion)
s""" To read this ${fileVersion.kind} file, use $newTool.$addendum""".stripMargin
@@ -189,9 +198,6 @@ class TastyHeaderUnpickler(config: UnpicklerConfig, reader: TastyReader) {
private def tastyAddendum: String = """
| Please refer to the documentation for information on TASTy versioning:
| https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin
-}
-
-object TastyHeaderUnpickler {
private object Compatibility {
final val BackwardIncompatibleMajor = -3
diff --git a/tests/best-effort/broken-macro-executed-in-dependency/err/ExecutedMacro.scala b/tests/best-effort/broken-macro-executed-in-dependency/err/ExecutedMacro.scala
new file mode 100644
index 000000000000..a6a071c9b85e
--- /dev/null
+++ b/tests/best-effort/broken-macro-executed-in-dependency/err/ExecutedMacro.scala
@@ -0,0 +1,2 @@
+object ExecutedMacro:
+ val failingMacro = FailingTransparent.execute()
diff --git a/tests/best-effort/broken-macro-executed-in-dependency/err/FailingTransparentInline.scala b/tests/best-effort/broken-macro-executed-in-dependency/err/FailingTransparentInline.scala
new file mode 100644
index 000000000000..9f9fdc22ee4b
--- /dev/null
+++ b/tests/best-effort/broken-macro-executed-in-dependency/err/FailingTransparentInline.scala
@@ -0,0 +1,11 @@
+object FailingTransparentInline:
+ sealed trait Foo
+ case class FooA() extends Foo
+ case class FooB() extends Foo
+
+ transparent inline def execute(): Foo = ${ executeImpl() }
+ def executeImpl(using Quotes)() = {
+ val a = 0
+ a.asInstanceOf[String]
+ FooB()
+ }
diff --git a/tests/best-effort/broken-macro-executed-in-dependency/main/Main.scala b/tests/best-effort/broken-macro-executed-in-dependency/main/Main.scala
new file mode 100644
index 000000000000..6603d4ee0cc1
--- /dev/null
+++ b/tests/best-effort/broken-macro-executed-in-dependency/main/Main.scala
@@ -0,0 +1,2 @@
+object Main:
+ ExecutedMacro.failingMacro
diff --git a/tests/best-effort/broken-macro-executed-in-dependent/err/BrokenMacros.scala b/tests/best-effort/broken-macro-executed-in-dependent/err/BrokenMacros.scala
new file mode 100644
index 000000000000..73d121022b23
--- /dev/null
+++ b/tests/best-effort/broken-macro-executed-in-dependent/err/BrokenMacros.scala
@@ -0,0 +1,13 @@
+import scala.quoted._
+object BrokenMacros:
+ transparent inline def macro1() = ${macroImpl()}
+ def macroImpl(using Quotes)(): Expr[String] =
+ val a: Int = "str" // source of the error
+ '{a}
+
+ sealed trait Foo
+ case class FooA() extends Foo
+ case class FooB()
+ transparent inline def macro2(): Foo = ${macro2Impl()}
+ def macro2Impl(using Quotes)(): Expr[Foo] =
+ '{FooB()}
diff --git a/tests/best-effort/broken-macro-executed-in-dependent/main/Main.scala b/tests/best-effort/broken-macro-executed-in-dependent/main/Main.scala
new file mode 100644
index 000000000000..d382bd4aabd7
--- /dev/null
+++ b/tests/best-effort/broken-macro-executed-in-dependent/main/Main.scala
@@ -0,0 +1,3 @@
+object Main
+ val a = BrokenMacros.macro1()
+ val b = BrokenMacros.macro2()
diff --git a/tests/best-effort/mirrors-in-dependency/err/MirrorTypes.scala b/tests/best-effort/mirrors-in-dependency/err/MirrorTypes.scala
new file mode 100644
index 000000000000..280805ba8ab9
--- /dev/null
+++ b/tests/best-effort/mirrors-in-dependency/err/MirrorTypes.scala
@@ -0,0 +1,2 @@
+object MirrorTypes:
+ case class BrokenType(a: NonExistent, b: Int)
diff --git a/tests/best-effort/mirrors-in-dependency/main/MirrorExec.scala b/tests/best-effort/mirrors-in-dependency/main/MirrorExec.scala
new file mode 100644
index 000000000000..12052a27b57d
--- /dev/null
+++ b/tests/best-effort/mirrors-in-dependency/main/MirrorExec.scala
@@ -0,0 +1,7 @@
+import scala.deriving.Mirror
+
+object MirrorExec:
+ transparent inline def getNames[T](using m: Mirror.Of[T]): m.MirroredElemTypes =
+ scala.compiletime.erasedValue[m.MirroredElemTypes]
+
+ val ab = getNames[MirrorTypes.BrokenType]
diff --git a/tests/best-effort/simple-type-error/err/SimpleTypeError.scala b/tests/best-effort/simple-type-error/err/SimpleTypeError.scala
new file mode 100644
index 000000000000..cf9ad8c8d56a
--- /dev/null
+++ b/tests/best-effort/simple-type-error/err/SimpleTypeError.scala
@@ -0,0 +1,2 @@
+object SimpleTypeError:
+ def foo: Int = "string"
diff --git a/tests/best-effort/simple-type-error/main/Main.scala b/tests/best-effort/simple-type-error/main/Main.scala
new file mode 100644
index 000000000000..c1e821d790e7
--- /dev/null
+++ b/tests/best-effort/simple-type-error/main/Main.scala
@@ -0,0 +1,2 @@
+object Main:
+ SimpleTypeError.foo