Upgrading Between Scala 3 Versions

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Scala 3.10 (3.10.0, released 2026-10-12) and the 3.9 LTS line (3.9.0, released 2026-09-03). Every program, flag and compiler message on it was run with Scala CLI 1.17.1 on JDK 26 (checked on 2026-10-15), compiling with 3.3.8, 3.7.4, 3.9.0 and 3.10.0 from Maven Central through the same Scala CLI; the places that were not run are listed at the end. The page answers one practical question: a project builds today on an older Scala 3 release, so what does it take to get it onto 3.9 LTS or 3.10? Which versions exist and which one to pick is covered in Getting Started & Versions, and what -source, TASTy and binary compatibility mean, with the consumer matrix, in Versions, Compatibility & JDKs; this page does not repeat them, it is the step-by-step recipe. Moving from Scala 2 is a different job, covered by Migrating from Scala 2 and Incompatibilities & Dropped Features.

The examples use the trailhead scenario: a small planner for mountain trails whose source still contains the constructs that Scala 3.4 started to deprecate.

The upgrade path

Scala 3 does not promise source compatibility between minor releases, so an upgrade is a sequence of small changes rather than one switch. The official guidance that came with 3.9 LTS is built on a single idea: let the compiler do the mechanical part. Scala 3 ships rewrites for most deprecated constructs, enabled one minor version at a time with -source:3.x-migration -rewrite, and about 1,780 of nearly 2,000 projects in the Open Community Build compiled on 3.9 with no or minimal changes. The picture below shows the usual route from the previous LTS to the new LTS and then to Scala Next.

flowchart TD A["Project on 3.3 LTS (3.3.x)"] --> B["1. Upgrade the build tool if needed"] B --> C["2. JDK 17 or later, same Scala version"] C --> D["3. Scala.js plugin if used"] D --> E["4. Bump Scala to 3.9.0 and run the ordered migration rewrites"] E --> F["5. Remove migration flags, run tests"] F --> G["6. Upgrade libraries"] G --> H{"Need Scala Next features?"} H -- "no" --> I["Stay on 3.9 LTS (recommended for libraries)"] H -- "yes" --> J["7. Bump to 3.10.0: fix the new errors, adapt macros"] J --> K["Optional: -preview features"]

The versions in between matter only for the rewrites: 3.4, 3.5, 3.6, 3.7, 3.8 and 3.9 are the migration steps, and the compiler that runs them is 3.9.0 (or 3.7.4, see below), not each intermediate release.

What changed on the way

Going from 3.3 LTS to 3.9 LTS crosses six minor releases. The table keeps only what affects an existing codebase; the full lists are in the release announcements.

Release Breaking or deprecating Worth knowing

3.4

Legacy syntax starts to warn: _ type wildcards, private[this], var x = _, A with B as a type, xs: _* and a trailing _ for eta expansion. A refutable pattern in a for generator needs case. Match types are specified (SIP-56), so some previously accepted ones are illegal. Type inference for inline methods changed.

Quote pattern type variables (SIP-53), multi-source extension overloads (SIP-54)

3.5

nothing that needs a rewrite

the scala command becomes the Scala CLI runner, best-effort compilation, binary literals

3.6

Context bounds desugar to using parameters. Java-defined annotations with several parameters need named arguments. Code whose meaning will change with the new given prioritization warns.

clause interleaving (SIP-47), new context-bound and given syntax (SIP-64)

3.7

Given search prefers the most general instance (-source:3.5 keeps the old rule). An explicit argument for an implicit parameter needs using. The standard library moves to 2.13.16.

named tuples (SIP-58), @publicInBinary

3.8

JDK 17 or later is required. The standard library is compiled by Scala 3, so its context bounds are given and explicit arguments need using. The REPL is its own artifact (scala3-repl). scala-reflect based runtime reflection on Scala 2.13 libraries can fail.

runtimeChecked (SIP-57), better fors (SIP-62), into as a preview, lazy vals on VarHandle

3.9

-source:3.9-migration rewrites backtick identifiers that contain $. A private companion given found by implicit search is reported under -deprecation.

into stable (SIP-71), the first release of the 3.9 LTS line

3.8.0 had a runtime regression that was fixed in 3.8.1, so never pick 3.8.0 as a stepping stone. The latest 3.8 release at the check date is 3.8.4.

