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