From 10f83390c22a86844c859d6bc3671c8fa0021b04 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Sun, 11 May 2025 22:24:33 +0200 Subject: [PATCH 1/2] Add experimental NamedTuple copyFrom method `copyFrom` chosen as it has a slight difference i.e. the argument should be surrounded in extra parentheses. Also, it can not be used for target typing of fields. --- library/src/scala/NamedTuple.scala | 48 +++++++++++++++++-- library/src/scala/Tuple.scala | 27 +++++++++++ library/src/scala/runtime/Tuples.scala | 17 +++++++ tests/neg/named-tuple-ops-copy.scala | 16 +++++++ .../stdlibExperimentalDefinitions.scala | 16 ++++++- tests/run/named-tuple-ops-copy.scala | 29 +++++++++++ 6 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 tests/neg/named-tuple-ops-copy.scala create mode 100644 tests/run/named-tuple-ops-copy.scala diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index 1e4d3c084a3c..ef646c76f1fd 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -1,5 +1,8 @@ package scala import compiletime.ops.boolean.* +import compiletime.ops.int.* +import scala.annotation.experimental +import scala.annotation.implicitNotFound object NamedTuple: @@ -25,10 +28,10 @@ object NamedTuple: extension [V <: Tuple](x: V) inline def withNames[N <: Tuple]: NamedTuple[N, V] = x - import NamedTupleDecomposition.{Names, DropNames} + import NamedTupleDecomposition.{Names, DropNames, Decompose} export NamedTupleDecomposition.{ - Names, DropNames, - apply, size, init, head, last, tail, take, drop, splitAt, ++, map, reverse, zip, toList, toArray, toIArray + Names, DropNames, Decompose, + apply, size, init, head, last, tail, take, drop, splitAt, ++, copyFrom, map, reverse, zip, toList, toArray, toIArray } extension [N <: Tuple, V <: Tuple](x: NamedTuple[N, V]) @@ -116,6 +119,14 @@ object NamedTuple: case Names[Y] => NamedTuple[Names[X], Tuple.Zip[DropNames[X], DropNames[Y]]] + @experimental + type Copy[X <: AnyNamedTuple, Y <: AnyNamedTuple] = (Decompose[X], Decompose[Y]) match + case ((nx, vx), (ny, vy)) => + NamedTuple[ + nx, + NamedTupleDecomposition.Copy0[nx, vx, ny, vy, EmptyTuple] + ] + /** A type specially treated by the compiler to represent all fields of a * class argument `T` as a named tuple. Or, if `T` is already a named tuple, * `From[T]` is the same as `T`. @@ -174,6 +185,18 @@ object NamedTupleDecomposition: : Concat[NamedTuple[N, V], NamedTuple[N2, V2]] = x.toTuple ++ that.toTuple + /** The named tuple consisting of all elements of this tuple, with fields replaced by those from `that`. + * The field names of the update tuple of updates must all be present in this tuple, but not all fields are required. + */ + @experimental + inline def copyFrom[N2 <: Tuple, V2 <: Tuple](that: NamedTuple[N2, V2])(using Tuple.ContainsAll[N2, N] =:= true) + : Copy[NamedTuple[N, V], NamedTuple[N2, V2]] + = scala.runtime.Tuples.copy( + self = x.toTuple, + that = that.toTuple, + indices = compiletime.constValueTuple[Tuple.Indices[N, N2]] + ).asInstanceOf[Copy[NamedTuple[N, V], NamedTuple[N2, V2]]] + /** The named tuple consisting of all element values of this tuple mapped by * the polymorphic mapping function `f`. The names of elements are preserved. * If `x = (n1 = v1, ..., ni = vi)` then `x.map(f) = `(n1 = f(v1), ..., ni = f(vi))`. @@ -205,6 +228,21 @@ object NamedTupleDecomposition: end extension + @experimental + type LookupName[X, N <: Tuple, V <: Tuple] <: Option[Any] = + (N, V) match + case (X *: _, v *: _) => Some[v] + case (_ *: ns, _ *: vs) => LookupName[X, ns, vs] + case (EmptyTuple, EmptyTuple) => None.type + + @experimental + type Copy0[Nx <: Tuple, Vx <: Tuple, Ny <: Tuple, Vy <: Tuple, Acc <: Tuple] <: Tuple = + (Nx, Vx) match + case (nx *: nxs, vx *: vxs) => LookupName[nx, Ny, Vy] match + case Some[vy] => Copy0[nxs, vxs, Ny, Vy, vy *: Acc] + case _ => Copy0[nxs, vxs, Ny, Vy, vx *: Acc] + case (EmptyTuple, EmptyTuple) => Tuple.Reverse[Acc] + /** The names of a named tuple, represented as a tuple of literal string values. */ type Names[X <: AnyNamedTuple] <: Tuple = X match case NamedTuple[n, _] => n @@ -212,3 +250,7 @@ object NamedTupleDecomposition: /** The value types of a named tuple represented as a regular tuple. */ type DropNames[NT <: AnyNamedTuple] <: Tuple = NT match case NamedTuple[_, x] => x + + @experimental + type Decompose[NT <: AnyNamedTuple] <: Tuple = NT match + case NamedTuple[n, x] => (n, x) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 57d1572772e2..61917d3d8861 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -3,6 +3,7 @@ package scala import annotation.showAsInfix import compiletime.* import compiletime.ops.int.* +import scala.annotation.experimental /** Tuple of arbitrary arity */ sealed trait Tuple extends Product { @@ -281,6 +282,32 @@ object Tuple { case false => Disjoint[xs, Y] case EmptyTuple => true + @experimental + type ContainsAll[X <: Tuple, Y <: Tuple] <: Boolean = X match + case x *: xs => Contains[Y, x] match + case true => ContainsAll[xs, Y] + case false => false + case EmptyTuple => true + + @experimental + type IndexOfOptionOnto[X, N <: Tuple, Acc <: Int] <: Option[Int] = N match + case X *: _ => Some[Acc] + case _ *: ns => IndexOfOptionOnto[X, ns, S[Acc]] + case EmptyTuple => None.type + + @experimental + type IndexOfOption[X, N <: Tuple] = IndexOfOptionOnto[X, N, 0] + + @experimental + type Indices[X <: Tuple, Y <: Tuple] = IndicesOnto[X, Y, 0, EmptyTuple] + + @experimental + type IndicesOnto[X <: Tuple, Y <: Tuple, Idx <: Int, Acc <: Tuple] <: Tuple = X match + case x *: xs => IndexOfOption[x, Y] match + case Some[i] => IndicesOnto[xs, Y, S[Idx], i *: Acc] + case _ => IndicesOnto[xs, Y, S[Idx], (-1 * S[Idx]) *: Acc] // no problem if Int overflow + case EmptyTuple => Reverse[Acc] + /** Empty tuple */ def apply(): EmptyTuple = EmptyTuple diff --git a/library/src/scala/runtime/Tuples.scala b/library/src/scala/runtime/Tuples.scala index 66dc486d2a1d..60bbb8a86398 100644 --- a/library/src/scala/runtime/Tuples.scala +++ b/library/src/scala/runtime/Tuples.scala @@ -1,5 +1,7 @@ package scala.runtime +import scala.annotation.experimental + object Tuples { inline val MaxSpecialized = 22 @@ -283,6 +285,21 @@ object Tuples { case self: Product => self.productArity } + @experimental + def copy(self: Tuple, that: Tuple, indices: Tuple): Tuple = indices match + case EmptyTuple => self + case _ => + val is = indices.productIterator.asInstanceOf[Iterator[Int]] + val arr = IArray.from( + is.map: i => + if i < 0 then + val i0 = Math.abs(i) - 1 // nice that it is correct even for min value + self.productElement(i0) + else + that.productElement(i) + ) + Tuple.fromIArray(arr) + // Tail for Tuple1 to Tuple22 private def specialCaseTail(self: Tuple): Tuple = { (self: Any) match { diff --git a/tests/neg/named-tuple-ops-copy.scala b/tests/neg/named-tuple-ops-copy.scala new file mode 100644 index 000000000000..d5ec2c6fa34e --- /dev/null +++ b/tests/neg/named-tuple-ops-copy.scala @@ -0,0 +1,16 @@ +//> using options -experimental + +trait Mod + +given Conversion[String, Mod] = _ => new Mod {} + +type Foo = (name: String, mod: Mod) +case class Foo0(name: String, mod: Mod) + +@main def Test = + val foo: Foo = (name = "foo", mod = "some_mod") + val foo_updated: Foo = foo.copyFrom((mod = "bar")) // error, stays as String + + + val foo0: Foo0 = Foo0(name = "foo", mod = "some_mod") + val foo0_updated: Foo0 = foo0.copy(mod = "bar") // ok - does the conversion diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index 36075f0a2cee..b2d6f48cb70a 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -105,7 +105,21 @@ val experimentalDefinitionInLibrary = Set( "scala.Predef$.runtimeChecked", "scala.annotation.internal.RuntimeChecked", // New feature: SIP 61 - @unroll annotation - "scala.annotation.unroll" + "scala.annotation.unroll", + + // New experimental method - copyFrom in NamedTuple.scala (and supporting definitions) + "scala.NamedTuple$.copyFrom", "scala.NamedTuple$.Copy", + "scala.NamedTuple$.Decompose", + "scala.NamedTupleDecomposition$.Copy0", + "scala.NamedTupleDecomposition$.Decompose", + "scala.NamedTupleDecomposition$.LookupName", + "scala.NamedTupleDecomposition$.copyFrom", + "scala.Tuple$.ContainsAll", + "scala.Tuple$.IndexOfOption", + "scala.Tuple$.IndexOfOptionOnto", + "scala.Tuple$.Indices", + "scala.Tuple$.IndicesOnto", + "scala.runtime.Tuples$.copy", ) diff --git a/tests/run/named-tuple-ops-copy.scala b/tests/run/named-tuple-ops-copy.scala new file mode 100644 index 000000000000..c5f00cd3ac18 --- /dev/null +++ b/tests/run/named-tuple-ops-copy.scala @@ -0,0 +1,29 @@ +//> using options -experimental + +type City = (name: String, zip: Int, pop: Int) +type Coord = (x: Double, y: Double) +type Labels = (x: String, y: String) + +@main def Test = + val city: City = (name = "Lausanne", zip = 1000, pop = 140000) + val coord: Coord = (x = 1.0, y = 0.0) + val labels: Labels = (x = "west", y = "north") + + // first field updated + val coord_update = coord.copyFrom((x = 2.0)) + val _: Coord = coord_update + assert(coord_update.x == 2.0 && coord_update.y == 0.0) + + // last field updated + val city_update = city.copyFrom((pop = 150000)) + val _: City = city_update + assert(city_update.name == "Lausanne" && city_update.zip == 1000 && city_update.pop == 150000) + + // replace field types + val coord_to_labels = coord.copyFrom((x = "east", y = "south")) + val _: Labels = coord_to_labels + assert(coord_to_labels.x == "east" && coord_to_labels.y == "south") + + // out of order + val city_update2 = city.copyFrom((pop = 150000, name = "Lausanne", zip = 1000)) + val _: City = city_update2 From d3fdfe08ae410629d1ae5fe1b27c8e7ce412ed53 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Tue, 13 May 2025 21:59:33 +0200 Subject: [PATCH 2/2] fix mima filters --- project/MiMaFilters.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index a7f857e8a719..c9fe006e7a24 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -13,6 +13,9 @@ object MiMaFilters { // Scala.js-only class ProblemFilters.exclude[FinalClassProblem]("scala.scalajs.runtime.AnonFunctionXXL"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.scalajs.runtime.AnonFunctionXXL.this"), + + // NamedTuples copy method + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.copy") ), // Additions since last LTS