Scala 3.7.0 released!

Wojciech Mazur, VirtusLab

RELEASE

Scala 3.7

We’re excited to announce the release of Scala 3.7.0 — a significant minor release that brings stabilized SIPs, powerful new features, expanded platform support, and notable improvements to tooling and compatibility.

What’s new in Scala 3.7?

Stabilised SIPs

The Scala Improvement Process (SIP) is how language changes and additions are made.

SIP-58: Named Tuples

Named Tuples, introduced as experimental in Scala 3.5, are now a stable feature. Named tuples are a convenient lightweight way to return multiple results from a function or to model the data using tuples while allowing you to use meaningful names for its fields.

@main def meaningfullReturnTypes =
  extension [T](seq: Seq[T])
    inline def partitionBy(predicate: PartialFunction[T, Boolean]): (matching: Seq[T], unmatched: Seq[T]) =
      seq.partition(predicate.unapply(_).getOrElse(false))

  List(
    (x = 1, y = 0),
    (x = 2, y = 3),
  ).partitionBy:
    case (x = x, y = 0) => x < 5
  .matching.map(_.x)
  .foreach(println)
  
  val measurements = List[(location: String, value: Double)](
    ("Lausanne",  8.05),
    ("London",      21),
    ("Rome",      37.0),
  )
  val average = measurements.map(_.value).sum / measurements.size

What’s more, this feature also enhances existing features. For example, it is now possible to match on a subset of case class fields by name. By doing so you no longer need to specify a long list of wildcard selectors for each field of a large case class.

@main def selectiveClassExtractors = 
  case class Order(
    id: String,
    createdAt: OffsetDateTime,
    shippedAt: Option[OffsetDateTime] = None,
    deliveredAt: Option[OffsetDateTime] = None,
    itemIds: List[String] = Nil
  )
  
  val orders = List.empty[Order]
  val selected = orders.collect:
    case Order(id = s"special-${internalId}", shippedAt = None) => internalId
    case Order(id = orderId, itemIds = items) if items.size > 5 => orderId

Last, but not least, named tuples are opening a new technique in metaprogramming by letting you compute structural types without the need for macros! The Selectable trait now has a Fields type member that can be instantiated to a named tuple.

class QueryResult[T](rawValues: Map[String, Any]) extends Selectable:
  type Fields = NamedTuple.Map[NamedTuple.From[T], Option]
  def selectDynamic(fieldName: String) = rawValues.get(fieldName)
  
case class Release(version: String, issues: List[String])

@main def Test =
  val query: QueryResult[Release] = QueryResult(Map("version" -> "3.7.0"))
  val version: Option[String] = query.version
  assert(query.version.contains("3.7.0"))
  assert(query.issues.isEmpty)

SIP-52: Binary APIs

For library maintainers, Scala 3.7.0 stabilizes the @publicInBinary annotation, introduced experimentally in Scala 3.4. This annotation ensures binary compatibility when inline methods access non-public members and prevents generation of redundant accessors required by inlining mechanism.

Inline methods are always inlined at their call sites. If they refer to members not visible outside their defining scope, the compiler generates accessor methods. The accessors are not subject to binary compatibility; they might be emitted differently by newer versions of compiler or eventually removed. The @publicInBinary annotation addresses this by emitting those members as public in bytecode, while maintaining restricted source-level visibility. This allows the compiler to refer to the original non-private member without the need to create additional accessors.

object api:
  class Service:
    private[api] def implementation(): Unit = println("Executing old API")
    inline def use() = implementation()
    // generated by compiler, accessed in `use()`
    // def api$Service$$inline$implementation(): Unit = implementation()
  
  class Consumer:
    inline def consume() = api.Service().implementation()
    // generated by compiler, accessed in `consume()`
    // def inline$implementation$i1(x$0: api$Service) = x$0.implementation()
    
    @scala.annotation.publicInBinary
    private[api] var isActive: Boolean = false
    inline def isStale = !isActive 

The new annotation also resolves a long-standing issue with the inability to invoke a private constructor of a class from inlined methods. Now by annotating the constructor with @publicInBinary you are allowed to access it directly from inlined methods.

import scala.annotation.publicInBinary