A newer compiler still compiles against the libraries you already have, but a newer library may require a newer compiler. That asymmetry fixes the order, from the 3.9 migration guide. Let CI pass and commit after each step.

  1. Upgrade the build tool if the new Scala version needs it (the sbt, Mill and Scala CLI pages are sbt & Build Tools and Scala CLI).

  2. Move to JDK 17 or later while staying on the current Scala version where possible. The JDK floor is covered in Versions, Compatibility & JDKs; a Java side of the same job is in Java: Build & Tooling.

  3. Upgrade Scala.js if the project uses it (the Scala.js backend is part of the compiler, so its minor version is fixed by the Scala minor: 1.22 for the 3.9 LTS line).

  4. Upgrade Scala itself, including the compiler rewrites below. If you use stepping stones, commit each one.

  5. Upgrade libraries after the Scala bump, not before.

For many projects the whole migration is the JDK bump plus the Scala version bump. For a larger code base the guide prefers a sequence of small, reviewable, compiler-assisted changes to one big jump.

The ordered rewrite recipe

Scala 3 can patch your source files in place. Two options do it: -source:3.x-migration selects the language rules of version 3.x and turns the errors that version introduced into migration warnings, and -rewrite makes the compiler write the fix for every warning that has a mechanical one. The flags go in the build’s scalacOptions, or on the Scala CLI command line as --scalac-option.

Two rules from the guide are easy to miss:

  • Later flags do not replay earlier patches. -source:3.6-migration -rewrite does not apply the 3.4 rewrites, so run each flag in order, one compile per flag: 3.4, 3.5, 3.6, 3.7, 3.8, 3.9.

  • The code must still type-check for the rewrite to run. A type error stops the patching, so a rewrite can only fix constructs that the compiler reports as warnings.

The compiler that runs the rewrites matters too. Use 3.9.0 if the code base uses many with types, because 3.9 improved the rewrite of with to &; the same flags on 3.7.4 could emit code that does not compile. Use 3.7.4 first if the code base passes explicit arguments to standard library methods whose context bounds are now given (shown in the next section), because 3.7.4 is the last release before the library was compiled with Scala 3.

A legacy file, before and after

The program below was written for 3.3 and has one example of each legacy construct from the 3.4 row of the table.

package trailhead

trait Named:
  def name: String
trait Sized:
  def km: Double

class Planner:
  private[this] var total: Double = 0.0
  var cache: Map[String, Double] = _

  def label(x: Named with Sized): String = s"${x.name}: ${x.km}"
  def count(xs: List[_]): Int = xs.size
  def sum(xs: Int*): Int = xs.sum
  def total2(xs: List[Int]): Int = sum(xs: _*)
  def inc(n: Int): Int = n + 1
  val eta = inc _
  def firsts(pairs: List[Option[Int]]): List[Int] =
    for Some(n) <- pairs yield n

@main def legacyDemo(): Unit =
  val p = Planner()
  val h = new Named with Sized { val name = "Pico"; val km = 1.5 }
  println(p.label(h))
  println(p.count(List(1, "a")))
  println(p.total2(List(1, 2, 3)))
  println(p.eta(4))
  println(p.firsts(List(Some(1), None, Some(3))))

On 3.3.8 it compiles with one warning (for the refutable for pattern) and prints:

Pico: 1.5
2
6
5
List(1, 3)

On 3.9.0 the same file has six warnings and one error: the for Some(n) <- pairs generator, which 3.3 only warned about, is now an error ("pattern’s type Some[Int] is more specialized than the right hand side expression’s type Option[Int]"). Each warning ends with the sentence that tells you a rewrite exists, for example:

-- [E003] Syntax Warning: Legacy.scala:12:21
12 |  def label(x: Named with Sized): String = s"${x.name}: ${x.km}"
   |                     ^^^^
   |with as a type operator has been deprecated; use & instead
   |This construct can be rewritten automatically under -rewrite -source 3.4-migration.

On 3.10.0 the warnings stay, but with is now an error as well, so the file has two errors and five warnings.

The 3.4 step on 3.9.0 is one command:

scala compile Legacy.scala -S 3.9.0 --scalac-option -rewrite --scalac-option -source:3.4-migration

It prints [patched file .../Legacy.scala]. The changes it made to the file:

