diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index 998a87ef485f..8e046d70b8ee 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -3,6 +3,7 @@ import compiletime.ops.boolean.* import collection.immutable.{SeqMap, ListMap} import language.experimental.captureChecking +import scala.annotation.experimental object NamedTuple: @@ -16,6 +17,16 @@ object NamedTuple: /** A type which is a supertype of all named tuples. */ opaque type AnyNamedTuple = Any + // This formulation fixes the name types, following the standard established in this file. + // Alternatively you could require additional evidence and compare `N1` and `N2` types. + // However if this is explored, you must not use `CanEqual[N1, N2]` because `CanEqual` + // for Strings widens singletons. + // Comparing different name types with `=:=` could also be possible. + @experimental + given namedTupleCanEqual: [N <: Tuple, V1 <: Tuple, V2 <: Tuple] + => (eq: CanEqual[V1, V2]) + => CanEqual[NamedTuple[N, V1], NamedTuple[N, V2]] = CanEqual.derived + def apply[N <: Tuple, V <: Tuple](x: V): NamedTuple[N, V] = x def unapply[N <: Tuple, V <: Tuple](x: NamedTuple[N, V]): Some[V] = Some(x) diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 75f4c9e86465..1e8674cac43c 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -11,6 +11,8 @@ object MiMaFilters { ProblemFilters.exclude[DirectMissingMethodProblem]("scala.caps.package#package.freeze"), // scala/scala3#24545 / scala/scala3#24788 ProblemFilters.exclude[MissingClassProblem]("scala.annotation.unchecked.uncheckedOverride"), + // new feature: CanEqual support for NamedTuple + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.NamedTuple.namedTupleCanEqual"), ), ) diff --git a/tests/neg/named-tuples-strictEquality.check b/tests/neg/named-tuples-strictEquality.check new file mode 100644 index 000000000000..094707b2afc1 --- /dev/null +++ b/tests/neg/named-tuples-strictEquality.check @@ -0,0 +1,18 @@ +-- [E172] Type Error: tests/neg/named-tuples-strictEquality.scala:9:20 ------------------------------------------------- +9 | val b2: Boolean = u == v // error + | ^^^^^^ + |Values of types (name : String, age : Int) and (name : String, birthYear : Int) cannot be compared with == or !=. + |I found: + | + | NamedTuple.namedTupleCanEqual[N, V1, V2] + | + |But given instance namedTupleCanEqual in object NamedTuple does not match type CanEqual[(name : String, age : Int), (name : String, birthYear : Int)]. +-- [E172] Type Error: tests/neg/named-tuples-strictEquality.scala:15:11 ------------------------------------------------ +15 | case ScalaBook => true // error + | ^^^^^^^^^ + |Values of types (name : String, published : Int) and (name : String, age : Int) cannot be compared with == or !=. + |I found: + | + | NamedTuple.namedTupleCanEqual[N, V1, V2] + | + |But given instance namedTupleCanEqual in object NamedTuple does not match type CanEqual[(name : String, published : Int), (name : String, age : Int)]. diff --git a/tests/neg/named-tuples-strictEquality.scala b/tests/neg/named-tuples-strictEquality.scala new file mode 100644 index 000000000000..d7c3405404c9 --- /dev/null +++ b/tests/neg/named-tuples-strictEquality.scala @@ -0,0 +1,17 @@ +//> using options -language:strictEquality + +object Test: + + val u: (name: String, age: Int) = (name = "Bob", age = 25) + val v: (name: String, birthYear: Int) = (name = "Charlie", birthYear = 1990) + + val b1: Boolean = u.toTuple == v.toTuple // ok + val b2: Boolean = u == v // error + + val ScalaBook = (name = "Programming in Scala, 5th edition", published = 2021) + + def hasScalaBook(books: IterableOnce[(name: String, age: Int)]): Boolean = + books.exists { + case ScalaBook => true // error + case _ => false + } diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index 4cfb23e229ac..058f91937ecf 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -98,6 +98,9 @@ val experimentalDefinitionInLibrary = Set( // New feature: Erased trait "scala.compiletime.Erased", + + // New API: Multiversal equality for Named Tuples + "scala.NamedTuple$.namedTupleCanEqual", ) diff --git a/tests/run/named-tuples-strictEquality.scala b/tests/run/named-tuples-strictEquality.scala new file mode 100644 index 000000000000..56f0d6bd95c8 --- /dev/null +++ b/tests/run/named-tuples-strictEquality.scala @@ -0,0 +1,24 @@ +//> using options -language:strictEquality + +@main def Test = + + val u: (name: String, age: Int) = (name = "Bob", age = 25) + val v: (name: String, birthYear: Int) = (name = "Charlie", birthYear = 1990) + + val ScalaBook = (name = "Programming in Scala, 5th edition", published = 2021) + val books = Map( + ScalaBook, + (name = "Hands on Scala, 2nd edition", published = 2026), + ) + + assert(u.toTuple != v.toTuple) + assert(u == u) + assert(v == v) + + def hasScalaBook(books: IterableOnce[(name: String, published: Int)]): Boolean = + books.exists { + case ScalaBook => true + case _ => false + } + + assert(hasScalaBook(books))