From b31edfe5865973ba3826860171042146ef684bff Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Wed, 27 Aug 2025 15:26:46 +0300 Subject: [PATCH 1/6] 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 6361f52727889881ffca832fe9f188b88ae813b4 Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Wed, 27 Aug 2025 15:48:40 +0300 Subject: [PATCH 2/6] 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 b28851484fe14c8c1044a5d89ab27d729e453149 Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Wed, 27 Aug 2025 16:58:29 +0300 Subject: [PATCH 3/6] 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 6dbcc2259608826d64598b59cf351588cba41d28 Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Wed, 27 Aug 2025 17:40:45 +0300 Subject: [PATCH 4/6] 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 d065ef17dcbb15f757dae4031640575ceb037650 Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Thu, 28 Aug 2025 19:48:37 +0300 Subject: [PATCH 5/6] 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 25bf11150f84c1c76de9df7abd44d6326e1f491c Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Tue, 2 Sep 2025 11:44:59 +0300 Subject: [PATCH 6/6] 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))