From 2813fd1aa6160094fa247a06411a61cb1b7ae57d Mon Sep 17 00:00:00 2001 From: Jan Chyb Date: Thu, 25 May 2023 13:18:44 +0200 Subject: [PATCH 01/10] Introduce best-effort compilation for IDEs 2 new experimental options are introduces for the compiler: `-Ybest-effort` and `-Ywith-best-effort-tasty`. A related Best Effort TASTy (.betasty) format, a TASTy aligned file format able to hold some errored trees was also added. Behaviour of the options and the format is documented as part of this commit in the `best-effort-compilation.md` docs file. `-Ybest-effort` is used to produce `.betasty` files in the `/META-INF/best-effort`. `-Ywith-best-effort-tasty` allows to use them during compilation, limiting it to the frontend phases if such file is used. If any .betasty is used, transparent inline macros also cease to be expanded by the compiler. Since best-effort compilation can fail (e.g. due to cyclic reference errors which sometimes are not able to be pickled or unpickled), the crashes caused by it are wrapped into an additional descriptive error message in the aim to fail more gracefully (and not pollute our issue tracker with known problems). The feature is tested in two ways: * with a set of pairs of dependent projects, one of which is meant to produce .betasty by using `-Ybest-effort`, and the other tries to consume it using `-Ywith-best-effort-tasty`. * by reusing the compiler nonbootstrapped neg tests, first by running them with `-Ybest-effort` option, and then by running read-tasty tests on the produced betasty files to thest best-effort tastt unpickling Additionally, `-Ywith-best-effort-tasty` allows to print `.betasty` via `-print-tasty`. --- .../dotty/tools/backend/jvm/GenBCode.scala | 2 + .../dotty/tools/backend/sjs/GenSJSIR.scala | 2 +- compiler/src/dotty/tools/dotc/Driver.scala | 6 +- compiler/src/dotty/tools/dotc/Run.scala | 13 +- .../src/dotty/tools/dotc/ast/TreeInfo.scala | 4 +- compiler/src/dotty/tools/dotc/ast/tpd.scala | 4 +- .../dotc/classpath/DirectoryClassPath.scala | 2 +- .../tools/dotc/classpath/FileUtils.scala | 8 + .../tools/dotc/config/ScalaSettings.scala | 3 + .../src/dotty/tools/dotc/core/Contexts.scala | 15 ++ .../tools/dotc/core/DenotTransformers.scala | 2 + .../dotty/tools/dotc/core/Denotations.scala | 3 +- .../tools/dotc/core/SymDenotations.scala | 18 +- .../dotty/tools/dotc/core/SymbolLoaders.scala | 42 ++- .../dotty/tools/dotc/core/TypeErasure.scala | 3 +- .../src/dotty/tools/dotc/core/Types.scala | 7 +- .../core/tasty/BestEffortTastyWriter.scala | 43 +++ .../dotc/core/tasty/DottyUnpickler.scala | 31 ++- .../dotc/core/tasty/TastyAnsiiPrinter.scala | 2 +- .../dotc/core/tasty/TastyClassName.scala | 4 +- .../tools/dotc/core/tasty/TastyPickler.scala | 9 +- .../tools/dotc/core/tasty/TastyPrinter.scala | 21 +- .../dotc/core/tasty/TastyUnpickler.scala | 12 +- .../tools/dotc/core/tasty/TreePickler.scala | 180 ++++++++----- .../tools/dotc/core/tasty/TreeUnpickler.scala | 21 +- .../dotty/tools/dotc/fromtasty/TASTYRun.scala | 2 + .../tools/dotc/fromtasty/TastyFileUtil.scala | 14 +- .../dotty/tools/dotc/inlines/Inliner.scala | 2 +- compiler/src/dotty/tools/dotc/report.scala | 16 ++ .../dotc/semanticdb/ExtractSemanticDB.scala | 2 +- .../tools/dotc/transform/MacroTransform.scala | 2 + .../tools/dotc/transform/MegaPhase.scala | 4 + .../dotty/tools/dotc/transform/Pickler.scala | 69 +++-- .../src/dotty/tools/dotc/typer/Typer.scala | 1 + .../src/dotty/tools/io/FileExtension.scala | 3 + .../dotc/neg-best-effort-pickling.blacklist | 16 ++ .../dotc/neg-best-effort-unpickling.blacklist | 14 + compiler/test/dotty/tools/TestSources.scala | 8 + .../dotc/BestEffortCompilationTests.scala | 59 +++++ .../dotty/tools/vulpix/ParallelTesting.scala | 250 +++++++++++++++++- .../tools/vulpix/TestConfiguration.scala | 1 + .../internals/best-effort-compilation.md | 88 ++++++ docs/sidebar.yml | 1 + project/Build.scala | 2 + tasty/src/dotty/tools/tasty/TastyFormat.scala | 1 - .../tools/tasty/TastyHeaderUnpickler.scala | 94 ++++--- .../besteffort/BestEffortTastyFormat.scala | 37 +++ .../BestEffortTastyHeaderUnpickler.scala | 77 ++++++ .../err/ExecutedMacro.scala | 2 + .../err/FailingTransparentInline.scala | 11 + .../main/Main.scala | 2 + .../err/BrokenMacros.scala | 13 + .../main/Main.scala | 3 + .../err/MirrorTypes.scala | 2 + .../main/MirrorExec.scala | 7 + .../err/SimpleTypeError.scala | 2 + .../simple-type-error/main/Main.scala | 2 + 57 files changed, 1044 insertions(+), 220 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/core/tasty/BestEffortTastyWriter.scala create mode 100644 compiler/test/dotc/neg-best-effort-pickling.blacklist create mode 100644 compiler/test/dotc/neg-best-effort-unpickling.blacklist create mode 100644 compiler/test/dotty/tools/dotc/BestEffortCompilationTests.scala create mode 100644 docs/_docs/internals/best-effort-compilation.md create mode 100644 tasty/src/dotty/tools/tasty/besteffort/BestEffortTastyFormat.scala create mode 100644 tasty/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala create mode 100644 tests/best-effort/broken-macro-executed-in-dependency/err/ExecutedMacro.scala create mode 100644 tests/best-effort/broken-macro-executed-in-dependency/err/FailingTransparentInline.scala create mode 100644 tests/best-effort/broken-macro-executed-in-dependency/main/Main.scala create mode 100644 tests/best-effort/broken-macro-executed-in-dependent/err/BrokenMacros.scala create mode 100644 tests/best-effort/broken-macro-executed-in-dependent/main/Main.scala create mode 100644 tests/best-effort/mirrors-in-dependency/err/MirrorTypes.scala create mode 100644 tests/best-effort/mirrors-in-dependency/main/MirrorExec.scala create mode 100644 tests/best-effort/simple-type-error/err/SimpleTypeError.scala create mode 100644 tests/best-effort/simple-type-error/main/Main.scala diff --git a/compiler/src/dotty/tools/backend/jvm/GenBCode.scala b/compiler/src/dotty/tools/backend/jvm/GenBCode.scala index d9f413a5d5ab..4d1be4937c6d 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.usesBestEffortTasty + 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..1f0e7b4382f5 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.usesBestEffortTasty 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..855f09430db2 100644 --- a/compiler/src/dotty/tools/dotc/Driver.scala +++ b/compiler/src/dotty/tools/dotc/Driver.scala @@ -39,6 +39,8 @@ class Driver { catch case ex: FatalError => report.error(ex.getMessage.nn) // signals that we should fail compilation. + case ex: Throwable if ctx.usesBestEffortTasty => + report.bestEffortError(ex, "Some best-effort tasty files were not able to be read.") case ex: TypeError if !runOrNull.enrichedErrorMessage => println(runOrNull.enrichErrorMessage(s"${ex.toMessage} while compiling ${files.map(_.path).mkString(", ")}")) throw ex @@ -102,8 +104,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..6dafc224f0cc 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -321,6 +321,10 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint ctx.settings.Yskip.value, ctx.settings.YstopBefore.value, stopAfter, ctx.settings.Ycheck.value) ctx.base.usePhases(phases, runCtx) + var forceReachPhaseMaybe = + if (ctx.isBestEffort && phases.exists(_.phaseName == "typer")) Some("typer") + else None + if ctx.settings.YnoDoubleBindings.value then ctx.base.checkNoDoubleBindings = true @@ -331,7 +335,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint 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,13 @@ 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)) + + forceReachPhaseMaybe match { + case Some(forceReachPhase) if phase.phaseName == forceReachPhase => + forceReachPhaseMaybe = None + case _ => + } + 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..b79079daeaf5 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) + if !ctx.isBestEffort then assert(sym.isClass) if (cls == sym || cls == sym.linkedClass) tdef :: Nil else Nil case vdef: ValDef => val sym = vdef.symbol - assert(sym.is(Module)) + if !ctx.isBestEffort then assert(sym.is(Module)) 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..438ebb7aa52d 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.isBestEffort || ctx.usesBestEffortTasty || ctx.reporter.errorsReported) 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.isBestEffort || ctx.usesBestEffortTasty || ctx.reporter.errorsReported, 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..5ef52aaaaf3c 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..b87f7301fa87 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -474,6 +474,18 @@ object Contexts { /** Is the flexible types option set? */ def flexibleTypes: Boolean = base.settings.YexplicitNulls.value && !base.settings.YnoFlexibleTypes.value + + /** Is best-effort-dir option set? */ + def isBestEffort: Boolean = base.settings.YbestEffort.value + + /** Is the from-best-effort-tasty option set to true? */ + def withBestEffortTasty: Boolean = base.settings.YwithBestEffortTasty.value + + /** Were any best effort tasty dependencies used during compilation? */ + def usesBestEffortTasty: Boolean = base.usedBestEffortTasty + + /** Confirm that a best effort tasty dependency was used during compilation. */ + def setUsesBestEffortTasty(): Unit = base.usedBestEffortTasty = true /** A fresh clone of this context embedded in this context. */ def fresh: FreshContext = freshOver(this) @@ -960,6 +972,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..a1f8bc1ccd2e 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.usesBestEffortTasty } /** 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..57952ad9d6ab 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.isBestEffort || ctx.usesBestEffortTasty, 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..54152e58efd3 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.isBestEffort || ctx.usesBestEffortTasty, 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..5f2e06d06b4d 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) = @@ -424,26 +426,39 @@ class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader { val compilationUnitInfo: CompilationUnitInfo | Null = unpickler.compilationUnitInfo - def description(using Context): String = "TASTy file " + tastyFile.toString + val isBestEffortTasty = tastyFile.name.endsWith(".betasty") + + def description(using Context): String = + if tastyFile.extension == ".betasty" 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 + val unpickler = new tasty.DottyUnpickler(tastyFile, tastyBytes, isBestEffortTasty = isBestEffortTasty) + unpickler.enter(roots = Set(classRoot, moduleRoot, moduleRoot.sourceModule))(using ctx.withSource(util.NoSource)) + if mayLoadTreesFromTasty || (isBestEffortTasty && ctx.withBestEffortTasty) then + classRoot.classSymbol.rootTreeOrProvider = unpickler + moduleRoot.classSymbol.rootTreeOrProvider = unpickler + if isBestEffortTasty then + checkBeTastyUUID(tastyFile, tastyBytes) + ctx.setUsesBestEffortTasty() + else + checkTastyUUID() + else + report.error(em"Best Effort TASTy $tastyFile file could not be read.") 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 +475,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..46c88fd9b038 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -756,7 +756,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..3581611525ef 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.isBestEffort || ctx.usesBestEffortTasty => cls.info // can happen in IDE if `cls` is stale } @@ -3719,8 +3720,8 @@ 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.usesBestEffortTasty) expectValueTypeOrWildcard(tp1, where) + if (!ctx.usesBestEffortTasty) 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..c30098f01d16 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 = false + ) 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 mode the tasty file contains package (TopLevel), an expression (Term) or a type (TypeTree) + * @param isBestEffortTasty specifies wheather file should be unpickled as a Best Effort TASTy */ -class DottyUnpickler(tastyFile: AbstractFile, bytes: Array[Byte], mode: UnpickleMode = UnpickleMode.TopLevel) extends ClassfileParser.Embedded with tpd.TreeProvider { +class DottyUnpickler( + tastyFile: AbstractFile, + bytes: Array[Byte], + mode: UnpickleMode = UnpickleMode.TopLevel, + isBestEffortTasty: Boolean = false +) 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..d14e9637d129 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TastyAnsiiPrinter.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyAnsiiPrinter.scala @@ -2,7 +2,7 @@ package dotty.tools.dotc package core package tasty -class TastyAnsiiPrinter(bytes: Array[Byte], testPickler: Boolean) extends TastyPrinter(bytes, testPickler) { +class TastyAnsiiPrinter(bytes: Array[Byte], testPickler: Boolean, isBestEffortTasty: Boolean = false) extends TastyPrinter(bytes, testPickler, isBestEffortTasty) { def this(bytes: Array[Byte]) = this(bytes, testPickler = false) 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/TastyPickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyPickler.scala index 214f7a5f6702..e13349ba3c09 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.* @@ -25,7 +26,7 @@ class TastyPickler(val rootCls: ClassSymbol) { def newSection(name: String, buf: TastyBuffer): Unit = sections += ((nameBuffer.nameIndex(name.toTermName), buf)) - def assembleParts(): Array[Byte] = { + def assembleParts(isBestEffortTasty: Boolean = false): Array[Byte] = { def lengthWithLength(buf: TastyBuffer) = buf.length + natSize(buf.length) @@ -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..89a3ea2d459e 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala @@ -23,9 +23,9 @@ 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, testPickler = false, isBestEffortTasty = false) - def showContents(bytes: Array[Byte], noColor: Boolean, testPickler: Boolean = false): String = + def showContents(bytes: Array[Byte], noColor: Boolean, testPickler: Boolean = false, isBestEffortTasty: Boolean = false): String = val printer = if noColor then new TastyPrinter(bytes, testPickler) else new TastyAnsiiPrinter(bytes, testPickler) @@ -33,24 +33,27 @@ object TastyPrinter: 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 = false): Unit = println(line) println(fileName) println(line) - println(showContents(bytes, noColor)) + println(showContents(bytes, noColor, isBestEffortTasty)) 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) @@ -68,11 +71,11 @@ object TastyPrinter: println(line) } -class TastyPrinter(bytes: Array[Byte], val testPickler: Boolean) { +class TastyPrinter(bytes: Array[Byte], val testPickler: Boolean, isBestEffortTasty: Boolean = false) { - def this(bytes: Array[Byte]) = this(bytes, testPickler = false) + def this(bytes: Array[Byte]) = this(bytes, testPickler = false, isBestEffortTasty = false) - class TastyPrinterUnpickler extends TastyUnpickler(bytes) { + class TastyPrinterUnpickler extends TastyUnpickler(bytes, isBestEffortTasty) { var namesStart: Addr = uninitialized var namesEnd: Addr = uninitialized override def readNames() = { diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala index 6fe648ee98d3..f0780d8f3535 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala @@ -5,6 +5,8 @@ package tasty import scala.language.unsafeNulls import dotty.tools.tasty.{TastyFormat, TastyVersion, TastyBuffer, TastyReader, TastyHeaderUnpickler, UnpicklerConfig} +import dotty.tools.tasty.besteffort.BestEffortTastyHeaderUnpickler + import TastyFormat.NameTags.*, TastyFormat.nameTagToString import TastyBuffer.NameRef @@ -63,10 +65,11 @@ object TastyUnpickler { import TastyUnpickler.* -class TastyUnpickler(protected val reader: TastyReader) { +class TastyUnpickler(protected val reader: TastyReader, isBestEffortTasty: Boolean = false) { 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 +126,9 @@ class TastyUnpickler(protected val reader: TastyReader) { result } - val header: TastyHeader = - new TastyHeaderUnpickler(scala3CompilerConfig, reader).readFullHeader() + val header = + if isBestEffortTasty then new BestEffortTastyHeaderUnpickler(scala3CompilerConfig, reader).readFullHeader() + else 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..31209b2a805f 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -65,6 +65,13 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { fillRef(lengthAddr, currentAddr, relative = true) } + /* There are certain expectations with code naturally being able to reach pickling + * phase as opposed to one that uses best-effort compilation features. For betasty + * files, we try to avoid calling any assertions which can be unfullfilled. + */ + private inline def assertForBestEffort(assertion: Boolean)(using Context): Boolean = + ((!ctx.isBestEffort && ctx.reporter.errorsReported) || ctx.usesBestEffortTasty) || assertion + def addrOfSym(sym: Symbol): Option[Addr] = symRefs.get(sym) @@ -295,9 +302,15 @@ 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 tpe: ErrorType if ctx.isBestEffort => + pickleErrorType() + case _ if ctx.isBestEffort => + pickleErrorType() } def pickleMethodic(tag: Int, tpe: LambdaType, mods: FlagSet)(using Context): Unit = { @@ -321,8 +334,13 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { pickled } + def pickleErrorType(): Unit = { + writeByte(ERRORtype) + } + def pickleTpt(tpt: Tree)(using Context): Unit = - pickleTree(tpt) + if assertForBestEffort(tpt.isType) then pickleTree(tpt) + else pickleErrorType() def pickleTreeUnlessEmpty(tree: Tree)(using Context): Unit = { if (!tree.isEmpty) pickleTree(tree) @@ -336,39 +354,41 @@ 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) + if assertForBestEffort(symRefs.get(sym) == Some(NoAddr) && !(tag == TYPEDEF && tpt.isInstanceOf[Template] && !tpt.symbol.exists)) 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 +418,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 assertForBestEffort(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 +444,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 +458,27 @@ 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 - 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) + if assertForBestEffort(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) - pickleType(tree.symbol.owner.typeRef) - } + else // select from owner + writeByte(SELECTin) + withLength { + pickleNameAndSig(name, tree.symbol.signature, ename) + pickleTree(qual) + pickleType(tree.symbol.owner.typeRef) + } + else pickleErrorType() } case Apply(fun, args) => if (fun.symbol eq defn.throwMethod) { @@ -480,12 +506,13 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { args.foreach(pickleTpt) } case Literal(const1) => - pickleConstant { - tree.tpe match { - case ConstantType(const2) => const2 - case _ => const1 + if assertForBestEffort(tree.hasType) then + pickleConstant { + tree.tpe match { + case ConstantType(const2) => const2 + case _ => const1 + } } - } case Super(qual, mix) => writeByte(SUPER) withLength { @@ -657,19 +684,22 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { writeByte(PACKAGE) withLength { pickleType(pid.tpe); pickleStats(stats) } case tree: TypeTree => - pickleType(tree.tpe) + if assertForBestEffort(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 assertForBestEffort(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 +765,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 +873,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..68e10cfcda56 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -53,12 +53,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.* @@ -495,7 +497,14 @@ class TreeUnpickler(reader: TastyReader, ConstantType(readConstant(tag)) } - if (tag < firstLengthTreeTag) readSimpleType() else readLengthType() + def readSimpleTypeBestEffort(): Type = tag match { + case ERRORtype => new PreviousErrorType + case _ => readSimpleType() + } + + if (tag < firstLengthTreeTag){ + if (isBestEffortTasty) readSimpleTypeBestEffort() else readSimpleType() + } else readLengthType() } private def readSymNameRef()(using Context): Type = { @@ -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..d18a30a7fda8 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.usesBestEffortTasty => 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/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..515d3f8439bf 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.usesBestEffortTasty + 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..e7f5856f6263 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.usesBestEffortTasty } } import MegaPhase.* @@ -164,6 +166,8 @@ class MegaPhase(val miniPhases: Array[MiniPhase]) extends Phase { relaxedTypingCache } + override def isRunnable(using Context): Boolean = super.isRunnable && !ctx.usesBestEffortTasty + 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..f0a0f8b0c44a 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,10 @@ 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.usesBestEffortTasty || ctx.isBestEffort) + // we do not want to pickle `.betasty` if will not create the file either way // when `-Yjava-tasty` is set we actually want to run this phase on Java sources override def skipIfJava(using Context): Boolean = false @@ -238,7 +242,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 +262,7 @@ class Pickler extends Phase { override def run(using Context): Unit = { val unit = ctx.compilationUnit + val isBestEffort = ctx.reporter.errorsReported || ctx.usesBestEffortTasty pickling.println(i"unpickling in run ${ctx.runId}") if ctx.settings.fromTasty.value then @@ -294,7 +300,14 @@ class Pickler extends Phase { val pickler = new TastyPickler(cls) 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]() @@ -319,7 +332,7 @@ class Pickler extends Phase { AttributePickler.pickleAttributes(attributes, pickler, scratch.attributeBuffer) - val pickled = pickler.assembleParts() + val pickled = pickler.assembleParts(isBestEffort) def rawBytes = // not needed right now, but useful to print raw format. pickled.iterator.grouped(10).toList.zipWithIndex.map { @@ -339,26 +352,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, testPickler = true) + () => pickled + + unit.pickled += (cls -> demandPickled) end for } @@ -396,6 +410,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.normalize + .resolve("META-INF") + .resolve("best-effort") + Files.createDirectories(outpath) + BestEffortTastyWriter.write(outpath.nn, result) result } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 612bd22ef19d..8dc4d17a4aba 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.usesBestEffortTasty => // 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..262f2f21e70a 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"` */ 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..e075e30139ba --- /dev/null +++ b/compiler/test/dotc/neg-best-effort-pickling.blacklist @@ -0,0 +1,16 @@ +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 + +# 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..835ca7812089 --- /dev/null +++ b/compiler/test/dotc/neg-best-effort-unpickling.blacklist @@ -0,0 +1,14 @@ +# 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 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..f65844c39c08 --- /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 nad 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/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala index e9975ed25b6d..9a3f0eac70af 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 + ":" + 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..a60cd2a09469 --- /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 +tast6y-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/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/TastyFormat.scala b/tasty/src/dotty/tools/tasty/TastyFormat.scala index 6cd63d0d8f01..45c8d0e7687f 100644 --- a/tasty/src/dotty/tools/tasty/TastyFormat.scala +++ b/tasty/src/dotty/tools/tasty/TastyFormat.scala @@ -295,7 +295,6 @@ Note: Attribute tags are grouped into categories that determine what follows, an ``` **************************************************************************************/ - object TastyFormat { /** The first four bytes of a TASTy file, followed by four values: diff --git a/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala b/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala index a51541192321..6df0e123ec9a 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 +} - private def check(cond: Boolean, msg: => String): Unit = { +object TastyHeaderUnpickler { + + def check(cond: Boolean, msg: => String): Unit = { if (!cond) throw new UnpickleException(msg) } + 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/tasty/src/dotty/tools/tasty/besteffort/BestEffortTastyFormat.scala b/tasty/src/dotty/tools/tasty/besteffort/BestEffortTastyFormat.scala new file mode 100644 index 000000000000..c0ead6443aca --- /dev/null +++ b/tasty/src/dotty/tools/tasty/besteffort/BestEffortTastyFormat.scala @@ -0,0 +1,37 @@ +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._ + + /** 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 +} diff --git a/tasty/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala b/tasty/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala new file mode 100644 index 000000000000..7e144bb339dc --- /dev/null +++ b/tasty/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala @@ -0,0 +1,77 @@ +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} + +/** + * 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 TastyHeaderUnpickler._ + 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[tasty] def check(cond: Boolean, msg: => String): Unit = { + if (!cond) throw new UnpickleException(msg) + } +} 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 From 0cd8452fcdb9da17b3087f3df42ff64333c39985 Mon Sep 17 00:00:00 2001 From: Florian3k Date: Tue, 20 Feb 2024 13:00:53 +0100 Subject: [PATCH 02/10] Apply review suggestions --- .../dotty/tools/backend/jvm/GenBCode.scala | 2 +- .../dotty/tools/backend/sjs/GenSJSIR.scala | 2 +- compiler/src/dotty/tools/dotc/Driver.scala | 2 +- compiler/src/dotty/tools/dotc/Run.scala | 15 +++---- compiler/src/dotty/tools/dotc/ast/tpd.scala | 4 +- .../src/dotty/tools/dotc/core/Contexts.scala | 11 +++-- .../tools/dotc/core/DenotTransformers.scala | 2 +- .../dotty/tools/dotc/core/Denotations.scala | 2 +- .../tools/dotc/core/SymDenotations.scala | 4 +- .../dotty/tools/dotc/core/SymbolLoaders.scala | 5 ++- .../dotty/tools/dotc/core/TypeErasure.scala | 4 +- .../src/dotty/tools/dotc/core/Types.scala | 7 ++-- .../dotc/core/tasty/DottyUnpickler.scala | 8 ++-- .../dotc/core/tasty/TastyHTMLPrinter.scala | 2 +- .../tools/dotc/core/tasty/TastyPickler.scala | 4 +- .../tools/dotc/core/tasty/TastyPrinter.scala | 2 +- .../dotc/core/tasty/TastyUnpickler.scala | 2 +- .../tools/dotc/core/tasty/TreePickler.scala | 41 ++++++++++++------- .../tools/dotc/core/tasty/TreeUnpickler.scala | 14 +++---- .../dotty/tools/dotc/inlines/Inliner.scala | 2 +- .../tools/dotc/quoted/PickledQuotes.scala | 8 ++-- .../tools/dotc/transform/MacroTransform.scala | 2 +- .../tools/dotc/transform/MegaPhase.scala | 4 +- .../dotty/tools/dotc/transform/Pickler.scala | 10 ++--- .../src/dotty/tools/dotc/typer/Typer.scala | 2 +- .../besteffort/BestEffortTastyFormat.scala | 10 ++++- .../BestEffortTastyHeaderUnpickler.scala | 2 +- .../dotc/BestEffortCompilationTests.scala | 2 +- .../dotc/core/tasty/CommentPicklingTest.scala | 2 +- .../dotc/core/tasty/PathPicklingTest.scala | 2 +- .../internals/best-effort-compilation.md | 2 +- .../src/main/dotty/tools/pc/TastyUtils.scala | 2 +- tasty/src/dotty/tools/tasty/TastyFormat.scala | 1 + .../tools/tasty/TastyHeaderUnpickler.scala | 4 +- 34 files changed, 104 insertions(+), 84 deletions(-) rename {tasty => compiler}/src/dotty/tools/tasty/besteffort/BestEffortTastyFormat.scala (86%) rename {tasty => compiler}/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala (97%) diff --git a/compiler/src/dotty/tools/backend/jvm/GenBCode.scala b/compiler/src/dotty/tools/backend/jvm/GenBCode.scala index 4d1be4937c6d..a616241d9a3e 100644 --- a/compiler/src/dotty/tools/backend/jvm/GenBCode.scala +++ b/compiler/src/dotty/tools/backend/jvm/GenBCode.scala @@ -21,7 +21,7 @@ class GenBCode extends Phase { self => override def description: String = GenBCode.description - override def isRunnable(using Context) = super.isRunnable && !ctx.usesBestEffortTasty + override def isRunnable(using Context) = super.isRunnable && !ctx.usedBestEffortTasty private val superCallsMap = new MutableSymbolMap[Set[ClassSymbol]] def registerSuperCall(sym: Symbol, calls: ClassSymbol): Unit = { diff --git a/compiler/src/dotty/tools/backend/sjs/GenSJSIR.scala b/compiler/src/dotty/tools/backend/sjs/GenSJSIR.scala index 1f0e7b4382f5..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 && !ctx.usesBestEffortTasty + 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 855f09430db2..e99ed4e8324a 100644 --- a/compiler/src/dotty/tools/dotc/Driver.scala +++ b/compiler/src/dotty/tools/dotc/Driver.scala @@ -39,7 +39,7 @@ class Driver { catch case ex: FatalError => report.error(ex.getMessage.nn) // signals that we should fail compilation. - case ex: Throwable if ctx.usesBestEffortTasty => + case ex: Throwable if ctx.usedBestEffortTasty => report.bestEffortError(ex, "Some best-effort tasty files were not able to be read.") case ex: TypeError if !runOrNull.enrichedErrorMessage => println(runOrNull.enrichErrorMessage(s"${ex.toMessage} while compiling ${files.map(_.path).mkString(", ")}")) diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index 6dafc224f0cc..64e216a39b2a 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -321,10 +321,6 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint ctx.settings.Yskip.value, ctx.settings.YstopBefore.value, stopAfter, ctx.settings.Ycheck.value) ctx.base.usePhases(phases, runCtx) - var forceReachPhaseMaybe = - if (ctx.isBestEffort && phases.exists(_.phaseName == "typer")) Some("typer") - else None - if ctx.settings.YnoDoubleBindings.value then ctx.base.checkNoDoubleBindings = true @@ -333,6 +329,10 @@ 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 || forceReachPhaseMaybe.nonEmpty @@ -349,11 +349,8 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint ctx.fresh.setPhase(phase.next).setCompilationUnit(unit)) lastPrintedTree = printTree(lastPrintedTree)(using printCtx(unit)) - forceReachPhaseMaybe match { - case Some(forceReachPhase) if phase.phaseName == forceReachPhase => - forceReachPhaseMaybe = None - case _ => - } + 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) diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 438ebb7aa52d..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.isBestEffort || ctx.usesBestEffortTasty || 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.isBestEffort || ctx.usesBestEffortTasty || 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/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index b87f7301fa87..230fffdf80ba 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -475,17 +475,20 @@ object Contexts { /** Is the flexible types option set? */ def flexibleTypes: Boolean = base.settings.YexplicitNulls.value && !base.settings.YnoFlexibleTypes.value - /** Is best-effort-dir option set? */ + /** Is the best-effort option set? */ def isBestEffort: Boolean = base.settings.YbestEffort.value - /** Is the from-best-effort-tasty option set to true? */ + /** 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 usesBestEffortTasty: Boolean = base.usedBestEffortTasty + def usedBestEffortTasty: Boolean = base.usedBestEffortTasty /** Confirm that a best effort tasty dependency was used during compilation. */ - def setUsesBestEffortTasty(): Unit = base.usedBestEffortTasty = true + 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) diff --git a/compiler/src/dotty/tools/dotc/core/DenotTransformers.scala b/compiler/src/dotty/tools/dotc/core/DenotTransformers.scala index a1f8bc1ccd2e..451561c1b84d 100644 --- a/compiler/src/dotty/tools/dotc/core/DenotTransformers.scala +++ b/compiler/src/dotty/tools/dotc/core/DenotTransformers.scala @@ -29,7 +29,7 @@ object DenotTransformers { /** The transformation method */ def transform(ref: SingleDenotation)(using Context): SingleDenotation - override def isRunnable(using Context) = super.isRunnable && !ctx.usesBestEffortTasty + 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 57952ad9d6ab..2418aba1978b 100644 --- a/compiler/src/dotty/tools/dotc/core/Denotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Denotations.scala @@ -720,7 +720,7 @@ object Denotations { || 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) - || ctx.isBestEffort || ctx.usesBestEffortTasty, + || 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 54152e58efd3..f01d2faf86c4 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -720,7 +720,7 @@ 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 = + final def isSelfSym(using Context): Boolean = if !ctx.isBestEffort || exists then owner.infoOrCompleter match { case ClassInfo(_, _, _, _, selfInfo) => @@ -2007,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) || ctx.isBestEffort || ctx.usesBestEffortTasty, 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 5f2e06d06b4d..7cfe8cce1817 100644 --- a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala +++ b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala @@ -439,16 +439,17 @@ class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader { val tastyBytes = tastyFile.toByteArray val unpickler = new tasty.DottyUnpickler(tastyFile, tastyBytes, isBestEffortTasty = isBestEffortTasty) unpickler.enter(roots = Set(classRoot, moduleRoot, moduleRoot.sourceModule))(using ctx.withSource(util.NoSource)) - if mayLoadTreesFromTasty || (isBestEffortTasty && ctx.withBestEffortTasty) then + if mayLoadTreesFromTasty || isBestEffortTasty then classRoot.classSymbol.rootTreeOrProvider = unpickler moduleRoot.classSymbol.rootTreeOrProvider = unpickler if isBestEffortTasty then checkBeTastyUUID(tastyFile, tastyBytes) - ctx.setUsesBestEffortTasty() + ctx.setUsedBestEffortTasty() else checkTastyUUID() else report.error(em"Best Effort TASTy $tastyFile file could not be read.") + private def handleUnpicklingExceptions[T](thunk: =>T): T = try thunk catch case e: RuntimeException => diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 46c88fd9b038..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 diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 3581611525ef..cd5fd83a0198 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -3150,7 +3150,7 @@ object Types extends TypeUtils { else cls.info match { case cinfo: ClassInfo => cinfo.selfType case _: ErrorType | NoType - if ctx.mode.is(Mode.Interactive) || ctx.isBestEffort || ctx.usesBestEffortTasty => cls.info + if ctx.mode.is(Mode.Interactive) || ctx.tolerateErrorsForBestEffort => cls.info // can happen in IDE if `cls` is stale } @@ -3720,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" - if (!ctx.usesBestEffortTasty) expectValueTypeOrWildcard(tp1, where) - if (!ctx.usesBestEffortTasty) 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/DottyUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala index c30098f01d16..3605a6cc9515 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala @@ -27,7 +27,7 @@ object DottyUnpickler { compilationUnitInfo: CompilationUnitInfo, posUnpickler: Option[PositionUnpickler], commentUnpickler: Option[CommentUnpickler], - isBestEffortTasty: Boolean = false + isBestEffortTasty: Boolean ) extends SectionUnpickler[TreeUnpickler](ASTsSection) { def unpickle(reader: TastyReader, nameAtRef: NameTable): TreeUnpickler = new TreeUnpickler(reader, nameAtRef, compilationUnitInfo, posUnpickler, commentUnpickler, isBestEffortTasty) @@ -52,14 +52,14 @@ 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 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) - * @param isBestEffortTasty specifies wheather file should be unpickled as a Best Effort TASTy */ class DottyUnpickler( tastyFile: AbstractFile, bytes: Array[Byte], - mode: UnpickleMode = UnpickleMode.TopLevel, - isBestEffortTasty: Boolean = false + isBestEffortTasty: Boolean, + mode: UnpickleMode = UnpickleMode.TopLevel ) extends ClassfileParser.Embedded with tpd.TreeProvider { import tpd.* import DottyUnpickler.* diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyHTMLPrinter.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyHTMLPrinter.scala index b234705413ae..e3f6752c3dd5 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) { 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 e13349ba3c09..e35ed5bb2466 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TastyPickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyPickler.scala @@ -17,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)] @@ -26,7 +26,7 @@ class TastyPickler(val rootCls: ClassSymbol) { def newSection(name: String, buf: TastyBuffer): Unit = sections += ((nameBuffer.nameIndex(name.toTermName), buf)) - def assembleParts(isBestEffortTasty: Boolean = false): Array[Byte] = { + def assembleParts(): Array[Byte] = { def lengthWithLength(buf: TastyBuffer) = buf.length + natSize(buf.length) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala index 89a3ea2d459e..fe3ff00cdebf 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala @@ -39,7 +39,7 @@ object TastyPrinter: val noColor = args.contains("-color:never") val allowBetasty = args.contains(betastyOpt) var printLastLine = false - def printTasty(fileName: String, bytes: Array[Byte], isBestEffortTasty: Boolean = false): Unit = + def printTasty(fileName: String, bytes: Array[Byte], isBestEffortTasty: Boolean): Unit = println(line) println(fileName) println(line) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala index f0780d8f3535..9879e45f8b15 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala @@ -65,7 +65,7 @@ object TastyUnpickler { import TastyUnpickler.* -class TastyUnpickler(protected val reader: TastyReader, isBestEffortTasty: Boolean = false) { +class TastyUnpickler(protected val reader: TastyReader, isBestEffortTasty: Boolean) { import reader.* def this(bytes: Array[Byte]) = this(new TastyReader(bytes), false) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index 31209b2a805f..02492d8215a3 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,12 +66,15 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { fillRef(lengthAddr, currentAddr, relative = true) } - /* There are certain expectations with code naturally being able to reach pickling - * phase as opposed to one that uses best-effort compilation features. For betasty - * files, we try to avoid calling any assertions which can be unfullfilled. + /** 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 then we can try to do something else. + * For regular non best-effort compilation (without best-effort and without using .betasty on classpath), + * this will always return true. */ - private inline def assertForBestEffort(assertion: Boolean)(using Context): Boolean = - ((!ctx.isBestEffort && ctx.reporter.errorsReported) || ctx.usesBestEffortTasty) || assertion + 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) @@ -307,8 +311,6 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { else if !pickled then pickleErrorType() case tpe: LazyRef => pickleType(tpe.ref) - case tpe: ErrorType if ctx.isBestEffort => - pickleErrorType() case _ if ctx.isBestEffort => pickleErrorType() } @@ -339,7 +341,7 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { } def pickleTpt(tpt: Tree)(using Context): Unit = - if assertForBestEffort(tpt.isType) then pickleTree(tpt) + if passesConditionForErroringBestEffortCode(tpt.isType) then pickleTree(tpt) else pickleErrorType() def pickleTreeUnlessEmpty(tree: Tree)(using Context): Unit = { @@ -354,7 +356,11 @@ 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 - if assertForBestEffort(symRefs.get(sym) == Some(NoAddr) && !(tag == TYPEDEF && tpt.isInstanceOf[Template] && !tpt.symbol.exists)) then + 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) @@ -418,7 +424,7 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { else try tree match { case Ident(name) => - if assertForBestEffort(tree.hasType) then + 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. @@ -458,7 +464,7 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { pickleType(tp) } case _ => - if assertForBestEffort(tree.hasType) then + if passesConditionForErroringBestEffortCode(tree.hasType) then val sig = tree.tpe.signature var ename = tree.symbol.targetName val selectFromQualifier = @@ -478,7 +484,11 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { pickleTree(qual) pickleType(tree.symbol.owner.typeRef) } - else pickleErrorType() + else + writeByte(if name.isTypeName then SELECTtpt else SELECT) + val ename = tree.symbol.targetName + pickleNameAndSig(name, Signature.NotAMethod, ename) + pickleTree(qual) } case Apply(fun, args) => if (fun.symbol eq defn.throwMethod) { @@ -506,13 +516,14 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { args.foreach(pickleTpt) } case Literal(const1) => - if assertForBestEffort(tree.hasType) then + 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 { @@ -684,7 +695,7 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { writeByte(PACKAGE) withLength { pickleType(pid.tpe); pickleStats(stats) } case tree: TypeTree => - if assertForBestEffort(tree.hasType) then pickleType(tree.tpe) + if passesConditionForErroringBestEffortCode(tree.hasType) then pickleType(tree.tpe) else pickleErrorType() case SingletonTypeTree(ref) => writeByte(SINGLETONtpt) @@ -692,7 +703,7 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { case RefinedTypeTree(parent, refinements) => if (refinements.isEmpty) pickleTree(parent) else { - if assertForBestEffort(refinements.head.symbol.exists) then + if passesConditionForErroringBestEffortCode(refinements.head.symbol.exists) then val refineCls = refinements.head.symbol.owner.asClass registerDef(refineCls) pickledTypes(refineCls.typeRef) = currentAddr diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 68e10cfcda56..de311d66468e 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 @@ -451,7 +452,7 @@ class TreeUnpickler(reader: TastyReader, result } - def readSimpleType(): Type = (tag: @switch) match { + def readSimpleType(): Type = tag match { case TYPEREFdirect | TERMREFdirect => NamedType(NoPrefix, readSymRef()) case TYPEREFsymbol | TERMREFsymbol => @@ -493,18 +494,13 @@ class TreeUnpickler(reader: TastyReader, typeAtAddr.getOrElseUpdate(ref, forkAt(ref).readType()) case BYNAMEtype => ExprType(readType()) + case ERRORtype if isBestEffortTasty => + new PreviousErrorType case _ => ConstantType(readConstant(tag)) } - def readSimpleTypeBestEffort(): Type = tag match { - case ERRORtype => new PreviousErrorType - case _ => readSimpleType() - } - - if (tag < firstLengthTreeTag){ - if (isBestEffortTasty) readSimpleTypeBestEffort() else readSimpleType() - } else readLengthType() + if (tag < firstLengthTreeTag) readSimpleType() else readLengthType() } private def readSymNameRef()(using Context): Type = { diff --git a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala index d18a30a7fda8..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 && !ctx.usesBestEffortTasty => + 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/transform/MacroTransform.scala b/compiler/src/dotty/tools/dotc/transform/MacroTransform.scala index 515d3f8439bf..137fbf4f837c 100644 --- a/compiler/src/dotty/tools/dotc/transform/MacroTransform.scala +++ b/compiler/src/dotty/tools/dotc/transform/MacroTransform.scala @@ -13,7 +13,7 @@ abstract class MacroTransform extends Phase { import ast.tpd.* - override def isRunnable(using Context) = super.isRunnable && !ctx.usesBestEffortTasty + override def isRunnable(using Context) = super.isRunnable && !ctx.usedBestEffortTasty override def run(using Context): Unit = { val unit = ctx.compilationUnit diff --git a/compiler/src/dotty/tools/dotc/transform/MegaPhase.scala b/compiler/src/dotty/tools/dotc/transform/MegaPhase.scala index e7f5856f6263..86acd009fd09 100644 --- a/compiler/src/dotty/tools/dotc/transform/MegaPhase.scala +++ b/compiler/src/dotty/tools/dotc/transform/MegaPhase.scala @@ -137,7 +137,7 @@ object MegaPhase { override def run(using Context): Unit = singletonGroup.run - override def isRunnable(using Context): Boolean = super.isRunnable && !ctx.usesBestEffortTasty + override def isRunnable(using Context): Boolean = super.isRunnable && !ctx.usedBestEffortTasty } } import MegaPhase.* @@ -166,7 +166,7 @@ class MegaPhase(val miniPhases: Array[MiniPhase]) extends Phase { relaxedTypingCache } - override def isRunnable(using Context): Boolean = super.isRunnable && !ctx.usesBestEffortTasty + override def isRunnable(using Context): Boolean = super.isRunnable && !ctx.usedBestEffortTasty private val cpy: TypedTreeCopier = cpyBetweenPhases diff --git a/compiler/src/dotty/tools/dotc/transform/Pickler.scala b/compiler/src/dotty/tools/dotc/transform/Pickler.scala index f0a0f8b0c44a..50d480490a7c 100644 --- a/compiler/src/dotty/tools/dotc/transform/Pickler.scala +++ b/compiler/src/dotty/tools/dotc/transform/Pickler.scala @@ -262,7 +262,7 @@ class Pickler extends Phase { override def run(using Context): Unit = { val unit = ctx.compilationUnit - val isBestEffort = ctx.reporter.errorsReported || ctx.usesBestEffortTasty + val isBestEffort = ctx.reporter.errorsReported || ctx.usedBestEffortTasty pickling.println(i"unpickling in run ${ctx.runId}") if ctx.settings.fromTasty.value then @@ -298,7 +298,7 @@ class Pickler extends Phase { isOutline = isOutline ) - val pickler = new TastyPickler(cls) + val pickler = new TastyPickler(cls, isBestEffortTasty = isBestEffort) val treePkl = new TreePickler(pickler, attributes) val successful = try @@ -332,7 +332,7 @@ class Pickler extends Phase { AttributePickler.pickleAttributes(attributes, pickler, scratch.attributeBuffer) - val pickled = pickler.assembleParts(isBestEffort) + val pickled = pickler.assembleParts() def rawBytes = // not needed right now, but useful to print raw format. pickled.iterator.grouped(10).toList.zipWithIndex.map { @@ -342,7 +342,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 @@ -426,7 +426,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 8dc4d17a4aba..9d0150f49a1f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -4322,7 +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.usesBestEffortTasty => // do nothing if error was already reported in previous compilation + 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/tasty/src/dotty/tools/tasty/besteffort/BestEffortTastyFormat.scala b/compiler/src/dotty/tools/tasty/besteffort/BestEffortTastyFormat.scala similarity index 86% rename from tasty/src/dotty/tools/tasty/besteffort/BestEffortTastyFormat.scala rename to compiler/src/dotty/tools/tasty/besteffort/BestEffortTastyFormat.scala index c0ead6443aca..99a24ce5f346 100644 --- a/tasty/src/dotty/tools/tasty/besteffort/BestEffortTastyFormat.scala +++ b/compiler/src/dotty/tools/tasty/besteffort/BestEffortTastyFormat.scala @@ -20,7 +20,7 @@ The following are the added productions to the grammar: ``` **************************************************************************************/ object BestEffortTastyFormat { - export TastyFormat._ + 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. @@ -34,4 +34,12 @@ object BestEffortTastyFormat { * 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/tasty/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala b/compiler/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala similarity index 97% rename from tasty/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala rename to compiler/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala index 7e144bb339dc..bbdd0376e7d4 100644 --- a/tasty/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala +++ b/compiler/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala @@ -71,7 +71,7 @@ class BestEffortTastyHeaderUnpickler(config: UnpicklerConfig, reader: TastyReade new BestEffortTastyHeader(uuid, fileMajor, fileMinor, filePatch, fileExperimental, toolingVersion) {} } - private[tasty] def check(cond: Boolean, msg: => String): Unit = { + private def check(cond: Boolean, msg: => String): Unit = { if (!cond) throw new UnpickleException(msg) } } diff --git a/compiler/test/dotty/tools/dotc/BestEffortCompilationTests.scala b/compiler/test/dotty/tools/dotc/BestEffortCompilationTests.scala index f65844c39c08..681c92f266d2 100644 --- a/compiler/test/dotty/tools/dotc/BestEffortCompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/BestEffortCompilationTests.scala @@ -16,7 +16,7 @@ class BestEffortCompilationTests { import BestEffortCompilationTests._ import CompilationTest.aggregateTests - // Since TASTy nad beTASTy files are read in a lazy manner (only when referenced by the source .scala file) + // 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. 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/docs/_docs/internals/best-effort-compilation.md b/docs/_docs/internals/best-effort-compilation.md index a60cd2a09469..2fed951c3fd8 100644 --- a/docs/_docs/internals/best-effort-compilation.md +++ b/docs/_docs/internals/best-effort-compilation.md @@ -4,7 +4,7 @@ title: Best Effort Compilation --- Best-effort compilation is a compilation mode introduced with the aim of improving IDE integration. It allows to generate -tast6y-like artifacts and semanticdb files in erroring programs. +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 diff --git a/presentation-compiler/src/main/dotty/tools/pc/TastyUtils.scala b/presentation-compiler/src/main/dotty/tools/pc/TastyUtils.scala index 747f104cfede..d4033ce29e09 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).showContents() private def htmlTasty( tastyURI: URI, diff --git a/tasty/src/dotty/tools/tasty/TastyFormat.scala b/tasty/src/dotty/tools/tasty/TastyFormat.scala index 45c8d0e7687f..6cd63d0d8f01 100644 --- a/tasty/src/dotty/tools/tasty/TastyFormat.scala +++ b/tasty/src/dotty/tools/tasty/TastyFormat.scala @@ -295,6 +295,7 @@ Note: Attribute tags are grouped into categories that determine what follows, an ``` **************************************************************************************/ + object TastyFormat { /** The first four bytes of a TASTy file, followed by four values: diff --git a/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala b/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala index 6df0e123ec9a..fbb8f68b1142 100644 --- a/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala +++ b/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala @@ -129,11 +129,11 @@ class TastyHeaderUnpickler(config: UnpicklerConfig, reader: TastyReader) { object TastyHeaderUnpickler { - def check(cond: Boolean, msg: => String): Unit = { + private def check(cond: Boolean, msg: => String): Unit = { if (!cond) throw new UnpickleException(msg) } - def checkValidVersion(fileMajor: Int, fileMinor: Int, fileExperimental: Int, toolingVersion: String, config: UnpicklerConfig) = { + private[tasty] 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 From df0feaa87062beae59bede8b5e5c1759ddad81c7 Mon Sep 17 00:00:00 2001 From: Florian3k Date: Tue, 20 Feb 2024 13:01:10 +0100 Subject: [PATCH 03/10] fixes after rebase --- .../dotty/tools/dotc/core/SymbolLoaders.scala | 8 +++---- .../dotc/core/tasty/TastyUnpickler.scala | 23 +++++++++++++++---- .../tools/dotc/core/tasty/TreeUnpickler.scala | 3 +++ .../dotc/neg-best-effort-pickling.blacklist | 3 +++ 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala index 7cfe8cce1817..bbc96e68c2cd 100644 --- a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala +++ b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala @@ -418,18 +418,16 @@ class ClassfileLoader(val classfile: AbstractFile) extends SymbolLoader { } class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader { - + val isBestEffortTasty = tastyFile.extension == "betasty" 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 - val isBestEffortTasty = tastyFile.name.endsWith(".betasty") - def description(using Context): String = - if tastyFile.extension == ".betasty" then "Best Effort TASTy file " + tastyFile.toString + if isBestEffortTasty then "Best Effort TASTy file " + tastyFile.toString else "TASTy file " + tastyFile.toString override def doComplete(root: SymDenotation)(using Context): Unit = diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala index 9879e45f8b15..f034f03298b1 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala @@ -2,10 +2,11 @@ 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.BestEffortTastyHeaderUnpickler +import dotty.tools.tasty.besteffort.{BestEffortTastyHeader, BestEffortTastyHeaderUnpickler} import TastyFormat.NameTags.*, TastyFormat.nameTagToString import TastyBuffer.NameRef @@ -16,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) { @@ -126,9 +139,11 @@ class TastyUnpickler(protected val reader: TastyReader, isBestEffortTasty: Boole result } - val header = - if isBestEffortTasty then new BestEffortTastyHeaderUnpickler(scala3CompilerConfig, reader).readFullHeader() - else new TastyHeaderUnpickler(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/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index de311d66468e..b823c5c539f5 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -447,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 diff --git a/compiler/test/dotc/neg-best-effort-pickling.blacklist b/compiler/test/dotc/neg-best-effort-pickling.blacklist index e075e30139ba..ff02be107a8a 100644 --- a/compiler/test/dotc/neg-best-effort-pickling.blacklist +++ b/compiler/test/dotc/neg-best-effort-pickling.blacklist @@ -10,6 +10,9 @@ 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 From 603a23e88475650f8652c548d8229647e38bb810 Mon Sep 17 00:00:00 2001 From: Jan Chyb Date: Mon, 25 Mar 2024 23:45:43 +0100 Subject: [PATCH 04/10] fix TastyPrinter for betasty --- .../dotc/core/tasty/TastyAnsiiPrinter.scala | 4 ++-- .../tools/dotc/core/tasty/TastyHTMLPrinter.scala | 2 +- .../tools/dotc/core/tasty/TastyPrinter.scala | 16 ++++++++-------- .../src/dotty/tools/dotc/transform/Pickler.scala | 2 +- .../src/main/dotty/tools/pc/TastyUtils.scala | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyAnsiiPrinter.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyAnsiiPrinter.scala index d14e9637d129..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, isBestEffortTasty: Boolean = false) extends TastyPrinter(bytes, testPickler, isBestEffortTasty) { +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/TastyHTMLPrinter.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyHTMLPrinter.scala index e3f6752c3dd5..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, isBestEffortTasty = false) { +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/TastyPrinter.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala index fe3ff00cdebf..57ce6e460d65 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala @@ -23,12 +23,12 @@ import dotty.tools.dotc.classpath.FileUtils.hasTastyExtension object TastyPrinter: def showContents(bytes: Array[Byte], noColor: Boolean): String = - showContents(bytes, noColor, testPickler = false, isBestEffortTasty = false) + showContents(bytes, noColor, isBestEffortTasty = false, testPickler = false) - def showContents(bytes: Array[Byte], noColor: Boolean, testPickler: Boolean = false, isBestEffortTasty: 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 = { @@ -43,7 +43,7 @@ object TastyPrinter: println(line) println(fileName) println(line) - println(showContents(bytes, noColor, isBestEffortTasty)) + println(showContents(bytes, noColor, isBestEffortTasty, testPickler = false)) println() printLastLine = true for arg <- args do @@ -71,9 +71,9 @@ object TastyPrinter: println(line) } -class TastyPrinter(bytes: Array[Byte], val testPickler: Boolean, isBestEffortTasty: Boolean = false) { +class TastyPrinter(bytes: Array[Byte], isBestEffortTasty: Boolean, val testPickler: Boolean) { - def this(bytes: Array[Byte]) = this(bytes, testPickler = false, isBestEffortTasty = false) + def this(bytes: Array[Byte]) = this(bytes, isBestEffortTasty = false, testPickler = false) class TastyPrinterUnpickler extends TastyUnpickler(bytes, isBestEffortTasty) { var namesStart: Addr = uninitialized @@ -133,7 +133,7 @@ class TastyPrinter(bytes: Array[Byte], val testPickler: Boolean, isBestEffortTas }) 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/transform/Pickler.scala b/compiler/src/dotty/tools/dotc/transform/Pickler.scala index 50d480490a7c..8e9b76225e7a 100644 --- a/compiler/src/dotty/tools/dotc/transform/Pickler.scala +++ b/compiler/src/dotty/tools/dotc/transform/Pickler.scala @@ -369,7 +369,7 @@ class Pickler extends Phase { 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) + printedTasty(cls) = TastyPrinter.showContents(pickled, noColor = true, isBestEffortTasty = false, testPickler = true) () => pickled unit.pickled += (cls -> demandPickled) diff --git a/presentation-compiler/src/main/dotty/tools/pc/TastyUtils.scala b/presentation-compiler/src/main/dotty/tools/pc/TastyUtils.scala index d4033ce29e09..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, isBestEffortTasty = false).showContents() + new TastyPrinter(tastyBytes.nn, isBestEffortTasty = false, testPickler = false).showContents() private def htmlTasty( tastyURI: URI, From bdd6d72769d5c6864233e0abc1a2992f0ce0ca23 Mon Sep 17 00:00:00 2001 From: Jan Chyb Date: Tue, 26 Mar 2024 10:32:39 +0100 Subject: [PATCH 05/10] Solve mima issue (by doubling private method definition) --- .../BestEffortTastyHeaderUnpickler.scala | 102 +++++++++++++++++- .../tools/tasty/TastyHeaderUnpickler.scala | 2 +- 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala b/compiler/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala index bbdd0376e7d4..4325f55be4a7 100644 --- a/compiler/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala +++ b/compiler/src/dotty/tools/tasty/besteffort/BestEffortTastyHeaderUnpickler.scala @@ -3,7 +3,7 @@ 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} +import dotty.tools.tasty.{UnpicklerConfig, TastyHeaderUnpickler, TastyReader, UnpickleException, TastyFormat, TastyVersion} /** * The Best Effort Tasty Header consists of six fields: @@ -33,7 +33,7 @@ sealed abstract case class BestEffortTastyHeader( ) class BestEffortTastyHeaderUnpickler(config: UnpicklerConfig, reader: TastyReader) { - import TastyHeaderUnpickler._ + import BestEffortTastyHeaderUnpickler._ import reader._ def this(reader: TastyReader) = this(UnpicklerConfig.generic, reader) @@ -75,3 +75,101 @@ class BestEffortTastyHeaderUnpickler(config: UnpicklerConfig, reader: TastyReade 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/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala b/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala index fbb8f68b1142..78c5c0ba72b9 100644 --- a/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala +++ b/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala @@ -133,7 +133,7 @@ object TastyHeaderUnpickler { if (!cond) throw new UnpickleException(msg) } - private[tasty] def checkValidVersion(fileMajor: Int, fileMinor: Int, fileExperimental: Int, toolingVersion: String, config: UnpicklerConfig) = { + 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 From c08db684908346aaa62b5a906749cb1ff636690e Mon Sep 17 00:00:00 2001 From: Jan Chyb Date: Tue, 26 Mar 2024 11:23:32 +0100 Subject: [PATCH 06/10] fix windows tests --- compiler/test/dotty/tools/vulpix/ParallelTesting.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala index 9a3f0eac70af..880a3bd1cc53 100644 --- a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala +++ b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala @@ -691,7 +691,7 @@ trait ParallelTesting extends RunnerOrchestration { self => val reporter = mkReporter val driver = new Driver - val args = Array("-classpath", flags.defaultClassPath + ":" + bestEffortDir.toString) ++ flags.options + val args = Array("-classpath", flags.defaultClassPath + JFile.pathSeparator + bestEffortDir.toString) ++ flags.options driver.process(args ++ files0.map(_.toString), reporter = reporter) From 14601e8fefe34523515cd3a74f8fa9be43a6f86d Mon Sep 17 00:00:00 2001 From: Jan Chyb Date: Fri, 12 Apr 2024 13:14:32 +0200 Subject: [PATCH 07/10] apply review suggestions --- compiler/src/dotty/tools/dotc/Driver.scala | 1 + compiler/src/dotty/tools/dotc/ast/TreeInfo.scala | 4 ++-- compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala | 3 +-- compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala | 2 +- .../src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala | 7 ++++--- compiler/test/dotc/neg-best-effort-unpickling.blacklist | 3 +++ 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/Driver.scala b/compiler/src/dotty/tools/dotc/Driver.scala index e99ed4e8324a..580c0eae1810 100644 --- a/compiler/src/dotty/tools/dotc/Driver.scala +++ b/compiler/src/dotty/tools/dotc/Driver.scala @@ -41,6 +41,7 @@ class Driver { 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 diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index b79079daeaf5..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 - if !ctx.isBestEffort then 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 - if !ctx.isBestEffort then 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/core/SymbolLoaders.scala b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala index bbc96e68c2cd..4bfb9aaf13cf 100644 --- a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala +++ b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala @@ -435,7 +435,6 @@ class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader { val (classRoot, moduleRoot) = rootDenots(root.asClass) if (!isBestEffortTasty || ctx.withBestEffortTasty) then val tastyBytes = tastyFile.toByteArray - val unpickler = new tasty.DottyUnpickler(tastyFile, tastyBytes, isBestEffortTasty = isBestEffortTasty) unpickler.enter(roots = Set(classRoot, moduleRoot, moduleRoot.sourceModule))(using ctx.withSource(util.NoSource)) if mayLoadTreesFromTasty || isBestEffortTasty then classRoot.classSymbol.rootTreeOrProvider = unpickler @@ -446,7 +445,7 @@ class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader { else checkTastyUUID() else - report.error(em"Best Effort TASTy $tastyFile file could not be read.") + 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 diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index 02492d8215a3..d1f526d524d7 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -73,7 +73,7 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { * For regular non best-effort compilation (without best-effort and without using .betasty on classpath), * this will always return true. */ - private inline def passesConditionForErroringBestEffortCode(condition: Boolean)(using Context): Boolean = + private inline def passesConditionForErroringBestEffortCode(condition: => Boolean)(using Context): Boolean = ((!ctx.isBestEffort && ctx.reporter.errorsReported) || ctx.usedBestEffortTasty) || condition def addrOfSym(sym: Symbol): Option[Addr] = diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index b823c5c539f5..45bd58e3c7c1 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -455,7 +455,7 @@ class TreeUnpickler(reader: TastyReader, result } - def readSimpleType(): Type = tag match { + def readSimpleType(): Type = (tag: @switch) match { case TYPEREFdirect | TERMREFdirect => NamedType(NoPrefix, readSymRef()) case TYPEREFsymbol | TERMREFsymbol => @@ -497,8 +497,9 @@ class TreeUnpickler(reader: TastyReader, typeAtAddr.getOrElseUpdate(ref, forkAt(ref).readType()) case BYNAMEtype => ExprType(readType()) - case ERRORtype if isBestEffortTasty => - new PreviousErrorType + case ERRORtype => + if isBestEffortTasty then new PreviousErrorType + else throw new Error(s"Illegal ERRORtype in non Best Effort TASTy file") case _ => ConstantType(readConstant(tag)) } diff --git a/compiler/test/dotc/neg-best-effort-unpickling.blacklist b/compiler/test/dotc/neg-best-effort-unpickling.blacklist index 835ca7812089..6c0a74b8bf8c 100644 --- a/compiler/test/dotc/neg-best-effort-unpickling.blacklist +++ b/compiler/test/dotc/neg-best-effort-unpickling.blacklist @@ -12,3 +12,6 @@ i14834.scala # other type related crashes i4653.scala overrideClass.scala + +# repeating on a top level type definition +# i18750.scala \ No newline at end of file From 3c19ebc62e7f91d0e47ab5dc1df9ebcd4e0a775c Mon Sep 17 00:00:00 2001 From: Jan Chyb Date: Fri, 12 Apr 2024 17:04:19 +0200 Subject: [PATCH 08/10] post-rebase fixes --- .../src/dotty/tools/dotc/classpath/DirectoryClassPath.scala | 2 +- compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala | 2 +- compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala | 2 +- compiler/src/dotty/tools/dotc/transform/Pickler.scala | 2 +- compiler/src/dotty/tools/io/FileExtension.scala | 2 ++ compiler/test/dotc/neg-best-effort-unpickling.blacklist | 2 +- 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/classpath/DirectoryClassPath.scala b/compiler/src/dotty/tools/dotc/classpath/DirectoryClassPath.scala index 5ef52aaaaf3c..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.isBestEffortTasty || (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/core/SymbolLoaders.scala b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala index 4bfb9aaf13cf..fdc1ba9697d0 100644 --- a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala +++ b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala @@ -418,7 +418,7 @@ class ClassfileLoader(val classfile: AbstractFile) extends SymbolLoader { } class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader { - val isBestEffortTasty = tastyFile.extension == "betasty" + val isBestEffortTasty = tastyFile.hasBetastyExtension private val unpickler: tasty.DottyUnpickler = handleUnpicklingExceptions: val tastyBytes = tastyFile.toByteArray diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala index 57ce6e460d65..72f6895f122c 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala @@ -61,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") diff --git a/compiler/src/dotty/tools/dotc/transform/Pickler.scala b/compiler/src/dotty/tools/dotc/transform/Pickler.scala index 8e9b76225e7a..f34eadbff241 100644 --- a/compiler/src/dotty/tools/dotc/transform/Pickler.scala +++ b/compiler/src/dotty/tools/dotc/transform/Pickler.scala @@ -194,7 +194,7 @@ class Pickler extends Phase { override def isRunnable(using Context): Boolean = (super.isRunnable || ctx.isBestEffort) && (!ctx.settings.fromTasty.value || doAsyncTasty) - && (!ctx.usesBestEffortTasty || ctx.isBestEffort) + && (!ctx.usedBestEffortTasty || ctx.isBestEffort) // we do not want to pickle `.betasty` if will not create the file either way // when `-Yjava-tasty` is set we actually want to run this phase on Java sources diff --git a/compiler/src/dotty/tools/io/FileExtension.scala b/compiler/src/dotty/tools/io/FileExtension.scala index 262f2f21e70a..3aeef5b902ce 100644 --- a/compiler/src/dotty/tools/io/FileExtension.scala +++ b/compiler/src/dotty/tools/io/FileExtension.scala @@ -63,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 @@ -75,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/test/dotc/neg-best-effort-unpickling.blacklist b/compiler/test/dotc/neg-best-effort-unpickling.blacklist index 6c0a74b8bf8c..1e22d919f25a 100644 --- a/compiler/test/dotc/neg-best-effort-unpickling.blacklist +++ b/compiler/test/dotc/neg-best-effort-unpickling.blacklist @@ -14,4 +14,4 @@ i4653.scala overrideClass.scala # repeating on a top level type definition -# i18750.scala \ No newline at end of file +i18750.scala From cdf4f987b58ce5a15a7758b2d7843d1280a559e8 Mon Sep 17 00:00:00 2001 From: Jan Chyb Date: Tue, 16 Apr 2024 11:21:58 +0200 Subject: [PATCH 09/10] fix passesConditionForErroringBestEffortCode and improve doc comments --- .../dotty/tools/dotc/core/tasty/TreePickler.scala | 15 ++++++++------- .../src/dotty/tools/dotc/transform/Pickler.scala | 3 ++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index d1f526d524d7..be616b9054ae 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -66,15 +66,16 @@ 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 then we can try to do something else. - * For regular non best-effort compilation (without best-effort and without using .betasty on classpath), - * this will always return 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 + !((ctx.isBestEffort && ctx.reporter.errorsReported) || ctx.usedBestEffortTasty) || condition def addrOfSym(sym: Symbol): Option[Addr] = symRefs.get(sym) diff --git a/compiler/src/dotty/tools/dotc/transform/Pickler.scala b/compiler/src/dotty/tools/dotc/transform/Pickler.scala index f34eadbff241..3c6220239986 100644 --- a/compiler/src/dotty/tools/dotc/transform/Pickler.scala +++ b/compiler/src/dotty/tools/dotc/transform/Pickler.scala @@ -195,7 +195,8 @@ class Pickler extends Phase { (super.isRunnable || ctx.isBestEffort) && (!ctx.settings.fromTasty.value || doAsyncTasty) && (!ctx.usedBestEffortTasty || ctx.isBestEffort) - // we do not want to pickle `.betasty` if will not create the file either way + // 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 From 2eb73c518a08196b2acfc97ce34464465b544ddf Mon Sep 17 00:00:00 2001 From: Jan Chyb Date: Wed, 17 Apr 2024 11:28:59 +0200 Subject: [PATCH 10/10] remove unsafe-nulls from pickler --- compiler/src/dotty/tools/dotc/transform/Pickler.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/Pickler.scala b/compiler/src/dotty/tools/dotc/transform/Pickler.scala index 3c6220239986..8bb396ca4081 100644 --- a/compiler/src/dotty/tools/dotc/transform/Pickler.scala +++ b/compiler/src/dotty/tools/dotc/transform/Pickler.scala @@ -413,8 +413,8 @@ class Pickler extends Phase { ) if ctx.isBestEffort then val outpath = - ctx.settings.outputDir.value.jpath.toAbsolutePath.normalize - .resolve("META-INF") + ctx.settings.outputDir.value.jpath.toAbsolutePath.nn.normalize.nn + .resolve("META-INF").nn .resolve("best-effort") Files.createDirectories(outpath) BestEffortTastyWriter.write(outpath.nn, result)