-  private[this] var total: Double = 0.0
-  var cache: Map[String, Double] = _
+  private var total: Double = 0.0
+  var cache: Map[String, Double] = scala.compiletime.uninitialized
 ...
-  def label(x: Named with Sized): String = s"${x.name}: ${x.km}"
-  def count(xs: List[_]): Int = xs.size
+  def label(x: Named & Sized): String = s"${x.name}: ${x.km}"
+  def count(xs: List[?]): Int = xs.size
 ...
-  def total2(xs: List[Int]): Int = sum(xs: _*)
+  def total2(xs: List[Int]): Int = sum(xs*)
 ...
-  val eta = inc _
+  val eta = inc
 ...
-    for Some(n) <- pairs yield n
+    for case Some(n) <- pairs yield n

The new Named with Sized { ... } in legacyDemo is an expression, not a type, and is untouched; it still compiles on 3.10. Running the later flags in order (3.5, 3.6, 3.7, 3.8 and 3.9 migration) on the patched file changed nothing, because nothing in it needed them. The result compiles and runs with no options on 3.9.0 and on 3.10.0. The last change is the one to review: for case Some(n) <- pairs filters the elements that do not match, which is the behaviour the 3.3 code had, now stated.

The table below lists what each -rewrite step changes, taken from the migration guide.

Flag What -rewrite changes

-source:3.4-migration

with to & in types; _ type wildcards to ?; xs: _* to xs*; var x = _ to scala.compiletime.uninitialized; drops [this] from private[this] and protected[this]; drops a trailing _ used only for eta expansion; backticks alphanumeric infix operators that are not declared infix; inserts using on explicit arguments to context-bound parameters; inserts case on refutable for generators

-source:3.5-migration

no new rewrites

-source:3.6-migration

named arguments on Java-defined annotations; (x = e) becomes {x = e} where it would now be a one-element named tuple instead of an assignment

-source:3.7-migration

inserts using at call sites of implicit parameters; drops an empty () that existed only to pass implicits; rewrites infix named-argument lists to dotted calls

-source:3.8-migration

refutable pattern bindings such as val hd :: tl = xs gain .runtimeChecked; : @unchecked ascriptions become .runtimeChecked

-source:3.9-migration

backtick identifiers that contain $

Some changes have no automatic rewrite; fix those by hand as the compiler reports them.

Two rewrites that depend on the order

A second small file shows the using insertion and the runtimeChecked rewrite, and why one of them needs the other step to run first. In Scala 3.3 a context bound is an implicit parameter and can be given explicitly:

package trailhead

def longest[T: Ordering](xs: List[T]): T = xs.max

def portFor(name: String): Option[Int] =
  if name == "http" then Some(8080) else None

@main def legacy2(): Unit =
  println(longest(List(3, 9, 4))(Ordering.Int))
  val Some(http) = portFor("http")
  println(http)

From 3.6 on a context bound is a using parameter, so on 3.9.0 and 3.10.0 the call fails:

-- [E050] Type Error: Legacy2.scala:9:10
9 |  println(longest(List(3, 9, 4))(Ordering.Int))
  |          ^^^^^^^^^^^^^^^^^^^^^^
  |         method longest in package trailhead does not take more parameters

Running -source:3.8-migration -rewrite first stops there: the type error blocks the patching and nothing is changed. Running the 3.4 step first changes the call to longest(List(3, 9, 4))(using Ordering.Int), and the file compiles. In that run the 3.4 step also patched the refutable val, to val Some(http) = portFor("http"): @unchecked, because the compiler applies the older patch for that warning as well. A file that has only the refutable val, such as this one:

package trailhead

def portFor(name: String): Option[Int] =
  if name == "http" then Some(8080) else None

@main def ports(): Unit =
  val Some(http) = portFor("http")
  val first = (portFor("http"): @unchecked) match
    case Some(p) => p
  println(s"$http $first")

is changed by -source:3.8-migration -rewrite on 3.9.0 to val Some(http) = portFor("http").runtimeChecked. The program prints 8080 8080 on 3.10.0. Only the val was rewritten; the (x: @unchecked) match form is not reported by the compiler as a warning, so the rewrite leaves it, and you can change it by hand to portFor("http").runtimeChecked match. runtimeChecked (SIP-57, stable in 3.8) is a method that marks "this match may fail at run time" instead of an awkward type ascription; see Pattern Matching and Extractors & Pattern Bindings.

