Skip to content

Commit

Permalink
Redefine Tuple operations
Browse files Browse the repository at this point in the history
This provides a way forward to fixing the signatures of some tuple
methods, and removes the inlining from the tuple methods. Optimization
will be implemented later directly on these method calls, which avoids
unnecessary complications due to inlining artifacts.

Fixes scala#12721
Fixes scala#16207
  • Loading branch information
nicolasstucki committed Dec 20, 2023
1 parent 43c5c55 commit 0c8e3dc
Show file tree
Hide file tree
Showing 15 changed files with 173 additions and 37 deletions.
1 change: 1 addition & 0 deletions compiler/test/dotc/pos-test-pickling.blacklist
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ i17149.scala
tuple-fold.scala
mt-redux-norm.perspective.scala
i18211.scala
i15743.scala

# Opaque type
i5720.scala
Expand Down
138 changes: 127 additions & 11 deletions library/src-bootstrapped/scala/Tuple.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,40 @@ sealed trait Tuple extends Product {
import Tuple.*

/** Create a copy of this tuple as an Array */
private[Tuple]
inline def toArray: Array[Object] =
runtime.Tuples.toArray(this)

/** Create a copy of this tuple as a List */
private[Tuple]
inline def toList: List[Union[this.type]] =
this.productIterator.toList
.asInstanceOf[List[Union[this.type]]]

/** Create a copy of this tuple as an IArray */
private[Tuple]
inline def toIArray: IArray[Object] =
runtime.Tuples.toIArray(this)

/** Return a copy of `this` tuple with an element appended */
inline def :* [This >: this.type <: Tuple, L] (x: L): Append[This, L] =
private[Tuple]
runtime.Tuples.append(x, this).asInstanceOf[Append[This, L]]

/** Return a new tuple by prepending the element to `this` tuple.
* This operation is O(this.size)
*/
private[Tuple]
inline def *: [H, This >: this.type <: Tuple] (x: H): H *: This =
runtime.Tuples.cons(x, this).asInstanceOf[H *: This]

/** Return a new tuple by concatenating `this` tuple with `that` tuple.
* This operation is O(this.size + that.size)
*/
private[Tuple]
inline def ++ [This >: this.type <: Tuple](that: Tuple): Concat[This, that.type] =
runtime.Tuples.concat(this, that).asInstanceOf[Concat[This, that.type]]

/** Return the size (or arity) of the tuple */
private[Tuple]
inline def size[This >: this.type <: Tuple]: Size[This] =
runtime.Tuples.size(this).asInstanceOf[Size[This]]

Expand All @@ -48,6 +53,7 @@ sealed trait Tuple extends Product {
* tuple types has a `EmptyTuple` tail. Otherwise the result type is
* `(A1, B1) *: ... *: (Ai, Bi) *: Tuple`
*/
private[Tuple]
inline def zip[This >: this.type <: Tuple, T2 <: Tuple](t2: T2): Zip[This, T2] =
runtime.Tuples.zip(this, t2).asInstanceOf[Zip[This, T2]]

Expand All @@ -56,39 +62,140 @@ sealed trait Tuple extends Product {
* If the tuple is of the form `a1 *: ... *: Tuple` (that is, the tail is not known
* to be the cons type.
*/
private[Tuple]
inline def map[F[_]](f: [t] => t => F[t]): Map[this.type, F] =
runtime.Tuples.map(this, f).asInstanceOf[Map[this.type, F]]

/** Given a tuple `(a1, ..., am)`, returns the tuple `(a1, ..., an)` consisting
* of its first n elements.
*/
private[Tuple]
inline def take[This >: this.type <: Tuple](n: Int): Take[This, n.type] =
runtime.Tuples.take(this, n).asInstanceOf[Take[This, n.type]]


/** Given a tuple `(a1, ..., am)`, returns the tuple `(an+1, ..., am)` consisting
* all its elements except the first n ones.
*/
private[Tuple]
inline def drop[This >: this.type <: Tuple](n: Int): Drop[This, n.type] =
runtime.Tuples.drop(this, n).asInstanceOf[Drop[This, n.type]]

/** Given a tuple `(a1, ..., am)`, returns a pair of the tuple `(a1, ..., an)`
* consisting of the first n elements, and the tuple `(an+1, ..., am)` consisting
* of the remaining elements.
*/
private[Tuple]
inline def splitAt[This >: this.type <: Tuple](n: Int): Split[This, n.type] =
runtime.Tuples.splitAt(this, n).asInstanceOf[Split[This, n.type]]

/** Given a tuple `(a1, ..., am)`, returns the reversed tuple `(am, ..., a1)`
* consisting all its elements.
*/
@experimental
inline def reverse[This >: this.type <: Tuple]: Reverse[This] =
runtime.Tuples.reverse(this).asInstanceOf[Reverse[This]]
}

object Tuple {

// TODO should it be `extension [H](x: H) def *:(tail: Tuple): H *: tuple.type` ?
extension [H, Tail <: Tuple](x: H)
/** Return a new tuple by prepending the element to `tail` tuple.
* This operation is O(tail.size)
*/
def *:(tail: Tail): H *: Tail = runtime.Tuples.cons(x, tail).asInstanceOf[H *: Tail]

extension [This <: Tuple](tuple: This)
/** Get the head of this tuple */
def head: Head[This] & Head[tuple.type] =
runtime.Tuples.apply(tuple, 0).asInstanceOf[Head[This] & Head[tuple.type]]

/** Get the tail of this tuple.
* This operation is O(tuple.size)
*/
def tail: Tail[This] & Tail[tuple.type] =
runtime.Tuples.tail(tuple).asInstanceOf[Tail[This] & Tail[tuple.type]]

/** Return the size (or arity) of the tuple */
def size: Size[This] & Size[tuple.type] =
runtime.Tuples.size(tuple).asInstanceOf[Size[This] & Size[tuple.type]]

/** Get the i-th element of this tuple.
* Equivalent to productElement but with a precise return type.
*/
def apply(n: Int): Elem[This, n.type] & Elem[tuple.type, n.type] =
runtime.Tuples.apply(tuple, n).asInstanceOf[Elem[This, n.type] & Elem[tuple.type, n.type]]

/** Get the initial part of the tuple without its last element */
def init: Init[This] & Init[tuple.type] =
runtime.Tuples.init(tuple).asInstanceOf[Init[This] & Init[tuple.type]]

/** Get the last of this tuple */
def last: Last[This] & Last[tuple.type] =
runtime.Tuples.last(tuple).asInstanceOf[Last[This] & Last[tuple.type]]

/** Return a copy of `tuple` with an element appended */
def :*[X] (x: X): Append[This, X] & Append[tuple.type, X] =
runtime.Tuples.append(x, tuple).asInstanceOf[Append[This, X] & Append[tuple.type, X]]

/** Return a new tuple by concatenating `this` tuple with `that` tuple.
* This operation is O(this.size + that.size)
*/
def ++(that: Tuple): Concat[This, that.type] & Concat[tuple.type, that.type] =
runtime.Tuples.concat(tuple, that).asInstanceOf[Concat[This, that.type] & Concat[tuple.type, that.type]]

/** Given a tuple `(a1, ..., am)`, returns the reversed tuple `(am, ..., a1)`
* consisting all its elements.
*/
@experimental
def reverse: Reverse[This] & Reverse[tuple.type] =
runtime.Tuples.reverse(tuple).asInstanceOf[Reverse[This] & Reverse[tuple.type]]

/** 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 `EmptyTuple` tail. Otherwise the result type is
* `(A1, B1) *: ... *: (Ai, Bi) *: Tuple`
*/
// TODO change signature? def zip[That <: Tuple](that: That): Zip[This, tuple.type] & Zip[tuple.type, tuple.type] =
def zip[That <: Tuple](that: That): Zip[This, That] & Zip[tuple.type, That] =
runtime.Tuples.zip(tuple, that).asInstanceOf[Zip[This, That] & Zip[tuple.type, That]]

/** 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.
*/
def map[F[_]](f: [t] => t => F[t]): Map[This, F] & Map[tuple.type, F] =
runtime.Tuples.map(tuple, f).asInstanceOf[Map[This, F] & Map[tuple.type, F]]

/** Given a tuple `(a1, ..., am)`, returns the tuple `(a1, ..., an)` consisting
* of its first n elements.
*/
def take(n: Int): Take[This, n.type] & Take[tuple.type, n.type] =
runtime.Tuples.take(tuple, n).asInstanceOf[Take[This, n.type] & Take[tuple.type, n.type]]

/** Given a tuple `(a1, ..., am)`, returns the tuple `(an+1, ..., am)` consisting
* all its elements except the first n ones.
*/
def drop(n: Int): Drop[This, n.type] & Take[tuple.type, n.type] =
runtime.Tuples.drop(tuple, n).asInstanceOf[Drop[This, n.type] & Take[tuple.type, n.type]]

/** Given a tuple `(a1, ..., am)`, returns a pair of the tuple `(a1, ..., an)`
* consisting of the first n elements, and the tuple `(an+1, ..., am)` consisting
* of the remaining elements.
*/
def splitAt(n: Int): Split[This, n.type] & Split[tuple.type, n.type] =
runtime.Tuples.splitAt(tuple, n).asInstanceOf[Split[This, n.type] & Split[tuple.type, n.type]]

/** Create a copy of this tuple as a List */
def toList: List[Union[This]] & List[Union[tuple.type]] =
tuple.productIterator.toList.asInstanceOf[List[Union[This]] & List[Union[tuple.type]]]
end extension

extension (tuple: Tuple)
/** Create a copy of this tuple as an Array */
def toArray: Array[AnyRef] = runtime.Tuples.toArray(tuple)

/** Create a copy of this tuple as an IArray */
def toIArray: IArray[AnyRef] = runtime.Tuples.toIArray(tuple)
end extension

/** Type of a tuple with an element appended */
type Append[X <: Tuple, Y] <: NonEmptyTuple = X match {
case EmptyTuple => Y *: EmptyTuple
Expand All @@ -98,24 +205,27 @@ object Tuple {
/** Type of the head of a tuple */
type Head[X <: Tuple] = X match {
case x *: _ => x
case EmptyTuple => Nothing
}

/** Type of the initial part of the tuple without its last element */
type Init[X <: Tuple] <: Tuple = X match {
case _ *: EmptyTuple => EmptyTuple
case x *: xs =>
x *: Init[xs]
case x *: xs => x *: Init[xs]
case EmptyTuple => Nothing
}

/** Type of the tail of a tuple */
type Tail[X <: Tuple] <: Tuple = X match {
case _ *: xs => xs
case EmptyTuple => Nothing
}

/** Type of the last element of a tuple */
type Last[X <: Tuple] = X match {
case x *: EmptyTuple => x
case _ *: xs => Last[xs]
case EmptyTuple => Nothing
}

/** Type of the concatenation of two tuples */
Expand Down Expand Up @@ -180,6 +290,7 @@ object Tuple {
* returns the tuple type `(A1, B1) *: ... *: (An, Bn) *: Ct`
* where `Ct` is `EmptyTuple` if `At` or `Bt` is `EmptyTuple`, otherwise `Ct` is `Tuple`.
*/
// TODO should zip be covariant? type Zip[T1 <: Tuple, +T2 <: Tuple] <: Tuple = ...
type Zip[T1 <: Tuple, T2 <: Tuple] <: Tuple = (T1, T2) match {
case (h1 *: t1, h2 *: t2) => (h1, h2) *: Zip[t1, t2]
case (EmptyTuple, _) => EmptyTuple
Expand Down Expand Up @@ -294,24 +405,29 @@ sealed trait NonEmptyTuple extends Tuple {
/** Get the i-th element of this tuple.
* Equivalent to productElement but with a precise return type.
*/
private[NonEmptyTuple]
inline def apply[This >: this.type <: NonEmptyTuple](n: Int): Elem[This, n.type] =
runtime.Tuples.apply(this, n).asInstanceOf[Elem[This, n.type]]

/** Get the head of this tuple */
private[NonEmptyTuple]
inline def head[This >: this.type <: NonEmptyTuple]: Head[This] =
runtime.Tuples.apply(this, 0).asInstanceOf[Head[This]]

/** Get the initial part of the tuple without its last element */
private[NonEmptyTuple]
inline def init[This >: this.type <: NonEmptyTuple]: Init[This] =
runtime.Tuples.init(this).asInstanceOf[Init[This]]

/** Get the last of this tuple */
private[NonEmptyTuple]
inline def last[This >: this.type <: NonEmptyTuple]: Last[This] =
runtime.Tuples.last(this).asInstanceOf[Last[This]]

/** Get the tail of this tuple.
* This operation is O(this.size)
*/
private[NonEmptyTuple]
inline def tail[This >: this.type <: NonEmptyTuple]: Tail[This] =
runtime.Tuples.tail(this).asInstanceOf[Tail[This]]
}
Expand Down
12 changes: 8 additions & 4 deletions library/src/scala/runtime/Tuples.scala
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ object Tuples {
// Tail for Tuple1 to Tuple22
private def specialCaseTail(self: Tuple): Tuple = {
(self: Any) match {
case self: EmptyTuple =>
throw new NoSuchElementException("tail of empty tuple")
case self: Tuple1[?] =>
EmptyTuple
case self: Tuple2[?, ?] =>
Expand Down Expand Up @@ -352,7 +354,7 @@ object Tuples {
}
}

def tail(self: NonEmptyTuple): Tuple = (self: Any) match {
def tail(self: Tuple): Tuple = (self: Any) match {
case xxl: TupleXXL => xxlTail(xxl)
case _ => specialCaseTail(self)
}
Expand Down Expand Up @@ -514,6 +516,8 @@ object Tuples {
// Init for Tuple1 to Tuple22
private def specialCaseInit(self: Tuple): Tuple = {
(self: Any) match {
case self: EmptyTuple =>
throw new NoSuchElementException("init of empty tuple")
case _: Tuple1[?] =>
EmptyTuple
case self: Tuple2[?, ?] =>
Expand Down Expand Up @@ -561,16 +565,16 @@ object Tuples {
}
}

def init(self: NonEmptyTuple): Tuple = (self: Any) match {
def init(self: Tuple): Tuple = (self: Any) match {
case xxl: TupleXXL => xxlInit(xxl)
case _ => specialCaseInit(self)
}

def last(self: NonEmptyTuple): Any = (self: Any) match {
def last(self: Tuple): Any = (self: Any) match {
case self: Product => self.productElement(self.productArity - 1)
}

def apply(self: NonEmptyTuple, n: Int): Any =
def apply(self: Tuple, n: Int): Any =
self.productElement(n)

// Benchmarks showed that this is faster than doing (it1 zip it2).copyToArray(...)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package transformers

class InheritanceInformationTransformer(using DocContext) extends (Module => Module):
override def apply(original: Module): Module =
val subtypes = getSupertypes(original.rootPackage).groupMap(_(0))(_(1)).view.mapValues(_.distinct).toMap
val subtypes = getSupertypes(original.rootPackage).groupMap(_._1)(_._2).view.mapValues(_.distinct).toMap
original.updateMembers { m =>
val edges = getEdges(m.asLink.copy(kind = bareClasslikeKind(m.kind)), subtypes)
val st: Seq[LinkToType] = edges.map(_._1).distinct
Expand Down
4 changes: 2 additions & 2 deletions tests/neg/i13780-1.check
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
-- [E007] Type Mismatch Error: tests/neg/i13780-1.scala:38:24 ----------------------------------------------------------
-- [E007] Type Mismatch Error: tests/neg/i13780-1.scala:38:26 ----------------------------------------------------------
38 | case x: (h *: t) => x.head // error
| ^^^^^^
| Found: Tuple.Head[VS & h *: t]
| Found: Tuple.Head[VS & h *: t] & Tuple.Head[(x : VS & h *: t)]
| Required: h
|
| where: VS is a type in method foo with bounds <: Tuple
Expand Down
4 changes: 2 additions & 2 deletions tests/neg/i13780-1.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* Note that the code can be fixed with an explicit type argument to `.head`:
*
* def foo[VS <: Tuple](x: VS): SelectH[VS] = x match
* case x: (h *: t) => x.head[h *: t]
* case x: (h *: t) => Tuple.head[h *: t](x)
*
* So it *seems* like it would be fine to relax the rule, based on the insight
* that `VS` in `Tuple.Head[VS & (h *: t)]` does not contribute anything to the
Expand All @@ -38,7 +38,7 @@ object ExampleFromSpata:
case x: (h *: t) => x.head // error

def bar[VS <: Tuple](x: VS): SelectH[VS] = x match
case x: (h *: t) => x.head[h *: t] // ok
case x: (h *: t) => Tuple.head[h *: t](x) // ok
end ExampleFromSpata

trait Z {
Expand Down
4 changes: 4 additions & 0 deletions tests/pos/Tuple_apply.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
def testTuple(tup: Tuple) = tup(0)
def testNonEmptyTuple(tup: NonEmptyTuple) = tup(0)
def testConsUnbound(tup: Any *: Tuple) = tup(0)
def testCons(tup: Any *: EmptyTuple) = tup(0)
3 changes: 3 additions & 0 deletions tests/pos/i12721.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
def bar(t: Any): Int = 1
def foo(t: AnyRef): Unit =
t.asInstanceOf[NonEmptyTuple].toList.map(bar)
2 changes: 1 addition & 1 deletion tests/pos/i15743.gadt.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ class Alt:
case c1 @ C1() => // GADT constr: T := Tuple
val t1: T = c1.getZ
val t2: Int *: T = (1: Int) *: t1
val i1: Int = (t2: Int *: T).head[Int *: T]
val i1: Int = (t2: Int *: T).head
2 changes: 1 addition & 1 deletion tests/pos/i15743.pass.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Like pos/i15743 but already passed, because the bounds are never lost so reduction never fails
class Pass:
def pass[T >: Tuple <: Tuple](t2: Int *: T) =
val i1: Int = (t2: Int *: T).head[Int *: T]
val i1: Int = (t2: Int *: T).head
2 changes: 1 addition & 1 deletion tests/pos/i15743.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ case class Bar[T <: Tuple](val x: Int *: T)

class Test:
def fail(e: Any): Int =
e match { case b: Bar[t] => b.x.head[Int *: t] }
e match { case b: Bar[t] => b.x.head }
8 changes: 8 additions & 0 deletions tests/pos/i16207.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import scala.compiletime.constValueTuple
import scala.deriving.Mirror.ProductOf

case class C(date: Int, time: Int)

inline def labelsOf[A](using p: ProductOf[A]): Tuple = constValueTuple[p.MirroredElemLabels]

val headers: List[String] = labelsOf[C].toList.map(_.toString)
Loading

0 comments on commit 0c8e3dc

Please sign in to comment.