diff --git a/compiler/test/dotc/run-test-pickling.blacklist b/compiler/test/dotc/run-test-pickling.blacklist index 6f44013b916d..f6dab8b29bcd 100644 --- a/compiler/test/dotc/run-test-pickling.blacklist +++ b/compiler/test/dotc/run-test-pickling.blacklist @@ -18,3 +18,4 @@ t8905 t10889 i5257.scala enum-java +tuple-ops.scala diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 87fb5c2073ec..3ba1f121c347 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -33,6 +33,23 @@ sealed trait Tuple extends Any { inline def size[This >: this.type <: Tuple]: Size[This] = DynamicTuple.dynamicSize(this) + /** Given two tuples, `(a1, ..., an)` and `(a1, ..., an)`, returns a tuple + * `((a1, b1), ..., (an, bn))`. If the two tuples have different sizes, + * the extra elements of the larger tuple will be disregarded. + * The result is typed as `((A1, B1), ..., (An, Bn))` if at least one of the + * tuple types has a `Unit` tail. Otherwise the result type is + * `(A1, B1) *: ... *: (Ai, Bi) *: Tuple` + */ + inline def zip[This >: this.type <: Tuple, T2 <: Tuple](t2: T2): Zip[This, T2] = + DynamicTuple.dynamicZip(this, t2) + + /** Called on a tuple `(a1, ..., an)`, returns a new tuple `(f(a1), ..., f(an))`. + * The result is typed as `(F[A1], ..., F[An])` if the tuple type is fully known. + * If the tuple is of the form `a1 *: ... *: Tuple` (that is, the tail is not known + * to be the cons type. + */ + inline def map[F[_]](f: [t] => t => F[t]): Map[this.type, F] = + DynamicTuple.dynamicMap(this, f) } object Tuple { @@ -74,6 +91,18 @@ object Tuple { case h *: t => F[h] *: Map[t, F] } + /** Given two tuples, `A1 *: ... *: An * At` and `B1 *: ... *: Bn *: Bt` + * where at least one of `At` or `Bt` is `Unit` or `Tuple`, + * returns the tuple type `(A1, B1) *: ... *: (An, Bn) *: Ct` + * where `Ct` is `Unit` if `At` or `Bt` is `Unit`, otherwise `Ct` is `Tuple`. + */ + type Zip[T1 <: Tuple, T2 <: Tuple] <: Tuple = (T1, T2) match { + case (h1 *: t1, h2 *: t2) => (h1, h2) *: Zip[t1, t2] + case (Unit, _) => Unit + case (_, Unit) => Unit + case _ => Tuple + } + /** Convert an array into a tuple of unknown arity and types */ def fromArray[T](xs: Array[T]): Tuple = { val xs2 = xs match { @@ -98,6 +127,8 @@ object Tuple { def fromProduct(product: Product): Tuple = runtime.DynamicTuple.dynamicFromProduct[Tuple](product) + def fromProductTyped[P <: Product](p: P) given (m: scala.deriving.Mirror.ProductOf[P]): m.MirroredElemTypes = + Tuple.fromArray(p.productIterator.toArray).asInstanceOf[m.MirroredElemTypes] // TODO use toIArray of Object to avoid double/triple array copy } /** Tuple of arbitrary non-zero arity */ diff --git a/library/src/scala/runtime/DynamicTuple.scala b/library/src/scala/runtime/DynamicTuple.scala index fa9c90cffd04..031b36a4eefd 100644 --- a/library/src/scala/runtime/DynamicTuple.scala +++ b/library/src/scala/runtime/DynamicTuple.scala @@ -1,10 +1,6 @@ package scala.runtime -import scala.Tuple.Concat -import scala.Tuple.Size -import scala.Tuple.Head -import scala.Tuple.Tail -import scala.Tuple.Elem +import scala.Tuple.{ Concat, Size, Head, Tail, Elem, Zip, Map } object DynamicTuple { inline val MaxSpecialized = 22 @@ -253,6 +249,18 @@ object DynamicTuple { res.asInstanceOf[Result] } + def dynamicZip[This <: Tuple, T2 <: Tuple](t1: This, t2: T2): Zip[This, T2] = { + if (t1.size == 0 || t2.size == 0) ().asInstanceOf[Zip[This, T2]] + else Tuple.fromArray( + t1.asInstanceOf[Product].productIterator.zip( + t2.asInstanceOf[Product].productIterator).toArray // TODO use toIArray of Object to avoid double/triple array copy + ).asInstanceOf[Zip[This, T2]] + } + + def dynamicMap[This <: Tuple, F[_]](self: This, f: [t] => t => F[t]): Map[This, F] = + Tuple.fromArray(self.asInstanceOf[Product].productIterator.map(f(_)).toArray) // TODO use toIArray of Object to avoid double/triple array copy + .asInstanceOf[Map[This, F]] + def consIterator(head: Any, tail: Tuple): Iterator[Any] = Iterator.single(head) ++ tail.asInstanceOf[Product].productIterator diff --git a/tests/neg/product-to-tuple.scala b/tests/neg/product-to-tuple.scala new file mode 100644 index 000000000000..52aa14b49ead --- /dev/null +++ b/tests/neg/product-to-tuple.scala @@ -0,0 +1,8 @@ +case class Foo(x: Int, y: String) + +val x = Foo(1, "2") +val y: (Int, Boolean) = Tuple.fromProductTyped(x) // error + +case class Bar[I <: Int, S <: String](x: I, y: S) +val a: Bar[1, "2"] = Bar(1, "2") +val b: (1, "3") = Tuple.fromProductTyped(a) // error diff --git a/tests/neg/tuple-ops.scala b/tests/neg/tuple-ops.scala new file mode 100644 index 000000000000..4e787c1932c8 --- /dev/null +++ b/tests/neg/tuple-ops.scala @@ -0,0 +1,26 @@ +val a: (1, 2, 3) = (1, 2, 3) +val b: (4, 5, 6) = (4, 5, 6) +val c: (7, 8) = (7, 8) +val d: Unit = () +val e: (1, "foo", 10.1) = (1, "foo", 10.1) + +// Zip +val r1: ((1, 4), (2, 5), (6, 6)) = a.zip(b) // error +val r2: ((1, 7), (1, 8)) = a.zip(c) // error +val r3: ((2, 1), (8, 2)) = c.zip(a) // error + +// Map +case class Foo[X](x: X) + +val r6: (Int, Int, String) = a.map[[t] =>> Int]([t] => x: t => x match { // error + case x: Int => x * x + case _ => ??? +}) + +val r7: ((1, Foo[1]), (2), (3, Foo[3])) = + a.map[[t] =>> (t, Foo[t])]( [t] => x: t => (x, Foo(x)) ) // error + +// More Zip +val t1: Int *: Long *: Tuple = (1, 2l, 100, 200) +val t2: Int *: Char *: Tuple = (1, 'c', 33, 42) +val t3: (Int, Int) *: (Long, Long) *: Tuple = t1.zip(t2) // error diff --git a/tests/pos/product-to-tuple.scala b/tests/pos/product-to-tuple.scala new file mode 100644 index 000000000000..22677874e5ad --- /dev/null +++ b/tests/pos/product-to-tuple.scala @@ -0,0 +1,8 @@ +case class Foo(x: Int, y: String) + +val x = Foo(1, "2") +val y: (Int, String) = Tuple.fromProductTyped(x) + +case class Bar[I <: Int, S <: String](x: I, y: S) +val a: Bar[1, "2"] = Bar(1, "2") +val b: (1, "2") = Tuple.fromProductTyped(a) diff --git a/tests/run/tuple-ops.check b/tests/run/tuple-ops.check new file mode 100644 index 000000000000..8aa59e8206f6 --- /dev/null +++ b/tests/run/tuple-ops.check @@ -0,0 +1,9 @@ +((1,4),(2,5),(3,6)) +((1,7),(2,8)) +((7,1),(8,2)) +() +() +(1,4,9) +((1,Foo(1)),(2,Foo(2)),(3,Foo(3))) +((1,1),(2,c),(100,33),(200,42)) +() diff --git a/tests/run/tuple-ops.scala b/tests/run/tuple-ops.scala new file mode 100644 index 000000000000..8986464d50e9 --- /dev/null +++ b/tests/run/tuple-ops.scala @@ -0,0 +1,32 @@ +val a: (1, 2, 3) = (1, 2, 3) +val b: (4, 5, 6) = (4, 5, 6) +val c: (7, 8) = (7, 8) +val d: Unit = () + +// Zip +val r1: ((1, 4), (2, 5), (3, 6)) = a.zip(b) +val r2: ((1, 7), (2, 8)) = a.zip(c) +val r3: ((7, 1), (8, 2)) = c.zip(a) +val r4: Unit = d.zip(a) +val r5: Unit = a.zip(d) + +// Map +case class Foo[X](x: X) + +val r6: (Int, Int, Int) = a.map[[t] =>> Int]([t] => x: t => x match { + case x: Int => x * x + case _ => ??? +}) + +val r7: ((1, Foo[1]), (2, Foo[2]), (3, Foo[3])) = + a.map[[t] =>> (t, Foo[t])]( [t] => x: t => (x, Foo(x)) ) + +// More Zip +val t1: Int *: Long *: Tuple = (1, 2l, 100, 200) +val t2: Int *: Char *: Tuple = (1, 'c', 33, 42) +val t3: (Int, Int) *: (Long, Char) *: Tuple = t1.zip(t2) + +val t4: Unit = d.zip(d) + +@main def Test = + List(r1, r2, r3, r4, r5, r6, r7, t3, t4).foreach(println)