The practical rule: rewrite in order, and keep the project compiling between steps. When a step is blocked by a type error that has no rewrite, fix that error by hand first.

-source:3.3 as a fallback

When a part of the code base is hard to migrate, the compiler can be told to read it under older rules. This is the second tool in the guide: compile the subproject with -source:3.3 on the new compiler, and the legacy constructs are accepted as they were. Here the legacy file from above on 3.10.0:

Option on 3.10.0 Result

-source:3.3

compiles with one warning (the for pattern) and prints the five lines shown above

-source:3.4

fails: the for pattern is an error, the other constructs are warnings

-source:3.9

fails the same way as the default would for the for pattern; with is a warning, not an error

The older rules are respected on a best-effort basis, and newer language features are not available in that part of the code until the flag is removed. Treat it as a temporary measure for a subproject, with a ticket to remove it. It does not change the binary output: code compiled by 3.10 under -source:3.3 still has the 3.10 TASTy version, as explained in Versions, Compatibility & JDKs.

The 3.8 boundary: standard library, JDK and reflection

Scala 3.8 is the largest single step, and it is why the guide mentions two compiler versions for the rewrites.

JDK 17 is required. From 3.8, including every later release and the 3.9 LTS, compiling and running needs JDK 17 or newer. The reason is lazy vals: older releases implemented them with sun.misc.Unsafe, which is terminally deprecated, so 3.8 uses VarHandle. The 3.3 LTS line keeps producing JDK 8 bytecode, so a project stuck on an old JDK can stay on 3.3.x, and 3.3.8 offers -Yfuture-lazy-vals for JDK 9 and later. The library on the class path matters as well: code compiled with 3.0 to 3.7 still uses the old lazy val API, and any such dependency brings the warning back.

The standard library is compiled by Scala 3. Its context bounds are now given, so an explicit argument needs using. On 3.7.4 this compiles with a warning, on 3.9.0 it is an error:

import scala.reflect.ClassTag

@main def buffers(): Unit =
  val empty = Array.empty(ClassTag.Int)
  println(empty.length)
-- Warning: Buffers.scala:4:35          (on 3.7.4)
4 |  val empty = Array.empty(ClassTag.Int)
  |                          ^^^^^^^^^^^^
  |Implicit parameters should be provided with a `using` clause.
  |This code can be rewritten automatically under -rewrite -source 3.7-migration.

-- [E172] Type Error: Buffers.scala:4:25   (on 3.9.0)
4 |  val empty = Array.empty(ClassTag.Int)
  |                         ^
  |                         No ClassTag available for T

Running scala compile Buffers.scala -S 3.7.4 --scalac-option -rewrite --scalac-option -source:3.7-migration patches line 4 to Array.empty(using ClassTag.Int), and the file then compiles and prints 0 on 3.9.0 and 3.10.0. This is the case for starting the rewrites on 3.7.4. The better fix, when the type is known, is to drop the argument: Array.empty[Int].

Other 3.8 effects. The REPL moved to the separate scala3-repl artifact, so tools that embed it need a dependency on it. The Scala 2.13 -Ytasty-reader stops at 3.7: a Scala 3.8 or later artifact cannot be read from Scala 2.13, which the compatibility page demonstrates with a run. And Scala 2.13 libraries that use scala-reflect for runtime reflection (scala.reflect.runtime.universe) may fail when initialised against the Scala 3 standard library, because the library no longer carries the ScalaSignature attribute they read. There is no planned fix; stay on 3.7.x if you need those libraries unchanged, avoid the code path, or move the reflection to the Java reflection API.

3.9 to 3.10: the breaking changes

Binary compatibility is preserved from 3.9 to 3.10: libraries compiled by earlier Scala 3 versions keep working, even those that used features that 3.10 removed. What breaks is source that still uses those features, and macros and compiler plugins that depend on the shape of the trees. 3.10 is the first Scala Next release after 3.9 LTS, and it begins to remove legacy syntax that has been deprecated for several minors. For libraries the Scala team still recommends building with the 3.9 LTS and not with 3.10; for applications, 3.10 is the way to the newest features.

The release notes group the breaking changes into three kinds: APIs that were easy to rely on by accident, syntax that has been deprecated for several releases, and standard library operations that became inline or extension methods.

Inaccessible companion implicits are no longer found. An instance defined in a private companion used to be found by implicit search even where it could not be written out by hand. 3.9 reported such a use under -deprecation; 3.10 stops finding it. This is the item most likely to hit a real project, because the instance is often in a library, so the library needs a new release. The program:

object api:
  class Config
  private object Config:
    given Config = new Config
  def describe(using Config): String = "configured"

@main def inaccessible(): Unit =
  println(api.describe)

On 3.9.0 with -deprecation it compiles with a warning and prints configured:

-- Deprecation Warning: Config.scala:8:22
8 |  println(api.describe)
  |                      ^
  |Usage of implicit given instance given_Config defined in object Config, which is not accessible here.
  |In Scala 3.10, this implicit will no longer be found.

On 3.10.0:

-- [E172] Type Error: Config.scala:8:22
8 |  println(api.describe)
  |                      ^
  |No given instance of type api.Config was found for parameter x$1 of method describe in object api

The fix is to make the companion public or to move the given into a public object. This is why the guide advises compiling with -deprecation on 3.9 before moving to 3.10: it shows every use in your code.

A with B is an error. It warned since 3.4 and is an error in 3.10, as the legacy file above showed. The 3.4 migration rewrite turns it into A & B, and in a typed pattern the intersection must be wrapped in parentheses, case x: (A & B) =>, which the rewrite adds. This is why the 3.4 flags are not obsolete after 3.9.

this is illegal in top-level definitions. A top-level definition has no enclosing class, and this used to resolve to the synthetic package object of the file. On 3.9.0 this compiled and printed TopThis$package$; on 3.10.0:

val self = this

@main def topThis(): Unit =
  println(self.getClass.getSimpleName)
-- Error: TopThis.scala:1:11
1 |val self = this
  |           ^^^^
  |           Top-level definitions cannot refer to this

Other items in the release notes, not run here because they touch macros, bytecode tools or specific libraries:

  • The a -> b syntax is now an inline extension method, and Predef.ArrowAssoc is deprecated and no longer an implicit conversion. Quote patterns that matched ArrowAssoc stop matching. A library DSL that defines -> through an implicit class should use an extension method instead, because Predef.-> may now be chosen over the implicit class.

  • runtime.RichInt and the other Rich* implicit classes are gone; until, to, abs, max and the rest are extension methods in the companion objects of the primitive types. Call sites such as 1 to 10 are unchanged.

  • Stronger constant folding during inlining fixes inlining bugs, but fails for macros that build ill-typed code, which previously worked, and 3.10 checks that a macro expansion has the type it claimed. The release notes name the ZIO ecosystem releases that were needed.

  • Generic signatures in class files now use boxed types for primitive type arguments, so Option[Int => String] has a signature with java.lang.Integer. The method descriptor is unchanged, so linking works; Java compilers and MiMa may notice (IncompatibleSignatureProblem), and MiMa 1.2.1 or later is needed for 3.9 and 3.10 artifacts.

  • The compiler no longer catches StackOverflowError. It spends "fuel" instead, and stops with "Recursion limit exceeded". Very deep types that used to compile may need -Xmax-fuel (default 550) and a larger stack with -Xss.

  • Deprecated no-op compiler settings are removed, including -Youtput-only-tasty; bytecode generation uses the stock ASM library, so tools that took the scala-asm fork transitively may need a dependency update.

  • Cross-compilation between Scala 2 and 3.10 is no longer supported. Libraries that need both stay on 3.9 LTS.

The release notes also advise macro and compiler plugin authors to add a CI job that builds against the nightly Scala Next releases, since several of these problems were found by the Open Community Build late in the cycle.

Features that arrive with the new version

An upgrade can be taken in two stages: first make the code compile, then adopt features. For the features that need -preview on 3.10 (@unroll, relaxed lambda syntax, referenceable package objects), and for the ones that are experimental, see Language Evolution: SIPs, Preview & Experimental. Two points matter for upgrading itself. Preview features stay off by default, so enabling -preview is an explicit opt-in and a project can move to 3.10 without taking any of them. And a feature that was experimental in an earlier release may be stable now: SIP-67 (pattern matching under strictEquality without CanEqual) was experimental in 3.8 and 3.9 and is stable in 3.10, while SIP-71 (into) was preview in 3.8 and is stable in 3.9.

