Skip to content

Backport build changes and TASTy version tests from main #185

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 19, 2025
147 changes: 118 additions & 29 deletions project/Build.scala
Original file line number Diff line number Diff line change
@@ -81,12 +81,74 @@ object DottyJSPlugin extends AutoPlugin {
object Build {
import ScaladocConfigs._

/** Version of the Scala compiler used to build the artifacts.
* Reference version should track the latest version pushed to Maven:
* - In main branch it should be the last RC version
* - In release branch it should be the last stable release
*
* Warning: Change of this variable needs to be consulted with `expectedTastyVersion`
*/
val referenceVersion = "3.3.5"

val baseVersion = "3.3.6-RC1"
/** Version of the Scala compiler targeted in the current release cycle
* Contains a version without RC/SNAPSHOT/NIGHTLY specific suffixes
* Should be updated ONLY after release or cutoff for previous release cycle.
*
* Should only be referred from `dottyVersion` or settings/tasks requiring simplified version string,
* eg. `compatMode` or Windows native distribution version.
*
* Warning: Change of this variable might require updating `expectedTastyVersion`
*/
val developedVersion = "3.3.6"

/** The version of the compiler including the RC prefix.
* Defined as common base before calculating environment specific suffixes in `dottyVersion`
*
* By default, during development cycle defined as `${developedVersion}-RC1`;
* During release candidate cycle incremented by the release officer before publishing a subsequent RC version;
* During final, stable release is set exactly to `developedVersion`.
*/
val baseVersion = s"$developedVersion-RC1"

/** The version of TASTY that should be emitted, checked in runtime test
* For defails on how TASTY version should be set see related discussions:
* - https://github.com/scala/scala3/issues/13447#issuecomment-912447107
* - https://github.com/scala/scala3/issues/14306#issuecomment-1069333516
* - https://github.com/scala/scala3/pull/19321
*
* Simplified rules, given 3.$minor.$patch = $developedVersion
* - Major version is always 28
* - TASTY minor version:
* - in main (NIGHTLY): {if $patch == 0 || ${referenceVersion.matches(raw"3.$minor.0-RC\d")} then $minor else ${minor + 1}}
* - in LTS branch (NIGHTLY): always equal to $minor
* - in release branch is always equal to $minor
* - TASTY experimental version:
* - in main (NIGHTLY) is always experimental
* - in LTS branch (NIGHTLY) is always non-experimental
* - in release candidate branch is experimental if {patch == 0}
* - in stable release is always non-experimetnal
*/
val expectedTastyVersion = "28.3"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we were once discussing that TASTy version of all nightlies, including LTS nightlies, should be experimental. However it's not so obvious which version of TASTy format should be used in such case.

In Scala Next that would be 28.{minorVersion + 1}-experimental-1 (assuming patch > 0), however such schema cannot be used for LTS - TASTy 28.4 already exists and is stable.

We can potentially use 28.3-experimental-2 but it also seems to not make sense becouse 28.3 is already stabilised.

In such case I think it's best to assume that LTS always emits stable TASTy. It should be fine, becouse every backported change is carefully checked and tested in both Scala CI and OpenCB before merging which should detect potential issues

Any opinions @prolativ @tgodzik

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm quite tempted to say that from TASTy's perspective we should treat LTS as any other minor version series. In theory we could also have nightly versions and backport PRs to 3.4, 3.5, etc. but we don't do that just because we don't have enough resources to manage that

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm quite tempted to say that from TASTy's perspective we should treat LTS as any other minor version series.

Totally agree
The main problem here comes from versioning, especially because we do actually release LTS nightlies. However, the versioning schema of TASTy seems to not be compatible with our needs.
With version 28.3-experimental-2 we'd get into issue in non-bootstrapped tests - these require that reference version of compiler has version in range 28.0 - 28.2 or exactly the same version experimental TASTy 28.3-experimental-2. We use 3.3.5 producing Tasty 28.3 which would not fit the required version bounds.

Unless you're suggesting to use 28.4-experimental-1 which would make the non-bootstrapped tests happy. Is it what you're refering to?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can potentially use 28.3-experimental-2 but it also seems to not make sense becouse 28.3 is already stabilised.

But what versions do we use normally? If we have 28.7 tasty currently being release, what will be the Scala 3 nightlies have as tasty version? 3.6.2 has 28.7 and 3.6.3 has 28.7, what did the nightlies between have as tasty version?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scala 3.3 LTS uses 28.3 for both RC and nightlies
Scala 3.4.0 uses 28.4 for stable and 28.4-experimental-1 for RC and nightly
Scala 3.4.1 uses 28.4 for RC/stable and 28.5-experimental-1 for nightlies

I think we can skip potential change to TASTy LTS versions for the future. Currently we'll continue to use existing schema - always stable TASTy

checkReleasedTastyVersion()

/** Final version of Scala compiler, controlled by environment variables. */
val dottyVersion = {
if (isRelease) baseVersion
else if (isNightly) s"${baseVersion}-bin-${VersionUtil.commitDate}-${VersionUtil.gitHash}-NIGHTLY"
else s"${baseVersion}-bin-SNAPSHOT"
}
def isRelease = sys.env.get("RELEASEBUILD").contains("yes")
def isNightly = sys.env.get("NIGHTLYBUILD").contains("yes")

/** Version calculate for `nonbootstrapped` projects */
val dottyNonBootstrappedVersion = {
// Make sure sbt always computes the scalaBinaryVersion correctly
val bin = if (!dottyVersion.contains("-bin")) "-bin" else ""
dottyVersion + bin + "-nonbootstrapped"
}

// LTS or Next
val versionLine = "LTS"
final val versionLine = "LTS"

// Versions used by the vscode extension to create a new project
// This should be the latest published releases.
@@ -95,23 +157,22 @@ object Build {
val publishedDottyVersion = referenceVersion
val sbtDottyVersion = "0.5.5"

/** Version against which we check binary compatibility.
/** LTS version against which we check binary compatibility.
*
* This must be the latest published release in the same versioning line.
* For example, if the next version is going to be 3.1.4, then this must be
* set to 3.1.3. If it is going to be 3.1.0, it must be set to the latest
* 3.0.x release.
* This must be the earliest published release in the LTS versioning line.
* For example, if the latest LTS release is be 3.3.4, then this must be
* set to 3.3.0.
*/
val previousDottyVersion = "3.3.5"
val mimaPreviousLTSDottyVersion = "3.3.0"

object CompatMode {
final val BinaryCompatible = 0
final val SourceAndBinaryCompatible = 1
}

val compatMode = {
val VersionRE = """^\d+\.(\d+).(\d+).*""".r
baseVersion match {
val VersionRE = """^\d+\.(\d+)\.(\d+)""".r
developedVersion match {
case VersionRE(_, "0") => CompatMode.BinaryCompatible
case _ => CompatMode.SourceAndBinaryCompatible
}
@@ -132,24 +193,6 @@ object Build {
val dottyGithubUrl = "https://github.com/scala/scala3"
val dottyGithubRawUserContentUrl = "https://raw.githubusercontent.com/scala/scala3"


val isRelease = sys.env.get("RELEASEBUILD") == Some("yes")

val dottyVersion = {
def isNightly = sys.env.get("NIGHTLYBUILD") == Some("yes")
if (isRelease)
baseVersion
else if (isNightly)
baseVersion + "-bin-" + VersionUtil.commitDate + "-" + VersionUtil.gitHash + "-NIGHTLY"
else
baseVersion + "-bin-SNAPSHOT"
}
val dottyNonBootstrappedVersion = {
// Make sure sbt always computes the scalaBinaryVersion correctly
val bin = if (!dottyVersion.contains("-bin")) "-bin" else ""
dottyVersion + bin + "-nonbootstrapped"
}

val sbtCommunityBuildVersion = "0.1.0-SNAPSHOT"

val agentOptions = List(
@@ -477,7 +520,7 @@ object Build {
case cv: Disabled => thisProjectID.name
case cv: Binary => s"${thisProjectID.name}_${cv.prefix}3${cv.suffix}"
}
(thisProjectID.organization % crossedName % previousDottyVersion)
(thisProjectID.organization % crossedName % mimaPreviousLTSDottyVersion)
},

mimaCheckDirection := (compatMode match {
@@ -2025,6 +2068,9 @@ object Build {
settings(disableDocSetting).
settings(
versionScheme := Some("semver-spec"),
Test / envVars ++= Map(
"EXPECTED_TASTY_VERSION" -> expectedTastyVersion,
),
if (mode == Bootstrapped) Def.settings(
commonMiMaSettings,
mimaBinaryIssueFilters ++= MiMaFilters.TastyCore,
@@ -2059,6 +2105,49 @@ object Build {
case Bootstrapped => commonBootstrappedSettings
})
}

/* Tests TASTy version invariants during NIGHLY, RC or Stable releases */
def checkReleasedTastyVersion(): Unit = {
case class ScalaVersion(minor: Int, patch: Int, isRC: Boolean)
def parseScalaVersion(version: String): ScalaVersion = version.split("\\.|-").take(4) match {
case Array("3", minor, patch) => ScalaVersion(minor.toInt, patch.toInt, false)
case Array("3", minor, patch, _) => ScalaVersion(minor.toInt, patch.toInt, true)
case other => sys.error(s"Invalid Scala base version string: $baseVersion")
}
lazy val version = parseScalaVersion(baseVersion)
lazy val referenceV = parseScalaVersion(referenceVersion)
lazy val (tastyMinor, tastyIsExperimental) = expectedTastyVersion.split("\\.|-").take(4) match {
case Array("28", minor) => (minor.toInt, false)
case Array("28", minor, "experimental", _) => (minor.toInt, true)
case other => sys.error(s"Invalid TASTy version string: $expectedTastyVersion")
}
val isLTS = versionLine == "LTS"

if(isNightly) {
assert(tastyIsExperimental || isLTS, "TASTY needs to be experimental in nightly builds")
val expectedTastyMinor = version.patch match {
case 0 => version.minor
case 1 if referenceV.patch == 0 && referenceV.isRC =>
// Special case for a period when reference version is a new unstable minor
// Needed for non_bootstrapped tests requiring either stable tasty or the same experimental version produced by both reference and bootstrapped compiler
assert(version.minor == referenceV.minor, "Expected reference and base version to use the same minor")
version.minor
case _ =>
if (isLTS) version.minor
else version.minor + 1
}
assert(tastyMinor == expectedTastyMinor, s"Invalid TASTy minor version, expected $expectedTastyMinor, got $tastyMinor")
}

if(isRelease) {
assert(version.minor == tastyMinor, "Minor versions of TASTY vesion and Scala version should match in release builds")
assert(!referenceV.isRC, "Stable release needs to use stable compiler version")
if (version.isRC && version.patch == 0)
assert(tastyIsExperimental, "TASTy should be experimental when releasing a new minor version RC")
else
assert(!tastyIsExperimental, "Stable version cannot use experimental TASTY")
}
}
}

object ScaladocConfigs {
3 changes: 3 additions & 0 deletions project/MiMaFilters.scala
Original file line number Diff line number Diff line change
@@ -16,6 +16,9 @@ object MiMaFilters {
// end of New experimental features in 3.3.X
)
val TastyCore: Seq[ProblemFilter] = Seq(
// Backported in 3.3.6
ProblemFilters.exclude[MissingClassProblem]("dotty.tools.tasty.TastyVersion"),
ProblemFilters.exclude[MissingClassProblem]("dotty.tools.tasty.TastyVersion$"),
)
val Interfaces: Seq[ProblemFilter] = Seq(
)
39 changes: 39 additions & 0 deletions tasty/src/dotty/tools/tasty/TastyVersion.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package dotty.tools.tasty

import scala.annotation.internal.sharable

case class TastyVersion private(major: Int, minor: Int, experimental: Int) {
def isExperimental: Boolean = experimental > 0

def nextStable: TastyVersion = copy(experimental = 0)

def minStable: TastyVersion = copy(minor = 0, experimental = 0)

def show: String = {
val suffix = if (isExperimental) s"-experimental-$experimental" else ""
s"$major.$minor$suffix"
}

def kind: String =
if (isExperimental) "experimental TASTy" else "TASTy"

def validRange: String = {
val min = TastyVersion(major, 0, 0)
val max = if (experimental == 0) this else TastyVersion(major, minor - 1, 0)
val extra = Option.when(experimental > 0)(this)
s"stable TASTy from ${min.show} to ${max.show}${extra.fold("")(e => s", or exactly ${e.show}")}"
}
}

object TastyVersion {

@sharable
private val cache: java.util.concurrent.ConcurrentHashMap[TastyVersion, TastyVersion] =
new java.util.concurrent.ConcurrentHashMap()

def apply(major: Int, minor: Int, experimental: Int): TastyVersion = {
val version = new TastyVersion(major, minor, experimental)
val cachedVersion = cache.putIfAbsent(version, version)
if (cachedVersion == null) version else cachedVersion
}
}
26 changes: 26 additions & 0 deletions tasty/test/dotty/tools/tasty/BuildTastyVersionTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dotty.tools.tasty

import org.junit.Assert._
import org.junit.Test

import TastyBuffer._

// Tests ensuring TASTY version emitted by compiler is matching expected TASTY version
class BuildTastyVersionTest {

val CurrentTastyVersion = TastyVersion(TastyFormat.MajorVersion, TastyFormat.MinorVersion, TastyFormat.ExperimentalVersion)

// Needs to be defined in build Test/envVars
val ExpectedTastyVersionEnvVar = "EXPECTED_TASTY_VERSION"

@Test def testBuildTastyVersion(): Unit = {
val expectedVersion = sys.env.get(ExpectedTastyVersionEnvVar)
.getOrElse(fail(s"Env variable $ExpectedTastyVersionEnvVar not defined"))
.match {
case s"$major.$minor-experimental-$experimental" => TastyVersion(major.toInt, minor.toInt, experimental.toInt)
case s"$major.$minor" if minor.forall(_.isDigit) => TastyVersion(major.toInt, minor.toInt, 0)
case other => fail(s"Invalid TASTY version string: $other")
}
assertEquals(expectedVersion, CurrentTastyVersion)
}
}