Skip to content

Commit

Permalink
Finalize work on @SInCE and -scala-release:
Browse files Browse the repository at this point in the history
* Add missing @SInCE annotations to definitions
* Add missing checks for @SInCE
* Add more tests
  • Loading branch information
prolativ committed Dec 21, 2021
1 parent f3b85f1 commit bb61b27
Show file tree
Hide file tree
Showing 29 changed files with 219 additions and 113 deletions.
21 changes: 21 additions & 0 deletions compiler/src/dotty/tools/dotc/config/ScalaRelease.scala
@@ -0,0 +1,21 @@
package dotty.tools.dotc.config

import dotty.tools.tasty.TastyVersion

enum ScalaRelease(val majorVersion: Int, val minorVersion: Int) extends Ordered[ScalaRelease]:
case Release3_0 extends ScalaRelease(3, 0)
case Release3_1 extends ScalaRelease(3, 1)

def show = s"$majorVersion.$minorVersion"

def compare(that: ScalaRelease) =
val ord = summon[Ordering[(Int, Int)]]
ord.compare((majorVersion, minorVersion), (that.majorVersion, that.minorVersion))

object ScalaRelease:
def latest = Release3_1

def parse(name: String) = name match
case "3.0" => Some(Release3_0)
case "3.1" => Some(Release3_1)
case _ => None
5 changes: 4 additions & 1 deletion compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Expand Up @@ -26,6 +26,9 @@ object ScalaSettings:
(minTargetVersion to maxVersion).toList.map(_.toString)
else List(minTargetVersion).map(_.toString)

def supportedScalaReleaseVersions: List[String] =
ScalaRelease.values.toList.map(_.show)

def defaultClasspath: String = sys.env.getOrElse("CLASSPATH", ".")