class Printer @publicInBinary private(id: Int)
object Printer:
  inline def create(): Printer = new Printer(42) // Not allowed previously

SIP-52 has also introduced the linting flag -WunstableInlineAccessors which detects and emits warnings for all unstable accessors generated for inline methods. This flag has been available since Scala 3.4.0. We highly recommend its usage of users’ codebases, especially in the case of public libraries.

Preview features

Scala 3.7 introduces the concept of preview features — fully implemented and SIP-approved, but potentially subject to small refinements before becoming stable. New Scala language features or standard library APIs are initially introduced as experimental, but once they become fully implemented and accepted by the SIP these can become a “preview” feature.

Preview language features and APIs are guaranteed to be standardized in some future Scala minor release, but allow the compiler team to introduce small, possibly binary incompatible, changes based on community feedback. These can be used by early adopters who can accept the possibility of binary compatibility breakage. For instance, preview features could be used in an application or internal tool. On the other hand, preview features are discouraged in publicly available libraries.

All preview features can be enabled together using the new -preview compiler option. More information about preview features can be found in preview definitions guide

SIP-62: For comprehension improvements

The first preview feature is SIP-62 - Better Fors, originally added to Scala 3.6 as experimental. A major user-facing improvement introduced by this SIP is the ability to start a for-comprehension block with assignment (for example, a = 1). It also introduces changes to how some for comprehensions are desugared by the compiler, avoiding redundant calls, for better code.

//> using options -preview
//> using scala 3.7
@main def betterFors = 
  for
    a = 1
    b <- Some(2)
    c = a * b
  yield c

The code above previously required a to be defined outside the for-comprehension block. Additionally, it was introducing redundant allocations and method calls to correctly handle possible if guards. In result it would have desugared to following code:

  Some(2)
  .map { b => 
    val c = a * b
    (b, c)
  }.map { case (b, c) => c }

With SIP-52 the same snippet would be desugared to simpler and more efficent code:

  Some(2).map { b =>
    val c = a * b
    c
  }

We encourage users to try out this feature which is likely going to be stabilised in Scala 3.8.

New experimental features

SIP-61: Unroll Default Arguments for Binary Compatibility

Some library maintainers have long avoided case classes in public APIs, since any change to their fields affected binary compatibility. This was typically observed whenever a new field was added to the case class primary constructor, even if it was defined with a default value to resolve source compatibility problems. Though such binary incompatibilities were detected by MiMa, avoiding them required manually creating shim methods to match the old signatures.

The @unroll annotation will eliminate this long-standing issue. This annotation lets you add additional parameters to method defs, class constructors, or case classes, without breaking binary compatibility. @unroll works by generating unrolled or “telescoping” forwarders, ensuring your code will remain binary compatible as new default parameters and fields are added over time.

//> using scala 3.7
//> using options -experimental

import scala.annotation.unroll

case class Product(
    name: String,
    price: BigDecimal,
    @unroll description: Option[String] = None,
){
  // generated by compiler:
  // def this(name: String, price: BigDecimal) = this(name, price, default$1)
  // def copy(name: String, price: BigDecimal) = this(name, price)
}

object Factory:
  def processOrder(
      products: List[Product],
      @unroll quantity: Int = 1,
      @unroll discount: Int = 0
  ): Unit = ???

  // generated by the compiler:
  // def processOrder(products: List[Product]): Unit = processOrder(products, default$1, default$2)
  // def processOrder(products: List[Product], quantity: Int): Unit = processOrder(products, quantity, default$2)

More details can be found in @unroll reference

SIP-68: Reference-able Package Objects

One limitation with package objects is that we cannot directly refer to them as values, even though that is possible for normal objects. Such references can already be made with a special “.package” accessor, but this is ugly and non-obvious. Or one could use a normal object, which is not always possible.

The SIP drops this limitation, allowing such direct references.

//> using scala 3.7
//> using options -experimental

package my.pkg
import scala.language.experimental.packageObjectValues

package object releases:
  val `3.7` = "3.7.0" 
  
@main def referencablePkgObject = 
  val versions = my.pkg.releases
  val oldStyle = my.pkg.releases.`package`
  println(versions eq oldStyle) // prints true

For more details visit the reference page for this feature.

Other notable changes

