Skip to content

Commit 8591fe6

Browse files
committed
Reworks for forward compat:
* Make -scala-release an experimental setting * Add better checks of of TASTy version * Validate names of releases inside since annotations * Extend Vulpix to check actual counts and possitions of errors from other compilers
1 parent e556cf9 commit 8591fe6

File tree

15 files changed

+129
-28
lines changed

15 files changed

+129
-28
lines changed

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ trait CommonScalaSettings:
104104
val silentWarnings: Setting[Boolean] = BooleanSetting("-nowarn", "Silence all warnings.", aliases = List("--no-warnings"))
105105

106106
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"))
107-
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"))
108107
val deprecation: Setting[Boolean] = BooleanSetting("-deprecation", "Emit warning and location for usages of deprecated APIs.", aliases = List("--deprecation"))
109108
val feature: Setting[Boolean] = BooleanSetting("-feature", "Emit warning and location for usages of features that should be imported explicitly.", aliases = List("--feature"))
110109
val explain: Setting[Boolean] = BooleanSetting("-explain", "Explain errors in more detail.", aliases = List("--explain"))
@@ -309,6 +308,7 @@ private sealed trait YSettings:
309308
val YexplicitNulls: Setting[Boolean] = BooleanSetting("-Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.")
310309
val YcheckInit: Setting[Boolean] = BooleanSetting("-Ysafe-init", "Ensure safe initialization of objects")
311310
val YrequireTargetName: Setting[Boolean] = BooleanSetting("-Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation")
311+
val YscalaRelease: Setting[String] = ChoiceSetting("-Yscala-release", "release", "Emit TASTy files that can be consumed by specified version of the compiler.", ScalaSettings.supportedScalaReleaseVersions, "", aliases = List("--Yscala-release"))
312312

313313
/** Area-specific debug output */
314314
val YexplainLowlevel: Setting[Boolean] = BooleanSetting("-Yexplain-lowlevel", "When explaining type errors, show types at a lower level.")

