diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 04c7b5872204..1768d5749f84 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -76,7 +76,7 @@ object Feature: def scala2ExperimentalMacroEnabled(using Context) = enabled(scala2macros) def sourceVersionSetting(using Context): SourceVersion = - SourceVersion.valueOf(ctx.settings.source.value) + SourceVersion.lookupSourceVersion.fromSetting(ctx.settings.source.value) def sourceVersion(using Context): SourceVersion = ctx.compilationUnit.sourceVersion match @@ -85,14 +85,11 @@ object Feature: def migrateTo3(using Context): Boolean = sourceVersion == `3.0-migration` - /** If current source migrates to `version`, issue given warning message + /** If current source migrates to a version in the series denoted by `version`, issue given warning message * and return `true`, otherwise return `false`. */ - def warnOnMigration(msg: Message, pos: SrcPos, - version: SourceVersion)(using Context): Boolean = - if sourceVersion.isMigrating && sourceVersion.stable == version - || (version == `3.0` || version == `3.1` || version == `3.2`) && migrateTo3 - then + def warnOnMigration(msg: Message, pos: SrcPos, version: SourceVersion)(using Context): Boolean = + if sourceVersion.isMigrating && sourceVersion.series == version.series then report.migrationWarning(msg, pos) true else diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index fa364110b052..7e5273c9086a 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -5,6 +5,7 @@ import scala.language.unsafeNulls import dotty.tools.dotc.config.PathResolver.Defaults import dotty.tools.dotc.config.Settings.{Setting, SettingGroup} +import dotty.tools.dotc.config.SourceVersion import dotty.tools.dotc.core.Contexts._ import dotty.tools.dotc.rewrites.Rewrites import dotty.tools.io.{AbstractFile, Directory, JDK9Reflectors, PlainDirectory} @@ -51,7 +52,15 @@ trait AllScalaSettings extends CommonScalaSettings, PluginSettings, VerboseSetti /* Path related settings */ val semanticdbTarget: Setting[String] = PathSetting("-semanticdb-target", "Specify an alternative output directory for SemanticDB files.", "") - val source: Setting[String] = ChoiceSetting("-source", "source version", "source version", List("3.0", "3.1", "future", "3.0-migration", "future-migration"), "3.0", aliases = List("--source")) + private val SourceVersionOptions = + SourceVersion + .lookupSourceVersion.fromSetting + .toList + // we want `3.1-migration` to be before `3.1`, but both map to same SourceVersion + .sortBy((key, version) => (version.ordinal, !key.endsWith("-migration"))) + .map((key, _) => key) + + val source: Setting[String] = ChoiceSetting("-source", "source version", "source version", SourceVersionOptions, SourceVersion.defaultSourceVersion.toString, aliases = List("--source")) val uniqid: Setting[Boolean] = BooleanSetting("-uniqid", "Uniquely tag all identifiers in debugging output.", aliases = List("--unique-id")) val rewrite: Setting[Option[Rewrites]] = OptionSetting[Rewrites]("-rewrite", "When used in conjunction with a `...-migration` source version, rewrites sources to migrate to new version.", aliases = List("--rewrite")) val fromTasty: Setting[Boolean] = BooleanSetting("-from-tasty", "Compile classes from tasty files. The arguments are .tasty or .jar files.", aliases = List("--from-tasty")) diff --git a/compiler/src/dotty/tools/dotc/config/SourceVersion.scala b/compiler/src/dotty/tools/dotc/config/SourceVersion.scala index 9361c7e6774d..abe8ee0a31f4 100644 --- a/compiler/src/dotty/tools/dotc/config/SourceVersion.scala +++ b/compiler/src/dotty/tools/dotc/config/SourceVersion.scala @@ -5,17 +5,61 @@ package config import core.Decorators.* import util.Property +import scala.annotation.threadUnsafe +import dotty.tools.dotc.core.Names.Name + +/** Enum expressing series of source versions, + * where each series begins by a migration version, followed by a series of stable versions. + * e.g. `3.0-migration`, `3.0`, `3.1` make one series, `3.0`, as they describe the same semantics. + * `future-migration`, then begins the another series of stable versions, `future`, because in this version we + * enable more features. + * + * @note This enum does not need to correspond to the scala.language imports. E.g. if a user imports + * `scala.language.3.1-migration`, the SourceVersion will be set to `3.1` (see `lookupSourceVersion`). + * + */ enum SourceVersion: - case `3.0-migration`, `3.0`, `3.1`, `3.2`, `future-migration`, `future` + case `3.0-migration`, `3.0`, `3.1` // Note: do not add `3.1-migration` here, 3.1 is the same language as 3.0. + // case `3.2-migration` // !!! UNCOMMENT `3.2-migration` BEFORE RELEASING 3.2.0 if we enable features from `future` + case `3.2` + case `future-migration`, `future` val isMigrating: Boolean = toString.endsWith("-migration") - def stable: SourceVersion = - if isMigrating then SourceVersion.values(ordinal + 1) else this + private inline def nextVersion: SourceVersion = SourceVersion.fromOrdinal(ordinal + 1) + private inline def previousVersion: SourceVersion = SourceVersion.fromOrdinal(ordinal - 1) + + @threadUnsafe lazy val series: SourceVersion = + if isMigrating then nextVersion else previousVersion.series - def isAtLeast(v: SourceVersion) = stable.ordinal >= v.ordinal + def isAtLeast(v: SourceVersion) = this.series.ordinal >= v.series.ordinal object SourceVersion extends Property.Key[SourceVersion]: + def defaultSourceVersion = `3.2` + + object lookupSourceVersion: + + /** A map from with keys matching the `scala.language` imports, + * and values being the corresponding `SourceVersion`, or the next + * stable version if the key ends with `-migration` and no matching SourceVersion exists. + */ + val fromSetting: Map[String, SourceVersion] = + val (migratingVersions, stableVersions) = values.partition(_.isMigrating) + val entries = stableVersions.flatMap(stable => + val migratingKey = s"${stable}-migration" + val migratingEntry = + migratingVersions.find(_.toString == migratingKey) match + case Some(migrating) => migratingKey -> migrating + case _ => migratingKey -> stable + val stableEntry = stable.toString -> stable + stableEntry :: migratingEntry :: Nil + ) + Map.from(entries) + + /** An immutable array with keys matching the `scala.language` imports + * corresponding to source versions + */ + val fromImport: IArray[Name] = + IArray.from(fromSetting.iterator.map((key, _) => key.toTermName)) - val allSourceVersionNames = values.toList.map(_.toString.toTermName) end SourceVersion diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 7a5620be0415..b91a8531145b 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3140,14 +3140,14 @@ object Parsers { in.languageImportContext = in.languageImportContext.importContext(imp, NoSymbol) for case ImportSelector(id @ Ident(imported), EmptyTree, _) <- selectors - if allSourceVersionNames.contains(imported) + if lookupSourceVersion.fromImport.contains(imported) do if !outermost then syntaxError(i"source version import is only allowed at the toplevel", id.span) else if ctx.compilationUnit.sourceVersion.isDefined then syntaxError(i"duplicate source version import", id.span) else - ctx.compilationUnit.sourceVersion = Some(SourceVersion.valueOf(imported.toString)) + ctx.compilationUnit.sourceVersion = Some(lookupSourceVersion.fromSetting(imported.toString)) case None => imp diff --git a/compiler/src/dotty/tools/dotc/report.scala b/compiler/src/dotty/tools/dotc/report.scala index 94a7b4f318fa..97637e56c675 100644 --- a/compiler/src/dotty/tools/dotc/report.scala +++ b/compiler/src/dotty/tools/dotc/report.scala @@ -67,10 +67,9 @@ object report: error(ex.toMessage, pos, sticky = true) if ctx.settings.YdebugTypeError.value then ex.printStackTrace() - def errorOrMigrationWarning(msg: Message, pos: SrcPos = NoSourcePosition, - from: SourceVersion)(using Context): Unit = + def errorOrMigrationWarning(msg: Message, pos: SrcPos = NoSourcePosition, from: SourceVersion)(using Context): Unit = if sourceVersion.isAtLeast(from) then - if sourceVersion.isMigrating && sourceVersion.ordinal <= from.ordinal then migrationWarning(msg, pos) + if sourceVersion.isMigrating && sourceVersion.series == from.series then migrationWarning(msg, pos) else error(msg, pos) def restrictionError(msg: Message, pos: SrcPos = NoSourcePosition)(using Context): Unit = diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index f8089dfcabfa..46df97937f46 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -38,7 +38,7 @@ class CompilationTests { compileFilesInDir("tests/pos-special/spec-t5545", defaultOptions), compileFilesInDir("tests/pos-special/strawman-collections", allowDeepSubtypes), compileFilesInDir("tests/pos-special/isInstanceOf", allowDeepSubtypes.and("-Xfatal-warnings")), - compileFilesInDir("tests/new", defaultOptions.and("-source", "3.1")), // just to see whether 3.1 works + compileFilesInDir("tests/new", defaultOptions.and("-source", "3.2")), // just to see whether 3.2 works compileFilesInDir("tests/pos-scala2", scala2CompatMode), compileFilesInDir("tests/pos-custom-args/erased", defaultOptions.and("-language:experimental.erasedDefinitions")), compileFilesInDir("tests/pos", defaultOptions.and("-Ysafe-init")), diff --git a/compiler/test/dotty/tools/dotc/config/SourceVersionTest.scala b/compiler/test/dotty/tools/dotc/config/SourceVersionTest.scala new file mode 100644 index 000000000000..ec8c03e43b9b --- /dev/null +++ b/compiler/test/dotty/tools/dotc/config/SourceVersionTest.scala @@ -0,0 +1,111 @@ +package dotty.tools.dotc.config + +import dotty.tools.dotc.core.Decorators.* + +import org.junit.Test +import org.junit.Assert.* + +import SourceVersionTest.* + +class SourceVersionTest: + + @Test def `importedVersion`: Unit = + assertEquals(SourceVersion.`3.0-migration`, importLanguageDot("3.0-migration")) + assertEquals(SourceVersion.`3.0`, importLanguageDot("3.0")) + + // crucial that here `import scala.language.3.1-migration` sets the source version to `3.1` + assertEquals(SourceVersion.`3.1`, importLanguageDot("3.1-migration")) + + assertEquals(SourceVersion.`3.1`, importLanguageDot("3.1")) + // assertEquals(SourceVersion.`3.2-migration`, importLanguageDot("3.2-migration")) // uncomment when we introduce `3.2-migration` + assertEquals(SourceVersion.`3.2`, importLanguageDot("3.2-migration")) // delete when we introduce `3.2-migration` + assertEquals(SourceVersion.`3.2`, importLanguageDot("3.2")) + assertEquals(SourceVersion.`future-migration`, importLanguageDot("future-migration")) + assertEquals(SourceVersion.`future`, importLanguageDot("future")) + + @Test def `series`: Unit = + assertEquals(SourceVersion.`3.0`, SourceVersion.`3.0-migration`.series) + assertEquals(SourceVersion.`3.0`, SourceVersion.`3.0`.series) + assertEquals(SourceVersion.`3.0`, SourceVersion.`3.1`.series) + // assertEquals(SourceVersion.`3.2`, SourceVersion.`3.2-migration`.series) // uncomment when we introduce `3.2-migration` + // assertEquals(SourceVersion.`3.2`, SourceVersion.`3.2`.series) // uncomment when we introduce `3.2-migration` + assertEquals(SourceVersion.`3.0`, SourceVersion.`3.2`.series) // delete when we introduce `3.2-migration` + assertEquals(SourceVersion.`future`, SourceVersion.`future-migration`.series) + assertEquals(SourceVersion.`future`, SourceVersion.`future`.series) + + @Test def `isAtLeast 3.0`: Unit = + // trues + assertTrue(SourceVersion.`3.2`.isAtLeast(SourceVersion.`3.2`)) // delete when we introduce `3.2-migration` + assertTrue(SourceVersion.`3.2`.isAtLeast(SourceVersion.`3.1`)) // delete when we introduce `3.2-migration` + assertTrue(SourceVersion.`3.2`.isAtLeast(SourceVersion.`3.0`)) // delete when we introduce `3.2-migration` + assertTrue(SourceVersion.`3.2`.isAtLeast(SourceVersion.`3.0-migration`)) // delete when we introduce `3.2-migration` + assertTrue(SourceVersion.`3.1`.isAtLeast(SourceVersion.`3.2`)) // delete when we introduce `3.2-migration` + assertTrue(SourceVersion.`3.1`.isAtLeast(SourceVersion.`3.1`)) + assertTrue(SourceVersion.`3.1`.isAtLeast(SourceVersion.`3.0`)) + assertTrue(SourceVersion.`3.1`.isAtLeast(SourceVersion.`3.0-migration`)) + assertTrue(SourceVersion.`3.0`.isAtLeast(SourceVersion.`3.2`)) // delete when we introduce `3.2-migration` + assertTrue(SourceVersion.`3.0`.isAtLeast(SourceVersion.`3.1`)) + assertTrue(SourceVersion.`3.0`.isAtLeast(SourceVersion.`3.0`)) + assertTrue(SourceVersion.`3.0`.isAtLeast(SourceVersion.`3.0-migration`)) + assertTrue(SourceVersion.`3.0-migration`.isAtLeast(SourceVersion.`3.2`)) // delete when we introduce `3.2-migration` + assertTrue(SourceVersion.`3.0-migration`.isAtLeast(SourceVersion.`3.1`)) + assertTrue(SourceVersion.`3.0-migration`.isAtLeast(SourceVersion.`3.0`)) + assertTrue(SourceVersion.`3.0-migration`.isAtLeast(SourceVersion.`3.0-migration`)) + + + // falses + assertFalse(SourceVersion.`3.0-migration`.isAtLeast(SourceVersion.`future`)) + assertFalse(SourceVersion.`3.0-migration`.isAtLeast(SourceVersion.`future-migration`)) + // assertFalse(SourceVersion.`3.0-migration`.isAtLeast(SourceVersion.`3.2`)) // uncomment when we introduce `3.2-migration` + // assertFalse(SourceVersion.`3.0-migration`.isAtLeast(SourceVersion.`3.2-migration`)) // uncomment when we introduce `3.2-migration` + assertFalse(SourceVersion.`3.0`.isAtLeast(SourceVersion.`future`)) + assertFalse(SourceVersion.`3.0`.isAtLeast(SourceVersion.`future-migration`)) + // assertFalse(SourceVersion.`3.0`.isAtLeast(SourceVersion.`3.2`)) // uncomment when we introduce `3.2-migration` + // assertFalse(SourceVersion.`3.0`.isAtLeast(SourceVersion.`3.2-migration`)) // uncomment when we introduce `3.2-migration` + assertFalse(SourceVersion.`3.1`.isAtLeast(SourceVersion.`future`)) + assertFalse(SourceVersion.`3.1`.isAtLeast(SourceVersion.`future-migration`)) + // assertFalse(SourceVersion.`3.1`.isAtLeast(SourceVersion.`3.2`)) // uncomment when we introduce `3.2-migration` + // assertFalse(SourceVersion.`3.1`.isAtLeast(SourceVersion.`3.2-migration`)) // uncomment when we introduce `3.2-migration + assertFalse(SourceVersion.`3.2`.isAtLeast(SourceVersion.`future`)) // delete when we introduce `3.2-migration` + assertFalse(SourceVersion.`3.2`.isAtLeast(SourceVersion.`future-migration`)) // delete when we introduce `3.2-migration` + + // @Test def `isAtLeast 3.2`: Unit = // uncomment when we introduce `3.2-migration` + // trues + // assertTrue(SourceVersion.`3.2`.isAtLeast(SourceVersion.`3.2`)) // uncomment when we introduce `3.2-migration` + // assertTrue(SourceVersion.`3.2`.isAtLeast(SourceVersion.`3.2-migration`)) // uncomment when we introduce `3.2-migration` + // assertTrue(SourceVersion.`3.2`.isAtLeast(SourceVersion.`3.1`)) // uncomment when we introduce `3.2-migration` + // assertTrue(SourceVersion.`3.2`.isAtLeast(SourceVersion.`3.0`)) // uncomment when we introduce `3.2-migration` + // assertTrue(SourceVersion.`3.2`.isAtLeast(SourceVersion.`3.0-migration`)) // uncomment when we introduce `3.2-migration` + // assertTrue(SourceVersion.`3.2-migration`.isAtLeast(SourceVersion.`3.2`)) // uncomment when we introduce `3.2-migration` + // assertTrue(SourceVersion.`3.2-migration`.isAtLeast(SourceVersion.`3.2-migration`)) // uncomment when we introduce `3.2-migration` + // assertTrue(SourceVersion.`3.2-migration`.isAtLeast(SourceVersion.`3.1`)) // uncomment when we introduce `3.2-migration` + // assertTrue(SourceVersion.`3.2-migration`.isAtLeast(SourceVersion.`3.0`)) // uncomment when we introduce `3.2-migration` + // assertTrue(SourceVersion.`3.2-migration`.isAtLeast(SourceVersion.`3.0-migration`)) // uncomment when we introduce `3.2-migration` + // falses + // assertFalse(SourceVersion.`3.2-migration`.isAtLeast(SourceVersion.`future`)) // uncomment when we introduce `3.2-migration` + // assertFalse(SourceVersion.`3.2-migration`.isAtLeast(SourceVersion.`future-migration`)) // uncomment when we introduce `3.2-migration` + // assertFalse(SourceVersion.`3.2`.isAtLeast(SourceVersion.`future`)) // uncomment when we introduce `3.2-migration` + // assertFalse(SourceVersion.`3.2`.isAtLeast(SourceVersion.`future-migration`)) // uncomment when we introduce `3.2-migration` + + @Test def `isAtLeast future`: Unit = + // trues + assertTrue(SourceVersion.`future`.isAtLeast(SourceVersion.`future`)) + assertTrue(SourceVersion.`future`.isAtLeast(SourceVersion.`future-migration`)) + assertTrue(SourceVersion.`future`.isAtLeast(SourceVersion.`3.2`)) + // assertTrue(SourceVersion.`future`.isAtLeast(SourceVersion.`3.2-migration`)) + assertTrue(SourceVersion.`future`.isAtLeast(SourceVersion.`3.1`)) + assertTrue(SourceVersion.`future`.isAtLeast(SourceVersion.`3.0`)) + assertTrue(SourceVersion.`future`.isAtLeast(SourceVersion.`3.0-migration`)) + assertTrue(SourceVersion.`future-migration`.isAtLeast(SourceVersion.`future`)) + assertTrue(SourceVersion.`future-migration`.isAtLeast(SourceVersion.`future-migration`)) + assertTrue(SourceVersion.`future-migration`.isAtLeast(SourceVersion.`3.2`)) + // assertTrue(SourceVersion.`future-migration`.isAtLeast(SourceVersion.`3.2-migration`)) + assertTrue(SourceVersion.`future-migration`.isAtLeast(SourceVersion.`3.1`)) + assertTrue(SourceVersion.`future-migration`.isAtLeast(SourceVersion.`3.0`)) + assertTrue(SourceVersion.`future-migration`.isAtLeast(SourceVersion.`3.0-migration`)) + // no falses + +object SourceVersionTest: + + def importLanguageDot(feature: String): SourceVersion = + SourceVersion.lookupSourceVersion.fromSetting(feature)