New scheme for given prioritization

In the Scala 3.5.0 release notes we announced upcoming changes to givens, to improve their prioritization. Under the old rules, the compiler always tried to select the instance with the most specific subtype of the requested type. Under the new rules, it always selects the instance with the most general subtype that satisfies the context bound. Starting with Scala 3.7, the compiler uses the new behavior by default. Running the compiler with -source:3.5 will allow you to temporarily keep using the old rules. Under -source:3.7-migration, code whose behaviour differs between new and old rules (ambiguity on new, passing on old, or vice versa) will emit warnings, but the new rules will still be applied.

For detailed motivation of the changes, with examples of code that will be easier to write and understand, see our 2024 blog post - Upcoming Changes to Givens in Scala 3.7.

Expression Compiler is now part of Scala 3 compiler (#22597)

The expression compiler powers the debug console in Metals and the IntelliJ Scala plugin, enabling the evaluation of arbitrary Scala expressions at runtime (even macros). The expression compiler produces class files that can be loaded by tooling to compute the evaluation output. Previously expression compiler was developed independently from the Scala compiler inside scalacenter/scala-debug-adapter repository and was cross-published for every Scala release. Starting with Scala 3.7 the expression compiler has been migrated to the main scala/scala3 repository and become an integral part of Scala toolchain. This change allows users to use the expression compiler with nightly versions of the compiler as well, easing the maintenance and release process for the compiler team.

Presentation Compiler: Show inferred type on holes in hover (#21423)

The presentation compiler is a special mode of the Scala compiler that runs interactively and is used by IDEs and language servers such as Metals. It provides immediate feedback about code correctness, type checking, symbol resolution, autocompletion, error highlighting, and other editing support functionalities. Some of the latest improvements to the presentation compiler focus on the ability to infer more information about expected types of expressions provided by the users. As a result, presentation compiler can now show the users the expected type of expression when hovering over so called type holes.

def someMethod(count: Int, label: String): Unit = ???
someMethod(???, ???)

Given the code snippet above, hovering on either placeholder will hint about the expected type (Int and String, respectively).

Quotes API changes

Scala 3.7 introduces changes to the experimental subset of Quotes API. One notable addition is the apply methods to import selectors (#22457), allowing to dynamically construct import statements within macros. This feature can be especially useful when summoning implicit instances in the generated code, as it allows to add additional instances to the search​.

Another advancement is the experimental summonIgnoring method (#22417). It allows developers to summon implicit instances while excluding specific instances of givens from the search. It’s particularly useful in complex derivation scenarios where certain implicit should be disregarded to prevent conflicts or unintended resolutions.​ It might be used to provide a custom implementation of macro only in cases where a user-defined variant of implicit is available, while avoiding recursively re-calling the macro method.

//> using options -experimental

import scala.quoted.*

trait ExcludedType
trait Show[T]:
  def show(): Unit

object Show {
  transparent inline given auto[T]: Show[T] = ${ autoImpl[T] }

  private def autoImpl[T: Type](using Quotes): Expr[Show[T]] =
    import quotes.reflect.*
    Expr.summonIgnoring[Show[ExcludedType]](
      // Ignore implicits produced by these symbols:
      Symbol.classSymbol("Show").companionModule.methodMember("auto")*
    ) match
      case Some(instance) =>
        // User provided their own instance of `Show[ExcludedType]` - use it 
        '{
          new Show[T]:
            def show(): Unit =
              println(s"Show[${${ Expr(TypeRepr.of[T].show) }}] including user defined Show[ExcludedType]:")
              $instance.show()
        }
      case None =>
        // No user-defined implicit for `Show[ExcludedType]` beside `Show.auto`
        '{
          new Show[T]:
            def show(): Unit =
              println(s"Show[${${ Expr(TypeRepr.of[T].show) }}]")
        }
}

Furthermore, the experimental Symbol.newClass method has been extended to support class parameters, flags, privateWithin, and annotations (#21880). This enhancement enables the dynamic creation of classes with constructors, access modifiers, and annotations, providing greater control over generated code structures. With those changes included, we are planning to stabilize this method (and ClassDef.apply) soon.

Improvements to -Wunused and -Wconf (#20894)

Scala 3.7 introduces a significant enhancement to its linting mechanism by revamping the CheckUnused compiler phase. Previously, developers encountered false-positive warnings when importing givens, particularly when they were defined in shared traits across multiple objects. This issue often led to unnecessary code restructuring or the disabling of linting checks.​

The updated implementation leverages the MiniPhase API, aligning the linting process more closely with the compiler’s contextual understanding. This change ensures that the compiler accurately identifies genuinely unused imports, thereby reducing misleading warnings. Additionally, the update introduces support for -Wunused:patvars, enabling warnings for unused pattern variables. Changes also enhance the -Wconf option to allow origin-based filtering -Wconf:origin=full.path.selector as in Scala 2. That allows, for example, to exclude certain blessed imports from warnings, or to work around false positives:

-Wconf:origin=scala.util.chaining.given:s

These improvements collectively provide developers with more precise and configurable linting tools, enhancing code quality and maintainability.​

Implicit parameters now warn at call site without using keyword (#22441)

As part of Scala’s ongoing migration from implicit to the newer, clearer given and using, Scala 3.7 introduces a change regarding method calls involving implicit parameters. Now, explicitly providing arguments to methods defined with implicit warns if the call site doesn’t say using. This adjustment reduces ambiguity inherent in Scala 2 syntax, where it was unclear whether an explicitly provided argument was intended for an implicit parameter or for the apply method of the resulting object.

trait Config
object Config:
  def default: Config = ???
  
def implicitBased(implicit config: Config) = ???
def givenBased(using Config) = ???

@main def explicitImplicits = 
  implicitBased(Config.default)       // warning: Implicit parameters should be provided with a `using` clause.
  implicitBased(using Config.default) // recommended
  
  givenBased(Config.default)       // error: Would not compile without `using``
  givenBased(using Config.default)

Scala 3.7 provides an automated migration path for existing codebases through the compiler flags -rewrite -source:3.7-migration, which automatically inserts the recommended using keywords, streamlining the transition to the new syntax. Users of Scala 2 can introduce the using syntax at callsite into their codebase as well. The implicit based example above would successfully compile when using at least Scala 2.12.17 or Scala 2.13.9 without any additional flags.

Scala 3 unblocked on Android (#22632)

Scala 3.7 brings a crucial fix that enhances its compatibility with the Android platform. Previously, developers faced issues when compiling Scala 3 code for Android due to the Android Runtime (ART) enforcing stricter type constraints on lambdas than the standard Java Virtual Machine (JVM). Specifically, ART requires that the return type of a Single Abstract Method (SAM) interface be a primitive type or explicitly boxed, a condition not mandated by the JVM.​

The update addresses this by modifying the Scala compiler to box the return type of native instantiated methods when the SAM’s return type isn’t primitive. This change ensures that lambda expressions conform to ART’s expectations, preventing runtime errors on Android devices. By aligning the compiler’s behaviour with ART’s requirements, the Scala 3 development for the Android platform should be unblocked, although it might require recompiling existing libraries using Scala 3.7 or the upcoming Scala 3.3 LTS version containing a backported fix.

Support for dependent case classes (#21698)

Case classes may now have dependent fields, improving expressiveness and type safety. This allows fields within a case class to depend on other constructor parameters via path-dependent types. One use case is encoding a configuration system where each setting has a distinct value type determined by the setting itself.

case class ConfigEntry(option: Setting, default: option.Value):
  def value(using Context): option.Value = option.userDefined.getOrElse(default)

trait Context
trait Setting(val name: String):
  type Value
  def userDefined(using Context): Option[Value] = None

class StringSetting(name: String) extends Setting(name):
  override type Value = String

class IntSetting(name: String) extends Setting(name):
  override type Value = Int


@main def DependentCaseClasses =
  given Context = new {}
  val settings = List[ConfigEntry](
    ConfigEntry(StringSetting("docs.page"), "https://docs.scala-lang.org/"),
    ConfigEntry(IntSetting("max.threads"), 32)
  )

This enhancement simplifies type-safe heterogeneous collections and makes it easier to express advanced design patterns previously requiring workarounds or more verbose type definitions.

Be aware of some pattern matching limitations for dependent case classes - the types of the extracted components are currently approximated to their non-dependent upper-bound:

trait A:  
  type B <: AnyRef  

case class CC(a: A, b: a.B)  
object Test:  
  def foo(cc: CC) = cc match  
    case CC(a, b) =>  
      // `a` has type `A`  
      // `b` has type `AnyRef` instead of the more precise `a.B`  

REPL: bring dependencies in with :jar (#22343)

It is now possible to bring new dependencies to an already running REPL session with the :jar command. It is effectively the equivalent of Scala 2’s :require.

For example, for a given simple example:

// example.scala
object Message:
  val hello = "Hello"

After packaging it into a JAR:

scala package example.scala --power --library  
# Compiling project (Scala 3.7.0, JVM (24))  
# Compiled project (Scala 3.7.0, JVM (24))  
# Wrote example.jar  

It’s possible to bring it into a running REPL session like this:

Welcome to Scala 3.7.0(23.0.1, Java OpenJDK 64-Bit Server VM).  
Type in expressions for evaluation. Or try :help.  

scala> :jar example.jar  
Added 'example.jar' to classpath.  

scala> Message.hello  
val res0: String = Hello  

Dependency updates

Scala 2 Standard Library

Scala 3 now uses the Scala 2.13.16 standard library. Be mindful of these breaking changes and deprecations since 2.13.15:

  • On the empty string, .tail and .init now throw (instead of returning the empty string) #10851
  • Do not use rangeHash when rangeDiff is 0 #10912
  • Deprecate collection.mutable.AnyRefMap #10862

Scala.js

Scala.js has been updated from version 1.16.0 to 1.19.0. Libraries published using Scala 3.7 must use Scala.js 1.19.0 or later. Please refer to Scala.js changelogs for more details:

Scala CLI

The Scala runner has been updated to Scala CLI 1.7.1.

Notable changes include:

  • The fmt sub-command now uses scalafmt binaries built with Scala Native. This change can improve the performance of formatting Scala sources.
  • (⚡️ experimental) Support for running scalafix rules has been added to the fix sub-command.

Please refer to Scala CLI changelogs for more details:

What’s next?

With the final release of Scala 3.7.0, the first patch version 3.7.1-RC1 has already been made available. This release includes additional fixes and improvements introduced after the branch cutoff.

Looking ahead, you can expect at least two more patch releases before Scala 3.8, which is scheduled for September 2025. The goal for Scala 3.8 is to be the last minor version before the next Long-Term Support (LTS) release, Scala 3.9, which is planned for early 2026.

The Scala compiler team aims to finalize all major changes in 3.8, followed by a soft feature freeze and a stabilization period. This ensures a smooth transition for users and maximizes the stability of the compiler and tooling ecosystem ahead of the LTS release.

Contributors

Thank you to all the contributors who made this release possible 🎉

According to git shortlog -sn --no-merges 3.6.4..3.7.0 these are:

  63  Martin Odersky
  47  Som Snytt
  33  Adrien Piquerez
  32  Hamza Remmal
  29  Wojciech Mazur
  19  aherlihy
  19  kasiaMarek
  16  Jan Chyb
  13  Dale Wijnand
  11  Kacper Korban
  10  EnzeXing
   9  Sébastien Doeraene
   7  Guillaume Martres
   7  Matt Bovel
   7  Oliver Bračevac
   7  noti0na1
   5  HarrisL2
   5  Jamie Thompson
   5  dependabot[bot]
   4  Joel Wilsson
   4  Piotr Chabelski
   4  Seth Tisue
   3  Roman Janusz
   3  anna herlihy
   2  David Hua
   2  Tomasz Godzik
   2  Yichen Xu
   1  Alec Theriault
   1  Daisy Li
   1  Daniel Thoma
   1  Dmitrii Naumenko
   1  Felix Herrmann
   1  He-Pin(kerr)
   1  João Ferreira
   1  Jędrzej Rochala
   1  Katarzyna Marek
   1  Kenji Yoshida
   1  Natsu Kagami
   1  Niklas Fiekas
   1  Rocco Mathijn Andela
   1  Vadim Chelyshov
   1  adpi2
   1  fan-tom
   1  philwalk
   1  rochala

For a full list of changes and contributor credits, please refer to the release notes.