compiler/src/dotty/tools/dotc/core/Contexts.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -485,15 +485,15 @@ object Contexts {
485485
fresh.setImportInfo(ImportInfo(sym, imp.selectors, imp.expr))
486486

487487
def scalaRelease: ScalaRelease =
488-
val releaseName = base.settings.scalaRelease.value
488+
val releaseName = base.settings.YscalaRelease.value
489489
if releaseName.nonEmpty then ScalaRelease.parse(releaseName).get else ScalaRelease.latest
490490

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

compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -957,16 +957,23 @@ class ClassfileParser(
957957
report.warning(s"$classfile is out of sync with its TASTy file. Loaded TASTy file. Try cleaning the project to fix this issue", NoSourcePosition)
958958

959959
val tastyFilePath = classfile.path.stripSuffix(".class") + ".tasty"
960-
val isTastyCompatible =
961-
TastyFormat.isVersionCompatible(fileVersion = fileTastyVersion, compilerVersion = ctx.tastyVersion) ||
962-
classRoot.symbol.showFullName.startsWith("scala.") // References to stdlib are considered safe because we check the values of @since annotations
963-
964-
if !isTastyCompatible then
965-
report.error(s"""The class ${classRoot.symbol.showFullName} cannot be loaded from file ${tastyFilePath} because its TASTy format version is too high
966-
|highest allowed: ${ctx.tastyVersion.show}
967-
|found: ${fileTastyVersion.show}
960+
961+
def reportWrongTasty(reason: String, highestAllowed: TastyVersion) =
962+
report.error(s"""The class ${classRoot.symbol.showFullName} cannot be loaded from file ${tastyFilePath} because $reason:
963+
|highest allowed: ${highestAllowed.show}
964+
|found: ${fileTastyVersion.show}
968965
""".stripMargin)
969966

967+
val isTastyReadable = TastyFormat.isVersionCompatible(fileVersion = fileTastyVersion, compilerVersion = TastyVersion.compilerVersion)
968+
if !isTastyReadable then
969+
reportWrongTasty("its TASTy format cannot be read by the compiler", TastyVersion.compilerVersion)
970+
else
971+
val isTastyCompatible =
972+
TastyFormat.isVersionCompatible(fileVersion = fileTastyVersion, compilerVersion = ctx.tastyVersion) ||
973+
classRoot.symbol.showFullName.startsWith("scala.") // References to stdlib are considered safe because we check the values of @since annotations
974+
if !isTastyCompatible then
975+
reportWrongTasty(s"its TASTy format is not compatible with the one of the targeted Scala release (${ctx.scalaRelease.show})", ctx.tastyVersion)
976+
970977
return unpickleTASTY(tastyBytes)
971978
}
972979
}

compiler/src/dotty/tools/dotc/typer/RefChecks.scala

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -974,16 +974,15 @@ object RefChecks {
974974
private def checkSinceAnnot(sym: Symbol, pos: SrcPos)(using Context): Unit =
975975
for
976976
annot <- sym.getAnnotation(defn.SinceAnnot)
977-
version <- annot.argumentConstantString(0)
977+
releaseName <- annot.argumentConstantString(0)
978978
do
979-
val releaseVersion = ctx.scalaRelease
980-
ScalaRelease.parse(version) match
981-
case Some(symVersion) if symVersion > releaseVersion =>
979+
ScalaRelease.parse(releaseName) match
980+
case Some(release) if release > ctx.scalaRelease =>
982981
report.error(
983-
i"$sym was added in Scala $version, therefore it cannot be used in the code targeting Scala ${releaseVersion.show}",
982+
i"$sym was added in Scala release ${releaseName.show}, therefore it cannot be used in the code targeting Scala ${ctx.scalaRelease.show}",
984983
pos)
985984
case None =>
986-
report.warning(i"$sym has an unparsable release name: '${version}'", pos)
985+
report.error(i"$sym has an unparsable release name: '${releaseName}'", annot.tree.srcPos)
987986
case _ =>
988987

989988
private def checkSinceAnnotInSignature(sym: Symbol, pos: SrcPos)(using Context) =
@@ -1281,6 +1280,7 @@ class RefChecks extends MiniPhase { thisPhase =>
12811280
checkDeprecatedOvers(tree)
12821281
checkExperimentalAnnots(tree.symbol)
12831282
checkExperimentalSignature(tree.symbol, tree)
1283+
checkSinceAnnot(tree.symbol, tree.srcPos)
12841284
checkSinceAnnotInSignature(tree.symbol, tree)
12851285
val sym = tree.symbol
12861286
if (sym.exists && sym.owner.isTerm) {
@@ -1320,7 +1320,6 @@ class RefChecks extends MiniPhase { thisPhase =>
13201320
checkImplicitNotFoundAnnotation.template(cls.classDenot)
13211321
checkExperimentalInheritance(cls)
13221322
checkExperimentalAnnots(cls)
1323-
checkSinceAnnot(cls, cls.srcPos)
13241323
tree
13251324
}
13261325
catch {
@@ -1384,6 +1383,7 @@ class RefChecks extends MiniPhase { thisPhase =>
13841383

13851384
override def transformTypeDef(tree: TypeDef)(using Context): TypeDef = {
13861385
checkExperimentalAnnots(tree.symbol)
1386+
checkSinceAnnot(tree.symbol, tree.srcPos)
13871387
tree
13881388
}
13891389
}

compiler/test/dotty/tools/vulpix/ParallelTesting.scala

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package dotty
22
package tools
33
package vulpix
44

5-
import java.io.{File => JFile, IOException}
5+
import java.io.{File => JFile, IOException, PrintStream, ByteArrayOutputStream}
66
import java.lang.System.{lineSeparator => EOL}
77
import java.net.URL
88
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
@@ -13,7 +13,7 @@ import java.util.{HashMap, Timer, TimerTask}
1313
import java.util.concurrent.{TimeUnit, TimeoutException, Executors => JExecutors}
1414

1515
import scala.collection.mutable
16-
import scala.io.Source
16+
import scala.io.{Codec, Source}
1717
import scala.util.{Random, Try, Failure => TryFailure, Success => TrySuccess, Using}
1818
import scala.util.control.NonFatal
1919
import scala.util.matching.Regex
@@ -27,7 +27,7 @@ import dotc.interfaces.Diagnostic.ERROR
2727
import dotc.reporting.{Reporter, TestReporter}
2828
import dotc.reporting.Diagnostic
2929
import dotc.config.Config
30-
import dotc.util.DiffUtil
30+
import dotc.util.{DiffUtil, SourceFile, SourcePosition, Spans}
3131
import io.AbstractFile
3232
import dotty.tools.vulpix.TestConfiguration.defaultOptions
3333

@@ -215,7 +215,7 @@ trait ParallelTesting extends RunnerOrchestration { self =>
215215

216216
case testSource @ SeparateCompilationSource(_, dir, flags, outDir) =>
217217
testSource.compilationGroups.map { (group, files) =>
218-
val flags1 = if group.release.isEmpty then flags else flags.and(s"-scala-release:${group.release}")
218+
val flags1 = if group.release.isEmpty then flags else flags.and("-Yscala-release", group.release)
219219
if group.compiler.isEmpty then
220220
compile(files, flags1, suppressErrors, outDir)
221221
else
@@ -504,6 +504,21 @@ trait ParallelTesting extends RunnerOrchestration { self =>
504504
reporter
505505
}
506506

507+
private def parseErrors(errorsText: String, compilerVersion: String) =
508+
val errorPattern = """.*Error: (.*\.scala):(\d+):(\d+).*""".r
509+
errorsText.linesIterator.toSeq.collect {
510+
case errorPattern(filePath, line, column) =>
511+
val lineNum = line.toInt
512+
val columnNum = column.toInt
513+
val abstractFile = AbstractFile.getFile(filePath)
514+
val sourceFile = SourceFile(abstractFile, Codec.UTF8)
515+
val offset = sourceFile.lineToOffset(lineNum - 1) + columnNum - 1
516+
val span = Spans.Span(offset)
517+
val sourcePos = SourcePosition(sourceFile, span)
518+
519+
Diagnostic.Error(s"Compilation of $filePath with Scala $compilerVersion failed at line: $line, column: $column. Full error output:\n\n$errorsText\n", sourcePos)
520+
}
521+
507522
protected def compileWithOtherCompiler(compiler: String, files: Array[JFile], flags: TestFlags, targetDir: JFile): TestReporter =
508523
val compilerDir = getCompiler(compiler).toString
509524

@@ -517,14 +532,18 @@ trait ParallelTesting extends RunnerOrchestration { self =>
517532
.withClasspath(targetDir.getPath)
518533
.and("-d", targetDir.getPath)
519534

520-
val reporter = TestReporter.reporter(realStdout, ERROR)
535+
val dummyStream = new PrintStream(new ByteArrayOutputStream())
536+
val reporter = TestReporter.reporter(dummyStream, ERROR)
521537

522538
val command = Array(compilerDir + "/bin/scalac") ++ flags1.all ++ files.map(_.getPath)
523539
val process = Runtime.getRuntime.exec(command)
524-
val output = Source.fromInputStream(process.getErrorStream).mkString
540+
val errorsText = Source.fromInputStream(process.getErrorStream).mkString
525541
if process.waitFor() != 0 then
526-
echo(s"\nCompilation using Scala $compiler failed: \n$output")
527-
fail()
542+
val diagnostics = parseErrors(errorsText, compiler)
543+
diagnostics.foreach { diag =>
544+
val context = (new ContextBase).initialCtx
545+
reporter.report(diag)(using context)
546+
}
528547

529548
reporter
530549

tasty/src/dotty/tools/tasty/TastyVersion.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package dotty.tools.tasty
22

33
case class TastyVersion(major: Int, minor: Int, experimental: Int) {
4-
def show = s"$major.$minor-$experimental"
4+
def show = "" + major + "." + minor + "-" + experimental
55
}
66

77
object TastyVersion {

tests/neg/forwardCompat-illegalReferences/Test_r3.0.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,8 @@ def useQuotes(using Quotes) =
2424

2525
class Box(w: Wildcard) // error
2626

27+
// The inferred result type also gets reported even though it's not written explicitly
2728
def castToWildcard(x: Any) = x.asInstanceOf[Wildcard] // error // error
29+
30+
// 2 errors reported because at the stage of compilation when this is checked (already after some code transformations) the illegal type is referred to more than once
31+
val selectable: Any = new Selectable.WithoutPreciseParameterTypes {} // error // error
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package scala.test
2+
3+
import annotation.since
4+
5+
@since("") // error
6+
val x = 1
7+
8+
@since("1.2.3.4") // error
9+
val y = "abc"
10+
11+
@since("xyz") // error
12+
class Foo
13+
14+
@since("-3") // error
15+
trait Bar
16+
17+
@since("3.0.2") // error
18+
type Baz = Int
19+
20+
@since("3.0 ") // error
21+
given String = ""
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Adapted from i11050
2+
3+
sealed trait TreeValue
4+
5+
sealed trait SubLevel extends TreeValue
6+
7+
case class Leaf1(value: String) extends TreeValue
8+
case class Leaf2(value: Int) extends SubLevel
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import scala.deriving._
2+
3+
object Test:
4+
def main(args: Array[String]): Unit =
5+
println(summon[Mirror.Of[TreeValue]]) // error
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Adapted from i11050
2+
3+
sealed trait TreeValue
4+
5+
sealed trait SubLevel extends TreeValue
6+
7+
case class Leaf1(value: String) extends TreeValue
8+
case class Leaf2(value: Int) extends SubLevel
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import scala.deriving._
2+
3+
val treeValueMirror = summon[Mirror.Of[TreeValue]]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
object Test:
2+
def main(args: Array[String]): Unit =
3+
println(treeValueMirror)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Adapted from i12949
2+
3+
object Catch22:
4+
trait TC[V]
5+
object TC:
6+
export Hodor.TC.given
7+
8+
object Hodor:
9+
object TC:
10+
import Catch22.TC
11+
given fromString[V <: String]: TC[V] = new TC[V] {}
12+
transparent inline given fromDouble[V <: Double]: TC[V] =
13+
new TC[V]:
14+
type Out = Double
15+
given fromInt[V <: Int]: TC[V] with
16+
type Out = Int
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Adapted from i12949
2+
3+
object Test:
4+
def main(args: Array[String]): Unit =
5+
summon[Catch22.TC["hi"]]
6+
summon[Catch22.TC[7.7]]
7+
summon[Catch22.TC[1]]

0 commit comments

Comments
 (0)