Skip to content

Commit 07bb86c

Browse files
committed
Add string singleton comparison operators to scala.compiletime.ops.string.
The numeric singleton types already have comparison operators, but for some reason this was still missing for singleton String types. This enables a lot of potential valuable type-level computations, but one in particular is the ability to sort a named tuple, which means that if you're commonly adding/removing entries from a named tuple, this provides a way to normalize them, so two named tuples that were constructed in a different order can still be treated as equivalent types.
1 parent ba45875 commit 07bb86c

File tree

4 files changed

+111
-1
lines changed

4 files changed

+111
-1
lines changed

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1430,7 +1430,7 @@ class Definitions {
14301430
)
14311431
private val compiletimePackageBooleanTypes: Set[Name] = Set(tpnme.Not, tpnme.Xor, tpnme.And, tpnme.Or)
14321432
private val compiletimePackageStringTypes: Set[Name] = Set(
1433-
tpnme.Plus, tpnme.Length, tpnme.Substring, tpnme.Matches, tpnme.CharAt
1433+
tpnme.Plus, tpnme.Length, tpnme.Substring, tpnme.Matches, tpnme.CharAt, tpnme.LT, tpnme.GT, tpnme.LE, tpnme.GE
14341434
)
14351435
private val compiletimePackageOpTypes: Set[Name] =
14361436
Set(tpnme.S, tpnme.From)

compiler/src/dotty/tools/dotc/core/TypeEval.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,10 @@ object TypeEval:
261261
constantFold3(stringValue, intValue, intValue, (s, b, e) => s.substring(b, e))
262262
case tpnme.CharAt =>
263263
constantFold2AB(stringValue, intValue, _.charAt(_))
264+
case tpnme.LT => constantFold2(stringValue, _ < _)
265+
case tpnme.GT => constantFold2(stringValue, _ > _)
266+
case tpnme.LE => constantFold2(stringValue, _ <= _)
267+
case tpnme.GE => constantFold2(stringValue, _ >= _)
264268
case _ => None
265269
else if owner == defn.CompiletimeOpsBooleanModuleClass then name match
266270
case tpnme.Not => constantFold1(boolValue, x => !x)

library/src/scala/compiletime/ops/string.scala

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,50 @@ object string:
1515
*/
1616
infix type +[X <: String, Y <: String] <: String
1717

18+
/** Lexicographic comparison of two `String` singleton types.
19+
* ```scala
20+
* //{
21+
* import compiletime.ops.string.*
22+
* //}
23+
* val hello: "hello " < "world" = true
24+
* ```
25+
* @syntax markdown
26+
*/
27+
infix type <[S1 <: String, S2 <: String] <: Boolean
28+
29+
/** Lexicographic comparison of two `String` singleton types.
30+
* ```scala
31+
* //{
32+
* import compiletime.ops.string.*
33+
* //}
34+
* val hello: "hello " > "world" = false
35+
* ```
36+
* @syntax markdown
37+
*/
38+
infix type >[S1 <: String, S2 <: String] <: Boolean
39+
40+
/** Lexicographic comparison of two `String` singleton types.
41+
* ```scala
42+
* //{
43+
* import compiletime.ops.string.*
44+
* //}
45+
* val hello: "hello " >= "world" = false
46+
* ```
47+
* @syntax markdown
48+
*/
49+
infix type >=[S1 <: String, S2 <: String] <: Boolean
50+
51+
/** Lexicographic comparison of two `String` singleton types.
52+
* ```scala
53+
* //{
54+
* import compiletime.ops.string.*
55+
* //}
56+
* val hello: "hello " <= "world" = true
57+
* ```
58+
* @syntax markdown
59+
*/
60+
infix type <=[S1 <: String, S2 <: String] <: Boolean
61+
1862
/** Length of a `String` singleton type.
1963
* ```scala
2064
* //{

tests/neg/singleton-ops-string.scala

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,66 @@ object Test {
3434
val t18: CharAt["String", -1] = '?' // error
3535
// ^
3636
// String index out of range: -1
37+
38+
val t19: "hello" < "world" = true
39+
val t20: "hello" < "world" = false // error
40+
41+
val t21: "hello" > "world" = false
42+
val t22: "hello" > "world" = true // error
43+
44+
val t23: "hello" <= "world" = true
45+
val t24: "hello" <= "world" = false // error
46+
val t25: "hello" <= "hello" = true
47+
val t26: "hello" <= "hello" = false // error
48+
49+
val t27: "hello" >= "world" = false
50+
val t28: "hello" >= "world" = true // error
51+
val t29: "hello" >= "hello" = true
52+
val t30: "hello" >= "hello" = false // error
53+
54+
import scala.compiletime.ops.int./
55+
56+
type If[C <: Boolean, Then, Else] = C match
57+
case true => Then
58+
case false => Else
59+
60+
type ZipTuples[First, Second] = (First, Second) match
61+
case (first *: EmptyTuple, second *: EmptyTuple) => (first, second) *: EmptyTuple
62+
case (first *: firstRest, second *: secondRest) => (first, second) *: ZipTuples[firstRest, secondRest]
63+
64+
type TuplePairs[InputTuple] = InputTuple match
65+
case NamedTuple.NamedTuple[names, values] => ZipTuples[names, values]
66+
67+
type PrependTuplePairs[First, Second, Pairs] = Pairs match
68+
case EmptyTuple => (First *: EmptyTuple) *: (Second *: EmptyTuple) *: EmptyTuple
69+
case left *: right *: EmptyTuple => (First *: left) *: (Second *: right) *: EmptyTuple
70+
71+
type UnzipTuples[Input] = Input match
72+
case (first *: second *: EmptyTuple) *: rest => PrependTuplePairs[first, second, UnzipTuples[rest]]
73+
case EmptyTuple => EmptyTuple
74+
75+
type NamedTupleFromPairs[Input] = UnzipTuples[Input] match
76+
case EmptyTuple => NamedTuple.NamedTuple[EmptyTuple, EmptyTuple]
77+
case left *: right *: EmptyTuple => NamedTuple.NamedTuple[left, right]
78+
79+
type Merge[P1 <: Tuple, P2 <: Tuple] <: Tuple = (P1, P2) match
80+
case (EmptyTuple, h *: t) => h *: t
81+
case (h *: t, EmptyTuple) => h *: t
82+
case ((k1, v1) *: t1, (k2, v2) *: t2) => If[k1 <= k2,
83+
(k1, v1) *: Merge[t1, (k2, v2) *: t2],
84+
(k2, v2) *: Merge[(k1, v1) *: t1, t2]
85+
]
86+
87+
type SortPairs[P <: Tuple] <: Tuple = P match
88+
case EmptyTuple => EmptyTuple
89+
case h *: EmptyTuple => h *: EmptyTuple
90+
case _ => Merge[
91+
SortPairs[Tuple.Take[P, Tuple.Size[P] / 2]],
92+
SortPairs[Tuple.Drop[P, Tuple.Size[P] / 2]]
93+
]
94+
95+
type SortNamedTuple[NT <: NamedTuple.AnyNamedTuple] = NamedTupleFromPairs[SortPairs[TuplePairs[NT]]]
96+
97+
val t31: SortNamedTuple[(foo: 45, bar: "x", baz: 6.0, quux: false)] = (bar = "x", baz = 6.0, foo = 45, quux = false)
98+
val t32: SortNamedTuple[(foo: 45, bar: "x", baz: 6.0, quux: false)] = (foo = 45, bar = "x", baz = 6.0, quux = false) // error
3799
}

0 commit comments

Comments
 (0)