From 6c0d6eb299373c4ad1b4aa0dc567a7be9b9e4423 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 13 Dec 2023 12:25:25 +0100 Subject: [PATCH 01/34] 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 111c8c0bada3..fb8b34211bb6 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -91,12 +91,6 @@ val experimentalDefinitionInLibrary = Set( "scala.quoted.Quotes.reflectModule.MethodTypeMethods.hasErasedParams", "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 ) From 4ce52c181fc3b4864044eedea1f410adf2208ecd Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 13 Dec 2023 14:54:23 +0100 Subject: [PATCH 02/34] 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 f3a77413b5a8e4951c03aaa141073629b8e74947 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 13 Dec 2023 15:00:11 +0100 Subject: [PATCH 03/34] 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 c132083d64ac4c1b95559b1de962c912190a3d19 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 13 Dec 2023 17:52:24 +0100 Subject: [PATCH 04/34] 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 e5def8e9facc8025ab8439b779c00f101ddd0d87 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 13 Dec 2023 17:53:15 +0100 Subject: [PATCH 05/34] 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 f7cf7c03e1e8600fb7ddd36c9d458bb0a171498b Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 2 Dec 2023 19:41:52 +0100 Subject: [PATCH 06/34] 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 fb8b34211bb6..3aee6badf552 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -91,6 +91,10 @@ val experimentalDefinitionInLibrary = Set( "scala.quoted.Quotes.reflectModule.MethodTypeMethods.hasErasedParams", "scala.quoted.Quotes.reflectModule.TermParamClauseMethods.erasedArgs", "scala.quoted.Quotes.reflectModule.TermParamClauseMethods.hasErasedArgs", + + // New feature: named tuples + "scala.NamedTuple", + "scala.NamedTuple$", ) From 467c765309a0bcc1696e8e3f2a145c785d771518 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 3 Dec 2023 13:15:24 +0100 Subject: [PATCH 07/34] 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 | 123 ++++++------ .../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 | 136 ++++++------- .../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, 1186 insertions(+), 207 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 9591bc5a93f0..6994e1389064 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 @@ -248,7 +248,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 @@ -1455,22 +1455,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 @@ -1989,7 +2068,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 5ded0e1262e4..b60349dd9b9b 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 aabfdd97d7bd..28c6d7edacb3 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -529,15 +529,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 2798828ad9a7..0a9e1aff9053 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -32,6 +32,7 @@ object Feature: val pureFunctions = experimental("pureFunctions") val captureChecking = experimental("captureChecking") val into = experimental("into") + val namedTuples = experimental("namedTuples") val globalOnlyImports: Set[TermName] = Set(pureFunctions, captureChecking) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 3cde29ee3d79..31f47535015b 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -957,6 +957,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") @@ -1310,6 +1313,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 a2e78add1338..c9e21b4cf604 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 c76b5117dc89..d79f8b4c6393 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) /** The TermRef referring to the companion of the underlying class reference diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 6892dfdd94ca..7b1aea3557ca 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -643,6 +643,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) @@ -1539,6 +1547,7 @@ object Parsers { val start = in.offset var imods = Modifiers() var erasedArgs: ListBuffer[Boolean] = ListBuffer() + def functionRest(params: List[Tree]): Tree = val paramSpan = Span(start, in.lastOffset) atSpan(start, in.offset) { @@ -1585,61 +1594,52 @@ object Parsers { Function(params, resultType) } - var isValParamList = false + 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 val t = - if (in.token == LPAREN) { + if in.token == LPAREN then in.nextToken() - if (in.token == RPAREN) { + if in.token == RPAREN then in.nextToken() functionRest(Nil) - } - else { + else val paramStart = in.offset def addErased() = - erasedArgs.addOne(isErasedKw) - if isErasedKw then { in.skipToken(); } + erasedArgs += isErasedKw + if isErasedKw then in.nextToken() addErased() - val ts = in.currentRegion.withCommasExpected { + var ts = in.currentRegion.withCommasExpected: funArgType() match case Ident(name) if name != tpnme.WILDCARD && in.isColon => - isValParamList = true - def funParam(start: Offset, mods: Modifiers) = { - atSpan(start) { + def funParam(start: Offset, mods: Modifiers) = + atSpan(start): addErased() typedFunParam(in.offset, ident(), imods) - } - } commaSeparatedRest( typedFunParam(paramStart, name.toTermName, imods), () => funParam(in.offset, imods)) case t => - def funParam() = { - addErased() - funArgType() - } + def funParam() = + addErased() + funArgType() commaSeparatedRest(t, funParam) - } accept(RPAREN) - if isValParamList || in.isArrow || isPureArrow then + if in.isArrow || isPureArrow || erasedArgs.contains(true) then functionRest(ts) - else { - val ts1 = ts.mapConserve { t => - if isByNameType(t) then - syntaxError(ByNameParameterNotSupported(t), t.span) - stripByNameType(t) - else - t - } - val tuple = atSpan(start) { makeTupleOrParens(ts1) } + else + val tuple = atSpan(start): + makeTupleOrParens(ts.mapConserve(convertToElem)) infixTypeRest( refinedTypeRest( withTypeRest( annotTypeRest( simpleTypeRest(tuple))))) - } - } - } else if (in.token == LBRACKET) { val start = in.offset val tparams = typeParamClause(ParamOwner.TypeParam) @@ -1921,6 +1921,7 @@ object Parsers { * | Singleton `.' id * | Singleton `.' type * | ‘(’ ArgTypes ‘)’ + * | ‘(’ NamesAndTypes ‘)’ * | Refinement * | TypeSplice -- deprecated syntax (since 3.0.0) * | SimpleType1 TypeArgs @@ -1929,7 +1930,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)) } @@ -2012,32 +2013,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 @@ -2083,7 +2085,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 `}' */ @@ -2659,7 +2661,9 @@ object Parsers { } /** ExprsInParens ::= ExprInParens {`,' ExprInParens} + * | NamedExprInParens {‘,’ NamedExprInParens} * Bindings ::= Binding {`,' Binding} + * NamedExprInParens ::= id '=' ExprInParens */ def exprsInParensOrBindings(): List[Tree] = if in.token == RPAREN then Nil @@ -2669,7 +2673,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) @@ -3023,7 +3027,7 @@ object Parsers { * | Literal * | Quoted * | XmlPattern - * | `(' [Patterns] `)' + * | `(' [Patterns | NamedPatterns] `)' * | SimplePattern1 [TypeArgs] [ArgumentPatterns] * | ‘given’ RefinedType * SimplePattern1 ::= SimpleRef @@ -3074,9 +3078,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 8fc0c568e125..ae18176c9480 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -68,7 +68,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 _ => @@ -120,16 +121,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 eb184cb22bd0..1a88863cef6e 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) ~ "*" @@ -491,7 +502,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 452c6d197310..f89abee84401 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1282,9 +1282,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 @@ -1500,7 +1501,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 } @@ -1516,7 +1520,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 7727c125d1e4..8dc23609b348 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -706,54 +706,63 @@ 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 = 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 = 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 = { @@ -2422,7 +2431,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. @@ -3022,37 +3031,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 b0da78f0a1eb..da72739c0403 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -63,6 +63,8 @@ i17149.scala tuple-fold.scala mt-redux-norm.perspective.scala i18211.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 aa8cd15f00a0..914f920a26fe 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -193,7 +193,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) @@ -212,6 +212,8 @@ Refinement ::= :<<< [RefineDef] {semi [RefineDef]} >>> TypeBounds ::= [‘>:’ Type] [‘<:’ Type] TypeBoundsTree(lo, hi) TypeParamBounds ::= TypeBounds {‘:’ Type} ContextBounds(typeBounds, tps) Types ::= Type {‘,’ Type} +NamesAndTypes ::= NameAndType {‘,’ NameAndType} +NameAndType ::= id ':' Type ``` ### Expressions @@ -280,8 +282,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)) @@ -331,6 +335,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 bf2c27d57863..8a4bbe99ccb9 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 16678e682dd6..9a2a2e45a0dd 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 d74bb1376912..97a9c54db62c 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 be02de1bd3705d08cdafd36cdef0018724baf29b Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 3 Dec 2023 13:16:06 +0100 Subject: [PATCH 08/34] 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 6b83159533f31e8ad795ba655163bc98b96cc34d Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 3 Dec 2023 14:27:35 +0100 Subject: [PATCH 09/34] 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 6225e0b8ba74c64ccf42eb03149c6f13ba740f1d Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 5 Dec 2023 11:19:23 +0100 Subject: [PATCH 10/34] 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 9651f62778f491d96d561d53da93598bd16ef569 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 5 Dec 2023 14:08:37 +0100 Subject: [PATCH 11/34] 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 d79f8b4c6393..1d896dc681de 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 f93275c714ed2b612f3a3270e281af1fef4815c1 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 5 Dec 2023 19:44:32 +0100 Subject: [PATCH 12/34] 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 8fe002e7b22d4524bf2531d2be88c24d3c905bd0 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 8 Dec 2023 11:37:45 +0100 Subject: [PATCH 13/34] 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 1d896dc681de..fa42a9dea042 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 f89abee84401..3b92d610e5fe 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 8dc23609b348..77d40334884d 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2432,6 +2432,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 d687d20c34e7e37f60ec1e79901cf46327d86e4c Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 8 Dec 2023 11:47:38 +0100 Subject: [PATCH 14/34] 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 3b92d610e5fe..bf7b692196c4 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 @@ -1487,25 +1546,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 5fc63f4575e3..ed7ea1271cb8 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 @@ -979,7 +979,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 77d40334884d..7aae0938863c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -112,6 +112,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`. * @@ -3413,31 +3438,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 82b5fe573c817711e1dffb7b0d59c0eb1870c8e6 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 14 Dec 2023 16:02:46 +0100 Subject: [PATCH 15/34] 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 bf7b692196c4..7c57806e1974 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 fc0078f8cd7ef0ab8a9b9a666de24411c0d43b2e Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 14 Dec 2023 18:51:03 +0100 Subject: [PATCH 16/34] Avoid widening into unreducible types when inferring types This is a general improvement, independent of named tuples. --- compiler/src/dotty/tools/dotc/core/TypeComparer.scala | 2 +- compiler/src/dotty/tools/dotc/core/TypeOps.scala | 7 ++++++- tests/pos/named-tuple-widen.scala | 9 +++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 tests/pos/named-tuple-widen.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index b04978357508..44e67ca148be 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2356,7 +2356,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling * @param canConstrain If true, new constraints might be added to simplify the lub. * @param isSoft If the lub is a union, this determines whether it's a soft union. */ - def lub(tp1: Type, tp2: Type, canConstrain: Boolean = false, isSoft: Boolean = true): Type = /*>|>*/ trace(s"lub(${tp1.show}, ${tp2.show}, canConstrain=$canConstrain, isSoft=$isSoft)", subtyping, show = true) /*<|<*/ { + def lub(tp1: Type, tp2: Type, canConstrain: Boolean = false, isSoft: Boolean = true): Type = trace(s"lub(${tp1.show}, ${tp2.show}, canConstrain=$canConstrain, isSoft=$isSoft)", subtyping, show = true) { if (tp1 eq tp2) tp1 else if !tp1.exists || (tp2 eq WildcardType) then tp1 else if !tp2.exists || (tp1 eq WildcardType) then tp2 diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 587c52688456..53ec474be800 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -390,7 +390,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 0693b6baa3dce575c730c33ee43e0db9770cd653 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 12 Jan 2024 14:59:15 +0100 Subject: [PATCH 17/34] 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 6e3ec86e233a13a96b0b21a27ca83e666872b42f Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 12 Jan 2024 18:40:19 +0100 Subject: [PATCH 18/34] 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 3aee6badf552..5bd7997601aa 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -95,6 +95,8 @@ val experimentalDefinitionInLibrary = Set( // New feature: named tuples "scala.NamedTuple", "scala.NamedTuple$", + "scala.NamedTupleDecomposition", + "scala.NamedTupleDecomposition$", ) From c5ac4aa0d8c0c6c63e4aeb3e1873f4ba8329c575 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 12 Jan 2024 18:40:55 +0100 Subject: [PATCH 19/34] 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 1a88863cef6e..9a6d2159d1f9 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 2354b45dcac655b91ce1df33de3042a7ead2faea Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 12 Jan 2024 18:41:31 +0100 Subject: [PATCH 20/34] 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 9bbe89a6073eb28bf004a87d0d177fb1578ec47d Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 12 Jan 2024 18:49:15 +0100 Subject: [PATCH 21/34] 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 f33e49627fa6e896dd4a2e19522ede27e2500165 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 14 Jan 2024 11:00:44 +0100 Subject: [PATCH 22/34] 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 9a6d2159d1f9..774ed85d639c 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -573,7 +573,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 b0ab1611581d2ea666b5bfa1314067a50049e6e8 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 14 Jan 2024 11:06:53 +0100 Subject: [PATCH 23/34] 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 31f47535015b..3dec673456c9 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1324,6 +1324,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 ) @@ -1352,7 +1355,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 @@ -1365,6 +1368,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 c9e21b4cf604..60f81b76bd89 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 19b4b2c7214020352e96e46c44a0543f5146115d Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 14 Jan 2024 11:08:49 +0100 Subject: [PATCH 24/34] 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 b3fb14dc64bf8e97b90a4177f2ecef60c0c2b1ad Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 14 Jan 2024 17:21:51 +0100 Subject: [PATCH 25/34] 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 3dec673456c9..ba1c6d370622 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1324,8 +1324,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 @@ -1355,7 +1355,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 @@ -1368,7 +1368,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 60f81b76bd89..9bb6268b4f05 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 cb9aa46bcb88805408c28db37f2ec77b21b74973 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 5 Feb 2024 18:16:09 +0100 Subject: [PATCH 26/34] 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 9bb6268b4f05..3ffda4330eff 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 7aae0938863c..0525d7d1a741 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -782,6 +782,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 5a31ba13f1f2804f1306b9dc139ed0539d791a64 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 5 Feb 2024 18:52:15 +0100 Subject: [PATCH 27/34] 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 | 173 ++++++++++-------- 1 file changed, 98 insertions(+), 75 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 0525d7d1a741..4f37d656a54a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -721,86 +721,109 @@ 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 + 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 = 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 9331dd9cc567e10e4f8e0e2475972d312d165363 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 5 Feb 2024 19:17:54 +0100 Subject: [PATCH 28/34] 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 7b15378ee680a63f894b05066d7105478c8f69e9 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 6 Feb 2024 16:35:49 +0100 Subject: [PATCH 29/34] 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 4f37d656a54a..2ff1c8465c4d 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -790,7 +790,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) @@ -800,7 +800,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, @@ -814,7 +814,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 8ebbf16f013a6be6e79a737bfc59f7cab86efbe9 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 6 Feb 2024 16:36:59 +0100 Subject: [PATCH 30/34] 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 fea6fff31fa23335b826b305af215240e163b655 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 7 Feb 2024 14:07:44 +0100 Subject: [PATCH 31/34] 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 4ec41b95a90b..5cee7190d3e2 100644 --- a/compiler/src/dotty/tools/dotc/ast/Trees.scala +++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala @@ -554,6 +554,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 8adffb701a9461dd275394afb5858a7965aad2df Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 7 Feb 2024 18:55:13 +0100 Subject: [PATCH 32/34] Fix rebase breakage --- compiler/src/dotty/tools/dotc/core/Contexts.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index c5f04d18b7fb..ee288a08b53f 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -95,7 +95,7 @@ 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 inMode[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 = From 1ba6a948fc7eae6fdc45e0955d081e6ee0418cef Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Thu, 25 Jan 2024 17:23:43 +0000 Subject: [PATCH 33/34] Drop unnamed tuple <: named tuple, but allow literal tuple to conform We can drop the unnamed tuple <: named tuple relationship, but still allow tuple syntax to conform to a named tuple prototype, with a small change in Desugar#tuple. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 5 +++- .../src/dotty/tools/dotc/core/TypeUtils.scala | 7 +++++ library/src/scala/NamedTuple.scala | 2 +- tests/neg/named-tuples.check | 30 ++++++++++++------- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 6994e1389064..e30fefe5d9c9 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1502,8 +1502,11 @@ object desugar { tree1.withSpan(tree.span) else cpy.Tuple(tree)(elemValues) - val names = elems.collect: + var names = elems.collect: case NamedArg(name, arg) => name + if names.isEmpty then + typer.Inferencing.isFullyDefined(pt, typer.ForceDegree.failBottom) + names = pt.namedTupleNames if names.isEmpty || ctx.mode.is(Mode.Pattern) then tup else diff --git a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala index fa42a9dea042..9c1ea0b4b214 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala @@ -134,6 +134,13 @@ class TypeUtils { def namedTupleElementTypes(using Context): List[(TermName, Type)] = namedTupleElementTypesUpTo(Int.MaxValue) + def namedTupleNames(using Context): List[Name] = + self.normalized.dealias match + case defn.NamedTuple(nmes, _) => + nmes.tupleElementTypes.getOrElse(Nil).map: + case ConstantType(Constants.Constant(str: String)) => str.toTermName + case _ => Nil + def isNamedTupleType(using Context): Boolean = self match case defn.NamedTuple(_, _) => true case _ => false diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index b06bc599f9fd..a19112c03f6d 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] <: AnyNamedTuple = V def apply[N <: Tuple, V <: Tuple](x: V): NamedTuple[N, V] = x diff --git a/tests/neg/named-tuples.check b/tests/neg/named-tuples.check index d9b6d686a587..a81041bf1b66 100644 --- a/tests/neg/named-tuples.check +++ b/tests/neg/named-tuples.check @@ -61,14 +61,24 @@ 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:30:27 --------------------------------------------------------------- +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:30:21 ------------------------------------------------------ 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:33:18 --------------------------------------------------------------- + | ^^^^^^ + | Found: (Int, Int) + | Required: NamedTuple.NamedTuple[N2, Tuple] + | + | where: N2 is a type variable with constraint <: Tuple + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:33:12 ------------------------------------------------------ 33 | person ++ (1, 2) match // error - | ^ - | Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), Tuple] =:= (true : Boolean). + | ^^^^^^ + | Found: (Int, Int) + | Required: NamedTuple.NamedTuple[N2, Tuple] + | + | where: N2 is a type variable with constraint <: Tuple + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg/named-tuples.scala:36:17 --------------------------------------------------------------------------- 36 | val bad = ("", age = 10) // error | ^^^^^^^^ @@ -103,8 +113,8 @@ -- 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) + | pattern's type (String, Int) does not match 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. + | 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. From 47facee2c811ef102f712cd674b27be26b95f85b Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Sat, 27 Jan 2024 11:58:27 +0000 Subject: [PATCH 34/34] Don't force type vars while typing (named) tuples --- compiler/src/dotty/tools/dotc/ast/Desugar.scala | 6 ++++-- tests/run/named-patterns.scala | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index e30fefe5d9c9..8466504dc8fe 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1505,8 +1505,10 @@ object desugar { var names = elems.collect: case NamedArg(name, arg) => name if names.isEmpty then - typer.Inferencing.isFullyDefined(pt, typer.ForceDegree.failBottom) - names = pt.namedTupleNames + val pt1 = pt.stripTypeVar match + case p: TypeParamRef => ctx.typerState.constraint.entry(p).hiBound + case _ => NoType + names = pt1.orElse(pt).namedTupleNames if names.isEmpty || ctx.mode.is(Mode.Pattern) then tup else diff --git a/tests/run/named-patterns.scala b/tests/run/named-patterns.scala index 7c24dc8d683a..5b4000a00825 100644 --- a/tests/run/named-patterns.scala +++ b/tests/run/named-patterns.scala @@ -72,3 +72,12 @@ object Test1: p3 match case (Person2(nn, a), Address(c, z, s, number)) => println(s"$nn, aged $a in $z $c, $s $number") + + // don't force type vars too early (while typing named tuples) + def ifBefore[T](end: Int)(op: => T, default: T): T = ??? + def ifBeforeTest(end: Int) = + sealed trait Type + case class TypeRef() extends Type + case object NoType extends Type + def readQualId(): (Int, TypeRef) = ??? + val (mixId, mixTpe) = ifBefore(end)(readQualId(), (2, NoType))