From 07ce2c9d55af4a340de21e7f21849ff5bdeda75b Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 23 Jun 2025 13:19:07 +0200 Subject: [PATCH 01/19] Disallow erased def --- .../src/dotty/tools/dotc/typer/Checking.scala | 10 ++++++---- tests/{ => invalid}/neg/erased-1.scala | 0 tests/{ => invalid}/neg/erased-2.scala | 0 tests/{ => invalid}/neg/erased-3.scala | 0 .../neg/erased-args-lifted.scala | 2 +- tests/{ => invalid}/neg/erased-implicit.scala | 0 tests/{ => invalid}/neg/erased-params.scala | 0 tests/{ => invalid}/pos/i11743.scala | 2 +- tests/{ => invalid}/pos/i17584a.scala | 0 tests/neg/erased-6.scala | 2 +- tests/neg/erased-assign.scala | 2 +- tests/neg/experimental-imports.scala | 12 ++++++------ tests/neg/experimental.scala | 2 +- tests/neg/magic-canthrow.scala | 11 +++++++++++ tests/neg/typeclass-derivation2.scala | 2 +- tests/new/test.scala | 19 ++++++++----------- tests/pos/erased-conforms.scala | 2 +- .../expeimental-flag-with-lang-feature.scala | 10 ---------- tests/pos/experimental-imports-top.scala | 2 +- tests/pos/experimentalErased.scala | 5 ----- tests/pos/i11864.scala | 2 +- tests/pos/i11896.scala | 2 +- tests/pos/i6419.scala | 4 ---- tests/pos/i7741.scala | 3 --- tests/pos/inline-match-gadt.scala | 2 +- tests/pos/phantom-Eq.scala | 15 ++++++++------- tests/pos/phantom-Eq2/Phantom-Eq_1.scala | 15 ++++++++------- tests/pos/phantom-Evidence.scala | 2 +- tests/run/erased-10.scala | 2 +- tests/run/erased-select-prefix.scala | 8 ++++---- tests/run/i11996.scala | 2 +- 31 files changed, 65 insertions(+), 75 deletions(-) rename tests/{ => invalid}/neg/erased-1.scala (100%) rename tests/{ => invalid}/neg/erased-2.scala (100%) rename tests/{ => invalid}/neg/erased-3.scala (100%) rename tests/{ => invalid}/neg/erased-args-lifted.scala (87%) rename tests/{ => invalid}/neg/erased-implicit.scala (100%) rename tests/{ => invalid}/neg/erased-params.scala (100%) rename tests/{ => invalid}/pos/i11743.scala (81%) rename tests/{ => invalid}/pos/i17584a.scala (100%) create mode 100644 tests/neg/magic-canthrow.scala delete mode 100644 tests/pos/expeimental-flag-with-lang-feature.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 73918dcaeb84..b422a5f21a6c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -681,10 +681,12 @@ object Checking { fail(ModifierNotAllowedForDefinition(Flags.Infix, s"A top-level ${sym.showKind} cannot be infix.")) if sym.isUpdateMethod && !sym.owner.derivesFrom(defn.Caps_Mutable) then fail(em"Update methods can only be used as members of classes extending the `Mutable` trait") - checkApplicable(Erased, - !sym.is(Lazy, butNot = Given) - && !sym.isMutableVarOrAccessor - && (!sym.isType || sym.isClass)) + val unerasable = + sym.is(Lazy, butNot = Given) + || sym.is(Method, butNot = Macro) + || sym.is(Mutable) + || sym.isType && !sym.isClass + checkApplicable(Erased, !unerasable) checkCombination(Final, Open) checkCombination(Sealed, Open) checkCombination(Final, Sealed) diff --git a/tests/neg/erased-1.scala b/tests/invalid/neg/erased-1.scala similarity index 100% rename from tests/neg/erased-1.scala rename to tests/invalid/neg/erased-1.scala diff --git a/tests/neg/erased-2.scala b/tests/invalid/neg/erased-2.scala similarity index 100% rename from tests/neg/erased-2.scala rename to tests/invalid/neg/erased-2.scala diff --git a/tests/neg/erased-3.scala b/tests/invalid/neg/erased-3.scala similarity index 100% rename from tests/neg/erased-3.scala rename to tests/invalid/neg/erased-3.scala diff --git a/tests/neg/erased-args-lifted.scala b/tests/invalid/neg/erased-args-lifted.scala similarity index 87% rename from tests/neg/erased-args-lifted.scala rename to tests/invalid/neg/erased-args-lifted.scala index dfa7b74ee3d4..a2f6a654429f 100644 --- a/tests/neg/erased-args-lifted.scala +++ b/tests/invalid/neg/erased-args-lifted.scala @@ -2,7 +2,7 @@ object Test { def foo(a: Int)(b: Int, c: Int) = 42 - erased def bar(erased i: Int): Int = { + inline def bar(erased i: Int): Int = { println(1) 42 } diff --git a/tests/neg/erased-implicit.scala b/tests/invalid/neg/erased-implicit.scala similarity index 100% rename from tests/neg/erased-implicit.scala rename to tests/invalid/neg/erased-implicit.scala diff --git a/tests/neg/erased-params.scala b/tests/invalid/neg/erased-params.scala similarity index 100% rename from tests/neg/erased-params.scala rename to tests/invalid/neg/erased-params.scala diff --git a/tests/pos/i11743.scala b/tests/invalid/pos/i11743.scala similarity index 81% rename from tests/pos/i11743.scala rename to tests/invalid/pos/i11743.scala index ae524ca01ad6..3114383b3433 100644 --- a/tests/pos/i11743.scala +++ b/tests/invalid/pos/i11743.scala @@ -2,7 +2,7 @@ import language.experimental.erasedDefinitions import scala.compiletime.erasedValue type UnivEq[A] object UnivEq: - erased def force[A]: UnivEq[A] = erasedValue + inline def force[A]: UnivEq[A] = erasedValue extension [A](erased proof: UnivEq[A]) inline def univEq(a: A, b: A): Boolean = a == b diff --git a/tests/pos/i17584a.scala b/tests/invalid/pos/i17584a.scala similarity index 100% rename from tests/pos/i17584a.scala rename to tests/invalid/pos/i17584a.scala diff --git a/tests/neg/erased-6.scala b/tests/neg/erased-6.scala index 4585ab876b3d..529fb5b2dd88 100644 --- a/tests/neg/erased-6.scala +++ b/tests/neg/erased-6.scala @@ -1,7 +1,7 @@ //> using options -language:experimental.erasedDefinitions object Test { - erased def foo: Foo = new Foo + erased val foo: Foo = new Foo foo.x() // error foo.y // error foo.z // error diff --git a/tests/neg/erased-assign.scala b/tests/neg/erased-assign.scala index 5026ca3f1856..61c8802e576e 100644 --- a/tests/neg/erased-assign.scala +++ b/tests/neg/erased-assign.scala @@ -4,7 +4,7 @@ object Test { var i: Int = 1 def foo(erased a: Int): Int = { i = a // error - erased def r = { + inline def r = { i = a () } diff --git a/tests/neg/experimental-imports.scala b/tests/neg/experimental-imports.scala index e3a91be45f08..10e655ddf3b2 100644 --- a/tests/neg/experimental-imports.scala +++ b/tests/neg/experimental-imports.scala @@ -8,14 +8,14 @@ object Object1: import language.experimental.namedTypeArguments import language.experimental.genericNumberLiterals import language.experimental.erasedDefinitions - erased def f = 1 + erased val f = 1 object Object2: import language.experimental.fewerBraces // error import language.experimental.namedTypeArguments // error import language.experimental.genericNumberLiterals // error import language.experimental.erasedDefinitions // error - erased def f = 1 + erased val f = 1 @experimental object Class1: @@ -23,14 +23,14 @@ object Class1: import language.experimental.namedTypeArguments import language.experimental.genericNumberLiterals import language.experimental.erasedDefinitions - erased def f = 1 + erased val f = 1 object Class2: import language.experimental.fewerBraces // error import language.experimental.namedTypeArguments // error import language.experimental.genericNumberLiterals // error import language.experimental.erasedDefinitions // error - erased def f = 1 + erased val f = 1 @experimental def fun1 = @@ -38,11 +38,11 @@ def fun1 = import language.experimental.namedTypeArguments import language.experimental.genericNumberLiterals import language.experimental.erasedDefinitions - erased def f = 1 + erased val f = 1 def fun2 = import language.experimental.fewerBraces // error import language.experimental.namedTypeArguments // error import language.experimental.genericNumberLiterals // error import language.experimental.erasedDefinitions // error - erased def f = 1 + erased val f = 1 diff --git a/tests/neg/experimental.scala b/tests/neg/experimental.scala index f35a7ca19d7f..583a8c5aa183 100644 --- a/tests/neg/experimental.scala +++ b/tests/neg/experimental.scala @@ -13,7 +13,7 @@ class Test1 { import scala.compiletime.erasedValue type UnivEq[A] object UnivEq: - erased def force[A]: UnivEq[A] = erasedValue + inline def force[A]: UnivEq[A] = erasedValue extension [A](erased proof: UnivEq[A]) inline def univEq(a: A, b: A): Boolean = a == b diff --git a/tests/neg/magic-canthrow.scala b/tests/neg/magic-canthrow.scala new file mode 100644 index 000000000000..ceca68fd233a --- /dev/null +++ b/tests/neg/magic-canthrow.scala @@ -0,0 +1,11 @@ +import language.experimental.erasedDefinitions +import java.io.IOException + +class CanThrow[-E <: Exception] + +def foo[E <: Exception](e: E)(using erased CanThrow[E]): Nothing = throw e + +erased def magic[E]: E = magic // error + +def Test = foo(new IOException)(using magic) + diff --git a/tests/neg/typeclass-derivation2.scala b/tests/neg/typeclass-derivation2.scala index eca11fb326ed..ba89fb4c39c8 100644 --- a/tests/neg/typeclass-derivation2.scala +++ b/tests/neg/typeclass-derivation2.scala @@ -119,7 +119,7 @@ object TypeLevel { type Subtype[t] = Type[_, t] type Supertype[t] = Type[t, _] type Exactly[t] = Type[t, t] - erased def typeOf[T]: Type[T, T] = compiletime.erasedValue + inline def typeOf[T]: Type[T, T] = compiletime.erasedValue } // An algebraic datatype diff --git a/tests/new/test.scala b/tests/new/test.scala index d350e15a8c9f..dc0b40d6a755 100644 --- a/tests/new/test.scala +++ b/tests/new/test.scala @@ -1,15 +1,12 @@ +import java.io.IOException -package foo - -package object bar: - opaque type O[X] >: X = X - -class Test: - import bar.O - - val x = "abc" - val y: O[String] = x - //val z: String = y +class CanThrow[-E <: Exception] +def foo[E <: Exception](e: E)(using erased CanThrow[E]): Nothing = throw e +erased def magic[E]: E = magic // error +inline def moreMagic[E]: E = moreMagic +def Test = + foo(new IOException)(using magic) + foo(new IOException)(using moreMagic) // should be error diff --git a/tests/pos/erased-conforms.scala b/tests/pos/erased-conforms.scala index 426490d5a53a..1b3d06d4e261 100644 --- a/tests/pos/erased-conforms.scala +++ b/tests/pos/erased-conforms.scala @@ -5,7 +5,7 @@ erased class <::<[-From, +To] extends ErasedTerm erased class =::=[From, To] extends (From <::< To) -erased given [X] => (X =::= X) = scala.compiletime.erasedValue +inline given [X] => (X =::= X) = scala.compiletime.erasedValue extension [From](x: From) inline def cast[To](using From <::< To): To = x.asInstanceOf[To] // Safe cast because we know `From <:< To` diff --git a/tests/pos/expeimental-flag-with-lang-feature.scala b/tests/pos/expeimental-flag-with-lang-feature.scala deleted file mode 100644 index 96069c332e02..000000000000 --- a/tests/pos/expeimental-flag-with-lang-feature.scala +++ /dev/null @@ -1,10 +0,0 @@ -//> using options -experimental - -import scala.language.experimental.erasedDefinitions -import scala.language.experimental.namedTypeArguments - -erased def erasedFun(erased x: Int): Int = x - -def namedTypeArgumentsFun[T, U]: Int = - namedTypeArgumentsFun[T = Int, U = Int] - namedTypeArgumentsFun[U = Int, T = Int] diff --git a/tests/pos/experimental-imports-top.scala b/tests/pos/experimental-imports-top.scala index 9ba2b5cd2c99..595caac66fe7 100644 --- a/tests/pos/experimental-imports-top.scala +++ b/tests/pos/experimental-imports-top.scala @@ -4,4 +4,4 @@ import language.experimental.erasedDefinitions import annotation.experimental @experimental -erased def f = 1 +erased val f = 1 diff --git a/tests/pos/experimentalErased.scala b/tests/pos/experimentalErased.scala index 358c134c714a..4a504e3d8a80 100644 --- a/tests/pos/experimentalErased.scala +++ b/tests/pos/experimentalErased.scala @@ -6,11 +6,6 @@ erased class Foo erased class Bar -@experimental -erased def foo = 2 - -erased def bar = 2 - @experimental erased val foo2 = 2 diff --git a/tests/pos/i11864.scala b/tests/pos/i11864.scala index ba43336e13ca..3c4c1004880a 100644 --- a/tests/pos/i11864.scala +++ b/tests/pos/i11864.scala @@ -40,7 +40,7 @@ final class CallbackTo[+A] { object CallbackTo { type MapGuard[A] = { type Out = A } - erased given MapGuard: [A] => MapGuard[A] = compiletime.erasedValue + inline given MapGuard: [A] => MapGuard[A] = compiletime.erasedValue def traverse[A, B](ta: List[A]): CallbackTo[List[B]] = val x: CallbackTo[List[A] => List[B]] = ??? diff --git a/tests/pos/i11896.scala b/tests/pos/i11896.scala index 49e5307f1a49..e9045ffe605f 100644 --- a/tests/pos/i11896.scala +++ b/tests/pos/i11896.scala @@ -1,7 +1,7 @@ import scala.language.experimental.erasedDefinitions type X -erased def x: X = compiletime.erasedValue +inline def x: X = compiletime.erasedValue def foo(using erased X): Unit = () diff --git a/tests/pos/i6419.scala b/tests/pos/i6419.scala index 550922f48d76..44136d9e48a3 100644 --- a/tests/pos/i6419.scala +++ b/tests/pos/i6419.scala @@ -9,8 +9,4 @@ class Foo { inline def bar: Unit = { foo } - - erased def baz: Unit = { - foo - } } diff --git a/tests/pos/i7741.scala b/tests/pos/i7741.scala index af9912915cc0..981789f14e2a 100644 --- a/tests/pos/i7741.scala +++ b/tests/pos/i7741.scala @@ -3,9 +3,6 @@ import scala.language.experimental.erasedDefinitions class A1 { @native private def a: Unit } -trait A2 { - erased def i(erased a: Int): Int -} trait A3 { erased val a: Int } \ No newline at end of file diff --git a/tests/pos/inline-match-gadt.scala b/tests/pos/inline-match-gadt.scala index cf2aae00b402..7c966f33bf48 100644 --- a/tests/pos/inline-match-gadt.scala +++ b/tests/pos/inline-match-gadt.scala @@ -2,7 +2,7 @@ import scala.language.experimental.erasedDefinitions object `inline-match-gadt` { class Exactly[T] - erased def exactType[T]: Exactly[T] = compiletime.erasedValue + inline def exactType[T]: Exactly[T] = compiletime.erasedValue inline def foo[T](t: T): T = inline exactType[T] match { diff --git a/tests/pos/phantom-Eq.scala b/tests/pos/phantom-Eq.scala index d844c4b110c6..f3a4af02a186 100644 --- a/tests/pos/phantom-Eq.scala +++ b/tests/pos/phantom-Eq.scala @@ -16,18 +16,19 @@ object PhantomEq { object EqUtil { - type PhantomEq[-L, -R] + class PhantomEq[-L, -R] type PhantomEqEq[T] = PhantomEq[T, T] + erased val phantomEq = PhantomEq[Any, Any]() extension [T](x: T) def ===[U](y: U)(using erased PhantomEq[T, U]) = x.equals(y) - erased given eqString: PhantomEqEq[String] = compiletime.erasedValue - erased given eqInt: PhantomEqEq[Int] = compiletime.erasedValue - erased given eqDouble: PhantomEqEq[Double] = compiletime.erasedValue + inline given eqString: PhantomEqEq[String] = phantomEq + inline given eqInt: PhantomEqEq[Int] = phantomEq + inline given eqDouble: PhantomEqEq[Double] = phantomEq - erased given eqByteNum: PhantomEq[Byte, Number] = compiletime.erasedValue - erased given eqNumByte: PhantomEq[Number, Byte] = compiletime.erasedValue + inline given eqByteNum: PhantomEq[Byte, Number] = phantomEq + inline given eqNumByte: PhantomEq[Number, Byte] = phantomEq - erased given eqSeq: [T, U] => (erased PhantomEq[T, U]) => PhantomEq[Seq[T], Seq[U]] = compiletime.erasedValue + inline given eqSeq: [T, U] => (erased PhantomEq[T, U]) => PhantomEq[Seq[T], Seq[U]] = phantomEq } diff --git a/tests/pos/phantom-Eq2/Phantom-Eq_1.scala b/tests/pos/phantom-Eq2/Phantom-Eq_1.scala index b041a4a87efe..b5021a30b09b 100644 --- a/tests/pos/phantom-Eq2/Phantom-Eq_1.scala +++ b/tests/pos/phantom-Eq2/Phantom-Eq_1.scala @@ -1,19 +1,20 @@ import scala.language.experimental.erasedDefinitions +import scala.annotation.publicInBinary /* This is a version of ../pos/phantomEq.scala that tests phantom with separate compilation */ object EqUtil { - final class PhantomEq[-L, -R] private[EqUtil]() + final class PhantomEq[-L, -R] @publicInBinary private[EqUtil]() type PhantomEqEq[T] = PhantomEq[T, T] extension [T](x: T) def ===[U] (y: U) (using erased PhantomEq[T, U]) = x.equals(y) - erased given eqString: PhantomEqEq[String] = new PhantomEq[String, String] - erased given eqInt: PhantomEqEq[Int] = new PhantomEq[Int, Int] - erased given eqDouble: PhantomEqEq[Double] = new PhantomEq[Double, Double] - erased given eqByteNum: PhantomEq[Byte, Number] = new PhantomEq[Byte, Number] - erased given eqNumByte: PhantomEq[Number, Byte] = new PhantomEq[Number, Byte] - erased given eqSeq: [T, U] => (erased eq: PhantomEq[T, U]) => PhantomEq[Seq[T], Seq[U]] = + inline given eqString: PhantomEqEq[String] = new PhantomEq[String, String] + inline given eqInt: PhantomEqEq[Int] = new PhantomEq[Int, Int] + inline given eqDouble: PhantomEqEq[Double] = new PhantomEq[Double, Double] + inline given eqByteNum: PhantomEq[Byte, Number] = new PhantomEq[Byte, Number] + inline given eqNumByte: PhantomEq[Number, Byte] = new PhantomEq[Number, Byte] + inline given eqSeq: [T, U] => (erased eq: PhantomEq[T, U]) => PhantomEq[Seq[T], Seq[U]] = new PhantomEq[Seq[T], Seq[U]] } diff --git a/tests/pos/phantom-Evidence.scala b/tests/pos/phantom-Evidence.scala index f56ce3b798ee..c24ec477a2a8 100644 --- a/tests/pos/phantom-Evidence.scala +++ b/tests/pos/phantom-Evidence.scala @@ -26,5 +26,5 @@ object WithNormalState { object Utils { type =::=[From, To] - erased given tpEquals: [A] => (A =::= A) = compiletime.erasedValue + inline given tpEquals: [A] => (A =::= A) = compiletime.erasedValue } diff --git a/tests/run/erased-10.scala b/tests/run/erased-10.scala index 004d07b4de37..ce8c8a42de4c 100644 --- a/tests/run/erased-10.scala +++ b/tests/run/erased-10.scala @@ -10,7 +10,7 @@ object Test { println("pacFun4") } - erased def inky: Int = { + inline def inky: Int = { println("inky") // in erased function 42 } diff --git a/tests/run/erased-select-prefix.scala b/tests/run/erased-select-prefix.scala index b877a0d209d7..06ed46d5ccce 100644 --- a/tests/run/erased-select-prefix.scala +++ b/tests/run/erased-select-prefix.scala @@ -29,9 +29,9 @@ object Test { def bar(erased i: Int): Unit = () - erased def foo0: Int = 0 - erased def foo1(): Int = 1 - erased def foo2[T]: Int = 2 - erased def foo3[T](): Int = 3 + inline def foo0: Int = 0 + inline def foo1(): Int = 1 + inline def foo2[T]: Int = 2 + inline def foo3[T](): Int = 3 } diff --git a/tests/run/i11996.scala b/tests/run/i11996.scala index 9724e12b575e..8769ca13eb88 100644 --- a/tests/run/i11996.scala +++ b/tests/run/i11996.scala @@ -3,7 +3,7 @@ final class UnivEq[A] object UnivEq: - erased def force[A]: UnivEq[A] = + inline def force[A]: UnivEq[A] = compiletime.erasedValue extension [A](a: A) From 589c9a74ff5a1fb8e013bb5f0834483b0333485f Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 23 Jun 2025 13:28:01 +0200 Subject: [PATCH 02/19] Don't treat `erased` as late initialized Since we will force erased expressions to be pure values, they are always initialized. --- compiler/src/dotty/tools/dotc/core/CheckRealizable.scala | 6 ++---- tests/neg/erased-path.scala | 2 +- tests/neg/i4060.scala | 4 ++-- tests/{neg => pos}/erased-24.scala | 4 ++-- tests/{neg => pos}/erased-pathdep-1.scala | 8 +++----- tests/{neg => pos}/erased-pathdep-2.scala | 4 ++-- tests/{neg => pos}/erased-singleton.scala | 2 +- 7 files changed, 13 insertions(+), 17 deletions(-) rename tests/{neg => pos}/erased-24.scala (77%) rename tests/{neg => pos}/erased-pathdep-1.scala (67%) rename tests/{neg => pos}/erased-pathdep-2.scala (81%) rename tests/{neg => pos}/erased-singleton.scala (67%) diff --git a/compiler/src/dotty/tools/dotc/core/CheckRealizable.scala b/compiler/src/dotty/tools/dotc/core/CheckRealizable.scala index 81b03d765676..f2e426612eeb 100644 --- a/compiler/src/dotty/tools/dotc/core/CheckRealizable.scala +++ b/compiler/src/dotty/tools/dotc/core/CheckRealizable.scala @@ -48,8 +48,6 @@ object CheckRealizable { def boundsRealizability(tp: Type)(using Context): Realizability = new CheckRealizable().boundsRealizability(tp) - - private val LateInitializedFlags = Lazy | Erased } /** Compute realizability status. @@ -72,7 +70,7 @@ class CheckRealizable(using Context) { /** Is symbol's definitition a lazy or erased val? * (note we exclude modules here, because their realizability is ensured separately) */ - private def isLateInitialized(sym: Symbol) = sym.isOneOf(LateInitializedFlags, butNot = Module) + private def isLateInitialized(sym: Symbol) = sym.is(Lazy, butNot = Module) /** The realizability status of given type `tp`*/ def realizability(tp: Type): Realizability = tp.dealias match { @@ -184,7 +182,7 @@ class CheckRealizable(using Context) { private def memberRealizability(tp: Type) = { def checkField(sofar: Realizability, fld: SingleDenotation): Realizability = sofar andAlso { - if (checkedFields.contains(fld.symbol) || fld.symbol.isOneOf(Private | Mutable | LateInitializedFlags)) + if (checkedFields.contains(fld.symbol) || fld.symbol.isOneOf(Private | Mutable | Lazy)) // if field is private it cannot be part of a visible path // if field is mutable it cannot be part of a path // if field is lazy or erased it does not need to be initialized when the owning object is diff --git a/tests/neg/erased-path.scala b/tests/neg/erased-path.scala index ece90e563483..6666165d5cc6 100644 --- a/tests/neg/erased-path.scala +++ b/tests/neg/erased-path.scala @@ -6,6 +6,6 @@ trait Obj { erased val s: Sys lazy val t: Sys - type S = s.X // error: not a legal path, since nonfinal + type S = s.X // now OK, was error: not a legal path, since nonfinal type T = t.X // error: not a legal path, since nonfinal } \ No newline at end of file diff --git a/tests/neg/i4060.scala b/tests/neg/i4060.scala index bd16ed867966..b85c1190cc3e 100644 --- a/tests/neg/i4060.scala +++ b/tests/neg/i4060.scala @@ -6,7 +6,7 @@ object App { trait A { type L >: Any} def upcast(erased a: A)(x: Any): a.L = x erased val p: A { type L <: Nothing } = p - def coerce(x: Any): Int = upcast(p)(x) // error + def coerce(x: Any): Int = upcast(p)(x) // ok? def coerceInline(x: Any): Int = upcast(compiletime.erasedValue[A {type L <: Nothing}])(x) // error @@ -14,7 +14,7 @@ object App { def upcast_dep_parameter(erased a: B)(x: a.L) : Int = x erased val q : B { type L >: Any } = compiletime.erasedValue - def coerceInlineWithB(x: Any): Int = upcast_dep_parameter(q)(x) // error + def coerceInlineWithB(x: Any): Int = upcast_dep_parameter(q)(x) // ok? def main(args: Array[String]): Unit = { println(coerce("Uh oh!")) diff --git a/tests/neg/erased-24.scala b/tests/pos/erased-24.scala similarity index 77% rename from tests/neg/erased-24.scala rename to tests/pos/erased-24.scala index bf2f1d21435e..410a1900a1c1 100644 --- a/tests/neg/erased-24.scala +++ b/tests/pos/erased-24.scala @@ -12,8 +12,8 @@ object Test { null.asInstanceOf[foo.X] // ok } - def fun2(erased foo: Foo)(erased bar: foo.B): bar.X = { // error - null.asInstanceOf[bar.X] // error + def fun2(erased foo: Foo)(erased bar: foo.B): bar.X = { // was error + null.asInstanceOf[bar.X] // was error } } diff --git a/tests/neg/erased-pathdep-1.scala b/tests/pos/erased-pathdep-1.scala similarity index 67% rename from tests/neg/erased-pathdep-1.scala rename to tests/pos/erased-pathdep-1.scala index 422ceb5e37fe..e696c48df328 100644 --- a/tests/neg/erased-pathdep-1.scala +++ b/tests/pos/erased-pathdep-1.scala @@ -1,16 +1,14 @@ //> using options -language:experimental.erasedDefinitions -// Could become a neg test if we had totality checking for erased arguments - object Test { fun1(new Bar) val _ = fun2(new Bar) val _ = fun3(new Bar) - def fun1[F >: Bar <: Foo](erased f: F): f.X = null.asInstanceOf[f.X] // error // error - def fun2[F >: Bar <: Foo](erased f: F)(erased bar: f.B): f.B = null.asInstanceOf[f.B] // error // error // error - def fun3[F >: Bar <: Foo](erased f: F)(erased b: f.B): b.X = null.asInstanceOf[b.X] // error // error // error + def fun1[F >: Bar <: Foo](erased f: F): f.X = null.asInstanceOf[f.X] + def fun2[F >: Bar <: Foo](erased f: F)(erased bar: f.B): f.B = null.asInstanceOf[f.B] + def fun3[F >: Bar <: Foo](erased f: F)(erased b: f.B): b.X = null.asInstanceOf[b.X] } class Foo { diff --git a/tests/neg/erased-pathdep-2.scala b/tests/pos/erased-pathdep-2.scala similarity index 81% rename from tests/neg/erased-pathdep-2.scala rename to tests/pos/erased-pathdep-2.scala index 0b50acbf3b30..8c9f7b414a98 100644 --- a/tests/neg/erased-pathdep-2.scala +++ b/tests/pos/erased-pathdep-2.scala @@ -7,8 +7,8 @@ object Test { type F >: Bar <: Foo class A(erased val f: F) { - type F1 <: f.X // error - type F2[Z <: f.X] // error + type F1 <: f.X // was error + type F2[Z <: f.X] // was error } } diff --git a/tests/neg/erased-singleton.scala b/tests/pos/erased-singleton.scala similarity index 67% rename from tests/neg/erased-singleton.scala rename to tests/pos/erased-singleton.scala index 5ffa78e24b07..f7ad5165ec0a 100644 --- a/tests/neg/erased-singleton.scala +++ b/tests/pos/erased-singleton.scala @@ -5,5 +5,5 @@ trait Sys trait Obj { erased val s: Sys - type S = s.type // error: non final + type S = s.type // now OK, was error: non final } From 65bd63ba80a65f5eca3c3bd64a73512bb41a5fb3 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 20 Jun 2025 15:51:27 +0200 Subject: [PATCH 03/19] Rename `erasedParams` to `paramErasureStatuses` # Conflicts: # compiler/src/dotty/tools/dotc/ast/TreeInfo.scala --- compiler/src/dotty/tools/dotc/ast/tpd.scala | 2 +- .../dotty/tools/dotc/core/TypeComparer.scala | 2 +- .../dotty/tools/dotc/core/TypeErasure.scala | 2 +- .../src/dotty/tools/dotc/core/Types.scala | 4 ++-- .../dotty/tools/dotc/quoted/Interpreter.scala | 4 ++-- .../dotty/tools/dotc/transform/Erasure.scala | 23 +++++++++++++++++-- .../tools/dotc/transform/PostTyper.scala | 6 ++--- .../dotty/tools/dotc/typer/EtaExpansion.scala | 6 ++--- .../src/dotty/tools/dotc/typer/Typer.scala | 4 ++-- .../quoted/runtime/impl/QuoteMatcher.scala | 2 +- .../quoted/runtime/impl/QuotesImpl.scala | 2 +- 11 files changed, 38 insertions(+), 19 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 991309293c0c..92c20afe7a73 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -301,7 +301,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { assert(vparams.hasSameLengthAs(tp.paramNames) && vparams.head.isTerm) (vparams.asInstanceOf[List[TermSymbol]], remaining1) case nil => - (tp.paramNames.lazyZip(tp.paramInfos).lazyZip(tp.erasedParams).map(valueParam), Nil) + (tp.paramNames.lazyZip(tp.paramInfos).lazyZip(tp.paramErasureStatuses).map(valueParam), Nil) val (rtp, paramss) = recur(tp.instantiate(vparams.map(_.termRef)), remaining1) (rtp, vparams :: paramss) case _ => diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index eb03a2b1c05d..5753d311baa9 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2387,7 +2387,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling formals2.isEmpty } // If methods have erased parameters, then the erased parameters must match - val erasedValid = (!tp1.hasErasedParams && !tp2.hasErasedParams) || (tp1.erasedParams == tp2.erasedParams) + val erasedValid = (!tp1.hasErasedParams && !tp2.hasErasedParams) || (tp1.paramErasureStatuses == tp2.paramErasureStatuses) erasedValid && loop(tp1.paramInfos, tp2.paramInfos) } diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 83f087239477..2e6fa7d94d43 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -697,7 +697,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst val (names, formals0) = if tp.hasErasedParams then tp.paramNames .zip(tp.paramInfos) - .zip(tp.erasedParams) + .zip(tp.paramErasureStatuses) .collect{ case (param, isErased) if !isErased => param } .unzip else (tp.paramNames, tp.paramInfos) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 61b3b958fca3..1b59faf81a6f 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -3931,7 +3931,7 @@ object Types extends TypeUtils { case tp: MethodType => val params = if (hasErasedParams) tp.paramInfos - .zip(tp.erasedParams) + .zip(tp.paramErasureStatuses) .collect { case (param, isErased) if !isErased => param } else tp.paramInfos resultSignature.prependTermParams(params, sourceLanguage) @@ -4163,7 +4163,7 @@ object Types extends TypeUtils { final override def isContextualMethod: Boolean = companion.eq(ContextualMethodType) - def erasedParams(using Context): List[Boolean] = + def paramErasureStatuses(using Context): List[Boolean] = paramInfos.map(p => p.hasAnnotation(defn.ErasedParamAnnot)) def nonErasedParamCount(using Context): Int = diff --git a/compiler/src/dotty/tools/dotc/quoted/Interpreter.scala b/compiler/src/dotty/tools/dotc/quoted/Interpreter.scala index dbc2694dc891..816bac14ddd2 100644 --- a/compiler/src/dotty/tools/dotc/quoted/Interpreter.scala +++ b/compiler/src/dotty/tools/dotc/quoted/Interpreter.scala @@ -127,8 +127,8 @@ class Interpreter(pos: SrcPos, classLoader0: ClassLoader)(using Context): case fnType: MethodType => val argTypes = fnType.paramInfos assert(argss.head.size == argTypes.size) - val nonErasedArgs = argss.head.lazyZip(fnType.erasedParams).collect { case (arg, false) => arg }.toList - val nonErasedArgTypes = fnType.paramInfos.lazyZip(fnType.erasedParams).collect { case (arg, false) => arg }.toList + val nonErasedArgs = argss.head.lazyZip(fnType.paramErasureStatuses).collect { case (arg, false) => arg }.toList + val nonErasedArgTypes = fnType.paramInfos.lazyZip(fnType.paramErasureStatuses).collect { case (arg, false) => arg }.toList assert(nonErasedArgs.size == nonErasedArgTypes.size) interpretArgsGroup(nonErasedArgs, nonErasedArgTypes) ::: interpretArgs(argss.tail, fnType.resType) case fnType: AppliedType if defn.isContextFunctionType(fnType) => diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index 3503c707aed9..f2798313fcef 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -583,6 +583,17 @@ object Erasure { checkNotErasedClass(tree) end checkNotErased + def checkPureErased(tree: untpd.Tree, isArgument: Boolean)(using Context): Unit = + if false then inContext(preErasureCtx): + if tpd.isPureExpr(tree.asInstanceOf[tpd.Tree]) then + val tree1 = tree.asInstanceOf[tpd.Tree] + println(i"$tree1 is pure, ${tree1.tpe.widen}") + else + def what = + if isArgument then "argument to erased parameter" + else "right-hand-side of erased value" + report.error(em"$what fails to be a pure expression", tree.srcPos) + private def checkNotErasedClass(tp: Type, tree: untpd.Tree)(using Context): Unit = tp match case JavaArrayType(et) => checkNotErasedClass(et, tree) @@ -848,7 +859,12 @@ object Erasure { val origFunType = origFun.tpe.widen(using preErasureCtx) val ownArgs = origFunType match case mt: MethodType if mt.hasErasedParams => - args.zip(mt.erasedParams).collect { case (arg, false) => arg } + args.lazyZip(mt.paramErasureStatuses).flatMap: (arg, isErased) => + if isErased then + checkPureErased(arg, isArgument = true) + Nil + else + arg :: Nil case _ => args val fun1 = typedExpr(fun, AnyFunctionProto) fun1.tpe.widen match @@ -916,7 +932,9 @@ object Erasure { } override def typedValDef(vdef: untpd.ValDef, sym: Symbol)(using Context): Tree = - if (sym.isEffectivelyErased) erasedDef(sym) + if sym.isEffectivelyErased then + checkPureErased(vdef.rhs, isArgument = false) + erasedDef(sym) else checkNotErasedClass(sym.info, vdef) super.typedValDef(untpd.cpy.ValDef(vdef)( @@ -928,6 +946,7 @@ object Erasure { */ override def typedDefDef(ddef: untpd.DefDef, sym: Symbol)(using Context): Tree = if sym.isEffectivelyErased || sym.name.is(BodyRetainerName) then + checkPureErased(ddef.rhs, isArgument = false) erasedDef(sym) else checkNotErasedClass(sym.info.finalResultType, ddef) diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 5e2ff2d43283..2f1e4a8b2b4f 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -408,7 +408,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => case app: Apply => val methType = app.fun.tpe.widen.asInstanceOf[MethodType] if (methType.hasErasedParams) - for (arg, isErased) <- app.args.lazyZip(methType.erasedParams) do + for (arg, isErased) <- app.args.lazyZip(methType.paramErasureStatuses) do if isErased then if methType.isResultDependent then Checking.checkRealizable(arg.tpe, arg.srcPos, "erased argument") @@ -475,7 +475,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => case tree: ValDef => annotateExperimentalCompanion(tree.symbol) registerIfHasMacroAnnotations(tree) - checkErasedDef(tree) + //checkErasedDef(tree) Checking.checkPolyFunctionType(tree.tpt) val tree1 = cpy.ValDef(tree)(tpt = makeOverrideTypeDeclared(tree.symbol, tree.tpt)) if tree1.removeAttachment(desugar.UntupledParam).isDefined then @@ -483,7 +483,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => processValOrDefDef(super.transform(tree1)) case tree: DefDef => registerIfHasMacroAnnotations(tree) - checkErasedDef(tree) + //checkErasedDef(tree) Checking.checkPolyFunctionType(tree.tpt) annotateContextResults(tree) val tree1 = cpy.DefDef(tree)(tpt = makeOverrideTypeDeclared(tree.symbol, tree.tpt)) diff --git a/compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala b/compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala index 26d03db4b7dc..55778017b76f 100644 --- a/compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala +++ b/compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala @@ -300,9 +300,9 @@ object EtaExpansion extends LiftImpure { val body = Apply(lifted, ids) if (mt.isContextualMethod) body.setApplyKind(ApplyKind.Using) val fn = - if (mt.isContextualMethod) new untpd.FunctionWithMods(params, body, Modifiers(Given), mt.erasedParams) - else if (mt.isImplicitMethod) new untpd.FunctionWithMods(params, body, Modifiers(Implicit), mt.erasedParams) - else if (mt.hasErasedParams) new untpd.FunctionWithMods(params, body, Modifiers(), mt.erasedParams) + if (mt.isContextualMethod) new untpd.FunctionWithMods(params, body, Modifiers(Given), mt.paramErasureStatuses) + else if (mt.isImplicitMethod) new untpd.FunctionWithMods(params, body, Modifiers(Implicit), mt.paramErasureStatuses) + else if (mt.hasErasedParams) new untpd.FunctionWithMods(params, body, Modifiers(), mt.paramErasureStatuses) else untpd.Function(params, body) if (defs.nonEmpty) untpd.Block(defs.toList map (untpd.TypedSplice(_)), fn) else fn } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 7d2bf2f463ba..79d924807a15 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1770,7 +1770,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer if (mt.isParamDependent) report.error(em"$mt is an illegal function type because it has inter-parameter dependencies", tree.srcPos) // Restart typechecking if there are erased classes that we want to mark erased - if mt.erasedParams.zip(mt.paramInfos.map(_.isErasedClass)).exists((paramErased, classErased) => classErased && !paramErased) then + if mt.paramErasureStatuses.zip(mt.paramInfos.map(_.isErasedClass)).exists((paramErased, classErased) => classErased && !paramErased) then val newParams = params3.zipWithConserve(mt.paramInfos.map(_.isErasedClass)) { (arg, isErasedClass) => if isErasedClass then arg.withAddedFlags(Erased) else arg } @@ -3812,7 +3812,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } val erasedParams = pt match { - case defn.PolyFunctionOf(mt: MethodType) => mt.erasedParams + case defn.PolyFunctionOf(mt: MethodType) => mt.paramErasureStatuses case _ => paramTypes.map(_ => false) } diff --git a/compiler/src/scala/quoted/runtime/impl/QuoteMatcher.scala b/compiler/src/scala/quoted/runtime/impl/QuoteMatcher.scala index 3790174526b3..e3d5355eaffa 100644 --- a/compiler/src/scala/quoted/runtime/impl/QuoteMatcher.scala +++ b/compiler/src/scala/quoted/runtime/impl/QuoteMatcher.scala @@ -448,7 +448,7 @@ class QuoteMatcher(debug: Boolean) { def matchErasedParams(sctype: Type, pttype: Type): optional[MatchingExprs] = (sctype, pttype) match case (sctpe: MethodType, pttpe: MethodType) => - if sctpe.erasedParams.sameElements(pttpe.erasedParams) then + if sctpe.paramErasureStatuses.sameElements(pttpe.paramErasureStatuses) then matchErasedParams(sctpe.resType, pttpe.resType) else notMatched diff --git a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala index 852d7ee8b20f..fdd16963d33b 100644 --- a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala +++ b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala @@ -2288,7 +2288,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler case _ => MethodTypeKind.Plain def param(idx: Int): TypeRepr = self.newParamRef(idx) - def erasedParams: List[Boolean] = self.erasedParams + def erasedParams: List[Boolean] = self.paramErasureStatuses def hasErasedParams: Boolean = self.hasErasedParams end extension end MethodTypeMethods From 9fb4803e2952ed0347a8a41d6b55f5b42ac41c8c Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 20 Jun 2025 18:31:50 +0200 Subject: [PATCH 04/19] Don't convert erased vals to getters --- compiler/src/dotty/tools/dotc/transform/Getters.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/Getters.scala b/compiler/src/dotty/tools/dotc/transform/Getters.scala index 6e6d84a9eaae..11adf4da83d5 100644 --- a/compiler/src/dotty/tools/dotc/transform/Getters.scala +++ b/compiler/src/dotty/tools/dotc/transform/Getters.scala @@ -90,7 +90,7 @@ class Getters extends MiniPhase with SymTransformer { thisPhase => d1 } - private val NoGetterNeededFlags = Method | Param | JavaDefined | JavaStatic | PhantomSymbol + private val NoGetterNeededFlags = Method | Param | JavaDefined | JavaStatic | PhantomSymbol | Erased val newSetters = util.HashSet[Symbol]() From 0949eaa2148d46c79652259839b6a3c691075bae Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 24 Jun 2025 13:58:02 +0200 Subject: [PATCH 05/19] Fix handling of compiletime.erasedValue Fixes #23406 --- .../dotty/tools/dotc/core/Definitions.scala | 5 +++ .../tools/dotc/core/SymDenotations.scala | 1 + .../tools/dotc/inlines/InlineReducer.scala | 11 +++++ .../src/dotty/tools/dotc/typer/Typer.scala | 8 ---- library/src/scala/compiletime/package.scala | 1 - tests/neg/i23406.scala | 5 +++ tests/pending/neg/erased-impure.check | 0 tests/pending/neg/erased-impure.scala | 44 +++++++++++++++++++ 8 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 tests/neg/i23406.scala create mode 100644 tests/pending/neg/erased-impure.check create mode 100644 tests/pending/neg/erased-impure.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 381caa775dbd..59966286c40e 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1558,6 +1558,11 @@ class Definitions { @tu lazy val pureSimpleClasses = Set(StringClass, NothingClass, NullClass) ++ ScalaValueClasses() + @tu lazy val capsErasedValueMethods = + Set[Symbol]() + @tu lazy val erasedValueMethods = + capsErasedValueMethods + Compiletime_erasedValue + @tu lazy val AbstractFunctionType: Array[TypeRef] = mkArityArray("scala.runtime.AbstractFunction", MaxImplementedFunctionArity, 0).asInstanceOf[Array[TypeRef]] val AbstractFunctionClassPerRun: PerRun[Array[Symbol]] = new PerRun(AbstractFunctionType.map(_.symbol.asClass)) def AbstractFunctionClass(n: Int)(using Context): Symbol = AbstractFunctionClassPerRun()(using ctx)(n) diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index bd43578e3d53..8155db9f01ef 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -1054,6 +1054,7 @@ object SymDenotations { def isEffectivelyErased(using Context): Boolean = isOneOf(EffectivelyErased) || is(Inline) && !isRetainedInline && !hasAnnotation(defn.ScalaStaticAnnot) + || defn.erasedValueMethods.contains(symbol) /** Is this a member that will become public in the generated binary */ def hasPublicInBinary(using Context): Boolean = diff --git a/compiler/src/dotty/tools/dotc/inlines/InlineReducer.scala b/compiler/src/dotty/tools/dotc/inlines/InlineReducer.scala index 3edb323e6b3b..80516047426f 100644 --- a/compiler/src/dotty/tools/dotc/inlines/InlineReducer.scala +++ b/compiler/src/dotty/tools/dotc/inlines/InlineReducer.scala @@ -341,6 +341,17 @@ class InlineReducer(inliner: Inliner)(using Context): val scrutineeSym = newSym(InlineScrutineeName.fresh(), Synthetic, scrutType).asTerm val scrutineeBinding = normalizeBinding(ValDef(scrutineeSym, scrutinee)) + // If scrutinee has embedded `compiletime.erasedValue[T]` expressions, convert them to + // mark scrutineeSym as Erased. This means that the scrutinee cannot be referenced in + // the reduced term. It is NOT checked that scrutinee is a pure expression, since + // there is a special case in Erase that exempts the RHS of an erased scrutinee definition. + if scrutinee.existsSubTree: + case tree @ TypeApply(fn, args) => tree.symbol == defn.Compiletime_erasedValue + case _ => false + then + scrutineeSym.setFlag(Erased) + + def reduceCase(cdef: CaseDef): MatchReduxWithGuard = { val caseBindingMap = new mutable.ListBuffer[(Symbol, MemberDef)]() diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 79d924807a15..d65c2765e0cf 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2974,14 +2974,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer EmptyTree def typedDefDef(ddef: untpd.DefDef, sym: Symbol)(using Context): Tree = if !sym.info.exists then retractDefDef(sym) else ctx.profiler.onTypedDef(sym) { - - // TODO: - Remove this when `scala.language.experimental.erasedDefinitions` is no longer experimental. - // - Modify signature to `erased def erasedValue[T]: T` - if sym.eq(defn.Compiletime_erasedValue) then - // scala.compiletime.erasedValue should be `erased` but we cannot add this in the source. - // The library cannot use experimental language features, - // hence we special case it until `erased` is no longer experimental. - sym.setFlag(Erased) val DefDef(name, paramss, tpt, _) = ddef checkNonRootName(ddef.name, ddef.nameSpan) completeAnnotations(ddef, sym) diff --git a/library/src/scala/compiletime/package.scala b/library/src/scala/compiletime/package.scala index 8215ae2452a3..1a161ebd4a03 100644 --- a/library/src/scala/compiletime/package.scala +++ b/library/src/scala/compiletime/package.scala @@ -23,7 +23,6 @@ import annotation.{compileTimeOnly, experimental} * the branches. * @syntax markdown */ -// TODO add `erased` once it is not an experimental feature anymore def erasedValue[T]: T = erasedValue[T] /** Used as the initializer of a mutable class or object field, like this: diff --git a/tests/neg/i23406.scala b/tests/neg/i23406.scala new file mode 100644 index 000000000000..79049f4eddff --- /dev/null +++ b/tests/neg/i23406.scala @@ -0,0 +1,5 @@ +inline def funny[T]: String = + inline compiletime.erasedValue[T] match + case x: String => x + +@main def Test = funny[String] // error diff --git a/tests/pending/neg/erased-impure.check b/tests/pending/neg/erased-impure.check new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/pending/neg/erased-impure.scala b/tests/pending/neg/erased-impure.scala new file mode 100644 index 000000000000..8dd668bbc529 --- /dev/null +++ b/tests/pending/neg/erased-impure.scala @@ -0,0 +1,44 @@ +//> using options -explain +import language.experimental.erasedDefinitions +import java.io.IOException +import caps.unsafe.unsafeErasedValue + +class CanThrow[-E <: Exception] + +def foo[E <: Exception](e: E)(using erased CanThrow[E]): Nothing = throw e + +erased val magic1: IOException = ??? // error +erased val magic2: IOException = compiletime.erasedValue[IOException] // error +erased val magic3: IOException = null.asInstanceOf[IOException] // error + +inline def inlineId[T](x: T) = x + +class C() + +def testPure[T](erased x: T): Unit = () + +case class Pair[A, B](x: A, y: B) +object Pair: + def apply(x: Int): Pair2[Int, Int] = + println("Pair2") + Pair2(x, x + 1) + +case class Box[A](x: A): + println(x) + +def Test = + foo(new IOException)(using ???) // error + foo(new IOException)(using inlineId(???)) // error + + testPure(C()) // OK + testPure(inlineId(C())) // OK + testPure(identity(C())) // error, identity is not an inline function + + testPure(Pair(unsafeErasedValue[Int], unsafeErasedValue[String])) // OK + testPure(Pair(unsafeErasedValue[Int])) // error + testPure(Box(unsafeErasedValue[Int])) // error + + + + + From 9d24e18aac5ec1853666cae99e78737e3d4b09ff Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 24 Jun 2025 14:12:10 +0200 Subject: [PATCH 06/19] Fixes to isPureExpr - erasedValue[] is now considered to be pure - calls of synthetic case class apply are considered pure if the case class is NoInits - Companions of Scala-2 classes Tuple and Some are assumed to be NoInits --- .../src/dotty/tools/dotc/ast/TreeInfo.scala | 18 ++++++++++++++++-- .../dotty/tools/dotc/core/Definitions.scala | 4 +++- .../src/dotty/tools/dotc/util/DiffUtil.scala | 1 - library/src/scala/CanThrow.scala | 2 +- tests/init/warn/inner30.scala | 2 +- 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 0a2c0c850e5d..9debb8651be5 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -588,9 +588,13 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => case New(_) | Closure(_, _, _) => Pure case TypeApply(fn, _) => + val sym = fn.symbol if tree.tpe.isInstanceOf[MethodOrPoly] then exprPurity(fn) - else if fn.symbol == defn.QuotedTypeModule_of || fn.symbol == defn.Predef_classOf then Pure - else if fn.symbol == defn.Compiletime_erasedValue && tree.tpe.dealias.isInstanceOf[ConstantType] then Pure + else if sym == defn.QuotedTypeModule_of + || sym == defn.Predef_classOf + || sym == defn.Compiletime_erasedValue && tree.tpe.dealias.isInstanceOf[ConstantType] + || defn.capsErasedValueMethods.contains(sym) + then Pure else Impure case Apply(fn, args) => val factorPurity = minOf(exprPurity(fn), args.map(exprPurity)) @@ -634,6 +638,15 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => def isPureBinding(tree: Tree)(using Context): Boolean = statPurity(tree) >= Pure + def isPureSyntheticCaseApply(sym: Symbol)(using Context): Boolean = + sym.isAllOf(SyntheticMethod) + && sym.name == nme.apply + && sym.owner.is(Module) + && { + val cls = sym.owner.companionClass + cls.is(Case) && cls.isNoInitsRealClass + } + /** Is the application `tree` with function part `fn` known to be pure? * Function value and arguments can still be impure. */ @@ -645,6 +658,7 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => tree.tpe.isInstanceOf[ConstantType] && tree.symbol != NoSymbol && isKnownPureOp(tree.symbol) // A constant expression with pure arguments is pure. || fn.symbol.isStableMember && fn.symbol.isConstructor // constructors of no-inits classes are stable + || isPureSyntheticCaseApply(fn.symbol) /** The purity level of this reference. * @return diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 59966286c40e..e2e15599ba52 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -2006,7 +2006,9 @@ class Definitions { /** A allowlist of Scala-2 classes that are known to be pure */ def isAssuredNoInits(sym: Symbol): Boolean = - (sym `eq` SomeClass) || isTupleClass(sym) + (sym `eq` SomeClass) + || isTupleClass(sym) + || sym.is(Module) && isAssuredNoInits(sym.companionClass) /** If `cls` is Tuple1..Tuple22, add the corresponding *: type as last parent to `parents` */ def adjustForTuple(cls: ClassSymbol, tparams: List[TypeSymbol], parents: List[Type]): List[Type] = { diff --git a/compiler/src/dotty/tools/dotc/util/DiffUtil.scala b/compiler/src/dotty/tools/dotc/util/DiffUtil.scala index 31acc91caa2e..126cff9b9c65 100644 --- a/compiler/src/dotty/tools/dotc/util/DiffUtil.scala +++ b/compiler/src/dotty/tools/dotc/util/DiffUtil.scala @@ -103,7 +103,6 @@ object DiffUtil { case Deleted(str) => deleted(str) }.mkString - (expectedDiff, actualDiff) val pad = " " * 0.max(expectedSize - expected.length) expectedDiff + pad + " | " + actualDiff diff --git a/library/src/scala/CanThrow.scala b/library/src/scala/CanThrow.scala index 485dcecb37df..e87d14671419 100644 --- a/library/src/scala/CanThrow.scala +++ b/library/src/scala/CanThrow.scala @@ -12,5 +12,5 @@ erased class CanThrow[-E <: Exception] extends caps.SharedCapability @experimental object unsafeExceptions: - given canThrowAny: CanThrow[Exception] = compiletime.erasedValue + inline given canThrowAny: CanThrow[Exception] = compiletime.erasedValue diff --git a/tests/init/warn/inner30.scala b/tests/init/warn/inner30.scala index d9b1eec3d6b1..1fd112579263 100644 --- a/tests/init/warn/inner30.scala +++ b/tests/init/warn/inner30.scala @@ -8,7 +8,7 @@ class Scanners { class Scanner { def foo() = - Conc(Run('a', 3), Run('b', 4)) + Conc(Run('a', 3), Run('b', 4)) // warn new LookAheadScanner class LookAheadScanner() extends Scanner From 0dd97430baf1906db61613040c42abf2aadd1223 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 24 Jun 2025 14:37:58 +0200 Subject: [PATCH 07/19] Add variants of compiletime.erasedValue --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 8 +++----- .../src/dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- .../src/dotty/tools/dotc/core/Definitions.scala | 4 +++- .../src/dotty/tools/dotc/typer/Synthesizer.scala | 4 ++-- compiler/src/dotty/tools/dotc/typer/Typer.scala | 2 +- library/src/scala/caps/package.scala | 14 ++++++++++++++ 6 files changed, 24 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 2f5c59c11071..d21e9c7d5064 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -495,15 +495,13 @@ extension (sym: Symbol) /** Does this symbol allow results carrying the universal capability? * Currently this is true only for function type applies (since their - * results are unboxed) and `erasedValue` since this function is magic in - * that is allows to conjure global capabilies from nothing (aside: can we find a - * more controlled way to achieve this?). + * results are unboxed) and `caps.{$internal,unsafe}.erasedValue` since + * these function are magic in that they allow to conjure global capabilies from nothing. * But it could be generalized to other functions that so that they can take capability * classes as arguments. */ def allowsRootCapture(using Context): Boolean = - sym == defn.Compiletime_erasedValue - || defn.isFunctionClass(sym.maybeOwner) + defn.capsErasedValueMethods.contains(sym) || defn.isFunctionClass(sym.maybeOwner) /** When applying `sym`, would the result type be unboxed? * This is the case if the result type contains a top-level reference to an enclosing diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index dccbd0a005d7..e8cc0eb69528 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -576,7 +576,7 @@ class CheckCaptures extends Recheck, SymTransformer: * @param args the type arguments */ def disallowCapInTypeArgs(fn: Tree, sym: Symbol, args: List[Tree])(using Context): Unit = - def isExempt = sym.isTypeTestOrCast || sym == defn.Compiletime_erasedValue + def isExempt = sym.isTypeTestOrCast || defn.capsErasedValueMethods.contains(sym) if !isExempt then val paramNames = atPhase(thisPhase.prev): fn.tpe.widenDealias match diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index e2e15599ba52..3aaf966597c3 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1004,9 +1004,11 @@ class Definitions { @tu lazy val Caps_Capability: ClassSymbol = requiredClass("scala.caps.Capability") @tu lazy val Caps_CapSet: ClassSymbol = requiredClass("scala.caps.CapSet") @tu lazy val CapsInternalModule: Symbol = requiredModule("scala.caps.internal") + @tu lazy val Caps_erasedValue: Symbol = CapsInternalModule.requiredMethod("erasedValue") @tu lazy val CapsUnsafeModule: Symbol = requiredModule("scala.caps.unsafe") @tu lazy val Caps_unsafeAssumePure: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumePure") @tu lazy val Caps_unsafeAssumeSeparate: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumeSeparate") + @tu lazy val Caps_unsafeErasedValue: Symbol = CapsUnsafeModule.requiredMethod("unsafeErasedValue") @tu lazy val Caps_ContainsTrait: TypeSymbol = CapsModule.requiredType("Contains") @tu lazy val Caps_ContainsModule: Symbol = requiredModule("scala.caps.Contains") @tu lazy val Caps_containsImpl: TermSymbol = Caps_ContainsModule.requiredMethod("containsImpl") @@ -1559,7 +1561,7 @@ class Definitions { Set(StringClass, NothingClass, NullClass) ++ ScalaValueClasses() @tu lazy val capsErasedValueMethods = - Set[Symbol]() + Set(Caps_erasedValue, Caps_unsafeErasedValue) @tu lazy val erasedValueMethods = capsErasedValueMethods + Compiletime_erasedValue diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index 761a24e10474..51cbd8df7c15 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -231,7 +231,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): case PreciseConstrained(tp, true) => if tp.isSingletonBounded(frozen = false) then withNoErrors: - ref(defn.Compiletime_erasedValue).appliedToType(formal).withSpan(span) + ref(defn.Caps_erasedValue).appliedToType(formal).withSpan(span) else withErrors(i"$tp is not a singleton") case _ => @@ -240,7 +240,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): val synthesizedPrecise: SpecialHandler = (formal, span) => formal match case PreciseConstrained(tp, false) => withNoErrors: - ref(defn.Compiletime_erasedValue).appliedToType(formal).withSpan(span) + ref(defn.Caps_erasedValue).appliedToType(formal).withSpan(span) case _ => EmptyTreeNoError diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index d65c2765e0cf..786c2ae47302 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2395,7 +2395,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer untpd.ValDef( CanThrowEvidenceName.fresh(), untpd.TypeTree(defn.CanThrowClass.typeRef.appliedTo(tp)), - untpd.ref(defn.Compiletime_erasedValue)) + untpd.ref(defn.Caps_erasedValue)) .withFlags(Given | Final | Erased) .withSpan(expr.span) val caughtExceptions = diff --git a/library/src/scala/caps/package.scala b/library/src/scala/caps/package.scala index fedfd7400e25..98df6ce5219c 100644 --- a/library/src/scala/caps/package.scala +++ b/library/src/scala/caps/package.scala @@ -110,6 +110,13 @@ object internal: */ final class inferredDepFun extends annotation.StaticAnnotation + /** An erasedValue issued internally by the compiler. Unlike the + * user-accessible compiletime.erasedValue, this version is assumed + * to be a pure expression, hence capability safe. The compiler generates this + * version only where it is known that a value can be generated. + */ + def erasedValue[T]: T = ??? + end internal @experimental @@ -135,4 +142,11 @@ object unsafe: */ def unsafeAssumeSeparate(op: Any): op.type = op + /** An unsafe variant of erasedValue that can be used as an escape hatch. Unlike the + * user-accessible compiletime.erasedValue, this version is assumed + * to be a pure expression, hence capability safe. But there is no proof + * of realizability, hence it is unsafe. + */ + def unsafeErasedValue[T]: T = ??? + end unsafe From 9c8d0d256f03d52e314e6f1bbe59e781f723c0d4 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 24 Jun 2025 15:00:39 +0200 Subject: [PATCH 08/19] Require erased values to be pure expressions --- .../tools/dotc/reporting/ErrorMessageID.scala | 1 + .../dotty/tools/dotc/reporting/messages.scala | 32 ++++++++++++++ .../dotty/tools/dotc/transform/Erasure.scala | 26 +++++------- library/src/scala/CanThrow.scala | 2 +- tests/{ => invalid}/run/erased-1.check | 0 tests/{ => invalid}/run/erased-1.scala | 0 tests/{ => invalid}/run/erased-10.check | 0 tests/{ => invalid}/run/erased-10.scala | 0 tests/{ => invalid}/run/erased-11.check | 0 tests/{ => invalid}/run/erased-11.scala | 0 tests/{ => invalid}/run/erased-12.check | 0 tests/{ => invalid}/run/erased-12.scala | 0 tests/{ => invalid}/run/erased-13.check | 0 tests/{ => invalid}/run/erased-13.scala | 0 tests/{ => invalid}/run/erased-14.check | 0 tests/{ => invalid}/run/erased-14.scala | 0 tests/{ => invalid}/run/erased-16.check | 0 tests/{ => invalid}/run/erased-16.scala | 0 tests/{ => invalid}/run/erased-17.check | 0 tests/{ => invalid}/run/erased-17.scala | 0 tests/{ => invalid}/run/erased-22.check | 0 tests/{ => invalid}/run/erased-22.scala | 0 tests/{ => invalid}/run/erased-27.check | 0 tests/{ => invalid}/run/erased-27.scala | 0 tests/{ => invalid}/run/erased-28.check | 0 tests/{ => invalid}/run/erased-28.scala | 0 tests/{ => invalid}/run/erased-3.check | 0 tests/{ => invalid}/run/erased-3.scala | 0 tests/{ => invalid}/run/erased-4.check | 0 tests/{ => invalid}/run/erased-4.scala | 0 tests/{ => invalid}/run/erased-5.check | 0 tests/{ => invalid}/run/erased-5.scala | 0 tests/{ => invalid}/run/erased-6.check | 0 tests/{ => invalid}/run/erased-6.scala | 0 tests/{ => invalid}/run/erased-8.check | 0 tests/{ => invalid}/run/erased-8.scala | 0 tests/{ => invalid}/run/erased-9.check | 0 tests/{ => invalid}/run/erased-9.scala | 0 .../run/erased-class-are-erased.check | 0 .../run/erased-class-are-erased.scala | 0 .../{ => invalid}/run/erased-frameless.check | 0 .../{ => invalid}/run/erased-frameless.scala | 8 ++-- .../run/erased-select-prefix.check | 0 .../run/erased-select-prefix.scala | 0 .../run/erased-value-class.check | 0 .../run/erased-value-class.scala | 0 .../run/polymorphic-erased-functions.scala | 0 tests/neg/erased-6.scala | 2 +- tests/neg/erased-class.scala | 8 ++-- tests/neg/erasedValueb.check | 10 +++++ tests/neg/erasedValueb.scala | 2 +- tests/pos-custom-args/captures/try.scala | 2 +- tests/pos/erased-asInstanceOf.scala | 2 +- tests/pos/erased-conforms.scala | 2 +- tests/pos/erased-pure.scala | 7 +++- tests/pos/i11864.scala | 2 +- tests/pos/i11896.scala | 2 +- tests/pos/i5938.scala | 3 +- tests/pos/i7868.scala | 42 ------------------- tests/pos/matchtype.scala | 2 +- tests/pos/phantom-Evidence.scala | 9 ++-- tests/run/erased-18.scala | 4 +- tests/run/erased-machine-state.scala | 6 +-- tests/run/erased-poly-ref.scala | 5 +-- tests/run/i11996.scala | 2 +- tests/run/i16943.scala | 2 +- 66 files changed, 90 insertions(+), 93 deletions(-) rename tests/{ => invalid}/run/erased-1.check (100%) rename tests/{ => invalid}/run/erased-1.scala (100%) rename tests/{ => invalid}/run/erased-10.check (100%) rename tests/{ => invalid}/run/erased-10.scala (100%) rename tests/{ => invalid}/run/erased-11.check (100%) rename tests/{ => invalid}/run/erased-11.scala (100%) rename tests/{ => invalid}/run/erased-12.check (100%) rename tests/{ => invalid}/run/erased-12.scala (100%) rename tests/{ => invalid}/run/erased-13.check (100%) rename tests/{ => invalid}/run/erased-13.scala (100%) rename tests/{ => invalid}/run/erased-14.check (100%) rename tests/{ => invalid}/run/erased-14.scala (100%) rename tests/{ => invalid}/run/erased-16.check (100%) rename tests/{ => invalid}/run/erased-16.scala (100%) rename tests/{ => invalid}/run/erased-17.check (100%) rename tests/{ => invalid}/run/erased-17.scala (100%) rename tests/{ => invalid}/run/erased-22.check (100%) rename tests/{ => invalid}/run/erased-22.scala (100%) rename tests/{ => invalid}/run/erased-27.check (100%) rename tests/{ => invalid}/run/erased-27.scala (100%) rename tests/{ => invalid}/run/erased-28.check (100%) rename tests/{ => invalid}/run/erased-28.scala (100%) rename tests/{ => invalid}/run/erased-3.check (100%) rename tests/{ => invalid}/run/erased-3.scala (100%) rename tests/{ => invalid}/run/erased-4.check (100%) rename tests/{ => invalid}/run/erased-4.scala (100%) rename tests/{ => invalid}/run/erased-5.check (100%) rename tests/{ => invalid}/run/erased-5.scala (100%) rename tests/{ => invalid}/run/erased-6.check (100%) rename tests/{ => invalid}/run/erased-6.scala (100%) rename tests/{ => invalid}/run/erased-8.check (100%) rename tests/{ => invalid}/run/erased-8.scala (100%) rename tests/{ => invalid}/run/erased-9.check (100%) rename tests/{ => invalid}/run/erased-9.scala (100%) rename tests/{ => invalid}/run/erased-class-are-erased.check (100%) rename tests/{ => invalid}/run/erased-class-are-erased.scala (100%) rename tests/{ => invalid}/run/erased-frameless.check (100%) rename tests/{ => invalid}/run/erased-frameless.scala (88%) rename tests/{ => invalid}/run/erased-select-prefix.check (100%) rename tests/{ => invalid}/run/erased-select-prefix.scala (100%) rename tests/{ => invalid}/run/erased-value-class.check (100%) rename tests/{ => invalid}/run/erased-value-class.scala (100%) rename tests/{ => invalid}/run/polymorphic-erased-functions.scala (100%) create mode 100644 tests/neg/erasedValueb.check delete mode 100644 tests/pos/i7868.scala diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index 96942f913934..99e1435d0e65 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -229,6 +229,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case PointlessAppliedConstructorTypeID // errorNumber: 213 case IllegalContextBoundsID // errorNumber: 214 case NamedPatternNotApplicableID // errorNumber: 215 + case ErasedNotPureID // errornumber 216 def errorNumber = ordinal - 1 diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 4974884f8286..4efd5e703834 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -3587,3 +3587,35 @@ final class NamedPatternNotApplicable(selectorType: Type)(using Context) extends i"Named patterns cannot be used with $selectorType, because it is not a named tuple or case class" override protected def explain(using Context): String = "" + +final class ErasedNotPure(tree: tpd.Tree, isArgument: Boolean, isImplicit: Boolean)(using Context) extends TypeMsg(ErasedNotPureID): + def what = + if isArgument then s"${if isImplicit then "implicit " else ""}argument to an erased parameter" + else "right-hand-side of an erased value" + override protected def msg(using Context): String = + i"$what fails to be a pure expression" + + override protected def explain(using Context): String = + def alternatives = + if tree.symbol == defn.Compiletime_erasedValue then + i"""An accepted (but unsafe) alternative for this expression uses function + | + | caps.unsafe.unsafeErasedValue + | + |instead.""" + else + """A pure expression is an expression that is clearly side-effect free and terminating. + |Some examples of pure expressions are: + | - literals, + | - references to values, + | - side-effect-free instance creations, + | - applications of inline functions to pure arguments.""" + + i"""The $what must be a pure expression, but I found: + | + | $tree + | + |This expression is not classified to be pure. + |$alternatives""" +end ErasedNotPure + diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index f2798313fcef..d0a89c9564f4 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -14,7 +14,7 @@ import core.Names.* import core.StdNames.* import core.NameOps.* import core.Periods.currentStablePeriod -import core.NameKinds.{AdaptedClosureName, BodyRetainerName, DirectMethName} +import core.NameKinds.{AdaptedClosureName, BodyRetainerName, DirectMethName, InlineScrutineeName} import core.Scopes.newScopeWith import core.Decorators.* import core.Constants.* @@ -583,16 +583,11 @@ object Erasure { checkNotErasedClass(tree) end checkNotErased - def checkPureErased(tree: untpd.Tree, isArgument: Boolean)(using Context): Unit = - if false then inContext(preErasureCtx): - if tpd.isPureExpr(tree.asInstanceOf[tpd.Tree]) then - val tree1 = tree.asInstanceOf[tpd.Tree] - println(i"$tree1 is pure, ${tree1.tpe.widen}") - else - def what = - if isArgument then "argument to erased parameter" - else "right-hand-side of erased value" - report.error(em"$what fails to be a pure expression", tree.srcPos) + def checkPureErased(tree: untpd.Tree, isArgument: Boolean, isImplicit: Boolean = false)(using Context): Unit = + val tree1 = tree.asInstanceOf[tpd.Tree] + inContext(preErasureCtx): + if !tpd.isPureExpr(tree1) then + report.error(ErasedNotPure(tree1, isArgument, isImplicit), tree1.srcPos) private def checkNotErasedClass(tp: Type, tree: untpd.Tree)(using Context): Unit = tp match case JavaArrayType(et) => @@ -861,7 +856,8 @@ object Erasure { case mt: MethodType if mt.hasErasedParams => args.lazyZip(mt.paramErasureStatuses).flatMap: (arg, isErased) => if isErased then - checkPureErased(arg, isArgument = true) + checkPureErased(arg, isArgument = true, + isImplicit = mt.isImplicitMethod && arg.span.isSynthetic) Nil else arg :: Nil @@ -933,9 +929,10 @@ object Erasure { override def typedValDef(vdef: untpd.ValDef, sym: Symbol)(using Context): Tree = if sym.isEffectivelyErased then - checkPureErased(vdef.rhs, isArgument = false) + if !sym.name.is(InlineScrutineeName) then + checkPureErased(vdef.rhs, isArgument = false) erasedDef(sym) - else + else trace(i"erasing $vdef"): checkNotErasedClass(sym.info, vdef) super.typedValDef(untpd.cpy.ValDef(vdef)( tpt = untpd.TypedSplice(TypeTree(sym.info).withSpan(vdef.tpt.span))), sym) @@ -946,7 +943,6 @@ object Erasure { */ override def typedDefDef(ddef: untpd.DefDef, sym: Symbol)(using Context): Tree = if sym.isEffectivelyErased || sym.name.is(BodyRetainerName) then - checkPureErased(ddef.rhs, isArgument = false) erasedDef(sym) else checkNotErasedClass(sym.info.finalResultType, ddef) diff --git a/library/src/scala/CanThrow.scala b/library/src/scala/CanThrow.scala index e87d14671419..46164d0133ea 100644 --- a/library/src/scala/CanThrow.scala +++ b/library/src/scala/CanThrow.scala @@ -12,5 +12,5 @@ erased class CanThrow[-E <: Exception] extends caps.SharedCapability @experimental object unsafeExceptions: - inline given canThrowAny: CanThrow[Exception] = compiletime.erasedValue + inline given canThrowAny: CanThrow[Exception] = caps.unsafe.unsafeErasedValue diff --git a/tests/run/erased-1.check b/tests/invalid/run/erased-1.check similarity index 100% rename from tests/run/erased-1.check rename to tests/invalid/run/erased-1.check diff --git a/tests/run/erased-1.scala b/tests/invalid/run/erased-1.scala similarity index 100% rename from tests/run/erased-1.scala rename to tests/invalid/run/erased-1.scala diff --git a/tests/run/erased-10.check b/tests/invalid/run/erased-10.check similarity index 100% rename from tests/run/erased-10.check rename to tests/invalid/run/erased-10.check diff --git a/tests/run/erased-10.scala b/tests/invalid/run/erased-10.scala similarity index 100% rename from tests/run/erased-10.scala rename to tests/invalid/run/erased-10.scala diff --git a/tests/run/erased-11.check b/tests/invalid/run/erased-11.check similarity index 100% rename from tests/run/erased-11.check rename to tests/invalid/run/erased-11.check diff --git a/tests/run/erased-11.scala b/tests/invalid/run/erased-11.scala similarity index 100% rename from tests/run/erased-11.scala rename to tests/invalid/run/erased-11.scala diff --git a/tests/run/erased-12.check b/tests/invalid/run/erased-12.check similarity index 100% rename from tests/run/erased-12.check rename to tests/invalid/run/erased-12.check diff --git a/tests/run/erased-12.scala b/tests/invalid/run/erased-12.scala similarity index 100% rename from tests/run/erased-12.scala rename to tests/invalid/run/erased-12.scala diff --git a/tests/run/erased-13.check b/tests/invalid/run/erased-13.check similarity index 100% rename from tests/run/erased-13.check rename to tests/invalid/run/erased-13.check diff --git a/tests/run/erased-13.scala b/tests/invalid/run/erased-13.scala similarity index 100% rename from tests/run/erased-13.scala rename to tests/invalid/run/erased-13.scala diff --git a/tests/run/erased-14.check b/tests/invalid/run/erased-14.check similarity index 100% rename from tests/run/erased-14.check rename to tests/invalid/run/erased-14.check diff --git a/tests/run/erased-14.scala b/tests/invalid/run/erased-14.scala similarity index 100% rename from tests/run/erased-14.scala rename to tests/invalid/run/erased-14.scala diff --git a/tests/run/erased-16.check b/tests/invalid/run/erased-16.check similarity index 100% rename from tests/run/erased-16.check rename to tests/invalid/run/erased-16.check diff --git a/tests/run/erased-16.scala b/tests/invalid/run/erased-16.scala similarity index 100% rename from tests/run/erased-16.scala rename to tests/invalid/run/erased-16.scala diff --git a/tests/run/erased-17.check b/tests/invalid/run/erased-17.check similarity index 100% rename from tests/run/erased-17.check rename to tests/invalid/run/erased-17.check diff --git a/tests/run/erased-17.scala b/tests/invalid/run/erased-17.scala similarity index 100% rename from tests/run/erased-17.scala rename to tests/invalid/run/erased-17.scala diff --git a/tests/run/erased-22.check b/tests/invalid/run/erased-22.check similarity index 100% rename from tests/run/erased-22.check rename to tests/invalid/run/erased-22.check diff --git a/tests/run/erased-22.scala b/tests/invalid/run/erased-22.scala similarity index 100% rename from tests/run/erased-22.scala rename to tests/invalid/run/erased-22.scala diff --git a/tests/run/erased-27.check b/tests/invalid/run/erased-27.check similarity index 100% rename from tests/run/erased-27.check rename to tests/invalid/run/erased-27.check diff --git a/tests/run/erased-27.scala b/tests/invalid/run/erased-27.scala similarity index 100% rename from tests/run/erased-27.scala rename to tests/invalid/run/erased-27.scala diff --git a/tests/run/erased-28.check b/tests/invalid/run/erased-28.check similarity index 100% rename from tests/run/erased-28.check rename to tests/invalid/run/erased-28.check diff --git a/tests/run/erased-28.scala b/tests/invalid/run/erased-28.scala similarity index 100% rename from tests/run/erased-28.scala rename to tests/invalid/run/erased-28.scala diff --git a/tests/run/erased-3.check b/tests/invalid/run/erased-3.check similarity index 100% rename from tests/run/erased-3.check rename to tests/invalid/run/erased-3.check diff --git a/tests/run/erased-3.scala b/tests/invalid/run/erased-3.scala similarity index 100% rename from tests/run/erased-3.scala rename to tests/invalid/run/erased-3.scala diff --git a/tests/run/erased-4.check b/tests/invalid/run/erased-4.check similarity index 100% rename from tests/run/erased-4.check rename to tests/invalid/run/erased-4.check diff --git a/tests/run/erased-4.scala b/tests/invalid/run/erased-4.scala similarity index 100% rename from tests/run/erased-4.scala rename to tests/invalid/run/erased-4.scala diff --git a/tests/run/erased-5.check b/tests/invalid/run/erased-5.check similarity index 100% rename from tests/run/erased-5.check rename to tests/invalid/run/erased-5.check diff --git a/tests/run/erased-5.scala b/tests/invalid/run/erased-5.scala similarity index 100% rename from tests/run/erased-5.scala rename to tests/invalid/run/erased-5.scala diff --git a/tests/run/erased-6.check b/tests/invalid/run/erased-6.check similarity index 100% rename from tests/run/erased-6.check rename to tests/invalid/run/erased-6.check diff --git a/tests/run/erased-6.scala b/tests/invalid/run/erased-6.scala similarity index 100% rename from tests/run/erased-6.scala rename to tests/invalid/run/erased-6.scala diff --git a/tests/run/erased-8.check b/tests/invalid/run/erased-8.check similarity index 100% rename from tests/run/erased-8.check rename to tests/invalid/run/erased-8.check diff --git a/tests/run/erased-8.scala b/tests/invalid/run/erased-8.scala similarity index 100% rename from tests/run/erased-8.scala rename to tests/invalid/run/erased-8.scala diff --git a/tests/run/erased-9.check b/tests/invalid/run/erased-9.check similarity index 100% rename from tests/run/erased-9.check rename to tests/invalid/run/erased-9.check diff --git a/tests/run/erased-9.scala b/tests/invalid/run/erased-9.scala similarity index 100% rename from tests/run/erased-9.scala rename to tests/invalid/run/erased-9.scala diff --git a/tests/run/erased-class-are-erased.check b/tests/invalid/run/erased-class-are-erased.check similarity index 100% rename from tests/run/erased-class-are-erased.check rename to tests/invalid/run/erased-class-are-erased.check diff --git a/tests/run/erased-class-are-erased.scala b/tests/invalid/run/erased-class-are-erased.scala similarity index 100% rename from tests/run/erased-class-are-erased.scala rename to tests/invalid/run/erased-class-are-erased.scala diff --git a/tests/run/erased-frameless.check b/tests/invalid/run/erased-frameless.check similarity index 100% rename from tests/run/erased-frameless.check rename to tests/invalid/run/erased-frameless.check diff --git a/tests/run/erased-frameless.scala b/tests/invalid/run/erased-frameless.scala similarity index 88% rename from tests/run/erased-frameless.scala rename to tests/invalid/run/erased-frameless.scala index fe654639492a..a366e705840c 100644 --- a/tests/run/erased-frameless.scala +++ b/tests/invalid/run/erased-frameless.scala @@ -28,7 +28,7 @@ trait Dataset[T] { // Use c.label to do an untyped select on actual Spark Dataset, and // cast the result to TypedDataset[A] - def col[S <: String, A](s: S) (using erased ev: Exists[T, s.type, A]) = + inline def col[S <: String, A](s: S) (using erased ev: Exists[T, s.type, A]) = new Column[T, A](s) // ev is only here to check than this is safe, it's never used at runtime! def collect(): Vector[T] @@ -71,17 +71,17 @@ case class Column[T, A](label: String) trait Exists[T, K, V] object Exists { - implicit def derive[T, H <: HList, K, V](implicit g: LabelledGeneric[T] { type Repr = H }, s: Selector[H, K, V]): Exists[T, K, V] = { + inline implicit def derive[T, H <: HList, K, V](implicit g: LabelledGeneric[T] { type Repr = H }, s: Selector[H, K, V]): Exists[T, K, V] = { println("Exists.derive") null } - implicit def caseFound[T <: HList, K <: String, V]: Selector[R[K, V] :: T, K, V] = { + inline implicit def caseFound[T <: HList, K <: String, V]: Selector[R[K, V] :: T, K, V] = { println("Selector.caseFound") null } - implicit def caseRecur[H, T <: HList, K <: String, V](implicit i: Selector[T, K, V]): Selector[H :: T, K, V] = { + inline implicit def caseRecur[H, T <: HList, K <: String, V](implicit i: Selector[T, K, V]): Selector[H :: T, K, V] = { println("Selector.caseRecur") null } diff --git a/tests/run/erased-select-prefix.check b/tests/invalid/run/erased-select-prefix.check similarity index 100% rename from tests/run/erased-select-prefix.check rename to tests/invalid/run/erased-select-prefix.check diff --git a/tests/run/erased-select-prefix.scala b/tests/invalid/run/erased-select-prefix.scala similarity index 100% rename from tests/run/erased-select-prefix.scala rename to tests/invalid/run/erased-select-prefix.scala diff --git a/tests/run/erased-value-class.check b/tests/invalid/run/erased-value-class.check similarity index 100% rename from tests/run/erased-value-class.check rename to tests/invalid/run/erased-value-class.check diff --git a/tests/run/erased-value-class.scala b/tests/invalid/run/erased-value-class.scala similarity index 100% rename from tests/run/erased-value-class.scala rename to tests/invalid/run/erased-value-class.scala diff --git a/tests/run/polymorphic-erased-functions.scala b/tests/invalid/run/polymorphic-erased-functions.scala similarity index 100% rename from tests/run/polymorphic-erased-functions.scala rename to tests/invalid/run/polymorphic-erased-functions.scala diff --git a/tests/neg/erased-6.scala b/tests/neg/erased-6.scala index 529fb5b2dd88..76fa1b937f00 100644 --- a/tests/neg/erased-6.scala +++ b/tests/neg/erased-6.scala @@ -1,7 +1,7 @@ //> using options -language:experimental.erasedDefinitions object Test { - erased val foo: Foo = new Foo + erased val foo: Foo = new Foo // error, Foo is not noInits foo.x() // error foo.y // error foo.z // error diff --git a/tests/neg/erased-class.scala b/tests/neg/erased-class.scala index 96a1c8769bb1..aede15b9ef70 100644 --- a/tests/neg/erased-class.scala +++ b/tests/neg/erased-class.scala @@ -4,7 +4,7 @@ erased class AA erased class BB extends AA // ok @main def Test = - val f1: Array[AA] = compiletime.erasedValue // error // error - def f2(x: Int): Array[AA] = compiletime.erasedValue // error // error - def bar: AA = compiletime.erasedValue // ok - val baz: AA = compiletime.erasedValue // ok + val f1: Array[AA] = caps.unsafe.unsafeErasedValue // error // error + def f2(x: Int): Array[AA] = caps.unsafe.unsafeErasedValue // error // error + def bar: AA = caps.unsafe.unsafeErasedValue // ok + val baz: AA = caps.unsafe.unsafeErasedValue // ok diff --git a/tests/neg/erasedValueb.check b/tests/neg/erasedValueb.check new file mode 100644 index 000000000000..c38b5feba33d --- /dev/null +++ b/tests/neg/erasedValueb.check @@ -0,0 +1,10 @@ +-- Error: tests/neg/erasedValueb.scala:7:7 ----------------------------------------------------------------------------- +7 | foo0(erasedValue[Int]) // error + | ^^^^^^^^^^^ + | method erasedValue is declared as `erased`, but is in fact used +-- [E215] Type Error: tests/neg/erasedValueb.scala:8:18 ---------------------------------------------------------------- +8 | foo1(erasedValue[Int]) // error + | ^^^^^^^^^^^^^^^^ + | argument to an erased parameter fails to be a pure expression + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/erasedValueb.scala b/tests/neg/erasedValueb.scala index 5c1f1d359e93..a25cf66ec3cb 100644 --- a/tests/neg/erasedValueb.scala +++ b/tests/neg/erasedValueb.scala @@ -5,5 +5,5 @@ object Test { def foo0(a: Int): Int = 3 def foo1(erased a: Int): Int = 3 foo0(erasedValue[Int]) // error - foo1(erasedValue[Int]) + foo1(erasedValue[Int]) // error } diff --git a/tests/pos-custom-args/captures/try.scala b/tests/pos-custom-args/captures/try.scala index 5faabecc411c..c88c842babc5 100644 --- a/tests/pos-custom-args/captures/try.scala +++ b/tests/pos-custom-args/captures/try.scala @@ -14,7 +14,7 @@ def foo(x: Boolean): Int throws Fail = if x then 1 else raise(Fail()) def handle[E <: Exception, R](op: (erased CanThrow[E]) -> R)(handler: E -> R): R = - erased val x: CanThrow[E] = ??? : CanThrow[E] + erased val x = caps.unsafe.unsafeErasedValue[CanThrow[E]] try op(x) catch case ex: E => handler(ex) diff --git a/tests/pos/erased-asInstanceOf.scala b/tests/pos/erased-asInstanceOf.scala index 692ff3a16b05..7029c298452c 100644 --- a/tests/pos/erased-asInstanceOf.scala +++ b/tests/pos/erased-asInstanceOf.scala @@ -11,7 +11,7 @@ object Test { val ds: Dataset = ??? - lazy val collD = new Column + val collD = new Column ds.select(collD) diff --git a/tests/pos/erased-conforms.scala b/tests/pos/erased-conforms.scala index 1b3d06d4e261..311b244b9872 100644 --- a/tests/pos/erased-conforms.scala +++ b/tests/pos/erased-conforms.scala @@ -5,7 +5,7 @@ erased class <::<[-From, +To] extends ErasedTerm erased class =::=[From, To] extends (From <::< To) -inline given [X] => (X =::= X) = scala.compiletime.erasedValue +inline given [X] => (X =::= X) = scala.caps.unsafe.unsafeErasedValue extension [From](x: From) inline def cast[To](using From <::< To): To = x.asInstanceOf[To] // Safe cast because we know `From <:< To` diff --git a/tests/pos/erased-pure.scala b/tests/pos/erased-pure.scala index 9d2b54ac02b4..e62563669e66 100644 --- a/tests/pos/erased-pure.scala +++ b/tests/pos/erased-pure.scala @@ -1,4 +1,5 @@ import language.experimental.erasedDefinitions +import caps.unsafe.unsafeErasedValue inline def id[T](x: T) = x @@ -8,6 +9,7 @@ def foo[T](erased x: T): Unit = () class Pair[A, B](x: A, y: B) +case class Pair2[A, B](x: A, y: B) def Test = foo(C()) @@ -17,7 +19,8 @@ def Test = foo(Pair(C(), "hello" + "world")) foo(id(Pair(id(C()), id("hello" + "world")))) - - + //erased val x1 = Pair(unsafeErasedValue[Int], unsafeErasedValue[String]) + //erased val x2 = Pair2(unsafeErasedValue[Int], unsafeErasedValue[String]) + erased val x3 = Tuple2(unsafeErasedValue[Int], unsafeErasedValue[String]) diff --git a/tests/pos/i11864.scala b/tests/pos/i11864.scala index 3c4c1004880a..0301b50d7021 100644 --- a/tests/pos/i11864.scala +++ b/tests/pos/i11864.scala @@ -40,7 +40,7 @@ final class CallbackTo[+A] { object CallbackTo { type MapGuard[A] = { type Out = A } - inline given MapGuard: [A] => MapGuard[A] = compiletime.erasedValue + inline given MapGuard: [A] => MapGuard[A] = caps.unsafe.unsafeErasedValue def traverse[A, B](ta: List[A]): CallbackTo[List[B]] = val x: CallbackTo[List[A] => List[B]] = ??? diff --git a/tests/pos/i11896.scala b/tests/pos/i11896.scala index e9045ffe605f..a4816eb5ad18 100644 --- a/tests/pos/i11896.scala +++ b/tests/pos/i11896.scala @@ -1,7 +1,7 @@ import scala.language.experimental.erasedDefinitions type X -inline def x: X = compiletime.erasedValue +inline def x: X = caps.unsafe.unsafeErasedValue def foo(using erased X): Unit = () diff --git a/tests/pos/i5938.scala b/tests/pos/i5938.scala index 17a20dcd0f1f..f392de153b4c 100644 --- a/tests/pos/i5938.scala +++ b/tests/pos/i5938.scala @@ -1,7 +1,6 @@ import scala.language.experimental.erasedDefinitions import compiletime.summonFrom -import compiletime.erasedValue trait Link[T, A] @@ -15,7 +14,7 @@ transparent inline def link[T] = class Foo object Foo { - erased implicit val barLink: Link[Foo, Bar.type] = erasedValue + erased implicit val barLink: Link[Foo, Bar.type] = caps.unsafe.unsafeErasedValue } implicit object Bar { diff --git a/tests/pos/i7868.scala b/tests/pos/i7868.scala deleted file mode 100644 index fa31bd131b0c..000000000000 --- a/tests/pos/i7868.scala +++ /dev/null @@ -1,42 +0,0 @@ -//> using options -language:experimental.erasedDefinitions - -import language.experimental.namedTypeArguments -import scala.compiletime.* -import scala.compiletime.ops.int.* - -final case class Coproduct[+Set, +Value, Index <: Int](value: Value & Set, index: Index) - -object Coproduct { - opaque type +:[+A, +B] = A | B - - trait At[+Set, -Value, Index <: Int] { - def cast: Value <:< Set - } - - object At { - - given atHead: [Head, Tail] => At[Head +: Tail, Head, 0]: - def cast: Head <:< Head +: Tail = summon[Head <:< Head +: Tail] - - given atTail[Head, Tail, Value, NextIndex <: Int] - (using atNext: At[Tail, Value, NextIndex]) - : At[Head +: Tail, Value, S[NextIndex]] with - val cast: Value <:< Head +: Tail = atNext.cast - - given [A] => A => (() => A) = { () => summon[A] } - } - - def upCast[A, B](a: A)(using erased evidence: (A <:< B) ): B = a.asInstanceOf[B] - - def from[Set, Value, Index <: Int](value: Value)(using erased at: At[Set, Value, Index]) : ValueOf[Index] ?=> Coproduct[Set, Value, Index] = { - Coproduct[Set, Value, Index](upCast(value: Value)(using at.cast.liftCo[[X] =>> Value & X]), valueOf[Index]) - } - -} - -object Test extends App { - import Coproduct.* - - // Error: No singleton value available for scala.compiletime.ops.int.S[scala.compiletime.ops.int.S[(0 : Int)]]. - val c = from[Set = Int +: String +: Seq[Double] +: Nothing](Nil) -} diff --git a/tests/pos/matchtype.scala b/tests/pos/matchtype.scala index 21c074deafd7..90d8f0dc6400 100644 --- a/tests/pos/matchtype.scala +++ b/tests/pos/matchtype.scala @@ -1,5 +1,5 @@ import scala.language.experimental.erasedDefinitions -import compiletime.erasedValue +import caps.unsafe.unsafeErasedValue as erasedValue import compiletime.ops.int.S object Test { type T[X] = X match { diff --git a/tests/pos/phantom-Evidence.scala b/tests/pos/phantom-Evidence.scala index c24ec477a2a8..db39a7b4659e 100644 --- a/tests/pos/phantom-Evidence.scala +++ b/tests/pos/phantom-Evidence.scala @@ -1,4 +1,5 @@ import scala.language.experimental.erasedDefinitions +import annotation.publicInBinary /** In this implementation variant of =:= (called =::=) we erase all instantiations and definitions of =::= */ object WithNormalState { @@ -11,9 +12,9 @@ object WithNormalState { object Instance { def newInstance(): Instance[Off] = new Instance[Off] } - class Instance[S <: State] private { - def getOnInstance (using erased ev: S =::= Off): Instance[On] = new Instance[On] // phantom parameter ev is erased - def getOffInstance (using erased ev: S =::= On): Instance[Off] = new Instance[Off] // phantom parameter ev is erased + class Instance[S <: State] @publicInBinary private { + inline def getOnInstance (using erased ev: S =::= Off): Instance[On] = new Instance[On] // phantom parameter ev is erased + inline def getOffInstance (using erased ev: S =::= On): Instance[Off] = new Instance[Off] // phantom parameter ev is erased } def run() = { @@ -26,5 +27,5 @@ object WithNormalState { object Utils { type =::=[From, To] - inline given tpEquals: [A] => (A =::= A) = compiletime.erasedValue + inline given tpEquals: [A] => (A =::= A) = caps.unsafe.unsafeErasedValue } diff --git a/tests/run/erased-18.scala b/tests/run/erased-18.scala index 46f7e44c7309..2e5275690ea2 100644 --- a/tests/run/erased-18.scala +++ b/tests/run/erased-18.scala @@ -11,8 +11,8 @@ object Test { )(foo) } - def foo = { - println("foo") + inline def foo = { + //println("foo") 42 } } diff --git a/tests/run/erased-machine-state.scala b/tests/run/erased-machine-state.scala index c84f1619366d..2431d34e9dfe 100644 --- a/tests/run/erased-machine-state.scala +++ b/tests/run/erased-machine-state.scala @@ -9,8 +9,7 @@ final class Off extends State @implicitNotFound("State must be Off") class IsOff[S <: State] object IsOff { - implicit def isOff: IsOff[Off] = { - println("isOff") + inline implicit def isOff: IsOff[Off] = { new IsOff[Off] } } @@ -18,8 +17,7 @@ object IsOff { @implicitNotFound("State must be On") class IsOn[S <: State] object IsOn { - implicit def isOn: IsOn[On] = { - println("isOn") + inline implicit def isOn: IsOn[On] = { new IsOn[On] } } diff --git a/tests/run/erased-poly-ref.scala b/tests/run/erased-poly-ref.scala index 59badb71255d..975a576cc15b 100644 --- a/tests/run/erased-poly-ref.scala +++ b/tests/run/erased-poly-ref.scala @@ -8,10 +8,9 @@ object Test { def fun(erased a: Int): Unit = println("fun") - def foo[P](erased x: Int)(erased y: Int): Int = 0 + inline def foo[P](erased x: Int)(erased y: Int): Int = 0 - def bar(x: Int) = { - println(x) + inline def bar(x: Int) = { x } } diff --git a/tests/run/i11996.scala b/tests/run/i11996.scala index 8769ca13eb88..a4318ace6c86 100644 --- a/tests/run/i11996.scala +++ b/tests/run/i11996.scala @@ -4,7 +4,7 @@ final class UnivEq[A] object UnivEq: inline def force[A]: UnivEq[A] = - compiletime.erasedValue + caps.unsafe.unsafeErasedValue extension [A](a: A) inline def ==*[B >: A](b: B)(using erased UnivEq[B]): Boolean = a == b diff --git a/tests/run/i16943.scala b/tests/run/i16943.scala index 68e1f8fb5aa3..697e9a2f38b7 100644 --- a/tests/run/i16943.scala +++ b/tests/run/i16943.scala @@ -1,6 +1,6 @@ @main @annotation.experimental -def Test(): Unit = fail(compiletime.erasedValue, 1) +def Test(): Unit = fail(caps.unsafe.unsafeErasedValue, 1) @annotation.experimental def fail(dumb: CanThrow[Exception], x: Int) = println(x) From 3f2c3f7c91e239288f794e8c1bbd82d2ee1f0f1b Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 24 Jun 2025 17:25:22 +0200 Subject: [PATCH 09/19] Some fixes to doc page --- .../reference/experimental/erased-defs.md | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/docs/_docs/reference/experimental/erased-defs.md b/docs/_docs/reference/experimental/erased-defs.md index d266cd6c9d19..4be4498e1058 100644 --- a/docs/_docs/reference/experimental/erased-defs.md +++ b/docs/_docs/reference/experimental/erased-defs.md @@ -4,7 +4,7 @@ title: "Erased Definitions" nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/erased-defs.html --- -`erased` is a modifier that expresses that some definition or expression is erased by the compiler instead of being represented in the compiled output. It is not yet part of the Scala language standard. To enable `erased`, turn on the language feature +`erased` is a modifier that expresses that some value or parameter is erased by the compiler instead of being represented in the compiled output. It is not yet part of the Scala language standard. To enable `erased`, turn on the language feature [`experimental.erasedDefinitions`](https://scala-lang.org/api/3.x/scala/runtime/stdLibPatches/language$$experimental$$erasedDefinitions$.html). This can be done with a language import ```scala import scala.language.experimental.erasedDefinitions @@ -74,9 +74,9 @@ def methodWithErasedInt2(erased i: Int): Int = methodWithErasedInt1(i) // OK ``` -Not only parameters can be marked as erased, `val` and `def` can also be marked -with `erased`. These will also only be usable as arguments to `erased` -parameters. +Besides parameters, `val` definitions can also be marked with `erased`. +These will also only be usable as arguments to `erased` parameters or +as part of the definitions of other erased `val`s. ```scala erased val erasedEvidence: Ev = ... @@ -85,7 +85,7 @@ methodWithErasedEv(erasedEvidence, 40) // 42 ## What happens with erased values at runtime? -As `erased` are guaranteed not to be used in computations, they can and will be +As `erased` vals and parameters are guaranteed not to be used in computations, they can and will be erased. ```scala @@ -130,7 +130,7 @@ class IsOff[S <: State] object IsOff: // will not be called at runtime for turnedOn, the // compiler will only require that this evidence exists - given IsOff[Off] = new IsOff[Off] + erased given IsOff[Off] = new IsOff[Off] @implicitNotFound("State must be On") class IsOn[S <: State] @@ -151,14 +151,7 @@ object Machine: val m = Machine.newMachine() m.turnedOn m.turnedOn.turnedOff - - // m.turnedOff - // ^ - // State must be On - - // m.turnedOn.turnedOn - // ^ - // State must be Off + m.turnedOn.turnedOn // error: State must be Off ``` Note that in [Compile-time operations](../metaprogramming/compiletime-ops.md#erasedvalue) we discussed `erasedValue` and inline From cd3f0fc8411e50cd770272961d5b7a3008b9f42c Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 25 Jun 2025 15:22:01 +0200 Subject: [PATCH 10/19] Update SemanticDB and scripting tests --- compiler/test-resources/type-printer/test-definitions | 5 ----- tests/semanticdb/metac.expect | 8 +++++++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/compiler/test-resources/type-printer/test-definitions b/compiler/test-resources/type-printer/test-definitions index cdda5f65cb0e..6566496d3181 100644 --- a/compiler/test-resources/type-printer/test-definitions +++ b/compiler/test-resources/type-printer/test-definitions @@ -18,8 +18,3 @@ scala> trait E scala> implicit def x: Int = 1 def x: Int - -scala> import scala.language.experimental.erasedDefinitions - -scala> erased def y: Int = 1 -def y: Int diff --git a/tests/semanticdb/metac.expect b/tests/semanticdb/metac.expect index 3f904b6bdda0..5f755e375ec3 100644 --- a/tests/semanticdb/metac.expect +++ b/tests/semanticdb/metac.expect @@ -2974,6 +2974,7 @@ Text => empty Language => Scala Symbols => 16 entries Occurrences => 12 entries +Diagnostics => 2 entries Symbols: example/NamedArguments# => class NamedArguments extends Object { self: NamedArguments => +4 decls } @@ -3007,6 +3008,10 @@ Occurrences: [5:7..5:12): apply -> example/NamedArguments#User.apply(). [5:13..5:17): name -> example/NamedArguments#User.apply().(name) +Diagnostics: +[4:2..4:21): [warning] A pure expression does nothing in statement position +[5:2..5:27): [warning] A pure expression does nothing in statement position + expect/NewModifiers.scala ------------------------- @@ -3654,7 +3659,7 @@ Text => empty Language => Scala Symbols => 62 entries Occurrences => 165 entries -Diagnostics => 3 entries +Diagnostics => 4 entries Synthetics => 39 entries Symbols: @@ -3890,6 +3895,7 @@ Occurrences: Diagnostics: [19:21..19:22): [warning] unused pattern variable +[28:4..28:9): [warning] A pure expression does nothing in statement position [41:4..41:5): [warning] unused pattern variable [63:10..63:11): [warning] unused explicit parameter From 1e89cca0121a9a6a4985e855318ac3a3c931a2a5 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 27 Jun 2025 14:04:41 +0200 Subject: [PATCH 11/19] Improve handling of compiletime.erasedValue --- .../src/dotty/tools/dotc/ast/TreeInfo.scala | 2 - .../src/dotty/tools/dotc/core/Flags.scala | 1 - .../tools/dotc/core/SymDenotations.scala | 8 ++- .../tools/dotc/inlines/InlineReducer.scala | 61 +++++++++++++++---- .../dotty/tools/dotc/transform/Erasure.scala | 5 +- tests/neg/i23406.check | 40 ++++++++++++ tests/neg/i23406.scala | 17 +++++- 7 files changed, 112 insertions(+), 22 deletions(-) create mode 100644 tests/neg/i23406.check diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 9debb8651be5..b71af156262f 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -667,8 +667,6 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => * or its type is a constant type * IdempotentPath if reference is lazy and stable * Impure otherwise - * @DarkDimius: need to make sure that lazy accessor methods have Lazy and Stable - * flags set. */ def refPurity(tree: Tree)(using Context): PurityLevel = { val sym = tree.symbol diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index c040d3e206b9..9fdc640d4d24 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -569,7 +569,6 @@ object Flags { val EnumCase: FlagSet = Case | Enum val CovariantLocal: FlagSet = Covariant | Local // A covariant type parameter val ContravariantLocal: FlagSet = Contravariant | Local // A contravariant type parameter - val EffectivelyErased = PhantomSymbol | Erased val ConstructorProxyModule: FlagSet = PhantomSymbol | Module val CaptureParam: FlagSet = PhantomSymbol | StableRealizable | Synthetic val DefaultParameter: FlagSet = HasDefault | Param // A Scala 2x default parameter diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 8155db9f01ef..2cac0a6d5b2f 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -1051,10 +1051,14 @@ object SymDenotations { && owner.ne(defn.StringContextClass) /** An erased value or an erased inline method or field */ + def isErased(using Context): Boolean = + is(Erased) || defn.erasedValueMethods.contains(symbol) + + /** An erased value, a phantom symbol or an erased inline method or field */ def isEffectivelyErased(using Context): Boolean = - isOneOf(EffectivelyErased) + isErased + || is(PhantomSymbol) || is(Inline) && !isRetainedInline && !hasAnnotation(defn.ScalaStaticAnnot) - || defn.erasedValueMethods.contains(symbol) /** Is this a member that will become public in the generated binary */ def hasPublicInBinary(using Context): Boolean = diff --git a/compiler/src/dotty/tools/dotc/inlines/InlineReducer.scala b/compiler/src/dotty/tools/dotc/inlines/InlineReducer.scala index 80516047426f..a1058b18e32c 100644 --- a/compiler/src/dotty/tools/dotc/inlines/InlineReducer.scala +++ b/compiler/src/dotty/tools/dotc/inlines/InlineReducer.scala @@ -172,6 +172,19 @@ class InlineReducer(inliner: Inliner)(using Context): val isImplicit = scrutinee.isEmpty + val unusable: util.EqHashSet[Symbol] = util.EqHashSet() + + /** Adjust internaly generated value definitions; + * - If the RHS refers to an erased symbol, mark the val as erased + * - If the RHS refers to an erased symbol, mark the val as unsuable + */ + def adjustErased(sym: TermSymbol, rhs: Tree): Unit = + rhs.foreachSubTree: + case id: Ident if id.symbol.isErased => + sym.setFlag(Erased) + if unusable.contains(id.symbol) then unusable += sym + case _ => + /** Try to match pattern `pat` against scrutinee reference `scrut`. If successful add * bindings for variables bound in this pattern to `caseBindingMap`. */ @@ -184,10 +197,11 @@ class InlineReducer(inliner: Inliner)(using Context): /** Create a binding of a pattern bound variable with matching part of * scrutinee as RHS and type that corresponds to RHS. */ - def newTermBinding(sym: TermSymbol, rhs: Tree): Unit = { - val copied = sym.copy(info = rhs.tpe.widenInlineScrutinee, coord = sym.coord, flags = sym.flags &~ Case).asTerm + def newTermBinding(sym: TermSymbol, rhs: Tree): Unit = + val copied = sym.copy(info = rhs.tpe.widenInlineScrutinee, coord = sym.coord, + flags = sym.flags &~ Case).asTerm + adjustErased(copied, rhs) caseBindingMap += ((sym, ValDef(copied, constToLiteral(rhs)).withSpan(sym.span))) - } def newTypeBinding(sym: TypeSymbol, alias: Type): Unit = { val copied = sym.copy(info = TypeAlias(alias), coord = sym.coord).asType @@ -306,6 +320,7 @@ class InlineReducer(inliner: Inliner)(using Context): case (Nil, Nil) => true case (pat :: pats1, selector :: selectors1) => val elem = newSym(InlineBinderName.fresh(), Synthetic, selector.tpe.widenInlineScrutinee).asTerm + adjustErased(elem, selector) val rhs = constToLiteral(selector) elem.defTree = rhs caseBindingMap += ((NoSymbol, ValDef(elem, rhs).withSpan(elem.span))) @@ -341,16 +356,18 @@ class InlineReducer(inliner: Inliner)(using Context): val scrutineeSym = newSym(InlineScrutineeName.fresh(), Synthetic, scrutType).asTerm val scrutineeBinding = normalizeBinding(ValDef(scrutineeSym, scrutinee)) - // If scrutinee has embedded `compiletime.erasedValue[T]` expressions, convert them to - // mark scrutineeSym as Erased. This means that the scrutinee cannot be referenced in - // the reduced term. It is NOT checked that scrutinee is a pure expression, since - // there is a special case in Erase that exempts the RHS of an erased scrutinee definition. - if scrutinee.existsSubTree: - case tree @ TypeApply(fn, args) => tree.symbol == defn.Compiletime_erasedValue - case _ => false - then + // If scrutinee has embedded references to `compiletime.erasedValue` or to + // other erased values, mark scrutineeSym as Erased. In addition, if scrutinee + // is not a pure expression, mark scrutineeSym as unusable. The reason is that + // scrutinee would then fail the tests in erasure that demand that the RHS of + // an erased val is a pure expression. At the end of the inline match reduction + // we throw out all unusable vals and check that the remaining code does not refer + // to unusable symbols. + // Note that compiletime.erasedValue is treated as erased but not pure, so scrutinees + // containing references to it becomes unusable. + if scrutinee.existsSubTree(_.symbol.isErased) then scrutineeSym.setFlag(Erased) - + if !tpd.isPureExpr(scrutinee) then unusable += scrutineeSym def reduceCase(cdef: CaseDef): MatchReduxWithGuard = { val caseBindingMap = new mutable.ListBuffer[(Symbol, MemberDef)]() @@ -393,7 +410,25 @@ class InlineReducer(inliner: Inliner)(using Context): case _ => None } - recur(cases) + for (bindings, expr) <- recur(cases) yield + // drop unusable vals and check that no referenes to unusable symbols remain + val cleanupUnusable = new TreeMap: + override def transform(tree: Tree)(using Context): Tree = + tree match + case tree: ValDef if unusable.contains(tree.symbol) => EmptyTree + case id: Ident if unusable.contains(id.symbol) => + report.error( + em"""${id.symbol} is unusable in ${ctx.owner} because it refers to an erased expression + |in the selector of an inline match that reduces to + | + |${Block(bindings, expr)}""", + tree.srcPos) + tree + case _ => super.transform(tree) + + val bindings1 = bindings.mapConserve(cleanupUnusable.transform).collect: + case mdef: MemberDef => mdef + (bindings1, cleanupUnusable.transform(expr)) } end InlineReducer diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index d0a89c9564f4..66f4db32be9e 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -14,7 +14,7 @@ import core.Names.* import core.StdNames.* import core.NameOps.* import core.Periods.currentStablePeriod -import core.NameKinds.{AdaptedClosureName, BodyRetainerName, DirectMethName, InlineScrutineeName} +import core.NameKinds.{AdaptedClosureName, BodyRetainerName, DirectMethName} import core.Scopes.newScopeWith import core.Decorators.* import core.Constants.* @@ -929,8 +929,7 @@ object Erasure { override def typedValDef(vdef: untpd.ValDef, sym: Symbol)(using Context): Tree = if sym.isEffectivelyErased then - if !sym.name.is(InlineScrutineeName) then - checkPureErased(vdef.rhs, isArgument = false) + checkPureErased(vdef.rhs, isArgument = false) erasedDef(sym) else trace(i"erasing $vdef"): checkNotErasedClass(sym.info, vdef) diff --git a/tests/neg/i23406.check b/tests/neg/i23406.check new file mode 100644 index 000000000000..a9ca83c9a8b3 --- /dev/null +++ b/tests/neg/i23406.check @@ -0,0 +1,40 @@ +-- Error: tests/neg/i23406.scala:18:7 ---------------------------------------------------------------------------------- +18 | funny[String] // error + | ^^^^^^^^^^^^^ + | value x is unusable in method Test because it refers to an erased expression + | in the selector of an inline match that reduces to + | + | { + | erased val $scrutinee1: String = compiletime.package$package.erasedValue[String] + | erased val x: String = $scrutinee1 + | { + | x:String + | } + | } + |-------------------------------------------------------------------------------------------------------------------- + |Inline stack trace + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + |This location contains code that was inlined from i23406.scala:7 + 7 | case x: String => x + | ^ + -------------------------------------------------------------------------------------------------------------------- +-- Error: tests/neg/i23406.scala:19:9 ---------------------------------------------------------------------------------- +19 | problem[String] // error + | ^^^^^^^^^^^^^^^ + | value x is unusable in method Test because it refers to an erased expression + | in the selector of an inline match that reduces to + | + | { + | erased val $scrutinee2: String = compiletime.package$package.erasedValue[String] + | erased val x: String = $scrutinee2 + | { + | foo(x) + | } + | } + |-------------------------------------------------------------------------------------------------------------------- + |Inline stack trace + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + |This location contains code that was inlined from i23406.scala:11 +11 | case x: String => foo(x) + | ^ + -------------------------------------------------------------------------------------------------------------------- diff --git a/tests/neg/i23406.scala b/tests/neg/i23406.scala index 79049f4eddff..ffebf0e78701 100644 --- a/tests/neg/i23406.scala +++ b/tests/neg/i23406.scala @@ -1,5 +1,20 @@ +import language.experimental.erasedDefinitions + +def foo(erased x: String): String = "" + inline def funny[T]: String = inline compiletime.erasedValue[T] match case x: String => x -@main def Test = funny[String] // error +inline def problem[T]: String = + inline compiletime.erasedValue[T] match + case x: String => foo(x) + +inline def ok[T]: String = + inline compiletime.erasedValue[T] match + case x: String => "hi" + +def Test = + funny[String] // error + problem[String] // error + ok[String] From 3cb8953a49544a2e224d37702a849509668377eb Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 27 Jun 2025 16:40:34 +0200 Subject: [PATCH 12/19] Update checkfile after rebase fixes --- tests/neg/erasedValueb.check | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/neg/erasedValueb.check b/tests/neg/erasedValueb.check index c38b5feba33d..f5765064a036 100644 --- a/tests/neg/erasedValueb.check +++ b/tests/neg/erasedValueb.check @@ -2,7 +2,7 @@ 7 | foo0(erasedValue[Int]) // error | ^^^^^^^^^^^ | method erasedValue is declared as `erased`, but is in fact used --- [E215] Type Error: tests/neg/erasedValueb.scala:8:18 ---------------------------------------------------------------- +-- [E216] Type Error: tests/neg/erasedValueb.scala:8:18 ---------------------------------------------------------------- 8 | foo1(erasedValue[Int]) // error | ^^^^^^^^^^^^^^^^ | argument to an erased parameter fails to be a pure expression From cfb28fa05458d2d6e580fe9b583ac904c07e2a55 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 28 Jun 2025 20:46:44 +0200 Subject: [PATCH 13/19] Desugar using clauses for Singleton context bounds to be erased Drop the previous convention that Singleton is an erased class under x.modularity. That does not work anynmore if we change to the Erased trait scheme. --- compiler/src/dotty/tools/dotc/core/Definitions.scala | 2 +- compiler/src/dotty/tools/dotc/core/TypeUtils.scala | 2 -- compiler/src/dotty/tools/dotc/typer/Namer.scala | 5 +++++ tests/new/test.scala | 11 +---------- tests/pos/singleton-ctx-bound.scala | 8 ++++---- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 3aaf966597c3..262c3f6b2750 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -543,7 +543,7 @@ class Definitions { // needed as a synthetic class because Scala 2.x refers to it in classfiles // but does not define it as an explicit class. val cls = enterCompleteClassSymbol( - ScalaPackageClass, tpnme.Singleton, PureInterfaceCreationFlags | Final | Erased, + ScalaPackageClass, tpnme.Singleton, PureInterfaceCreationFlags | Final, List(AnyType)) enterTypeField(cls, tpnme.Self, Deferred, cls.info.decls.openForMutations) cls diff --git a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala index 82c027744c38..82c7f05c5e99 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala @@ -26,8 +26,6 @@ class TypeUtils: def isErasedClass(using Context): Boolean = val cls = self.underlyingClassRef(refinementOK = true).typeSymbol cls.is(Flags.Erased) - && (cls != defn.SingletonClass || Feature.enabled(Feature.modularity)) - // Singleton counts as an erased class only under x.modularity /** Is this type a checked exception? This is the case if the type diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index e4d237072041..6fdacf5e9216 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1906,6 +1906,11 @@ class Namer { typer: Typer => case _ => val mbrTpe = paramFn(checkSimpleKinded(typedAheadType(mdef.tpt, tptProto)).tpe) + // Add an erased to the using clause generated from a `: Singleton` context bound + mdef.tpt match + case tpt: untpd.ContextBoundTypeTree if mbrTpe.typeSymbol == defn.SingletonClass => + sym.setFlag(Erased) + case _ => if (ctx.explicitNulls && mdef.mods.is(JavaDefined)) JavaNullInterop.nullifyMember(sym, mbrTpe, mdef.mods.isAllOf(JavaEnumValue)) else mbrTpe diff --git a/tests/new/test.scala b/tests/new/test.scala index dc0b40d6a755..de3073216898 100644 --- a/tests/new/test.scala +++ b/tests/new/test.scala @@ -1,12 +1,3 @@ -import java.io.IOException -class CanThrow[-E <: Exception] -def foo[E <: Exception](e: E)(using erased CanThrow[E]): Nothing = throw e - -erased def magic[E]: E = magic // error -inline def moreMagic[E]: E = moreMagic - -def Test = - foo(new IOException)(using magic) - foo(new IOException)(using moreMagic) // should be error +def foo[T: Singleton](x: T) = x diff --git a/tests/pos/singleton-ctx-bound.scala b/tests/pos/singleton-ctx-bound.scala index c6b0d2fb823c..a6af8f9038ab 100644 --- a/tests/pos/singleton-ctx-bound.scala +++ b/tests/pos/singleton-ctx-bound.scala @@ -1,4 +1,4 @@ -//> using options -language:experimental.modularity -source future +//> using options -language:experimental.modularity -source future -language:experimental.erasedDefinitions object Test: class Wrap[T](x: T) @@ -11,7 +11,7 @@ object Test: val x1 = f1(1) val _: Wrap[1] = x1 - def f2[T](x: T)(using Singleton { type Self = T}): Wrap[T] = Wrap(x) + def f2[T](x: T)(using erased Singleton { type Self = T}): Wrap[T] = Wrap(x) val x2 = f2(1) val _: Wrap[1] = x2 @@ -19,7 +19,7 @@ object Test: val x3 = f3(1) val _: Wrap[1] = x3 - def f4[T](x: T)(using T is Singleton): Wrap[T] = Wrap(x) + def f4[T](x: T)(using erased T is Singleton): Wrap[T] = Wrap(x) val x4 = f4(1) val _: Wrap[1] = x4 @@ -33,7 +33,7 @@ object Test: val y1 = C1("hi") val _: "hi" = y1.fld - class C2[T](x: T)(using T is Singleton): + class C2[T](x: T)(using erased T is Singleton): def fld: T = x val y2 = C2("hi") val _: "hi" = y2.fld From 798ced99d63caa4862d620ec95a896d04a1e822f Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 28 Jun 2025 20:49:35 +0200 Subject: [PATCH 14/19] Add Erased base trait --- library/src/scala/compiletime/Erased.scala | 7 +++++++ .../stdlibExperimentalDefinitions.scala | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 library/src/scala/compiletime/Erased.scala diff --git a/library/src/scala/compiletime/Erased.scala b/library/src/scala/compiletime/Erased.scala new file mode 100644 index 000000000000..665639322122 --- /dev/null +++ b/library/src/scala/compiletime/Erased.scala @@ -0,0 +1,7 @@ +package scala.compiletime +import annotation.experimental + +/** A marker trait for erased values. vals or parameters whose type extends + * `Erased` get an implicit `erased` modifier. + */ +@experimental trait Erased \ No newline at end of file diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index fd0281c5fffc..960b0f3c5fb0 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -105,7 +105,10 @@ val experimentalDefinitionInLibrary = Set( "scala.Predef$.runtimeChecked", "scala.annotation.internal.RuntimeChecked", // New feature: SIP 61 - @unroll annotation - "scala.annotation.unroll" + "scala.annotation.unroll", + + // New feature: Erased trait + "scala.compiletime.Erased", ) From 35576980b70017a37c4b95c7bcefbe5cea57d36d Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 29 Jun 2025 13:49:52 +0200 Subject: [PATCH 15/19] Drop erased class Replace with test whether a type derivesFrom Erased. --- .../dotty/tools/dotc/core/Definitions.scala | 3 +- .../src/dotty/tools/dotc/core/TypeUtils.scala | 5 -- .../src/dotty/tools/dotc/core/Types.scala | 4 +- .../dotty/tools/dotc/transform/Erasure.scala | 27 ++--------- .../src/dotty/tools/dotc/typer/Checking.scala | 5 +- .../src/dotty/tools/dotc/typer/Typer.scala | 48 +++++++++---------- library/src/scala/CanThrow.scala | 2 +- library/src/scala/Precise.scala | 2 +- .../neg/erased-inheritance.scala | 0 .../neg/safeThrowsStrawman2.scala | 2 +- .../captures/erased-methods2.scala | 2 +- tests/neg/erased-class.scala | 10 ++-- tests/neg/i20317a.scala | 2 +- tests/neg/lambda-infer.scala | 2 +- tests/neg/safeThrowsStrawman.scala | 2 +- tests/pos/erased-class-as-args.scala | 6 +-- tests/pos/erased-class-separate/A_1.scala | 2 +- tests/pos/erased-conforms.scala | 6 +-- tests/{neg => pos}/erased-lazy-val.scala | 2 +- tests/pos/experimental-erased-2.scala | 2 +- tests/pos/experimental-erased.scala | 2 +- tests/pos/experimentalErased.scala | 4 +- tests/pos/i13392.scala | 2 +- tests/pos/i20206.scala | 2 +- tests/pos/poly-erased-functions.scala | 2 +- tests/pos/tailrec.scala | 2 +- tests/run-macros/i12021/Test_2.scala | 2 +- tests/run/i13691.scala | 4 +- tests/run/i23305.scala | 2 +- tests/run/quotes-reflection/Test_2.scala | 2 +- tests/run/safeThrowsStrawman.scala | 2 +- tests/run/safeThrowsStrawman2.scala | 2 +- 32 files changed, 68 insertions(+), 94 deletions(-) rename tests/{ => invalid}/neg/erased-inheritance.scala (100%) rename tests/{ => invalid}/neg/safeThrowsStrawman2.scala (94%) rename tests/{neg => pos}/erased-lazy-val.scala (66%) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 262c3f6b2750..531e8a5aa21f 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -266,6 +266,7 @@ class Definitions { @tu lazy val CompiletimeOpsDoubleModuleClass: Symbol = requiredModule("scala.compiletime.ops.double").moduleClass @tu lazy val CompiletimeOpsStringModuleClass: Symbol = requiredModule("scala.compiletime.ops.string").moduleClass @tu lazy val CompiletimeOpsBooleanModuleClass: Symbol = requiredModule("scala.compiletime.ops.boolean").moduleClass + @tu lazy val ErasedClass: ClassSymbol = requiredClass("scala.compiletime.Erased") /** Note: We cannot have same named methods defined in Object and Any (and AnyVal, for that matter) * because after erasure the Any and AnyVal references get remapped to the Object methods @@ -1015,7 +1016,7 @@ class Definitions { @tu lazy val Caps_Mutable: ClassSymbol = requiredClass("scala.caps.Mutable") @tu lazy val Caps_SharedCapability: ClassSymbol = requiredClass("scala.caps.SharedCapability") - @tu lazy val PureClass: Symbol = requiredClass("scala.Pure") + @tu lazy val PureClass: ClassSymbol = requiredClass("scala.Pure") // Annotation base classes @tu lazy val AnnotationClass: ClassSymbol = requiredClass("scala.annotation.Annotation") diff --git a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala index 82c7f05c5e99..d2a8a499c90a 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala @@ -23,11 +23,6 @@ class TypeUtils: def isPrimitiveValueType(using Context): Boolean = self.classSymbol.isPrimitiveValueClass - def isErasedClass(using Context): Boolean = - val cls = self.underlyingClassRef(refinementOK = true).typeSymbol - cls.is(Flags.Erased) - - /** Is this type a checked exception? This is the case if the type * derives from Exception but not from RuntimeException. According to * that definition Throwable is unchecked. That makes sense since you should diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 1b59faf81a6f..444da66253ca 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -275,7 +275,7 @@ object Types extends TypeUtils { tp.isBottomType && (tp.hasClassSymbol(defn.NothingClass) || cls != defn.NothingClass && !cls.isValueClass) - def loop(tp: Type): Boolean = tp match { + def loop(tp: Type): Boolean = try tp match case tp: TypeRef => val sym = tp.symbol if (sym.isClass) sym.derivesFrom(cls) else loop(tp.superType) @@ -301,7 +301,7 @@ object Types extends TypeUtils { cls == defn.ObjectClass case _ => false - } + catch case ex: Throwable => handleRecursive(i"derivesFrom $cls:", show, ex) loop(this) } diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index 66f4db32be9e..2aedc90e47ef 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -579,8 +579,7 @@ object Erasure { |it should have been processed and eliminated during expansion of an enclosing macro or term erasure.""" report.error(message, tree.srcPos) case _ => // OK - - checkNotErasedClass(tree) + tree end checkNotErased def checkPureErased(tree: untpd.Tree, isArgument: Boolean, isImplicit: Boolean = false)(using Context): Unit = @@ -589,21 +588,6 @@ object Erasure { if !tpd.isPureExpr(tree1) then report.error(ErasedNotPure(tree1, isArgument, isImplicit), tree1.srcPos) - private def checkNotErasedClass(tp: Type, tree: untpd.Tree)(using Context): Unit = tp match - case JavaArrayType(et) => - checkNotErasedClass(et, tree) - case _ => - if tp.isErasedClass then - val (kind, tree1) = tree match - case tree: untpd.ValOrDefDef => ("definition", tree.tpt) - case tree: untpd.DefTree => ("definition", tree) - case _ => ("expression", tree) - report.error(em"illegal reference to erased ${tp.typeSymbol} in $kind that is not itself erased", tree1.srcPos) - - private def checkNotErasedClass(tree: Tree)(using Context): tree.type = - checkNotErasedClass(tree.tpe.widen.finalResultType, tree) - tree - def erasedDef(sym: Symbol)(using Context): Tree = if sym.isClass then // We cannot simply drop erased classes, since then they would not generate classfiles @@ -631,7 +615,7 @@ object Erasure { * are handled separately by [[typedDefDef]], [[typedValDef]] and [[typedTyped]]. */ override def typedTypeTree(tree: untpd.TypeTree, pt: Type)(using Context): TypeTree = - checkNotErasedClass(tree.withType(erasure(tree.typeOpt))) + tree.withType(erasure(tree.typeOpt)) /** This override is only needed to semi-erase type ascriptions */ override def typedTyped(tree: untpd.Typed, pt: Type)(using Context): Tree = @@ -650,7 +634,7 @@ object Erasure { if (tree.typeOpt.isRef(defn.UnitClass)) tree.withType(tree.typeOpt) else if (tree.const.tag == Constants.ClazzTag) - checkNotErasedClass(clsOf(tree.const.typeValue)) + clsOf(tree.const.typeValue) else super.typedLiteral(tree) @@ -932,7 +916,6 @@ object Erasure { checkPureErased(vdef.rhs, isArgument = false) erasedDef(sym) else trace(i"erasing $vdef"): - checkNotErasedClass(sym.info, vdef) super.typedValDef(untpd.cpy.ValDef(vdef)( tpt = untpd.TypedSplice(TypeTree(sym.info).withSpan(vdef.tpt.span))), sym) @@ -944,7 +927,6 @@ object Erasure { if sym.isEffectivelyErased || sym.name.is(BodyRetainerName) then erasedDef(sym) else - checkNotErasedClass(sym.info.finalResultType, ddef) val restpe = if sym.isConstructor then defn.UnitType else sym.info.resultType var vparams = outerParamDefs(sym) ::: ddef.paramss.collect { @@ -1063,9 +1045,6 @@ object Erasure { adaptClosure(implClosure) } - override def typedNew(tree: untpd.New, pt: Type)(using Context): Tree = - checkNotErasedClass(super.typedNew(tree, pt)) - override def typedTypeDef(tdef: untpd.TypeDef, sym: Symbol)(using Context): Tree = EmptyTree diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index b422a5f21a6c..aedb5034a9cf 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -682,10 +682,9 @@ object Checking { if sym.isUpdateMethod && !sym.owner.derivesFrom(defn.Caps_Mutable) then fail(em"Update methods can only be used as members of classes extending the `Mutable` trait") val unerasable = - sym.is(Lazy, butNot = Given) - || sym.is(Method, butNot = Macro) + sym.is(Method, butNot = Macro) || sym.is(Mutable) - || sym.isType && !sym.isClass + || sym.isType checkApplicable(Erased, !unerasable) checkCombination(Final, Open) checkCombination(Sealed, Open) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 786c2ae47302..9099105a0da9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1770,24 +1770,26 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer if (mt.isParamDependent) report.error(em"$mt is an illegal function type because it has inter-parameter dependencies", tree.srcPos) // Restart typechecking if there are erased classes that we want to mark erased - if mt.paramErasureStatuses.zip(mt.paramInfos.map(_.isErasedClass)).exists((paramErased, classErased) => classErased && !paramErased) then - val newParams = params3.zipWithConserve(mt.paramInfos.map(_.isErasedClass)) { (arg, isErasedClass) => - if isErasedClass then arg.withAddedFlags(Erased) else arg - } - return typedDependent(newParams) - val core = - if mt.hasErasedParams then TypeTree(defn.PolyFunctionClass.typeRef) - else - val resTpt = TypeTree(mt.nonDependentResultApprox).withSpan(body.span) - val paramTpts = appDef.termParamss.head.map(p => TypeTree(p.tpt.tpe).withSpan(p.tpt.span)) - val funSym = defn.FunctionSymbol(numArgs, isContextual) - val tycon = TypeTree(funSym.typeRef) - AppliedTypeTree(tycon, paramTpts :+ resTpt) - val res = RefinedTypeTree(core, List(appDef), ctx.owner.asClass) - if isImpure then - typed(untpd.makeRetaining(untpd.TypedSplice(res), Nil, tpnme.retainsCap), pt) + if mt.paramErasureStatuses.lazyZip(mt.paramInfos).exists: (paramErased, info) => + !paramErased && info.derivesFrom(defn.ErasedClass) + then + val newParams = params3.zipWithConserve(mt.paramInfos): (param, info) => + if info.derivesFrom(defn.ErasedClass) then param.withAddedFlags(Erased) else param + typedDependent(newParams) else - res + val core = + if mt.hasErasedParams then TypeTree(defn.PolyFunctionClass.typeRef) + else + val resTpt = TypeTree(mt.nonDependentResultApprox).withSpan(body.span) + val paramTpts = appDef.termParamss.head.map(p => TypeTree(p.tpt.tpe).withSpan(p.tpt.span)) + val funSym = defn.FunctionSymbol(numArgs, isContextual) + val tycon = TypeTree(funSym.typeRef) + AppliedTypeTree(tycon, paramTpts :+ resTpt) + val res = RefinedTypeTree(core, List(appDef), ctx.owner.asClass) + if isImpure then + typed(untpd.makeRetaining(untpd.TypedSplice(res), Nil, tpnme.retainsCap), pt) + else + res end typedDependent args match { @@ -1802,7 +1804,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val result = typed(cpy.AppliedTypeTree(tree)(untpd.TypeTree(funSym.typeRef), args :+ body), pt) // if there are any erased classes, we need to re-do the typecheck. result match - case r: AppliedTypeTree if r.args.exists(_.tpe.isErasedClass) => + case r: AppliedTypeTree if r.args.init.exists(_.tpe.derivesFrom(defn.ErasedClass)) => typedFunctionType(desugar.makeFunctionWithValDefs(tree, pt), pt) case _ => result } @@ -2947,6 +2949,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer completeAnnotations(vdef, sym) if sym.is(Implicit) then checkImplicitConversionDefOK(sym) if sym.is(Module) then checkNoModuleClash(sym) + else if sym.info.derivesFrom(defn.ErasedClass) then + sym.setFlag(Erased) val tpt1 = checkSimpleKinded(typedType(tpt)) val rhs1 = vdef.rhs match case rhs @ Ident(nme.WILDCARD) => @@ -3071,16 +3075,12 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } /** (1) Check that the signature of the class member does not return a repeated parameter type - * (2) If info is an erased class, set erased flag of member - * (3) Check that erased classes are not parameters of polymorphic functions. - * (4) Make sure the definition's symbol is `sym`. - * (5) Set the `defTree` of `sym` to be `mdef`. + * (2) Make sure the definition's symbol is `sym`. + * (3) Set the `defTree` of `sym` to be `mdef`. */ private def postProcessInfo(mdef: MemberDef, sym: Symbol)(using Context): MemberDef = if (!sym.isOneOf(Synthetic | InlineProxy | Param) && sym.info.finalResultType.isRepeatedParam) report.error(em"Cannot return repeated parameter type ${sym.info.finalResultType}", sym.srcPos) - if !sym.is(Module) && !sym.isConstructor && sym.info.finalResultType.isErasedClass then - sym.setFlag(Erased) mdef.ensureHasSym(sym) mdef.setDefTree diff --git a/library/src/scala/CanThrow.scala b/library/src/scala/CanThrow.scala index 46164d0133ea..e1b00bcb395b 100644 --- a/library/src/scala/CanThrow.scala +++ b/library/src/scala/CanThrow.scala @@ -8,7 +8,7 @@ import annotation.{implicitNotFound, experimental, capability} */ @experimental @implicitNotFound("The capability to throw exception ${E} is missing.\nThe capability can be provided by one of the following:\n - Adding a using clause `(using CanThrow[${E}])` to the definition of the enclosing method\n - Adding `throws ${E}` clause after the result type of the enclosing method\n - Wrapping this piece of code with a `try` block that catches ${E}") -erased class CanThrow[-E <: Exception] extends caps.SharedCapability +class CanThrow[-E <: Exception] extends caps.SharedCapability, compiletime.Erased @experimental object unsafeExceptions: diff --git a/library/src/scala/Precise.scala b/library/src/scala/Precise.scala index aad42ca8950f..f8a8dd6b47f4 100644 --- a/library/src/scala/Precise.scala +++ b/library/src/scala/Precise.scala @@ -7,5 +7,5 @@ import language.experimental.erasedDefinitions * in precise mode. This means that singleton types and union types are not * widened. */ -@experimental erased trait Precise: +@experimental trait Precise extends compiletime.Erased: type Self diff --git a/tests/neg/erased-inheritance.scala b/tests/invalid/neg/erased-inheritance.scala similarity index 100% rename from tests/neg/erased-inheritance.scala rename to tests/invalid/neg/erased-inheritance.scala diff --git a/tests/neg/safeThrowsStrawman2.scala b/tests/invalid/neg/safeThrowsStrawman2.scala similarity index 94% rename from tests/neg/safeThrowsStrawman2.scala rename to tests/invalid/neg/safeThrowsStrawman2.scala index 8d95494e30e0..c6ef62317c6e 100644 --- a/tests/neg/safeThrowsStrawman2.scala +++ b/tests/invalid/neg/safeThrowsStrawman2.scala @@ -1,7 +1,7 @@ import language.experimental.erasedDefinitions object scalax: - erased class CanThrow[E <: Exception] + class CanThrow[E <: Exception] extends compiletime.Erased type CTF = CanThrow[Fail] infix type raises[R, E <: Exception] = CanThrow[E] ?=> R diff --git a/tests/neg-custom-args/captures/erased-methods2.scala b/tests/neg-custom-args/captures/erased-methods2.scala index 6e111f1702da..4eda00d1b4ac 100644 --- a/tests/neg-custom-args/captures/erased-methods2.scala +++ b/tests/neg-custom-args/captures/erased-methods2.scala @@ -6,7 +6,7 @@ class Ex1 extends Exception("Ex1") class Ex2 extends Exception("Ex2") class Ex3 extends Exception("Ex3") -erased class CT[-E <: Exception] extends caps.Capability +class CT[-E <: Exception] extends caps.Capability, compiletime.Erased def Throw[Ex <: Exception](ex: Ex)(using CT[Ex]^): Nothing = ??? diff --git a/tests/neg/erased-class.scala b/tests/neg/erased-class.scala index aede15b9ef70..53dc08a38ccd 100644 --- a/tests/neg/erased-class.scala +++ b/tests/neg/erased-class.scala @@ -1,10 +1,10 @@ import language.experimental.erasedDefinitions import scala.annotation.compileTimeOnly -erased class AA -erased class BB extends AA // ok +class AA extends compiletime.Erased +class BB extends AA // ok @main def Test = - val f1: Array[AA] = caps.unsafe.unsafeErasedValue // error // error - def f2(x: Int): Array[AA] = caps.unsafe.unsafeErasedValue // error // error - def bar: AA = caps.unsafe.unsafeErasedValue // ok + val f1: Array[AA] = caps.unsafe.unsafeErasedValue // error + def f2(x: Int): Array[AA] = caps.unsafe.unsafeErasedValue // error + def bar: AA = caps.unsafe.unsafeErasedValue // error val baz: AA = caps.unsafe.unsafeErasedValue // ok diff --git a/tests/neg/i20317a.scala b/tests/neg/i20317a.scala index d7b8b66eb80e..df0667d53ab7 100644 --- a/tests/neg/i20317a.scala +++ b/tests/neg/i20317a.scala @@ -1,5 +1,5 @@ type SemigroupStructural[A] = A & { def combine(a: A): A } def combineAll[A <: SemigroupStructural[A]]( - i: A, l: List[A] + i: A, l: List[A] // error ): A = l.foldLeft(i)(_.combine(_)) // error diff --git a/tests/neg/lambda-infer.scala b/tests/neg/lambda-infer.scala index 6c3db90cb893..ed2737a6f7ad 100644 --- a/tests/neg/lambda-infer.scala +++ b/tests/neg/lambda-infer.scala @@ -2,7 +2,7 @@ type F = (x: Int, erased y: Int) => Int -erased class A +class A extends compiletime.Erased @main def Test() = val a: F = (x, y) => x + 1 // error: Expected F got (Int, Int) => Int diff --git a/tests/neg/safeThrowsStrawman.scala b/tests/neg/safeThrowsStrawman.scala index bc07eb0bb3f9..a94bec429899 100644 --- a/tests/neg/safeThrowsStrawman.scala +++ b/tests/neg/safeThrowsStrawman.scala @@ -3,7 +3,7 @@ import annotation.implicitNotFound object scalax: @implicitNotFound("The capability to throw exception ${E} is missing.\nThe capability can be provided by one of the following:\n - A using clause `(using CanThrow[${E}])`\n - A raises clause in a result type such as `X raises ${E}`\n - an enclosing `try` that catches ${E}") - erased class CanThrow[-E <: Exception] + class CanThrow[-E <: Exception] extends compiletime.Erased infix type raises[R, +E <: Exception] = CanThrow[E] ?=> R diff --git a/tests/pos/erased-class-as-args.scala b/tests/pos/erased-class-as-args.scala index 128cd2b818e4..c223e583aed5 100644 --- a/tests/pos/erased-class-as-args.scala +++ b/tests/pos/erased-class-as-args.scala @@ -1,8 +1,8 @@ -//> using options -language:experimental.erasedDefinitions +import language.experimental.erasedDefinitions -erased class A +class A extends compiletime.Erased -erased class B(val x: Int) extends A +class B(val x: Int) extends A type T = (x: A, y: Int) => Int diff --git a/tests/pos/erased-class-separate/A_1.scala b/tests/pos/erased-class-separate/A_1.scala index 5c874ce6d89b..778f271463da 100644 --- a/tests/pos/erased-class-separate/A_1.scala +++ b/tests/pos/erased-class-separate/A_1.scala @@ -1,3 +1,3 @@ import language.experimental.erasedDefinitions -erased class A +class A extends compiletime.Erased diff --git a/tests/pos/erased-conforms.scala b/tests/pos/erased-conforms.scala index 311b244b9872..21456fc4848a 100644 --- a/tests/pos/erased-conforms.scala +++ b/tests/pos/erased-conforms.scala @@ -1,9 +1,9 @@ import language.experimental.erasedDefinitions -erased class ErasedTerm +class ErasedTerm extends compiletime.Erased -erased class <::<[-From, +To] extends ErasedTerm +class <::<[-From, +To] extends ErasedTerm -erased class =::=[From, To] extends (From <::< To) +class =::=[From, To] extends (From <::< To) inline given [X] => (X =::= X) = scala.caps.unsafe.unsafeErasedValue diff --git a/tests/neg/erased-lazy-val.scala b/tests/pos/erased-lazy-val.scala similarity index 66% rename from tests/neg/erased-lazy-val.scala rename to tests/pos/erased-lazy-val.scala index 271f87cc2cf0..e0ac0bcd9db3 100644 --- a/tests/neg/erased-lazy-val.scala +++ b/tests/pos/erased-lazy-val.scala @@ -1,5 +1,5 @@ //> using options -language:experimental.erasedDefinitions object Test { - erased lazy val i: Int = 1 // error + erased lazy val i: Int = 1 // now OK } diff --git a/tests/pos/experimental-erased-2.scala b/tests/pos/experimental-erased-2.scala index f3b524e18463..33bf4f4abf2b 100644 --- a/tests/pos/experimental-erased-2.scala +++ b/tests/pos/experimental-erased-2.scala @@ -3,6 +3,6 @@ import annotation.experimental @experimental object Test: - erased class CanThrow[-E <: Exception] + class CanThrow[-E <: Exception] extends compiletime.Erased def other = 1 diff --git a/tests/pos/experimental-erased.scala b/tests/pos/experimental-erased.scala index 156ad639f42d..1031cf9423c2 100644 --- a/tests/pos/experimental-erased.scala +++ b/tests/pos/experimental-erased.scala @@ -2,7 +2,7 @@ import language.experimental.erasedDefinitions import annotation.experimental @experimental -erased class CanThrow[-E <: Exception](val i: Int = 0) +class CanThrow[-E <: Exception](val i: Int = 0) extends compiletime.Erased @experimental object Foo diff --git a/tests/pos/experimentalErased.scala b/tests/pos/experimentalErased.scala index 4a504e3d8a80..6045f96164a5 100644 --- a/tests/pos/experimentalErased.scala +++ b/tests/pos/experimentalErased.scala @@ -2,9 +2,9 @@ import language.experimental.erasedDefinitions import annotation.experimental @experimental -erased class Foo +class Foo extends compiletime.Erased -erased class Bar +class Bar extends compiletime.Erased @experimental erased val foo2 = 2 diff --git a/tests/pos/i13392.scala b/tests/pos/i13392.scala index 614f711eebb5..5e5e2908722e 100644 --- a/tests/pos/i13392.scala +++ b/tests/pos/i13392.scala @@ -4,7 +4,7 @@ import annotation.{implicitNotFound, experimental} @experimental @implicitNotFound("The capability to throw exception ${E} is missing.\nThe capability can be provided by one of the following:\n - A using clause `(using CanThrow[${E}])`\n - A `throws` clause in a result type such as `X throws ${E}`\n - an enclosing `try` that catches ${E}") -erased class CanThrow[-E <: Exception] +class CanThrow[-E <: Exception] extends compiletime.Erased @experimental object unsafeExceptions: diff --git a/tests/pos/i20206.scala b/tests/pos/i20206.scala index 07ef3dc0ba73..89c3c7971f01 100644 --- a/tests/pos/i20206.scala +++ b/tests/pos/i20206.scala @@ -2,7 +2,7 @@ import language.experimental.erasedDefinitions -erased trait A +trait A extends compiletime.Erased trait B def foo1: A ?=> B ?=> Nothing = ??? diff --git a/tests/pos/poly-erased-functions.scala b/tests/pos/poly-erased-functions.scala index 8c7385edb86a..50ba245e782c 100644 --- a/tests/pos/poly-erased-functions.scala +++ b/tests/pos/poly-erased-functions.scala @@ -7,7 +7,7 @@ object Test: val t1 = [X] => (erased x: X, y: Int) => y val t2 = [X] => (x: X, erased y: Int) => x - erased class A + class A extends compiletime.Erased type T3 = [X] => (x: A, y: X) => X diff --git a/tests/pos/tailrec.scala b/tests/pos/tailrec.scala index 95e667c07515..902ccbf4e6ea 100644 --- a/tests/pos/tailrec.scala +++ b/tests/pos/tailrec.scala @@ -2,7 +2,7 @@ import scala.annotation.tailrec -erased class Foo1 +class Foo1 extends compiletime.Erased class Foo2 @tailrec diff --git a/tests/run-macros/i12021/Test_2.scala b/tests/run-macros/i12021/Test_2.scala index a542b14f1175..437a18959785 100644 --- a/tests/run-macros/i12021/Test_2.scala +++ b/tests/run-macros/i12021/Test_2.scala @@ -1,6 +1,6 @@ import scala.language.experimental.erasedDefinitions -erased class EC +class EC extends compiletime.Erased class X1(implicit i: Int) class X2(using i: Int) diff --git a/tests/run/i13691.scala b/tests/run/i13691.scala index 224656d87923..04f953d2da6b 100644 --- a/tests/run/i13691.scala +++ b/tests/run/i13691.scala @@ -1,7 +1,7 @@ import language.experimental.erasedDefinitions -erased class CanThrow[-E <: Exception] -erased class Foo +class CanThrow[-E <: Exception] extends compiletime.Erased +class Foo extends compiletime.Erased class Bar object unsafeExceptions: diff --git a/tests/run/i23305.scala b/tests/run/i23305.scala index 22cfe04339fc..862aed9d3362 100644 --- a/tests/run/i23305.scala +++ b/tests/run/i23305.scala @@ -1,6 +1,6 @@ //> using options -language:experimental.erasedDefinitions -erased trait DBMeta[A] +trait DBMeta[A] extends compiletime.Erased trait Table[A] diff --git a/tests/run/quotes-reflection/Test_2.scala b/tests/run/quotes-reflection/Test_2.scala index ce1cc8d3dff1..4ad0b17da9fa 100644 --- a/tests/run/quotes-reflection/Test_2.scala +++ b/tests/run/quotes-reflection/Test_2.scala @@ -1,6 +1,6 @@ import scala.language.experimental.erasedDefinitions -erased class EC +class EC extends compiletime.Erased trait X { def m1(using i: Int): Int diff --git a/tests/run/safeThrowsStrawman.scala b/tests/run/safeThrowsStrawman.scala index 973c9d8f5137..dc8da2f828c3 100644 --- a/tests/run/safeThrowsStrawman.scala +++ b/tests/run/safeThrowsStrawman.scala @@ -1,7 +1,7 @@ import language.experimental.erasedDefinitions object scalax: - erased class CanThrow[-E <: Exception] + class CanThrow[-E <: Exception] extends compiletime.Erased infix type raises[R, +E <: Exception] = CanThrow[E] ?=> R diff --git a/tests/run/safeThrowsStrawman2.scala b/tests/run/safeThrowsStrawman2.scala index 1c84d84babc7..4de8f9bc7e2b 100644 --- a/tests/run/safeThrowsStrawman2.scala +++ b/tests/run/safeThrowsStrawman2.scala @@ -1,7 +1,7 @@ import language.experimental.erasedDefinitions object scalax: - erased class CanThrow[-E <: Exception] + class CanThrow[-E <: Exception] extends compiletime.Erased infix type raises[R, +E <: Exception] = CanThrow[E] ?=> R From 3fe1596dcd568483ed988562d7423d338ce57a9d Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 29 Jun 2025 14:39:39 +0200 Subject: [PATCH 16/19] Make erased export forwarders inline methods --- compiler/src/dotty/tools/dotc/core/Flags.scala | 2 +- compiler/src/dotty/tools/dotc/typer/Namer.scala | 1 + tests/pos/erased-export.scala | 9 +++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 tests/pos/erased-export.scala diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index 9fdc640d4d24..b69f9142ee46 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -540,7 +540,7 @@ object Flags { val RetainedModuleClassFlags: FlagSet = RetainedModuleValAndClassFlags | Enum /** Flags retained in term export forwarders */ - val RetainedExportTermFlags = Infix | Given | Implicit | Inline | Transparent | Erased | HasDefaultParams | NoDefaultParams | ExtensionMethod + val RetainedExportTermFlags = Infix | Given | Implicit | Inline | Transparent | HasDefaultParams | NoDefaultParams | ExtensionMethod /** Flags retained in parameters of term export forwarders */ val RetainedExportTermParamFlags = Given | Implicit | Erased | HasDefault | Inline diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 6fdacf5e9216..298ec3682daa 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1325,6 +1325,7 @@ class Namer { typer: Typer => else mbr.info.ensureMethodic (EmptyFlags, mbrInfo) var mbrFlags = MandatoryExportTermFlags | maybeStable | (sym.flags & RetainedExportTermFlags) + if sym.is(Erased) then mbrFlags |= Inline if pathMethod.exists then mbrFlags |= ExtensionMethod val forwarderName = checkNoConflict(alias, span) newSymbol(cls, forwarderName, mbrFlags, mbrInfo, coord = span) diff --git a/tests/pos/erased-export.scala b/tests/pos/erased-export.scala new file mode 100644 index 000000000000..c11e3cc57d8a --- /dev/null +++ b/tests/pos/erased-export.scala @@ -0,0 +1,9 @@ +import language.experimental.erasedDefinitions + +class C(x: Int): + erased val e = x + +class D: + val c = C(22) + export c.* + erased val x = e From 0a62414a6677891afc937548354b613b984d1a6e Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 29 Jun 2025 19:40:45 +0200 Subject: [PATCH 17/19] Cleanups and drop redundant code --- .../dotty/tools/dotc/inlines/Inliner.scala | 4 +- .../dotty/tools/dotc/parsing/Parsers.scala | 15 +++--- .../dotty/tools/dotc/parsing/Scanners.scala | 2 - .../dotty/tools/dotc/transform/Erasure.scala | 46 +++++++++---------- .../dotty/tools/dotc/transform/Mixin.scala | 1 - .../tools/dotc/transform/PostTyper.scala | 17 ------- .../dotty/tools/dotc/transform/Recheck.scala | 2 +- .../tools/dotc/transform/SuperAccessors.scala | 3 +- 8 files changed, 33 insertions(+), 57 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala index 047ab80e6b0f..4b5ead7f19ca 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala @@ -58,12 +58,12 @@ object Inliner: case Ident(_) => isPureRef(tree) || tree.symbol.isAllOf(InlineParam) case Select(qual, _) => - if (tree.symbol.is(Erased)) true + if tree.symbol.isErased then true else isPureRef(tree) && apply(qual) case New(_) | Closure(_, _, _) => true case TypeApply(fn, _) => - if (fn.symbol.is(Erased) || fn.symbol == defn.QuotedTypeModule_of) true else apply(fn) + if fn.symbol.isErased || fn.symbol == defn.QuotedTypeModule_of then true else apply(fn) case Apply(fn, args) => val isCaseClassApply = { val cls = tree.tpe.classSymbol diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index e4314b27a32c..f7382e5fa74d 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -214,9 +214,8 @@ object Parsers { def isIdent(name: Name) = in.isIdent(name) def isPureArrow(name: Name): Boolean = isIdent(name) && Feature.pureFunsEnabled def isPureArrow: Boolean = isPureArrow(nme.PUREARROW) || isPureArrow(nme.PURECTXARROW) - def isErased = isIdent(nme.erased) && in.erasedEnabled - // Are we seeing an `erased` soft keyword that will not be an identifier? - def isErasedKw = isErased && in.isSoftModifierInParamModifierPosition + def isErased = + isIdent(nme.erased) && in.erasedEnabled && in.isSoftModifierInParamModifierPosition def isSimpleLiteral = simpleLiteralTokens.contains(in.token) || isIdent(nme.raw.MINUS) && numericLitTokens.contains(in.lookahead.token) @@ -1725,8 +1724,8 @@ object Parsers { else val paramStart = in.offset def addErased() = - erasedArgs.addOne(isErasedKw) - if isErasedKw then in.skipToken() + erasedArgs.addOne(isErased) + if isErased then in.skipToken() addErased() val args = in.currentRegion.withCommasExpected: @@ -2625,7 +2624,7 @@ object Parsers { */ def binding(mods: Modifiers): Tree = atSpan(in.offset) { - val mods1 = if isErasedKw then addModifier(mods) else mods + val mods1 = if isErased then addModifier(mods) else mods makeParameter(bindingName(), typedOpt(), mods1) } @@ -2828,7 +2827,7 @@ object Parsers { else in.currentRegion.withCommasExpected { var isFormalParams = false def exprOrBinding() = - if isErasedKw then isFormalParams = true + if isErased then isFormalParams = true if isFormalParams then binding(Modifiers()) else val t = maybeNamed(exprInParens)() @@ -3579,7 +3578,7 @@ object Parsers { def param(): ValDef = { val start = in.offset var mods = impliedMods.withAnnotations(annotations()) - if isErasedKw then + if isErased then mods = addModifier(mods) if paramOwner.isClass then mods = addFlag(modifiers(start = mods), ParamAccessor) diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 9987eaaa81b9..332ceb028a27 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -1236,8 +1236,6 @@ object Scanners { def isSoftModifierInParamModifierPosition: Boolean = isSoftModifier && !lookahead.isColon - def isErased: Boolean = isIdent(nme.erased) && erasedEnabled - def canStartStatTokens = if migrateTo3 then canStartStatTokens2 else canStartStatTokens3 diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index 2aedc90e47ef..c743e757b8b4 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -105,12 +105,6 @@ class Erasure extends Phase with DenotTransformer { if oldSymbol.isRetainedInlineMethod then newFlags = newFlags &~ Flags.Inline newAnnotations = newAnnotations.filterConserve(!_.isInstanceOf[BodyAnnotation]) - oldSymbol match - case cls: ClassSymbol if cls.is(Flags.Erased) => - newFlags = newFlags | Flags.Trait | Flags.JavaInterface - newAnnotations = Nil - newInfo = erasedClassInfo(cls) - case _ => // TODO: define derivedSymDenotation? if ref.is(Flags.PackageClass) || !ref.isClass // non-package classes are always copied since their base types change @@ -550,8 +544,11 @@ object Erasure { case _ => tree.symbol.isEffectivelyErased } - /** Check that Java statics and packages can only be used in selections. - */ + /** Check that + * - erased values are not referred to from normal code + * - inline method applications were inlined + * - Java statics and packages can only be used in selections. + */ private def checkNotErased(tree: Tree)(using Context): tree.type = if !ctx.mode.is(Mode.Type) then if isErased(tree) then @@ -582,6 +579,9 @@ object Erasure { tree end checkNotErased + /** Check that initializers of erased vals and arguments to erased parameters + * are pure expressions. + */ def checkPureErased(tree: untpd.Tree, isArgument: Boolean, isImplicit: Boolean = false)(using Context): Unit = val tree1 = tree.asInstanceOf[tpd.Tree] inContext(preErasureCtx): @@ -1049,22 +1049,20 @@ object Erasure { EmptyTree override def typedClassDef(cdef: untpd.TypeDef, cls: ClassSymbol)(using Context): Tree = - if cls.is(Flags.Erased) then erasedDef(cls) - else - val typedTree@TypeDef(name, impl @ Template(constr, _, self, _)) = super.typedClassDef(cdef, cls): @unchecked - // In the case where a trait extends a class, we need to strip any non trait class from the signature - // and accept the first one (see tests/run/mixins.scala) - val newTraits = impl.parents.tail.filterConserve: tree => - def isTraitConstructor = tree match - case Trees.Block(_, expr) => // Specific management for trait constructors (see tests/pos/i9213.scala) - expr.symbol.isConstructor && expr.symbol.owner.is(Flags.Trait) - case _ => tree.symbol.isConstructor && tree.symbol.owner.is(Flags.Trait) - tree.symbol.is(Flags.Trait) || isTraitConstructor - - val newParents = - if impl.parents.tail eq newTraits then impl.parents - else impl.parents.head :: newTraits - cpy.TypeDef(typedTree)(rhs = cpy.Template(impl)(parents = newParents)) + val typedTree@TypeDef(name, impl @ Template(constr, _, self, _)) = super.typedClassDef(cdef, cls): @unchecked + // In the case where a trait extends a class, we need to strip any non trait class from the signature + // and accept the first one (see tests/run/mixins.scala) + val newTraits = impl.parents.tail.filterConserve: tree => + def isTraitConstructor = tree match + case Trees.Block(_, expr) => // Specific management for trait constructors (see tests/pos/i9213.scala) + expr.symbol.isConstructor && expr.symbol.owner.is(Flags.Trait) + case _ => tree.symbol.isConstructor && tree.symbol.owner.is(Flags.Trait) + tree.symbol.is(Flags.Trait) || isTraitConstructor + + val newParents = + if impl.parents.tail eq newTraits then impl.parents + else impl.parents.head :: newTraits + cpy.TypeDef(typedTree)(rhs = cpy.Template(impl)(parents = newParents)) override def typedAnnotated(tree: untpd.Annotated, pt: Type)(using Context): Tree = typed(tree.arg, pt) diff --git a/compiler/src/dotty/tools/dotc/transform/Mixin.scala b/compiler/src/dotty/tools/dotc/transform/Mixin.scala index ce3f26071b77..b3285f62c062 100644 --- a/compiler/src/dotty/tools/dotc/transform/Mixin.scala +++ b/compiler/src/dotty/tools/dotc/transform/Mixin.scala @@ -264,7 +264,6 @@ class Mixin extends MiniPhase with SymTransformer { thisPhase => for getter <- mixin.info.decls.toList if getter.isGetter - && !getter.isEffectivelyErased && !wasOneOf(getter, Deferred) && !getter.isConstExprFinalVal yield diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 2f1e4a8b2b4f..9f79c063dc03 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -475,7 +475,6 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => case tree: ValDef => annotateExperimentalCompanion(tree.symbol) registerIfHasMacroAnnotations(tree) - //checkErasedDef(tree) Checking.checkPolyFunctionType(tree.tpt) val tree1 = cpy.ValDef(tree)(tpt = makeOverrideTypeDeclared(tree.symbol, tree.tpt)) if tree1.removeAttachment(desugar.UntupledParam).isDefined then @@ -483,7 +482,6 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => processValOrDefDef(super.transform(tree1)) case tree: DefDef => registerIfHasMacroAnnotations(tree) - //checkErasedDef(tree) Checking.checkPolyFunctionType(tree.tpt) annotateContextResults(tree) val tree1 = cpy.DefDef(tree)(tpt = makeOverrideTypeDeclared(tree.symbol, tree.tpt)) @@ -624,21 +622,6 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => if sym.derivesFrom(defn.MacroAnnotationClass) && !sym.isStatic then report.error("classes that extend MacroAnnotation must not be inner/local classes", sym.srcPos) - private def checkErasedDef(tree: ValOrDefDef)(using Context): Unit = - def checkOnlyErasedParams(): Unit = tree match - case tree: DefDef => - for params <- tree.paramss; param <- params if !param.symbol.isType && !param.symbol.is(Erased) do - report.error("erased definition can only have erased parameters", param.srcPos) - case _ => - - if tree.symbol.is(Erased, butNot = Macro) then - checkOnlyErasedParams() - val tpe = tree.rhs.tpe - if tpe.derivesFrom(defn.NothingClass) then - report.error("`erased` definition cannot be implemented with en expression of type Nothing", tree.srcPos) - else if tpe.derivesFrom(defn.NullClass) then - report.error("`erased` definition cannot be implemented with en expression of type Null", tree.srcPos) - private def annotateExperimentalCompanion(sym: Symbol)(using Context): Unit = if sym.is(Module) then ExperimentalAnnotation.copy(sym.companionClass).foreach(sym.addAnnotation) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index b04b28aebd01..51ccdfe57274 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -269,7 +269,7 @@ abstract class Recheck extends Phase, SymTransformer: def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Type = inContext(linkConstructorParams(sym).withOwner(sym)): val resType = recheck(tree.tpt) - if tree.rhs.isEmpty || sym.isInlineMethod || sym.isEffectivelyErased + if tree.rhs.isEmpty || sym.isInlineMethod then resType else recheck(tree.rhs, resType) diff --git a/compiler/src/dotty/tools/dotc/transform/SuperAccessors.scala b/compiler/src/dotty/tools/dotc/transform/SuperAccessors.scala index 5a63235fc3c0..0077cb969e3a 100644 --- a/compiler/src/dotty/tools/dotc/transform/SuperAccessors.scala +++ b/compiler/src/dotty/tools/dotc/transform/SuperAccessors.scala @@ -155,8 +155,7 @@ class SuperAccessors(thisPhase: DenotTransformer) { val needAccessor = name.isTermName // Types don't need super accessors - && !sym.isEffectivelyErased // Erased and concrete inline methods are not called at runtime - && !sym.isInlineMethod // so they don't need superaccessors. + && !sym.isInlineMethod // Inline methods are not called at runtime so they don't need superaccessors. && (clazz != currentClass || !validCurrentClass || mix.name.isEmpty && clazz.is(Trait)) if (needAccessor) atPhase(thisPhase.next)(superAccessorCall(sel, mix.name)) From 3e4bf1e5bbcde8458d551a79232218ae5b56241f Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 29 Jun 2025 21:49:04 +0200 Subject: [PATCH 18/19] Disable flaky test We should never have flaky tests. Please, everyone adding new tests, watch out for this. --- .../tools/pc/tests/completion/CompletionOverrideSuite.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionOverrideSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionOverrideSuite.scala index 94c444b0feb9..3720d170eb26 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionOverrideSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionOverrideSuite.scala @@ -284,7 +284,8 @@ class CompletionOverrideSuite extends BaseCompletionSuite: includeDetail = false ) - @Test def `mutable` = + // Disabled since the test is flaky @Test + def `mutable` = checkEdit( """|abstract class Mutable { | def foo: scala.collection.mutable.Set[Int] From c43b83d74cded86d97b2d7eb44f5c99e8bb9e323 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 30 Jun 2025 20:24:52 +0200 Subject: [PATCH 19/19] Update doc pages --- .../experimental/erased-defs-spec.md | 45 +-- .../reference/experimental/erased-defs.md | 285 +++++++++--------- tests/neg/erased-can-serialize.scala | 17 ++ tests/run/erased-machine-state.check | 3 +- tests/run/erased-machine-state.scala | 61 ++-- 5 files changed, 192 insertions(+), 219 deletions(-) create mode 100644 tests/neg/erased-can-serialize.scala diff --git a/docs/_docs/reference/experimental/erased-defs-spec.md b/docs/_docs/reference/experimental/erased-defs-spec.md index 1861b734bb47..4cedfcb8c5cc 100644 --- a/docs/_docs/reference/experimental/erased-defs-spec.md +++ b/docs/_docs/reference/experimental/erased-defs-spec.md @@ -4,67 +4,38 @@ title: "Erased Definitions - More Details" nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/erased-defs-spec.html --- -TODO: complete ## Rules -1. `erased` is a soft modifier. It can appear: - * At the start of a parameter block of a method, function or class - * In a method definition - * In a `val` definition (but not `lazy val` or `var`) - * In a `class` or `trait` definition +1. `erased` is a soft modifier. It can appear in a `val` definition or in a parameter. - ```scala - erased val x = ... - erased def f = ... - - def g(erased x: Int) = ... - - (erased x: Int, y: Int) => ... - def h(x: (Int, erased Int) => Int) = ... - - class K(erased x: Int) { ... } - erased class E {} - ``` - - -2. A reference to an `erased` val or def can only be used +2. A reference to an `erased` value can only be used * Inside the expression of argument to an `erased` parameter * Inside the body of an `erased` `val` or `def` +3. `erased` can also be used in a function type, e.g. -3. Functions - * `(erased x1: T1, x2: T2, ..., xN: TN) => y : (erased T1, T2, ..., TN) => R` - * `(using x1: T1, erased x2: T2, ..., xN: TN) => y: (using T1, erased T2, ..., TN) => R` - * `(using erased T1) => R <:< erased T1 => R` - * `(using T1, erased T2) => R <:< (T1, erased T2) => R` - * ... - - Note that there is no subtype relation between `(erased T) => R` and `T => R` (or `(given erased T) => R` and `(given T) => R`). The `erased` parameters must match exactly in their respective positions. + * `(erased T1, T2) => R` + * `(x: T1, y: erased T2) ?=> T` + Note that there is no subtype relation between `(erased T) => R` and `T => R` (or `(erased T) ?=> R` and `T ?=> R`). The `erased` parameters must match exactly in their respective positions. 4. Eta expansion if `def f(erased x: T): U` then `f: (erased T) => U`. - 5. Erasure semantics * All `erased` parameters are removed from the function * All argument to `erased` parameters are not passed to the function - * All `erased` definitions are removed - * `(erased ET1, erased ET2, T1, ..., erased ETN, TM) => R` are erased to `(T1, ..., TM) => R`. - * `(given erased ET1, erased ET2, T1, ..., erased ETN, TM) => R` are erased to `(given T1, ..., TM) => R`. - + * All `erased` value definitions are removed + * All `erased` argument types are removed from a function type 6. Overloading Method with `erased` parameters will follow the normal overloading constraints after erasure. - 7. Overriding * Member definitions overriding each other must both be `erased` or not be `erased`. * `def foo(x: T): U` cannot be overridden by `def foo(erased x: T): U` and vice-versa. 8. Type Restrictions - * For dependent functions, `erased` parameters are limited to realizable types, that is, types that are inhabited by non-null values. - This restriction stops us from using a bad bound introduced by an erased value, which leads to unsoundness (see #4060). * Polymorphic functions with erased parameters are currently not supported, and will be rejected by the compiler. This is purely an implementation restriction, and might be lifted in the future. diff --git a/docs/_docs/reference/experimental/erased-defs.md b/docs/_docs/reference/experimental/erased-defs.md index 4be4498e1058..426628edef13 100644 --- a/docs/_docs/reference/experimental/erased-defs.md +++ b/docs/_docs/reference/experimental/erased-defs.md @@ -10,48 +10,92 @@ nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/erased-defs import scala.language.experimental.erasedDefinitions ``` or by setting the command line option `-language:experimental.erasedDefinitions`. -Erased definitions must be in an experimental scope (see [Experimental definitions](../other-new-features/experimental-defs.md)). -## Why erased terms? +## Introduction -Let's describe the motivation behind erased terms with an example. In the -following we show a simple state machine which can be in a state `On` or `Off`. -The machine can change state from `Off` to `On` with `turnedOn` only if it is -currently `Off`. This last constraint is captured with the `IsOff[S]` contextual -evidence which only exists for `IsOff[Off]`. For example, not allowing calling -`turnedOn` on in an `On` state as we would require an evidence of type -`IsOff[On]` that will not be found. +## Why erased? +Sometimes, we need a value only to present evidence that some type can be constructed, whereas at runtime that value would not be referenced. For example, say we want to make Java serialization safe. This means that, when serializing values of some type, we want to have evidence that serialization of such a type will not fail at runtime. Java defines the `java.io.Serializable` interface to mark extending types as serializable. But this alone is not safe, since a `Serializable` class might well have unserializable fields. For instance Scala's `List` extends `Serializable` since we want to be able to serialize `List` data. But a particular list might have elements that are not serializable, for instance it might be a list of functions. If we try to serialize such a value, a `NotSerializableException` will be thrown. + +We can make serialization safe by defining an additional type class that has instances precisely for those types that are deeply serializable. For instance, like this: ```scala -sealed trait State -final class On extends State -final class Off extends State +/** Type class for types that are deeply serializable */ +trait CanSerialize[T] -@implicitNotFound("State must be Off") -class IsOff[S <: State] -object IsOff: - given isOff: IsOff[Off] = new IsOff[Off] +inline given CanSerialize[String] = CanSerialize() +inline given [T: CanSerialize] => CanSerialize[List[T]] = CanSerialize() +``` +We find a given instance of `CanSerialize` for strings, since strings are serializable. We also find a conditional given instance that says lists are serializable if their elements are. We would assume to have further instances for all types that are serializable (perhaps conditionally). -class Machine[S <: State]: - def turnedOn(using IsOff[S]): Machine[On] = new Machine[On] +Now, we can formulate a method `safeWriteObject` that serializes an object to an `ObjectOutputStream`: +```scala +def safeWriteObject[T <: java.io.Serializable] + (out: java.io.ObjectOutputStream, x: T) + (using CanSerialize[T]): Unit = + out.writeObject(x) +``` +The method works for objects of its type parameter `T`. `T` is required to conform to `java.io.Serializable` so that we can use the `writeObject` method of the output stream `out` on it. In addition we need a type class instance `CanSerialize[T]` that serves as evidence that the Java serialization will not fail at runtime. We can specialize the method to list arguments, as in the following: +```scala +def writeList[T] + (out: java.io.ObjectOutputStream, xs: List[T]) + (using erased CanSerialize[T]): Unit = + safeWriteObject(out, xs) +``` +We can test `writeList` by applying it to different types of lists: +```scala +@main def Test(out: java.io.ObjectOutputStream) = + writeList(out, List("a", "b")) // ok + writeList(out, List[Int => Int](x => x + 1, y => y * 2)) // error +``` +The first call will pass, but the second call will be rejected with a type error: +``` +No given instance of type CanSerialize[Int => Int] was found for parameter x$3 of method writeList +``` + +So far, this is a standard typeclass pattern to set up evidence that certain operations can be performed safely. But there is a problem with this scheme: The type class instances are passed as +additional parameters to methods `safeWriteObject` and `writeList` even though at run-time these objects will not be used anywhere. The only role of these parameters is to provide compile-time evidence that serialization for a particular type is safe. It would be nice if we could somehow "erase" these parameters so that they do not show up at run-time. This is precisely what erased does. Using erased, our example would look like this: +```scala +import language.experimental.erasedDefinitions + +class CanSerialize[T] + +inline given CanSerialize[String] = CanSerialize() +inline given [T: CanSerialize] => CanSerialize[List[T]] = CanSerialize() + +def safeWriteObject[T <: java.io.Serializable](out: java.io.ObjectOutputStream, x: T)(using erased CanSerialize[T]) = + out.writeObject(x) + +def writeList[T](out: java.io.ObjectOutputStream, xs: List[T])(using erased CanSerialize[T]) = + safeWriteObject(out, xs) + +@main def Test(out: java.io.ObjectOutputStream) = + writeList(out, List("a", "b")) // ok + writeList(out, List[Int => Int](x => x + 1, y => y * 2)) // error +``` +Note the two parameters to `safeWriteObject` and `writeList` are now `erased`. This means the parameters and their arguments are not present in the generated code. -val m = new Machine[Off] -m.turnedOn -m.turnedOn.turnedOn // ERROR -// ^ -// State must be Off +A safety requirement for `erased` is that we cannot simply make up evidence. For instance, say we want to make the second `writeList` pass by making up a given of the problematic type: +```scala +writeList(out, List[Int => Int](x => x + 1, y => y * 2)) + (using null.asInstanceOfCanSerialize[Int => Int]) +``` +This is just one way to do it, here is another: +```scala +def fakeEvidence: CanSerialize[Int => Int] = fakeEvidence +writeList(out, List[Int => Int](x => x + 1, y => y * 2)) + (using fakeEvidence) ``` +To rule out these attacks, we demand that the argument to an erased parameter is +a _pure expression_. Only a few expressions in Scala are pure, including -Note that in the code above the actual context arguments for `IsOff` are never -used at runtime; they serve only to establish the right constraints at compile -time. As these terms are never used at runtime there is not real need to have -them around, but they still need to be present in some form in the generated -code to be able to do separate compilation and retain binary compatibility. We -introduce _erased terms_ to overcome this limitation: we are able to enforce the -right constrains on terms at compile time. These terms have no run time -semantics and they are completely erased. + - constants, + - non-lazy, immutable vals, + - constructors of classes that don't have an initializer, applied to pure arguments, + - `apply` methods of case classes that don't have an initializer, applied to pure arguments. -## How to define erased terms? +Other function calls are not classified as pure expressions. That's why the two given instances in the erased version of our examples are inline methods. After inlining, the arguments to the erased parameters are simple class constructions of `CanSerialize` which count as pure expressions. + +## Details Parameters of methods and functions can be declared as erased, placing `erased` in front of each erased parameter (like `inline`). @@ -74,51 +118,68 @@ def methodWithErasedInt2(erased i: Int): Int = methodWithErasedInt1(i) // OK ``` +The arguments to erased parameters must be pure expressions. +```scala +def f(x: Int): Int = + if x == 0 then 1 else x * f(x - 1) + +inline def g(x: Int): Int = + if x == 0 then 1 else x * g(x - 1) + +methodWithErasedInt2(5) // ok +methodWithErasedInt2(f(5)) // error, f(22) is not a pure expression +methodWithErasedInt2(g(5)) // ok since `g` is `inline`. + Besides parameters, `val` definitions can also be marked with `erased`. These will also only be usable as arguments to `erased` parameters or -as part of the definitions of other erased `val`s. +as part of the definitions of other erased `val`s. Furthermore, the +defining right hand side of such `val` must be a pure expression. ```scala -erased val erasedEvidence: Ev = ... +erased val erasedEvidence: Ev = Ev() methodWithErasedEv(erasedEvidence, 40) // 42 ``` -## What happens with erased values at runtime? - -As `erased` vals and parameters are guaranteed not to be used in computations, they can and will be -erased. +## The Erased Trait +In some cases we would expect all instances of a trait to be erased. For instance, one could argue that it does not make sense to ever have a `CanSerialize[T]` instance at runtime. In that case we +can make `CanSerialize` extend from a new trait `compiletimetime.Erased` and avoid the explicit +`erased` modifiers in erased parameters and vals. Here is an alternative version our example using this scheme: ```scala -// becomes def methodWithErasedEv(x: Int): Int at runtime -def methodWithErasedEv(x: Int, erased ev: Ev): Int = ... +class CanSerialize[T] extends compiletime.Erased +... +def safeWriteObject[T <: java.io.Serializable](out: java.io.ObjectOutputStream, x: T)(using CanSerialize[T]) = ... -def evidence1: Ev = ... -erased def erasedEvidence2: Ev = ... // does not exist at runtime -erased val erasedEvidence3: Ev = ... // does not exist at runtime - -// evidence1 is not evaluated and only `x` is passed to methodWithErasedEv -methodWithErasedEv(x, evidence1) +def writeList[T: CanSerialize](out: java.io.ObjectOutputStream, xs: List[T]) = ... ``` +Because `CanSerialize` extends `Erased` we can elide the explicit `erased` modifier in the using clause of `safeWriteObject`. It now also becomes possible to use a context bound for `CanSerialize` as is shown in the `writeList` method above. The context bound expands to a +using clause `(using CanSerialize[T])` which gets implicitly tagged with `erased`. + +## Uses of `Erased` in existing Code + + - The `CanThrow[T]` typeclass is used to declare that an can be thrown. The compiler generates a `CanThrow[E]` instances for exceptions that are handled in a `try`. Methods take an implicit `CanThrow[E]` parameter to indicate that they might throw exception `E`. `CanThrow` is declared to be an `Erased` capability class, so no actual evidence of `CanThrow` remains at run-time. + + - The `CanEqual` evidence of [multiversal equality](../contextual/multiversal-equality.html) checks that two types can be compared. The actual comparison is done by the universal `equals` method of class `Object` or an overriding instance, it does not rely on the `CanEqual` value. +So far, `CanEqual` is handled specially in the compiler. With erased definitions, we could +avoid some of the special treatment by making `CanThrow` extend `compiletime.Erased`. + +- The conforms `<:<` typeclass asserts that we can prove that two types are in a subtype relation. `<:<` does offer a method to upcast values, but that could be also provided as a compiler-generated +cast operation. In that case, run-time instances of `<:<` (and also `=:=`) would be no longer needed and could be erased. + -## State machine with erased evidence example +## Example: State machine with erased evidence The following example is an extended implementation of a simple state machine which can be in a state `On` or `Off`. The machine can change state from `Off` -to `On` with `turnedOn` only if it is currently `Off`, conversely from `On` to -`Off` with `turnedOff` only if it is currently `On`. These last constraint are -captured with the `IsOff[S]` and `IsOn[S]` given evidence only exist for -`IsOff[Off]` and `IsOn[On]`. For example, not allowing calling `turnedOff` on in -an `Off` state as we would require an evidence `IsOn[Off]` that will not be -found. - -As the given evidences of `turnedOn` and `turnedOff` are not used in the -bodies of those functions we can mark them as `erased`. This will remove the -evidence parameters at runtime, but we would still evaluate the `isOn` and -`isOff` givens that were found as arguments. As `isOn` and `isOff` are not -used except as `erased` arguments, we can mark them as `erased`, hence removing -the evaluation of the `isOn` and `isOff` evidences. +to `On` with `turnOn` only if it is currently `Off`, conversely from `On` to +`Off` with `turnOff` only if it is currently `On`. These constraints are +captured represented with two typeclass traits `IsOn[T]` and `IsOff[T]`. Two given instances for these traits exist only for the right kinds of state. There is a given instance for `IsOn[On]` and one for `IsOff[Off]` but there are no given instances for the other combinations. + +The `turnOn` and `turnOff` methods each require one of these given instances to ensure the machine is in the correct state for the operation to be allowed. +As the given instances required by `turnedOn` and `turnedOff` are not used in the bodies of those functions we can mark them as `erased`. ```scala +import language.experimental.erasedDefinitions import scala.annotation.implicitNotFound sealed trait State @@ -128,97 +189,41 @@ final class Off extends State @implicitNotFound("State must be Off") class IsOff[S <: State] object IsOff: - // will not be called at runtime for turnedOn, the - // compiler will only require that this evidence exists - erased given IsOff[Off] = new IsOff[Off] + inline given IsOff[Off]() @implicitNotFound("State must be On") class IsOn[S <: State] object IsOn: - // will not exist at runtime, the compiler will only - // require that this evidence exists at compile time - erased given IsOn[On] = new IsOn[On] - -class Machine[S <: State] private (): - // ev will disappear from both functions - def turnedOn(using erased ev: IsOff[S]): Machine[On] = new Machine[On] - def turnedOff(using erased ev: IsOn[S]): Machine[Off] = new Machine[Off] - -object Machine: - def newMachine(): Machine[Off] = new Machine[Off] - -@main def test = - val m = Machine.newMachine() - m.turnedOn - m.turnedOn.turnedOff - m.turnedOn.turnedOn // error: State must be Off -``` - -Note that in [Compile-time operations](../metaprogramming/compiletime-ops.md#erasedvalue) we discussed `erasedValue` and inline -matches. `erasedValue` is internally implemented with `erased` (and is not experimental), so the state machine above -can be encoded as follows: - -```scala -import scala.compiletime.* - -sealed trait State -final class On extends State -final class Off extends State + inline given IsOn[On]() class Machine[S <: State]: - transparent inline def turnOn(): Machine[On] = - inline erasedValue[S] match - case _: Off => new Machine[On] - case _: On => error("Turning on an already turned on machine") - - transparent inline def turnOff(): Machine[Off] = - inline erasedValue[S] match - case _: On => new Machine[Off] - case _: Off => error("Turning off an already turned off machine") - -object Machine: - def newMachine(): Machine[Off] = - println("newMachine") - new Machine[Off] -end Machine + // ev will disappear from both functions + def turnOn(using erased IsOff[S]): Machine[On] = new Machine[On] + def turnOff(using erased IsOn[S]): Machine[Off] = new Machine[Off] @main def test = - val m = Machine.newMachine() - m.turnOn() - m.turnOn().turnOff() - m.turnOn().turnOn() // error: Turning on an already turned on machine + val m = Machine[Off]() + val m1 = m.turnOn + val m2 = m1.turnOff + m2.turnOn + + // m1.turnOn + // ^ error: State must be Off + // m2.turnOff + // ^ error: State must be On ``` +The first four lines of method `test` are all valid. The commented-out operations are invalid. The operation `m1.turnOn` is invalid since `m1` is of type `Machine[On]` and `m1.turnOn` requires the given instance `IsOff[On]` which does not exist. `m2.turnOff` is invalid by analogous reasoning. -## Erased Classes +## ErasedValue -`erased` can also be used as a modifier for a class. An erased class is intended to be used only in erased definitions. If the type of a val definition or parameter is -a (possibly aliased, refined, or instantiated) erased class, the definition is assumed to be `erased` itself. Likewise, a method with an erased class return type is assumed to be `erased` itself. Since given instances expand to vals and defs, they are also assumed to be erased if the type they produce is an erased class. Finally -function types with erased classes as arguments turn into erased function types. +The `compiletime.erasedValue` method was discussed in +[Compile-time operations](../metaprogramming/compiletime-ops.md#erasedvalue). A call to `erasedValue[T]` counts as an erased reference, so it could only be +used in an erased context, i.e. as an argument to an erased parameter or on the right-hand side of an erased `val` definition. At the same time +`erasedValue` does _not_ count as a pure expression, and for that reason cannot be part of these expressions. The net effect is that any references +to `erasedValue` must be eliminated by inlining. This is intentional: +allowing `erasedValue[T]` as a legal erased expression would undermine the safety of erased capabilities, since evidence for _any_ value of an erased type can be made up by it. -Example: -```scala -erased class CanRead - -val x: CanRead = ... // `x` is turned into an erased val -val y: CanRead => Int = ... // the function is turned into an erased function -def f(x: CanRead) = ... // `f` takes an erased parameter -def g(): CanRead = ... // `g` is turned into an erased def -given CanRead = ... // the anonymous given is assumed to be erased -``` -The code above expands to -```scala -erased class CanRead - -erased val x: CanRead = ... -val y: (erased CanRead) => Int = ... -def f(erased x: CanRead) = ... -erased def g(): CanRead = ... -erased given CanRead = ... -``` -After erasure, it is checked that no references to values of erased classes remain and that no instances of erased classes are created. So the following would be an error: -```scala -val err: Any = CanRead() // error: illegal reference to erased class CanRead -``` -Here, the type of `err` is `Any`, so `err` is not considered erased. Yet its initializing value is a reference to the erased class `CanRead`. +As an escape hatch, there also a method `unsafeErasedValue` in the +`scala.caps.unsafe` object. `scala.caps.unsafe.unsafeErasedValue[T]` does count as a pure expression for every type `T`, so it can be used in an erased context. But it should be used only if we can prove by other means that the established erased evidence is valid. [More Details](./erased-defs-spec.md) diff --git a/tests/neg/erased-can-serialize.scala b/tests/neg/erased-can-serialize.scala new file mode 100644 index 000000000000..13d9ad072f01 --- /dev/null +++ b/tests/neg/erased-can-serialize.scala @@ -0,0 +1,17 @@ +import language.experimental.erasedDefinitions + +class CanSerialize[T] + +inline given CanSerialize[String] = CanSerialize() +inline given [T: CanSerialize] => CanSerialize[List[T]] = CanSerialize() + +def safeWriteObject[T <: java.io.Serializable](out: java.io.ObjectOutputStream, x: T)(using erased CanSerialize[T]) = + out.writeObject(x) + +def writeList[T](out: java.io.ObjectOutputStream, xs: List[T])(using erased CanSerialize[T]) = + safeWriteObject(out, xs) + +@main def Test(out: java.io.ObjectOutputStream) = + writeList(out, List(List("a", "b"))) + writeList(out, List[Int => Int](x => x + 1, y => y * 2)) // error + diff --git a/tests/run/erased-machine-state.check b/tests/run/erased-machine-state.check index f9d7929a8fc9..730786f063a9 100644 --- a/tests/run/erased-machine-state.check +++ b/tests/run/erased-machine-state.check @@ -1,4 +1,3 @@ -newMachine -turnedOn turnedOn turnedOff +turnedOn diff --git a/tests/run/erased-machine-state.scala b/tests/run/erased-machine-state.scala index 2431d34e9dfe..17bbef55b753 100644 --- a/tests/run/erased-machine-state.scala +++ b/tests/run/erased-machine-state.scala @@ -1,4 +1,4 @@ -//> using options -language:experimental.erasedDefinitions +import language.experimental.erasedDefinitions import scala.annotation.implicitNotFound @@ -8,50 +8,31 @@ final class Off extends State @implicitNotFound("State must be Off") class IsOff[S <: State] -object IsOff { - inline implicit def isOff: IsOff[Off] = { - new IsOff[Off] - } -} +object IsOff: + inline given IsOff[Off]() @implicitNotFound("State must be On") class IsOn[S <: State] -object IsOn { - inline implicit def isOn: IsOn[On] = { - new IsOn[On] - } -} - -class Machine[S <: State] private { - def turnedOn (using erased s: IsOff[S]): Machine[On] = { +object IsOn: + inline given IsOn[On]() + +class Machine[S <: State]: + def turnOn(using erased IsOff[S]): Machine[On] = println("turnedOn") new Machine[On] - } - def turnedOff (using erased s: IsOn[S]): Machine[Off] = { + + def turnOff (using erased IsOn[S]): Machine[Off] = println("turnedOff") new Machine[Off] - } -} -object Machine { - def newMachine(): Machine[Off] = { - println("newMachine") - new Machine[Off] - } -} - -object Test { - def main(args: Array[String]): Unit = { - val m = Machine.newMachine() - m.turnedOn - m.turnedOn.turnedOff - - // m.turnedOff - // ^ - // State must be On - - // m.turnedOn.turnedOn - // ^ - // State must be Off - } -} + +@main def Test = + val m = Machine[Off]() + val m1 = m.turnOn + val m2 = m1.turnOff + m2.turnOn + + // m1.turnOn + // ^ error: State must be Off + // m2.turnOff + // ^ error: State must be On