A checklist for the upgrade

  • Read the release announcement of every version you cross, at least the "Breaking changes" and "Migration" parts.

  • Do not start with 3.8.0; if you stop at 3.8, take 3.8.4.

  • Commit a clean build on the old version, then change one thing per commit: build tool, JDK, Scala.js, Scala, libraries.

  • Compile with -deprecation and -feature on the old version first; fix what it reports.

  • Run the rewrites in order with 3.9.0, one flag per compile, and review the diff as ordinary source.

  • If the project passes explicit arguments to standard library methods that use context bounds, run the 3.7 step on 3.7.4 before the Scala 3.8 boundary.

  • Use -source:3.3 only for a subproject and only temporarily; remove it before declaring the upgrade done.

  • For libraries, publish with 3.9 LTS, check each release with MiMa 1.2.1 or later, and consider TASTy-MiMa; a library compiled with a newer minor cannot be used by a project on an older one.

  • For macros and compiler plugins, test against 3.10 before users move, and add a nightly CI job.

  • Pin the compiler version in the build and bump it on purpose.

Where the books are outdated

  • The Scala 3 Book and most older books treat Scala 3 as a single version and show the Scala 2 constructs as simply "still working" with a migration warning. Since 3.4 these warn, and in 3.10 several are errors, so code copied from older material may need a rewrite flag on a current compiler.

  • Material written for 3.0 to 3.3 shows -source:3.0-migration as the way into Scala 3. The relevant flags for an existing Scala 3 project are the 3.4 to 3.9 ones; -source:3.0-migration and -source:3.1-migration are for Scala 2 code and are covered in Migrating from Scala 2.

  • Books and posts that say JDK 8 or 11 are normal targets are out of date: from 3.8 the floor is JDK 17, and only the 3.3 line still runs on older JDKs.

  • Advice to build libraries with the newest Scala is outdated: the Scala team recommends the 3.9 LTS for publishing, because artifacts built with a newer minor cannot be consumed by older ones.

  • Pre-3.8 posts say that Scala 3 uses the Scala 2.13 standard library. That was true until 3.7; the library is compiled by Scala 3 from 3.8, and 3.10 begins to use inline and extension methods in it.

Pitfalls

  • Rewriting out of order. -source:3.6-migration does not apply the 3.4 rewrites. Run each flag, in order.

  • A type error blocks -rewrite. The compiler patches only what it reports as a warning; a build that fails to type-check is not patched at all.

  • Rewrites are not reviewed code. They do exactly what the warning said; for example for Some(n) <- pairs becomes a filtering for case, which hides the elements that do not match.

  • Upgrading libraries first. A newer library may need a newer compiler. Upgrade Scala, then the libraries.

  • -source:3.3 as a permanent setting. It is best effort and blocks newer features; treat it as a loan.

  • Counting on a 3.3 consumer for 3.9 artifacts. Artifacts built with 3.9 cannot be used by 3.3 projects. Publishing from 3.3 to 3.9 is a minor-version decision for your own library.

  • Forgetting -deprecation on 3.9. The inaccessible companion implicit shows only there; on 3.10 it is an error with a message that does not say why.

  • Choosing 3.8.0. It had a runtime regression fixed in 3.8.1.

What was run and what was not

Run with Scala CLI 1.17.1 on JDK 26: the legacy file on 3.3.8, 3.9.0 and 3.10.0, the ordered -rewrite steps 3.4 to 3.9 on 3.9.0 with the resulting diff, the -source:3.3, -source:3.4 and -source:3.9 options on 3.10.0, the second file with the using insertion, the runtimeChecked rewrite, the Array.empty case on 3.7.4 and 3.9.0 with the 3.7 rewrite, the inaccessible companion given on 3.9.0 and 3.10.0, and the top-level this. Compiler messages are abridged to the relevant lines, with the absolute file path shortened. Not run: JDK 8 and 11 bytecode, the build tool upgrades (sbt, Mill), Scala.js, the ->, Rich*, constant folding, generic signature and fuel items from the 3.10 notes, and MiMa on a real artifact. Those are taken from the release notes, which are the source for the statements about them.