Skip to content

Commit 7b9ccb4

Browse files
committed
Add singleton string concatenation
This commit serves as a proof-of-concept of "overloaded" singleton ops. `+` is already addition, so adding concatenation requires "overloading" the `+` op. Scala does not support overloaded type aliases, so we must have two + types in different objects. However: - unqualified references (`+`) can be ambiguous (`int.+` or `string.+`) - qualified types (e.g. `int.+`) cannot be used infix The solution is to have a top-level `+` match type that redirects to the qualified `int.+` or `string.+`. These are kept private, as an implementation detail. The top-level match type is not considered as a compiletime applied type, as we cannot do constant folding before the match type has been applied.
1 parent 5e287cd commit 7b9ccb4

File tree

4 files changed

+35
-4
lines changed

4 files changed

+35
-4
lines changed

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -903,15 +903,19 @@ class Definitions {
903903

904904
private val compiletimePackageTypes: Set[Name] = Set(
905905
tpnme.Equals, tpnme.NotEquals,
906-
tpnme.Plus, tpnme.Minus, tpnme.Times, tpnme.Div, tpnme.Mod,
906+
tpnme.Minus, tpnme.Times, tpnme.Div, tpnme.Mod,
907907
tpnme.Lt, tpnme.Gt, tpnme.Ge, tpnme.Le,
908908
tpnme.Abs, tpnme.Negate, tpnme.Min, tpnme.Max, tpnme.ToString,
909909
tpnme.Not, tpnme.Xor, tpnme.And, tpnme.Or
910910
)
911+
private val compiletimePackageIntTypes: Set[Name] = Set(tpnme.Plus)
912+
private val compiletimePackageStringTypes: Set[Name] = Set(tpnme.Plus)
911913

912914
final def isCompiletimeAppliedType(sym: Symbol)(implicit ctx: Context): Boolean = {
913915
def isOpsPackageObjectAppliedType: Boolean =
914-
sym.owner == CompiletimeOpsPackageObject.moduleClass && compiletimePackageTypes.contains(sym.name)
916+
sym.owner == CompiletimeOpsPackageObject.moduleClass && compiletimePackageTypes.contains(sym.name) ||
917+
sym.owner == CompiletimeOpsPackageObjectInt.moduleClass && compiletimePackageIntTypes.contains(sym.name) ||
918+
sym.owner == CompiletimeOpsPackageObjectString.moduleClass && compiletimePackageStringTypes.contains(sym.name)
915919

916920
sym.isType && (isCompiletime_S(sym) || isOpsPackageObjectAppliedType)
917921
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3619,6 +3619,11 @@ object Types {
36193619
case _ => None
36203620
}
36213621

3622+
def stringValue(tp: Type): Option[String] = tp match {
3623+
case ConstantType(Constant(n: String)) => Some(n)
3624+
case _ => None
3625+
}
3626+
36223627
def natValue(tp: Type): Option[Int] = intValue(tp).filter(n => n >= 0 && n < Int.MaxValue)
36233628

36243629
def constantFold1[T](extractor: Type => Option[T], op: T => Any): Option[Type] =
@@ -3641,7 +3646,10 @@ object Types {
36413646
} else if (args.length == 2) tycon.symbol.name match {
36423647
case tpnme.Equals => constantFold2(constValue, _ == _)
36433648
case tpnme.NotEquals => constantFold2(constValue, _ != _)
3644-
case tpnme.Plus => constantFold2(intValue, _ + _)
3649+
case tpnme.Plus if tycon.symbol.owner == defn.CompiletimeOpsPackageObjectInt.moduleClass =>
3650+
constantFold2(intValue, _ + _)
3651+
case tpnme.Plus if tycon.symbol.owner == defn.CompiletimeOpsPackageObjectString.moduleClass =>
3652+
constantFold2(stringValue, _ + _)
36453653
case tpnme.Minus => constantFold2(intValue, _ - _)
36463654
case tpnme.Times => constantFold2(intValue, _ * _)
36473655
case tpnme.Div => constantFold2(intValue, {

library/src/scala/compiletime/ops/package.scala

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ package object ops {
66
@infix type ==[X <: AnyVal, Y <: AnyVal] <: Boolean
77
@infix type !=[X <: AnyVal, Y <: AnyVal] <: Boolean
88

9-
@infix type +[X <: Int, Y <: Int] <: Int
9+
@infix type +[X <: Int | String, Y <: Int | String] = (X, Y) match {
10+
case (Int, Int) => int.+[X, Y]
11+
case (String, String) => string.+[X, Y]
12+
case (String, Int) => string.+[X, ToString[Y]]
13+
case (Int, String) => string.+[ToString[X], Y]
14+
}
15+
1016
@infix type -[X <: Int, Y <: Int] <: Int
1117
@infix type *[X <: Int, Y <: Int] <: Int
1218
@infix type /[X <: Int, Y <: Int] <: Int
@@ -27,4 +33,12 @@ package object ops {
2733
@infix type ^[X <: Boolean, Y <: Boolean] <: Boolean
2834
@infix type &&[X <: Boolean, Y <: Boolean] <: Boolean
2935
@infix type ||[X <: Boolean, Y <: Boolean] <: Boolean
36+
37+
private object string {
38+
type +[X <: String, Y <: String] <: String
39+
}
40+
41+
private object int {
42+
type +[X <: Int, Y <: Int] <: Int
43+
}
3044
}

tests/neg/singleton-ops.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,9 @@ object Test {
9797
val t69: ToString[-1] = "-1"
9898
val t70: ToString[0] = "-0" // error
9999
val t71: ToString[200] = "100" // error
100+
101+
val t72: "hello " + "world" = "hello world"
102+
val t73: "hello " + 2 = "hello 2"
103+
val t74: 2 + " hello" = "2 hello"
104+
val t75: "hello" + "world" = "hi" // error
100105
}

0 commit comments

Comments
 (0)