From 9d1f20d6f181f2c6a44aa227bb69fcbcb07c43bd Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 13 Dec 2023 12:25:25 +0100 Subject: [PATCH 01/68] Improvements to tuples: Drop experimental --- library/src/scala/Tuple.scala | 20 +++++++++---------- library/src/scala/runtime/Tuples.scala | 1 - .../stdlibExperimentalDefinitions.scala | 6 ------ 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 3738bd05a19b..663d124b2df5 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -1,6 +1,6 @@ package scala -import annotation.{experimental, showAsInfix} +import annotation.showAsInfix import compiletime.* import compiletime.ops.int.* @@ -65,7 +65,6 @@ sealed trait Tuple extends Product { 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. */ @@ -82,7 +81,6 @@ sealed trait Tuple extends Product { /** 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]] } @@ -201,14 +199,14 @@ object Tuple { type IsMappedBy[F[_]] = [X <: Tuple] =>> X =:= Map[InverseMap[X, F], F] /** Type of the reversed tuple */ - @experimental - type Reverse[X <: Tuple] = ReverseOnto[X, EmptyTuple] - - /** Prepends all elements of a tuple in reverse order onto the other tuple */ - @experimental - type ReverseOnto[From <: Tuple, +To <: Tuple] <: Tuple = From match - case x *: xs => ReverseOnto[xs, x *: To] - case EmptyTuple => To + type Reverse[X <: Tuple] = Helpers.ReverseImpl[EmptyTuple, X] + + object Helpers: + + /** Type of the reversed tuple */ + type ReverseImpl[Acc <: Tuple, X <: Tuple] <: Tuple = X match + case x *: xs => ReverseImpl[x *: Acc, xs] + case EmptyTuple => Acc /** Transforms a tuple `(T1, ..., Tn)` into `(T1, ..., Ti)`. */ type Take[T <: Tuple, N <: Int] <: Tuple = N match { diff --git a/library/src/scala/runtime/Tuples.scala b/library/src/scala/runtime/Tuples.scala index 41425e8559ba..81dca31e355e 100644 --- a/library/src/scala/runtime/Tuples.scala +++ b/library/src/scala/runtime/Tuples.scala @@ -505,7 +505,6 @@ object Tuples { } } - @experimental def reverse(self: Tuple): Tuple = (self: Any) match { case xxl: TupleXXL => xxlReverse(xxl) case _ => specialCaseReverse(self) diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index 76c08fa24213..b19ce1c2ebef 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -71,12 +71,6 @@ val experimentalDefinitionInLibrary = Set( "scala.quoted.Quotes.reflectModule.TermParamClauseMethods.erasedArgs", "scala.quoted.Quotes.reflectModule.TermParamClauseMethods.hasErasedArgs", - // New feature: reverse method on Tuple - "scala.Tuple.reverse", // can be stabilized in 3.5 - "scala.Tuple$.Reverse", // can be stabilized in 3.5 - "scala.Tuple$.ReverseOnto", // can be stabilized in 3.5 - "scala.runtime.Tuples$.reverse", // can be stabilized in 3.5 - // New feature: fromNullable for explicit nulls "scala.Predef$.fromNullable", ) From 2395ece1106a62f7841efc850415f6be11d55378 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 13 Dec 2023 14:54:23 +0100 Subject: [PATCH 02/68] Improvements to Tuples: New methods New methods: filter, indicesWhere, reverseOnto --- library/src/scala/Tuple.scala | 80 +++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 663d124b2df5..471bb228b4af 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -5,7 +5,7 @@ import compiletime.* import compiletime.ops.int.* /** Tuple of arbitrary arity */ -sealed trait Tuple extends Product { +sealed trait Tuple extends Product: import Tuple.* /** Create a copy of this tuple as an Array */ @@ -83,9 +83,23 @@ sealed trait Tuple extends Product { */ inline def reverse[This >: this.type <: Tuple]: Reverse[This] = runtime.Tuples.reverse(this).asInstanceOf[Reverse[This]] -} -object Tuple { + /** A tuple with the elements of this tuple in reversed order added in front of `acc` */ + inline def reverseOnto[This >: this.type <: Tuple, Acc <: Tuple](acc: Acc): ReverseOnto[This, Acc] = + (this.reverse ++ acc).asInstanceOf[ReverseOnto[This, Acc]] + + /** A tuple consisting of all elements of this tuple that have types + * for which the given type level predicate `P` reduces to the literal + * constant `true`. + */ + inline def filter[This >: this.type <: Tuple, P[_] <: Boolean]: Filter[This, P] = + val toInclude = constValueTuple[IndicesWhere[This, P]].toArray + val arr = new Array[Object](toInclude.length) + for i <- 0 until toInclude.length do + arr(i) = this.productElement(toInclude(i).asInstanceOf[Int]).asInstanceOf[Object] + Tuple.fromArray(arr).asInstanceOf[Filter[This, P]] + +object Tuple: /** Type of a tuple with an element appended */ type Append[X <: Tuple, Y] <: NonEmptyTuple = X match { @@ -165,25 +179,38 @@ object Tuple { * ``` * @syntax markdown */ - type Filter[Tup <: Tuple, P[_] <: Boolean] <: Tuple = Tup match { + type Filter[X <: Tuple, P[_] <: Boolean] <: Tuple = X match case EmptyTuple => EmptyTuple - case h *: t => P[h] match { + case h *: t => P[h] match case true => h *: Filter[t, P] case false => Filter[t, P] - } - } - /** Given two tuples, `A1 *: ... *: An * At` and `B1 *: ... *: Bn *: Bt` - * where at least one of `At` or `Bt` is `EmptyTuple` or `Tuple`, - * returns the tuple type `(A1, B1) *: ... *: (An, Bn) *: Ct` - * where `Ct` is `EmptyTuple` if `At` or `Bt` is `EmptyTuple`, otherwise `Ct` is `Tuple`. + /** A tuple consisting of those indices `N` of tuple `X` where the predicate `P` + * is true for `Elem[X, N]`. Indices are type level values <: Int. */ - type Zip[T1 <: Tuple, T2 <: Tuple] <: Tuple = (T1, T2) match { + type IndicesWhere[X <: Tuple, P[_] <: Boolean] = + helpers.IndicesWhereHelper[X, P, 0] + + /** The type of the tuple consisting of all element values of + * tuple `X` zipped with corresponding elements of tuple `Y`. + * If the two tuples have different sizes, + * the extra elements of the larger tuple will be disregarded. + * For example, if + * ``` + * X = (S1, ..., Si) + * Y = (T1, ..., Tj) where j >= i + * ``` + * then + * ``` + * Zip[X, Y] = ((S1, T1), ..., (Si, Ti)) + * ``` + * @syntax markdown + */ + type Zip[T1 <: Tuple, T2 <: Tuple] <: Tuple = (T1, T2) match case (h1 *: t1, h2 *: t2) => (h1, h2) *: Zip[t1, t2] case (EmptyTuple, _) => EmptyTuple case (_, EmptyTuple) => EmptyTuple case _ => Tuple - } /** Converts a tuple `(F[T1], ..., F[Tn])` to `(T1, ... Tn)` */ type InverseMap[X <: Tuple, F[_]] <: Tuple = X match { @@ -198,15 +225,13 @@ object Tuple { */ type IsMappedBy[F[_]] = [X <: Tuple] =>> X =:= Map[InverseMap[X, F], F] - /** Type of the reversed tuple */ - type Reverse[X <: Tuple] = Helpers.ReverseImpl[EmptyTuple, X] - - object Helpers: + /** A tuple with the elements of tuple `X` in reversed order */ + type Reverse[X <: Tuple] = ReverseOnto[X, EmptyTuple] - /** Type of the reversed tuple */ - type ReverseImpl[Acc <: Tuple, X <: Tuple] <: Tuple = X match - case x *: xs => ReverseImpl[x *: Acc, xs] - case EmptyTuple => Acc + /** A tuple with the elements of tuple `X` in reversed order added in front of `Acc` */ + type ReverseOnto[X <: Tuple, Acc <: Tuple] <: Tuple = X match + case x *: xs => ReverseOnto[xs, x *: Acc] + case EmptyTuple => Acc /** Transforms a tuple `(T1, ..., Tn)` into `(T1, ..., Ti)`. */ type Take[T <: Tuple, N <: Int] <: Tuple = N match { @@ -275,7 +300,18 @@ object Tuple { given canEqualTuple[H1, T1 <: Tuple, H2, T2 <: Tuple]( using eqHead: CanEqual[H1, H2], eqTail: CanEqual[T1, T2] ): CanEqual[H1 *: T1, H2 *: T2] = CanEqual.derived -} + + object helpers: + + /** Used to implement IndicesWhere */ + type IndicesWhereHelper[X <: Tuple, P[_] <: Boolean, N <: Int] <: Tuple = X match + case EmptyTuple => EmptyTuple + case h *: t => P[h] match + case true => N *: IndicesWhereHelper[t, P, S[N]] + case false => IndicesWhereHelper[t, P, S[N]] + + end helpers +end Tuple /** A tuple of 0 elements */ type EmptyTuple = EmptyTuple.type From 64a79c914ce0dc71306b9cb57546923b78191cb3 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 13 Dec 2023 15:00:11 +0100 Subject: [PATCH 03/68] Improvements to tuples: Allow prefix slice in fromArray --- library/src/scala/Tuple.scala | 15 +++++++++++---- library/src/scala/runtime/Tuples.scala | 11 ++++++++--- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 471bb228b4af..a5967686ad3e 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -271,22 +271,29 @@ object Tuple: def unapply(x: EmptyTuple): true = true /** Convert an array into a tuple of unknown arity and types */ - def fromArray[T](xs: Array[T]): Tuple = { + def fromArray[T](xs: Array[T]): Tuple = + fromArray(xs, xs.length) + + /** Convert the first `n` elements of an array into a tuple of unknown arity and types */ + def fromArray[T](xs: Array[T], n: Int): Tuple = { val xs2 = xs match { case xs: Array[Object] => xs case xs => xs.map(_.asInstanceOf[Object]) } - runtime.Tuples.fromArray(xs2) + runtime.Tuples.fromArray(xs2, n) } /** Convert an immutable array into a tuple of unknown arity and types */ - def fromIArray[T](xs: IArray[T]): Tuple = { + def fromIArray[T](xs: IArray[T]): Tuple = fromIArray(xs, xs.length) + + /** Convert the first `n` elements of an immutable array into a tuple of unknown arity and types */ + def fromIArray[T](xs: IArray[T], n: Int): Tuple = { val xs2: IArray[Object] = xs match { case xs: IArray[Object] @unchecked => xs case _ => xs.map(_.asInstanceOf[Object]) } - runtime.Tuples.fromIArray(xs2) + runtime.Tuples.fromIArray(xs2, n) } /** Convert a Product into a tuple of unknown arity and types */ diff --git a/library/src/scala/runtime/Tuples.scala b/library/src/scala/runtime/Tuples.scala index 81dca31e355e..be6904b9d1d0 100644 --- a/library/src/scala/runtime/Tuples.scala +++ b/library/src/scala/runtime/Tuples.scala @@ -28,7 +28,7 @@ object Tuples { arr } - def fromArray(xs: Array[Object]): Tuple = xs.length match { + def fromArray(xs: Array[Object], n: Int): Tuple = n match { case 0 => EmptyTuple case 1 => Tuple1(xs(0)) case 2 => Tuple2(xs(0), xs(1)) @@ -55,10 +55,15 @@ object Tuples { case _ => TupleXXL.fromIArray(xs.clone().asInstanceOf[IArray[Object]]).asInstanceOf[Tuple] } - def fromIArray(xs: IArray[Object]): Tuple = - if (xs.length <= 22) fromArray(xs.asInstanceOf[Array[Object]]) + def fromArray(xs: Array[Object]): Tuple = fromArray(xs, xs.length) + + def fromIArray(xs: IArray[Object], n: Int): Tuple = + if n <= 22 || n != xs.length + then fromArray(xs.asInstanceOf[Array[Object]], n) else TupleXXL.fromIArray(xs).asInstanceOf[Tuple] + def fromIArray(xs: IArray[Object]): Tuple = fromIArray(xs, xs.length) + def fromProduct(xs: Product): Tuple = (xs.productArity match { case 0 => EmptyTuple case 1 => From cb952651f0d417a4e05c383f85ad18e3b6362dfb Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 13 Dec 2023 17:52:24 +0100 Subject: [PATCH 04/68] Improvements to tuples: Rearrange types into a more logical order --- library/src/scala/Tuple.scala | 131 +++++++++++++++++----------------- 1 file changed, 64 insertions(+), 67 deletions(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index a5967686ad3e..2c9b22f0b761 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -101,75 +101,95 @@ sealed trait Tuple extends Product: object Tuple: - /** Type of a tuple with an element appended */ - type Append[X <: Tuple, Y] <: NonEmptyTuple = X match { - case EmptyTuple => Y *: EmptyTuple - case x *: xs => x *: Append[xs, Y] - } + /** The size of a tuple, represented as a literal constant subtype of Int */ + type Size[X <: Tuple] <: Int = X match + case EmptyTuple => 0 + case x *: xs => S[Size[xs]] + + /** The type of the element at position N in the tuple X */ + type Elem[X <: Tuple, N <: Int] = X match + case x *: xs => + N match + case 0 => x + case S[n1] => Elem[xs, n1] - /** Type of the head of a tuple */ - type Head[X <: Tuple] = X match { + /** The type of the first element of a tuple */ + type Head[X <: Tuple] = X match case x *: _ => x - } - /** Type of the initial part of the tuple without its last element */ - type Init[X <: Tuple] <: Tuple = X match { + /** The type of the last element of a tuple */ + type Last[X <: Tuple] = X match + case x *: EmptyTuple => x + case _ *: xs => Last[xs] + + /** The type of a tuple consisting of all elements of tuple X except the first one */ + type Tail[X <: Tuple] <: Tuple = X match + case _ *: xs => xs + + /** The type of the initial part of a tuple without its last element */ + type Init[X <: Tuple] <: Tuple = X match case _ *: EmptyTuple => EmptyTuple case x *: xs => x *: Init[xs] - } - /** Type of the tail of a tuple */ - type Tail[X <: Tuple] <: Tuple = X match { - case _ *: xs => xs - } + /** The type of the tuple consisting of the first `N` elements of `X`, + * or all elements if `N` exceeds `Size[X]`. + */ + type Take[X <: Tuple, N <: Int] <: Tuple = N match + case 0 => EmptyTuple + case S[n1] => X match + case EmptyTuple => EmptyTuple + case x *: xs => x *: Take[xs, n1] - /** Type of the last element of a tuple */ - type Last[X <: Tuple] = X match { - case x *: EmptyTuple => x - case _ *: xs => Last[xs] + /** The type of the tuple consisting of all elements of `X` except the first `N` ones, + * or no elements if `N` exceeds `Size[X]`. + */ + type Drop[X <: Tuple, N <: Int] <: Tuple = N match { + case 0 => X + case S[n1] => X match { + case EmptyTuple => EmptyTuple + case x *: xs => Drop[xs, n1] + } } - /** Type of the concatenation of two tuples */ - type Concat[X <: Tuple, +Y <: Tuple] <: Tuple = X match { - case EmptyTuple => Y - case x1 *: xs1 => x1 *: Concat[xs1, Y] - } + /** The pair type `(Take(X, N), Drop[X, N]). */ + type Split[X <: Tuple, N <: Int] = (Take[X, N], Drop[X, N]) - /** Type of the element at position N in the tuple X */ - type Elem[X <: Tuple, N <: Int] = X match { - case x *: xs => - N match { - case 0 => x - case S[n1] => Elem[xs, n1] - } + /** Type of a tuple with an element appended */ + type Append[X <: Tuple, Y] <: NonEmptyTuple = X match { + case EmptyTuple => Y *: EmptyTuple + case x *: xs => x *: Append[xs, Y] } - /** Literal constant Int size of a tuple */ - type Size[X <: Tuple] <: Int = X match { - case EmptyTuple => 0 - case x *: xs => S[Size[xs]] - } + /** Type of the concatenation of two tuples `X` and `Y` */ + type Concat[X <: Tuple, +Y <: Tuple] <: Tuple = X match + case EmptyTuple => Y + case x1 *: xs1 => x1 *: Concat[xs1, Y] /** Fold a tuple `(T1, ..., Tn)` into `F[T1, F[... F[Tn, Z]...]]]` */ type Fold[Tup <: Tuple, Z, F[_, _]] = Tup match case EmptyTuple => Z case h *: t => F[h, Fold[t, Z, F]] - /** Converts a tuple `(T1, ..., Tn)` to `(F[T1], ..., F[Tn])` */ - type Map[Tup <: Tuple, F[_ <: Union[Tup]]] <: Tuple = Tup match { + /** The type of tuple `X` mapped with the type-level function `F`. + * If `X = (T1, ..., Ti)` then `Map[X, F] = `(F[T1], ..., F[Ti])`. + */ + type Map[Tup <: Tuple, F[_ <: Union[Tup]]] <: Tuple = Tup match case EmptyTuple => EmptyTuple case h *: t => F[h] *: Map[t, F] - } - /** Converts a tuple `(T1, ..., Tn)` to a flattened `(..F[T1], ..., ..F[Tn])` */ - type FlatMap[Tup <: Tuple, F[_ <: Union[Tup]] <: Tuple] <: Tuple = Tup match { + /** The type of tuple `X` flat-mapped with the type-level function `F`. + * If `X = (T1, ..., Ti)` then `FlatMap[X, F] = `F[T1] ++ ... ++ F[Ti]` + */ + type FlatMap[Tup <: Tuple, F[_ <: Union[Tup]] <: Tuple] <: Tuple = Tup match case EmptyTuple => EmptyTuple case h *: t => Concat[F[h], FlatMap[t, F]] - } + // TODO: implement term level analogue - /** Filters out those members of the tuple for which the predicate `P` returns `false`. - * A predicate `P[X]` is a type that can be either `true` or `false`. For example: + /** The type of the tuple consisting of all elements of tuple `X` that have types + * for which the given type level predicate `P` reduces to the literal + * constant `true`. A predicate `P[X]` is a type that can be either `true` + * or `false`. For example: * ```scala * type IsString[x] <: Boolean = x match { * case String => true @@ -233,29 +253,6 @@ object Tuple: case x *: xs => ReverseOnto[xs, x *: Acc] case EmptyTuple => Acc - /** Transforms a tuple `(T1, ..., Tn)` into `(T1, ..., Ti)`. */ - type Take[T <: Tuple, N <: Int] <: Tuple = N match { - case 0 => EmptyTuple - case S[n1] => T match { - case EmptyTuple => EmptyTuple - case x *: xs => x *: Take[xs, n1] - } - } - - /** Transforms a tuple `(T1, ..., Tn)` into `(Ti+1, ..., Tn)`. */ - type Drop[T <: Tuple, N <: Int] <: Tuple = N match { - case 0 => T - case S[n1] => T match { - case EmptyTuple => EmptyTuple - case x *: xs => Drop[xs, n1] - } - } - - /** Splits a tuple (T1, ..., Tn) into a pair of two tuples `(T1, ..., Ti)` and - * `(Ti+1, ..., Tn)`. - */ - type Split[T <: Tuple, N <: Int] = (Take[T, N], Drop[T, N]) - /** Given a tuple `(T1, ..., Tn)`, returns a union of its * member types: `T1 | ... | Tn`. Returns `Nothing` if the tuple is empty. */ From 6ae8252653f5a03b6ba9f8f430537bca1b5b33b4 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 13 Dec 2023 17:53:15 +0100 Subject: [PATCH 05/68] Improvements to tuples: more new types and methods --- library/src/scala/Tuple.scala | 72 +++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 2c9b22f0b761..e84e1fe562c3 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -166,6 +166,17 @@ object Tuple: case EmptyTuple => Y case x1 *: xs1 => x1 *: Concat[xs1, Y] + /** An infix shorthand for `Concat[X, Y]` */ + infix type ++[X <: Tuple, +Y <: Tuple] = Concat[X, Y] + + /** The index of `Y` in tuple `X` as a literal constant Int, + * or `Size[X]` if `Y` does not occur in `X` + */ + type IndexOf[X <: Tuple, Y] <: Int = X match + case Y *: _ => 0 + case x *: xs => S[IndexOf[xs, Y]] + case EmptyTuple => 0 + /** Fold a tuple `(T1, ..., Tn)` into `F[T1, F[... F[Tn, Z]...]]]` */ type Fold[Tup <: Tuple, Z, F[_, _]] = Tup match case EmptyTuple => Z @@ -258,6 +269,42 @@ object Tuple: */ type Union[T <: Tuple] = Fold[T, Nothing, [x, y] =>> x | y] + /** A type level Boolean indicating whether the tuple `X` conforms + * to the tuple `Y`. This means: + * - the two tuples have the same number of elements + * - for corresponding elements `x` in `X` and `y` in `Y`, `x` matches `y`. + * @pre The elements of `X` are assumed to be singleton types + */ + type Conforms[X <: Tuple, Y <: Tuple] <: Boolean = Y match + case EmptyTuple => + X match + case EmptyTuple => true + case _ => false + case y *: ys => + X match + case `y` *: xs => Conforms[xs, ys] + case _ => false + + /** A type level Boolean indicating whether the tuple `X` has an element + * that matches `Y`. + * @pre The elements of `X` are assumed to be singleton types + */ + type Contains[X <: Tuple, Y] <: Boolean = X match + case Y *: _ => true + case x *: xs => Contains[xs, Y] + case EmptyTuple => false + + /** A type level Boolean indicating whether the type `Y` contains + * none of the elements of `X`. + * @pre The elements of `X` and `Y` are assumed to be singleton types + */ + type Disjoint[X <: Tuple, Y <: Tuple] <: Boolean = X match + case x *: xs => + Contains[Y, x] match + case true => false + case false => Disjoint[xs, Y] + case EmptyTuple => true + /** Empty tuple */ def apply(): EmptyTuple = EmptyTuple @@ -297,6 +344,31 @@ object Tuple: def fromProduct(product: Product): Tuple = runtime.Tuples.fromProduct(product) + extension [X <: Tuple](inline x: X) + + /** The index (starting at 0) of the first element in the type `X` of `x` + * that matches type `Y`. + */ + inline def indexOfType[Y] = constValue[IndexOf[X, Y]] + + /** A boolean indicating whether there is an element in the type `X` of `x` + * that matches type `Y`. + */ + + inline def containsType[Y] = constValue[Contains[X, Y]] + + /* Note: It would be nice to add the following two extension methods: + + inline def indexOf[Y: Precise](y: Y) = constValue[IndexOf[X, Y]] + inline def containsType[Y: Precise](y: Y) = constValue[Contains[X, Y]] + + because we could then move indexOf/contains completely to the value level. + But this requires `Y` to be inferred precisely, and therefore a mechanism + like the `Precise` context bound used above, which does not yet exist. + */ + + end extension + def fromProductTyped[P <: Product](p: P)(using m: scala.deriving.Mirror.ProductOf[P]): m.MirroredElemTypes = runtime.Tuples.fromProduct(p).asInstanceOf[m.MirroredElemTypes] From 1f79b87803b2aa55b0b2dfd0ed260337e1d77dd2 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 2 Dec 2023 19:41:52 +0100 Subject: [PATCH 06/68] Add NamedTuple object to library --- library/src/scala/NamedTuple.scala | 112 ++++++++++++++++++ .../stdlibExperimentalDefinitions.scala | 4 + 2 files changed, 116 insertions(+) create mode 100644 library/src/scala/NamedTuple.scala diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala new file mode 100644 index 000000000000..9970adfccce7 --- /dev/null +++ b/library/src/scala/NamedTuple.scala @@ -0,0 +1,112 @@ +package scala +import annotation.experimental +import compiletime.ops.boolean.* + +@experimental +object NamedTuple: + + opaque type AnyNamedTuple = Any + opaque type NamedTuple[N <: Tuple, V <: Tuple] >: V <: AnyNamedTuple = V + + def apply[N <: Tuple, V <: Tuple](x: V) = x + + def unapply[N <: Tuple, V <: Tuple](x: NamedTuple[N, V]): Some[V] = Some(x) + + extension [V <: Tuple](x: V) + inline def withNames[N <: Tuple]: NamedTuple[N, V] = x + + extension [N <: Tuple, V <: Tuple](x: NamedTuple[N, V]) + + inline def values: V = x + + inline def size: Tuple.Size[V] = values.size + + // This intentionally works for empty named tuples as well. I think NnEmptyTuple is a dead end + // and should be reverted, justy like NonEmptyList is also appealing at first, but a bad idea + // in the end. + inline def apply(n: Int): Tuple.Elem[V, n.type] = + inline values match + case tup: NonEmptyTuple => tup(n).asInstanceOf[Tuple.Elem[V, n.type]] + case tup => tup.productElement(n).asInstanceOf[Tuple.Elem[V, n.type]] + + inline def head: Tuple.Elem[V, 0] = apply(0) + inline def tail: Tuple.Drop[V, 1] = values.drop(1) + + inline def last: Tuple.Last[V] = apply(size - 1).asInstanceOf[Tuple.Last[V]] + inline def init: Tuple.Init[V] = values.take(size - 1).asInstanceOf[Tuple.Init[V]] + + inline def take(n: Int): NamedTuple[Tuple.Take[N, n.type], Tuple.Take[V, n.type]] = + values.take(n) + + inline def drop(n: Int): NamedTuple[Tuple.Drop[N, n.type], Tuple.Drop[V, n.type]] = + values.drop(n) + + inline def splitAt(n: Int): NamedTuple[Tuple.Split[N, n.type], Tuple.Split[V, n.type]] = + values.splitAt(n) + + inline def ++ [N2 <: Tuple, V2 <: Tuple](that: NamedTuple[N2, V2])(using Tuple.Disjoint[N, N2] =:= true) + : NamedTuple[Tuple.Concat[N, N2], Tuple.Concat[V, V2]] + = values ++ that.values + + // inline def :* [L] (x: L): NamedTuple[Append[N, ???], Append[V, L] = ??? + // inline def *: [H] (x: H): NamedTuple[??? *: N], H *: V] = ??? + + inline def map[F[_]](f: [t] => t => F[t]): NamedTuple[N, Tuple.Map[V, F]] = + values.map(f).asInstanceOf[NamedTuple[N, Tuple.Map[V, F]]] + + inline def reverse: NamedTuple[Tuple.Reverse[N], Tuple.Reverse[V]] = + values.reverse + + inline def zip[V2 <: Tuple](that: NamedTuple[N, V2]): NamedTuple[N, Tuple.Zip[V, V2]] = + values.zip(that.values) + + inline def toList: List[Tuple.Union[V]] = values.toList.asInstanceOf[List[Tuple.Union[V]]] + inline def toArray: Array[Object] = values.toArray + inline def toIArray: IArray[Object] = values.toIArray + + end extension + + /** The names of the named tuple type `NT` */ + type Names[NT <: AnyNamedTuple] <: Tuple = NT match + case NamedTuple[n, _] => n + + /** The value types of the named tuple type `NT` */ + type DropNames[NT <: AnyNamedTuple] <: Tuple = NT match + case NamedTuple[_, x] => x + + type Size[X <: AnyNamedTuple] = Tuple.Size[DropNames[X]] + + type Elem[X <: AnyNamedTuple, N <: Int] = Tuple.Elem[DropNames[X], N] + + type Head[X <: AnyNamedTuple] = Elem[X, 0] + + type Last[X <: AnyNamedTuple] = Tuple.Last[DropNames[X]] + + type Init[X <: AnyNamedTuple] = + NamedTuple[Tuple.Init[Names[X]], Tuple.Init[DropNames[X]]] + + type Tail[X <: AnyNamedTuple] = Drop[X, 1] + + type Take[X <: AnyNamedTuple, N <: Int] = + NamedTuple[Tuple.Take[Names[X], N], Tuple.Take[DropNames[X], N]] + + type Drop[X <: AnyNamedTuple, N <: Int] = + NamedTuple[Tuple.Drop[Names[X], N], Tuple.Drop[DropNames[X], N]] + + type Split[X <: AnyNamedTuple, N <: Int] = (Take[X, N], Drop[X, N]) + + type Concat[X <: AnyNamedTuple, Y <: AnyNamedTuple] = + NamedTuple[Tuple.Concat[Names[X], Names[Y]], Tuple.Concat[DropNames[X], DropNames[Y]]] + + type Map[X <: AnyNamedTuple, F[_ <: Tuple.Union[DropNames[X]]]] = + NamedTuple[Names[X], Tuple.Map[DropNames[X], F]] + + type Reverse[X <: AnyNamedTuple] = + NamedTuple[Tuple.Reverse[Names[X]], Tuple.Reverse[DropNames[X]]] + + type Zip[X <: AnyNamedTuple, Y <: AnyNamedTuple] = + Tuple.Conforms[Names[X], Names[Y]] match + case true => + NamedTuple[Names[X], Tuple.Zip[DropNames[X], DropNames[Y]]] + +end NamedTuple diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index b19ce1c2ebef..26cad0668b37 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -73,6 +73,10 @@ val experimentalDefinitionInLibrary = Set( // New feature: fromNullable for explicit nulls "scala.Predef$.fromNullable", + + // New feature: named tuples + "scala.NamedTuple", + "scala.NamedTuple$", ) From b9899b7718284280b6d1762e5ce3119160e6d8f4 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 3 Dec 2023 13:15:24 +0100 Subject: [PATCH 07/68] Support for named tuples with new representation --- .../src/dotty/tools/dotc/ast/Desugar.scala | 107 ++++++++-- .../src/dotty/tools/dotc/ast/TreeInfo.scala | 4 + compiler/src/dotty/tools/dotc/ast/untpd.scala | 8 +- .../src/dotty/tools/dotc/config/Feature.scala | 1 + .../dotty/tools/dotc/core/Definitions.scala | 11 ++ .../src/dotty/tools/dotc/core/StdNames.scala | 2 + .../src/dotty/tools/dotc/core/TypeUtils.scala | 36 +++- .../dotty/tools/dotc/parsing/Parsers.scala | 80 +++++--- .../tools/dotc/printing/PlainPrinter.scala | 14 +- .../tools/dotc/printing/RefinedPrinter.scala | 17 +- .../tools/dotc/transform/PatternMatcher.scala | 24 ++- .../dotty/tools/dotc/typer/Applications.scala | 12 +- .../src/dotty/tools/dotc/typer/Typer.scala | 140 ++++++------- .../test/dotc/pos-test-pickling.blacklist | 2 + docs/_docs/internals/syntax.md | 9 +- docs/_docs/reference/syntax.md | 8 +- docs/sidebar.yml | 1 + library/src/scala/NamedTuple.scala | 114 +++++++++-- library/src/scala/runtime/LazyVals.scala | 2 +- .../runtime/stdLibPatches/language.scala | 7 + tests/neg/depfuns.scala | 4 +- tests/neg/i7247.scala | 2 +- tests/neg/i7751.scala | 2 +- tests/neg/named-tuples-2.check | 8 + tests/neg/named-tuples-2.scala | 6 + tests/neg/named-tuples.check | 105 ++++++++++ tests/neg/named-tuples.scala | 51 +++++ tests/neg/namedTypeParams.check | 16 +- tests/new/test.scala | 11 +- tests/pos/named-tuples-strawman-2.scala | 185 ++++++++++++++++++ tests/pos/named-tuples-strawman.scala | 48 +++++ tests/pos/named-tuples.check | 10 + tests/pos/named-tuples1.scala | 13 ++ tests/pos/tuple-ops.scala | 36 ++++ tests/run/named-patterns.check | 10 + tests/run/named-patterns.scala | 43 ++++ tests/run/named-tuples-xxl.check | 6 + tests/run/named-tuples-xxl.scala | 91 +++++++++ tests/run/named-tuples.check | 9 + tests/run/named-tuples.scala | 99 ++++++++++ 40 files changed, 1175 insertions(+), 179 deletions(-) create mode 100644 tests/neg/named-tuples-2.check create mode 100644 tests/neg/named-tuples-2.scala create mode 100644 tests/neg/named-tuples.check create mode 100644 tests/neg/named-tuples.scala create mode 100644 tests/pos/named-tuples-strawman-2.scala create mode 100644 tests/pos/named-tuples-strawman.scala create mode 100644 tests/pos/named-tuples.check create mode 100644 tests/pos/named-tuples1.scala create mode 100644 tests/pos/tuple-ops.scala create mode 100644 tests/run/named-patterns.check create mode 100644 tests/run/named-patterns.scala create mode 100644 tests/run/named-tuples-xxl.check create mode 100644 tests/run/named-tuples-xxl.scala create mode 100644 tests/run/named-tuples.check create mode 100644 tests/run/named-tuples.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 03505075121a..39a30a185c72 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -9,10 +9,10 @@ import Decorators.* import Annotations.Annotation import NameKinds.{UniqueName, ContextBoundParamName, ContextFunctionParamName, DefaultGetterName, WildcardParamName} import typer.{Namer, Checking} -import util.{Property, SourceFile, SourcePosition, Chars} +import util.{Property, SourceFile, SourcePosition, SrcPos, Chars} import config.Feature.{sourceVersion, migrateTo3, enabled} import config.SourceVersion.* -import collection.mutable.ListBuffer +import collection.mutable import reporting.* import annotation.constructorOnly import printing.Formatting.hl @@ -234,7 +234,7 @@ object desugar { private def elimContextBounds(meth: DefDef, isPrimaryConstructor: Boolean)(using Context): DefDef = val DefDef(_, paramss, tpt, rhs) = meth - val evidenceParamBuf = ListBuffer[ValDef]() + val evidenceParamBuf = mutable.ListBuffer[ValDef]() var seenContextBounds: Int = 0 def desugarContextBounds(rhs: Tree): Tree = rhs match @@ -1441,22 +1441,101 @@ object desugar { AppliedTypeTree( TypeTree(defn.throwsAlias.typeRef).withSpan(op.span), tpt :: excepts :: Nil) + private def checkWellFormedTupleElems(elems: List[Tree])(using Context): List[Tree] = + val seen = mutable.Set[Name]() + for case arg @ NamedArg(name, _) <- elems do + if seen.contains(name) then + report.error(em"Duplicate tuple element name", arg.srcPos) + seen += name + if name.startsWith("_") && name.toString.tail.toIntOption.isDefined then + report.error( + em"$name cannot be used as the name of a tuple element because it is a regular tuple selector", + arg.srcPos) + + elems match + case elem :: elems1 => + val mismatchOpt = + if elem.isInstanceOf[NamedArg] + then elems1.find(!_.isInstanceOf[NamedArg]) + else elems1.find(_.isInstanceOf[NamedArg]) + mismatchOpt match + case Some(misMatch) => + report.error(em"Illegal combination of named and unnamed tuple elements", misMatch.srcPos) + elems.mapConserve(dropNamedArg) + case None => elems + case _ => elems + end checkWellFormedTupleElems + /** Translate tuple expressions of arity <= 22 * * () ==> () * (t) ==> t * (t1, ..., tN) ==> TupleN(t1, ..., tN) */ - def smallTuple(tree: Tuple)(using Context): Tree = { - val ts = tree.trees - val arity = ts.length - assert(arity <= Definitions.MaxTupleArity) - def tupleTypeRef = defn.TupleType(arity).nn - if (arity == 0) - if (ctx.mode is Mode.Type) TypeTree(defn.UnitType) else unitLiteral - else if (ctx.mode is Mode.Type) AppliedTypeTree(ref(tupleTypeRef), ts) - else Apply(ref(tupleTypeRef.classSymbol.companionModule.termRef), ts) - } + def tuple(tree: Tuple, pt: Type)(using Context): Tree = + var elems = checkWellFormedTupleElems(tree.trees) + if ctx.mode.is(Mode.Pattern) then elems = adaptPatternArgs(elems, pt) + val elemValues = elems.mapConserve(dropNamedArg) + val tup = + val arity = elems.length + if arity <= Definitions.MaxTupleArity then + def tupleTypeRef = defn.TupleType(arity).nn + val tree1 = + if arity == 0 then + if ctx.mode is Mode.Type then TypeTree(defn.UnitType) else unitLiteral + else if ctx.mode is Mode.Type then AppliedTypeTree(ref(tupleTypeRef), elemValues) + else Apply(ref(tupleTypeRef.classSymbol.companionModule.termRef), elemValues) + tree1.withSpan(tree.span) + else + cpy.Tuple(tree)(elemValues) + val names = elems.collect: + case NamedArg(name, arg) => name + if names.isEmpty || ctx.mode.is(Mode.Pattern) then + tup + else + def namesTuple = inMode(ctx.mode &~ Mode.Pattern | Mode.Type): + tuple(Tuple( + names.map: name => + SingletonTypeTree(Literal(Constant(name.toString))).withSpan(tree.span)), + WildcardType) + if ctx.mode.is(Mode.Type) then + AppliedTypeTree(ref(defn.NamedTupleTypeRef), namesTuple :: tup :: Nil) + else + TypeApply( + Apply(Select(ref(defn.NamedTupleModule), nme.withNames), tup), + namesTuple :: Nil) + + /** When desugaring a list pattern arguments `elems` adapt them and the + * expected type `pt` to each other. This means: + * - If `elems` are named pattern elements, rearrange them to match `pt`. + * This requires all names in `elems` to be also present in `pt`. + * - If `elems` are unnamed elements, and `pt` is a named tuple, drop all + * tuple element names from `pt`. + */ + def adaptPatternArgs(elems: List[Tree], pt: Type)(using Context): List[Tree] = + + def reorderedNamedArgs(wildcardSpan: Span): List[untpd.Tree] = + var selNames = pt.namedTupleElementTypes.map(_(0)) + if selNames.isEmpty && pt.classSymbol.is(CaseClass) then + selNames = pt.classSymbol.caseAccessors.map(_.name.asTermName) + val nameToIdx = selNames.zipWithIndex.toMap + val reordered = Array.fill[untpd.Tree](selNames.length): + untpd.Ident(nme.WILDCARD).withSpan(wildcardSpan) + for case arg @ NamedArg(name: TermName, _) <- elems do + nameToIdx.get(name) match + case Some(idx) => + if reordered(idx).isInstanceOf[Ident] then + reordered(idx) = arg + else + report.error(em"Duplicate named pattern", arg.srcPos) + case _ => + report.error(em"No element named `$name` is defined in selector type $pt", arg.srcPos) + reordered.toList + + elems match + case (first @ NamedArg(_, _)) :: _ => reorderedNamedArgs(first.span.startPos) + case _ => elems + end adaptPatternArgs private def isTopLevelDef(stat: Tree)(using Context): Boolean = stat match case _: ValDef | _: PatDef | _: DefDef | _: Export | _: ExtMethods => true @@ -1990,7 +2069,7 @@ object desugar { * without duplicates */ private def getVariables(tree: Tree, shouldAddGiven: Context ?=> Bind => Boolean)(using Context): List[VarInfo] = { - val buf = ListBuffer[VarInfo]() + val buf = mutable.ListBuffer[VarInfo]() def seenName(name: Name) = buf exists (_._1.name == name) def add(named: NameTree, t: Tree): Unit = if (!seenName(named.name) && named.name.isTermName) buf += ((named, t)) diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 34c87eedb081..fbf3cfe163b2 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -244,6 +244,10 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] => def hasNamedArg(args: List[Any]): Boolean = args exists isNamedArg val isNamedArg: Any => Boolean = (arg: Any) => arg.isInstanceOf[Trees.NamedArg[?]] + def dropNamedArg(arg: Tree) = arg match + case NamedArg(_, arg1) => arg1 + case arg => arg + /** Is this pattern node a catch-all (wildcard or variable) pattern? */ def isDefaultCase(cdef: CaseDef): Boolean = cdef match { case CaseDef(pat, EmptyTree, _) => isWildcardArg(pat) diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 08f3db4981ff..fa13bd6610ba 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -528,15 +528,15 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { def makeSelfDef(name: TermName, tpt: Tree)(using Context): ValDef = ValDef(name, tpt, EmptyTree).withFlags(PrivateLocal) - def makeTupleOrParens(ts: List[Tree])(using Context): Tree = ts match { + def makeTupleOrParens(ts: List[Tree])(using Context): Tree = ts match + case (t: NamedArg) :: Nil => Tuple(t :: Nil) case t :: Nil => Parens(t) case _ => Tuple(ts) - } - def makeTuple(ts: List[Tree])(using Context): Tree = ts match { + def makeTuple(ts: List[Tree])(using Context): Tree = ts match + case (t: NamedArg) :: Nil => Tuple(t :: Nil) case t :: Nil => t case _ => Tuple(ts) - } def makeAndType(left: Tree, right: Tree)(using Context): AppliedTypeTree = AppliedTypeTree(ref(defn.andType.typeRef), left :: right :: Nil) diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 4852eaba9334..1fe9cae936c9 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -33,6 +33,7 @@ object Feature: val pureFunctions = experimental("pureFunctions") val captureChecking = experimental("captureChecking") val into = experimental("into") + val namedTuples = experimental("namedTuples") def experimentalAutoEnableFeatures(using Context): List[TermName] = defn.languageExperimentalFeatures diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 675084ec230b..5868da397fc3 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -949,6 +949,9 @@ class Definitions { def TupleXXL_fromIterator(using Context): Symbol = TupleXXLModule.requiredMethod("fromIterator") def TupleXXL_unapplySeq(using Context): Symbol = TupleXXLModule.requiredMethod(nme.unapplySeq) + @tu lazy val NamedTupleModule = requiredModule("scala.NamedTuple") + @tu lazy val NamedTupleTypeRef: TypeRef = NamedTupleModule.termRef.select(tpnme.NamedTuple).asInstanceOf + @tu lazy val RuntimeTupleMirrorTypeRef: TypeRef = requiredClassRef("scala.runtime.TupleMirror") @tu lazy val RuntimeTuplesModule: Symbol = requiredModule("scala.runtime.Tuples") @@ -1304,6 +1307,14 @@ class Definitions { case ByNameFunction(_) => true case _ => false + object NamedTuple: + def apply(nmes: Type, vals: Type)(using Context): Type = + AppliedType(NamedTupleTypeRef, nmes :: vals :: Nil) + def unapply(t: Type)(using Context): Option[(Type, Type)] = t match + case AppliedType(tycon, nmes :: vals :: Nil) if tycon.typeSymbol == NamedTupleTypeRef.symbol => + Some((nmes, vals)) + case _ => None + final def isCompiletime_S(sym: Symbol)(using Context): Boolean = sym.name == tpnme.S && sym.owner == CompiletimeOpsIntModuleClass diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 9772199678d7..ebd246d20575 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -374,6 +374,7 @@ object StdNames { val MirroredMonoType: N = "MirroredMonoType" val MirroredType: N = "MirroredType" val Modifiers: N = "Modifiers" + val NamedTuple: N = "NamedTuple" val NestedAnnotArg: N = "NestedAnnotArg" val NoFlags: N = "NoFlags" val NoPrefix: N = "NoPrefix" @@ -649,6 +650,7 @@ object StdNames { val wildcardType: N = "wildcardType" val withFilter: N = "withFilter" val withFilterIfRefutable: N = "withFilterIfRefutable$" + val withNames: N = "withNames" val WorksheetWrapper: N = "WorksheetWrapper" val wrap: N = "wrap" val writeReplace: N = "writeReplace" diff --git a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala index a3d6ab065a77..7ac0df05b268 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala @@ -4,7 +4,8 @@ package core import TypeErasure.ErasedValueType import Types.*, Contexts.*, Symbols.*, Flags.*, Decorators.* -import Names.Name +import Names.{Name, TermName} +import Constants.Constant class TypeUtils { /** A decorator that provides methods on types @@ -65,8 +66,12 @@ class TypeUtils { case tp: AppliedType if defn.isTupleNType(tp) && normalize => Some(tp.args) // if normalize is set, use the dealiased tuple // otherwise rely on the default case below to print unaliased tuples. + case tp: SkolemType => + recur(tp.underlying, bound) case tp: SingletonType => - if tp.termSymbol == defn.EmptyTupleModule then Some(Nil) else None + if tp.termSymbol == defn.EmptyTupleModule then Some(Nil) + else if normalize then recur(tp.widen, bound) + else None case _ => if defn.isTupleClass(tp.typeSymbol) && !normalize then Some(tp.dealias.argInfos) else None @@ -114,6 +119,33 @@ class TypeUtils { case Some(types) => TypeOps.nestedPairs(types) case None => throw new AssertionError("not a tuple") + def namedTupleElementTypesUpTo(bound: Int, normalize: Boolean = true)(using Context): List[(TermName, Type)] = + (if normalize then self.normalized else self).dealias match + case defn.NamedTuple(nmes, vals) => + val names = nmes.tupleElementTypesUpTo(bound, normalize).getOrElse(Nil).map: + case ConstantType(Constant(str: String)) => str.toTermName + val values = vals.tupleElementTypesUpTo(bound, normalize).getOrElse(Nil) + names.zip(values) + case t => + Nil + + def namedTupleElementTypes(using Context): List[(TermName, Type)] = + namedTupleElementTypesUpTo(Int.MaxValue) + + def isNamedTupleType(using Context): Boolean = self match + case defn.NamedTuple(_, _) => true + case _ => false + + /** Drop all named elements in tuple type */ + def stripNamedTuple(using Context): Type = self.normalized.dealias match + case defn.NamedTuple(_, vals) => + vals + case self @ AnnotatedType(tp, annot) => + val tp1 = tp.stripNamedTuple + if tp1 ne tp then AnnotatedType(tp1, annot) else self + case _ => + self + def refinedWith(name: Name, info: Type)(using Context) = RefinedType(self, name, info) /** Is this type a methodic type that takes at least one parameter? */ diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 2f3daa79fb07..d24dd2882ad6 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -646,6 +646,14 @@ object Parsers { ts.toList else leading :: Nil + def maybeNamed(op: () => Tree): () => Tree = () => + if isIdent && in.lookahead.token == EQUALS && in.featureEnabled(Feature.namedTuples) then + atSpan(in.offset): + val name = ident() + in.nextToken() + NamedArg(name, op()) + else op() + def inSepRegion[T](f: Region => Region)(op: => T): T = val cur = in.currentRegion in.currentRegion = f(cur) @@ -1644,6 +1652,14 @@ object Parsers { && in.featureEnabled(Feature.into) && canStartTypeTokens.contains(in.lookahead.token) + def convertToElem(t: Tree): Tree = t match + case ByNameTypeTree(t1) => + syntaxError(ByNameParameterNotSupported(t), t.span) + t1 + case ValDef(name, tpt, _) => + NamedArg(name, convertToElem(tpt)).withSpan(t.span) + case _ => t + var isValParamList = false if in.token == LPAREN then in.nextToken() @@ -1703,7 +1719,8 @@ object Parsers { if isValParamList || in.isArrow || isPureArrow then functionRest(args) else - val tuple = atSpan(start)(makeTupleOrParens(args1)) + val tuple = atSpan(start): + makeTupleOrParens(args.mapConserve(convertToElem)) typeRest: infixTypeRest: refinedTypeRest: @@ -1979,6 +1996,7 @@ object Parsers { * | Singleton `.' id * | Singleton `.' type * | ‘(’ ArgTypes ‘)’ + * | ‘(’ NamesAndTypes ‘)’ * | Refinement * | TypeSplice -- deprecated syntax (since 3.0.0) * | SimpleType1 TypeArgs @@ -1987,7 +2005,7 @@ object Parsers { def simpleType1() = simpleTypeRest { if in.token == LPAREN then atSpan(in.offset) { - makeTupleOrParens(inParensWithCommas(argTypes(namedOK = false, wildOK = true))) + makeTupleOrParens(inParensWithCommas(argTypes(namedOK = false, wildOK = true, tupleOK = true))) } else if in.token == LBRACE then atSpan(in.offset) { RefinedTypeTree(EmptyTree, refinement(indentOK = false)) } @@ -2070,32 +2088,33 @@ object Parsers { /** ArgTypes ::= Type {`,' Type} * | NamedTypeArg {`,' NamedTypeArg} * NamedTypeArg ::= id `=' Type + * NamesAndTypes ::= NameAndType {‘,’ NameAndType} + * NameAndType ::= id ':' Type */ - def argTypes(namedOK: Boolean, wildOK: Boolean): List[Tree] = { - - def argType() = { + def argTypes(namedOK: Boolean, wildOK: Boolean, tupleOK: Boolean): List[Tree] = + def argType() = val t = typ() - if (wildOK) t else rejectWildcardType(t) - } + if wildOK then t else rejectWildcardType(t) - def namedTypeArg() = { - val name = ident() - accept(EQUALS) - NamedArg(name.toTypeName, argType()) - } + def namedArgType() = + atSpan(in.offset): + val name = ident() + accept(EQUALS) + NamedArg(name.toTypeName, argType()) - if (namedOK && in.token == IDENTIFIER) - in.currentRegion.withCommasExpected { - argType() match { - case Ident(name) if in.token == EQUALS => - in.nextToken() - commaSeparatedRest(NamedArg(name, argType()), () => namedTypeArg()) - case firstArg => - commaSeparatedRest(firstArg, () => argType()) - } - } - else commaSeparated(() => argType()) - } + def namedElem() = + atSpan(in.offset): + val name = ident() + acceptColon() + NamedArg(name, argType()) + + if namedOK && isIdent && in.lookahead.token == EQUALS then + commaSeparated(() => namedArgType()) + else if tupleOK && isIdent && in.lookahead.isColon && in.featureEnabled(Feature.namedTuples) then + commaSeparated(() => namedElem()) + else + commaSeparated(() => argType()) + end argTypes def paramTypeOf(core: () => Tree): Tree = if in.token == ARROW || isPureArrow(nme.PUREARROW) then @@ -2142,7 +2161,7 @@ object Parsers { * NamedTypeArgs ::= `[' NamedTypeArg {`,' NamedTypeArg} `]' */ def typeArgs(namedOK: Boolean, wildOK: Boolean): List[Tree] = - inBracketsWithCommas(argTypes(namedOK, wildOK)) + inBracketsWithCommas(argTypes(namedOK, wildOK, tupleOK = false)) /** Refinement ::= `{' RefineStatSeq `}' */ @@ -2719,7 +2738,9 @@ object Parsers { } /** ExprsInParens ::= ExprInParens {`,' ExprInParens} + * | NamedExprInParens {‘,’ NamedExprInParens} * Bindings ::= Binding {`,' Binding} + * NamedExprInParens ::= id '=' ExprInParens */ def exprsInParensOrBindings(): List[Tree] = if in.token == RPAREN then Nil @@ -2729,7 +2750,7 @@ object Parsers { if isErasedKw then isFormalParams = true if isFormalParams then binding(Modifiers()) else - val t = exprInParens() + val t = maybeNamed(exprInParens)() if t.isInstanceOf[ValDef] then isFormalParams = true t commaSeparatedRest(exprOrBinding(), exprOrBinding) @@ -3083,7 +3104,7 @@ object Parsers { * | Literal * | Quoted * | XmlPattern - * | `(' [Patterns] `)' + * | `(' [Patterns | NamedPatterns] `)' * | SimplePattern1 [TypeArgs] [ArgumentPatterns] * | ‘given’ RefinedType * SimplePattern1 ::= SimpleRef @@ -3134,9 +3155,12 @@ object Parsers { p /** Patterns ::= Pattern [`,' Pattern] + * | NamedPattern {‘,’ NamedPattern} + * NamedPattern ::= id '=' Pattern */ def patterns(location: Location = Location.InPattern): List[Tree] = - commaSeparated(() => pattern(location)) + commaSeparated(maybeNamed(() => pattern(location))) + // check that patterns are all named or all unnamed is done at desugaring def patternsOpt(location: Location = Location.InPattern): List[Tree] = if (in.token == RPAREN) Nil else patterns(location) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 241bfb4f7c7b..87f7c88e0407 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -69,7 +69,8 @@ class PlainPrinter(_ctx: Context) extends Printer { homogenize(tp.ref) case tp @ AppliedType(tycon, args) => if (defn.isCompiletimeAppliedType(tycon.typeSymbol)) tp.tryCompiletimeConstantFold - else tycon.dealias.appliedTo(args) + else if !tycon.typeSymbol.isOpaqueAlias then tycon.dealias.appliedTo(args) + else tp case tp: NamedType => tp.reduceProjection case _ => @@ -121,16 +122,17 @@ class PlainPrinter(_ctx: Context) extends Printer { } (keyword ~ refinementNameString(rt) ~ toTextRHS(rt.refinedInfo)).close - protected def argText(arg: Type, isErased: Boolean = false): Text = keywordText("erased ").provided(isErased) ~ (homogenizeArg(arg) match { - case arg: TypeBounds => "?" ~ toText(arg) - case arg => toText(arg) - }) + protected def argText(arg: Type, isErased: Boolean = false): Text = + keywordText("erased ").provided(isErased) + ~ homogenizeArg(arg).match + case arg: TypeBounds => "?" ~ toText(arg) + case arg => toText(arg) /** Pretty-print comma-separated type arguments for a constructor to be inserted among parentheses or brackets * (hence with `GlobalPrec` precedence). */ protected def argsText(args: List[Type]): Text = - atPrec(GlobalPrec) { Text(args.map(arg => argText(arg) ), ", ") } + atPrec(GlobalPrec) { Text(args.map(argText(_)), ", ") } /** The longest sequence of refinement types, starting at given type * and following parents. diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index e84cbc7c50d5..2873325aecb6 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -205,6 +205,11 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { def toTextTuple(args: List[Type]): Text = "(" ~ argsText(args) ~ ")" + def toTextNamedTuple(elems: List[(TermName, Type)]): Text = + val elemsText = atPrec(GlobalPrec): + Text(elems.map((name, tp) => toText(name) ~ " : " ~ toText(tp)), ", ") + "(" ~ elemsText ~ ")" + def isInfixType(tp: Type): Boolean = tp match case AppliedType(tycon, args) => args.length == 2 @@ -239,8 +244,14 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { def appliedText(tp: Type): Text = tp match case tp @ AppliedType(tycon, args) => - tp.tupleElementTypesUpTo(200, normalize = false) match - case Some(types) if types.size >= 2 && !printDebug => toTextTuple(types) + val namedElems = tp.namedTupleElementTypesUpTo(200, normalize = false) + if namedElems.nonEmpty then + toTextNamedTuple(namedElems) + else tp.tupleElementTypesUpTo(200, normalize = false) match + //case Some(types @ (defn.NamedTupleElem(_, _) :: _)) if !printDebug => + // toTextTuple(types) + case Some(types) if types.size >= 2 && !printDebug => + toTextTuple(types) case _ => val tsym = tycon.typeSymbol if tycon.isRepeatedParam then toTextLocal(args.head) ~ "*" @@ -490,7 +501,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { exprText ~ colon ~ toText(tpt) } case NamedArg(name, arg) => - toText(name) ~ " = " ~ toText(arg) + toText(name) ~ (if name.isTermName && arg.isType then " : " else " = ") ~ toText(arg) case Assign(lhs, rhs) => changePrec(GlobalPrec) { toTextLocal(lhs) ~ " = " ~ toText(rhs) } case block: Block => diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index bed29a122399..a7f987b8b2f3 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -112,8 +112,13 @@ object PatternMatcher { sanitize(tpe), coord = rhs.span) // TODO: Drop Case once we use everywhere else `isPatmatGenerated`. + private def dropNamedTuple(tree: Tree): Tree = + val tpe = tree.tpe.widen + if tpe.isNamedTupleType then tree.cast(tpe.stripNamedTuple) else tree + /** The plan `let x = rhs in body(x)` where `x` is a fresh variable */ - private def letAbstract(rhs: Tree, tpe: Type = NoType)(body: Symbol => Plan): Plan = { + private def letAbstract(rhs0: Tree, tpe: Type = NoType)(body: Symbol => Plan): Plan = { + val rhs = dropNamedTuple(rhs0) val declTpe = if tpe.exists then tpe else rhs.tpe val vble = newVar(rhs, EmptyFlags, declTpe) initializer(vble) = rhs @@ -334,6 +339,7 @@ object PatternMatcher { def unapplyPlan(unapp: Tree, args: List[Tree]): Plan = { def caseClass = unapp.symbol.owner.linkedClass lazy val caseAccessors = caseClass.caseAccessors + val unappType = unapp.tpe.widen.stripNamedTuple def isSyntheticScala2Unapply(sym: Symbol) = sym.is(Synthetic) && sym.owner.is(Scala2x) @@ -349,28 +355,26 @@ object PatternMatcher { !defn.isTupleNType(tree.tpe match { case tp: OrType => tp.join case tp => tp }) // widen even hard unions, to see if it's a union of tuples val components = if isGenericTuple then caseAccessors.indices.toList.map(tupleApp(_, ref(scrutinee))) else caseAccessors.map(tupleSel) matchArgsPlan(components, args, onSuccess) - else if (unapp.tpe <:< (defn.BooleanType)) + else if unappType.isRef(defn.BooleanClass) then TestPlan(GuardTest, unapp, unapp.span, onSuccess) else letAbstract(unapp) { unappResult => val isUnapplySeq = unapp.symbol.name == nme.unapplySeq - if (isProductMatch(unapp.tpe.widen, args.length) && !isUnapplySeq) { - val selectors = productSelectors(unapp.tpe).take(args.length) + if isProductMatch(unappType, args.length) && !isUnapplySeq then + val selectors = productSelectors(unappType).take(args.length) .map(ref(unappResult).select(_)) matchArgsPlan(selectors, args, onSuccess) } - else if (isUnapplySeq && unapplySeqTypeElemTp(unapp.tpe.widen.finalResultType).exists) { + else if isUnapplySeq && unapplySeqTypeElemTp(unappType.finalResultType).exists then unapplySeqPlan(unappResult, args) - } - else if (isUnapplySeq && isProductSeqMatch(unapp.tpe.widen, args.length, unapp.srcPos)) { - val arity = productArity(unapp.tpe.widen, unapp.srcPos) + else if isUnapplySeq && isProductSeqMatch(unappType, args.length, unapp.srcPos) then + val arity = productArity(unappType, unapp.srcPos) unapplyProductSeqPlan(unappResult, args, arity) - } else if unappResult.info <:< defn.NonEmptyTupleTypeRef then val components = (0 until foldApplyTupleType(unappResult.denot.info).length).toList.map(tupleApp(_, ref(unappResult))) matchArgsPlan(components, args, onSuccess) else { - assert(isGetMatch(unapp.tpe)) + assert(isGetMatch(unappType)) val argsPlan = { val get = ref(unappResult).select(nme.get, _.info.isParameterless) val arity = productArity(get.tpe, unapp.srcPos) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index d91c4592a77b..082c8bf3d1db 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1343,9 +1343,10 @@ trait Applications extends Compatibility { case _ => false case _ => false - def typedUnApply(tree: untpd.Apply, selType: Type)(using Context): Tree = { + def typedUnApply(tree: untpd.Apply, selType0: Type)(using Context): Tree = { record("typedUnApply") - val Apply(qual, args) = tree + val Apply(qual, unadaptedArgs) = tree + val selType = selType0.stripNamedTuple def notAnExtractor(tree: Tree): Tree = // prefer inner errors @@ -1562,7 +1563,10 @@ trait Applications extends Compatibility { for (argType <- argTypes) assert(!isBounds(argType), unapplyApp.tpe.show) val bunchedArgs = argTypes match { case argType :: Nil => - if (args.lengthCompare(1) > 0 && Feature.autoTuplingEnabled && defn.isTupleNType(argType)) untpd.Tuple(args) :: Nil + if args.lengthCompare(1) > 0 + && Feature.autoTuplingEnabled + && defn.isTupleNType(argType) + then untpd.Tuple(args) :: Nil else args case _ => args } @@ -1578,7 +1582,7 @@ trait Applications extends Compatibility { else tryWithTypeTest(Typed(result, TypeTree(ownType)), selType) case tp => val unapplyErr = if (tp.isError) unapplyFn else notAnExtractor(unapplyFn) - val typedArgsErr = args mapconserve (typed(_, defn.AnyType)) + val typedArgsErr = unadaptedArgs.mapconserve(typed(_, defn.AnyType)) cpy.UnApply(tree)(unapplyErr, Nil, typedArgsErr) withType unapplyErr.tpe } } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 68a14603cb7a..a510a6eaa578 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -720,56 +720,65 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // There's a second trial where we try to instantiate all type variables in `qual.tpe.widen`, // but that is done only after we search for extension methods or conversions. typedSelect(tree, pt, qual) - else if qual.tpe.isSmallGenericTuple then - val elems = qual.tpe.widenTermRefExpr.tupleElementTypes.getOrElse(Nil) - typedSelect(tree, pt, qual.cast(defn.tupleType(elems))) else - val tree1 = { - if selName.isTypeName then EmptyTree - else tryExtensionOrConversion( - tree, pt, IgnoredProto(pt), qual, ctx.typerState.ownedVars, this, inSelect = true) - }.orElse { - if ctx.gadt.isNarrowing then - // try GADT approximation if we're trying to select a member - // Member lookup cannot take GADTs into account b/c of cache, so we - // approximate types based on GADT constraints instead. For an example, - // see MemberHealing in gadt-approximation-interaction.scala. - val wtp = qual.tpe.widen - gadts.println(i"Trying to heal member selection by GADT-approximating $wtp") - val gadtApprox = Inferencing.approximateGADT(wtp) - gadts.println(i"GADT-approximated $wtp ~~ $gadtApprox") - val qual1 = qual.cast(gadtApprox) - val tree1 = cpy.Select(tree0)(qual1, selName) - val checkedType1 = accessibleType(selectionType(tree1, qual1), superAccess = false) - if checkedType1.exists then - gadts.println(i"Member selection healed by GADT approximation") - finish(tree1, qual1, checkedType1) - else if qual1.tpe.isSmallGenericTuple then - gadts.println(i"Tuple member selection healed by GADT approximation") - typedSelect(tree, pt, qual1) - else - tryExtensionOrConversion(tree1, pt, IgnoredProto(pt), qual1, ctx.typerState.ownedVars, this, inSelect = true) - else EmptyTree - } - if !tree1.isEmpty then - tree1 - else if canDefineFurther(qual.tpe.widen) then - typedSelect(tree, pt, qual) - else if qual.tpe.derivesFrom(defn.DynamicClass) - && selName.isTermName && !isDynamicExpansion(tree) - then - val tree2 = cpy.Select(tree0)(untpd.TypedSplice(qual), selName) - if pt.isInstanceOf[FunOrPolyProto] || pt == LhsProto then - assignType(tree2, TryDynamicCallType) - else - typedDynamicSelect(tree2, Nil, pt) + val namedTupleElems = qual.tpe.widen.namedTupleElementTypes + val nameIdx = namedTupleElems.indexWhere(_._1 == selName) + if nameIdx >= 0 && Feature.enabled(Feature.namedTuples) then + typed( + untpd.Apply( + untpd.Select(untpd.TypedSplice(qual), nme.apply), + untpd.Literal(Constant(nameIdx))), + pt) + else if qual.tpe.isSmallGenericTuple then + val elems = qual.tpe.widenTermRefExpr.tupleElementTypes.getOrElse(Nil) + typedSelect(tree, pt, qual.cast(defn.tupleType(elems))) else - assignType(tree, - rawType match - case rawType: NamedType => - inaccessibleErrorType(rawType, superAccess, tree.srcPos) - case _ => - notAMemberErrorType(tree, qual, pt)) + val tree1 = + if selName.isTypeName then EmptyTree + else tryExtensionOrConversion( + tree, pt, IgnoredProto(pt), qual, ctx.typerState.ownedVars, this, inSelect = true) + .orElse { + if ctx.gadt.isNarrowing then + // try GADT approximation if we're trying to select a member + // Member lookup cannot take GADTs into account b/c of cache, so we + // approximate types based on GADT constraints instead. For an example, + // see MemberHealing in gadt-approximation-interaction.scala. + val wtp = qual.tpe.widen + gadts.println(i"Trying to heal member selection by GADT-approximating $wtp") + val gadtApprox = Inferencing.approximateGADT(wtp) + gadts.println(i"GADT-approximated $wtp ~~ $gadtApprox") + val qual1 = qual.cast(gadtApprox) + val tree1 = cpy.Select(tree0)(qual1, selName) + val checkedType1 = accessibleType(selectionType(tree1, qual1), superAccess = false) + if checkedType1.exists then + gadts.println(i"Member selection healed by GADT approximation") + finish(tree1, qual1, checkedType1) + else if qual1.tpe.isSmallGenericTuple then + gadts.println(i"Tuple member selection healed by GADT approximation") + typedSelect(tree, pt, qual1) + else + tryExtensionOrConversion(tree1, pt, IgnoredProto(pt), qual1, ctx.typerState.ownedVars, this, inSelect = true) + else EmptyTree + } + if !tree1.isEmpty then + tree1 + else if canDefineFurther(qual.tpe.widen) then + typedSelect(tree, pt, qual) + else if qual.tpe.derivesFrom(defn.DynamicClass) + && selName.isTermName && !isDynamicExpansion(tree) + then + val tree2 = cpy.Select(tree0)(untpd.TypedSplice(qual), selName) + if pt.isInstanceOf[FunOrPolyProto] || pt == LhsProto then + assignType(tree2, TryDynamicCallType) + else + typedDynamicSelect(tree2, Nil, pt) + else + assignType(tree, + rawType match + case rawType: NamedType => + inaccessibleErrorType(rawType, superAccess, tree.srcPos) + case _ => + notAMemberErrorType(tree, qual, pt)) end typedSelect def typedSelect(tree: untpd.Select, pt: Type)(using Context): Tree = { @@ -2450,7 +2459,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer body1.isInstanceOf[RefTree] && !isWildcardArg(body1) || body1.isInstanceOf[Literal] val symTp = - if isStableIdentifierOrLiteral then pt + if isStableIdentifierOrLiteral || pt.isNamedTupleType then pt else if isWildcardStarArg(body1) || pt == defn.ImplicitScrutineeTypeRef || body1.tpe <:< pt // There is some strange interaction with gadt matching. @@ -3050,37 +3059,32 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } /** Translate tuples of all arities */ - def typedTuple(tree: untpd.Tuple, pt: Type)(using Context): Tree = { - val arity = tree.trees.length - if (arity <= Definitions.MaxTupleArity) - typed(desugar.smallTuple(tree).withSpan(tree.span), pt) - else { - val pts = - pt.tupleElementTypes match - case Some(types) if types.size == arity => types - case _ => List.fill(arity)(defn.AnyType) - val elems = tree.trees.lazyZip(pts).map( + def typedTuple(tree: untpd.Tuple, pt: Type)(using Context): Tree = + val tree1 = desugar.tuple(tree, pt) + if tree1 ne tree then typed(tree1, pt) + else + val arity = tree.trees.length + val pts = pt.stripNamedTuple.tupleElementTypes match + case Some(types) if types.size == arity => types + case _ => List.fill(arity)(defn.AnyType) + val elems = tree.trees.lazyZip(pts).map: if ctx.mode.is(Mode.Type) then typedType(_, _, mapPatternBounds = true) - else typed(_, _)) - if (ctx.mode.is(Mode.Type)) + else typed(_, _) + if ctx.mode.is(Mode.Type) then elems.foldRight(TypeTree(defn.EmptyTupleModule.termRef): Tree)((elemTpt, elemTpts) => AppliedTypeTree(TypeTree(defn.PairClass.typeRef), List(elemTpt, elemTpts))) .withSpan(tree.span) - else { + else val tupleXXLobj = untpd.ref(defn.TupleXXLModule.termRef) val app = untpd.cpy.Apply(tree)(tupleXXLobj, elems.map(untpd.TypedSplice(_))) .withSpan(tree.span) val app1 = typed(app, if ctx.mode.is(Mode.Pattern) then pt else defn.TupleXXLClass.typeRef) - if (ctx.mode.is(Mode.Pattern)) app1 - else { + if ctx.mode.is(Mode.Pattern) then app1 + else val elemTpes = elems.lazyZip(pts).map((elem, pt) => TypeComparer.widenInferred(elem.tpe, pt, widenUnions = true)) val resTpe = TypeOps.nestedPairs(elemTpes) app1.cast(resTpe) - } - } - } - } /** Retrieve symbol attached to given tree */ protected def retrieveSym(tree: untpd.Tree)(using Context): Symbol = tree.removeAttachment(SymOfTree) match { diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.blacklist index 638455e7f2de..94e510e04396 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -66,6 +66,8 @@ tuple-fold.scala mt-redux-norm.perspective.scala i18211.scala 10867.scala +named-tuples1.scala +named-tuples-strawman-2.scala # Opaque type i5720.scala diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index 4207a13ea66d..8cc070d5dbc5 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -198,7 +198,7 @@ SimpleType ::= SimpleLiteral SimpleType1 ::= id Ident(name) | Singleton ‘.’ id Select(t, name) | Singleton ‘.’ ‘type’ SingletonTypeTree(p) - | ‘(’ Types ‘)’ Tuple(ts) + | ‘(’ [Types | NamesAndTypes] ‘)’ Tuple(ts) | Refinement RefinedTypeTree(EmptyTree, refinement) | TypeSplice -- deprecated syntax | SimpleType1 TypeArgs AppliedTypeTree(t, args) @@ -222,6 +222,8 @@ Refinement ::= :<<< [RefineDcl] {semi [RefineDcl]} >>> TypeBounds ::= [‘>:’ Type] [‘<:’ Type] TypeBoundsTree(lo, hi) TypeParamBounds ::= TypeBounds {‘:’ Type} ContextBounds(typeBounds, tps) Types ::= Type {‘,’ Type} +NamesAndTypes ::= NameAndType {‘,’ NameAndType} +NameAndType ::= id ':' Type ``` ### Expressions @@ -290,8 +292,10 @@ TypeSplice ::= spliceId | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted type pattern -- deprecated syntax | ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted type pattern -- deprecated syntax ExprsInParens ::= ExprInParens {‘,’ ExprInParens} + | NamedExprInParens {‘,’ NamedExprInParens} ExprInParens ::= PostfixExpr ‘:’ Type -- normal Expr allows only RefinedType here | Expr +NamedExprInParens ::= id '=' ExprInParens ParArgumentExprs ::= ‘(’ [ExprsInParens] ‘)’ exprs | ‘(’ ‘using’ ExprsInParens ‘)’ | ‘(’ [ExprsInParens ‘,’] PostfixExpr ‘*’ ‘)’ exprs :+ Typed(expr, Ident(wildcardStar)) @@ -343,6 +347,9 @@ SimplePattern1 ::= SimpleRef PatVar ::= varid | ‘_’ Patterns ::= Pattern {‘,’ Pattern} + | NamedPattern {‘,’ NamedPattern} +NamedPattern ::= id '=' Pattern + ArgumentPatterns ::= ‘(’ [Patterns] ‘)’ Apply(fn, pats) | ‘(’ [Patterns ‘,’] PatVar ‘*’ ‘)’ ``` diff --git a/docs/_docs/reference/syntax.md b/docs/_docs/reference/syntax.md index f8e7ba6a5cbc..ae541b65d8c4 100644 --- a/docs/_docs/reference/syntax.md +++ b/docs/_docs/reference/syntax.md @@ -198,7 +198,7 @@ SimpleType ::= SimpleLiteral | id | Singleton ‘.’ id | Singleton ‘.’ ‘type’ - | ‘(’ Types ‘)’ + | ‘(’ [Types] ‘)’ | Refinement | SimpleType1 TypeArgs | SimpleType1 ‘#’ id @@ -263,7 +263,7 @@ SimpleExpr ::= SimpleRef | quoteId -- only inside splices | ‘new’ ConstrApp {‘with’ ConstrApp} [TemplateBody] | ‘new’ TemplateBody - | ‘(’ ExprsInParens ‘)’ + | ‘(’ [ExprsInParens] ‘)’ | SimpleExpr ‘.’ id | SimpleExpr ‘.’ MatchClause | SimpleExpr TypeArgs @@ -279,8 +279,7 @@ ExprSplice ::= spliceId | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern | ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted pattern ExprsInParens ::= ExprInParens {‘,’ ExprInParens} -ExprInParens ::= PostfixExpr ‘:’ Type - | Expr +ExprInParens ::= PostfixExpr ‘:’ Type | Expr ParArgumentExprs ::= ‘(’ [ExprsInParens] ‘)’ | ‘(’ ‘using’ ExprsInParens ‘)’ | ‘(’ [ExprsInParens ‘,’] PostfixExpr ‘*’ ‘)’ @@ -331,6 +330,7 @@ SimplePattern1 ::= SimpleRef PatVar ::= varid | ‘_’ Patterns ::= Pattern {‘,’ Pattern} + ArgumentPatterns ::= ‘(’ [Patterns] ‘)’ | ‘(’ [Patterns ‘,’] PatVar ‘*’ ‘)’ ``` diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 5d72f15838cd..b38e057f06b1 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -154,6 +154,7 @@ subsection: - page: reference/experimental/cc.md - page: reference/experimental/purefuns.md - page: reference/experimental/tupled-function.md + - page: reference/experimental/named-tuples.md - page: reference/syntax.md - title: Language Versions index: reference/language-versions/language-versions.md diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index 9970adfccce7..c65a760ee22c 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -17,93 +17,165 @@ object NamedTuple: extension [N <: Tuple, V <: Tuple](x: NamedTuple[N, V]) - inline def values: V = x + /** The underlying tuple without the names */ + inline def toTuple: V = x - inline def size: Tuple.Size[V] = values.size + /** The number of elements in this tuple */ + inline def size: Tuple.Size[V] = toTuple.size // This intentionally works for empty named tuples as well. I think NnEmptyTuple is a dead end // and should be reverted, justy like NonEmptyList is also appealing at first, but a bad idea // in the end. + + /** The value (without the name) at index `n` of this tuple */ inline def apply(n: Int): Tuple.Elem[V, n.type] = - inline values match + inline toTuple match case tup: NonEmptyTuple => tup(n).asInstanceOf[Tuple.Elem[V, n.type]] case tup => tup.productElement(n).asInstanceOf[Tuple.Elem[V, n.type]] + /** The first element value of this tuple */ inline def head: Tuple.Elem[V, 0] = apply(0) - inline def tail: Tuple.Drop[V, 1] = values.drop(1) + /** The tuple consisting of all elements of this tuple except the first one */ + inline def tail: Tuple.Drop[V, 1] = toTuple.drop(1) + + /** The last element value of this tuple */ inline def last: Tuple.Last[V] = apply(size - 1).asInstanceOf[Tuple.Last[V]] - inline def init: Tuple.Init[V] = values.take(size - 1).asInstanceOf[Tuple.Init[V]] + /** The tuple consisting of all elements of this tuple except the last one */ + inline def init: Tuple.Init[V] = toTuple.take(size - 1).asInstanceOf[Tuple.Init[V]] + + /** The tuple consisting of the first `n` elements of this tuple, or all + * elements if `n` exceeds `size`. + */ inline def take(n: Int): NamedTuple[Tuple.Take[N, n.type], Tuple.Take[V, n.type]] = - values.take(n) + toTuple.take(n) + /** The tuple consisting of all elements of this tuple except the first `n` ones, + * or no elements if `n` exceeds `size`. + */ inline def drop(n: Int): NamedTuple[Tuple.Drop[N, n.type], Tuple.Drop[V, n.type]] = - values.drop(n) + toTuple.drop(n) + /** The tuple `(x.take(n), x.drop(n))` */ inline def splitAt(n: Int): NamedTuple[Tuple.Split[N, n.type], Tuple.Split[V, n.type]] = - values.splitAt(n) + toTuple.splitAt(n) + /** The tuple consisting of all elements of this tuple followed by all elements + * of tuple `that`. The names of the two tuples must be disjoint. + */ inline def ++ [N2 <: Tuple, V2 <: Tuple](that: NamedTuple[N2, V2])(using Tuple.Disjoint[N, N2] =:= true) : NamedTuple[Tuple.Concat[N, N2], Tuple.Concat[V, V2]] - = values ++ that.values + = toTuple ++ that.toTuple // inline def :* [L] (x: L): NamedTuple[Append[N, ???], Append[V, L] = ??? // inline def *: [H] (x: H): NamedTuple[??? *: N], H *: V] = ??? + /** 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))`. + */ inline def map[F[_]](f: [t] => t => F[t]): NamedTuple[N, Tuple.Map[V, F]] = - values.map(f).asInstanceOf[NamedTuple[N, Tuple.Map[V, F]]] + toTuple.map(f).asInstanceOf[NamedTuple[N, Tuple.Map[V, F]]] + /** The named tuple consisting of all elements of this tuple in reverse */ inline def reverse: NamedTuple[Tuple.Reverse[N], Tuple.Reverse[V]] = - values.reverse - + toTuple.reverse + + /** The named tuple consisting of all elements values of this tuple zipped + * with corresponding element values in named tuple `that`. + * If the two tuples have different sizes, + * the extra elements of the larger tuple will be disregarded. + * The names of `x` and `that` at the same index must be the same. + * The result tuple keeps the same names as the operand tuples. + */ inline def zip[V2 <: Tuple](that: NamedTuple[N, V2]): NamedTuple[N, Tuple.Zip[V, V2]] = - values.zip(that.values) + toTuple.zip(that.toTuple) + + /** A list consisting of all element values */ + inline def toList: List[Tuple.Union[V]] = toTuple.toList.asInstanceOf[List[Tuple.Union[V]]] - inline def toList: List[Tuple.Union[V]] = values.toList.asInstanceOf[List[Tuple.Union[V]]] - inline def toArray: Array[Object] = values.toArray - inline def toIArray: IArray[Object] = values.toIArray + /** An array consisting of all element values */ + inline def toArray: Array[Object] = toTuple.toArray + + /** An immutable array consisting of all element values */ + inline def toIArray: IArray[Object] = toTuple.toIArray end extension - /** The names of the named tuple type `NT` */ - type Names[NT <: AnyNamedTuple] <: Tuple = NT match + /** 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 - /** The value types of the named tuple type `NT` */ + /** The value types of a named tuple represented as a regular tuple. */ type DropNames[NT <: AnyNamedTuple] <: Tuple = NT match case NamedTuple[_, x] => x + /** The size of a named tuple, represented as a literal constant subtype of Int */ type Size[X <: AnyNamedTuple] = Tuple.Size[DropNames[X]] + /** The type of the element value at position N in the named tuple X */ type Elem[X <: AnyNamedTuple, N <: Int] = Tuple.Elem[DropNames[X], N] + /** The type of the first element value of a named tuple */ type Head[X <: AnyNamedTuple] = Elem[X, 0] + /** The type of the last element value of a named tuple */ type Last[X <: AnyNamedTuple] = Tuple.Last[DropNames[X]] + /** The type of a named tuple consisting of all elements of named tuple X except the first one */ + type Tail[X <: AnyNamedTuple] = Drop[X, 1] + + /** The type of the initial part of a named tuple without its last element */ type Init[X <: AnyNamedTuple] = NamedTuple[Tuple.Init[Names[X]], Tuple.Init[DropNames[X]]] - type Tail[X <: AnyNamedTuple] = Drop[X, 1] - + /** The type of the named tuple consisting of the first `N` elements of `X`, + * or all elements if `N` exceeds `Size[X]`. + */ type Take[X <: AnyNamedTuple, N <: Int] = NamedTuple[Tuple.Take[Names[X], N], Tuple.Take[DropNames[X], N]] + /** The type of the named tuple consisting of all elements of `X` except the first `N` ones, + * or no elements if `N` exceeds `Size[X]`. + */ type Drop[X <: AnyNamedTuple, N <: Int] = NamedTuple[Tuple.Drop[Names[X], N], Tuple.Drop[DropNames[X], N]] + /** The pair type `(Take(X, N), Drop[X, N]). */ type Split[X <: AnyNamedTuple, N <: Int] = (Take[X, N], Drop[X, N]) + /** Type of the concatenation of two tuples `X` and `Y` */ type Concat[X <: AnyNamedTuple, Y <: AnyNamedTuple] = NamedTuple[Tuple.Concat[Names[X], Names[Y]], Tuple.Concat[DropNames[X], DropNames[Y]]] + /** The type of the named tuple `X` mapped with the type-level function `F`. + * If `X = (n1 : T1, ..., ni : Ti)` then `Map[X, F] = `(n1 : F[T1], ..., ni : F[Ti])`. + */ type Map[X <: AnyNamedTuple, F[_ <: Tuple.Union[DropNames[X]]]] = NamedTuple[Names[X], Tuple.Map[DropNames[X], F]] + /** A named tuple with the elements of tuple `X` in reversed order */ type Reverse[X <: AnyNamedTuple] = NamedTuple[Tuple.Reverse[Names[X]], Tuple.Reverse[DropNames[X]]] + /** The type of the named tuple consisting of all element values of + * named tuple `X` zipped with corresponding element values of + * named tuple `Y`. If the two tuples have different sizes, + * the extra elements of the larger tuple will be disregarded. + * The names of `X` and `Y` at the same index must be the same. + * The result tuple keeps the same names as the operand tuples. + * For example, if + * ``` + * X = (n1 : S1, ..., ni : Si) + * Y = (n1 : T1, ..., nj : Tj) where j >= i + * ``` + * then + * ``` + * Zip[X, Y] = (n1 : (S1, T1), ..., ni: (Si, Ti)) + * ``` + * @syntax markdown + */ type Zip[X <: AnyNamedTuple, Y <: AnyNamedTuple] = Tuple.Conforms[Names[X], Names[Y]] match case true => diff --git a/library/src/scala/runtime/LazyVals.scala b/library/src/scala/runtime/LazyVals.scala index ea369539d021..e38e016f5182 100644 --- a/library/src/scala/runtime/LazyVals.scala +++ b/library/src/scala/runtime/LazyVals.scala @@ -9,7 +9,7 @@ import scala.annotation.* */ object LazyVals { @nowarn - private[this] val unsafe: sun.misc.Unsafe = { + private val unsafe: sun.misc.Unsafe = { def throwInitializationException() = throw new ExceptionInInitializerError( new IllegalStateException("Can't find instance of sun.misc.Unsafe") diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 372e1e34bb85..b2bd4b791423 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -91,6 +91,13 @@ object language: @compileTimeOnly("`into` can only be used at compile time in import statements") object into + /** Experimental support for named tuples. + * + * @see [[https://dotty.epfl.ch/docs/reference/experimental/into-modifier]] + */ + @compileTimeOnly("`namedTuples` can only be used at compile time in import statements") + object namedTuples + /** Was needed to add support for relaxed imports of extension methods. * The language import is no longer needed as this is now a standard feature since SIP was accepted. * @see [[http://dotty.epfl.ch/docs/reference/contextual/extension-methods]] diff --git a/tests/neg/depfuns.scala b/tests/neg/depfuns.scala index ac96915a78b5..989aa72be820 100644 --- a/tests/neg/depfuns.scala +++ b/tests/neg/depfuns.scala @@ -1,5 +1,7 @@ +import language.experimental.erasedDefinitions + object Test { - type T = (x: Int) + type T = (erased x: Int) } // error: `=>' expected diff --git a/tests/neg/i7247.scala b/tests/neg/i7247.scala index 9172f90fad07..3514f20c47fe 100644 --- a/tests/neg/i7247.scala +++ b/tests/neg/i7247.scala @@ -1,2 +1,2 @@ val x = "foo" match - case _: (a *: (b: Any)) => ??? // error \ No newline at end of file + case _: (a *: (b: Any)) => ??? // error, now OK since (b: Any) is a named tuple \ No newline at end of file diff --git a/tests/neg/i7751.scala b/tests/neg/i7751.scala index 978ed860574f..fd66e7d451be 100644 --- a/tests/neg/i7751.scala +++ b/tests/neg/i7751.scala @@ -1,3 +1,3 @@ import language.`3.3` -val a = Some(a=a,)=> // error // error +val a = Some(a=a,)=> // error // error // error // error val a = Some(x=y,)=> diff --git a/tests/neg/named-tuples-2.check b/tests/neg/named-tuples-2.check new file mode 100644 index 000000000000..0a52d5f3989b --- /dev/null +++ b/tests/neg/named-tuples-2.check @@ -0,0 +1,8 @@ +-- Error: tests/neg/named-tuples-2.scala:5:9 --------------------------------------------------------------------------- +5 | case (name, age) => () // error + | ^ + | this case is unreachable since type (String, Int, Boolean) is not a subclass of class Tuple2 +-- Error: tests/neg/named-tuples-2.scala:6:9 --------------------------------------------------------------------------- +6 | case (n, a, m, x) => () // error + | ^ + | this case is unreachable since type (String, Int, Boolean) is not a subclass of class Tuple4 diff --git a/tests/neg/named-tuples-2.scala b/tests/neg/named-tuples-2.scala new file mode 100644 index 000000000000..0507891e0549 --- /dev/null +++ b/tests/neg/named-tuples-2.scala @@ -0,0 +1,6 @@ +import language.experimental.namedTuples +def Test = + val person = (name = "Bob", age = 33, married = true) + person match + case (name, age) => () // error + case (n, a, m, x) => () // error diff --git a/tests/neg/named-tuples.check b/tests/neg/named-tuples.check new file mode 100644 index 000000000000..485858fb18a0 --- /dev/null +++ b/tests/neg/named-tuples.check @@ -0,0 +1,105 @@ +-- Error: tests/neg/named-tuples.scala:9:19 ---------------------------------------------------------------------------- +9 | val illformed = (_2 = 2) // error + | ^^^^^^ + | _2 cannot be used as the name of a tuple element because it is a regular tuple selector +-- Error: tests/neg/named-tuples.scala:10:20 --------------------------------------------------------------------------- +10 | type Illformed = (_1: Int) // error + | ^^^^^^^ + | _1 cannot be used as the name of a tuple element because it is a regular tuple selector +-- Error: tests/neg/named-tuples.scala:11:40 --------------------------------------------------------------------------- +11 | val illformed2 = (name = "", age = 0, name = true) // error + | ^^^^^^^^^^^ + | Duplicate tuple element name +-- Error: tests/neg/named-tuples.scala:12:45 --------------------------------------------------------------------------- +12 | type Illformed2 = (name: String, age: Int, name: Boolean) // error + | ^^^^^^^^^^^^^ + | Duplicate tuple element name +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:18:25 ------------------------------------------------------ +18 | val y: (String, Int) = person // error + | ^^^^^^ + | Found: (Test.person : (name : String, age : Int)) + | Required: (String, Int) + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:19:20 ------------------------------------------------------ +19 | val _: NameOnly = person // error + | ^^^^^^ + | Found: (Test.person : (name : String, age : Int)) + | Required: Test.NameOnly + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:20:18 ------------------------------------------------------ +20 | val _: Person = nameOnly // error + | ^^^^^^^^ + | Found: (Test.nameOnly : (name : String)) + | Required: Test.Person + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:22:36 ------------------------------------------------------ +22 | val _: (age: Int, name: String) = person // error + | ^^^^^^ + | Found: (Test.person : (name : String, age : Int)) + | Required: (age : Int, name : String) + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg/named-tuples.scala:24:17 --------------------------------------------------------------------------- +24 | val (name = x, agee = y) = person // error + | ^^^^^^^^ + | No element named `agee` is defined in selector type (name : String, age : Int) +-- Error: tests/neg/named-tuples.scala:27:10 --------------------------------------------------------------------------- +27 | case (name = n, age = a) => () // error // error + | ^^^^^^^^ + | No element named `name` is defined in selector type (String, Int) +-- Error: tests/neg/named-tuples.scala:27:20 --------------------------------------------------------------------------- +27 | case (name = n, age = a) => () // error // error + | ^^^^^^^ + | No element named `age` is defined in selector type (String, Int) +-- [E172] Type Error: tests/neg/named-tuples.scala:29:27 --------------------------------------------------------------- +29 | val pp = person ++ (1, 2) // error + | ^ + | Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), Tuple] =:= (true : Boolean). +-- [E172] Type Error: tests/neg/named-tuples.scala:32:18 --------------------------------------------------------------- +32 | person ++ (1, 2) match // error + | ^ + | Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), Tuple] =:= (true : Boolean). +-- Error: tests/neg/named-tuples.scala:35:17 --------------------------------------------------------------------------- +35 | val bad = ("", age = 10) // error + | ^^^^^^^^ + | Illegal combination of named and unnamed tuple elements +-- Error: tests/neg/named-tuples.scala:38:20 --------------------------------------------------------------------------- +38 | case (name = n, age) => () // error + | ^^^ + | Illegal combination of named and unnamed tuple elements +-- Error: tests/neg/named-tuples.scala:39:16 --------------------------------------------------------------------------- +39 | case (name, age = a) => () // error + | ^^^^^^^ + | Illegal combination of named and unnamed tuple elements +-- Error: tests/neg/named-tuples.scala:42:10 --------------------------------------------------------------------------- +42 | case (age = x) => // error + | ^^^^^^^ + | No element named `age` is defined in selector type Tuple +-- [E172] Type Error: tests/neg/named-tuples.scala:44:27 --------------------------------------------------------------- +44 | val p2 = person ++ person // error + | ^ + |Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), (("name" : String), ("age" : String))] =:= (true : Boolean). +-- [E172] Type Error: tests/neg/named-tuples.scala:45:43 --------------------------------------------------------------- +45 | val p3 = person ++ (first = 11, age = 33) // error + | ^ + |Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), (("first" : String), ("age" : String))] =:= (true : Boolean). +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:47:22 ------------------------------------------------------ +47 | val p5 = person.zip(first = 11, age = 33) // error + | ^^^^^^^^^^^^^^^^^^^^ + | Found: (first : Int, age : Int) + | Required: NamedTuple.NamedTuple[(("name" : String), ("age" : String)), V2] + | + | where: V2 is a type variable with constraint <: Tuple + | + | longer explanation available when compiling with `-explain` +-- Warning: tests/neg/named-tuples.scala:24:29 ------------------------------------------------------------------------- +24 | val (name = x, agee = y) = person // error + | ^^^^^^ + |pattern's type (String, Int) is more specialized than the right hand side expression's type (name : String, age : Int) + | + |If the narrowing is intentional, this can be communicated by adding `: @unchecked` after the expression, + |which may result in a MatchError at runtime. + |This patch can be rewritten automatically under -rewrite -source 3.2-migration. diff --git a/tests/neg/named-tuples.scala b/tests/neg/named-tuples.scala new file mode 100644 index 000000000000..5d1f3777dd73 --- /dev/null +++ b/tests/neg/named-tuples.scala @@ -0,0 +1,51 @@ +import annotation.experimental +import language.experimental.namedTuples + +@experimental object Test: + + type Person = (name: String, age: Int) + val person = (name = "Bob", age = 33): (name: String, age: Int) + + val illformed = (_2 = 2) // error + type Illformed = (_1: Int) // error + val illformed2 = (name = "", age = 0, name = true) // error + type Illformed2 = (name: String, age: Int, name: Boolean) // error + + type NameOnly = (name: String) + + val nameOnly = (name = "Louis") + + val y: (String, Int) = person // error + val _: NameOnly = person // error + val _: Person = nameOnly // error + + val _: (age: Int, name: String) = person // error + + val (name = x, agee = y) = person // error + + ("Ives", 2) match + case (name = n, age = a) => () // error // error + + val pp = person ++ (1, 2) // error + val qq = ("a", true) ++ (1, 2) + + person ++ (1, 2) match // error + case _ => + + val bad = ("", age = 10) // error + + person match + case (name = n, age) => () // error + case (name, age = a) => () // error + + (??? : Tuple) match + case (age = x) => // error + + val p2 = person ++ person // error + val p3 = person ++ (first = 11, age = 33) // error + val p4 = person.zip(person) // ok + val p5 = person.zip(first = 11, age = 33) // error + + + + diff --git a/tests/neg/namedTypeParams.check b/tests/neg/namedTypeParams.check index 3f6f9f7913e8..5e0672f20f25 100644 --- a/tests/neg/namedTypeParams.check +++ b/tests/neg/namedTypeParams.check @@ -24,16 +24,16 @@ 19 | f[X = Int, String](1, "") // error // error | ^ | '=' expected, but ']' found --- Error: tests/neg/namedTypeParams.scala:6:8 -------------------------------------------------------------------------- +-- Error: tests/neg/namedTypeParams.scala:6:4 -------------------------------------------------------------------------- 6 | f[X = Int, Y = Int](1, 2) // error: experimental // error: experimental - | ^^^ - | Named type arguments are experimental, - | they must be enabled with a `experimental.namedTypeArguments` language import or setting --- Error: tests/neg/namedTypeParams.scala:6:17 ------------------------------------------------------------------------- + | ^^^^^^^ + | Named type arguments are experimental, + | they must be enabled with a `experimental.namedTypeArguments` language import or setting +-- Error: tests/neg/namedTypeParams.scala:6:13 ------------------------------------------------------------------------- 6 | f[X = Int, Y = Int](1, 2) // error: experimental // error: experimental - | ^^^ - | Named type arguments are experimental, - | they must be enabled with a `experimental.namedTypeArguments` language import or setting + | ^^^^^^^ + | Named type arguments are experimental, + | they must be enabled with a `experimental.namedTypeArguments` language import or setting -- [E006] Not Found Error: tests/neg/namedTypeParams.scala:11:11 ------------------------------------------------------- 11 | val x: C[T = Int] = // error: ']' expected, but `=` found // error | ^ diff --git a/tests/new/test.scala b/tests/new/test.scala index e6bfc29fd808..16a823547553 100644 --- a/tests/new/test.scala +++ b/tests/new/test.scala @@ -1,2 +1,9 @@ -object Test: - def f: Any = 1 +import language.experimental.namedTuples + +type Person = (name: String, age: Int) + +def test = + val bob = (name = "Bob", age = 33): (name: String, age: Int) + + val silly = bob match + case (name = n, age = a) => n.length + a diff --git a/tests/pos/named-tuples-strawman-2.scala b/tests/pos/named-tuples-strawman-2.scala new file mode 100644 index 000000000000..7c18d063bcf4 --- /dev/null +++ b/tests/pos/named-tuples-strawman-2.scala @@ -0,0 +1,185 @@ +import compiletime.* +import compiletime.ops.int.* +import compiletime.ops.boolean.! +import Tuple.* + +object TupleOps: + + /** The `X` tuple, with its element at index `N` replaced by `Y`. + * If `N` is equal to `Size[X]`, the element `Y` is appended instead + */ + type UpdateOrAppend[X <: Tuple, N <: Int, Y] <: Tuple = X match + case x *: xs => + N match + case 0 => Y *: xs + case S[n1] => x *: UpdateOrAppend[xs, n1, Y] + case EmptyTuple => + N match + case 0 => Y *: EmptyTuple + + inline def updateOrAppend[X <: Tuple, N <: Int, Y](xs: X, y: Y): UpdateOrAppend[X, N, Y] = + locally: + val n = constValue[N] + val size = xs.size + require(0 <= n && n <= xs.size, s"Index $n out of range 0..$size") + if n == size then xs :* y + else + val elems = xs.toArray + elems(n) = y.asInstanceOf[Object] + fromArray(elems) + .asInstanceOf[UpdateOrAppend[X, N, Y]] + + extension [X <: Tuple](inline xs: X) + // Note: Y must be inferred precisely, or given explicitly. This means even though `updateOrAppend` + // is clearly useful, we cannot yet move it to tuple since it is still too awkward to use. + // Once we have precise inference, we could replace `Y <: Singleton` with `Y: Precise` + // and then it should work beautifully. + inline def updateOrAppend[N <: Int & Singleton, Y <: Singleton](inline n: N, inline y: Y): UpdateOrAppend[X, N, Y] = + locally: + val size = xs.size + require(0 <= n && n <= size, s"Index $n out of range 0..$size") + if n == size then xs :* y + else + val elems = xs.toArray + elems(n) = y.asInstanceOf[Object] + fromArray(elems) + .asInstanceOf[UpdateOrAppend[X, N, Y]] + + /** If `Y` does not occur in tuple `X`, `X` with `Y` appended. Otherwise `X`. */ + type AppendIfDistinct[X <: Tuple, Y] <: Tuple = X match + case Y *: xs => X + case x *: xs => x *: AppendIfDistinct[xs, Y] + case EmptyTuple => Y *: EmptyTuple + + inline def appendIfDistinct[X <: Tuple, Y](xs: X, y: Y): AppendIfDistinct[X, Y] = + (if xs.containsType[Y] then xs else xs :* y).asInstanceOf[AppendIfDistinct[X, Y]] + + /** `X` with all elements from `Y` that do not occur in `X` appended */ + type ConcatDistinct[X <: Tuple, Y <: Tuple] <: Tuple = Y match + case y *: ys => ConcatDistinct[AppendIfDistinct[X, y], ys] + case EmptyTuple => X + + inline def concatDistinct[X <: Tuple, Y <: Tuple](xs: X, ys: Y): ConcatDistinct[X, Y] = + (xs ++ ys.filter[Y, [Elem] =>> ![Contains[X, Elem]]]).asInstanceOf[ConcatDistinct[X, Y]] + +object NamedTupleOps: + import TupleOps.* + + opaque type AnyNamedTuple = Any + + opaque type NamedTuple[N <: Tuple, +X <: Tuple] >: X <: AnyNamedTuple = X + + object NamedTuple: + def apply[N <: Tuple, X <: Tuple](x: X): NamedTuple[N, X] = x + + /** The names of the named tuple type `NT` */ + type Names[NT <: AnyNamedTuple] <: Tuple = NT match + case NamedTuple[n, _] => n + + /** The value types of the named tuple type `NT` */ + type DropNames[NT <: AnyNamedTuple] <: Tuple = NT match + case NamedTuple[_, x] => x + + extension [NT <: AnyNamedTuple](x: NT) + inline def toTuple: DropNames[NT] = x.asInstanceOf + inline def names: Names[NT] = constValueTuple[Names[NT]] + + /** Internal use only: Merge names and value components of two named tuple to + * impement `UpdateWith`. + * @param N the names of the combined tuple + * @param X the value types of the first named tuple + * @param N2 the names of the second named tuple + * @param Y the value types of the second named tuple + */ + type Merge[N <: Tuple, X <: Tuple, N2 <: Tuple, Y <: Tuple] = (N2, Y) match + case (n *: ns, y *: ys) => + Merge[N, UpdateOrAppend[X, IndexOf[N, n], y], ns, ys] + case (EmptyTuple, EmptyTuple) => + NamedTuple[N, X] + + /** A joint named tuple where + * - The names are the names of named tuple `NT1` followed by those names of `NT2` which + * do not appear in `NT1` + * - The values are the values of `NT1` and `NT2` corresponding to these names. + * If a name is present in both `NT1` and `NT2` the value in `NT2` is used. + */ + type UpdateWith[NT1 <: AnyNamedTuple, NT2 <: AnyNamedTuple] = + Merge[ConcatDistinct[Names[NT1], Names[NT2]], DropNames[NT1], Names[NT2], DropNames[NT2]] + + extension [NT1 <: AnyNamedTuple](nt1: NT1) + inline def updateWith[NT2 <: AnyNamedTuple](nt2: NT2): UpdateWith[NT1, NT2] = + val names = constValueTuple[ConcatDistinct[Names[NT1], Names[NT2]]].toArray + val names2 = constValueTuple[Names[NT2]].toArray + val values1 = nt1.toTuple + val values2 = nt2.toTuple + val values = new Array[Object](names.length) + values1.toArray.copyToArray(values) + for i <- 0 until values2.size do + val idx = names.indexOf(names2(i)) + values(idx) = values2.productElement(i).asInstanceOf[Object] + Tuple.fromArray(values).asInstanceOf[UpdateWith[NT1, NT2]] + +@main def Test = + import TupleOps.* + import NamedTupleOps.* + + type Names = "first" *: "last" *: "age" *: EmptyTuple + type Values = "Bob" *: "Miller" *: 33 *: EmptyTuple + + val names: Names = ("first", "last", "age") + val values: Values = ("Bob", "Miller", 33) + + val x1: IndexOf[Names, "first"] = constValue + val _: 0 = x1 + + val x2: IndexOf[Names, "age"] = names.indexOfType["age"] + val _: 2 = x2 + + val x3: IndexOf[Names, "what?"] = names.indexOfType["what?"] + val _: 3 = x3 + + type Releases = "first" *: "middle" *: EmptyTuple + type ReleaseValues = 1.0 *: true *: EmptyTuple + + val releases: Releases = ("first", "middle") + val releaseValues: ReleaseValues = (1.0, true) + + val x4 = values.updateOrAppend(names.indexOfType["age"], 11) + //updateOrAppend[Values](values)[IndexOf[Names, "age"], 11](indexOf[Names](names)["age"]("age"), 11) + val _: ("Bob", "Miller", 11) = x4 + assert(("Bob", "Miller", 11) == x4) + + val x5 = updateOrAppend[Values, IndexOf[Names, "what"], true](values, true) + val _: ("Bob", "Miller", 33, true) = x5 + assert(("Bob", "Miller", 33, true) == x5) + + val x6 = updateOrAppend[Values, IndexOf[Names, "first"], "Peter"](values, "Peter") + val _: ("Peter", "Miller", 33) = x6 + assert(("Peter", "Miller", 33) == x6) + + val x7 = concatDistinct[Names, Releases](names, releases) + val _: ("first", "last", "age", "middle") = x7 + assert(("first", "last", "age", "middle") == x7, x7) + + val x8 = concatDistinct[Releases, Names](releases, names) + val _: ("first", "middle", "last", "age") = x8 + assert(("first", "middle", "last", "age") == x8) + + def x9: Merge[ConcatDistinct[Names, Releases], Values, Releases, ReleaseValues] = ??? + def x9c: NamedTuple[("first", "last", "age", "middle"), (1.0, "Miller", 33, true)] = x9 + + val person = NamedTuple[Names, Values](values) + val release = NamedTuple[Releases, ReleaseValues](releaseValues) + + val x10 = person.updateWith(release) + val _: UpdateWith[NamedTuple[Names, Values], NamedTuple[Releases, ReleaseValues]] = x10 + val _: ("first", "last", "age", "middle") = x10.names + val _: (1.0, "Miller", 33, true) = x10.toTuple + assert((("first", "last", "age", "middle") == x10.names)) + assert((1.0, "Miller", 33, true) == x10.toTuple) + + val x11 = release.updateWith(person) + val _: UpdateWith[NamedTuple[Releases, ReleaseValues], NamedTuple[Names, Values]] = x11 + val _: NamedTuple[("first", "middle", "last", "age"), ("Bob", true, "Miller", 33)] = x11 + assert(("first", "middle", "last", "age") == x11.names) + assert(("Bob", true, "Miller", 33) == x11.toTuple) diff --git a/tests/pos/named-tuples-strawman.scala b/tests/pos/named-tuples-strawman.scala new file mode 100644 index 000000000000..859e1d1448e2 --- /dev/null +++ b/tests/pos/named-tuples-strawman.scala @@ -0,0 +1,48 @@ +object Test: + + object Named: + opaque type Named[name <: String & Singleton, A] >: A = A + def apply[S <: String & Singleton, A](name: S, x: A): Named[name.type, A] = x + extension [name <: String & Singleton, A](named: Named[name, A]) def value: A = named + import Named.* + + type DropNames[T <: Tuple] = T match + case Named[_, x] *: xs => x *: DropNames[xs] + case _ => T + + extension [T <: Tuple](x: T) def toTuple: DropNames[T] = + x.asInstanceOf // named and unnamed tuples have the same runtime representation + + val name = "hi" + val named = Named(name, 33) // ok, but should be rejectd + + inline val name2 = "hi" + val named2 = Named(name2, 33) // ok, but should be rejectd + val _: Named["hi", Int] = named2 + + var x = (Named("name", "Bob"), Named("age", 33)) + + val y: (String, Int) = x.toTuple + + x = y + + val z = y.toTuple + + type PersonInfo = (Named["name", String], Named["age", Int]) + type AddressInfo = (Named["city", String], Named["zip", Int]) + + val ok1: (Named["name", String], Named["age", Int]) = x + val ok2: PersonInfo = y + //val err1: (Named["bad", String], Named["age", Int]) = x // error + val err2: (Named["bad", String], Named["age", Int]) = x.toTuple // ok + val ok3: (Named["bad", String], Named["age", Int]) = y // ok + + val addr = (Named("city", "Lausanne"), Named("zip", 1003)) + val _: AddressInfo = addr + + type CombinedInfo = Tuple.Concat[PersonInfo, AddressInfo] + + val combined: CombinedInfo = x ++ addr + +// val person = (name = "Bob", age = 33): (name: String, age: Int) +// person.age diff --git a/tests/pos/named-tuples.check b/tests/pos/named-tuples.check new file mode 100644 index 000000000000..24928c7dbdac --- /dev/null +++ b/tests/pos/named-tuples.check @@ -0,0 +1,10 @@ +(Bob,33) +33 +Bob +(Bob,33,Lausanne,1003) +33 +no match +Bob is younger than Bill +Bob is younger than Lucy +Bill is younger than Lucy +matched elements (name, Bob), (age, 33) diff --git a/tests/pos/named-tuples1.scala b/tests/pos/named-tuples1.scala new file mode 100644 index 000000000000..58e3fc065e61 --- /dev/null +++ b/tests/pos/named-tuples1.scala @@ -0,0 +1,13 @@ +import annotation.experimental +import language.experimental.namedTuples + +@main def Test = + val bob = (name = "Bob", age = 33): (name: String, age: Int) + val persons = List( + bob, + (name = "Bill", age = 40), + (name = "Lucy", age = 45) + ) + val ages = persons.map(_.age) + // pickling failure: matchtype is reduced after pickling, unreduced before. + assert(ages.sum == 118) diff --git a/tests/pos/tuple-ops.scala b/tests/pos/tuple-ops.scala new file mode 100644 index 000000000000..df708e669e0f --- /dev/null +++ b/tests/pos/tuple-ops.scala @@ -0,0 +1,36 @@ +import language.experimental.namedTuples +import Tuple.* + +def test = + val x1: Conforms[(1, 2), (1, 2)] = ??? + val _: true = x1 + + val x2: Conforms[(1, 2), (1, 3)] = ??? + val _: false = x2 + + val x3: Conforms[(1, 2), (1, 2, 4)] = ??? + val _: false = x2 + + val x4: Conforms[(1, 2, 4), (1, 2)] = ??? + val _: false = x2 + + summon[Disjoint[(1, 2, 3), (4, 5)] =:= true] + summon[Disjoint[(1, 2, 6), (4, 5)] =:= true] + summon[Disjoint[(1, 2, 6), EmptyTuple] =:= true] + summon[Disjoint[EmptyTuple, EmptyTuple] =:= true] + + summon[Contains[(1, 2, 3), Int] =:= true] + summon[Contains[(1, 2, 3), 2] =:= true] + summon[Contains[(1, 2, 3), 4] =:= false] + + summon[Conforms[(1, 2, 3), (1, 2, 3)] =:= true] + summon[Conforms[(1, 2, 3), (1, 2)] =:= false] + summon[Conforms[(1, 2, 3), (1, 2, 4)] =:= false] + summon[Conforms[(1, 2, 3), (Int, 2, 3)] =:= true] +// summon[Conforms[(Int, 2, 3), (1, 2, 3)] =:= true] // error, reduction gets stuck + + summon[Disjoint[(1, 2, 3), (4, 2)] =:= false] + summon[Disjoint[("a", "b"), ("b", "c")] =:= false] + summon[Disjoint[(1, 2, 6), Tuple1[2]] =:= false] + summon[Disjoint[Tuple1[3], (4, 3, 6)] =:= false] + diff --git a/tests/run/named-patterns.check b/tests/run/named-patterns.check new file mode 100644 index 000000000000..ba8dbb8b21f7 --- /dev/null +++ b/tests/run/named-patterns.check @@ -0,0 +1,10 @@ +name Bob, age 22 +name Bob +age 22 +age 22, name Bob +Bob, 22 +1003 Lausanne, Rue de la Gare 44 +1003 Lausanne +Rue de la Gare in Lausanne +1003 Lausanne, Rue de la Gare 44 +1003 Lausanne, Rue de la Gare 44 diff --git a/tests/run/named-patterns.scala b/tests/run/named-patterns.scala new file mode 100644 index 000000000000..1e7e0697e782 --- /dev/null +++ b/tests/run/named-patterns.scala @@ -0,0 +1,43 @@ +import language.experimental.namedTuples + +object Test1: + class Person(val name: String, val age: Int) + + object Person: + def unapply(p: Person): (name: String, age: Int) = (p.name, p.age) + + case class Address(city: String, zip: Int, street: String, number: Int) + + @main def Test = + val bob = Person("Bob", 22) + bob match + case Person(name = n, age = a) => println(s"name $n, age $a") + bob match + case Person(name = n) => println(s"name $n") + bob match + case Person(age = a) => println(s"age $a") + bob match + case Person(age = a, name = n) => println(s"age $a, name $n") + bob match + case Person(age, name) => println(s"$age, $name") + + val addr = Address("Lausanne", 1003, "Rue de la Gare", 44) + addr match + case Address(city = c, zip = z, street = s, number = n) => + println(s"$z $c, $s $n") + addr match + case Address(zip = z, city = c) => + println(s"$z $c") + addr match + case Address(city = c, street = s) => + println(s"$s in $c") + addr match + case Address(number = n, street = s, zip = z, city = c) => + println(s"$z $c, $s $n") + addr match + case Address(c, z, s, number) => + println(s"$z $c, $s $number") + + + + diff --git a/tests/run/named-tuples-xxl.check b/tests/run/named-tuples-xxl.check new file mode 100644 index 000000000000..ee5f60bec756 --- /dev/null +++ b/tests/run/named-tuples-xxl.check @@ -0,0 +1,6 @@ +(0,0,0,0,0,0,0,0,0,0,Bob,0,33,0,0,0,0,0,0,0,0,0,0,0) +(0,0,0,0,0,0,0,0,0,0,Bob,0,33,0,0,0,0,0,0,0,0,0,0,0) +(0,0,0,0,0,0,0,0,0,0,Bob,0,33,0,0,0,0,0,0,0,0,0,0,0) +Bob is younger than Bill +Bob is younger than Lucy +Bill is younger than Lucy diff --git a/tests/run/named-tuples-xxl.scala b/tests/run/named-tuples-xxl.scala new file mode 100644 index 000000000000..3a0a1e5e1294 --- /dev/null +++ b/tests/run/named-tuples-xxl.scala @@ -0,0 +1,91 @@ +import language.experimental.namedTuples +import NamedTuple.toTuple + +type Person = ( + x0: Int, x1: Int, x2: Int, x3: Int, x4: Int, x5: Int, x6: Int, x7: Int, x8: Int, x9: Int, + name: String, y1: Int, age: Int, y2: Int, + z0: Int, z1: Int, z2: Int, z3: Int, z4: Int, z5: Int, z6: Int, z7: Int, z8: Int, z9: Int) + +val bob = ( + x0 = 0, x1 = 0, x2 = 0, x3 = 0, x4 = 0, x5 = 0, x6 = 0, x7 = 0, x8 = 0, x9 = 0, + name = "Bob", y1 = 0, age = 33, y2 = 0, + z0 = 0, z1 = 0, z2 = 0, z3 = 0, z4 = 0, z5 = 0, z6 = 0, z7 = 0, z8 = 0, z9 = 0) + +val person2: Person = bob + + +type AddressInfo = (city: String, zip: Int) +val addr = (city = "Lausanne", zip = 1003) + +type CombinedInfo = NamedTuple.Concat[Person, AddressInfo] +val bobWithAddr = bob ++ addr +val _: CombinedInfo = bobWithAddr +val _: CombinedInfo = bob ++ addr + +@main def Test = + assert(bob.name == "Bob") + assert(bob.age == 33) + bob match + case p @ (name = "Bob", age = a) => + val x = p + println(x) + assert(p.age == 33) + assert(a == 33) + case _ => + assert(false) + + bob match + case p @ (name = "Peter", age = _) => assert(false) + case p @ (name = "Bob", age = 0) => assert(false) + case _ => + bob match + case b @ (x0 = 0, x1 = 0, x2 = 0, x3 = 0, x4 = 0, x5 = 0, x6 = 0, x7 = 0, x8 = 0, x9 = 0, + name = "Bob", y1 = 0, age = 33, y2 = 0, + z0 = 0, z1 = 0, z2 = 0, z3 = 0, z4 = 0, z5 = 0, z6 = 0, z7 = 0, z8 = 0, z9 = 0) + => // !!! spurious unreachable case warning + println(bob) + println(b) + case _ => assert(false) + + val x = bob.age + assert(x == 33) + + val y: ( + Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, + String, Int, Int, Int, + Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) + = bob.toTuple + + def ageOf(person: Person) = person.age + + assert(ageOf(bob) == 33) + + val persons = List( + bob, + (x0 = 0, x1 = 0, x2 = 0, x3 = 0, x4 = 0, x5 = 0, x6 = 0, x7 = 0, x8 = 0, x9 = 0, + name = "Bill", y1 = 0, age = 40, y2 = 0, + z0 = 0, z1 = 0, z2 = 0, z3 = 0, z4 = 0, z5 = 0, z6 = 0, z7 = 0, z8 = 0, z9 = 0), + (x0 = 0, x1 = 0, x2 = 0, x3 = 0, x4 = 0, x5 = 0, x6 = 0, x7 = 0, x8 = 0, x9 = 0, + name = "Lucy", y1 = 0, age = 45, y2 = 0, + z0 = 0, z1 = 0, z2 = 0, z3 = 0, z4 = 0, z5 = 0, z6 = 0, z7 = 0, z8 = 0, z9 = 0), + ) + for + p <- persons + q <- persons + if p.age < q.age + do + println(s"${p.name} is younger than ${q.name}") + + val name1 = bob(10) + val age1 = bob(12) + + val minors = persons.filter: + case (age = a) => a < 18 + case _ => false + + assert(minors.isEmpty) + + bob match + case bob1 @ (age = 33, name = "Bob") => + val x: Person = bob1 // bob1 still has type Person with the unswapped elements + case _ => assert(false) diff --git a/tests/run/named-tuples.check b/tests/run/named-tuples.check new file mode 100644 index 000000000000..c53a2f52ff09 --- /dev/null +++ b/tests/run/named-tuples.check @@ -0,0 +1,9 @@ +(Bob,33) +33 +Bob +(Bob,33,Lausanne,1003) +33 +no match +Bob is younger than Bill +Bob is younger than Lucy +Bill is younger than Lucy diff --git a/tests/run/named-tuples.scala b/tests/run/named-tuples.scala new file mode 100644 index 000000000000..0c9e3fb4d455 --- /dev/null +++ b/tests/run/named-tuples.scala @@ -0,0 +1,99 @@ +import language.experimental.namedTuples +import NamedTuple.toTuple + +type Person = (name: String, age: Int) +val bob = (name = "Bob", age = 33): (name: String, age: Int) +val person2: (name: String, age: Int) = bob + +type Uni = (uni: Double) +val uni = (uni = 1.0) +val _: Uni = uni + +type AddressInfo = (city: String, zip: Int) +val addr = (city = "Lausanne", zip = 1003) +val _: AddressInfo = addr + +type CombinedInfo = NamedTuple.Concat[Person, AddressInfo] +val bobWithAddr = bob ++ addr +val _: CombinedInfo = bobWithAddr +val _: CombinedInfo = bob ++ addr + +@main def Test = + println(bob) + println(bob.age) + println(person2.name) + println(bobWithAddr) + bob match + case p @ (name = "Bob", age = _) => println(p.age) + bob match + case p @ (name = "Bob", age = age) => assert(age == 33) + bob match + case p @ (name = "Peter", age = _) => println(p.age) + case p @ (name = "Bob", age = 0) => println(p.age) + case _ => println("no match") + + val x = bob.age + assert(x == 33) + + val y: (String, Int) = bob.toTuple + + def ageOf(person: Person) = person.age + + assert(ageOf(bob) == 33) + assert(ageOf((name = "anon", age = 22)) == 22) + assert(ageOf(("anon", 11)) == 11) + + val persons = List( + bob, + (name = "Bill", age = 40), + (name = "Lucy", age = 45) + ) + for + p <- persons + q <- persons + if p.age < q.age + do + println(s"${p.name} is younger than ${q.name}") + + //persons.select(_.age, _.name) + //persons.join(addresses).withCommon(_.name) + + def minMax(elems: Int*): (min: Int, max: Int) = + var min = elems(0) + var max = elems(0) + for elem <- elems do + if elem < min then min = elem + if elem > max then max = elem + (min = min, max = max) + + val mm = minMax(1, 3, 400, -3, 10) + assert(mm.min == -3) + assert(mm.max == 400) + + val name1 = bob(0) + val age1 = bob(1) + val _: String = name1 + val _: Int = age1 + + val bobS = bob.reverse + val _: (age: Int, name: String) = bobS + val _: NamedTuple.Reverse[Person] = bobS + + val silly = bob match + case (name, age) => name.length + age + + assert(silly == 36) + + val minors = persons.filter: + case (age = a) => a < 18 + case _ => false + + assert(minors.isEmpty) + + bob match + case bob1 @ (age = 33, name = "Bob") => + val x: Person = bob1 // bob1 still has type Person with the unswapped elements + case _ => assert(false) + + + From 27d62886267e9d9069cd842f4fd50bd3dc908993 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 3 Dec 2023 13:16:06 +0100 Subject: [PATCH 08/68] Add doc page --- .../reference/experimental/named-tuples.md | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 docs/_docs/reference/experimental/named-tuples.md diff --git a/docs/_docs/reference/experimental/named-tuples.md b/docs/_docs/reference/experimental/named-tuples.md new file mode 100644 index 000000000000..a32581336eac --- /dev/null +++ b/docs/_docs/reference/experimental/named-tuples.md @@ -0,0 +1,136 @@ +--- +layout: doc-page +title: "Named Tuples" +nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/named-tuples.html +--- + +The elements of a tuple can now be named. Example: +```scala +type Person = (name: String, age: Int) +val Bob: Person = (name = "Bob", age = 33) + +Bob match + case (name, age) => + println(s"$name is $age years old") + +val persons: List[Person] = ... +val minors = persons.filter: p => + p.age < 18 +``` +Named bindings in tuples are similar to function parameters and arguments. We use `name: Type` for element types and `name = value` for element values. It is illegal to mix named and unnamed elements in a tuple, or to use the same same +name for two different elements. + +Fields of named tuples can be selected by their name, as in the line `p.age < 18` above. + +### Conformance + +The order of names in a named tuple matters. For instance, the type `Person` above and the type `(age: Int, name: String)` would be different, incompatible types. + +Values of named tuple types can also be be defined using regular tuples. For instance: +```scala +val x: Person = ("Laura", 25) + +def register(person: Person) = ... +register(person = ("Silvain", 16)) +register(("Silvain", 16)) +``` +This follows since a regular tuple `(T_1, ..., T_n)` is treated as a subtype of a named tuple `(N_1 = T_1, ..., N_n = T_n)` with the same element types. On the other hand, named tuples do not conform to unnamed tuples, so the following is an error: +```scala +val x: (String, Int) = Bob // error: type mismatch +``` +One can convert a named tuple to an unnamed tuple with the `dropNames` method, so the following works: +```scala +val x: (String, Int) = Bob.dropNames // ok +``` +Note that conformance rules for named tuples are analogous to the rules for named parameters. One can assign parameters by position to a named parameter list. +```scala + def f(param: Int) = ... + f(param = 1) // OK + f(2) // Also OK +``` +But one cannot use a name to pass an argument to an unnamed parameter: +```scala + val f: Int => T + f(2) // OK + f(param = 2) // Not OK +``` +The rules for tuples are analogous. Unnamed tuples conform to named tuple types, but the opposite does not hold. + + +### Pattern Matching + +When pattern matching on a named tuple, the pattern may be named or unnamed. +If the pattern is named it needs to mention only a subset of the tuple names, and these names can come in any order. So the following are all OK: +```scala +Bob match + case (name, age) => ... + +Bob match + case (name = x, age = y) => ... + +Bob match + case (age = x) => ... + +Bob match + case (age = x, name = y) => ... +``` + +### Expansion + +Named tuples are in essence just a convenient syntax for regular tuples. In the internal representation, a named tuple type is represented at compile time as a pair of two tuples. One tuple contains the names as literal constant string types, the other contains the element types. The runtime representation of a named tuples consists of just the element values, whereas the names are forgotten. This is achieved by declaring `NamedTuple` +in package `scala` as an opaque type as follows: +```scala + opaque type NamedTuple[N <: Tuple, +V <: Tuple] >: V = V +``` +For instance, the `Person` type would be represented as the type +```scala +NamedTuple[("name", "age"), (String, Int)] +``` +`NamedTuple` is an opaque type alias of its second, value parameter. The first parameter is a string constant type which determines the name of the element. Since the type is just an alias of its value part, names are erased at runtime, and named tuples and regular tuples have the same representation. + +A `NamedTuple[N, V]` type is publicly known to be a supertype (but not a subtype) of its value paramater `V`, which means that regular tuples can be assigned to named tuples but not _vice versa_. + +The `NamedTuple` object contains a number of extension methods for named tuples hat mirror the same functions in `Tuple`. Examples are +`apply`, `head`, `tail`, `take`, `drop`, `++`, `map`, or `zip`. +Similar to `Tuple`, the `NamedTuple` object also contains types such as `Elem`, `Head`, `Concat` +that describe the results of these extension methods. + +The translation of named tuples to instances of `NamedTuple` is fixed by the specification and therefore known to the programmer. This means that: + + - All tuple operations also work with named tuples "out of the box". + - Macro libraries can rely on this expansion. + +### Restrictions + +The following restrictions apply to named tuple elements: + + 1. Either all elements of a tuple are named or none are named. It is illegal to mix named and unnamed elements in a tuple. For instance, the following is in error: + ```scala + val illFormed1 = ("Bob", age = 33) // error + ``` + 2. Each element name in a named tuple must be unique. For instance, the following is in error: + ```scala + val illFormed2 = (name = "", age = 0, name = true) // error + ``` + 3. Named tuples can be matched with either named or regular patterns. But regular tuples and other selector types can only be matched with regular tuple patterns. For instance, the following is in error: + ```scala + (tuple: Tuple) match + case (age = x) => // error + ``` + +### Syntax + +The syntax of Scala is extended as follows to support named tuples: +``` +SimpleType ::= ... + | ‘(’ NameAndType {‘,’ NameAndType} ‘)’ +NameAndType ::= id ':' Type + +SimpleExpr ::= ... + | '(' NamedExprInParens {‘,’ NamedExprInParens} ')' +NamedExprInParens ::= id '=' ExprInParens + +SimplePattern ::= ... + | '(' NamedPattern {‘,’ NamedPattern} ')' +NamedPattern ::= id '=' Pattern +``` From 5dd48f9406fa58a8df1e53a92f216d8056c7b3e5 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 3 Dec 2023 14:27:35 +0100 Subject: [PATCH 09/68] Make NamedTuple covariant in its value type --- library/src/scala/NamedTuple.scala | 2 +- tests/neg/named-tuples.check | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index c65a760ee22c..ddc2b545f9ba 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -6,7 +6,7 @@ import compiletime.ops.boolean.* object NamedTuple: opaque type AnyNamedTuple = Any - opaque type NamedTuple[N <: Tuple, V <: Tuple] >: V <: AnyNamedTuple = V + opaque type NamedTuple[N <: Tuple, +V <: Tuple] >: V <: AnyNamedTuple = V def apply[N <: Tuple, V <: Tuple](x: V) = x diff --git a/tests/neg/named-tuples.check b/tests/neg/named-tuples.check index 485858fb18a0..067b6eeb937c 100644 --- a/tests/neg/named-tuples.check +++ b/tests/neg/named-tuples.check @@ -90,9 +90,7 @@ 47 | val p5 = person.zip(first = 11, age = 33) // error | ^^^^^^^^^^^^^^^^^^^^ | Found: (first : Int, age : Int) - | Required: NamedTuple.NamedTuple[(("name" : String), ("age" : String)), V2] - | - | where: V2 is a type variable with constraint <: Tuple + | Required: NamedTuple.NamedTuple[(("name" : String), ("age" : String)), Tuple] | | longer explanation available when compiling with `-explain` -- Warning: tests/neg/named-tuples.scala:24:29 ------------------------------------------------------------------------- From bd9bb8a777684eaa6025e08f9c05188d8d74b954 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 5 Dec 2023 11:19:23 +0100 Subject: [PATCH 10/68] Various tweaks --- .../reference/experimental/named-tuples.md | 4 +- library/src/scala/NamedTuple.scala | 2 +- library/src/scala/Tuple.scala | 1 - tests/neg/named-tuples.check | 71 ++++++++++--------- tests/neg/named-tuples.scala | 1 + tests/run/named-tuples.check | 1 + tests/run/named-tuples.scala | 14 +++- 7 files changed, 55 insertions(+), 39 deletions(-) diff --git a/docs/_docs/reference/experimental/named-tuples.md b/docs/_docs/reference/experimental/named-tuples.md index a32581336eac..f9ba87382e32 100644 --- a/docs/_docs/reference/experimental/named-tuples.md +++ b/docs/_docs/reference/experimental/named-tuples.md @@ -38,9 +38,9 @@ This follows since a regular tuple `(T_1, ..., T_n)` is treated as a subtype of ```scala val x: (String, Int) = Bob // error: type mismatch ``` -One can convert a named tuple to an unnamed tuple with the `dropNames` method, so the following works: +One can convert a named tuple to an unnamed tuple with the `toTuple` method, so the following works: ```scala -val x: (String, Int) = Bob.dropNames // ok +val x: (String, Int) = Bob.toTuple // ok ``` Note that conformance rules for named tuples are analogous to the rules for named parameters. One can assign parameters by position to a named parameter list. ```scala diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index ddc2b545f9ba..d5334cc2773d 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -8,7 +8,7 @@ object NamedTuple: opaque type AnyNamedTuple = Any opaque type NamedTuple[N <: Tuple, +V <: Tuple] >: V <: AnyNamedTuple = V - def apply[N <: Tuple, V <: Tuple](x: V) = x + 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/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index e84e1fe562c3..e128fa8f0e81 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -354,7 +354,6 @@ object Tuple: /** A boolean indicating whether there is an element in the type `X` of `x` * that matches type `Y`. */ - inline def containsType[Y] = constValue[Contains[X, Y]] /* Note: It would be nice to add the following two extension methods: diff --git a/tests/neg/named-tuples.check b/tests/neg/named-tuples.check index 067b6eeb937c..d9b6d686a587 100644 --- a/tests/neg/named-tuples.check +++ b/tests/neg/named-tuples.check @@ -21,80 +21,87 @@ | Required: (String, Int) | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:19:20 ------------------------------------------------------ -19 | val _: NameOnly = person // error +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:19:25 ------------------------------------------------------ +19 | val _: (String, Int) = (name = "", age = 0) // error + | ^^^^^^^^^^^^^^^^^^^^ + | Found: (name : String, age : Int) + | Required: (String, Int) + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:20:20 ------------------------------------------------------ +20 | val _: NameOnly = person // error | ^^^^^^ | Found: (Test.person : (name : String, age : Int)) | Required: Test.NameOnly | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:20:18 ------------------------------------------------------ -20 | val _: Person = nameOnly // error +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:21:18 ------------------------------------------------------ +21 | val _: Person = nameOnly // error | ^^^^^^^^ | Found: (Test.nameOnly : (name : String)) | Required: Test.Person | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:22:36 ------------------------------------------------------ -22 | val _: (age: Int, name: String) = person // error +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:23:36 ------------------------------------------------------ +23 | val _: (age: Int, name: String) = person // error | ^^^^^^ | Found: (Test.person : (name : String, age : Int)) | Required: (age : Int, name : String) | | longer explanation available when compiling with `-explain` --- Error: tests/neg/named-tuples.scala:24:17 --------------------------------------------------------------------------- -24 | val (name = x, agee = y) = person // error +-- Error: tests/neg/named-tuples.scala:25:17 --------------------------------------------------------------------------- +25 | val (name = x, agee = y) = person // error | ^^^^^^^^ | No element named `agee` is defined in selector type (name : String, age : Int) --- Error: tests/neg/named-tuples.scala:27:10 --------------------------------------------------------------------------- -27 | case (name = n, age = a) => () // error // error +-- Error: tests/neg/named-tuples.scala:28:10 --------------------------------------------------------------------------- +28 | case (name = n, age = a) => () // error // error | ^^^^^^^^ | No element named `name` is defined in selector type (String, Int) --- Error: tests/neg/named-tuples.scala:27:20 --------------------------------------------------------------------------- -27 | case (name = n, age = a) => () // error // error +-- Error: tests/neg/named-tuples.scala:28:20 --------------------------------------------------------------------------- +28 | case (name = n, age = a) => () // error // error | ^^^^^^^ | No element named `age` is defined in selector type (String, Int) --- [E172] Type Error: tests/neg/named-tuples.scala:29:27 --------------------------------------------------------------- -29 | val pp = person ++ (1, 2) // error +-- [E172] Type Error: tests/neg/named-tuples.scala:30:27 --------------------------------------------------------------- +30 | val pp = person ++ (1, 2) // error | ^ | Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), Tuple] =:= (true : Boolean). --- [E172] Type Error: tests/neg/named-tuples.scala:32:18 --------------------------------------------------------------- -32 | person ++ (1, 2) match // error +-- [E172] Type Error: tests/neg/named-tuples.scala:33:18 --------------------------------------------------------------- +33 | person ++ (1, 2) match // error | ^ | Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), Tuple] =:= (true : Boolean). --- Error: tests/neg/named-tuples.scala:35:17 --------------------------------------------------------------------------- -35 | val bad = ("", age = 10) // error +-- Error: tests/neg/named-tuples.scala:36:17 --------------------------------------------------------------------------- +36 | val bad = ("", age = 10) // error | ^^^^^^^^ | Illegal combination of named and unnamed tuple elements --- Error: tests/neg/named-tuples.scala:38:20 --------------------------------------------------------------------------- -38 | case (name = n, age) => () // error +-- Error: tests/neg/named-tuples.scala:39:20 --------------------------------------------------------------------------- +39 | case (name = n, age) => () // error | ^^^ | Illegal combination of named and unnamed tuple elements --- Error: tests/neg/named-tuples.scala:39:16 --------------------------------------------------------------------------- -39 | case (name, age = a) => () // error +-- Error: tests/neg/named-tuples.scala:40:16 --------------------------------------------------------------------------- +40 | case (name, age = a) => () // error | ^^^^^^^ | Illegal combination of named and unnamed tuple elements --- Error: tests/neg/named-tuples.scala:42:10 --------------------------------------------------------------------------- -42 | case (age = x) => // error +-- Error: tests/neg/named-tuples.scala:43:10 --------------------------------------------------------------------------- +43 | case (age = x) => // error | ^^^^^^^ | No element named `age` is defined in selector type Tuple --- [E172] Type Error: tests/neg/named-tuples.scala:44:27 --------------------------------------------------------------- -44 | val p2 = person ++ person // error +-- [E172] Type Error: tests/neg/named-tuples.scala:45:27 --------------------------------------------------------------- +45 | val p2 = person ++ person // error | ^ |Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), (("name" : String), ("age" : String))] =:= (true : Boolean). --- [E172] Type Error: tests/neg/named-tuples.scala:45:43 --------------------------------------------------------------- -45 | val p3 = person ++ (first = 11, age = 33) // error +-- [E172] Type Error: tests/neg/named-tuples.scala:46:43 --------------------------------------------------------------- +46 | val p3 = person ++ (first = 11, age = 33) // error | ^ |Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), (("first" : String), ("age" : String))] =:= (true : Boolean). --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:47:22 ------------------------------------------------------ -47 | val p5 = person.zip(first = 11, age = 33) // error +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:48:22 ------------------------------------------------------ +48 | val p5 = person.zip(first = 11, age = 33) // error | ^^^^^^^^^^^^^^^^^^^^ | Found: (first : Int, age : Int) | Required: NamedTuple.NamedTuple[(("name" : String), ("age" : String)), Tuple] | | longer explanation available when compiling with `-explain` --- Warning: tests/neg/named-tuples.scala:24:29 ------------------------------------------------------------------------- -24 | val (name = x, agee = y) = person // error +-- Warning: tests/neg/named-tuples.scala:25:29 ------------------------------------------------------------------------- +25 | val (name = x, agee = y) = person // error | ^^^^^^ |pattern's type (String, Int) is more specialized than the right hand side expression's type (name : String, age : Int) | diff --git a/tests/neg/named-tuples.scala b/tests/neg/named-tuples.scala index 5d1f3777dd73..7dcf2221ec40 100644 --- a/tests/neg/named-tuples.scala +++ b/tests/neg/named-tuples.scala @@ -16,6 +16,7 @@ import language.experimental.namedTuples val nameOnly = (name = "Louis") val y: (String, Int) = person // error + val _: (String, Int) = (name = "", age = 0) // error val _: NameOnly = person // error val _: Person = nameOnly // error diff --git a/tests/run/named-tuples.check b/tests/run/named-tuples.check index c53a2f52ff09..6485aefafa9a 100644 --- a/tests/run/named-tuples.check +++ b/tests/run/named-tuples.check @@ -7,3 +7,4 @@ no match Bob is younger than Bill Bob is younger than Lucy Bill is younger than Lucy +(((Lausanne,Pully),Preverenges),((1003,1009),1028)) diff --git a/tests/run/named-tuples.scala b/tests/run/named-tuples.scala index 0c9e3fb4d455..29b058adab18 100644 --- a/tests/run/named-tuples.scala +++ b/tests/run/named-tuples.scala @@ -1,5 +1,5 @@ import language.experimental.namedTuples -import NamedTuple.toTuple +import NamedTuple.* type Person = (name: String, age: Int) val bob = (name = "Bob", age = 33): (name: String, age: Int) @@ -9,8 +9,8 @@ type Uni = (uni: Double) val uni = (uni = 1.0) val _: Uni = uni -type AddressInfo = (city: String, zip: Int) -val addr = (city = "Lausanne", zip = 1003) +type AddressInfo = (city: String, zipCode: Int) +val addr = (city = "Lausanne", zipCode = 1003) val _: AddressInfo = addr type CombinedInfo = NamedTuple.Concat[Person, AddressInfo] @@ -95,5 +95,13 @@ val _: CombinedInfo = bob ++ addr val x: Person = bob1 // bob1 still has type Person with the unswapped elements case _ => assert(false) + val addr2 = (city = "Pully", zipCode = 1009) + val addr3 = addr.zip(addr2) + val addr4 = addr3.zip("Preverenges", 1028) + println(addr4) + + + + From e0a11cd3d28c1b78dea46cfd6865d5707b71161f Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 5 Dec 2023 14:08:37 +0100 Subject: [PATCH 11/68] Harden NamedTuple handling against ill-formed NamedTuples --- compiler/src/dotty/tools/dotc/core/TypeUtils.scala | 1 + tests/neg/named-tuples-3.scala | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 tests/neg/named-tuples-3.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala index 7ac0df05b268..0dafc6515928 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala @@ -124,6 +124,7 @@ class TypeUtils { case defn.NamedTuple(nmes, vals) => val names = nmes.tupleElementTypesUpTo(bound, normalize).getOrElse(Nil).map: case ConstantType(Constant(str: String)) => str.toTermName + case t => throw TypeError(em"Malformed NamedTuple: names must be string types, but $t was found.") val values = vals.tupleElementTypesUpTo(bound, normalize).getOrElse(Nil) names.zip(values) case t => diff --git a/tests/neg/named-tuples-3.scala b/tests/neg/named-tuples-3.scala new file mode 100644 index 000000000000..55b13bbe12c0 --- /dev/null +++ b/tests/neg/named-tuples-3.scala @@ -0,0 +1,7 @@ +import language.experimental.namedTuples + +def f: NamedTuple.NamedTuple[(Int, Any), (Int, String)] = ??? // error + +type Person = (name: Int, age: String) + +val p: Person = f From b9d86fe471e0cdb8a3e7b93d36c7afaf80b7cc6f Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 5 Dec 2023 19:44:32 +0100 Subject: [PATCH 12/68] Fix test --- tests/neg/named-tuples-3.check | 4 ++++ tests/neg/named-tuples-3.scala | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 tests/neg/named-tuples-3.check diff --git a/tests/neg/named-tuples-3.check b/tests/neg/named-tuples-3.check new file mode 100644 index 000000000000..131f6164748b --- /dev/null +++ b/tests/neg/named-tuples-3.check @@ -0,0 +1,4 @@ +-- Error: tests/neg/named-tuples-3.scala:7:16 -------------------------------------------------------------------------- +7 |val p: Person = f // error + | ^ + | Malformed NamedTuple: names must be string types, but Int was found. diff --git a/tests/neg/named-tuples-3.scala b/tests/neg/named-tuples-3.scala index 55b13bbe12c0..0f1215338b0a 100644 --- a/tests/neg/named-tuples-3.scala +++ b/tests/neg/named-tuples-3.scala @@ -1,7 +1,7 @@ import language.experimental.namedTuples -def f: NamedTuple.NamedTuple[(Int, Any), (Int, String)] = ??? // error +def f: NamedTuple.NamedTuple[(Int, Any), (Int, String)] = ??? type Person = (name: Int, age: String) -val p: Person = f +val p: Person = f // error From 0fbdb497c40aee5b3e17c22bf9030f55ddcc3bca Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 8 Dec 2023 11:37:45 +0100 Subject: [PATCH 13/68] Simplify tupleElementTypes unapply handling --- .../src/dotty/tools/dotc/core/TypeUtils.scala | 1 + .../tools/dotc/transform/PatternMatcher.scala | 4 +- .../dotty/tools/dotc/typer/Applications.scala | 44 +++++++------------ .../src/dotty/tools/dotc/typer/Typer.scala | 1 + 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala index 0dafc6515928..d4be03e9aae4 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala @@ -8,6 +8,7 @@ import Names.{Name, TermName} import Constants.Constant class TypeUtils { + /** A decorator that provides methods on types * that are needed in the transformer pipeline. */ diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index a7f987b8b2f3..11c782b04ec6 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -371,7 +371,9 @@ object PatternMatcher { val arity = productArity(unappType, unapp.srcPos) unapplyProductSeqPlan(unappResult, args, arity) else if unappResult.info <:< defn.NonEmptyTupleTypeRef then - val components = (0 until foldApplyTupleType(unappResult.denot.info).length).toList.map(tupleApp(_, ref(unappResult))) + val components = + (0 until unappResult.denot.info.tupleElementTypes.getOrElse(Nil).length) + .toList.map(tupleApp(_, ref(unappResult))) matchArgsPlan(components, args, onSuccess) else { assert(isGetMatch(unappType)) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 082c8bf3d1db..43e6fe30f370 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -150,7 +150,7 @@ object Applications { (0 until argsNum).map(i => if (i < arity - 1) selTps(i) else elemTp).toList } - def unapplyArgs(unapplyResult: Type, unapplyFn: Tree, args: List[untpd.Tree], pos: SrcPos)(using Context): List[Type] = { + def unapplyArgs(unapplyResult: Type, unapplyFn: Tree, args: List[untpd.Tree], pos: SrcPos)(using Context): List[Type] = def getName(fn: Tree): Name = fn match case TypeApply(fn, _) => getName(fn) @@ -165,46 +165,36 @@ object Applications { Nil } - def unapplySeq(tp: Type)(fallback: => List[Type]): List[Type] = { + def unapplySeq(tp: Type)(fallback: => List[Type]): List[Type] = val elemTp = unapplySeqTypeElemTp(tp) - if (elemTp.exists) args.map(Function.const(elemTp)) - else if (isProductSeqMatch(tp, args.length, pos)) productSeqSelectors(tp, args.length, pos) - else if tp.derivesFrom(defn.NonEmptyTupleClass) then foldApplyTupleType(tp) + if elemTp.exists then + args.map(Function.const(elemTp)) + else if isProductSeqMatch(tp, args.length, pos) then + productSeqSelectors(tp, args.length, pos) + else if tp.derivesFrom(defn.NonEmptyTupleClass) then + tp.tupleElementTypes.getOrElse(Nil) else fallback - } - if (unapplyName == nme.unapplySeq) - unapplySeq(unapplyResult) { + if unapplyName == nme.unapplySeq then + unapplySeq(unapplyResult): if (isGetMatch(unapplyResult, pos)) unapplySeq(getTp)(fail) else fail - } - else { + else assert(unapplyName == nme.unapply) - if (isProductMatch(unapplyResult, args.length, pos)) + if isProductMatch(unapplyResult, args.length, pos) then productSelectorTypes(unapplyResult, pos) - else if (isGetMatch(unapplyResult, pos)) + else if isGetMatch(unapplyResult, pos) then getUnapplySelectors(getTp, args, pos) - else if (unapplyResult.widenSingleton isRef defn.BooleanClass) + else if unapplyResult.derivesFrom(defn.BooleanClass) then Nil - else if (defn.isProductSubType(unapplyResult) && productArity(unapplyResult, pos) != 0) + else if defn.isProductSubType(unapplyResult) && productArity(unapplyResult, pos) != 0 then productSelectorTypes(unapplyResult, pos) // this will cause a "wrong number of arguments in pattern" error later on, // which is better than the message in `fail`. else if unapplyResult.derivesFrom(defn.NonEmptyTupleClass) then - foldApplyTupleType(unapplyResult) + unapplyResult.tupleElementTypes.getOrElse(Nil) else fail - } - } - - def foldApplyTupleType(tp: Type)(using Context): List[Type] = - object tupleFold extends TypeAccumulator[List[Type]]: - override def apply(accum: List[Type], t: Type): List[Type] = - t match - case AppliedType(tycon, x :: x2 :: Nil) if tycon.typeSymbol == defn.PairClass => - apply(x :: accum, x2) - case x => foldOver(accum, x) - end tupleFold - tupleFold(Nil, tp).reverse + end unapplyArgs def wrapDefs(defs: mutable.ListBuffer[Tree] | Null, tree: Tree)(using Context): Tree = if (defs != null && defs.nonEmpty) tpd.Block(defs.toList, tree) else tree diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index a510a6eaa578..5dda242df4ff 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2460,6 +2460,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer || body1.isInstanceOf[Literal] val symTp = if isStableIdentifierOrLiteral || pt.isNamedTupleType then pt + // need to combine tuple element types with expected named type else if isWildcardStarArg(body1) || pt == defn.ImplicitScrutineeTypeRef || body1.tpe <:< pt // There is some strange interaction with gadt matching. From 6f53dcdb37ea6036c75eca5639e685ac93b6eaab Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 8 Dec 2023 11:47:38 +0100 Subject: [PATCH 14/68] Fix pattern matching for get matches --- .../tools/dotc/transform/PatternMatcher.scala | 4 +- .../dotty/tools/dotc/typer/Applications.scala | 147 +++++++++++------- .../src/dotty/tools/dotc/typer/Checking.scala | 4 +- .../src/dotty/tools/dotc/typer/Typer.scala | 50 +++--- tests/run/named-patterns.check | 5 + tests/run/named-patterns.scala | 20 ++- 6 files changed, 145 insertions(+), 85 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index 11c782b04ec6..8856bd10bf08 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -379,7 +379,7 @@ object PatternMatcher { assert(isGetMatch(unappType)) val argsPlan = { val get = ref(unappResult).select(nme.get, _.info.isParameterless) - val arity = productArity(get.tpe, unapp.srcPos) + val arity = productArity(get.tpe.stripNamedTuple, unapp.srcPos) if (isUnapplySeq) letAbstract(get) { getResult => if unapplySeqTypeElemTp(get.tpe).exists @@ -390,7 +390,7 @@ object PatternMatcher { letAbstract(get) { getResult => val selectors = if (args.tail.isEmpty) ref(getResult) :: Nil - else productSelectors(get.tpe).map(ref(getResult).select(_)) + else productSelectors(getResult.info).map(ref(getResult).select(_)) matchArgsPlan(selectors, args, onSuccess) } } diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 43e6fe30f370..f7fe5be7a1c7 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -18,6 +18,7 @@ import Names.* import StdNames.* import ContextOps.* import NameKinds.DefaultGetterName +import Typer.tryEither import ProtoTypes.* import Inferencing.* import reporting.* @@ -135,14 +136,6 @@ object Applications { sels.takeWhile(_.exists).toList } - def getUnapplySelectors(tp: Type, args: List[untpd.Tree], pos: SrcPos)(using Context): List[Type] = - if (args.length > 1 && !(tp.derivesFrom(defn.SeqClass))) { - val sels = productSelectorTypes(tp, pos) - if (sels.length == args.length) sels - else tp :: Nil - } - else tp :: Nil - def productSeqSelectors(tp: Type, argsNum: Int, pos: SrcPos)(using Context): List[Type] = { val selTps = productSelectorTypes(tp, pos) val arity = selTps.length @@ -150,22 +143,30 @@ object Applications { (0 until argsNum).map(i => if (i < arity - 1) selTps(i) else elemTp).toList } - def unapplyArgs(unapplyResult: Type, unapplyFn: Tree, args: List[untpd.Tree], pos: SrcPos)(using Context): List[Type] = - def getName(fn: Tree): Name = + /** A utility class that matches results of unapplys with patterns. Two queriable members: + * val argTypes: List[Type] + * def typedPatterns(qual: untpd.Tree, typer: Typer): List[Tree] + * TODO: Move into Applications trait. No need to keep it outside. But it's a large + * refactor, so do this when the rest is merged. + */ + class UnapplyArgs(unapplyResult: Type, unapplyFn: Tree, unadaptedArgs: List[untpd.Tree], pos: SrcPos)(using Context): + private var args = unadaptedArgs + + private def getName(fn: Tree): Name = fn match case TypeApply(fn, _) => getName(fn) case Apply(fn, _) => getName(fn) case fn: RefTree => fn.name - val unapplyName = getName(unapplyFn) // tolerate structural `unapply`, which does not have a symbol + private val unapplyName = getName(unapplyFn) // tolerate structural `unapply`, which does not have a symbol - def getTp = extractorMemberType(unapplyResult, nme.get, pos) + private def getTp = extractorMemberType(unapplyResult, nme.get, pos) - def fail = { + private def fail = { report.error(UnapplyInvalidReturnType(unapplyResult, unapplyName), pos) Nil } - def unapplySeq(tp: Type)(fallback: => List[Type]): List[Type] = + private def unapplySeq(tp: Type)(fallback: => List[Type]): List[Type] = val elemTp = unapplySeqTypeElemTp(tp) if elemTp.exists then args.map(Function.const(elemTp)) @@ -175,26 +176,84 @@ object Applications { tp.tupleElementTypes.getOrElse(Nil) else fallback - if unapplyName == nme.unapplySeq then - unapplySeq(unapplyResult): - if (isGetMatch(unapplyResult, pos)) unapplySeq(getTp)(fail) - else fail - else - assert(unapplyName == nme.unapply) - if isProductMatch(unapplyResult, args.length, pos) then - productSelectorTypes(unapplyResult, pos) - else if isGetMatch(unapplyResult, pos) then - getUnapplySelectors(getTp, args, pos) - else if unapplyResult.derivesFrom(defn.BooleanClass) then - Nil - else if defn.isProductSubType(unapplyResult) && productArity(unapplyResult, pos) != 0 then - productSelectorTypes(unapplyResult, pos) - // this will cause a "wrong number of arguments in pattern" error later on, - // which is better than the message in `fail`. - else if unapplyResult.derivesFrom(defn.NonEmptyTupleClass) then - unapplyResult.tupleElementTypes.getOrElse(Nil) - else fail - end unapplyArgs + private def tryAdaptPatternArgs(elems: List[untpd.Tree], pt: Type)(using Context): Option[List[untpd.Tree]] = + tryEither[Option[List[untpd.Tree]]] + (Some(desugar.adaptPatternArgs(elems, pt))) + ((_, _) => None) + + private def getUnapplySelectors(tp: Type)(using Context): List[Type] = + if args.length > 1 && !(tp.derivesFrom(defn.SeqClass)) then + productUnapplySelectors(tp).getOrElse: + // There are unapplys with return types which have `get` and `_1, ..., _n` + // as members, but which are not subtypes of Product. So `productUnapplySelectors` + // would return None for these, but they are still valid types + // for a get match. A test case is pos/extractors.scala. + val sels = productSelectorTypes(tp, pos) + if (sels.length == args.length) sels + else tp :: Nil + else tp :: Nil + + private def productUnapplySelectors(tp: Type)(using Context): Option[List[Type]] = + if defn.isProductSubType(tp) then + tryAdaptPatternArgs(args, tp) match + case Some(args1) if isProductMatch(tp, args1.length, pos) => + args = args1 + Some(productSelectorTypes(tp, pos)) + case _ => None + else tp.widen.normalized.dealias match + case tp @ defn.NamedTuple(_, tt) => + tryAdaptPatternArgs(args, tp) match + case Some(args1) => + args = args1 + tt.tupleElementTypes + case _ => None + case _ => None + + /** The computed argument types which will be the scutinees of the sub-patterns. */ + val argTypes: List[Type] = + if unapplyName == nme.unapplySeq then + unapplySeq(unapplyResult): + if (isGetMatch(unapplyResult, pos)) unapplySeq(getTp)(fail) + else fail + else + assert(unapplyName == nme.unapply) + productUnapplySelectors(unapplyResult).getOrElse: + if isGetMatch(unapplyResult, pos) then + getUnapplySelectors(getTp) + else if unapplyResult.derivesFrom(defn.BooleanClass) then + Nil + else if unapplyResult.derivesFrom(defn.NonEmptyTupleClass) then + unapplyResult.tupleElementTypes.getOrElse(Nil) + else if defn.isProductSubType(unapplyResult) && productArity(unapplyResult, pos) != 0 then + productSelectorTypes(unapplyResult, pos) + // this will cause a "wrong number of arguments in pattern" error later on, + // which is better than the message in `fail`. + else fail + + /** The typed pattens of this unapply */ + def typedPatterns(qual: untpd.Tree, typer: Typer): List[Tree] = + unapp.println(i"unapplyQual = $qual, unapplyArgs = ${unapplyResult} with $argTypes / $args") + for argType <- argTypes do + assert(!isBounds(argType), unapplyResult.show) + val alignedArgs = argTypes match + case argType :: Nil + if args.lengthCompare(1) > 0 + && Feature.autoTuplingEnabled + && defn.isTupleNType(argType) => + untpd.Tuple(args) :: Nil + case _ => + args + val alignedArgTypes = + if argTypes.length == alignedArgs.length then + argTypes + else + report.error(UnapplyInvalidNumberOfArguments(qual, argTypes), pos) + argTypes.take(args.length) ++ + List.fill(argTypes.length - args.length)(WildcardType) + alignedArgs.lazyZip(alignedArgTypes).map(typer.typed(_, _)) + .showing(i"unapply patterns = $result", unapp) + + end UnapplyArgs def wrapDefs(defs: mutable.ListBuffer[Tree] | Null, tree: Tree)(using Context): Tree = if (defs != null && defs.nonEmpty) tpd.Block(defs.toList, tree) else tree @@ -1549,25 +1608,9 @@ trait Applications extends Compatibility { typedExpr(untpd.TypedSplice(Apply(unapplyFn, dummyArg :: Nil))) inlinedUnapplyFnAndApp(dummyArg, unapplyAppCall) - var argTypes = unapplyArgs(unapplyApp.tpe, unapplyFn, args, tree.srcPos) - for (argType <- argTypes) assert(!isBounds(argType), unapplyApp.tpe.show) - val bunchedArgs = argTypes match { - case argType :: Nil => - if args.lengthCompare(1) > 0 - && Feature.autoTuplingEnabled - && defn.isTupleNType(argType) - then untpd.Tuple(args) :: Nil - else args - case _ => args - } - if (argTypes.length != bunchedArgs.length) { - report.error(UnapplyInvalidNumberOfArguments(qual, argTypes), tree.srcPos) - argTypes = argTypes.take(args.length) ++ - List.fill(argTypes.length - args.length)(WildcardType) - } - val unapplyPatterns = bunchedArgs.lazyZip(argTypes) map (typed(_, _)) + val unapplyPatterns = UnapplyArgs(unapplyApp.tpe, unapplyFn, unadaptedArgs, tree.srcPos) + .typedPatterns(qual, this) val result = assignType(cpy.UnApply(tree)(newUnapplyFn, unapplyImplicits(dummyArg, unapplyApp), unapplyPatterns), ownType) - unapp.println(s"unapply patterns = $unapplyPatterns") if (ownType.stripped eq selType.stripped) || ownType.isError then result else tryWithTypeTest(Typed(result, TypeTree(ownType)), selType) case tp => diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 3c74e9f4ed90..662a4feb867a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -29,7 +29,7 @@ import config.Printers.{typr, patmatch} import NameKinds.DefaultGetterName import NameOps.* import SymDenotations.{NoCompleter, NoDenotation} -import Applications.unapplyArgs +import Applications.UnapplyArgs import Inferencing.isFullyDefined import transform.patmat.SpaceEngine.{isIrrefutable, isIrrefutableQuotePattern} import transform.ValueClasses.underlyingOfValueClass @@ -982,7 +982,7 @@ trait Checking { case UnApply(fn, implicits, pats) => check(pat, pt) && (isIrrefutable(fn, pats.length) || fail(pat, pt, Reason.RefutableExtractor)) && { - val argPts = unapplyArgs(fn.tpe.widen.finalResultType, fn, pats, pat.srcPos) + val argPts = UnapplyArgs(fn.tpe.widen.finalResultType, fn, pats, pat.srcPos).argTypes pats.corresponds(argPts)(recur) } case Alternative(pats) => diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 5dda242df4ff..038693d3d7e1 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -113,6 +113,31 @@ object Typer { def rememberSearchFailure(tree: tpd.Tree, fail: SearchFailure) = tree.putAttachment(HiddenSearchFailure, fail :: tree.attachmentOrElse(HiddenSearchFailure, Nil)) + + def tryEither[T](op: Context ?=> T)(fallBack: (T, TyperState) => T)(using Context): T = { + val nestedCtx = ctx.fresh.setNewTyperState() + val result = op(using nestedCtx) + if (nestedCtx.reporter.hasErrors && !nestedCtx.reporter.hasStickyErrors) { + record("tryEither.fallBack") + fallBack(result, nestedCtx.typerState) + } + else { + record("tryEither.commit") + nestedCtx.typerState.commit() + result + } + } + + /** Try `op1`, if there are errors, try `op2`, if `op2` also causes errors, fall back + * to errors and result of `op1`. + */ + def tryAlternatively[T](op1: Context ?=> T)(op2: Context ?=> T)(using Context): T = + tryEither(op1) { (failedVal, failedState) => + tryEither(op2) { (_, _) => + failedState.commit() + failedVal + } + } } /** Typecheck trees, the main entry point is `typed`. * @@ -3441,31 +3466,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def typedPattern(tree: untpd.Tree, selType: Type = WildcardType)(using Context): Tree = withMode(Mode.Pattern)(typed(tree, selType)) - def tryEither[T](op: Context ?=> T)(fallBack: (T, TyperState) => T)(using Context): T = { - val nestedCtx = ctx.fresh.setNewTyperState() - val result = op(using nestedCtx) - if (nestedCtx.reporter.hasErrors && !nestedCtx.reporter.hasStickyErrors) { - record("tryEither.fallBack") - fallBack(result, nestedCtx.typerState) - } - else { - record("tryEither.commit") - nestedCtx.typerState.commit() - result - } - } - - /** Try `op1`, if there are errors, try `op2`, if `op2` also causes errors, fall back - * to errors and result of `op1`. - */ - def tryAlternatively[T](op1: Context ?=> T)(op2: Context ?=> T)(using Context): T = - tryEither(op1) { (failedVal, failedState) => - tryEither(op2) { (_, _) => - failedState.commit() - failedVal - } - } - /** Is `pt` a prototype of an `apply` selection, or a parameterless function yielding one? */ def isApplyProto(pt: Type)(using Context): Boolean = pt.revealIgnored match { case pt: SelectionProto => pt.name == nme.apply diff --git a/tests/run/named-patterns.check b/tests/run/named-patterns.check index ba8dbb8b21f7..0b9f3661b7ba 100644 --- a/tests/run/named-patterns.check +++ b/tests/run/named-patterns.check @@ -3,6 +3,11 @@ name Bob age 22 age 22, name Bob Bob, 22 +name Bob, age 22 +name (Bob,22) +age (Bob,22) +age 22, name Bob +Bob, 22 1003 Lausanne, Rue de la Gare 44 1003 Lausanne Rue de la Gare in Lausanne diff --git a/tests/run/named-patterns.scala b/tests/run/named-patterns.scala index 1e7e0697e782..73753c855073 100644 --- a/tests/run/named-patterns.scala +++ b/tests/run/named-patterns.scala @@ -6,6 +6,10 @@ object Test1: object Person: def unapply(p: Person): (name: String, age: Int) = (p.name, p.age) + class Person2(val name: String, val age: Int) + object Person2: + def unapply(p: Person2): Option[(name: String, age: Int)] = Some((p.name, p.age)) + case class Address(city: String, zip: Int, street: String, number: Int) @main def Test = @@ -21,6 +25,18 @@ object Test1: bob match case Person(age, name) => println(s"$age, $name") + val bob2 = Person2("Bob", 22) + bob2 match + case Person2(name = n, age = a) => println(s"name $n, age $a") + bob2 match + case Person2(name = n) => println(s"name $n") + bob2 match + case Person2(age = a) => println(s"age $a") + bob2 match + case Person2(age = a, name = n) => println(s"age $a, name $n") + bob2 match + case Person2(age, name) => println(s"$age, $name") + val addr = Address("Lausanne", 1003, "Rue de la Gare", 44) addr match case Address(city = c, zip = z, street = s, number = n) => @@ -37,7 +53,3 @@ object Test1: addr match case Address(c, z, s, number) => println(s"$z $c, $s $number") - - - - From b44f15d477da2b71854b3036f016ac868e0a9eb4 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 14 Dec 2023 16:02:46 +0100 Subject: [PATCH 15/68] Another fix for named get patterns Also, add deep matches to tests --- .../dotty/tools/dotc/typer/Applications.scala | 7 ++++++- tests/run/named-patterns.check | 9 +++++++-- tests/run/named-patterns.scala | 19 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index f7fe5be7a1c7..76d057f15408 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -182,7 +182,12 @@ object Applications { ((_, _) => None) private def getUnapplySelectors(tp: Type)(using Context): List[Type] = - if args.length > 1 && !(tp.derivesFrom(defn.SeqClass)) then + // We treat patterns as product elements if + // they are named, or there is more than one pattern + val isProduct = args match + case x :: xs => x.isInstanceOf[untpd.NamedArg] || xs.nonEmpty + case _ => false + if isProduct && !tp.derivesFrom(defn.SeqClass) then productUnapplySelectors(tp).getOrElse: // There are unapplys with return types which have `get` and `_1, ..., _n` // as members, but which are not subtypes of Product. So `productUnapplySelectors` diff --git a/tests/run/named-patterns.check b/tests/run/named-patterns.check index 0b9f3661b7ba..9ccc08d67069 100644 --- a/tests/run/named-patterns.check +++ b/tests/run/named-patterns.check @@ -4,8 +4,8 @@ age 22 age 22, name Bob Bob, 22 name Bob, age 22 -name (Bob,22) -age (Bob,22) +name Bob +age 22 age 22, name Bob Bob, 22 1003 Lausanne, Rue de la Gare 44 @@ -13,3 +13,8 @@ Bob, 22 Rue de la Gare in Lausanne 1003 Lausanne, Rue de la Gare 44 1003 Lausanne, Rue de la Gare 44 +Bob, aged 22, in 1003 Lausanne, Rue de la Gare 44 +Bob in 1003 Lausanne +aged 22 in Rue de la Gare in Lausanne +Bob, aged 22 in 1003 Lausanne, Rue de la Gare 44 +Bob, aged 22 in 1003 Lausanne, Rue de la Gare 44 diff --git a/tests/run/named-patterns.scala b/tests/run/named-patterns.scala index 73753c855073..7c24dc8d683a 100644 --- a/tests/run/named-patterns.scala +++ b/tests/run/named-patterns.scala @@ -53,3 +53,22 @@ object Test1: addr match case Address(c, z, s, number) => println(s"$z $c, $s $number") + + type Person3 = (p: Person2, addr: Address) + + val p3 = (p = bob2, addr = addr) + p3 match + case (addr = Address(city = c, zip = z, street = s, number = n), p = Person2(name = nn, age = a)) => + println(s"$nn, aged $a, in $z $c, $s $n") + p3 match + case (p = Person2(name = nn), addr = Address(zip = z, city = c)) => + println(s"$nn in $z $c") + p3 match + case (p = Person2(age = a), addr = Address(city = c, street = s)) => + println(s"aged $a in $s in $c") + p3 match + case (Person2(age = a, name = nn), Address(number = n, street = s, zip = z, city = c)) => + println(s"$nn, aged $a in $z $c, $s $n") + p3 match + case (Person2(nn, a), Address(c, z, s, number)) => + println(s"$nn, aged $a in $z $c, $s $number") From 02aa578d26335ee1ce211339e33bf993a7c804ee Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 14 Dec 2023 18:51:03 +0100 Subject: [PATCH 16/68] Avoid widening into unreducible types when inferring types This is a general improvement, independent of named tuples. --- compiler/src/dotty/tools/dotc/core/TypeOps.scala | 7 ++++++- tests/pos/named-tuple-widen.scala | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 tests/pos/named-tuple-widen.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 012464f71d9b..e67e60dd45ea 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -385,7 +385,12 @@ object TypeOps: (tp.tp1.dealias, tp.tp2.dealias) match case (tp1 @ AppliedType(tycon1, args1), tp2 @ AppliedType(tycon2, args2)) if tycon1.typeSymbol == tycon2.typeSymbol && (tycon1 =:= tycon2) => - mergeRefinedOrApplied(tp1, tp2) + mergeRefinedOrApplied(tp1, tp2) match + case tp: AppliedType if tp.isUnreducibleWild => + // fall back to or-dominators rather tahn inferring a type that would + // caue an unreducible type error later. + approximateOr(tp1, tp2) + case tp => tp case (tp1, tp2) => approximateOr(tp1, tp2) case _ => diff --git a/tests/pos/named-tuple-widen.scala b/tests/pos/named-tuple-widen.scala new file mode 100644 index 000000000000..410832e04c17 --- /dev/null +++ b/tests/pos/named-tuple-widen.scala @@ -0,0 +1,9 @@ +import language.experimental.namedTuples + +class A +class B +val y1: (a1: A, b1: B) = ??? +val y2: (a2: A, b2: B) = ??? +var z1 = if ??? then y1 else y2 // -- what is the type of z2 +var z2: NamedTuple.AnyNamedTuple = z1 +val _ = z1 = z2 \ No newline at end of file From b7115e795afb0a5507044bdf2a50b4ad2e6ccb03 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 12 Jan 2024 14:59:15 +0100 Subject: [PATCH 17/68] Fix rebase breakage --- compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index 8856bd10bf08..f22a17f7fd27 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -364,7 +364,6 @@ object PatternMatcher { val selectors = productSelectors(unappType).take(args.length) .map(ref(unappResult).select(_)) matchArgsPlan(selectors, args, onSuccess) - } else if isUnapplySeq && unapplySeqTypeElemTp(unappType.finalResultType).exists then unapplySeqPlan(unappResult, args) else if isUnapplySeq && isProductSeqMatch(unappType, args.length, unapp.srcPos) then From 5c9bb5f29b1da50779ac3700f3641b3c9a13fdc9 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 12 Jan 2024 18:40:19 +0100 Subject: [PATCH 18/68] Make NamedTuples work under new MatchType spec --- library/src/scala/NamedTuple.scala | 23 ++++++++++++------- tests/pos/named-tuples-strawman-2.scala | 21 ++++++++++------- .../stdlibExperimentalDefinitions.scala | 2 ++ 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index d5334cc2773d..58e342e74864 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -15,6 +15,8 @@ object NamedTuple: extension [V <: Tuple](x: V) inline def withNames[N <: Tuple]: NamedTuple[N, V] = x + export NamedTupleDecomposition.{Names, DropNames} + extension [N <: Tuple, V <: Tuple](x: NamedTuple[N, V]) /** The underlying tuple without the names */ @@ -103,14 +105,6 @@ object NamedTuple: end extension - /** 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 - - /** The value types of a named tuple represented as a regular tuple. */ - type DropNames[NT <: AnyNamedTuple] <: Tuple = NT match - case NamedTuple[_, x] => x - /** The size of a named tuple, represented as a literal constant subtype of Int */ type Size[X <: AnyNamedTuple] = Tuple.Size[DropNames[X]] @@ -182,3 +176,16 @@ object NamedTuple: NamedTuple[Names[X], Tuple.Zip[DropNames[X], DropNames[Y]]] end NamedTuple + +@experimental +/** Separate from NamedTuple object so that we can match on the opaque type NamedTuple. */ +object NamedTupleDecomposition: + import NamedTuple.* + + /** 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 + + /** The value types of a named tuple represented as a regular tuple. */ + type DropNames[NT <: AnyNamedTuple] <: Tuple = NT match + case NamedTuple[_, x] => x diff --git a/tests/pos/named-tuples-strawman-2.scala b/tests/pos/named-tuples-strawman-2.scala index 7c18d063bcf4..709f537f8114 100644 --- a/tests/pos/named-tuples-strawman-2.scala +++ b/tests/pos/named-tuples-strawman-2.scala @@ -62,6 +62,17 @@ object TupleOps: inline def concatDistinct[X <: Tuple, Y <: Tuple](xs: X, ys: Y): ConcatDistinct[X, Y] = (xs ++ ys.filter[Y, [Elem] =>> ![Contains[X, Elem]]]).asInstanceOf[ConcatDistinct[X, Y]] +object NamedTupleDecomposition: + import NamedTupleOps.* + + /** The names of the named tuple type `NT` */ + type Names[NT <: AnyNamedTuple] <: Tuple = NT match + case NamedTuple[n, _] => n + + /** The value types of the named tuple type `NT` */ + type DropNames[NT <: AnyNamedTuple] <: Tuple = NT match + case NamedTuple[_, x] => x + object NamedTupleOps: import TupleOps.* @@ -69,17 +80,11 @@ object NamedTupleOps: opaque type NamedTuple[N <: Tuple, +X <: Tuple] >: X <: AnyNamedTuple = X + export NamedTupleDecomposition.* + object NamedTuple: def apply[N <: Tuple, X <: Tuple](x: X): NamedTuple[N, X] = x - /** The names of the named tuple type `NT` */ - type Names[NT <: AnyNamedTuple] <: Tuple = NT match - case NamedTuple[n, _] => n - - /** The value types of the named tuple type `NT` */ - type DropNames[NT <: AnyNamedTuple] <: Tuple = NT match - case NamedTuple[_, x] => x - extension [NT <: AnyNamedTuple](x: NT) inline def toTuple: DropNames[NT] = x.asInstanceOf inline def names: Names[NT] = constValueTuple[Names[NT]] diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index 26cad0668b37..48ff5407ac87 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -77,6 +77,8 @@ val experimentalDefinitionInLibrary = Set( // New feature: named tuples "scala.NamedTuple", "scala.NamedTuple$", + "scala.NamedTupleDecomposition", + "scala.NamedTupleDecomposition$", ) From 47588307161a9e64163bbda3d70215dc72edc35a Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 12 Jan 2024 18:40:55 +0100 Subject: [PATCH 19/68] Avoid TypeError exception in RefinedPrinter --- compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 2873325aecb6..7e8bc24d1aee 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -244,7 +244,9 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { def appliedText(tp: Type): Text = tp match case tp @ AppliedType(tycon, args) => - val namedElems = tp.namedTupleElementTypesUpTo(200, normalize = false) + val namedElems = + try tp.namedTupleElementTypesUpTo(200, normalize = false) + catch case ex: TypeError => Nil if namedElems.nonEmpty then toTextNamedTuple(namedElems) else tp.tupleElementTypesUpTo(200, normalize = false) match From 18f600de794e5a985fa77e59cb4833c51d3e3fed Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 12 Jan 2024 18:41:31 +0100 Subject: [PATCH 20/68] Move named-tuples-strawman.ccala to pending --- tests/{ => pending}/pos/named-tuples-strawman.scala | 1 + 1 file changed, 1 insertion(+) rename tests/{ => pending}/pos/named-tuples-strawman.scala (96%) diff --git a/tests/pos/named-tuples-strawman.scala b/tests/pending/pos/named-tuples-strawman.scala similarity index 96% rename from tests/pos/named-tuples-strawman.scala rename to tests/pending/pos/named-tuples-strawman.scala index 859e1d1448e2..35675d1bfc76 100644 --- a/tests/pos/named-tuples-strawman.scala +++ b/tests/pending/pos/named-tuples-strawman.scala @@ -1,3 +1,4 @@ +// Currently does not compile because of #19434 object Test: object Named: From 5513ed6b334a5c2b63d0fa67ad142c47257ff85e Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 12 Jan 2024 18:49:15 +0100 Subject: [PATCH 21/68] Update check file --- tests/neg/named-tuples-3.check | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/neg/named-tuples-3.check b/tests/neg/named-tuples-3.check index 131f6164748b..2091c36191c0 100644 --- a/tests/neg/named-tuples-3.check +++ b/tests/neg/named-tuples-3.check @@ -1,4 +1,7 @@ --- Error: tests/neg/named-tuples-3.scala:7:16 -------------------------------------------------------------------------- +-- [E007] Type Mismatch Error: tests/neg/named-tuples-3.scala:7:16 ----------------------------------------------------- 7 |val p: Person = f // error | ^ - | Malformed NamedTuple: names must be string types, but Int was found. + | Found: NamedTuple.NamedTuple[(Int, Any), (Int, String)] + | Required: Person + | + | longer explanation available when compiling with `-explain` From fb1541adaf80a796d9cd8cccd59673e49b287570 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 14 Jan 2024 11:00:44 +0100 Subject: [PATCH 22/68] Better printing of NamedTuple type trees Use the sugared representation, not the raw NamedTuple type tree. --- .../src/dotty/tools/dotc/printing/RefinedPrinter.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 7e8bc24d1aee..5e15fd2ddd15 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -572,7 +572,12 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { changePrec(AndTypePrec) { toText(args(0)) ~ " & " ~ atPrec(AndTypePrec + 1) { toText(args(1)) } } else if defn.isFunctionSymbol(tpt.symbol) && tpt.isInstanceOf[TypeTree] && tree.hasType && !printDebug - then changePrec(GlobalPrec) { toText(tree.typeOpt) } + then + changePrec(GlobalPrec) { toText(tree.typeOpt) } + else if tpt.symbol == defn.NamedTupleTypeRef.symbol + && !printDebug && tree.typeOpt.exists + then + toText(tree.typeOpt) else args match case arg :: _ if arg.isTerm => toTextLocal(tpt) ~ "(" ~ Text(args.map(argText), ", ") ~ ")" From cf09b19a63be7354c2f3fa744c22ddef1e8fe0e2 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 14 Jan 2024 11:06:53 +0100 Subject: [PATCH 23/68] Add FieldsOf type --- .../dotty/tools/dotc/core/Definitions.scala | 6 ++- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../src/dotty/tools/dotc/core/TypeEval.scala | 27 ++++++++++-- .../reference/experimental/named-tuples.md | 44 ++++++++++++++++++- library/src/scala/NamedTuple.scala | 4 +- tests/neg/fieldsOf.scala | 13 ++++++ tests/pos/fieldsOf.scala | 18 ++++++++ tests/run/fieldsOf.check | 17 +++++++ 8 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 tests/neg/fieldsOf.scala create mode 100644 tests/pos/fieldsOf.scala create mode 100644 tests/run/fieldsOf.check diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 5868da397fc3..4b91cff4dda2 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1318,6 +1318,9 @@ class Definitions { final def isCompiletime_S(sym: Symbol)(using Context): Boolean = sym.name == tpnme.S && sym.owner == CompiletimeOpsIntModuleClass + final def isNamedTuple_FieldsOf(sym: Symbol)(using Context): Boolean = + sym.name == tpnme.FieldsOf && sym.owner == NamedTupleModule.moduleClass + private val compiletimePackageAnyTypes: Set[Name] = Set( tpnme.Equals, tpnme.NotEquals, tpnme.IsConst, tpnme.ToString ) @@ -1346,7 +1349,7 @@ class Definitions { tpnme.Plus, tpnme.Length, tpnme.Substring, tpnme.Matches, tpnme.CharAt ) private val compiletimePackageOpTypes: Set[Name] = - Set(tpnme.S) + Set(tpnme.S, tpnme.FieldsOf) ++ compiletimePackageAnyTypes ++ compiletimePackageIntTypes ++ compiletimePackageLongTypes @@ -1359,6 +1362,7 @@ class Definitions { compiletimePackageOpTypes.contains(sym.name) && ( isCompiletime_S(sym) + || isNamedTuple_FieldsOf(sym) || sym.owner == CompiletimeOpsAnyModuleClass && compiletimePackageAnyTypes.contains(sym.name) || sym.owner == CompiletimeOpsIntModuleClass && compiletimePackageIntTypes.contains(sym.name) || sym.owner == CompiletimeOpsLongModuleClass && compiletimePackageLongTypes.contains(sym.name) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index ebd246d20575..7ab6750f7a0b 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -361,6 +361,7 @@ object StdNames { val Eql: N = "Eql" val EnumValue: N = "EnumValue" val ExistentialTypeTree: N = "ExistentialTypeTree" + val FieldsOf: N = "FieldsOf" val Flag : N = "Flag" val Ident: N = "Ident" val Import: N = "Import" diff --git a/compiler/src/dotty/tools/dotc/core/TypeEval.scala b/compiler/src/dotty/tools/dotc/core/TypeEval.scala index b5684b07f181..643b83882648 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeEval.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeEval.scala @@ -6,11 +6,14 @@ import Types.*, Contexts.*, Symbols.*, Constants.*, Decorators.* import config.Printers.typr import reporting.trace import StdNames.tpnme +import Flags.CaseClass +import TypeOps.nestedPairs object TypeEval: def tryCompiletimeConstantFold(tp: AppliedType)(using Context): Type = tp.tycon match case tycon: TypeRef if defn.isCompiletimeAppliedType(tycon.symbol) => + extension (tp: Type) def fixForEvaluation: Type = tp.normalized.dealias match // enable operations for constant singleton terms. E.g.: @@ -94,6 +97,21 @@ object TypeEval: throw TypeError(em"${e.getMessage.nn}") ConstantType(Constant(result)) + def fieldsOf: Option[Type] = + expectArgsNum(1) + val arg = tp.args.head + val cls = arg.classSymbol + if cls.is(CaseClass) then + val fields = cls.caseAccessors + val fieldLabels = fields.map: field => + ConstantType(Constant(field.name.toString)) + val fieldTypes = fields.map(arg.memberInfo) + Some: + defn.NamedTupleTypeRef.appliedTo: + nestedPairs(fieldLabels) :: nestedPairs(fieldTypes) :: Nil + else + None + def constantFold1[T](extractor: Type => Option[T], op: T => Any): Option[Type] = expectArgsNum(1) extractor(tp.args.head).map(a => runConstantOp(op(a))) @@ -122,11 +140,14 @@ object TypeEval: yield runConstantOp(op(a, b, c)) trace(i"compiletime constant fold $tp", typr, show = true) { - val name = tycon.symbol.name - val owner = tycon.symbol.owner + val sym = tycon.symbol + val name = sym.name + val owner = sym.owner val constantType = - if defn.isCompiletime_S(tycon.symbol) then + if defn.isCompiletime_S(sym) then constantFold1(natValue, _ + 1) + else if defn.isNamedTuple_FieldsOf(sym) then + fieldsOf else if owner == defn.CompiletimeOpsAnyModuleClass then name match case tpnme.Equals => constantFold2(constValue, _ == _) case tpnme.NotEquals => constantFold2(constValue, _ != _) diff --git a/docs/_docs/reference/experimental/named-tuples.md b/docs/_docs/reference/experimental/named-tuples.md index f9ba87382e32..7dd7049d1126 100644 --- a/docs/_docs/reference/experimental/named-tuples.md +++ b/docs/_docs/reference/experimental/named-tuples.md @@ -100,6 +100,24 @@ The translation of named tuples to instances of `NamedTuple` is fixed by the spe - All tuple operations also work with named tuples "out of the box". - Macro libraries can rely on this expansion. +### The FieldsOf Type + +The `NamedTuple` object contains a type definition +```scala + type FieldsOf[T] <: AnyNamedTuple +``` +`FieldsOf` is treated specially by the compiler. When `FieldsOf` is applied to +an argument type that is an instance of a case class, the type expands to the named +tuple consisting of all the fields of that case class. Here, fields means: elements of the first parameter section. For instance, assuming +```scala +case class City(zip: Int, name: String, population: Int) +``` +then `FieldsOf[City]` is the named tuple +```scala +(zip: Int, name: String, population: Int) +``` +The same works for enum cases expanding to case classes. + ### Restrictions The following restrictions apply to named tuple elements: @@ -130,7 +148,29 @@ SimpleExpr ::= ... | '(' NamedExprInParens {‘,’ NamedExprInParens} ')' NamedExprInParens ::= id '=' ExprInParens -SimplePattern ::= ... - | '(' NamedPattern {‘,’ NamedPattern} ')' +Patterns ::= Pattern {‘,’ Pattern} + | NamedPattern {‘,’ NamedPattern} NamedPattern ::= id '=' Pattern ``` + +### Named Pattern Matching + +We allow named patterns not just for named tuples but also for case classes. +For instance: +```scala +city match + case c @ City(name = "London") => println(p.population) + case City(name = n, zip = 1026, population = pop) => println(pop) +``` + +Named constructor patterns are analogous to named tuple patterns. In both cases + + - either all fields are named or none is, + - every name must match the name some field of the selector, + - names can come in any order, + - not all fields of the selector need to be matched. + +This revives SIP 43, with a much simpler desugaring than originally proposed. +Named patterns are compatible with extensible pattern matching simply because +`unapply` results can be named tuples. + diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index 58e342e74864..f6f3087f79d7 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -175,10 +175,12 @@ object NamedTuple: case true => NamedTuple[Names[X], Tuple.Zip[DropNames[X], DropNames[Y]]] + type FieldsOf[T] <: AnyNamedTuple + end NamedTuple -@experimental /** Separate from NamedTuple object so that we can match on the opaque type NamedTuple. */ +@experimental object NamedTupleDecomposition: import NamedTuple.* diff --git a/tests/neg/fieldsOf.scala b/tests/neg/fieldsOf.scala new file mode 100644 index 000000000000..2c88b18f977d --- /dev/null +++ b/tests/neg/fieldsOf.scala @@ -0,0 +1,13 @@ +import NamedTuple.FieldsOf + +case class Person(name: String, age: Int) +class Anon(name: String, age: Int) +def foo[T](): FieldsOf[T] = ??? + +def test = + var x: FieldsOf[Person] = ??? + x = foo[Person]() // ok + x = foo[Anon]() // error + x = foo() // error + + diff --git a/tests/pos/fieldsOf.scala b/tests/pos/fieldsOf.scala new file mode 100644 index 000000000000..3ce14f36cf28 --- /dev/null +++ b/tests/pos/fieldsOf.scala @@ -0,0 +1,18 @@ +import NamedTuple.FieldsOf + +case class Person(name: String, age: Int) + +type PF = FieldsOf[Person] + +def foo[T]: FieldsOf[T] = ??? + +class Anon(name: String, age: Int) + +def test = + var x: FieldsOf[Person] = ??? + val y: (name: String, age: Int) = x + x = y + x = foo[Person] + //x = foo[Anon] // error + + diff --git a/tests/run/fieldsOf.check b/tests/run/fieldsOf.check new file mode 100644 index 000000000000..beb79c056527 --- /dev/null +++ b/tests/run/fieldsOf.check @@ -0,0 +1,17 @@ +-- [E007] Type Mismatch Error: ../neg/fieldsOf.scala:10:15 --------------------- +10 | x = foo[Anon]() // error + | ^^^^^^^^^^^ + | Found: NamedTuple.FieldsOf[Anon] + | Required: (name : String, age : Int) + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: ../neg/fieldsOf.scala:11:9 ---------------------- +11 | x = foo() // error + | ^^^^^ + | Found: NamedTuple.FieldsOf[T] + | Required: (name : String, age : Int) + | + | where: T is a type variable + | + | longer explanation available when compiling with `-explain` +2 errors found From 4baa5094a943236d001c6fbad6abb208f3bbb2d2 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 14 Jan 2024 11:08:49 +0100 Subject: [PATCH 24/68] Describe and add tests for source incompabilities --- .../reference/experimental/named-tuples.md | 21 +++++++++++++++++++ tests/pos/namedtuple-src-incompat.scala | 17 +++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 tests/pos/namedtuple-src-incompat.scala diff --git a/docs/_docs/reference/experimental/named-tuples.md b/docs/_docs/reference/experimental/named-tuples.md index 7dd7049d1126..ea0996435213 100644 --- a/docs/_docs/reference/experimental/named-tuples.md +++ b/docs/_docs/reference/experimental/named-tuples.md @@ -174,3 +174,24 @@ This revives SIP 43, with a much simpler desugaring than originally proposed. Named patterns are compatible with extensible pattern matching simply because `unapply` results can be named tuples. +### Source Incompatibilities + +There are some source incompatibilities involving named tuples of length one. +First, what was previously classified as an assignment could now be interpreted as a named tuple. Example: +```scala +var age: Int +(age = 1) +``` +This was an assignment in parentheses before, and is a named tuple of arity one now. It is however not idiomatic Scala code, since assignments are not usually enclosed in parentheses. + +Second, what was a named argument to an infix operator can now be interpreted as a named tuple. +```scala +class C: + infix def f(age: Int) +val c: C +``` +then +```scala +c f (age = 1) +``` +will now construct a tuple as second operand instead of passing a named parameter. diff --git a/tests/pos/namedtuple-src-incompat.scala b/tests/pos/namedtuple-src-incompat.scala new file mode 100644 index 000000000000..57451a4321b7 --- /dev/null +++ b/tests/pos/namedtuple-src-incompat.scala @@ -0,0 +1,17 @@ +import language.experimental.namedTuples +var age = 22 +val x = (age = 1) +val _: (age: Int) = x +val x2 = {age = 1} +val _: Unit = x2 + +class C: + infix def id[T](age: T): T = age + +def test = + val c: C = ??? + val y = c id (age = 1) + val _: (age: Int) = y + val y2 = c.id(age = 1) + val _: Int = y2 + From 702dcd5e4267046268439201a7382c0ffa40669c Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 14 Jan 2024 17:21:51 +0100 Subject: [PATCH 25/68] Rename NamedTuple.FieldsOf --> NamedTuple.From --- compiler/src/dotty/tools/dotc/core/Definitions.scala | 8 ++++---- compiler/src/dotty/tools/dotc/core/StdNames.scala | 2 +- compiler/src/dotty/tools/dotc/core/TypeEval.scala | 2 +- docs/_docs/reference/experimental/named-tuples.md | 8 ++++---- library/src/scala/NamedTuple.scala | 2 +- tests/neg/fieldsOf.scala | 6 ++---- tests/pos/fieldsOf.scala | 8 +++----- 7 files changed, 16 insertions(+), 20 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 4b91cff4dda2..15880207b3c8 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1318,8 +1318,8 @@ class Definitions { final def isCompiletime_S(sym: Symbol)(using Context): Boolean = sym.name == tpnme.S && sym.owner == CompiletimeOpsIntModuleClass - final def isNamedTuple_FieldsOf(sym: Symbol)(using Context): Boolean = - sym.name == tpnme.FieldsOf && sym.owner == NamedTupleModule.moduleClass + final def isNamedTuple_From(sym: Symbol)(using Context): Boolean = + sym.name == tpnme.From && sym.owner == NamedTupleModule.moduleClass private val compiletimePackageAnyTypes: Set[Name] = Set( tpnme.Equals, tpnme.NotEquals, tpnme.IsConst, tpnme.ToString @@ -1349,7 +1349,7 @@ class Definitions { tpnme.Plus, tpnme.Length, tpnme.Substring, tpnme.Matches, tpnme.CharAt ) private val compiletimePackageOpTypes: Set[Name] = - Set(tpnme.S, tpnme.FieldsOf) + Set(tpnme.S, tpnme.From) ++ compiletimePackageAnyTypes ++ compiletimePackageIntTypes ++ compiletimePackageLongTypes @@ -1362,7 +1362,7 @@ class Definitions { compiletimePackageOpTypes.contains(sym.name) && ( isCompiletime_S(sym) - || isNamedTuple_FieldsOf(sym) + || isNamedTuple_From(sym) || sym.owner == CompiletimeOpsAnyModuleClass && compiletimePackageAnyTypes.contains(sym.name) || sym.owner == CompiletimeOpsIntModuleClass && compiletimePackageIntTypes.contains(sym.name) || sym.owner == CompiletimeOpsLongModuleClass && compiletimePackageLongTypes.contains(sym.name) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 7ab6750f7a0b..38240d03864c 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -361,8 +361,8 @@ object StdNames { val Eql: N = "Eql" val EnumValue: N = "EnumValue" val ExistentialTypeTree: N = "ExistentialTypeTree" - val FieldsOf: N = "FieldsOf" val Flag : N = "Flag" + val From: N = "From" val Ident: N = "Ident" val Import: N = "Import" val Literal: N = "Literal" diff --git a/compiler/src/dotty/tools/dotc/core/TypeEval.scala b/compiler/src/dotty/tools/dotc/core/TypeEval.scala index 643b83882648..b16a89a1aeb4 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeEval.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeEval.scala @@ -146,7 +146,7 @@ object TypeEval: val constantType = if defn.isCompiletime_S(sym) then constantFold1(natValue, _ + 1) - else if defn.isNamedTuple_FieldsOf(sym) then + else if defn.isNamedTuple_From(sym) then fieldsOf else if owner == defn.CompiletimeOpsAnyModuleClass then name match case tpnme.Equals => constantFold2(constValue, _ == _) diff --git a/docs/_docs/reference/experimental/named-tuples.md b/docs/_docs/reference/experimental/named-tuples.md index ea0996435213..6ee8bc9bcdec 100644 --- a/docs/_docs/reference/experimental/named-tuples.md +++ b/docs/_docs/reference/experimental/named-tuples.md @@ -100,19 +100,19 @@ The translation of named tuples to instances of `NamedTuple` is fixed by the spe - All tuple operations also work with named tuples "out of the box". - Macro libraries can rely on this expansion. -### The FieldsOf Type +### The NamedTuple.From Type The `NamedTuple` object contains a type definition ```scala - type FieldsOf[T] <: AnyNamedTuple + type From[T] <: AnyNamedTuple ``` -`FieldsOf` is treated specially by the compiler. When `FieldsOf` is applied to +`From` is treated specially by the compiler. When `NamedTuple.From` is applied to an argument type that is an instance of a case class, the type expands to the named tuple consisting of all the fields of that case class. Here, fields means: elements of the first parameter section. For instance, assuming ```scala case class City(zip: Int, name: String, population: Int) ``` -then `FieldsOf[City]` is the named tuple +then `NamedTuple.From[City]` is the named tuple ```scala (zip: Int, name: String, population: Int) ``` diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index f6f3087f79d7..b06bc599f9fd 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -175,7 +175,7 @@ object NamedTuple: case true => NamedTuple[Names[X], Tuple.Zip[DropNames[X], DropNames[Y]]] - type FieldsOf[T] <: AnyNamedTuple + type From[T] <: AnyNamedTuple end NamedTuple diff --git a/tests/neg/fieldsOf.scala b/tests/neg/fieldsOf.scala index 2c88b18f977d..d3539070b556 100644 --- a/tests/neg/fieldsOf.scala +++ b/tests/neg/fieldsOf.scala @@ -1,11 +1,9 @@ -import NamedTuple.FieldsOf - case class Person(name: String, age: Int) class Anon(name: String, age: Int) -def foo[T](): FieldsOf[T] = ??? +def foo[T](): NamedTuple.From[T] = ??? def test = - var x: FieldsOf[Person] = ??? + var x: NamedTuple.From[Person] = ??? x = foo[Person]() // ok x = foo[Anon]() // error x = foo() // error diff --git a/tests/pos/fieldsOf.scala b/tests/pos/fieldsOf.scala index 3ce14f36cf28..08f20a1f7e8e 100644 --- a/tests/pos/fieldsOf.scala +++ b/tests/pos/fieldsOf.scala @@ -1,15 +1,13 @@ -import NamedTuple.FieldsOf - case class Person(name: String, age: Int) -type PF = FieldsOf[Person] +type PF = NamedTuple.From[Person] -def foo[T]: FieldsOf[T] = ??? +def foo[T]: NamedTuple.From[T] = ??? class Anon(name: String, age: Int) def test = - var x: FieldsOf[Person] = ??? + var x: NamedTuple.From[Person] = ??? val y: (name: String, age: Int) = x x = y x = foo[Person] From 1e31d16dec1ee6d17c099336eed814562498e0bb Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 5 Feb 2024 18:16:09 +0100 Subject: [PATCH 26/68] Implement Fields as a Selectable type member Subclasses of Selectable can instantiate Fields to a named tuple type that provides possible selection names and their types on instances of the type. See: https://contributors.scala-lang.org/t/expanding-changing-selectable-based-on-upcoming-named-tuples-feature/6395/5 --- compiler/src/dotty/tools/dotc/core/StdNames.scala | 1 + compiler/src/dotty/tools/dotc/typer/Typer.scala | 13 +++++++++++++ library/src/scala/Selectable.scala | 3 ++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 38240d03864c..dd8e23a0030f 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -362,6 +362,7 @@ object StdNames { val EnumValue: N = "EnumValue" val ExistentialTypeTree: N = "ExistentialTypeTree" val Flag : N = "Flag" + val Fields: N = "Fields" val From: N = "From" val Ident: N = "Ident" val Import: N = "Import" diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 038693d3d7e1..58f08768d9bf 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -798,6 +798,19 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer else typedDynamicSelect(tree2, Nil, pt) else + if qual.tpe.derivesFrom(defn.SelectableClass) + && selName.isTermName && !isDynamicExpansion(tree) + && !pt.isInstanceOf[FunOrPolyProto] && pt != LhsProto + then + val fieldsType = qual.tpe.select(tpnme.Fields).dealias.simplified + val fields = fieldsType.namedTupleElementTypes + typr.println(i"try dyn select $qual, $selName, $fields") + fields.find(_._1 == selName) match + case Some((fieldName, fieldType)) => + val tree2 = cpy.Select(tree0)(untpd.TypedSplice(qual), selName) + val sel = typedDynamicSelect(tree2, Nil, pt) + return sel.cast(fieldType) + case _ => assignType(tree, rawType match case rawType: NamedType => diff --git a/library/src/scala/Selectable.scala b/library/src/scala/Selectable.scala index 74004a350679..93c799dd124b 100644 --- a/library/src/scala/Selectable.scala +++ b/library/src/scala/Selectable.scala @@ -22,7 +22,8 @@ package scala * In this case the call will synthesize `Class` arguments for the erasure of * all formal parameter types of the method in the structural type. */ -trait Selectable extends Any +trait Selectable extends Any: + type Fields // TODO: add <: NamedTyple.AnyNamedTuple when NamedTuple is no longer experimental object Selectable: /* Scala 2 compat + allowing for cross-compilation: From 22e6c89282ad64d458a467f115a1a77ae5194854 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 5 Feb 2024 18:52:15 +0100 Subject: [PATCH 27/68] Refactor typedSelect I usually try to avoid explicit returns, but here they do make the code easier to read. --- .../src/dotty/tools/dotc/typer/Typer.scala | 176 ++++++++++-------- 1 file changed, 99 insertions(+), 77 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 58f08768d9bf..52908a4cf9bc 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -735,88 +735,110 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer checkLegalValue(select, pt) ConstFold(select) + // If regular selection is typeable, we are done if checkedType.exists then - finish(tree, qual, checkedType) - else if selName == nme.apply && qual.tpe.widen.isInstanceOf[MethodType] then - // Simplify `m.apply(...)` to `m(...)` - qual - else if couldInstantiateTypeVar(qual.tpe.widen) then + return finish(tree, qual, checkedType) + + // Otherwise, simplify `m.apply(...)` to `m(...)` + if selName == nme.apply && qual.tpe.widen.isInstanceOf[MethodType] then + return qual + + // Otherwise, if there's a simply visible type variable in the result, try again + // with a more defined qualifier type. There's a second trial where we try to instantiate + // all type variables in `qual.tpe.widen`, but that is done only after we search for + // extension methods or conversions. + if couldInstantiateTypeVar(qual.tpe.widen) then // there's a simply visible type variable in the result; try again with a more defined qualifier type // There's a second trial where we try to instantiate all type variables in `qual.tpe.widen`, // but that is done only after we search for extension methods or conversions. - typedSelect(tree, pt, qual) - else - val namedTupleElems = qual.tpe.widen.namedTupleElementTypes - val nameIdx = namedTupleElems.indexWhere(_._1 == selName) - if nameIdx >= 0 && Feature.enabled(Feature.namedTuples) then - typed( - untpd.Apply( - untpd.Select(untpd.TypedSplice(qual), nme.apply), - untpd.Literal(Constant(nameIdx))), - pt) - else if qual.tpe.isSmallGenericTuple then - val elems = qual.tpe.widenTermRefExpr.tupleElementTypes.getOrElse(Nil) - typedSelect(tree, pt, qual.cast(defn.tupleType(elems))) + return typedSelect(tree, pt, qual) + + // Otherwise, try to expand a named tuple selection + val namedTupleElems = qual.tpe.widen.namedTupleElementTypes + val nameIdx = namedTupleElems.indexWhere(_._1 == selName) + if nameIdx >= 0 && Feature.enabled(Feature.namedTuples) then + return typed( + untpd.Apply( + untpd.Select(untpd.TypedSplice(qual), nme.apply), + untpd.Literal(Constant(nameIdx))), + pt) + + // Otherwise, map combinations of A *: B *: .... EmptyTuple with nesting levels <= 22 + // to the Tuple class of the right arity and select from that one + if qual.tpe.isSmallGenericTuple then + val elems = qual.tpe.widenTermRefExpr.tupleElementTypes.getOrElse(Nil) + return typedSelect(tree, pt, qual.cast(defn.tupleType(elems))) + + // Otherwise try an extension or conversion + if selName.isTermName then + val tree1 = tryExtensionOrConversion( + tree, pt, IgnoredProto(pt), qual, ctx.typerState.ownedVars, this, inSelect = true) + if !tree1.isEmpty then + return tree1 + + // Otherwise, try a GADT approximation if we're trying to select a member + // Member lookup cannot take GADTs into account b/c of cache, so we + // approximate types based on GADT constraints instead. For an example, + // see MemberHealing in gadt-approximation-interaction.scala. + if ctx.gadt.isNarrowing then + val wtp = qual.tpe.widen + gadts.println(i"Trying to heal member selection by GADT-approximating $wtp") + val gadtApprox = Inferencing.approximateGADT(wtp) + gadts.println(i"GADT-approximated $wtp ~~ $gadtApprox") + val qual1 = qual.cast(gadtApprox) + val tree1 = cpy.Select(tree0)(qual1, selName) + val checkedType1 = accessibleType(selectionType(tree1, qual1), superAccess = false) + if checkedType1.exists then + gadts.println(i"Member selection healed by GADT approximation") + return finish(tree1, qual1, checkedType1) + + if qual1.tpe.isSmallGenericTuple then + gadts.println(i"Tuple member selection healed by GADT approximation") + return typedSelect(tree, pt, qual1) + + val tree2 = tryExtensionOrConversion(tree1, pt, IgnoredProto(pt), qual1, ctx.typerState.ownedVars, this, inSelect = true) + if !tree2.isEmpty then + return tree2 + + // Otherwise, if there are uninstantiated type variables in the qualifier type, + // instantiate them and try again + if canDefineFurther(qual.tpe.widen) then + return typedSelect(tree, pt, qual) + + def dynamicSelect = + val tree2 = cpy.Select(tree0)(untpd.TypedSplice(qual), selName) + if pt.isInstanceOf[FunOrPolyProto] || pt == LhsProto then + assignType(tree2, TryDynamicCallType) else - val tree1 = - if selName.isTypeName then EmptyTree - else tryExtensionOrConversion( - tree, pt, IgnoredProto(pt), qual, ctx.typerState.ownedVars, this, inSelect = true) - .orElse { - if ctx.gadt.isNarrowing then - // try GADT approximation if we're trying to select a member - // Member lookup cannot take GADTs into account b/c of cache, so we - // approximate types based on GADT constraints instead. For an example, - // see MemberHealing in gadt-approximation-interaction.scala. - val wtp = qual.tpe.widen - gadts.println(i"Trying to heal member selection by GADT-approximating $wtp") - val gadtApprox = Inferencing.approximateGADT(wtp) - gadts.println(i"GADT-approximated $wtp ~~ $gadtApprox") - val qual1 = qual.cast(gadtApprox) - val tree1 = cpy.Select(tree0)(qual1, selName) - val checkedType1 = accessibleType(selectionType(tree1, qual1), superAccess = false) - if checkedType1.exists then - gadts.println(i"Member selection healed by GADT approximation") - finish(tree1, qual1, checkedType1) - else if qual1.tpe.isSmallGenericTuple then - gadts.println(i"Tuple member selection healed by GADT approximation") - typedSelect(tree, pt, qual1) - else - tryExtensionOrConversion(tree1, pt, IgnoredProto(pt), qual1, ctx.typerState.ownedVars, this, inSelect = true) - else EmptyTree - } - if !tree1.isEmpty then - tree1 - else if canDefineFurther(qual.tpe.widen) then - typedSelect(tree, pt, qual) - else if qual.tpe.derivesFrom(defn.DynamicClass) - && selName.isTermName && !isDynamicExpansion(tree) - then - val tree2 = cpy.Select(tree0)(untpd.TypedSplice(qual), selName) - if pt.isInstanceOf[FunOrPolyProto] || pt == LhsProto then - assignType(tree2, TryDynamicCallType) - else - typedDynamicSelect(tree2, Nil, pt) - else - if qual.tpe.derivesFrom(defn.SelectableClass) - && selName.isTermName && !isDynamicExpansion(tree) - && !pt.isInstanceOf[FunOrPolyProto] && pt != LhsProto - then - val fieldsType = qual.tpe.select(tpnme.Fields).dealias.simplified - val fields = fieldsType.namedTupleElementTypes - typr.println(i"try dyn select $qual, $selName, $fields") - fields.find(_._1 == selName) match - case Some((fieldName, fieldType)) => - val tree2 = cpy.Select(tree0)(untpd.TypedSplice(qual), selName) - val sel = typedDynamicSelect(tree2, Nil, pt) - return sel.cast(fieldType) - case _ => - assignType(tree, - rawType match - case rawType: NamedType => - inaccessibleErrorType(rawType, superAccess, tree.srcPos) - case _ => - notAMemberErrorType(tree, qual, pt)) + typedDynamicSelect(tree2, Nil, pt) + + // Otherwise, if the qualifier derives from class Dynamic, expand to a + // dynamic dispatch using selectDynamic or applyDynamic + if qual.tpe.derivesFrom(defn.DynamicClass) && selName.isTermName && !isDynamicExpansion(tree) then + return dynamicSelect + + // Otherwise, if the qualifier derives from class Selectable, + // and the selector name matches one of the element of the `Fields` type member, + // and the selector is neither applied nor assigned to, + // expand to a typed dynamic dispatch using selectDynamic wrapped in a cast + if qual.tpe.derivesFrom(defn.SelectableClass) && !isDynamicExpansion(tree) + && !pt.isInstanceOf[FunOrPolyProto] && pt != LhsProto + then + val fieldsType = qual.tpe.select(tpnme.Fields).dealias.simplified + val fields = fieldsType.namedTupleElementTypes + typr.println(i"try dyn select $qual, $selName, $fields") + fields.find(_._1 == selName) match + case Some((_, fieldType)) => + return dynamicSelect.cast(fieldType) + case _ => + + // Otherwise, report an error + assignType(tree, + rawType match + case rawType: NamedType => + inaccessibleErrorType(rawType, superAccess, tree.srcPos) + case _ => + notAMemberErrorType(tree, qual, pt)) end typedSelect def typedSelect(tree: untpd.Select, pt: Type)(using Context): Tree = { From d0888f61e830f517fb81ddddef68eb4aca088aca Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 5 Feb 2024 19:17:54 +0100 Subject: [PATCH 28/68] Add section on computable field names to reference doc page --- .../reference/experimental/named-tuples.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/_docs/reference/experimental/named-tuples.md b/docs/_docs/reference/experimental/named-tuples.md index 6ee8bc9bcdec..95bcfb1d10fe 100644 --- a/docs/_docs/reference/experimental/named-tuples.md +++ b/docs/_docs/reference/experimental/named-tuples.md @@ -195,3 +195,44 @@ then c f (age = 1) ``` will now construct a tuple as second operand instead of passing a named parameter. + +### Computed Field Names + +The `Selectable` trait now has a `Fields` type member that can be instantiated +to a named tuple. + +```scala +trait Selectable: + type Fields <: NamedTuple.AnyNamedTuple +``` + +If `Fields` is instantiated in a subclass of `Selectable` to some named tuple type, +then the available fields and their types will be defined by that type. Assume `n: T` +is an element of the `Fields` type in some class `C` that implements `Selectable`, +that `c: C`, and that `n` is not otherwise legal as a name of a selection on `c`. +Then `c.n` is a legal selection, which expands to `c.selectDynamic("n").asInstanceOf[T]`. + +It is the task of the implementation of `selectDynamic` in `C` to ensure that its +computed result conforms to the predicted type `T` + +As an example, assume we have a query type `Q[T]` defined as follows: + +```scala +trait Q[T] extends Selectable: + type Fields = NamedTuple.Map[NamedTuple.From[T], Q] + def selectDynamic(fieldName: String) = ... +``` + +Assume in the user domain: +```scala +case class City(zipCode: Int, name: String, population: Int) +val city: Q[City] +``` +Then +```scala +city.zipCode +``` +has type `Q[Int]` and it expands to +```scala +city.selectDynamic("zipCode").asInstanceOf[Q[Int]] +``` From 69964b0f02c3f86c98f3a0c1ed3ba180499b4af9 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 6 Feb 2024 16:35:49 +0100 Subject: [PATCH 29/68] Fix fields as a selectable type member The selectDynamic call could already have influenced type variables in the expected type before we wrap it in a cast. Need to pass in the right expected type to the typedDynamicSelect. --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 52908a4cf9bc..e771df49af94 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -805,7 +805,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer if canDefineFurther(qual.tpe.widen) then return typedSelect(tree, pt, qual) - def dynamicSelect = + def dynamicSelect(pt: Type) = val tree2 = cpy.Select(tree0)(untpd.TypedSplice(qual), selName) if pt.isInstanceOf[FunOrPolyProto] || pt == LhsProto then assignType(tree2, TryDynamicCallType) @@ -815,7 +815,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // Otherwise, if the qualifier derives from class Dynamic, expand to a // dynamic dispatch using selectDynamic or applyDynamic if qual.tpe.derivesFrom(defn.DynamicClass) && selName.isTermName && !isDynamicExpansion(tree) then - return dynamicSelect + return dynamicSelect(pt) // Otherwise, if the qualifier derives from class Selectable, // and the selector name matches one of the element of the `Fields` type member, @@ -829,7 +829,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer typr.println(i"try dyn select $qual, $selName, $fields") fields.find(_._1 == selName) match case Some((_, fieldType)) => - return dynamicSelect.cast(fieldType) + return dynamicSelect(fieldType).ensureConforms(fieldType) case _ => // Otherwise, report an error From a3409e0b884b3ae9ed7209c225f6de34a436a3af Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 6 Feb 2024 16:36:59 +0100 Subject: [PATCH 30/68] Make NamedTuple.From work for named tuple arguments NamedTyple.From should be the identity for named tuple arguments --- compiler/src/dotty/tools/dotc/core/TypeEval.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeEval.scala b/compiler/src/dotty/tools/dotc/core/TypeEval.scala index b16a89a1aeb4..af4f1e0153dd 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeEval.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeEval.scala @@ -109,8 +109,9 @@ object TypeEval: Some: defn.NamedTupleTypeRef.appliedTo: nestedPairs(fieldLabels) :: nestedPairs(fieldTypes) :: Nil - else - None + else arg.widenDealias match + case arg @ defn.NamedTuple(_, _) => Some(arg) + case _ => None def constantFold1[T](extractor: Type => Option[T], op: T => Any): Option[Type] = expectArgsNum(1) From 111674c3f209a988efff4782452cc4ea16378ddd Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 7 Feb 2024 14:07:44 +0100 Subject: [PATCH 31/68] Fix NamedArg term/type classification --- compiler/src/dotty/tools/dotc/ast/Trees.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala index 41899ed661f5..f45415b9ce0b 100644 --- a/compiler/src/dotty/tools/dotc/ast/Trees.scala +++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala @@ -555,6 +555,8 @@ object Trees { case class NamedArg[+T <: Untyped] private[ast] (name: Name, arg: Tree[T])(implicit @constructorOnly src: SourceFile) extends Tree[T] { type ThisTree[+T <: Untyped] = NamedArg[T] + override def isTerm = arg.isTerm + override def isType = arg.isType } /** name = arg, outside a parameter list */ From 1fd5962f2c1bb2a1eb6c8fb5e0da0156a780f8dd Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 10 Feb 2024 12:47:43 +0100 Subject: [PATCH 32/68] Rename inMode to withModeBits inMode is useful, but its name is can easily be misinterpreted. Hopefully, withModeBits is better. Fixes rebase breakage where inMode was made private, but needs to be public since it is now used in Desugar. --- compiler/src/dotty/tools/dotc/ast/Desugar.scala | 2 +- compiler/src/dotty/tools/dotc/core/Contexts.scala | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 39a30a185c72..9be2f9ea64a4 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1493,7 +1493,7 @@ object desugar { if names.isEmpty || ctx.mode.is(Mode.Pattern) then tup else - def namesTuple = inMode(ctx.mode &~ Mode.Pattern | Mode.Type): + def namesTuple = withModeBits(ctx.mode &~ Mode.Pattern | Mode.Type): tuple(Tuple( names.map: name => SingletonTypeTree(Literal(Constant(name.toString))).withSpan(tree.span)), diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 29cb83000fde..d0c30a665289 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -95,14 +95,14 @@ object Contexts { inline def atPhaseNoEarlier[T](limit: Phase)(inline op: Context ?=> T)(using Context): T = op(using if !limit.exists || limit <= ctx.phase then ctx else ctx.withPhase(limit)) - inline private def inMode[T](mode: Mode)(inline op: Context ?=> T)(using ctx: Context): T = + inline def withModeBits[T](mode: Mode)(inline op: Context ?=> T)(using ctx: Context): T = op(using if mode != ctx.mode then ctx.fresh.setMode(mode) else ctx) inline def withMode[T](mode: Mode)(inline op: Context ?=> T)(using ctx: Context): T = - inMode(ctx.mode | mode)(op) + withModeBits(ctx.mode | mode)(op) inline def withoutMode[T](mode: Mode)(inline op: Context ?=> T)(using ctx: Context): T = - inMode(ctx.mode &~ mode)(op) + withModeBits(ctx.mode &~ mode)(op) /** A context is passed basically everywhere in dotc. * This is convenient but carries the risk of captured contexts in From c0b792f659f07913206df5cd4d3188df4d31f61d Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 10 Feb 2024 13:30:19 +0100 Subject: [PATCH 33/68] Revert NamedArg term/type classification Strip NamedArgs instead when making a term/type classification of tuples. The previous classification of NamedArgs as terms or types broke the completion test suite. It seems that completion relies on NamedArgs being neither types nor terms. And in a sense, that's correct. They are not. --- compiler/src/dotty/tools/dotc/ast/TreeInfo.scala | 4 ++++ compiler/src/dotty/tools/dotc/ast/Trees.scala | 2 -- compiler/src/dotty/tools/dotc/ast/untpd.scala | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index fbf3cfe163b2..6ad3c6a41e98 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -108,6 +108,10 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] => case _ => tree + def stripNamedArg(tree: Tree) = tree match + case NamedArg(_, arg) => arg + case _ => tree + /** The number of arguments in an application */ def numArgs(tree: Tree): Int = unsplice(tree) match { case Apply(fn, args) => numArgs(fn) + args.length diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala index f45415b9ce0b..41899ed661f5 100644 --- a/compiler/src/dotty/tools/dotc/ast/Trees.scala +++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala @@ -555,8 +555,6 @@ object Trees { case class NamedArg[+T <: Untyped] private[ast] (name: Name, arg: Tree[T])(implicit @constructorOnly src: SourceFile) extends Tree[T] { type ThisTree[+T <: Untyped] = NamedArg[T] - override def isTerm = arg.isTerm - override def isType = arg.isType } /** name = arg, outside a parameter list */ diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index fa13bd6610ba..0dfe52c421d9 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -107,7 +107,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { def forwardTo: Tree = t } case class Tuple(trees: List[Tree])(implicit @constructorOnly src: SourceFile) extends Tree { - override def isTerm: Boolean = trees.isEmpty || trees.head.isTerm + override def isTerm: Boolean = trees.isEmpty || stripNamedArg(trees.head).isTerm override def isType: Boolean = !isTerm } case class Throw(expr: Tree)(implicit @constructorOnly src: SourceFile) extends TermTree From 611861b6015ac32ebb94c87e0193dcdec0c7f3c8 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 10 Feb 2024 13:43:45 +0100 Subject: [PATCH 34/68] Update MimaFilters --- project/MiMaFilters.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 5ccb70ad6fdf..064bb9cc2260 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -10,6 +10,14 @@ object MiMaFilters { Build.mimaPreviousDottyVersion -> Seq( ProblemFilters.exclude[DirectMissingMethodProblem]("scala.annotation.experimental.this"), ProblemFilters.exclude[FinalClassProblem]("scala.annotation.experimental"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.Tuple.fromArray"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.Tuple.fromIArray"), + ProblemFilters.exclude[MissingFieldProblem]("scala.Tuple.helpers"), + ProblemFilters.exclude[MissingClassProblem]("scala.Tuple$helpers$"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.fromArray"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.fromIArray"), + ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.namedTuples"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$namedTuples$"), ), // Additions since last LTS From 640da16e7f337084f6b8cb7a95b1481b6fbc85e0 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 16 Feb 2024 18:34:05 +0100 Subject: [PATCH 35/68] Fix rebase breakage in Parsers.scala --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index d24dd2882ad6..e1f355f30c40 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1660,7 +1660,6 @@ object Parsers { NamedArg(name, convertToElem(tpt)).withSpan(t.span) case _ => t - var isValParamList = false if in.token == LPAREN then in.nextToken() if in.token == RPAREN then @@ -1676,7 +1675,6 @@ object Parsers { in.currentRegion.withCommasExpected: funArgType() match case Ident(name) if name != tpnme.WILDCARD && in.isColon => - isValParamList = true def funParam(start: Offset, mods: Modifiers) = atSpan(start): addErased() @@ -1714,9 +1712,9 @@ object Parsers { cpy.Function(arg)(args, sanitize(res)) case arg => arg - val args1 = args.mapConserve(sanitize) - if isValParamList || in.isArrow || isPureArrow then + + if in.isArrow || isPureArrow || erasedArgs.contains(true) then functionRest(args) else val tuple = atSpan(start): From 67c0af2bd2e38d1418ad3ff294d938b57ccb9c7d Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 16 Feb 2024 18:34:55 +0100 Subject: [PATCH 36/68] Add tyql test case It now work with just named tuples, no other extensions are needed. --- tests/run/tyql.scala | 205 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 tests/run/tyql.scala diff --git a/tests/run/tyql.scala b/tests/run/tyql.scala new file mode 100644 index 000000000000..35777e9a4c13 --- /dev/null +++ b/tests/run/tyql.scala @@ -0,0 +1,205 @@ +import language.experimental.namedTuples +import NamedTuple.{NamedTuple, AnyNamedTuple} + +/* This is a demonstrator that shows how to map regular for expressions to + * internal data that can be optimized by a query engine. It needs NamedTuples + * and type classes but no macros. It's so far very provisional and experimental, + * intended as a basis for further exploration. + */ + +/** The type of expressions in the query language */ +trait Expr[Result] extends Selectable: + + /** This type is used to support selection with any of the field names + * defined by Fields. + */ + type Fields = NamedTuple.Map[NamedTuple.From[Result], Expr] + + /** A selection of a field name defined by Fields is implemented by `selectDynamic`. + * The implementation will add a cast to the right Expr type corresponding + * to the field type. + */ + def selectDynamic(fieldName: String) = Expr.Select(this, fieldName) + + /** Member methods to implement universal equality on Expr level. */ + def == (other: Expr[?]): Expr[Boolean] = Expr.Eq(this, other) + def != (other: Expr[?]): Expr[Boolean] = Expr.Ne(this, other) + +object Expr: + + /** Sample extension methods for individual types */ + extension (x: Expr[Int]) + def > (y: Expr[Int]): Expr[Boolean] = Gt(x, y) + def > (y: Int): Expr[Boolean] = Gt(x, IntLit(y)) + extension (x: Expr[Boolean]) + def &&(y: Expr[Boolean]): Expr[Boolean] = And(x, y) + def || (y: Expr[Boolean]): Expr[Boolean] = Or(x, y) + + // Note: All field names of constructors in the query language are prefixed with `$` + // so that we don't accidentally pick a field name of a constructor class where we want + // a name in the domain model instead. + + // Some sample constructors for Exprs + case class Gt($x: Expr[Int], $y: Expr[Int]) extends Expr[Boolean] + case class Plus(x: Expr[Int], y: Expr[Int]) extends Expr[Int] + case class And($x: Expr[Boolean], $y: Expr[Boolean]) extends Expr[Boolean] + case class Or($x: Expr[Boolean], $y: Expr[Boolean]) extends Expr[Boolean] + + // So far Select is weakly typed, so `selectDynamic` is easy to implement. + // Todo: Make it strongly typed like the other cases + case class Select[A]($x: Expr[A], $name: String) extends Expr + + case class Single[S <: String, A]($x: Expr[A]) + extends Expr[NamedTuple[S *: EmptyTuple, A *: EmptyTuple]] + + case class Concat[A <: AnyNamedTuple, B <: AnyNamedTuple]($x: Expr[A], $y: Expr[B]) + extends Expr[NamedTuple.Concat[A, B]] + + case class Join[A <: AnyNamedTuple](a: A) + extends Expr[NamedTuple.Map[A, StripExpr]] + + type StripExpr[E] = E match + case Expr[b] => b + + // Also weakly typed in the arguents since these two classes model universal equality */ + case class Eq($x: Expr[?], $y: Expr[?]) extends Expr[Boolean] + case class Ne($x: Expr[?], $y: Expr[?]) extends Expr[Boolean] + + /** References are placeholders for parameters */ + private var refCount = 0 + + case class Ref[A]($name: String = "") extends Expr[A]: + val id = refCount + refCount += 1 + override def toString = s"ref$id(${$name})" + + /** Literals are type-specific, tailored to the types that the DB supports */ + case class IntLit($value: Int) extends Expr[Int] + + /** Scala values can be lifted into literals by conversions */ + given Conversion[Int, IntLit] = IntLit(_) + + /** The internal representation of a function `A => B` + * Query languages are ususally first-order, so Fun is not an Expr + */ + case class Fun[A, B](param: Ref[A], f: B) + + type Pred[A] = Fun[A, Expr[Boolean]] + + /** Explicit conversion from + * (name_1: Expr[T_1], ..., name_n: Expr[T_n]) + * to + * Expr[(name_1: T_1, ..., name_n: T_n)] + */ + extension [A <: AnyNamedTuple](x: A) def toRow: Join[A] = Join(x) + + /** Same as _.toRow, as an implicit conversion */ + given [A <: AnyNamedTuple]: Conversion[A, Expr.Join[A]] = Expr.Join(_) + +end Expr + +/** The type of database queries. So far, we have queries + * that represent whole DB tables and queries that reify + * for-expressions as data. + */ +trait Query[A] + +object Query: + import Expr.{Pred, Fun, Ref} + + case class Filter[A]($q: Query[A], $p: Pred[A]) extends Query[A] + case class Map[A, B]($q: Query[A], $f: Fun[A, Expr[B]]) extends Query[B] + case class FlatMap[A, B]($q: Query[A], $f: Fun[A, Query[B]]) extends Query[B] + + // Extension methods to support for-expression syntax for queries + extension [R](x: Query[R]) + + def withFilter(p: Ref[R] => Expr[Boolean]): Query[R] = + val ref = Ref[R]() + Filter(x, Fun(ref, p(ref))) + + def map[B](f: Ref[R] => Expr[B]): Query[B] = + val ref = Ref[R]() + Map(x, Fun(ref, f(ref))) + + def flatMap[B](f: Ref[R] => Query[B]): Query[B] = + val ref = Ref[R]() + FlatMap(x, Fun(ref, f(ref))) +end Query + +/** The type of query references to database tables */ +case class Table[R]($name: String) extends Query[R] + +// Everything below is code using the model ----------------------------- + +// Some sample types +case class City(zipCode: Int, name: String, population: Int) +type Address = (city: City, street: String, number: Int) +type Person = (name: String, age: Int, addr: Address) + +@main def Test = + + val cities = Table[City]("cities") + + val q1 = cities.map: c => + c.zipCode + val q2 = cities.withFilter: city => + city.population > 10_000 + .map: city => + city.name + + val q3 = + for + city <- cities + if city.population > 10_000 + yield city.name + + val q4 = + for + city <- cities + alt <- cities + if city.name == alt.name && city.zipCode != alt.zipCode + yield + city + + val addresses = Table[Address]("addresses") + val q5 = + for + city <- cities + addr <- addresses + if addr.street == city.name + yield + (name = city.name, num = addr.number) + + val q6 = + cities.map: city => + (name = city.name, zipCode = city.zipCode) + + def run[T](q: Query[T]): Iterator[T] = ??? + + def x1: Iterator[Int] = run(q1) + def x2: Iterator[String] = run(q2) + def x3: Iterator[String] = run(q3) + def x4: Iterator[City] = run(q4) + def x5: Iterator[(name: String, num: Int)] = run(q5) + def x6: Iterator[(name: String, zipCode: Int)] = run(q6) + + println(q1) + println(q2) + println(q3) + println(q4) + println(q5) + println(q6) + +/* The following is not needed currently + +/** A type class for types that can map to a database table */ +trait Row: + type Self + type Fields = NamedTuple.From[Self] + type FieldExprs = NamedTuple.Map[Fields, Expr] + + //def toFields(x: Self): Fields = ??? + //def fromFields(x: Fields): Self = ??? + +*/ \ No newline at end of file From 2cd5d7e0c626a90ce0508cb44290a546eb4d5527 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 17 Feb 2024 17:48:21 +0100 Subject: [PATCH 37/68] Introduce auto-conversion from named tuples to tuples --- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../dotty/tools/dotc/typer/Implicits.scala | 2 + .../src/dotty/tools/dotc/typer/Typer.scala | 4 +- .../reference/experimental/named-tuples.md | 35 +++++++-- tests/neg/named-tuples.check | 76 ++++++++----------- tests/neg/named-tuples.scala | 18 +++-- tests/run/named-tuples.scala | 7 ++ 7 files changed, 86 insertions(+), 57 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index dd8e23a0030f..62d7afa22ed2 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -623,6 +623,7 @@ object StdNames { val throws: N = "throws" val toArray: N = "toArray" val toList: N = "toList" + val toTuple: N = "toTuple" val toObjectArray : N = "toObjectArray" val toSeq: N = "toSeq" val toString_ : N = "toString" diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 9f2e0628e70e..bc19e97b85d8 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -857,6 +857,8 @@ trait Implicits: || inferView(dummyTreeOfType(from), to) (using ctx.fresh.addMode(Mode.ImplicitExploration).setExploreTyperState()).isSuccess // TODO: investigate why we can't TyperState#test here + || from.widen.isNamedTupleType && to.derivesFrom(defn.TupleClass) + && from.widen.stripNamedTuple <:< to ) /** Find an implicit conversion to apply to given tree `from` so that the diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index e771df49af94..c392c195482c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -4319,7 +4319,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case _: SelectionProto => tree // adaptations for selections are handled in typedSelect case _ if ctx.mode.is(Mode.ImplicitsEnabled) && tree.tpe.isValueType => - if pt.isRef(defn.AnyValClass, skipRefined = false) + if tree.tpe.widen.isNamedTupleType && pt.derivesFrom(defn.TupleClass) then + readapt(typed(untpd.Select(untpd.TypedSplice(tree), nme.toTuple))) + else if pt.isRef(defn.AnyValClass, skipRefined = false) || pt.isRef(defn.ObjectClass, skipRefined = false) then recover(TooUnspecific(pt)) diff --git a/docs/_docs/reference/experimental/named-tuples.md b/docs/_docs/reference/experimental/named-tuples.md index 95bcfb1d10fe..8eded366dc87 100644 --- a/docs/_docs/reference/experimental/named-tuples.md +++ b/docs/_docs/reference/experimental/named-tuples.md @@ -22,25 +22,45 @@ name for two different elements. Fields of named tuples can be selected by their name, as in the line `p.age < 18` above. -### Conformance +### Conformance and Convertibility The order of names in a named tuple matters. For instance, the type `Person` above and the type `(age: Int, name: String)` would be different, incompatible types. Values of named tuple types can also be be defined using regular tuples. For instance: ```scala -val x: Person = ("Laura", 25) +val Laura: Person = ("Laura", 25) def register(person: Person) = ... register(person = ("Silvain", 16)) register(("Silvain", 16)) ``` -This follows since a regular tuple `(T_1, ..., T_n)` is treated as a subtype of a named tuple `(N_1 = T_1, ..., N_n = T_n)` with the same element types. On the other hand, named tuples do not conform to unnamed tuples, so the following is an error: +This follows since a regular tuple `(T_1, ..., T_n)` is treated as a subtype of a named tuple `(N_1 = T_1, ..., N_n = T_n)` with the same element types. + +In the other direction, one can convert a named tuple to an unnamed tuple with the `toTuple` method. Example: +```scala +val x: (String, Int) = Bob.toTuple // ok +``` +`toTuple` is defined as an extension method in the `NamedTuple` object. +It returns the given tuple unchanged and simply "forgets" the names. + +A `.toTuple` selection is inserted implicitly by the compiler if it encounters a named tuple but the expected type is a regular tuple. So the following works as well: ```scala -val x: (String, Int) = Bob // error: type mismatch +val x: (String, Int) = Bob // works, expanded to Bob.toTuple ``` -One can convert a named tuple to an unnamed tuple with the `toTuple` method, so the following works: +The difference between subtyping in one direction and automatic `.toTuple` conversions in the other is relatively minor. The main difference is that `.toTuple` conversions don't work inside type constructors. So the following is OK: ```scala -val x: (String, Int) = Bob.toTuple // ok + val names = List("Laura", "Silvain") + val ages = List(25, 16) + val persons: List[Person] = names.zip(ages) +``` +But the following would be illegal. +```scala + val persons: List[Person] = List(Bob, Laura) + val pairs: List[(String, Int)] = persons // error +``` +We would need an explicit `_.toTuple` selection to express this: +```scala + val pairs: List[(String, Int)] = persons.map(_.toTuple) ``` Note that conformance rules for named tuples are analogous to the rules for named parameters. One can assign parameters by position to a named parameter list. ```scala @@ -54,8 +74,7 @@ But one cannot use a name to pass an argument to an unnamed parameter: f(2) // OK f(param = 2) // Not OK ``` -The rules for tuples are analogous. Unnamed tuples conform to named tuple types, but the opposite does not hold. - +The rules for tuples are analogous. Unnamed tuples conform to named tuple types, but the opposite requires a conversion. ### Pattern Matching diff --git a/tests/neg/named-tuples.check b/tests/neg/named-tuples.check index d9b6d686a587..9735879fc494 100644 --- a/tests/neg/named-tuples.check +++ b/tests/neg/named-tuples.check @@ -14,20 +14,6 @@ 12 | type Illformed2 = (name: String, age: Int, name: Boolean) // error | ^^^^^^^^^^^^^ | Duplicate tuple element name --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:18:25 ------------------------------------------------------ -18 | val y: (String, Int) = person // error - | ^^^^^^ - | Found: (Test.person : (name : String, age : Int)) - | Required: (String, Int) - | - | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:19:25 ------------------------------------------------------ -19 | val _: (String, Int) = (name = "", age = 0) // error - | ^^^^^^^^^^^^^^^^^^^^ - | Found: (name : String, age : Int) - | Required: (String, Int) - | - | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:20:20 ------------------------------------------------------ 20 | val _: NameOnly = person // error | ^^^^^^ @@ -42,66 +28,70 @@ | Required: Test.Person | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:23:36 ------------------------------------------------------ -23 | val _: (age: Int, name: String) = person // error +-- [E008] Not Found Error: tests/neg/named-tuples.scala:22:9 ----------------------------------------------------------- +22 | person._1 // error + | ^^^^^^^^^ + | value _1 is not a member of (name : String, age : Int) +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:24:36 ------------------------------------------------------ +24 | val _: (age: Int, name: String) = person // error | ^^^^^^ | Found: (Test.person : (name : String, age : Int)) | Required: (age : Int, name : String) | | longer explanation available when compiling with `-explain` --- Error: tests/neg/named-tuples.scala:25:17 --------------------------------------------------------------------------- -25 | val (name = x, agee = y) = person // error +-- Error: tests/neg/named-tuples.scala:26:17 --------------------------------------------------------------------------- +26 | val (name = x, agee = y) = person // error | ^^^^^^^^ | No element named `agee` is defined in selector type (name : String, age : Int) --- Error: tests/neg/named-tuples.scala:28:10 --------------------------------------------------------------------------- -28 | case (name = n, age = a) => () // error // error +-- Error: tests/neg/named-tuples.scala:29:10 --------------------------------------------------------------------------- +29 | case (name = n, age = a) => () // error // error | ^^^^^^^^ | No element named `name` is defined in selector type (String, Int) --- Error: tests/neg/named-tuples.scala:28:20 --------------------------------------------------------------------------- -28 | case (name = n, age = a) => () // error // error +-- Error: tests/neg/named-tuples.scala:29:20 --------------------------------------------------------------------------- +29 | case (name = n, age = a) => () // error // error | ^^^^^^^ | No element named `age` is defined in selector type (String, Int) --- [E172] Type Error: tests/neg/named-tuples.scala:30:27 --------------------------------------------------------------- -30 | val pp = person ++ (1, 2) // error +-- [E172] Type Error: tests/neg/named-tuples.scala:31:27 --------------------------------------------------------------- +31 | val pp = person ++ (1, 2) // error | ^ | Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), Tuple] =:= (true : Boolean). --- [E172] Type Error: tests/neg/named-tuples.scala:33:18 --------------------------------------------------------------- -33 | person ++ (1, 2) match // error +-- [E172] Type Error: tests/neg/named-tuples.scala:34:18 --------------------------------------------------------------- +34 | person ++ (1, 2) match // error | ^ | Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), Tuple] =:= (true : Boolean). --- Error: tests/neg/named-tuples.scala:36:17 --------------------------------------------------------------------------- -36 | val bad = ("", age = 10) // error +-- Error: tests/neg/named-tuples.scala:37:17 --------------------------------------------------------------------------- +37 | val bad = ("", age = 10) // error | ^^^^^^^^ | Illegal combination of named and unnamed tuple elements --- Error: tests/neg/named-tuples.scala:39:20 --------------------------------------------------------------------------- -39 | case (name = n, age) => () // error +-- Error: tests/neg/named-tuples.scala:40:20 --------------------------------------------------------------------------- +40 | case (name = n, age) => () // error | ^^^ | Illegal combination of named and unnamed tuple elements --- Error: tests/neg/named-tuples.scala:40:16 --------------------------------------------------------------------------- -40 | case (name, age = a) => () // error +-- Error: tests/neg/named-tuples.scala:41:16 --------------------------------------------------------------------------- +41 | case (name, age = a) => () // error | ^^^^^^^ | Illegal combination of named and unnamed tuple elements --- Error: tests/neg/named-tuples.scala:43:10 --------------------------------------------------------------------------- -43 | case (age = x) => // error +-- Error: tests/neg/named-tuples.scala:44:10 --------------------------------------------------------------------------- +44 | case (age = x) => // error | ^^^^^^^ | No element named `age` is defined in selector type Tuple --- [E172] Type Error: tests/neg/named-tuples.scala:45:27 --------------------------------------------------------------- -45 | val p2 = person ++ person // error +-- [E172] Type Error: tests/neg/named-tuples.scala:46:27 --------------------------------------------------------------- +46 | val p2 = person ++ person // error | ^ |Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), (("name" : String), ("age" : String))] =:= (true : Boolean). --- [E172] Type Error: tests/neg/named-tuples.scala:46:43 --------------------------------------------------------------- -46 | val p3 = person ++ (first = 11, age = 33) // error +-- [E172] Type Error: tests/neg/named-tuples.scala:47:43 --------------------------------------------------------------- +47 | val p3 = person ++ (first = 11, age = 33) // error | ^ |Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), (("first" : String), ("age" : String))] =:= (true : Boolean). --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:48:22 ------------------------------------------------------ -48 | val p5 = person.zip(first = 11, age = 33) // error - | ^^^^^^^^^^^^^^^^^^^^ +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:49:22 ------------------------------------------------------ +49 | val p5 = person.zip((first = 11, age = 33)) // error + | ^^^^^^^^^^^^^^^^^^^^^^ | Found: (first : Int, age : Int) | Required: NamedTuple.NamedTuple[(("name" : String), ("age" : String)), Tuple] | | longer explanation available when compiling with `-explain` --- Warning: tests/neg/named-tuples.scala:25:29 ------------------------------------------------------------------------- -25 | val (name = x, agee = y) = person // error +-- Warning: tests/neg/named-tuples.scala:26:29 ------------------------------------------------------------------------- +26 | val (name = x, agee = y) = person // error | ^^^^^^ |pattern's type (String, Int) is more specialized than the right hand side expression's type (name : String, age : Int) | diff --git a/tests/neg/named-tuples.scala b/tests/neg/named-tuples.scala index 7dcf2221ec40..b2148244e1ba 100644 --- a/tests/neg/named-tuples.scala +++ b/tests/neg/named-tuples.scala @@ -15,10 +15,11 @@ import language.experimental.namedTuples val nameOnly = (name = "Louis") - val y: (String, Int) = person // error - val _: (String, Int) = (name = "", age = 0) // error + val y: (String, Int) = person // ok, conversion + val _: (String, Int) = (name = "", age = 0) // ok, conversion val _: NameOnly = person // error val _: Person = nameOnly // error + person._1 // error val _: (age: Int, name: String) = person // error @@ -45,8 +46,15 @@ import language.experimental.namedTuples val p2 = person ++ person // error val p3 = person ++ (first = 11, age = 33) // error val p4 = person.zip(person) // ok - val p5 = person.zip(first = 11, age = 33) // error - - + val p5 = person.zip((first = 11, age = 33)) // error + // Note: this one depends on the details of the conversion named -> unnamed + // we do a conversion only of the expected type is a tuple. If we used a + // regular implicit conversion, then (first = 11, age = 33) would be converted + // to (Int, Int) and that would be upcast to (name: Int, age: Int), which + // would hide an error. So we have be careful that the "downwards" conversion + // is specific and does not apply to a different "upwards" type. + // The same problem happens if we assume named <: unnamed. In that case we would first + // upcast (first: Int, age: Int) to (Int, Int), and then use the downwards + // conversion to (name: Int, age: Int). This one would be harder to guard against. diff --git a/tests/run/named-tuples.scala b/tests/run/named-tuples.scala index 29b058adab18..676c21a0e434 100644 --- a/tests/run/named-tuples.scala +++ b/tests/run/named-tuples.scala @@ -100,6 +100,13 @@ val _: CombinedInfo = bob ++ addr val addr4 = addr3.zip("Preverenges", 1028) println(addr4) + // testing conversions +object Conv: + + val p: (String, Int) = bob + def f22(x: (String, Int)) = x._1 + def f22(x: String) = x + f22(bob) From d8b7595f48898fd9cf77bf0b777217fc93b0f869 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 18 Feb 2024 12:19:42 +0100 Subject: [PATCH 38/68] Update documentation following review suggestions Co-authored-by: Dale Wijnand --- compiler/src/dotty/tools/dotc/ast/Desugar.scala | 4 +--- .../reference/experimental/named-tuples.md | 9 +++++++-- library/src/scala/NamedTuple.scala | 17 ++++++++++++++--- library/src/scala/Tuple.scala | 4 ++++ 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 9be2f9ea64a4..537822d67594 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1466,7 +1466,7 @@ object desugar { case _ => elems end checkWellFormedTupleElems - /** Translate tuple expressions of arity <= 22 + /** Translate tuple expressions * * () ==> () * (t) ==> t @@ -1509,8 +1509,6 @@ object desugar { * expected type `pt` to each other. This means: * - If `elems` are named pattern elements, rearrange them to match `pt`. * This requires all names in `elems` to be also present in `pt`. - * - If `elems` are unnamed elements, and `pt` is a named tuple, drop all - * tuple element names from `pt`. */ def adaptPatternArgs(elems: List[Tree], pt: Type)(using Context): List[Tree] = diff --git a/docs/_docs/reference/experimental/named-tuples.md b/docs/_docs/reference/experimental/named-tuples.md index 8eded366dc87..6a0baebe36e6 100644 --- a/docs/_docs/reference/experimental/named-tuples.md +++ b/docs/_docs/reference/experimental/named-tuples.md @@ -127,7 +127,8 @@ The `NamedTuple` object contains a type definition ``` `From` is treated specially by the compiler. When `NamedTuple.From` is applied to an argument type that is an instance of a case class, the type expands to the named -tuple consisting of all the fields of that case class. Here, fields means: elements of the first parameter section. For instance, assuming +tuple consisting of all the fields of that case class. +Here, _fields_ means: elements of the first parameter section. For instance, assuming ```scala case class City(zip: Int, name: String, population: Int) ``` @@ -135,7 +136,11 @@ then `NamedTuple.From[City]` is the named tuple ```scala (zip: Int, name: String, population: Int) ``` -The same works for enum cases expanding to case classes. +The same works for enum cases expanding to case classes, abstract types with case classes as upper bound, alias types expanding to case classes +and singleton types with case classes as underlying type. + +`From` is also defined on named tuples. If `NT` is a named tuple type, then `From[NT] = NT`. + ### Restrictions diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index b06bc599f9fd..a787ea85d37c 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -5,9 +5,16 @@ import compiletime.ops.boolean.* @experimental object NamedTuple: - opaque type AnyNamedTuple = Any + /** The type to which named tuples get mapped to. For instance, + * (name: String, age: Int) + * gets mapped to + * NamedTuple[("name", "age"), (String, Int)] + */ opaque type NamedTuple[N <: Tuple, +V <: Tuple] >: V <: AnyNamedTuple = V + /** A type which is a supertype of all named tuples */ + opaque type AnyNamedTuple = Any + 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) @@ -25,8 +32,8 @@ object NamedTuple: /** The number of elements in this tuple */ inline def size: Tuple.Size[V] = toTuple.size - // This intentionally works for empty named tuples as well. I think NnEmptyTuple is a dead end - // and should be reverted, justy like NonEmptyList is also appealing at first, but a bad idea + // This intentionally works for empty named tuples as well. I think NonEmptyTuple is a dead end + // and should be reverted, just like NonEmptyList is also appealing at first, but a bad idea // in the end. /** The value (without the name) at index `n` of this tuple */ @@ -175,6 +182,10 @@ object NamedTuple: case true => NamedTuple[Names[X], Tuple.Zip[DropNames[X], DropNames[Y]]] + /** 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 tyuple, + * `From[T]` is the same as `T`. + */ type From[T] <: AnyNamedTuple end NamedTuple diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index e128fa8f0e81..8bd78013210b 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -114,6 +114,10 @@ object Tuple: case S[n1] => Elem[xs, n1] /** The type of the first element of a tuple */ + // Only bounded by `<: Tuple` not `<: NonEmptyTuple` + // even though it only matches non-empty tuples. + // Avoids bounds check failures from an irreducible type + // like `Tuple.Head[Tuple.Tail[X]]` type Head[X <: Tuple] = X match case x *: _ => x From e0eb2471759e42fb88187a194825c787fb6530f9 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 24 Apr 2024 18:50:16 +0200 Subject: [PATCH 39/68] Drop TreeInfo's `dropNamedArg` --- compiler/src/dotty/tools/dotc/ast/Desugar.scala | 4 ++-- compiler/src/dotty/tools/dotc/ast/TreeInfo.scala | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 537822d67594..ff260c0efc16 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1461,7 +1461,7 @@ object desugar { mismatchOpt match case Some(misMatch) => report.error(em"Illegal combination of named and unnamed tuple elements", misMatch.srcPos) - elems.mapConserve(dropNamedArg) + elems.mapConserve(stripNamedArg) case None => elems case _ => elems end checkWellFormedTupleElems @@ -1475,7 +1475,7 @@ object desugar { def tuple(tree: Tuple, pt: Type)(using Context): Tree = var elems = checkWellFormedTupleElems(tree.trees) if ctx.mode.is(Mode.Pattern) then elems = adaptPatternArgs(elems, pt) - val elemValues = elems.mapConserve(dropNamedArg) + val elemValues = elems.mapConserve(stripNamedArg) val tup = val arity = elems.length if arity <= Definitions.MaxTupleArity then diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 6ad3c6a41e98..941e7b8f1219 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -248,10 +248,6 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] => def hasNamedArg(args: List[Any]): Boolean = args exists isNamedArg val isNamedArg: Any => Boolean = (arg: Any) => arg.isInstanceOf[Trees.NamedArg[?]] - def dropNamedArg(arg: Tree) = arg match - case NamedArg(_, arg1) => arg1 - case arg => arg - /** Is this pattern node a catch-all (wildcard or variable) pattern? */ def isDefaultCase(cdef: CaseDef): Boolean = cdef match { case CaseDef(pat, EmptyTree, _) => isWildcardArg(pat) From 4279a5828c691925b3840255d127470f5dd51eff Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 24 Apr 2024 18:50:43 +0200 Subject: [PATCH 40/68] Print wildcard types in named tuples correctly --- .../tools/dotc/printing/RefinedPrinter.scala | 2 +- tests/neg/named-tuples.check | 71 +++++++++++-------- tests/neg/named-tuples.scala | 3 +- 3 files changed, 44 insertions(+), 32 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 5e15fd2ddd15..0329f0639d87 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -207,7 +207,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { def toTextNamedTuple(elems: List[(TermName, Type)]): Text = val elemsText = atPrec(GlobalPrec): - Text(elems.map((name, tp) => toText(name) ~ " : " ~ toText(tp)), ", ") + Text(elems.map((name, tp) => toText(name) ~ " : " ~ argText(tp)), ", ") "(" ~ elemsText ~ ")" def isInfixType(tp: Type): Boolean = tp match diff --git a/tests/neg/named-tuples.check b/tests/neg/named-tuples.check index 9735879fc494..791a10f20b16 100644 --- a/tests/neg/named-tuples.check +++ b/tests/neg/named-tuples.check @@ -28,70 +28,81 @@ | Required: Test.Person | | longer explanation available when compiling with `-explain` --- [E008] Not Found Error: tests/neg/named-tuples.scala:22:9 ----------------------------------------------------------- -22 | person._1 // error +-- [E172] Type Error: tests/neg/named-tuples.scala:22:41 --------------------------------------------------------------- +22 | val _: Person = (name = "") ++ nameOnly // error + | ^ + | Cannot prove that Tuple.Disjoint[Tuple1[("name" : String)], Tuple1[("name" : String)]] =:= (true : Boolean). +-- [E008] Not Found Error: tests/neg/named-tuples.scala:23:9 ----------------------------------------------------------- +23 | person._1 // error | ^^^^^^^^^ | value _1 is not a member of (name : String, age : Int) --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:24:36 ------------------------------------------------------ -24 | val _: (age: Int, name: String) = person // error +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:25:36 ------------------------------------------------------ +25 | val _: (age: Int, name: String) = person // error | ^^^^^^ | Found: (Test.person : (name : String, age : Int)) | Required: (age : Int, name : String) | | longer explanation available when compiling with `-explain` --- Error: tests/neg/named-tuples.scala:26:17 --------------------------------------------------------------------------- -26 | val (name = x, agee = y) = person // error +-- Error: tests/neg/named-tuples.scala:27:17 --------------------------------------------------------------------------- +27 | val (name = x, agee = y) = person // error | ^^^^^^^^ | No element named `agee` is defined in selector type (name : String, age : Int) --- Error: tests/neg/named-tuples.scala:29:10 --------------------------------------------------------------------------- -29 | case (name = n, age = a) => () // error // error +-- Error: tests/neg/named-tuples.scala:30:10 --------------------------------------------------------------------------- +30 | case (name = n, age = a) => () // error // error | ^^^^^^^^ | No element named `name` is defined in selector type (String, Int) --- Error: tests/neg/named-tuples.scala:29:20 --------------------------------------------------------------------------- -29 | case (name = n, age = a) => () // error // error +-- Error: tests/neg/named-tuples.scala:30:20 --------------------------------------------------------------------------- +30 | case (name = n, age = a) => () // error // error | ^^^^^^^ | No element named `age` is defined in selector type (String, Int) --- [E172] Type Error: tests/neg/named-tuples.scala:31:27 --------------------------------------------------------------- -31 | val pp = person ++ (1, 2) // error +-- [E172] Type Error: tests/neg/named-tuples.scala:32:27 --------------------------------------------------------------- +32 | val pp = person ++ (1, 2) // error | ^ | Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), Tuple] =:= (true : Boolean). --- [E172] Type Error: tests/neg/named-tuples.scala:34:18 --------------------------------------------------------------- -34 | person ++ (1, 2) match // error +-- [E172] Type Error: tests/neg/named-tuples.scala:35:18 --------------------------------------------------------------- +35 | person ++ (1, 2) match // error | ^ | Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), Tuple] =:= (true : Boolean). --- Error: tests/neg/named-tuples.scala:37:17 --------------------------------------------------------------------------- -37 | val bad = ("", age = 10) // error +-- Error: tests/neg/named-tuples.scala:38:17 --------------------------------------------------------------------------- +38 | val bad = ("", age = 10) // error | ^^^^^^^^ | Illegal combination of named and unnamed tuple elements --- Error: tests/neg/named-tuples.scala:40:20 --------------------------------------------------------------------------- -40 | case (name = n, age) => () // error +-- Error: tests/neg/named-tuples.scala:41:20 --------------------------------------------------------------------------- +41 | case (name = n, age) => () // error | ^^^ | Illegal combination of named and unnamed tuple elements --- Error: tests/neg/named-tuples.scala:41:16 --------------------------------------------------------------------------- -41 | case (name, age = a) => () // error +-- Error: tests/neg/named-tuples.scala:42:16 --------------------------------------------------------------------------- +42 | case (name, age = a) => () // error | ^^^^^^^ | Illegal combination of named and unnamed tuple elements --- Error: tests/neg/named-tuples.scala:44:10 --------------------------------------------------------------------------- -44 | case (age = x) => // error +-- Error: tests/neg/named-tuples.scala:45:10 --------------------------------------------------------------------------- +45 | case (age = x) => // error | ^^^^^^^ | No element named `age` is defined in selector type Tuple --- [E172] Type Error: tests/neg/named-tuples.scala:46:27 --------------------------------------------------------------- -46 | val p2 = person ++ person // error +-- [E172] Type Error: tests/neg/named-tuples.scala:47:27 --------------------------------------------------------------- +47 | val p2 = person ++ person // error | ^ |Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), (("name" : String), ("age" : String))] =:= (true : Boolean). --- [E172] Type Error: tests/neg/named-tuples.scala:47:43 --------------------------------------------------------------- -47 | val p3 = person ++ (first = 11, age = 33) // error +-- [E172] Type Error: tests/neg/named-tuples.scala:48:43 --------------------------------------------------------------- +48 | val p3 = person ++ (first = 11, age = 33) // error | ^ |Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), (("first" : String), ("age" : String))] =:= (true : Boolean). --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:49:22 ------------------------------------------------------ -49 | val p5 = person.zip((first = 11, age = 33)) // error +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:50:22 ------------------------------------------------------ +50 | val p5 = person.zip((first = 11, age = 33)) // error | ^^^^^^^^^^^^^^^^^^^^^^ | Found: (first : Int, age : Int) | Required: NamedTuple.NamedTuple[(("name" : String), ("age" : String)), Tuple] | | longer explanation available when compiling with `-explain` --- Warning: tests/neg/named-tuples.scala:26:29 ------------------------------------------------------------------------- -26 | val (name = x, agee = y) = person // error +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:61:32 ------------------------------------------------------ +61 | val typo: (name: ?, age: ?) = (name = "he", ag = 1) // error + | ^^^^^^^^^^^^^^^^^^^^^ + | Found: (name : String, ag : Int) + | Required: (name : ?, age : ?) + | + | longer explanation available when compiling with `-explain` +-- Warning: tests/neg/named-tuples.scala:27:29 ------------------------------------------------------------------------- +27 | val (name = x, agee = y) = person // error | ^^^^^^ |pattern's type (String, Int) is more specialized than the right hand side expression's type (name : String, age : Int) | diff --git a/tests/neg/named-tuples.scala b/tests/neg/named-tuples.scala index b2148244e1ba..8f78f7915206 100644 --- a/tests/neg/named-tuples.scala +++ b/tests/neg/named-tuples.scala @@ -19,6 +19,7 @@ import language.experimental.namedTuples val _: (String, Int) = (name = "", age = 0) // ok, conversion val _: NameOnly = person // error val _: Person = nameOnly // error + val _: Person = (name = "") ++ nameOnly // error person._1 // error val _: (age: Int, name: String) = person // error @@ -57,4 +58,4 @@ import language.experimental.namedTuples // upcast (first: Int, age: Int) to (Int, Int), and then use the downwards // conversion to (name: Int, age: Int). This one would be harder to guard against. - + val typo: (name: ?, age: ?) = (name = "he", ag = 1) // error From c0bd1e44da49f54b6e6c7717d9dc42d54655fac2 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 24 Apr 2024 18:51:52 +0200 Subject: [PATCH 41/68] Don't abbreviate tuple bindings if right-hand-side is named We need to go through an explicit pattern match to drop the names. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 3 +- tests/pos/named-tuples.check | 10 ------ tests/run/fieldsOf.check | 17 --------- tests/run/named-patmatch.scala | 36 +++++++++++++++++++ 4 files changed, 38 insertions(+), 28 deletions(-) delete mode 100644 tests/pos/named-tuples.check delete mode 100644 tests/run/fieldsOf.check create mode 100644 tests/run/named-patmatch.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index ff260c0efc16..1801a7fada7c 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1254,8 +1254,9 @@ object desugar { pats.forall(isVarPattern) case _ => false } + val isMatchingTuple: Tree => Boolean = { - case Tuple(es) => isTuplePattern(es.length) + case Tuple(es) => isTuplePattern(es.length) && !hasNamedArg(es) case _ => false } diff --git a/tests/pos/named-tuples.check b/tests/pos/named-tuples.check deleted file mode 100644 index 24928c7dbdac..000000000000 --- a/tests/pos/named-tuples.check +++ /dev/null @@ -1,10 +0,0 @@ -(Bob,33) -33 -Bob -(Bob,33,Lausanne,1003) -33 -no match -Bob is younger than Bill -Bob is younger than Lucy -Bill is younger than Lucy -matched elements (name, Bob), (age, 33) diff --git a/tests/run/fieldsOf.check b/tests/run/fieldsOf.check deleted file mode 100644 index beb79c056527..000000000000 --- a/tests/run/fieldsOf.check +++ /dev/null @@ -1,17 +0,0 @@ --- [E007] Type Mismatch Error: ../neg/fieldsOf.scala:10:15 --------------------- -10 | x = foo[Anon]() // error - | ^^^^^^^^^^^ - | Found: NamedTuple.FieldsOf[Anon] - | Required: (name : String, age : Int) - | - | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: ../neg/fieldsOf.scala:11:9 ---------------------- -11 | x = foo() // error - | ^^^^^ - | Found: NamedTuple.FieldsOf[T] - | Required: (name : String, age : Int) - | - | where: T is a type variable - | - | longer explanation available when compiling with `-explain` -2 errors found diff --git a/tests/run/named-patmatch.scala b/tests/run/named-patmatch.scala new file mode 100644 index 000000000000..e62497e4aa8f --- /dev/null +++ b/tests/run/named-patmatch.scala @@ -0,0 +1,36 @@ +import annotation.experimental +import language.experimental.namedTuples + +@main def Test = + locally: + val (x = x, y = y) = (x = 11, y = 22) + assert(x == 11 && y == 22) + + locally: + val (x = a, y = b) = (x = 1, y = 2) + assert(a == 1 && b == 2) + + locally: + val (x = a, y = b) = (x = 1, y = 2) + assert(a == 1 && b == 2) + + locally: + val (x, y) = (x = 1, y = 2) + assert(x == 1 && y == 2) + + locally: + val (a, b) = (x = 1, y = 2) + assert(a == 1 && b == 2) + + (x = 1, y = 2) match + case (x = x, y = y) => assert(x == 1 && y == 2) + + (x = 1, y = 2) match + case (x, y) => assert(x == 1 && y == 2) + + (x = 1, y = 2) match + case (a, b) => assert(a == 1 && b == 2) + + + + From b997f3d6d846f5ddbaac6dfde689621a2eaacefd Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 24 Apr 2024 19:20:10 +0200 Subject: [PATCH 42/68] Strip named tuple from scrutinee when testing refutability of a named pattern match --- compiler/src/dotty/tools/dotc/core/TypeOps.scala | 4 ++-- compiler/src/dotty/tools/dotc/typer/Checking.scala | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index e67e60dd45ea..8461c0f091fe 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -387,8 +387,8 @@ object TypeOps: if tycon1.typeSymbol == tycon2.typeSymbol && (tycon1 =:= tycon2) => mergeRefinedOrApplied(tp1, tp2) match case tp: AppliedType if tp.isUnreducibleWild => - // fall back to or-dominators rather tahn inferring a type that would - // caue an unreducible type error later. + // fall back to or-dominators rather than inferring a type that would + // cause an unreducible type error later. approximateOr(tp1, tp2) case tp => tp case (tp1, tp2) => diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 662a4feb867a..7745c620312c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -966,10 +966,16 @@ trait Checking { false } - def check(pat: Tree, pt: Type): Boolean = + // Is scrutinee type `pt` a subtype of `pat.tpe`, after stripping named tuples + // and accounting for large generic tuples? + // Named tuples need to be stripped off, since names are dropped in patterns + def conforms(pat: Tree, pt: Type): Boolean = pt.isTupleXXLExtract(pat.tpe) // See isTupleXXLExtract, fixes TupleXXL parameter type - || pt <:< pat.tpe - || fail(pat, pt, Reason.NonConforming) + || pt.stripNamedTuple <:< pat.tpe + || (pt.widen ne pt) && conforms(pat, pt.widen) + + def check(pat: Tree, pt: Type): Boolean = + conforms(pat, pt) || fail(pat, pt, Reason.NonConforming) def recur(pat: Tree, pt: Type): Boolean = !sourceVersion.isAtLeast(`3.2`) From 2206d8883c0095f69642ba373074ad7060bcc1df Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 25 Apr 2024 00:21:21 +0200 Subject: [PATCH 43/68] Fix tuple selection in pattern matcher The pattern matcher selects tuples up to 22 using _1, _2, ... But if the scrutinee is a named tuple this only works if it is cast to a regular tuple first. --- .../dotty/tools/dotc/transform/PatternMatcher.scala | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index f22a17f7fd27..0b8507f3b6c7 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -347,13 +347,20 @@ object PatternMatcher { def tupleApp(i: Int, receiver: Tree) = // manually inlining the call to NonEmptyTuple#apply, because it's an inline method ref(defn.RuntimeTuplesModule) .select(defn.RuntimeTuples_apply) - .appliedTo(receiver, Literal(Constant(i))) + .appliedTo( + receiver.ensureConforms(defn.NonEmptyTupleTypeRef), // If scrutinee is a named tuple, cast to underlying tuple + Literal(Constant(i))) if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length) - def tupleSel(sym: Symbol) = ref(scrutinee).select(sym) + def tupleSel(sym: Symbol) = + // If scrutinee is a named tuple, cast to underlying tuple, so that we can + // continue to select with _1, _2, ... + ref(scrutinee).ensureConforms(scrutinee.info.stripNamedTuple).select(sym) val isGenericTuple = defn.isTupleClass(caseClass) && !defn.isTupleNType(tree.tpe match { case tp: OrType => tp.join case tp => tp }) // widen even hard unions, to see if it's a union of tuples - val components = if isGenericTuple then caseAccessors.indices.toList.map(tupleApp(_, ref(scrutinee))) else caseAccessors.map(tupleSel) + val components = + if isGenericTuple then caseAccessors.indices.toList.map(tupleApp(_, ref(scrutinee))) + else caseAccessors.map(tupleSel) matchArgsPlan(components, args, onSuccess) else if unappType.isRef(defn.BooleanClass) then TestPlan(GuardTest, unapp, unapp.span, onSuccess) From ca19f1a5589ca70b65afb89eda3725bc405245ac Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 25 Apr 2024 13:21:51 +0200 Subject: [PATCH 44/68] Fixes to NamedTuple --- library/src/scala/NamedTuple.scala | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index a787ea85d37c..d4e5a72eb1fc 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -46,13 +46,15 @@ object NamedTuple: inline def head: Tuple.Elem[V, 0] = apply(0) /** The tuple consisting of all elements of this tuple except the first one */ - inline def tail: Tuple.Drop[V, 1] = toTuple.drop(1) + inline def tail: NamedTuple[Tuple.Tail[N], Tuple.Tail[V]] = + toTuple.drop(1).asInstanceOf[NamedTuple[Tuple.Tail[N], Tuple.Tail[V]]] /** The last element value of this tuple */ inline def last: Tuple.Last[V] = apply(size - 1).asInstanceOf[Tuple.Last[V]] /** The tuple consisting of all elements of this tuple except the last one */ - inline def init: Tuple.Init[V] = toTuple.take(size - 1).asInstanceOf[Tuple.Init[V]] + inline def init: NamedTuple[Tuple.Init[N], Tuple.Init[V]] = + toTuple.take(size - 1).asInstanceOf[NamedTuple[Tuple.Init[N], Tuple.Init[V]]] /** The tuple consisting of the first `n` elements of this tuple, or all * elements if `n` exceeds `size`. @@ -67,7 +69,11 @@ object NamedTuple: toTuple.drop(n) /** The tuple `(x.take(n), x.drop(n))` */ - inline def splitAt(n: Int): NamedTuple[Tuple.Split[N, n.type], Tuple.Split[V, n.type]] = + inline def splitAt(n: Int): + (NamedTuple[Tuple.Take[N, n.type], Tuple.Take[V, n.type]], + NamedTuple[Tuple.Drop[N, n.type], Tuple.Drop[V, n.type]]) = + // would be nice if this could have type `Split[NamedTuple[N, V]]` instead, but + // we get a type error then. Similar for other methods here. toTuple.splitAt(n) /** The tuple consisting of all elements of this tuple followed by all elements @@ -188,6 +194,12 @@ object NamedTuple: */ type From[T] <: AnyNamedTuple + /** The type of the empty named tuple */ + type Empty = EmptyTuple.type + + /** The empty named tuple */ + val Empty: Empty = EmptyTuple.asInstanceOf[Empty] + end NamedTuple /** Separate from NamedTuple object so that we can match on the opaque type NamedTuple. */ @@ -202,3 +214,4 @@ 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 + From 984fe6291faae9ea222226bedf523cdabae64701 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 25 Apr 2024 13:22:32 +0200 Subject: [PATCH 45/68] More tests --- tests/neg/named-tuples.check | 8 -- .../pos/named-tuples-strawman.scala | 0 tests/pos/selectable-fields.scala | 3 + tests/run/named-tuple-ops.scala | 89 +++++++++++++++++++ 4 files changed, 92 insertions(+), 8 deletions(-) rename tests/{pending => }/pos/named-tuples-strawman.scala (100%) create mode 100644 tests/pos/selectable-fields.scala create mode 100644 tests/run/named-tuple-ops.scala diff --git a/tests/neg/named-tuples.check b/tests/neg/named-tuples.check index 791a10f20b16..db3cc703722f 100644 --- a/tests/neg/named-tuples.check +++ b/tests/neg/named-tuples.check @@ -101,11 +101,3 @@ | Required: (name : ?, age : ?) | | longer explanation available when compiling with `-explain` --- Warning: tests/neg/named-tuples.scala:27:29 ------------------------------------------------------------------------- -27 | val (name = x, agee = y) = person // error - | ^^^^^^ - |pattern's type (String, Int) is more specialized than the right hand side expression's type (name : String, age : Int) - | - |If the narrowing is intentional, this can be communicated by adding `: @unchecked` after the expression, - |which may result in a MatchError at runtime. - |This patch can be rewritten automatically under -rewrite -source 3.2-migration. diff --git a/tests/pending/pos/named-tuples-strawman.scala b/tests/pos/named-tuples-strawman.scala similarity index 100% rename from tests/pending/pos/named-tuples-strawman.scala rename to tests/pos/named-tuples-strawman.scala diff --git a/tests/pos/selectable-fields.scala b/tests/pos/selectable-fields.scala new file mode 100644 index 000000000000..65b024fdfcd6 --- /dev/null +++ b/tests/pos/selectable-fields.scala @@ -0,0 +1,3 @@ +val foo1 = new Selectable: + type Fields = (xyz: Int) + def selectDynamic(name: String): Any = 23 diff --git a/tests/run/named-tuple-ops.scala b/tests/run/named-tuple-ops.scala new file mode 100644 index 000000000000..076ab5028c6c --- /dev/null +++ b/tests/run/named-tuple-ops.scala @@ -0,0 +1,89 @@ +//> using options -source future +import language.experimental.namedTuples +import scala.compiletime.asMatchable + +type City = (name: String, zip: Int, pop: Int) +type Raw = (String, Int, 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") + + val size: 3 = city.size + assert(city.size == 3) + + val zip: Int = city(1) + assert(zip == 1000) + + val name: String = city.head + assert(name == "Lausanne") + + val zip_pop: (zip: Int, pop: Int) = city.tail + val (_: Int, _: Int) = zip_pop + assert(zip_pop == (zip = 1000, pop = 140000)) + + val cinit = city.init + val _: (name: String, zip: Int) = cinit + assert(cinit == (name = "Lausanne", zip = 1000)) + + val ctake1: (name: String) = city.take(1) + assert(ctake1 == (name = "Lausanne")) + + val cdrop1 = city.drop(1) + val _: (zip: Int, pop: Int) = cdrop1 + assert(cdrop1 == zip_pop) + + val cdrop3 = city.drop(3) + val _: NamedTuple.Empty = cdrop3 + assert(cdrop3 == NamedTuple.Empty) + + val cdrop4 = city.drop(4) + val _: NamedTuple.Empty = cdrop4 + assert(cdrop4 == NamedTuple.Empty) + + val csplit = city.splitAt(1) + val _: ((name: String), (zip: Int, pop: Int)) = csplit + assert(csplit == ((name = "Lausanne"), zip_pop)) + + val city_coord = city ++ coord + val _: NamedTuple.Concat[City, Coord] = city_coord + val _: (name: String, zip: Int, pop: Int, x: Double, y: Double) = city_coord + assert(city_coord == (name = "Lausanne", zip = 1000, pop = 140000, x = 1.0, y = 0.0)) + + type IntToString[X] = X match + case Int => String + case _ => X + + val intToString = [X] => (x: X) => x.asMatchable match + case x: Int => x.toString + case x => x + + val citymap = city.map[IntToString](intToString.asInstanceOf) + val _: (name: String, zip: String, pop: String) = citymap + assert(citymap == (name = "Lausanne", zip = "1000", pop = "140000")) + + val cityreverse = city.reverse + val _: (pop: Int, zip: Int, name: String) = cityreverse + assert(cityreverse == (pop = 140000, zip = 1000, name = "Lausanne")) + + val zipped = coord.zip(labels) + val _: (x: (Double, String), y: (Double, String)) = zipped + val (x3, y3) = zipped + val _: (Double, String) = x3 + assert(zipped == (x = (1.0, "west"), y = (0.0, "north"))) + + val zippedRaw = ((1.0, "west"), (0.0, "north")) + val (x1: (Double, String), x2: (Double, String)) = zippedRaw + + val cityFields = city.toList + val _: List[String | Int] = cityFields + assert(cityFields == List("Lausanne", 1000, 140000)) + + val citArr = city.toArray + val _: List[String | Int] = cityFields + assert(cityFields == List("Lausanne", 1000, 140000)) + From 21bcfef05ffccd29bc7131b8f3190f24ef43b329 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 25 Apr 2024 14:04:16 +0200 Subject: [PATCH 46/68] Mention restriction against `_1`, `_2`, ... as named tuple labels --- docs/_docs/reference/experimental/named-tuples.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_docs/reference/experimental/named-tuples.md b/docs/_docs/reference/experimental/named-tuples.md index 6a0baebe36e6..3867b4d13f15 100644 --- a/docs/_docs/reference/experimental/named-tuples.md +++ b/docs/_docs/reference/experimental/named-tuples.md @@ -159,6 +159,7 @@ The following restrictions apply to named tuple elements: (tuple: Tuple) match case (age = x) => // error ``` + 4. Regular selector names `_1`, `_2`, ... are not allowed as names in named tuples. ### Syntax From 92d22c9efdf64895c296a9e239a6803eae04e767 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 26 Apr 2024 20:06:28 +0200 Subject: [PATCH 47/68] Address review comments on Tuple.scala --- library/src/scala/NamedTuple.scala | 4 +-- library/src/scala/Tuple.scala | 40 +++++-------------------- tests/pos/named-tuples-strawman-2.scala | 8 ++--- tests/pos/tuple-ops.scala | 18 ----------- 4 files changed, 13 insertions(+), 57 deletions(-) diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index d4e5a72eb1fc..dc6e6c3144f6 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -184,8 +184,8 @@ object NamedTuple: * @syntax markdown */ type Zip[X <: AnyNamedTuple, Y <: AnyNamedTuple] = - Tuple.Conforms[Names[X], Names[Y]] match - case true => + Names[X] match + case Names[Y] => NamedTuple[Names[X], Tuple.Zip[DropNames[X], DropNames[Y]]] /** A type specially treated by the compiler to represent all fields of a diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 8bd78013210b..30f0e44ecf45 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -118,6 +118,7 @@ object Tuple: // even though it only matches non-empty tuples. // Avoids bounds check failures from an irreducible type // like `Tuple.Head[Tuple.Tail[X]]` + // Other types that don't reduce for empty tuples follow the same principle. type Head[X <: Tuple] = X match case x *: _ => x @@ -273,22 +274,6 @@ object Tuple: */ type Union[T <: Tuple] = Fold[T, Nothing, [x, y] =>> x | y] - /** A type level Boolean indicating whether the tuple `X` conforms - * to the tuple `Y`. This means: - * - the two tuples have the same number of elements - * - for corresponding elements `x` in `X` and `y` in `Y`, `x` matches `y`. - * @pre The elements of `X` are assumed to be singleton types - */ - type Conforms[X <: Tuple, Y <: Tuple] <: Boolean = Y match - case EmptyTuple => - X match - case EmptyTuple => true - case _ => false - case y *: ys => - X match - case `y` *: xs => Conforms[xs, ys] - case _ => false - /** A type level Boolean indicating whether the tuple `X` has an element * that matches `Y`. * @pre The elements of `X` are assumed to be singleton types @@ -350,25 +335,14 @@ object Tuple: extension [X <: Tuple](inline x: X) - /** The index (starting at 0) of the first element in the type `X` of `x` - * that matches type `Y`. + /** The index (starting at 0) of the first occurrence of y.type in the type `X` of `x` + * or Size[X] if no such element exists. */ - inline def indexOfType[Y] = constValue[IndexOf[X, Y]] + transparent inline def indexOf(y: Any): Int = constValue[IndexOf[X, y.type]] - /** A boolean indicating whether there is an element in the type `X` of `x` - * that matches type `Y`. + /** A boolean indicating whether there is an element `y.type` in the type `X` of `x` */ - inline def containsType[Y] = constValue[Contains[X, Y]] - - /* Note: It would be nice to add the following two extension methods: - - inline def indexOf[Y: Precise](y: Y) = constValue[IndexOf[X, Y]] - inline def containsType[Y: Precise](y: Y) = constValue[Contains[X, Y]] - - because we could then move indexOf/contains completely to the value level. - But this requires `Y` to be inferred precisely, and therefore a mechanism - like the `Precise` context bound used above, which does not yet exist. - */ + transparent inline def contains(y: Any): Boolean = constValue[Contains[X, y.type]] end extension @@ -380,7 +354,7 @@ object Tuple: using eqHead: CanEqual[H1, H2], eqTail: CanEqual[T1, T2] ): CanEqual[H1 *: T1, H2 *: T2] = CanEqual.derived - object helpers: + private object helpers: /** Used to implement IndicesWhere */ type IndicesWhereHelper[X <: Tuple, P[_] <: Boolean, N <: Int] <: Tuple = X match diff --git a/tests/pos/named-tuples-strawman-2.scala b/tests/pos/named-tuples-strawman-2.scala index 709f537f8114..4b32dd83f2eb 100644 --- a/tests/pos/named-tuples-strawman-2.scala +++ b/tests/pos/named-tuples-strawman-2.scala @@ -52,7 +52,7 @@ object TupleOps: case EmptyTuple => Y *: EmptyTuple inline def appendIfDistinct[X <: Tuple, Y](xs: X, y: Y): AppendIfDistinct[X, Y] = - (if xs.containsType[Y] then xs else xs :* y).asInstanceOf[AppendIfDistinct[X, Y]] + (if xs.contains(y) then xs else xs :* y).asInstanceOf[AppendIfDistinct[X, Y]] /** `X` with all elements from `Y` that do not occur in `X` appended */ type ConcatDistinct[X <: Tuple, Y <: Tuple] <: Tuple = Y match @@ -137,10 +137,10 @@ object NamedTupleOps: val x1: IndexOf[Names, "first"] = constValue val _: 0 = x1 - val x2: IndexOf[Names, "age"] = names.indexOfType["age"] + val x2: IndexOf[Names, "age"] = names.indexOf("age") val _: 2 = x2 - val x3: IndexOf[Names, "what?"] = names.indexOfType["what?"] + val x3: IndexOf[Names, "what?"] = names.indexOf("what?") val _: 3 = x3 type Releases = "first" *: "middle" *: EmptyTuple @@ -149,7 +149,7 @@ object NamedTupleOps: val releases: Releases = ("first", "middle") val releaseValues: ReleaseValues = (1.0, true) - val x4 = values.updateOrAppend(names.indexOfType["age"], 11) + val x4 = values.updateOrAppend(names.indexOf("age"), 11) //updateOrAppend[Values](values)[IndexOf[Names, "age"], 11](indexOf[Names](names)["age"]("age"), 11) val _: ("Bob", "Miller", 11) = x4 assert(("Bob", "Miller", 11) == x4) diff --git a/tests/pos/tuple-ops.scala b/tests/pos/tuple-ops.scala index df708e669e0f..739b1ebeeb02 100644 --- a/tests/pos/tuple-ops.scala +++ b/tests/pos/tuple-ops.scala @@ -2,18 +2,6 @@ import language.experimental.namedTuples import Tuple.* def test = - val x1: Conforms[(1, 2), (1, 2)] = ??? - val _: true = x1 - - val x2: Conforms[(1, 2), (1, 3)] = ??? - val _: false = x2 - - val x3: Conforms[(1, 2), (1, 2, 4)] = ??? - val _: false = x2 - - val x4: Conforms[(1, 2, 4), (1, 2)] = ??? - val _: false = x2 - summon[Disjoint[(1, 2, 3), (4, 5)] =:= true] summon[Disjoint[(1, 2, 6), (4, 5)] =:= true] summon[Disjoint[(1, 2, 6), EmptyTuple] =:= true] @@ -23,12 +11,6 @@ def test = summon[Contains[(1, 2, 3), 2] =:= true] summon[Contains[(1, 2, 3), 4] =:= false] - summon[Conforms[(1, 2, 3), (1, 2, 3)] =:= true] - summon[Conforms[(1, 2, 3), (1, 2)] =:= false] - summon[Conforms[(1, 2, 3), (1, 2, 4)] =:= false] - summon[Conforms[(1, 2, 3), (Int, 2, 3)] =:= true] -// summon[Conforms[(Int, 2, 3), (1, 2, 3)] =:= true] // error, reduction gets stuck - summon[Disjoint[(1, 2, 3), (4, 2)] =:= false] summon[Disjoint[("a", "b"), ("b", "c")] =:= false] summon[Disjoint[(1, 2, 6), Tuple1[2]] =:= false] From 1613ee181173325f0dae326a4e54898b0c4bc315 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 27 Apr 2024 10:32:01 +0200 Subject: [PATCH 48/68] Reject recursive dynamicSelect corner case --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 10 ++++++++-- tests/neg/unselectable-fields.check | 4 ++++ .../unselectable-fields.scala} | 3 +++ 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 tests/neg/unselectable-fields.check rename tests/{pos/selectable-fields.scala => neg/unselectable-fields.scala} (77%) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index c392c195482c..a44d9b9512db 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -822,14 +822,20 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // and the selector is neither applied nor assigned to, // expand to a typed dynamic dispatch using selectDynamic wrapped in a cast if qual.tpe.derivesFrom(defn.SelectableClass) && !isDynamicExpansion(tree) - && !pt.isInstanceOf[FunOrPolyProto] && pt != LhsProto + && !pt.isInstanceOf[FunOrPolyProto] && pt != LhsProto then val fieldsType = qual.tpe.select(tpnme.Fields).dealias.simplified val fields = fieldsType.namedTupleElementTypes typr.println(i"try dyn select $qual, $selName, $fields") fields.find(_._1 == selName) match case Some((_, fieldType)) => - return dynamicSelect(fieldType).ensureConforms(fieldType) + val dynSelected = dynamicSelect(fieldType) + dynSelected match + case Apply(sel: Select, _) if !sel.denot.symbol.exists => + // Reject corner case where selectDynamic needs annother selectDynamic to be called. E.g. as in neg/unselectable-fields.scala. + report.error(i"Cannot use selectDynamic here since it it needs another selectDynamic to be invoked", tree.srcPos) + case _ => + return dynSelected.ensureConforms(fieldType) case _ => // Otherwise, report an error diff --git a/tests/neg/unselectable-fields.check b/tests/neg/unselectable-fields.check new file mode 100644 index 000000000000..06992eded299 --- /dev/null +++ b/tests/neg/unselectable-fields.check @@ -0,0 +1,4 @@ +-- Error: tests/neg/unselectable-fields.scala:4:13 --------------------------------------------------------------------- +4 |val _ = foo1.xyz // error + | ^^^^^^^^ + | Cannot use selectDynamic here since it it needs another selectDynamic to be invoked diff --git a/tests/pos/selectable-fields.scala b/tests/neg/unselectable-fields.scala similarity index 77% rename from tests/pos/selectable-fields.scala rename to tests/neg/unselectable-fields.scala index 65b024fdfcd6..7abe49d24764 100644 --- a/tests/pos/selectable-fields.scala +++ b/tests/neg/unselectable-fields.scala @@ -1,3 +1,6 @@ val foo1 = new Selectable: type Fields = (xyz: Int) def selectDynamic(name: String): Any = 23 +val _ = foo1.xyz // error + + From a04d3a7429e3ac3964bd8d867c71708d54e0c264 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 27 Apr 2024 12:53:17 +0200 Subject: [PATCH 49/68] Update compiler/src/dotty/tools/dotc/typer/Typer.scala --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index a44d9b9512db..46982cf1406d 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -833,7 +833,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer dynSelected match case Apply(sel: Select, _) if !sel.denot.symbol.exists => // Reject corner case where selectDynamic needs annother selectDynamic to be called. E.g. as in neg/unselectable-fields.scala. - report.error(i"Cannot use selectDynamic here since it it needs another selectDynamic to be invoked", tree.srcPos) + report.error(i"Cannot use selectDynamic here since it needs another selectDynamic to be invoked", tree.srcPos) case _ => return dynSelected.ensureConforms(fieldType) case _ => From 37b1bd208713b5558b40fe6c388fa8028baed902 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 27 Apr 2024 13:33:36 +0200 Subject: [PATCH 50/68] Update tests/neg/unselectable-fields.check --- tests/neg/unselectable-fields.check | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/neg/unselectable-fields.check b/tests/neg/unselectable-fields.check index 06992eded299..f7f0bf51a6bc 100644 --- a/tests/neg/unselectable-fields.check +++ b/tests/neg/unselectable-fields.check @@ -1,4 +1,4 @@ -- Error: tests/neg/unselectable-fields.scala:4:13 --------------------------------------------------------------------- 4 |val _ = foo1.xyz // error | ^^^^^^^^ - | Cannot use selectDynamic here since it it needs another selectDynamic to be invoked + | Cannot use selectDynamic here since it needs another selectDynamic to be invoked From 3f8f6c694bddeeafe3bb16cf8171c8b0dd789464 Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Mon, 29 Apr 2024 11:30:27 +0200 Subject: [PATCH 51/68] Use consistent naming and drop remaining braces in tuple type ops --- library/src/scala/Tuple.scala | 67 ++++++++++++++----------------- tests/neg/print-tuple-union.check | 2 +- tests/neg/wildcard-match.check | 7 ++-- 3 files changed, 34 insertions(+), 42 deletions(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 30f0e44ecf45..b643f606baae 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -22,13 +22,13 @@ sealed trait Tuple extends Product: 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] = + inline def :* [This >: this.type <: Tuple, L](x: L): Append[This, L] = 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) */ - inline def *: [H, This >: this.type <: Tuple] (x: H): H *: This = + 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. @@ -104,14 +104,13 @@ object Tuple: /** The size of a tuple, represented as a literal constant subtype of Int */ type Size[X <: Tuple] <: Int = X match case EmptyTuple => 0 - case x *: xs => S[Size[xs]] + case _ *: xs => S[Size[xs]] /** The type of the element at position N in the tuple X */ type Elem[X <: Tuple, N <: Int] = X match - case x *: xs => - N match - case 0 => x - case S[n1] => Elem[xs, n1] + case x *: xs => N match + case 0 => x + case S[n1] => Elem[xs, n1] /** The type of the first element of a tuple */ // Only bounded by `<: Tuple` not `<: NonEmptyTuple` @@ -134,8 +133,7 @@ object Tuple: /** The type of the initial part of a 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] /** The type of the tuple consisting of the first `N` elements of `X`, * or all elements if `N` exceeds `Size[X]`. @@ -149,27 +147,24 @@ object Tuple: /** The type of the tuple consisting of all elements of `X` except the first `N` ones, * or no elements if `N` exceeds `Size[X]`. */ - type Drop[X <: Tuple, N <: Int] <: Tuple = N match { + type Drop[X <: Tuple, N <: Int] <: Tuple = N match case 0 => X - case S[n1] => X match { + case S[n1] => X match case EmptyTuple => EmptyTuple - case x *: xs => Drop[xs, n1] - } - } + case _ *: xs => Drop[xs, n1] /** The pair type `(Take(X, N), Drop[X, N]). */ type Split[X <: Tuple, N <: Int] = (Take[X, N], Drop[X, N]) /** Type of a tuple with an element appended */ - type Append[X <: Tuple, Y] <: NonEmptyTuple = X match { + type Append[X <: Tuple, Y] <: NonEmptyTuple = X match case EmptyTuple => Y *: EmptyTuple case x *: xs => x *: Append[xs, Y] - } /** Type of the concatenation of two tuples `X` and `Y` */ type Concat[X <: Tuple, +Y <: Tuple] <: Tuple = X match case EmptyTuple => Y - case x1 *: xs1 => x1 *: Concat[xs1, Y] + case x *: xs => x *: Concat[xs, Y] /** An infix shorthand for `Concat[X, Y]` */ infix type ++[X <: Tuple, +Y <: Tuple] = Concat[X, Y] @@ -179,27 +174,27 @@ object Tuple: */ type IndexOf[X <: Tuple, Y] <: Int = X match case Y *: _ => 0 - case x *: xs => S[IndexOf[xs, Y]] + case _ *: xs => S[IndexOf[xs, Y]] case EmptyTuple => 0 /** Fold a tuple `(T1, ..., Tn)` into `F[T1, F[... F[Tn, Z]...]]]` */ - type Fold[Tup <: Tuple, Z, F[_, _]] = Tup match + type Fold[X <: Tuple, Z, F[_, _]] = X match case EmptyTuple => Z - case h *: t => F[h, Fold[t, Z, F]] + case x *: xs => F[x, Fold[xs, Z, F]] /** The type of tuple `X` mapped with the type-level function `F`. * If `X = (T1, ..., Ti)` then `Map[X, F] = `(F[T1], ..., F[Ti])`. */ - type Map[Tup <: Tuple, F[_ <: Union[Tup]]] <: Tuple = Tup match + type Map[X <: Tuple, F[_ <: Union[X]]] <: Tuple = X match case EmptyTuple => EmptyTuple - case h *: t => F[h] *: Map[t, F] + case x *: xs => F[x] *: Map[xs, F] /** The type of tuple `X` flat-mapped with the type-level function `F`. * If `X = (T1, ..., Ti)` then `FlatMap[X, F] = `F[T1] ++ ... ++ F[Ti]` */ - type FlatMap[Tup <: Tuple, F[_ <: Union[Tup]] <: Tuple] <: Tuple = Tup match + type FlatMap[X <: Tuple, F[_ <: Union[X]] <: Tuple] <: Tuple = X match case EmptyTuple => EmptyTuple - case h *: t => Concat[F[h], FlatMap[t, F]] + case x *: xs => Concat[F[x], FlatMap[xs, F]] // TODO: implement term level analogue /** The type of the tuple consisting of all elements of tuple `X` that have types @@ -217,9 +212,9 @@ object Tuple: */ type Filter[X <: Tuple, P[_] <: Boolean] <: Tuple = X match case EmptyTuple => EmptyTuple - case h *: t => P[h] match - case true => h *: Filter[t, P] - case false => Filter[t, P] + case x *: xs => P[x] match + case true => x *: Filter[xs, P] + case false => Filter[xs, P] /** A tuple consisting of those indices `N` of tuple `X` where the predicate `P` * is true for `Elem[X, N]`. Indices are type level values <: Int. @@ -242,17 +237,16 @@ object Tuple: * ``` * @syntax markdown */ - type Zip[T1 <: Tuple, T2 <: Tuple] <: Tuple = (T1, T2) match - case (h1 *: t1, h2 *: t2) => (h1, h2) *: Zip[t1, t2] + type Zip[X <: Tuple, Y <: Tuple] <: Tuple = (X, Y) match + case (x *: xs, y *: ys) => (x, y) *: Zip[xs, ys] case (EmptyTuple, _) => EmptyTuple case (_, EmptyTuple) => EmptyTuple case _ => Tuple /** Converts a tuple `(F[T1], ..., F[Tn])` to `(T1, ... Tn)` */ - type InverseMap[X <: Tuple, F[_]] <: Tuple = X match { - case F[x] *: t => x *: InverseMap[t, F] + type InverseMap[X <: Tuple, F[_]] <: Tuple = X match + case F[x] *: xs => x *: InverseMap[xs, F] case EmptyTuple => EmptyTuple - } /** Implicit evidence. IsMappedBy[F][X] is present in the implicit scope iff * X is a tuple for which each element's type is constructed via `F`. E.g. @@ -280,7 +274,7 @@ object Tuple: */ type Contains[X <: Tuple, Y] <: Boolean = X match case Y *: _ => true - case x *: xs => Contains[xs, Y] + case _ *: xs => Contains[xs, Y] case EmptyTuple => false /** A type level Boolean indicating whether the type `Y` contains @@ -288,10 +282,9 @@ object Tuple: * @pre The elements of `X` and `Y` are assumed to be singleton types */ type Disjoint[X <: Tuple, Y <: Tuple] <: Boolean = X match - case x *: xs => - Contains[Y, x] match - case true => false - case false => Disjoint[xs, Y] + case x *: xs => Contains[Y, x] match + case true => false + case false => Disjoint[xs, Y] case EmptyTuple => true /** Empty tuple */ diff --git a/tests/neg/print-tuple-union.check b/tests/neg/print-tuple-union.check index f3754aa5b17e..7d2c019de5a6 100644 --- a/tests/neg/print-tuple-union.check +++ b/tests/neg/print-tuple-union.check @@ -13,6 +13,6 @@ | and cannot be shown to be disjoint from it either. | Therefore, reduction cannot advance to the remaining case | - | case h *: t => h | Tuple.Fold[t, Nothing, [x, y] =>> x | y] + | case x *: xs => x | Tuple.Fold[xs, Nothing, [x, y] =>> x | y] | | longer explanation available when compiling with `-explain` diff --git a/tests/neg/wildcard-match.check b/tests/neg/wildcard-match.check index d405326c3d2b..fd20443c0a9f 100644 --- a/tests/neg/wildcard-match.check +++ b/tests/neg/wildcard-match.check @@ -87,8 +87,7 @@ | trying to reduce shapeless.tuples.length[T2] | trying to reduce Tuple.Size[shapeless.tuples.to[T2]] | failed since selector shapeless.tuples.to[T2] - | does not uniquely determine parameters x, xs in - | case x *: xs => scala.compiletime.ops.int.S[Tuple.Size[xs]] - | The computed bounds for the parameters are: - | x <: Int + | does not uniquely determine parameter xs in + | case _ *: xs => scala.compiletime.ops.int.S[Tuple.Size[xs]] + | The computed bounds for the parameter are: | xs <: (Int, Int) From 9627c08818cbe55e1d215a4192b675548c92a957 Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Mon, 29 Apr 2024 11:32:05 +0200 Subject: [PATCH 52/68] Add an infix shorthand for `Append[X, Y]` as is the case for `Concat` --- library/src/scala/Tuple.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index b643f606baae..34274c4b0413 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -22,7 +22,7 @@ sealed trait Tuple extends Product: 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] = + inline def :* [This >: this.type <: Tuple, L](x: L): This :* L = runtime.Tuples.append(x, this).asInstanceOf[Append[This, L]] /** Return a new tuple by prepending the element to `this` tuple. @@ -34,7 +34,7 @@ sealed trait Tuple extends Product: /** Return a new tuple by concatenating `this` tuple with `that` tuple. * This operation is O(this.size + that.size) */ - inline def ++ [This >: this.type <: Tuple](that: Tuple): Concat[This, that.type] = + inline def ++ [This >: this.type <: Tuple](that: Tuple): This ++ that.type = runtime.Tuples.concat(this, that).asInstanceOf[Concat[This, that.type]] /** Return the size (or arity) of the tuple */ @@ -161,6 +161,9 @@ object Tuple: case EmptyTuple => Y *: EmptyTuple case x *: xs => x *: Append[xs, Y] + /** An infix shorthand for `Append[X, Y]` */ + infix type :*[X <: Tuple, Y] = Append[X, Y] + /** Type of the concatenation of two tuples `X` and `Y` */ type Concat[X <: Tuple, +Y <: Tuple] <: Tuple = X match case EmptyTuple => Y From 8a4162f70761e9b2937b94a33562682a36af34da Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Mon, 29 Apr 2024 11:53:11 +0200 Subject: [PATCH 53/68] Drop unreachable case from `type Zip` --- library/src/scala/Tuple.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 34274c4b0413..7afa96a067b0 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -244,7 +244,6 @@ object Tuple: case (x *: xs, y *: ys) => (x, y) *: Zip[xs, ys] case (EmptyTuple, _) => EmptyTuple case (_, EmptyTuple) => EmptyTuple - case _ => Tuple /** Converts a tuple `(F[T1], ..., F[Tn])` to `(T1, ... Tn)` */ type InverseMap[X <: Tuple, F[_]] <: Tuple = X match From 0ab9e7bb76bd60b8e0617f28b111aaeeee7babdb Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Mon, 29 Apr 2024 11:53:50 +0200 Subject: [PATCH 54/68] Document `Concat` covariance in 2nd parameter --- library/src/scala/Tuple.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 7afa96a067b0..e46d5c5348d8 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -34,6 +34,8 @@ sealed trait Tuple extends Product: /** Return a new tuple by concatenating `this` tuple with `that` tuple. * This operation is O(this.size + that.size) */ + // Contrarily to `this`, `that` does not need a type parameter + // since `++` is covariant in its second argument. inline def ++ [This >: this.type <: Tuple](that: Tuple): This ++ that.type = runtime.Tuples.concat(this, that).asInstanceOf[Concat[This, that.type]] @@ -165,6 +167,7 @@ object Tuple: infix type :*[X <: Tuple, Y] = Append[X, Y] /** Type of the concatenation of two tuples `X` and `Y` */ + // Can be covariant in `Y` since it never appears as a match type scrutinee. type Concat[X <: Tuple, +Y <: Tuple] <: Tuple = X match case EmptyTuple => Y case x *: xs => x *: Concat[xs, Y] From 075b7d148b0f16c54ac8f50d5af4e2536f5e821e Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Mon, 29 Apr 2024 12:07:54 +0200 Subject: [PATCH 55/68] Refine bounds of `type Filter` predicate to only require being defined on the element types. Similar to what we have for `type FlatMap` --- library/src/scala/Tuple.scala | 8 ++++---- tests/pos/tuple-filter.scala | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index e46d5c5348d8..fa598f3bd105 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -94,7 +94,7 @@ sealed trait Tuple extends Product: * for which the given type level predicate `P` reduces to the literal * constant `true`. */ - inline def filter[This >: this.type <: Tuple, P[_] <: Boolean]: Filter[This, P] = + inline def filter[This >: this.type <: Tuple, P[_ <: Union[This]] <: Boolean]: Filter[This, P] = val toInclude = constValueTuple[IndicesWhere[This, P]].toArray val arr = new Array[Object](toInclude.length) for i <- 0 until toInclude.length do @@ -216,7 +216,7 @@ object Tuple: * ``` * @syntax markdown */ - type Filter[X <: Tuple, P[_] <: Boolean] <: Tuple = X match + type Filter[X <: Tuple, P[_ <: Union[X]] <: Boolean] <: Tuple = X match case EmptyTuple => EmptyTuple case x *: xs => P[x] match case true => x *: Filter[xs, P] @@ -225,7 +225,7 @@ object Tuple: /** A tuple consisting of those indices `N` of tuple `X` where the predicate `P` * is true for `Elem[X, N]`. Indices are type level values <: Int. */ - type IndicesWhere[X <: Tuple, P[_] <: Boolean] = + type IndicesWhere[X <: Tuple, P[_ <: Union[X]] <: Boolean] = helpers.IndicesWhereHelper[X, P, 0] /** The type of the tuple consisting of all element values of @@ -355,7 +355,7 @@ object Tuple: private object helpers: /** Used to implement IndicesWhere */ - type IndicesWhereHelper[X <: Tuple, P[_] <: Boolean, N <: Int] <: Tuple = X match + type IndicesWhereHelper[X <: Tuple, P[_ <: Union[X]] <: Boolean, N <: Int] <: Tuple = X match case EmptyTuple => EmptyTuple case h *: t => P[h] match case true => N *: IndicesWhereHelper[t, P, S[N]] diff --git a/tests/pos/tuple-filter.scala b/tests/pos/tuple-filter.scala index 2c9638b2e47b..0964d2e982d9 100644 --- a/tests/pos/tuple-filter.scala +++ b/tests/pos/tuple-filter.scala @@ -8,3 +8,6 @@ def Test = summon[Tuple.Filter[(1, 2, 3, 4), P] =:= (1, 2, 4)] summon[Tuple.Filter[(1, 2, 3, 4), RejectAll] =:= EmptyTuple] summon[Tuple.Filter[EmptyTuple, P] =:= EmptyTuple] + + import compiletime.ops.int.< + summon[Tuple.Filter[(1, 4, 7, 2, 10, 3, 4), [X <: Int] =>> X < 5] =:= (1, 4, 2, 3, 4)] From c58c8c24129097ea6d372e4c6e45bbe1722a84d7 Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Mon, 29 Apr 2024 13:44:15 +0200 Subject: [PATCH 56/68] Do `contains` runtime operation based on term equality --- library/src/scala/Tuple.scala | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index fa598f3bd105..21aa9dbf598f 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -338,9 +338,15 @@ object Tuple: */ transparent inline def indexOf(y: Any): Int = constValue[IndexOf[X, y.type]] - /** A boolean indicating whether there is an element `y.type` in the type `X` of `x` - */ - transparent inline def contains(y: Any): Boolean = constValue[Contains[X, y.type]] + /** A boolean indicating whether there is an element `y.type` in the type `X` of `x` */ + // Note this isn't equivalent to `constValue[Contains[X, y.type]]` + // since it also accepts cases unknown at compiletime. + // Also note it would be unsound to use a type parameter for `y` in the + // type level `Contains`, since it is rightfully not covariant in `Y`. + inline def contains(y: Any): Contains[X, y.type] = + x.productIterator.contains(y).asInstanceOf[Contains[X, y.type]] + + // TODO containsType ? end extension From 57b17accced114c61c0222099b69f044657c86df Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Mon, 29 Apr 2024 13:58:58 +0200 Subject: [PATCH 57/68] Do `indexOf` runtime operation based on term equality and refine `type IndexOf` doc --- library/src/scala/Tuple.scala | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 21aa9dbf598f..7f6923c976dd 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -176,7 +176,7 @@ object Tuple: infix type ++[X <: Tuple, +Y <: Tuple] = Concat[X, Y] /** The index of `Y` in tuple `X` as a literal constant Int, - * or `Size[X]` if `Y` does not occur in `X` + * or `Size[X]` if `Y` is disjoint from all element types in `X`. */ type IndexOf[X <: Tuple, Y] <: Int = X match case Y *: _ => 0 @@ -332,21 +332,22 @@ object Tuple: runtime.Tuples.fromProduct(product) extension [X <: Tuple](inline x: X) + // Note the two methods are not equivalent to using `constValue`, + // since they also allow cases unknown at compiletime. + // Also note it would be unsound to use a type parameter for `y` in the type level + // operations, since they are rightfully not covariant in their second parameter. /** The index (starting at 0) of the first occurrence of y.type in the type `X` of `x` * or Size[X] if no such element exists. */ - transparent inline def indexOf(y: Any): Int = constValue[IndexOf[X, y.type]] + inline def indexOf(y: Any): IndexOf[X, y.type] = + x.productIterator.indexOf(y).asInstanceOf[IndexOf[X, y.type]] /** A boolean indicating whether there is an element `y.type` in the type `X` of `x` */ - // Note this isn't equivalent to `constValue[Contains[X, y.type]]` - // since it also accepts cases unknown at compiletime. - // Also note it would be unsound to use a type parameter for `y` in the - // type level `Contains`, since it is rightfully not covariant in `Y`. inline def contains(y: Any): Contains[X, y.type] = x.productIterator.contains(y).asInstanceOf[Contains[X, y.type]] - // TODO containsType ? + // TODO indexOfType & containsType ? end extension From f427ec96c2254ba34576482540dd759b3419ea0e Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Mon, 29 Apr 2024 14:04:45 +0200 Subject: [PATCH 58/68] Do `filter` runtime operation based on a term level predicate --- library/src/scala/Tuple.scala | 13 ++++--------- tests/pos/named-tuples-strawman-2.scala | 5 ++++- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 7f6923c976dd..208f6b464286 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -90,15 +90,10 @@ sealed trait Tuple extends Product: inline def reverseOnto[This >: this.type <: Tuple, Acc <: Tuple](acc: Acc): ReverseOnto[This, Acc] = (this.reverse ++ acc).asInstanceOf[ReverseOnto[This, Acc]] - /** A tuple consisting of all elements of this tuple that have types - * for which the given type level predicate `P` reduces to the literal - * constant `true`. - */ - inline def filter[This >: this.type <: Tuple, P[_ <: Union[This]] <: Boolean]: Filter[This, P] = - val toInclude = constValueTuple[IndicesWhere[This, P]].toArray - val arr = new Array[Object](toInclude.length) - for i <- 0 until toInclude.length do - arr(i) = this.productElement(toInclude(i).asInstanceOf[Int]).asInstanceOf[Object] + /** A tuple consisting of all elements of this tuple that satisfy the predicate `p`. */ + inline def filter[This >: this.type <: Tuple, P[_ <: Union[This]] <: Boolean] + (p: (x: Union[This]) => P[x.type]): Filter[This, P] = + val arr = this.toArray.filter(x => p(x.asInstanceOf[Union[This]])) Tuple.fromArray(arr).asInstanceOf[Filter[This, P]] object Tuple: diff --git a/tests/pos/named-tuples-strawman-2.scala b/tests/pos/named-tuples-strawman-2.scala index 4b32dd83f2eb..7cd763bb7b00 100644 --- a/tests/pos/named-tuples-strawman-2.scala +++ b/tests/pos/named-tuples-strawman-2.scala @@ -60,7 +60,10 @@ object TupleOps: case EmptyTuple => X inline def concatDistinct[X <: Tuple, Y <: Tuple](xs: X, ys: Y): ConcatDistinct[X, Y] = - (xs ++ ys.filter[Y, [Elem] =>> ![Contains[X, Elem]]]).asInstanceOf[ConcatDistinct[X, Y]] + // Note the type parameter is needed due to the invariance of compiletime.ops.boolean.! + extension [B <: Boolean](self: B) def negated: ![B] = (!self).asInstanceOf + val ysDistinct = ys.filter[Y, [y] =>> ![Contains[X, y]]](xs.contains(_).negated) + (xs ++ ysDistinct).asInstanceOf[ConcatDistinct[X, Y]] object NamedTupleDecomposition: import NamedTupleOps.* From 94b7c1f2f9f8996c8d5b68d28794349f3dcb2000 Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Mon, 29 Apr 2024 14:17:48 +0200 Subject: [PATCH 59/68] Mark `type Append` 2nd argument as covariant --- library/src/scala/Tuple.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 208f6b464286..e32a1bd8f124 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -154,12 +154,12 @@ object Tuple: type Split[X <: Tuple, N <: Int] = (Take[X, N], Drop[X, N]) /** Type of a tuple with an element appended */ - type Append[X <: Tuple, Y] <: NonEmptyTuple = X match + type Append[X <: Tuple, +Y] <: NonEmptyTuple = X match case EmptyTuple => Y *: EmptyTuple case x *: xs => x *: Append[xs, Y] /** An infix shorthand for `Append[X, Y]` */ - infix type :*[X <: Tuple, Y] = Append[X, Y] + infix type :*[X <: Tuple, +Y] = Append[X, Y] /** Type of the concatenation of two tuples `X` and `Y` */ // Can be covariant in `Y` since it never appears as a match type scrutinee. From 5df2120e841955ab2b30e5e7fd1c6bf5282ae3c7 Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Mon, 29 Apr 2024 14:52:42 +0200 Subject: [PATCH 60/68] Move `NonEmptyTuple` methods into `Tuple` This is for the same reason as we changed `type Head[X <: NonEmptyTuple] = ...` to `type Head[X <: Tuple] = ...` Also, this is no more unsafe than the other operations already defined for all tuples. `drop(1)` for example was always defined, even though `tail` wasn't. --- library/src/scala/Tuple.scala | 52 +++++++++++++------------- library/src/scala/runtime/Tuples.scala | 8 ++-- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index e32a1bd8f124..1c009e4d65e6 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -31,6 +31,30 @@ sealed trait Tuple extends Product: inline def *: [H, This >: this.type <: Tuple](x: H): H *: This = runtime.Tuples.cons(x, this).asInstanceOf[H *: This] + /** Get the i-th element of this tuple. + * Equivalent to productElement but with a precise return type. + */ + inline def apply[This >: this.type <: Tuple](n: Int): Elem[This, n.type] = + runtime.Tuples.apply(this, n).asInstanceOf[Elem[This, n.type]] + + /** Get the head of this tuple */ + inline def head[This >: this.type <: Tuple]: Head[This] = + runtime.Tuples.apply(this, 0).asInstanceOf[Head[This]] + + /** Get the initial part of the tuple without its last element */ + inline def init[This >: this.type <: Tuple]: Init[This] = + runtime.Tuples.init(this).asInstanceOf[Init[This]] + + /** Get the last of this tuple */ + inline def last[This >: this.type <: Tuple]: Last[This] = + runtime.Tuples.last(this).asInstanceOf[Last[This]] + + /** Get the tail of this tuple. + * This operation is O(this.size) + */ + inline def tail[This >: this.type <: Tuple]: Tail[This] = + runtime.Tuples.tail(this).asInstanceOf[Tail[This]] + /** Return a new tuple by concatenating `this` tuple with `that` tuple. * This operation is O(this.size + that.size) */ @@ -375,33 +399,7 @@ case object EmptyTuple extends Tuple { } /** Tuple of arbitrary non-zero arity */ -sealed trait NonEmptyTuple extends Tuple { - import Tuple.* - - /** Get the i-th element of this tuple. - * Equivalent to productElement but with a precise return type. - */ - 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 */ - 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 */ - inline def init[This >: this.type <: NonEmptyTuple]: Init[This] = - runtime.Tuples.init(this).asInstanceOf[Init[This]] - - /** Get the last of this tuple */ - 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) - */ - inline def tail[This >: this.type <: NonEmptyTuple]: Tail[This] = - runtime.Tuples.tail(this).asInstanceOf[Tail[This]] -} +sealed trait NonEmptyTuple extends Tuple @showAsInfix sealed abstract class *:[+H, +T <: Tuple] extends NonEmptyTuple diff --git a/library/src/scala/runtime/Tuples.scala b/library/src/scala/runtime/Tuples.scala index be6904b9d1d0..8da21c777943 100644 --- a/library/src/scala/runtime/Tuples.scala +++ b/library/src/scala/runtime/Tuples.scala @@ -357,7 +357,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) } @@ -565,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(...) From 40c61388a9e7cae8777b67fbfd4e738c1374ced0 Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Mon, 29 Apr 2024 15:23:36 +0200 Subject: [PATCH 61/68] Reorder operations to be same between term and type level --- library/src/scala/Tuple.scala | 202 +++++++++++++++++----------------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 1c009e4d65e6..7f9e220b2bf2 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -21,18 +21,22 @@ sealed trait Tuple extends Product: 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): This :* L = - 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) */ inline def *: [H, This >: this.type <: Tuple](x: H): H *: This = runtime.Tuples.cons(x, this).asInstanceOf[H *: This] + /** Return a copy of `this` tuple with an element appended */ + inline def :* [This >: this.type <: Tuple, L](x: L): This :* L = + runtime.Tuples.append(x, this).asInstanceOf[Append[This, L]] + + /** Return the size (or arity) of the tuple */ + inline def size[This >: this.type <: Tuple]: Size[This] = + runtime.Tuples.size(this).asInstanceOf[Size[This]] + /** Get the i-th element of this tuple. - * Equivalent to productElement but with a precise return type. + * Equivalent to productElement but with a precise return type. */ inline def apply[This >: this.type <: Tuple](n: Int): Elem[This, n.type] = runtime.Tuples.apply(this, n).asInstanceOf[Elem[This, n.type]] @@ -41,19 +45,38 @@ sealed trait Tuple extends Product: inline def head[This >: this.type <: Tuple]: Head[This] = runtime.Tuples.apply(this, 0).asInstanceOf[Head[This]] - /** Get the initial part of the tuple without its last element */ - inline def init[This >: this.type <: Tuple]: Init[This] = - runtime.Tuples.init(this).asInstanceOf[Init[This]] + /** Get the tail of this tuple. + * This operation is O(this.size) + */ + inline def tail[This >: this.type <: Tuple]: Tail[This] = + runtime.Tuples.tail(this).asInstanceOf[Tail[This]] /** Get the last of this tuple */ inline def last[This >: this.type <: Tuple]: Last[This] = runtime.Tuples.last(this).asInstanceOf[Last[This]] - /** Get the tail of this tuple. - * This operation is O(this.size) + /** Get the initial part of the tuple without its last element */ + inline def init[This >: this.type <: Tuple]: Init[This] = + runtime.Tuples.init(this).asInstanceOf[Init[This]] + + /** Given a tuple `(a1, ..., am)`, returns the tuple `(a1, ..., an)` consisting + * of its first n elements. */ - inline def tail[This >: this.type <: Tuple]: Tail[This] = - runtime.Tuples.tail(this).asInstanceOf[Tail[This]] + 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. + */ + 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. + */ + inline def splitAt[This >: this.type <: Tuple](n: Int): Split[This, n.type] = + runtime.Tuples.splitAt(this, n).asInstanceOf[Split[This, n.type]] /** Return a new tuple by concatenating `this` tuple with `that` tuple. * This operation is O(this.size + that.size) @@ -63,10 +86,6 @@ sealed trait Tuple extends Product: inline def ++ [This >: this.type <: Tuple](that: Tuple): This ++ that.type = runtime.Tuples.concat(this, that).asInstanceOf[Concat[This, that.type]] - /** Return the size (or arity) of the tuple */ - inline def size[This >: this.type <: Tuple]: Size[This] = - runtime.Tuples.size(this).asInstanceOf[Size[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. @@ -85,24 +104,11 @@ sealed trait Tuple extends Product: 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. - */ - 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. - */ - 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. - */ - inline def splitAt[This >: this.type <: Tuple](n: Int): Split[This, n.type] = - runtime.Tuples.splitAt(this, n).asInstanceOf[Split[This, n.type]] + /** A tuple consisting of all elements of this tuple that satisfy the predicate `p`. */ + inline def filter[This >: this.type <: Tuple, P[_ <: Union[This]] <: Boolean] + (p: (x: Union[This]) => P[x.type]): Filter[This, P] = + val arr = this.toArray.filter(x => p(x.asInstanceOf[Union[This]])) + Tuple.fromArray(arr).asInstanceOf[Filter[This, P]] /** Given a tuple `(a1, ..., am)`, returns the reversed tuple `(am, ..., a1)` * consisting all its elements. @@ -114,14 +120,16 @@ sealed trait Tuple extends Product: inline def reverseOnto[This >: this.type <: Tuple, Acc <: Tuple](acc: Acc): ReverseOnto[This, Acc] = (this.reverse ++ acc).asInstanceOf[ReverseOnto[This, Acc]] - /** A tuple consisting of all elements of this tuple that satisfy the predicate `p`. */ - inline def filter[This >: this.type <: Tuple, P[_ <: Union[This]] <: Boolean] - (p: (x: Union[This]) => P[x.type]): Filter[This, P] = - val arr = this.toArray.filter(x => p(x.asInstanceOf[Union[This]])) - Tuple.fromArray(arr).asInstanceOf[Filter[This, P]] - object Tuple: + /** Type of a tuple with an element appended */ + type Append[X <: Tuple, +Y] <: NonEmptyTuple = X match + case EmptyTuple => Y *: EmptyTuple + case x *: xs => x *: Append[xs, Y] + + /** An infix shorthand for `Append[X, Y]` */ + infix type :*[X <: Tuple, +Y] = Append[X, Y] + /** The size of a tuple, represented as a literal constant subtype of Int */ type Size[X <: Tuple] <: Int = X match case EmptyTuple => 0 @@ -142,15 +150,15 @@ object Tuple: type Head[X <: Tuple] = X match case x *: _ => x + /** The type of a tuple consisting of all elements of tuple X except the first one */ + type Tail[X <: Tuple] <: Tuple = X match + case _ *: xs => xs + /** The type of the last element of a tuple */ type Last[X <: Tuple] = X match case x *: EmptyTuple => x case _ *: xs => Last[xs] - /** The type of a tuple consisting of all elements of tuple X except the first one */ - type Tail[X <: Tuple] <: Tuple = X match - case _ *: xs => xs - /** The type of the initial part of a tuple without its last element */ type Init[X <: Tuple] <: Tuple = X match case _ *: EmptyTuple => EmptyTuple @@ -177,14 +185,6 @@ object Tuple: /** The pair type `(Take(X, N), Drop[X, N]). */ type Split[X <: Tuple, N <: Int] = (Take[X, N], Drop[X, N]) - /** Type of a tuple with an element appended */ - type Append[X <: Tuple, +Y] <: NonEmptyTuple = X match - case EmptyTuple => Y *: EmptyTuple - case x *: xs => x *: Append[xs, Y] - - /** An infix shorthand for `Append[X, Y]` */ - infix type :*[X <: Tuple, +Y] = Append[X, Y] - /** Type of the concatenation of two tuples `X` and `Y` */ // Can be covariant in `Y` since it never appears as a match type scrutinee. type Concat[X <: Tuple, +Y <: Tuple] <: Tuple = X match @@ -194,18 +194,25 @@ object Tuple: /** An infix shorthand for `Concat[X, Y]` */ infix type ++[X <: Tuple, +Y <: Tuple] = Concat[X, Y] - /** The index of `Y` in tuple `X` as a literal constant Int, - * or `Size[X]` if `Y` is disjoint from all element types in `X`. + /** The type of the tuple consisting of all element values of + * tuple `X` zipped with corresponding elements of tuple `Y`. + * If the two tuples have different sizes, + * the extra elements of the larger tuple will be disregarded. + * For example, if + * ``` + * X = (S1, ..., Si) + * Y = (T1, ..., Tj) where j >= i + * ``` + * then + * ``` + * Zip[X, Y] = ((S1, T1), ..., (Si, Ti)) + * ``` + * @syntax markdown */ - type IndexOf[X <: Tuple, Y] <: Int = X match - case Y *: _ => 0 - case _ *: xs => S[IndexOf[xs, Y]] - case EmptyTuple => 0 - - /** Fold a tuple `(T1, ..., Tn)` into `F[T1, F[... F[Tn, Z]...]]]` */ - type Fold[X <: Tuple, Z, F[_, _]] = X match - case EmptyTuple => Z - case x *: xs => F[x, Fold[xs, Z, F]] + type Zip[X <: Tuple, Y <: Tuple] <: Tuple = (X, Y) match + case (x *: xs, y *: ys) => (x, y) *: Zip[xs, ys] + case (EmptyTuple, _) => EmptyTuple + case (_, EmptyTuple) => EmptyTuple /** The type of tuple `X` mapped with the type-level function `F`. * If `X = (T1, ..., Ti)` then `Map[X, F] = `(F[T1], ..., F[Ti])`. @@ -214,6 +221,18 @@ object Tuple: case EmptyTuple => EmptyTuple case x *: xs => F[x] *: Map[xs, F] + /** Converts a tuple `(F[T1], ..., F[Tn])` to `(T1, ... Tn)` */ + type InverseMap[X <: Tuple, F[_]] <: Tuple = X match + case F[x] *: xs => x *: InverseMap[xs, F] + case EmptyTuple => EmptyTuple + + /** Implicit evidence. IsMappedBy[F][X] is present in the implicit scope iff + * X is a tuple for which each element's type is constructed via `F`. E.g. + * (F[A1], ..., F[An]), but not `(F[A1], B2, ..., F[An])` where B2 does not + * have the shape of `F[A]`. + */ + type IsMappedBy[F[_]] = [X <: Tuple] =>> X =:= Map[InverseMap[X, F], F] + /** The type of tuple `X` flat-mapped with the type-level function `F`. * If `X = (T1, ..., Ti)` then `FlatMap[X, F] = `F[T1] ++ ... ++ F[Ti]` */ @@ -241,44 +260,6 @@ object Tuple: case true => x *: Filter[xs, P] case false => Filter[xs, P] - /** A tuple consisting of those indices `N` of tuple `X` where the predicate `P` - * is true for `Elem[X, N]`. Indices are type level values <: Int. - */ - type IndicesWhere[X <: Tuple, P[_ <: Union[X]] <: Boolean] = - helpers.IndicesWhereHelper[X, P, 0] - - /** The type of the tuple consisting of all element values of - * tuple `X` zipped with corresponding elements of tuple `Y`. - * If the two tuples have different sizes, - * the extra elements of the larger tuple will be disregarded. - * For example, if - * ``` - * X = (S1, ..., Si) - * Y = (T1, ..., Tj) where j >= i - * ``` - * then - * ``` - * Zip[X, Y] = ((S1, T1), ..., (Si, Ti)) - * ``` - * @syntax markdown - */ - type Zip[X <: Tuple, Y <: Tuple] <: Tuple = (X, Y) match - case (x *: xs, y *: ys) => (x, y) *: Zip[xs, ys] - case (EmptyTuple, _) => EmptyTuple - case (_, EmptyTuple) => EmptyTuple - - /** Converts a tuple `(F[T1], ..., F[Tn])` to `(T1, ... Tn)` */ - type InverseMap[X <: Tuple, F[_]] <: Tuple = X match - case F[x] *: xs => x *: InverseMap[xs, F] - case EmptyTuple => EmptyTuple - - /** Implicit evidence. IsMappedBy[F][X] is present in the implicit scope iff - * X is a tuple for which each element's type is constructed via `F`. E.g. - * (F[A1], ..., F[An]), but not `(F[A1], B2, ..., F[An])` where B2 does not - * have the shape of `F[A]`. - */ - type IsMappedBy[F[_]] = [X <: Tuple] =>> X =:= Map[InverseMap[X, F], F] - /** A tuple with the elements of tuple `X` in reversed order */ type Reverse[X <: Tuple] = ReverseOnto[X, EmptyTuple] @@ -287,11 +268,30 @@ object Tuple: case x *: xs => ReverseOnto[xs, x *: Acc] case EmptyTuple => Acc + /** Fold a tuple `(T1, ..., Tn)` into `F[T1, F[... F[Tn, Z]...]]]` */ + type Fold[X <: Tuple, Z, F[_, _]] = X match + case EmptyTuple => Z + case x *: xs => F[x, Fold[xs, Z, F]] + /** Given a tuple `(T1, ..., Tn)`, returns a union of its * member types: `T1 | ... | Tn`. Returns `Nothing` if the tuple is empty. */ type Union[T <: Tuple] = Fold[T, Nothing, [x, y] =>> x | y] + /** The index of `Y` in tuple `X` as a literal constant Int, + * or `Size[X]` if `Y` is disjoint from all element types in `X`. + */ + type IndexOf[X <: Tuple, Y] <: Int = X match + case Y *: _ => 0 + case _ *: xs => S[IndexOf[xs, Y]] + case EmptyTuple => 0 + + /** A tuple consisting of those indices `N` of tuple `X` where the predicate `P` + * is true for `Elem[X, N]`. Indices are type level values <: Int. + */ + type IndicesWhere[X <: Tuple, P[_ <: Union[X]] <: Boolean] = + helpers.IndicesWhereHelper[X, P, 0] + /** A type level Boolean indicating whether the tuple `X` has an element * that matches `Y`. * @pre The elements of `X` are assumed to be singleton types From 03509b8c09c519cc9fc8e5d9479ce939d23642bc Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Mon, 29 Apr 2024 15:34:25 +0200 Subject: [PATCH 62/68] Drop braces remaining at term level --- library/src/scala/Tuple.scala | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 7f9e220b2bf2..a9401ea1be7e 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -246,10 +246,9 @@ object Tuple: * constant `true`. A predicate `P[X]` is a type that can be either `true` * or `false`. For example: * ```scala - * type IsString[x] <: Boolean = x match { + * type IsString[x] <: Boolean = x match * case String => true * case _ => false - * } * summon[Tuple.Filter[(1, "foo", 2, "bar"), IsString] =:= ("foo", "bar")] * ``` * @syntax markdown @@ -325,26 +324,21 @@ object Tuple: fromArray(xs, xs.length) /** Convert the first `n` elements of an array into a tuple of unknown arity and types */ - def fromArray[T](xs: Array[T], n: Int): Tuple = { - val xs2 = xs match { + def fromArray[T](xs: Array[T], n: Int): Tuple = + val xs2 = xs match case xs: Array[Object] => xs case xs => xs.map(_.asInstanceOf[Object]) - } runtime.Tuples.fromArray(xs2, n) - } /** Convert an immutable array into a tuple of unknown arity and types */ def fromIArray[T](xs: IArray[T]): Tuple = fromIArray(xs, xs.length) /** Convert the first `n` elements of an immutable array into a tuple of unknown arity and types */ - def fromIArray[T](xs: IArray[T], n: Int): Tuple = { - val xs2: IArray[Object] = xs match { + def fromIArray[T](xs: IArray[T], n: Int): Tuple = + val xs2: IArray[Object] = xs match case xs: IArray[Object] @unchecked => xs - case _ => - xs.map(_.asInstanceOf[Object]) - } + case _ => xs.map(_.asInstanceOf[Object]) runtime.Tuples.fromIArray(xs2, n) - } /** Convert a Product into a tuple of unknown arity and types */ def fromProduct(product: Product): Tuple = @@ -394,9 +388,8 @@ end Tuple type EmptyTuple = EmptyTuple.type /** A tuple of 0 elements. */ -case object EmptyTuple extends Tuple { +case object EmptyTuple extends Tuple: override def toString(): String = "()" -} /** Tuple of arbitrary non-zero arity */ sealed trait NonEmptyTuple extends Tuple @@ -404,6 +397,5 @@ sealed trait NonEmptyTuple extends Tuple @showAsInfix sealed abstract class *:[+H, +T <: Tuple] extends NonEmptyTuple -object *: { +object `*:`: def unapply[H, T <: Tuple](x: H *: T): (H, T) = (x.head, x.tail) -} From 9047ac3d216b7231e4a3233de63a5afc4ce4e318 Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Wed, 1 May 2024 14:09:50 +0200 Subject: [PATCH 63/68] Fix `def indexOf` to return the size instead of -1 --- library/src/scala/Tuple.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index a9401ea1be7e..92460c078b54 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -350,13 +350,14 @@ object Tuple: // Also note it would be unsound to use a type parameter for `y` in the type level // operations, since they are rightfully not covariant in their second parameter. - /** The index (starting at 0) of the first occurrence of y.type in the type `X` of `x` - * or Size[X] if no such element exists. + /** The index (starting at 0) of the first occurrence of `y` in `x` + * or its size if no such element exists. */ inline def indexOf(y: Any): IndexOf[X, y.type] = - x.productIterator.indexOf(y).asInstanceOf[IndexOf[X, y.type]] + val i = x.productIterator.indexOf(y) + (if i >= 0 then i else x.size).asInstanceOf[IndexOf[X, y.type]] - /** A boolean indicating whether there is an element `y.type` in the type `X` of `x` */ + /** A boolean indicating whether `x` contains the element `y` */ inline def contains(y: Any): Contains[X, y.type] = x.productIterator.contains(y).asInstanceOf[Contains[X, y.type]] From 579e14ab8bdb1a7e522ca4a5eb9fe87751a3098f Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Wed, 1 May 2024 18:23:16 +0200 Subject: [PATCH 64/68] Revert doing tuple runtime operations based on a term level predicates --- library/src/scala/Tuple.scala | 32 +++++++++++-------------- tests/pos/named-tuples-strawman-2.scala | 5 +--- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 92460c078b54..364124481db7 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -104,10 +104,15 @@ sealed trait Tuple extends Product: inline def map[F[_]](f: [t] => t => F[t]): Map[this.type, F] = runtime.Tuples.map(this, f).asInstanceOf[Map[this.type, F]] - /** A tuple consisting of all elements of this tuple that satisfy the predicate `p`. */ - inline def filter[This >: this.type <: Tuple, P[_ <: Union[This]] <: Boolean] - (p: (x: Union[This]) => P[x.type]): Filter[This, P] = - val arr = this.toArray.filter(x => p(x.asInstanceOf[Union[This]])) + /** A tuple consisting of all elements of this tuple that have types + * for which the given type level predicate `P` reduces to the literal + * constant `true`. + */ + inline def filter[This >: this.type <: Tuple, P[_ <: Union[This]] <: Boolean]: Filter[This, P] = + val toInclude = constValueTuple[IndicesWhere[This, P]].toArray + val arr = new Array[Object](toInclude.length) + for i <- toInclude.indices do + arr(i) = this.productElement(toInclude(i).asInstanceOf[Int]).asInstanceOf[Object] Tuple.fromArray(arr).asInstanceOf[Filter[This, P]] /** Given a tuple `(a1, ..., am)`, returns the reversed tuple `(am, ..., a1)` @@ -345,23 +350,14 @@ object Tuple: runtime.Tuples.fromProduct(product) extension [X <: Tuple](inline x: X) - // Note the two methods are not equivalent to using `constValue`, - // since they also allow cases unknown at compiletime. - // Also note it would be unsound to use a type parameter for `y` in the type level - // operations, since they are rightfully not covariant in their second parameter. - /** The index (starting at 0) of the first occurrence of `y` in `x` - * or its size if no such element exists. + /** The index (starting at 0) of the first occurrence of `y.type` in the type `X` of `x` + * or `Size[X]` if no such element exists. */ - inline def indexOf(y: Any): IndexOf[X, y.type] = - val i = x.productIterator.indexOf(y) - (if i >= 0 then i else x.size).asInstanceOf[IndexOf[X, y.type]] - - /** A boolean indicating whether `x` contains the element `y` */ - inline def contains(y: Any): Contains[X, y.type] = - x.productIterator.contains(y).asInstanceOf[Contains[X, y.type]] + inline def indexOf(y: Any): IndexOf[X, y.type] = constValue[IndexOf[X, y.type]] - // TODO indexOfType & containsType ? + /** A boolean indicating whether there is an element `y.type` in the type `X` of `x` */ + inline def contains(y: Any): Contains[X, y.type] = constValue[Contains[X, y.type]] end extension diff --git a/tests/pos/named-tuples-strawman-2.scala b/tests/pos/named-tuples-strawman-2.scala index 7cd763bb7b00..4b32dd83f2eb 100644 --- a/tests/pos/named-tuples-strawman-2.scala +++ b/tests/pos/named-tuples-strawman-2.scala @@ -60,10 +60,7 @@ object TupleOps: case EmptyTuple => X inline def concatDistinct[X <: Tuple, Y <: Tuple](xs: X, ys: Y): ConcatDistinct[X, Y] = - // Note the type parameter is needed due to the invariance of compiletime.ops.boolean.! - extension [B <: Boolean](self: B) def negated: ![B] = (!self).asInstanceOf - val ysDistinct = ys.filter[Y, [y] =>> ![Contains[X, y]]](xs.contains(_).negated) - (xs ++ ysDistinct).asInstanceOf[ConcatDistinct[X, Y]] + (xs ++ ys.filter[Y, [Elem] =>> ![Contains[X, Elem]]]).asInstanceOf[ConcatDistinct[X, Y]] object NamedTupleDecomposition: import NamedTupleOps.* From 8d6fa37248efb27920c4ac16e129c63dda0c46a4 Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Wed, 1 May 2024 18:51:40 +0200 Subject: [PATCH 65/68] Make named-tuples-strawman-2.scala a run test The tuple term level definitions were not being tested before --- compiler/test/dotc/pos-test-pickling.blacklist | 1 - compiler/test/dotc/run-test-pickling.blacklist | 1 + tests/{pos => run}/named-tuples-strawman-2.scala | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename tests/{pos => run}/named-tuples-strawman-2.scala (100%) diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.blacklist index 94e510e04396..a856a5b84d92 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -67,7 +67,6 @@ mt-redux-norm.perspective.scala i18211.scala 10867.scala named-tuples1.scala -named-tuples-strawman-2.scala # Opaque type i5720.scala diff --git a/compiler/test/dotc/run-test-pickling.blacklist b/compiler/test/dotc/run-test-pickling.blacklist index 954a64db1b66..dacbc63bb520 100644 --- a/compiler/test/dotc/run-test-pickling.blacklist +++ b/compiler/test/dotc/run-test-pickling.blacklist @@ -45,4 +45,5 @@ t6138-2 i12656.scala trait-static-forwarder i17255 +named-tuples-strawman-2.scala diff --git a/tests/pos/named-tuples-strawman-2.scala b/tests/run/named-tuples-strawman-2.scala similarity index 100% rename from tests/pos/named-tuples-strawman-2.scala rename to tests/run/named-tuples-strawman-2.scala From 1e29c4f3ffa2257eee7a691f7910edca97d3bda2 Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Thu, 2 May 2024 16:59:05 +0200 Subject: [PATCH 66/68] import language.experimental.namedTuples in pos/fieldsOf.scala --- tests/pos/fieldsOf.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/pos/fieldsOf.scala b/tests/pos/fieldsOf.scala index 08f20a1f7e8e..2594dae2cbf7 100644 --- a/tests/pos/fieldsOf.scala +++ b/tests/pos/fieldsOf.scala @@ -1,3 +1,5 @@ +import language.experimental.namedTuples + case class Person(name: String, age: Int) type PF = NamedTuple.From[Person] From 0c89c9226b11923de22ae3922aef10275b1d3e04 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 7 May 2024 09:47:52 +0200 Subject: [PATCH 67/68] Undo all unnecessary changes to Tuples. Keep only the changes we need for making NamedTuple work properly. We still keep operations Contains and Disjoint in Tuple and make Reverse standard. We remove or amend tests that relied on the changes to Tuple. No tests about NamedTuple's are affected. --- library/src/scala/Tuple.scala | 414 ++++++++++-------------- library/src/scala/runtime/Tuples.scala | 21 +- tests/neg/print-tuple-union.check | 2 +- tests/neg/wildcard-match.check | 7 +- tests/pos/tuple-filter.scala | 3 - tests/run/named-tuples-strawman-2.scala | 60 +++- 6 files changed, 249 insertions(+), 258 deletions(-) diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 364124481db7..8074fe3664e5 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -5,7 +5,7 @@ import compiletime.* import compiletime.ops.int.* /** Tuple of arbitrary arity */ -sealed trait Tuple extends Product: +sealed trait Tuple extends Product { import Tuple.* /** Create a copy of this tuple as an Array */ @@ -21,43 +21,43 @@ sealed trait Tuple extends Product: 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] = + 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) */ - inline def *: [H, This >: this.type <: Tuple](x: H): H *: This = + inline def *: [H, This >: this.type <: Tuple] (x: H): H *: This = runtime.Tuples.cons(x, this).asInstanceOf[H *: This] - /** Return a copy of `this` tuple with an element appended */ - inline def :* [This >: this.type <: Tuple, L](x: L): This :* L = - runtime.Tuples.append(x, this).asInstanceOf[Append[This, L]] + /** Return a new tuple by concatenating `this` tuple with `that` tuple. + * This operation is O(this.size + that.size) + */ + 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 */ inline def size[This >: this.type <: Tuple]: Size[This] = runtime.Tuples.size(this).asInstanceOf[Size[This]] - /** Get the i-th element of this tuple. - * Equivalent to productElement but with a precise return 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` */ - inline def apply[This >: this.type <: Tuple](n: Int): Elem[This, n.type] = - runtime.Tuples.apply(this, n).asInstanceOf[Elem[This, n.type]] - - /** Get the head of this tuple */ - inline def head[This >: this.type <: Tuple]: Head[This] = - runtime.Tuples.apply(this, 0).asInstanceOf[Head[This]] + inline def zip[This >: this.type <: Tuple, T2 <: Tuple](t2: T2): Zip[This, T2] = + runtime.Tuples.zip(this, t2).asInstanceOf[Zip[This, T2]] - /** Get the tail of this tuple. - * This operation is O(this.size) + /** 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 tail[This >: this.type <: Tuple]: Tail[This] = - runtime.Tuples.tail(this).asInstanceOf[Tail[This]] - - /** Get the last of this tuple */ - inline def last[This >: this.type <: Tuple]: Last[This] = - runtime.Tuples.last(this).asInstanceOf[Last[This]] - - /** Get the initial part of the tuple without its last element */ - inline def init[This >: this.type <: Tuple]: Init[This] = - runtime.Tuples.init(this).asInstanceOf[Init[This]] + 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. @@ -65,6 +65,7 @@ sealed trait Tuple extends Product: 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. */ @@ -78,158 +79,118 @@ sealed trait Tuple extends Product: inline def splitAt[This >: this.type <: Tuple](n: Int): Split[This, n.type] = runtime.Tuples.splitAt(this, n).asInstanceOf[Split[This, n.type]] - /** Return a new tuple by concatenating `this` tuple with `that` tuple. - * This operation is O(this.size + that.size) - */ - // Contrarily to `this`, `that` does not need a type parameter - // since `++` is covariant in its second argument. - inline def ++ [This >: this.type <: Tuple](that: Tuple): This ++ that.type = - runtime.Tuples.concat(this, that).asInstanceOf[Concat[This, that.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` - */ - inline def zip[This >: this.type <: Tuple, T2 <: Tuple](t2: T2): Zip[This, T2] = - runtime.Tuples.zip(this, t2).asInstanceOf[Zip[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] = - runtime.Tuples.map(this, f).asInstanceOf[Map[this.type, F]] - - /** A tuple consisting of all elements of this tuple that have types - * for which the given type level predicate `P` reduces to the literal - * constant `true`. - */ - inline def filter[This >: this.type <: Tuple, P[_ <: Union[This]] <: Boolean]: Filter[This, P] = - val toInclude = constValueTuple[IndicesWhere[This, P]].toArray - val arr = new Array[Object](toInclude.length) - for i <- toInclude.indices do - arr(i) = this.productElement(toInclude(i).asInstanceOf[Int]).asInstanceOf[Object] - Tuple.fromArray(arr).asInstanceOf[Filter[This, P]] - /** Given a tuple `(a1, ..., am)`, returns the reversed tuple `(am, ..., a1)` * consisting all its elements. */ inline def reverse[This >: this.type <: Tuple]: Reverse[This] = runtime.Tuples.reverse(this).asInstanceOf[Reverse[This]] +} - /** A tuple with the elements of this tuple in reversed order added in front of `acc` */ - inline def reverseOnto[This >: this.type <: Tuple, Acc <: Tuple](acc: Acc): ReverseOnto[This, Acc] = - (this.reverse ++ acc).asInstanceOf[ReverseOnto[This, Acc]] - -object Tuple: +object Tuple { /** Type of a tuple with an element appended */ - type Append[X <: Tuple, +Y] <: NonEmptyTuple = X match + type Append[X <: Tuple, Y] <: NonEmptyTuple = X match { case EmptyTuple => Y *: EmptyTuple case x *: xs => x *: Append[xs, Y] + } - /** An infix shorthand for `Append[X, Y]` */ - infix type :*[X <: Tuple, +Y] = Append[X, Y] - - /** The size of a tuple, represented as a literal constant subtype of Int */ - type Size[X <: Tuple] <: Int = X match - case EmptyTuple => 0 - case _ *: xs => S[Size[xs]] - - /** The type of the element at position N in the tuple X */ - type Elem[X <: Tuple, N <: Int] = X match - case x *: xs => N match - case 0 => x - case S[n1] => Elem[xs, n1] - - /** The type of the first element of a tuple */ - // Only bounded by `<: Tuple` not `<: NonEmptyTuple` - // even though it only matches non-empty tuples. - // Avoids bounds check failures from an irreducible type - // like `Tuple.Head[Tuple.Tail[X]]` - // Other types that don't reduce for empty tuples follow the same principle. - type Head[X <: Tuple] = X match + /** Type of the head of a tuple */ + type Head[X <: Tuple] = X match { case x *: _ => x + } - /** The type of a tuple consisting of all elements of tuple X except the first one */ - type Tail[X <: Tuple] <: Tuple = X match + /** 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] + } + + /** Type of the tail of a tuple */ + type Tail[X <: Tuple] <: Tuple = X match { case _ *: xs => xs + } - /** The type of the last element of a tuple */ - type Last[X <: Tuple] = X match + /** Type of the last element of a tuple */ + type Last[X <: Tuple] = X match { case x *: EmptyTuple => x case _ *: xs => Last[xs] + } - /** The type of the initial part of a tuple without its last element */ - type Init[X <: Tuple] <: Tuple = X match - case _ *: EmptyTuple => EmptyTuple - case x *: xs => x *: Init[xs] - - /** The type of the tuple consisting of the first `N` elements of `X`, - * or all elements if `N` exceeds `Size[X]`. - */ - type Take[X <: Tuple, N <: Int] <: Tuple = N match - case 0 => EmptyTuple - case S[n1] => X match - case EmptyTuple => EmptyTuple - case x *: xs => x *: Take[xs, n1] - - /** The type of the tuple consisting of all elements of `X` except the first `N` ones, - * or no elements if `N` exceeds `Size[X]`. - */ - type Drop[X <: Tuple, N <: Int] <: Tuple = N match - case 0 => X - case S[n1] => X match - case EmptyTuple => EmptyTuple - case _ *: xs => Drop[xs, n1] + /** Type of the concatenation of two tuples */ + type Concat[X <: Tuple, +Y <: Tuple] <: Tuple = X match { + case EmptyTuple => Y + case x1 *: xs1 => x1 *: Concat[xs1, Y] + } + + /** Type of the element at position N in the tuple X */ + type Elem[X <: Tuple, N <: Int] = X match { + case x *: xs => + N match { + case 0 => x + case S[n1] => Elem[xs, n1] + } + } + + /** Literal constant Int size of a tuple */ + type Size[X <: Tuple] <: Int = X match { + case EmptyTuple => 0 + case x *: xs => S[Size[xs]] + } - /** The pair type `(Take(X, N), Drop[X, N]). */ - type Split[X <: Tuple, N <: Int] = (Take[X, N], Drop[X, N]) + /** Fold a tuple `(T1, ..., Tn)` into `F[T1, F[... F[Tn, Z]...]]]` */ + type Fold[Tup <: Tuple, Z, F[_, _]] = Tup match + case EmptyTuple => Z + case h *: t => F[h, Fold[t, Z, F]] - /** Type of the concatenation of two tuples `X` and `Y` */ - // Can be covariant in `Y` since it never appears as a match type scrutinee. - type Concat[X <: Tuple, +Y <: Tuple] <: Tuple = X match - case EmptyTuple => Y - case x *: xs => x *: Concat[xs, Y] + /** Converts a tuple `(T1, ..., Tn)` to `(F[T1], ..., F[Tn])` */ + type Map[Tup <: Tuple, F[_ <: Union[Tup]]] <: Tuple = Tup match { + case EmptyTuple => EmptyTuple + case h *: t => F[h] *: Map[t, F] + } - /** An infix shorthand for `Concat[X, Y]` */ - infix type ++[X <: Tuple, +Y <: Tuple] = Concat[X, Y] + /** Converts a tuple `(T1, ..., Tn)` to a flattened `(..F[T1], ..., ..F[Tn])` */ + type FlatMap[Tup <: Tuple, F[_ <: Union[Tup]] <: Tuple] <: Tuple = Tup match { + case EmptyTuple => EmptyTuple + case h *: t => Concat[F[h], FlatMap[t, F]] + } - /** The type of the tuple consisting of all element values of - * tuple `X` zipped with corresponding elements of tuple `Y`. - * If the two tuples have different sizes, - * the extra elements of the larger tuple will be disregarded. - * For example, if - * ``` - * X = (S1, ..., Si) - * Y = (T1, ..., Tj) where j >= i - * ``` - * then - * ``` - * Zip[X, Y] = ((S1, T1), ..., (Si, Ti)) + /** Filters out those members of the tuple for which the predicate `P` returns `false`. + * A predicate `P[X]` is a type that can be either `true` or `false`. For example: + * ```scala + * type IsString[x] <: Boolean = x match { + * case String => true + * case _ => false + * } + * summon[Tuple.Filter[(1, "foo", 2, "bar"), IsString] =:= ("foo", "bar")] * ``` * @syntax markdown */ - type Zip[X <: Tuple, Y <: Tuple] <: Tuple = (X, Y) match - case (x *: xs, y *: ys) => (x, y) *: Zip[xs, ys] + type Filter[Tup <: Tuple, P[_] <: Boolean] <: Tuple = Tup match { + case EmptyTuple => EmptyTuple + case h *: t => P[h] match { + case true => h *: Filter[t, P] + case false => Filter[t, P] + } + } + + /** Given two tuples, `A1 *: ... *: An * At` and `B1 *: ... *: Bn *: Bt` + * where at least one of `At` or `Bt` is `EmptyTuple` or `Tuple`, + * returns the tuple type `(A1, B1) *: ... *: (An, Bn) *: Ct` + * where `Ct` is `EmptyTuple` if `At` or `Bt` is `EmptyTuple`, 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 (EmptyTuple, _) => EmptyTuple case (_, EmptyTuple) => EmptyTuple - - /** The type of tuple `X` mapped with the type-level function `F`. - * If `X = (T1, ..., Ti)` then `Map[X, F] = `(F[T1], ..., F[Ti])`. - */ - type Map[X <: Tuple, F[_ <: Union[X]]] <: Tuple = X match - case EmptyTuple => EmptyTuple - case x *: xs => F[x] *: Map[xs, F] + case _ => Tuple + } /** Converts a tuple `(F[T1], ..., F[Tn])` to `(T1, ... Tn)` */ - type InverseMap[X <: Tuple, F[_]] <: Tuple = X match - case F[x] *: xs => x *: InverseMap[xs, F] + type InverseMap[X <: Tuple, F[_]] <: Tuple = X match { + case F[x] *: t => x *: InverseMap[t, F] case EmptyTuple => EmptyTuple + } /** Implicit evidence. IsMappedBy[F][X] is present in the implicit scope iff * X is a tuple for which each element's type is constructed via `F`. E.g. @@ -238,64 +199,42 @@ object Tuple: */ type IsMappedBy[F[_]] = [X <: Tuple] =>> X =:= Map[InverseMap[X, F], F] - /** The type of tuple `X` flat-mapped with the type-level function `F`. - * If `X = (T1, ..., Ti)` then `FlatMap[X, F] = `F[T1] ++ ... ++ F[Ti]` - */ - type FlatMap[X <: Tuple, F[_ <: Union[X]] <: Tuple] <: Tuple = X match - case EmptyTuple => EmptyTuple - case x *: xs => Concat[F[x], FlatMap[xs, F]] - // TODO: implement term level analogue + /** Type of the reversed tuple */ + type Reverse[X <: Tuple] = ReverseOnto[X, EmptyTuple] - /** The type of the tuple consisting of all elements of tuple `X` that have types - * for which the given type level predicate `P` reduces to the literal - * constant `true`. A predicate `P[X]` is a type that can be either `true` - * or `false`. For example: - * ```scala - * type IsString[x] <: Boolean = x match - * case String => true - * case _ => false - * summon[Tuple.Filter[(1, "foo", 2, "bar"), IsString] =:= ("foo", "bar")] - * ``` - * @syntax markdown - */ - type Filter[X <: Tuple, P[_ <: Union[X]] <: Boolean] <: Tuple = X match - case EmptyTuple => EmptyTuple - case x *: xs => P[x] match - case true => x *: Filter[xs, P] - case false => Filter[xs, P] + /** Prepends all elements of a tuple in reverse order onto the other tuple */ + type ReverseOnto[From <: Tuple, +To <: Tuple] <: Tuple = From match + case x *: xs => ReverseOnto[xs, x *: To] + case EmptyTuple => To - /** A tuple with the elements of tuple `X` in reversed order */ - type Reverse[X <: Tuple] = ReverseOnto[X, EmptyTuple] + /** Transforms a tuple `(T1, ..., Tn)` into `(T1, ..., Ti)`. */ + type Take[T <: Tuple, N <: Int] <: Tuple = N match { + case 0 => EmptyTuple + case S[n1] => T match { + case EmptyTuple => EmptyTuple + case x *: xs => x *: Take[xs, n1] + } + } - /** A tuple with the elements of tuple `X` in reversed order added in front of `Acc` */ - type ReverseOnto[X <: Tuple, Acc <: Tuple] <: Tuple = X match - case x *: xs => ReverseOnto[xs, x *: Acc] - case EmptyTuple => Acc + /** Transforms a tuple `(T1, ..., Tn)` into `(Ti+1, ..., Tn)`. */ + type Drop[T <: Tuple, N <: Int] <: Tuple = N match { + case 0 => T + case S[n1] => T match { + case EmptyTuple => EmptyTuple + case x *: xs => Drop[xs, n1] + } + } - /** Fold a tuple `(T1, ..., Tn)` into `F[T1, F[... F[Tn, Z]...]]]` */ - type Fold[X <: Tuple, Z, F[_, _]] = X match - case EmptyTuple => Z - case x *: xs => F[x, Fold[xs, Z, F]] + /** Splits a tuple (T1, ..., Tn) into a pair of two tuples `(T1, ..., Ti)` and + * `(Ti+1, ..., Tn)`. + */ + type Split[T <: Tuple, N <: Int] = (Take[T, N], Drop[T, N]) /** Given a tuple `(T1, ..., Tn)`, returns a union of its * member types: `T1 | ... | Tn`. Returns `Nothing` if the tuple is empty. */ type Union[T <: Tuple] = Fold[T, Nothing, [x, y] =>> x | y] - /** The index of `Y` in tuple `X` as a literal constant Int, - * or `Size[X]` if `Y` is disjoint from all element types in `X`. - */ - type IndexOf[X <: Tuple, Y] <: Int = X match - case Y *: _ => 0 - case _ *: xs => S[IndexOf[xs, Y]] - case EmptyTuple => 0 - - /** A tuple consisting of those indices `N` of tuple `X` where the predicate `P` - * is true for `Elem[X, N]`. Indices are type level values <: Int. - */ - type IndicesWhere[X <: Tuple, P[_ <: Union[X]] <: Boolean] = - helpers.IndicesWhereHelper[X, P, 0] - /** A type level Boolean indicating whether the tuple `X` has an element * that matches `Y`. * @pre The elements of `X` are assumed to be singleton types @@ -325,42 +264,28 @@ object Tuple: def unapply(x: EmptyTuple): true = true /** Convert an array into a tuple of unknown arity and types */ - def fromArray[T](xs: Array[T]): Tuple = - fromArray(xs, xs.length) - - /** Convert the first `n` elements of an array into a tuple of unknown arity and types */ - def fromArray[T](xs: Array[T], n: Int): Tuple = - val xs2 = xs match + def fromArray[T](xs: Array[T]): Tuple = { + val xs2 = xs match { case xs: Array[Object] => xs case xs => xs.map(_.asInstanceOf[Object]) - runtime.Tuples.fromArray(xs2, n) + } + runtime.Tuples.fromArray(xs2) + } /** Convert an immutable array into a tuple of unknown arity and types */ - def fromIArray[T](xs: IArray[T]): Tuple = fromIArray(xs, xs.length) - - /** Convert the first `n` elements of an immutable array into a tuple of unknown arity and types */ - def fromIArray[T](xs: IArray[T], n: Int): Tuple = - val xs2: IArray[Object] = xs match + def fromIArray[T](xs: IArray[T]): Tuple = { + val xs2: IArray[Object] = xs match { case xs: IArray[Object] @unchecked => xs - case _ => xs.map(_.asInstanceOf[Object]) - runtime.Tuples.fromIArray(xs2, n) + case _ => + xs.map(_.asInstanceOf[Object]) + } + runtime.Tuples.fromIArray(xs2) + } /** Convert a Product into a tuple of unknown arity and types */ def fromProduct(product: Product): Tuple = runtime.Tuples.fromProduct(product) - extension [X <: Tuple](inline x: X) - - /** The index (starting at 0) of the first occurrence of `y.type` in the type `X` of `x` - * or `Size[X]` if no such element exists. - */ - inline def indexOf(y: Any): IndexOf[X, y.type] = constValue[IndexOf[X, y.type]] - - /** A boolean indicating whether there is an element `y.type` in the type `X` of `x` */ - inline def contains(y: Any): Contains[X, y.type] = constValue[Contains[X, y.type]] - - end extension - def fromProductTyped[P <: Product](p: P)(using m: scala.deriving.Mirror.ProductOf[P]): m.MirroredElemTypes = runtime.Tuples.fromProduct(p).asInstanceOf[m.MirroredElemTypes] @@ -368,31 +293,48 @@ object Tuple: given canEqualTuple[H1, T1 <: Tuple, H2, T2 <: Tuple]( using eqHead: CanEqual[H1, H2], eqTail: CanEqual[T1, T2] ): CanEqual[H1 *: T1, H2 *: T2] = CanEqual.derived - - private object helpers: - - /** Used to implement IndicesWhere */ - type IndicesWhereHelper[X <: Tuple, P[_ <: Union[X]] <: Boolean, N <: Int] <: Tuple = X match - case EmptyTuple => EmptyTuple - case h *: t => P[h] match - case true => N *: IndicesWhereHelper[t, P, S[N]] - case false => IndicesWhereHelper[t, P, S[N]] - - end helpers -end Tuple +} /** A tuple of 0 elements */ type EmptyTuple = EmptyTuple.type /** A tuple of 0 elements. */ -case object EmptyTuple extends Tuple: +case object EmptyTuple extends Tuple { override def toString(): String = "()" +} /** Tuple of arbitrary non-zero arity */ -sealed trait NonEmptyTuple extends Tuple +sealed trait NonEmptyTuple extends Tuple { + import Tuple.* + + /** Get the i-th element of this tuple. + * Equivalent to productElement but with a precise return type. + */ + 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 */ + 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 */ + inline def init[This >: this.type <: NonEmptyTuple]: Init[This] = + runtime.Tuples.init(this).asInstanceOf[Init[This]] + + /** Get the last of this tuple */ + 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) + */ + inline def tail[This >: this.type <: NonEmptyTuple]: Tail[This] = + runtime.Tuples.tail(this).asInstanceOf[Tail[This]] +} @showAsInfix sealed abstract class *:[+H, +T <: Tuple] extends NonEmptyTuple -object `*:`: +object *: { def unapply[H, T <: Tuple](x: H *: T): (H, T) = (x.head, x.tail) +} diff --git a/library/src/scala/runtime/Tuples.scala b/library/src/scala/runtime/Tuples.scala index 8da21c777943..efb54c54d50b 100644 --- a/library/src/scala/runtime/Tuples.scala +++ b/library/src/scala/runtime/Tuples.scala @@ -1,7 +1,5 @@ package scala.runtime -import scala.annotation.experimental - object Tuples { inline val MaxSpecialized = 22 @@ -28,7 +26,7 @@ object Tuples { arr } - def fromArray(xs: Array[Object], n: Int): Tuple = n match { + def fromArray(xs: Array[Object]): Tuple = xs.length match { case 0 => EmptyTuple case 1 => Tuple1(xs(0)) case 2 => Tuple2(xs(0), xs(1)) @@ -55,15 +53,10 @@ object Tuples { case _ => TupleXXL.fromIArray(xs.clone().asInstanceOf[IArray[Object]]).asInstanceOf[Tuple] } - def fromArray(xs: Array[Object]): Tuple = fromArray(xs, xs.length) - - def fromIArray(xs: IArray[Object], n: Int): Tuple = - if n <= 22 || n != xs.length - then fromArray(xs.asInstanceOf[Array[Object]], n) + def fromIArray(xs: IArray[Object]): Tuple = + if (xs.length <= 22) fromArray(xs.asInstanceOf[Array[Object]]) else TupleXXL.fromIArray(xs).asInstanceOf[Tuple] - def fromIArray(xs: IArray[Object]): Tuple = fromIArray(xs, xs.length) - def fromProduct(xs: Product): Tuple = (xs.productArity match { case 0 => EmptyTuple case 1 => @@ -357,7 +350,7 @@ object Tuples { } } - def tail(self: Tuple): Tuple = (self: Any) match { + def tail(self: NonEmptyTuple): Tuple = (self: Any) match { case xxl: TupleXXL => xxlTail(xxl) case _ => specialCaseTail(self) } @@ -565,16 +558,16 @@ object Tuples { } } - def init(self: Tuple): Tuple = (self: Any) match { + def init(self: NonEmptyTuple): Tuple = (self: Any) match { case xxl: TupleXXL => xxlInit(xxl) case _ => specialCaseInit(self) } - def last(self: Tuple): Any = (self: Any) match { + def last(self: NonEmptyTuple): Any = (self: Any) match { case self: Product => self.productElement(self.productArity - 1) } - def apply(self: Tuple, n: Int): Any = + def apply(self: NonEmptyTuple, n: Int): Any = self.productElement(n) // Benchmarks showed that this is faster than doing (it1 zip it2).copyToArray(...) diff --git a/tests/neg/print-tuple-union.check b/tests/neg/print-tuple-union.check index 7d2c019de5a6..f3754aa5b17e 100644 --- a/tests/neg/print-tuple-union.check +++ b/tests/neg/print-tuple-union.check @@ -13,6 +13,6 @@ | and cannot be shown to be disjoint from it either. | Therefore, reduction cannot advance to the remaining case | - | case x *: xs => x | Tuple.Fold[xs, Nothing, [x, y] =>> x | y] + | case h *: t => h | Tuple.Fold[t, Nothing, [x, y] =>> x | y] | | longer explanation available when compiling with `-explain` diff --git a/tests/neg/wildcard-match.check b/tests/neg/wildcard-match.check index fd20443c0a9f..d405326c3d2b 100644 --- a/tests/neg/wildcard-match.check +++ b/tests/neg/wildcard-match.check @@ -87,7 +87,8 @@ | trying to reduce shapeless.tuples.length[T2] | trying to reduce Tuple.Size[shapeless.tuples.to[T2]] | failed since selector shapeless.tuples.to[T2] - | does not uniquely determine parameter xs in - | case _ *: xs => scala.compiletime.ops.int.S[Tuple.Size[xs]] - | The computed bounds for the parameter are: + | does not uniquely determine parameters x, xs in + | case x *: xs => scala.compiletime.ops.int.S[Tuple.Size[xs]] + | The computed bounds for the parameters are: + | x <: Int | xs <: (Int, Int) diff --git a/tests/pos/tuple-filter.scala b/tests/pos/tuple-filter.scala index 0964d2e982d9..2c9638b2e47b 100644 --- a/tests/pos/tuple-filter.scala +++ b/tests/pos/tuple-filter.scala @@ -8,6 +8,3 @@ def Test = summon[Tuple.Filter[(1, 2, 3, 4), P] =:= (1, 2, 4)] summon[Tuple.Filter[(1, 2, 3, 4), RejectAll] =:= EmptyTuple] summon[Tuple.Filter[EmptyTuple, P] =:= EmptyTuple] - - import compiletime.ops.int.< - summon[Tuple.Filter[(1, 4, 7, 2, 10, 3, 4), [X <: Int] =>> X < 5] =:= (1, 4, 2, 3, 4)] diff --git a/tests/run/named-tuples-strawman-2.scala b/tests/run/named-tuples-strawman-2.scala index 4b32dd83f2eb..95f37ad23a93 100644 --- a/tests/run/named-tuples-strawman-2.scala +++ b/tests/run/named-tuples-strawman-2.scala @@ -5,6 +5,53 @@ import Tuple.* object TupleOps: + private object helpers: + + /** Used to implement IndicesWhere */ + type IndicesWhereHelper[X <: Tuple, P[_ <: Union[X]] <: Boolean, N <: Int] <: Tuple = X match + case EmptyTuple => EmptyTuple + case h *: t => P[h] match + case true => N *: IndicesWhereHelper[t, P, S[N]] + case false => IndicesWhereHelper[t, P, S[N]] + + end helpers + + /** A type level Boolean indicating whether the tuple `X` has an element + * that matches `Y`. + * @pre The elements of `X` are assumed to be singleton types + */ + type Contains[X <: Tuple, Y] <: Boolean = X match + case Y *: _ => true + case _ *: xs => Contains[xs, Y] + case EmptyTuple => false + + /** The index of `Y` in tuple `X` as a literal constant Int, + * or `Size[X]` if `Y` is disjoint from all element types in `X`. + */ + type IndexOf[X <: Tuple, Y] <: Int = X match + case Y *: _ => 0 + case _ *: xs => S[IndexOf[xs, Y]] + case EmptyTuple => 0 + + /** A tuple consisting of those indices `N` of tuple `X` where the predicate `P` + * is true for `Elem[X, N]`. Indices are type level values <: Int. + */ + type IndicesWhere[X <: Tuple, P[_ <: Union[X]] <: Boolean] = + helpers.IndicesWhereHelper[X, P, 0] + + extension [X <: Tuple](inline x: X) + + /** The index (starting at 0) of the first occurrence of `y.type` in the type `X` of `x` + * or `Size[X]` if no such element exists. + */ + inline def indexOf(y: Any): IndexOf[X, y.type] = constValue[IndexOf[X, y.type]] + + /** A boolean indicating whether there is an element `y.type` in the type `X` of `x` */ + inline def contains(y: Any): Contains[X, y.type] = constValue[Contains[X, y.type]] + + end extension + + /** The `X` tuple, with its element at index `N` replaced by `Y`. * If `N` is equal to `Size[X]`, the element `Y` is appended instead */ @@ -60,7 +107,18 @@ object TupleOps: case EmptyTuple => X inline def concatDistinct[X <: Tuple, Y <: Tuple](xs: X, ys: Y): ConcatDistinct[X, Y] = - (xs ++ ys.filter[Y, [Elem] =>> ![Contains[X, Elem]]]).asInstanceOf[ConcatDistinct[X, Y]] + (xs ++ filter[Y, [Elem] =>> ![Contains[X, Elem]]](ys)).asInstanceOf[ConcatDistinct[X, Y]] + + /** A tuple consisting of all elements of this tuple that have types + * for which the given type level predicate `P` reduces to the literal + * constant `true`. + */ + inline def filter[X <: Tuple, P[_] <: Boolean](xs: X): Filter[X, P] = + val toInclude = constValueTuple[IndicesWhere[X, P]].toArray + val arr = new Array[Object](toInclude.length) + for i <- toInclude.indices do + arr(i) = xs.productElement(toInclude(i).asInstanceOf[Int]).asInstanceOf[Object] + Tuple.fromArray(arr).asInstanceOf[Filter[X, P]] object NamedTupleDecomposition: import NamedTupleOps.* From f80a8ddfa1afb5a4a81f33e8297f8aac068161ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 6 May 2024 18:19:49 +0200 Subject: [PATCH 68/68] Revert the addition of `type Fields` in `Selectable`. If we add it as is now, we will *not* be able to add the bound in a future release. It is best to leave it completely undefined. The typer is happy to ignore the case where it does not exist at all. --- library/src/scala/Selectable.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/src/scala/Selectable.scala b/library/src/scala/Selectable.scala index 93c799dd124b..74004a350679 100644 --- a/library/src/scala/Selectable.scala +++ b/library/src/scala/Selectable.scala @@ -22,8 +22,7 @@ package scala * In this case the call will synthesize `Class` arguments for the erasure of * all formal parameter types of the method in the structural type. */ -trait Selectable extends Any: - type Fields // TODO: add <: NamedTyple.AnyNamedTuple when NamedTuple is no longer experimental +trait Selectable extends Any object Selectable: /* Scala 2 compat + allowing for cross-compilation: