From 4e2b4a2afff8b625cf4c93e33d8814055d86df4f Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Wed, 27 Aug 2025 15:26:46 +0300 Subject: [PATCH 1/7] named tuple toMap extension method --- library/src/scala/NamedTuple.scala | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index f88f7760365b..f1346fefcb66 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -1,5 +1,6 @@ package scala import compiletime.ops.boolean.* +import compiletime.summonAll import language.experimental.captureChecking @@ -209,6 +210,14 @@ object NamedTupleDecomposition: /** An immutable array consisting of all element values */ inline def toIArray: IArray[Object] = x.toTuple.toIArray + /** An immutable map consisting of all element values. + * Keys are the names of the elements. + */ + inline def toMap: collection.Map[String, Tuple.Union[V]] = + summonAll[Tuple.Map[N, ValueOf]].toList + .map(_.asInstanceOf[ValueOf[? <: String]].value) + .lazyZip(x.toList) + .toMap end extension /** The names of a named tuple, represented as a tuple of literal string values. */ From bd22585f43c039065086e9299729d0cfbfddc55a Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Wed, 27 Aug 2025 15:48:40 +0300 Subject: [PATCH 2/7] add test --- tests/run/named-tuple-ops.scala | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/run/named-tuple-ops.scala b/tests/run/named-tuple-ops.scala index 8c6db6f2fa1c..09a086c6e816 100644 --- a/tests/run/named-tuple-ops.scala +++ b/tests/run/named-tuple-ops.scala @@ -82,7 +82,10 @@ type Labels = (x: String, y: String) val _: List[String | Int] = cityFields assert(cityFields == List("Lausanne", 1000, 140000)) - val citArr = city.toArray - val _: List[String | Int] = cityFields - assert(cityFields == List("Lausanne", 1000, 140000)) + val cityArr = city.toArray + val _: Array[Object] = cityArr + assert(cityArr.sameElements(Array("Lausanne", 1000, 140000))) + val cityMap = city.toMap + val _: Map[String, String | Int] = cityMap + assert(cityMap == Map("name" -> "Lausanne", "zip" -> 1000, "pop" -> 140000)) From c72dbb5310057d5723eb024858ba67648c73cde2 Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Wed, 27 Aug 2025 16:58:29 +0300 Subject: [PATCH 3/7] add toMap method to export --- library/src/scala/NamedTuple.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index f1346fefcb66..e9ee4e2118ba 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -31,7 +31,7 @@ object NamedTuple: import NamedTupleDecomposition.{Names, DropNames} export NamedTupleDecomposition.{ Names, DropNames, - apply, size, init, head, last, tail, take, drop, splitAt, ++, map, reverse, zip, toList, toArray, toIArray + apply, size, init, head, last, tail, take, drop, splitAt, ++, map, reverse, zip, toList, toArray, toIArray, toMap } extension [N <: Tuple, V <: Tuple](x: NamedTuple[N, V]) From fadb8b739a8c29e12450550089bace029e85720b Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Wed, 27 Aug 2025 17:40:45 +0300 Subject: [PATCH 4/7] fix --- library/src/scala/NamedTuple.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index e9ee4e2118ba..35c505312a52 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -213,7 +213,7 @@ object NamedTupleDecomposition: /** An immutable map consisting of all element values. * Keys are the names of the elements. */ - inline def toMap: collection.Map[String, Tuple.Union[V]] = + inline def toMap: collection.immutable.Map[String, Tuple.Union[V]] = summonAll[Tuple.Map[N, ValueOf]].toList .map(_.asInstanceOf[ValueOf[? <: String]].value) .lazyZip(x.toList) From dc3e6b6ae94b425d5c170f39783e6e9f59d3f080 Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Thu, 28 Aug 2025 19:48:37 +0300 Subject: [PATCH 5/7] get rid of asInstanceOf --- library/src/scala/NamedTuple.scala | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index 35c505312a52..40eabfc51705 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -214,10 +214,8 @@ object NamedTupleDecomposition: * Keys are the names of the elements. */ inline def toMap: collection.immutable.Map[String, Tuple.Union[V]] = - summonAll[Tuple.Map[N, ValueOf]].toList - .map(_.asInstanceOf[ValueOf[? <: String]].value) - .lazyZip(x.toList) - .toMap + inline compiletime.constValueTuple[N].toList match + case names: List[String] => names.lazyZip(x.toList).toMap end extension /** The names of a named tuple, represented as a tuple of literal string values. */ From ae286c9a1840a2966e28e0b5cb59c8d286f3d108 Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Tue, 2 Sep 2025 11:44:59 +0300 Subject: [PATCH 6/7] use ListMap instead of HashMap --- library/src/scala/NamedTuple.scala | 9 +++++---- tests/run/named-tuple-ops.scala | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index 40eabfc51705..1d45781f898d 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -1,6 +1,7 @@ package scala import compiletime.ops.boolean.* import compiletime.summonAll +import collection.immutable.ListMap import language.experimental.captureChecking @@ -31,7 +32,7 @@ object NamedTuple: import NamedTupleDecomposition.{Names, DropNames} export NamedTupleDecomposition.{ Names, DropNames, - apply, size, init, head, last, tail, take, drop, splitAt, ++, map, reverse, zip, toList, toArray, toIArray, toMap + apply, size, init, head, last, tail, take, drop, splitAt, ++, map, reverse, zip, toList, toArray, toIArray, toListMap } extension [N <: Tuple, V <: Tuple](x: NamedTuple[N, V]) @@ -210,12 +211,12 @@ object NamedTupleDecomposition: /** An immutable array consisting of all element values */ inline def toIArray: IArray[Object] = x.toTuple.toIArray - /** An immutable map consisting of all element values. + /** An immutable map consisting of all element values preserving an order of fields. * Keys are the names of the elements. */ - inline def toMap: collection.immutable.Map[String, Tuple.Union[V]] = + inline def toListMap: ListMap[String, Tuple.Union[V]] = inline compiletime.constValueTuple[N].toList match - case names: List[String] => names.lazyZip(x.toList).toMap + case names: List[String] => ListMap.from(names.zip(x.toList)) end extension /** The names of a named tuple, represented as a tuple of literal string values. */ diff --git a/tests/run/named-tuple-ops.scala b/tests/run/named-tuple-ops.scala index 09a086c6e816..0dfd0ab7d1ec 100644 --- a/tests/run/named-tuple-ops.scala +++ b/tests/run/named-tuple-ops.scala @@ -1,5 +1,6 @@ //> using options -source future import scala.compiletime.asMatchable +import scala.collection.immutable.ListMap type City = (name: String, zip: Int, pop: Int) type Raw = (String, Int, Int) @@ -86,6 +87,6 @@ type Labels = (x: String, y: String) val _: Array[Object] = cityArr assert(cityArr.sameElements(Array("Lausanne", 1000, 140000))) - val cityMap = city.toMap - val _: Map[String, String | Int] = cityMap - assert(cityMap == Map("name" -> "Lausanne", "zip" -> 1000, "pop" -> 140000)) + val cityMap = city.toListMap + val _: ListMap[String, String | Int] = cityMap + assert(cityMap == ListMap("name" -> "Lausanne", "zip" -> 1000, "pop" -> 140000)) From 1253c3d68bbda82930c0d31a26dbdc556ba68b00 Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Tue, 9 Sep 2025 10:07:34 +0300 Subject: [PATCH 7/7] feat: review fixes --- library/src/scala/NamedTuple.scala | 5 ++++- tests/run/named-tuple-ops.scala | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index 1d45781f898d..9e7d5d547e89 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -216,7 +216,10 @@ object NamedTupleDecomposition: */ inline def toListMap: ListMap[String, Tuple.Union[V]] = inline compiletime.constValueTuple[N].toList match - case names: List[String] => ListMap.from(names.zip(x.toList)) + case names: List[String] => + ListMap.from(names.iterator.zip( + x.toTuple.productIterator.asInstanceOf[Iterator[Tuple.Union[V]]] + )) end extension /** The names of a named tuple, represented as a tuple of literal string values. */ diff --git a/tests/run/named-tuple-ops.scala b/tests/run/named-tuple-ops.scala index 0dfd0ab7d1ec..4e5000d3f688 100644 --- a/tests/run/named-tuple-ops.scala +++ b/tests/run/named-tuple-ops.scala @@ -84,7 +84,6 @@ type Labels = (x: String, y: String) assert(cityFields == List("Lausanne", 1000, 140000)) val cityArr = city.toArray - val _: Array[Object] = cityArr assert(cityArr.sameElements(Array("Lausanne", 1000, 140000))) val cityMap = city.toListMap