def defaultPageWidth: Int = {
Expand Down Expand Up @@ -101,7 +104,7 @@ trait CommonScalaSettings:
val silentWarnings: Setting[Boolean] = BooleanSetting("-nowarn", "Silence all warnings.", aliases = List("--no-warnings"))

val release: Setting[String] = ChoiceSetting("-release", "release", "Compile code with classes specific to the given version of the Java platform available on the classpath and emit bytecode for this version.", ScalaSettings.supportedReleaseVersions, "", aliases = List("--release"))
val scalaRelease: Setting[ScalaVersion] = VersionSetting("-scala-release", "Emit TASTy files that can be consumed by specified version of the compiler.")
val scalaRelease: Setting[String] = ChoiceSetting("-scala-release", "release", "Emit TASTy files that can be consumed by specified version of the compiler.", ScalaSettings.supportedScalaReleaseVersions, "", aliases = List("--scala-release"))
val deprecation: Setting[Boolean] = BooleanSetting("-deprecation", "Emit warning and location for usages of deprecated APIs.", aliases = List("--deprecation"))
val feature: Setting[Boolean] = BooleanSetting("-feature", "Emit warning and location for usages of features that should be imported explicitly.", aliases = List("--feature"))
val explain: Setting[Boolean] = BooleanSetting("-explain", "Explain errors in more detail.", aliases = List("--explain"))
Expand Down
27 changes: 17 additions & 10 deletions compiler/src/dotty/tools/dotc/core/Contexts.scala
Expand Up @@ -24,7 +24,7 @@ import io.{AbstractFile, NoAbstractFile, PlainFile, Path}
import scala.io.Codec
import collection.mutable
import printing._
import config.{JavaPlatform, SJSPlatform, Platform, ScalaSettings}
import config.{JavaPlatform, SJSPlatform, Platform, ScalaSettings, ScalaRelease}
import classfile.ReusableDataReader
import StdNames.nme

Expand All @@ -38,8 +38,8 @@ import xsbti.AnalysisCallback
import plugins._
import java.util.concurrent.atomic.AtomicInteger
import java.nio.file.InvalidPathException
import dotty.tools.tasty.TastyFormat
import dotty.tools.dotc.config.{ NoScalaVersion, SpecificScalaVersion, AnyScalaVersion }
import dotty.tools.tasty.{ TastyFormat, TastyVersion }
import dotty.tools.dotc.config.{ NoScalaVersion, SpecificScalaVersion, AnyScalaVersion, ScalaBuild }

object Contexts {

Expand Down Expand Up @@ -484,13 +484,20 @@ object Contexts {
def importContext(imp: Import[?], sym: Symbol): FreshContext =
fresh.setImportInfo(ImportInfo(sym, imp.selectors, imp.expr))

def tastyVersion: (Int, Int, Int) =
base.settings.scalaRelease.value match
case NoScalaVersion =>
import TastyFormat.*
(MajorVersion, MinorVersion, ExperimentalVersion)
case SpecificScalaVersion(maj, min, _, _) => (maj.toInt + 25, min.toInt, 0)
case AnyScalaVersion => (28, 0, 0) // 3.0
def scalaRelease: ScalaRelease =
val releaseName = base.settings.scalaRelease.value
if releaseName.nonEmpty then ScalaRelease.parse(releaseName).get else ScalaRelease.latest

def tastyVersion: TastyVersion =
import math.Ordered.orderingToOrdered
val latestRelease = ScalaRelease.latest
val specifiedRelease = scalaRelease
if ((specifiedRelease.majorVersion, specifiedRelease.minorVersion) < (latestRelease.majorVersion, latestRelease.majorVersion)) then
// This is needed to make -scala-release a no-op when set to the latest release for unstable versions of the compiler
// (which might have the tasty format version numbers set to higher values before they're decreased during a release)
TastyVersion.fromStableScalaRelease(specifiedRelease.majorVersion, specifiedRelease.minorVersion)
else
TastyVersion.compilerVersion

/** Is the debug option set? */
def debug: Boolean = base.settings.Ydebug.value
Expand Down
20 changes: 17 additions & 3 deletions compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala
Expand Up @@ -3,7 +3,7 @@ package dotc
package core
package classfile

import dotty.tools.tasty.{ TastyReader, TastyHeaderUnpickler }
import dotty.tools.tasty.{ TastyFormat, TastyReader, TastyHeaderUnpickler, TastyVersion }

import Contexts._, Symbols._, Types._, Names._, StdNames._, NameOps._, Scopes._, Decorators._
import SymDenotations._, unpickleScala2.Scala2Unpickler._, Constants._, Annotations._, util.Spans._
Expand Down Expand Up @@ -884,7 +884,7 @@ class ClassfileParser(
}

def unpickleTASTY(bytes: Array[Byte]): Some[Embedded] = {
val unpickler = new tasty.DottyUnpickler(bytes)
val unpickler = new tasty.DottyUnpickler(bytes, ctx.tastyVersion)
unpickler.enter(roots = Set(classRoot, moduleRoot, moduleRoot.sourceModule))(using ctx.withSource(util.NoSource))
Some(unpickler)
}
Expand Down Expand Up @@ -950,9 +950,23 @@ class ClassfileParser(
if (tastyBytes.nonEmpty) {
val reader = new TastyReader(bytes, 0, 16)
val expectedUUID = new UUID(reader.readUncompressedLong(), reader.readUncompressedLong())
val tastyUUID = new TastyHeaderUnpickler(tastyBytes).readHeader()
val tastyHeader = new TastyHeaderUnpickler(tastyBytes).readFullHeader()
val fileTastyVersion = TastyVersion(tastyHeader.majorVersion, tastyHeader.minorVersion, tastyHeader.experimentalVersion)
val tastyUUID = tastyHeader.uuid
if (expectedUUID != tastyUUID)
report.warning(s"$classfile is out of sync with its TASTy file. Loaded TASTy file. Try cleaning the project to fix this issue", NoSourcePosition)

val tastyFilePath = classfile.path.stripSuffix(".class") + ".tasty"
val isTastyCompatible =
TastyFormat.isVersionCompatible(fileVersion = fileTastyVersion, compilerVersion = ctx.tastyVersion) ||
classRoot.symbol.showFullName.startsWith("scala.") // References to stdlib are considered safe because we check the values of @since annotations

if !isTastyCompatible then
report.error(s"""The class ${classRoot.symbol.showFullName} cannot be loaded from file ${tastyFilePath} because its TASTy format version is too high
|highest allowed: ${ctx.tastyVersion.show}
|found: ${fileTastyVersion.show}
""".stripMargin)

return unpickleTASTY(tastyBytes)
}
}
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala
Expand Up @@ -10,7 +10,7 @@ import classfile.ClassfileParser
import Names.SimpleName
import TreeUnpickler.UnpickleMode

import dotty.tools.tasty.TastyReader
import dotty.tools.tasty.{ TastyReader, TastyVersion }
import dotty.tools.tasty.TastyFormat.{ASTsSection, PositionsSection, CommentsSection}

object DottyUnpickler {
Expand Down Expand Up @@ -39,7 +39,7 @@ object DottyUnpickler {
* @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)
*/
class DottyUnpickler(bytes: Array[Byte], mode: UnpickleMode = UnpickleMode.TopLevel) extends ClassfileParser.Embedded with tpd.TreeProvider {
class DottyUnpickler(bytes: Array[Byte], maximalTastyVersion: TastyVersion, mode: UnpickleMode = UnpickleMode.TopLevel) extends ClassfileParser.Embedded with tpd.TreeProvider {
import tpd._
import DottyUnpickler._

Expand Down
10 changes: 5 additions & 5 deletions compiler/src/dotty/tools/dotc/core/tasty/TastyPickler.scala
Expand Up @@ -36,14 +36,14 @@ class TastyPickler(val rootCls: ClassSymbol) {
def lengthWithLength(buf: TastyBuffer) =
buf.length + natSize(buf.length)

val (majorVersion, minorVersion, experimentalVersion) = ctx.tastyVersion

nameBuffer.assemble()
sections.foreach(_._2.assemble())

val nameBufferHash = TastyHash.pjwHash64(nameBuffer.bytes)
val treeSectionHash +: otherSectionHashes = sections.map(x => TastyHash.pjwHash64(x._2.bytes))

val tastyVersion = ctx.tastyVersion

// Hash of name table and tree
val uuidLow: Long = nameBufferHash ^ treeSectionHash
// Hash of positions, comments and any additional section
Expand All @@ -52,9 +52,9 @@ class TastyPickler(val rootCls: ClassSymbol) {
val headerBuffer = {
val buf = new TastyBuffer(header.length + TastyPickler.versionStringBytes.length + 32)
for (ch <- header) buf.writeByte(ch.toByte)
buf.writeNat(majorVersion)
buf.writeNat(minorVersion)
buf.writeNat(experimentalVersion)
buf.writeNat(tastyVersion.major)
buf.writeNat(tastyVersion.minor)
buf.writeNat(tastyVersion.experimental)
buf.writeNat(TastyPickler.versionStringBytes.length)
buf.writeBytes(TastyPickler.versionStringBytes, TastyPickler.versionStringBytes.length)
buf.writeUncompressedLong(uuidLow)
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala
Expand Up @@ -198,7 +198,7 @@ object PickledQuotes {
quotePickling.println(s"**** unpickling quote from TASTY\n${TastyPrinter.showContents(bytes, ctx.settings.color.value == "never")}")

val mode = if (isType) UnpickleMode.TypeTree else UnpickleMode.Term
val unpickler = new DottyUnpickler(bytes, mode)
val unpickler = new DottyUnpickler(bytes, ctx.tastyVersion, mode)
unpickler.enter(Set.empty)

val tree = unpickler.tree
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/transform/Pickler.scala
Expand Up @@ -125,7 +125,7 @@ class Pickler extends Phase {
ctx.initialize()
val unpicklers =
for ((cls, pickler) <- picklers) yield {
val unpickler = new DottyUnpickler(pickler.assembleParts())
val unpickler = new DottyUnpickler(pickler.assembleParts(), ctx.tastyVersion)
unpickler.enter(roots = Set.empty)
cls -> unpickler
}
Expand Down
17 changes: 10 additions & 7 deletions compiler/src/dotty/tools/dotc/typer/RefChecks.scala
Expand Up @@ -15,7 +15,7 @@ import ast._
import MegaPhase._
import config.Printers.{checks, noPrinter}
import scala.util.{Try, Failure, Success}
import config.{ScalaVersion, NoScalaVersion}
import config.{ScalaVersion, NoScalaVersion, ScalaRelease}
import Decorators._
import OverridingPairs.isOverridingPair
import typer.ErrorReporting._
Expand Down Expand Up @@ -976,14 +976,14 @@ object RefChecks {
annot <- sym.getAnnotation(defn.SinceAnnot)
version <- annot.argumentConstantString(0)
do
val releaseVersion = ctx.settings.scalaRelease.value
ScalaVersion.parse(version) match
case Success(symVersion) if symVersion > ctx.settings.scalaRelease.value =>
val releaseVersion = ctx.scalaRelease
ScalaRelease.parse(version) match
case Some(symVersion) if symVersion > releaseVersion =>
report.error(
i"$sym was added in Scala $version, therefore it cannot be used in the code targeting Scala ${releaseVersion.unparse}",
i"$sym was added in Scala $version, therefore it cannot be used in the code targeting Scala ${releaseVersion.show}",
pos)
case Failure(ex) =>
report.warning(i"$sym has an unparsable version number: ${ex.getMessage}", pos)
case None =>
report.warning(i"$sym has an unparsable release name: '${version}'", pos)
case _ =>

private def checkSinceAnnotInSignature(sym: Symbol, pos: SrcPos)(using Context) =
Expand Down Expand Up @@ -1320,6 +1320,7 @@ class RefChecks extends MiniPhase { thisPhase =>
checkImplicitNotFoundAnnotation.template(cls.classDenot)
checkExperimentalInheritance(cls)
checkExperimentalAnnots(cls)
checkSinceAnnot(cls, cls.srcPos)
tree
}
catch {
Expand Down Expand Up @@ -1371,9 +1372,11 @@ class RefChecks extends MiniPhase { thisPhase =>
case TypeRef(_, sym: Symbol) =>
checkDeprecated(sym, tree.srcPos)
checkExperimental(sym, tree.srcPos)
checkSinceAnnot(sym, tree.srcPos)
case TermRef(_, sym: Symbol) =>
checkDeprecated(sym, tree.srcPos)
checkExperimental(sym, tree.srcPos)
checkSinceAnnot(sym, tree.srcPos)
case _ =>
}
tree
Expand Down
Expand Up @@ -114,7 +114,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(f.toByteArray())
val unpickler = new DottyUnpickler(f.toByteArray(), ctx.tastyVersion)
unpickler.enter(roots = Set.empty)
unpickler.rootTrees(using ctx)
}
Expand Down
51 changes: 27 additions & 24 deletions compiler/test/dotty/tools/vulpix/ParallelTesting.scala
Expand Up @@ -128,7 +128,7 @@ trait ParallelTesting extends RunnerOrchestration { self =>
}
sb.toString + "\n\n"
}
case self: SeparateCompilationSource => { // TODO: this is incorrect when using other versions of compiler
case self: SeparateCompilationSource => { // TODO: this won't work when using other versions of compiler
val command = sb.toString
val fsb = new StringBuilder(command)
self.compilationGroups.foreach { (_, files) =>
Expand Down Expand Up @@ -174,25 +174,25 @@ trait ParallelTesting extends RunnerOrchestration { self =>
flags: TestFlags,
outDir: JFile
) extends TestSource {
case class Group(ordinal: Int, compiler: String, target: String)
case class Group(ordinal: Int, compiler: String, release: String)

lazy val compilationGroups: List[(Group, Array[JFile])] =
val Target = """t([\d\.]+)""".r
val Compiler = """v([\d\.]+)""".r
val Release = """r([\d\.]+)""".r
val Compiler = """c([\d\.]+)""".r
val Ordinal = """(\d+)""".r
def groupFor(file: JFile): Group =
val annotPart = file.getName.dropWhile(_ != '_').stripSuffix(".scala").stripSuffix(".java")
val annots = annotPart.split("_")
val ordinal = annots.collectFirst { case Ordinal(n) => n.toInt }.getOrElse(Int.MinValue)
val target = annots.collectFirst { case Target(t) => t }.getOrElse("")
val compiler = annots.collectFirst { case Compiler(c) => c}.getOrElse("")
Group(ordinal, compiler, target)
val groupSuffix = file.getName.dropWhile(_ != '_').stripSuffix(".scala").stripSuffix(".java")
val groupSuffixParts = groupSuffix.split("_")
val ordinal = groupSuffixParts.collectFirst { case Ordinal(n) => n.toInt }.getOrElse(Int.MinValue)
val release = groupSuffixParts.collectFirst { case Release(r) => r }.getOrElse("")
val compiler = groupSuffixParts.collectFirst { case Compiler(c) => c }.getOrElse("")
Group(ordinal, compiler, release)

dir.listFiles
.filter(isSourceFile)
.groupBy(groupFor)
.toList
.sortBy { (g, _) => (g.ordinal, g.compiler, g.target) }
.sortBy { (g, _) => (g.ordinal, g.compiler, g.release) }
.map { (g, f) => (g, f.sorted) }

def sourceFiles = compilationGroups.map(_._2).flatten.toArray
Expand All @@ -215,7 +215,7 @@ trait ParallelTesting extends RunnerOrchestration { self =>

case testSource @ SeparateCompilationSource(_, dir, flags, outDir) =>
testSource.compilationGroups.map { (group, files) =>
val flags1 = if group.target.isEmpty then flags else flags.and(s"-scala-release:${group.target}")
val flags1 = if group.release.isEmpty then flags else flags.and(s"-scala-release:${group.release}")
if group.compiler.isEmpty then
compile(files, flags1, suppressErrors, outDir)
else
Expand Down Expand Up @@ -509,15 +509,15 @@ trait ParallelTesting extends RunnerOrchestration { self =>

def substituteClasspath(old: String): String =
old.split(JFile.pathSeparator).map { o =>
if JFile(o) == JFile(Properties.dottyLibrary) then s"$compilerDir/lib/scala3-library_3-${trueVersions(compiler)}.jar"
if JFile(o) == JFile(Properties.dottyLibrary) then s"$compilerDir/lib/scala3-library_3-${patchVersions(compiler)}.jar"
else o
}.mkString(JFile.pathSeparator)

val flags1 = flags.copy(defaultClassPath = substituteClasspath(flags.defaultClassPath))
.withClasspath(targetDir.getPath)
.and("-d", targetDir.getPath)

val reporter = TestReporter.reporter(realStdout, ERROR) // TODO: do some reporting
val reporter = TestReporter.reporter(realStdout, ERROR)

val command = Array(compilerDir + "/bin/scalac") ++ flags1.all ++ files.map(_.getPath)
val process = Runtime.getRuntime.exec(command)
Expand Down Expand Up @@ -1401,24 +1401,27 @@ object ParallelTesting {
f.getName.endsWith(".tasty")

def getCompiler(version: String): JFile =
val patch = trueVersions(version)
val patch = patchVersions(version)
val dir = cache.resolve(s"scala3-${patch}").toFile
if dir.exists then
dir
else
import scala.sys.process._
val zipPath = cache.resolve(s"scala3-$patch.zip")
(URL(s"https://github.com/lampepfl/dotty/releases/download/$patch/scala3-$patch.zip") #>> zipPath.toFile #&& s"unzip $zipPath -d $cache").!!
dir
synchronized {
if dir.exists then
dir
else
import scala.sys.process._
val zipPath = cache.resolve(s"scala3-$patch.zip")
val compilerDownloadUrl = s"https://github.com/lampepfl/dotty/releases/download/$patch/scala3-$patch.zip"
(URL(compilerDownloadUrl) #>> zipPath.toFile #&& s"unzip $zipPath -d $cache").!!
dir
}


val trueVersions = Map(
val patchVersions = Map(
"3.0" -> "3.0.2",
"3.1" -> "3.1.0"
)

private lazy val cache =
val dir = Files.createTempDirectory("dotty.tests")
// dir.toFile.deleteOnExit()
dir.toFile.deleteOnExit()
dir
}
2 changes: 1 addition & 1 deletion library/src/scala/annotation/since.scala
@@ -1,4 +1,4 @@
package scala.annotation

/** An annotation that is used to mark symbols added to the stdlib after 3.0 release */
private[scala] class since(scalaRelease: String) extends scala.annotation.StaticAnnotation
private[scala] class since(scalaRelease: String) extends scala.annotation.StaticAnnotation
14 changes: 5 additions & 9 deletions tasty/src/dotty/tools/tasty/TastyFormat.scala
Expand Up @@ -338,16 +338,12 @@ object TastyFormat {
* @syntax markdown
*/
def isVersionCompatible(
fileMajor: Int,
fileMinor: Int,
fileExperimental: Int,
compilerMajor: Int,
compilerMinor: Int,
compilerExperimental: Int
fileVersion: TastyVersion,
compilerVersion: TastyVersion
): Boolean = (
fileMajor == compilerMajor &&
( fileMinor == compilerMinor && fileExperimental == compilerExperimental // full equality
|| fileMinor < compilerMinor && fileExperimental == 0 // stable backwards compatibility
fileVersion.major == compilerVersion.major &&
( fileVersion.minor == compilerVersion.minor && fileVersion.experimental == compilerVersion.experimental // full equality
|| fileVersion.minor < compilerVersion.minor && fileVersion.experimental == 0 // stable backwards compatibility
)
)

Expand Down

0 comments on commit bb61b27

Please sign in to comment.