Skip to content

Commit e87a5d1

Browse files
authored
Merge pull request #3961 from Wojtechnology/ws-optimize-string-interpolation
String interpolation optimization miniphase
2 parents 1c79982 + 449aff2 commit e87a5d1

File tree

7 files changed

+224
-5
lines changed

7 files changed

+224
-5
lines changed

compiler/src/dotty/tools/dotc/Compiler.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,15 @@ import Periods._
77
import Symbols._
88
import Types._
99
import Scopes._
10-
import typer.{FrontEnd, Typer, ImportInfo, RefChecks}
11-
import reporting.{Reporter, ConsoleReporter}
10+
import typer.{FrontEnd, ImportInfo, RefChecks, Typer}
11+
import reporting.{ConsoleReporter, Reporter}
1212
import Phases.Phase
1313
import transform._
1414
import util.FreshNameCreator
1515
import core.DenotTransformers.DenotTransformer
1616
import core.Denotations.SingleDenotation
17-
18-
import dotty.tools.backend.jvm.{LabelDefs, GenBCode, CollectSuperCalls}
19-
import dotty.tools.dotc.transform.localopt.Simplify
17+
import dotty.tools.backend.jvm.{CollectSuperCalls, GenBCode, LabelDefs}
18+
import dotty.tools.dotc.transform.localopt.{Simplify, StringInterpolatorOpt}
2019

2120
/** The central class of the dotc compiler. The job of a compiler is to create
2221
* runs, which process given `phases` in a given `rootContext`.
@@ -78,6 +77,7 @@ class Compiler {
7877
new PatternMatcher, // Compile pattern matches
7978
new ExplicitOuter, // Add accessors to outer classes from nested ones.
8079
new ExplicitSelf, // Make references to non-trivial self types explicit as casts
80+
new StringInterpolatorOpt, // Optimizes raw and s string interpolators by rewriting them to string concatentations
8181
new CrossCastAnd, // Normalize selections involving intersection types.
8282
new Splitter) :: // Expand selections involving union types into conditionals
8383
List(new ErasedDecls, // Removes all erased defs and vals decls (except for parameters)

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,16 @@ class Definitions {
573573
lazy val StringAdd_plusR = StringAddClass.requiredMethodRef(nme.raw.PLUS)
574574
def StringAdd_+(implicit ctx: Context) = StringAdd_plusR.symbol
575575

576+
lazy val StringContextType: TypeRef = ctx.requiredClassRef("scala.StringContext")
577+
def StringContextClass(implicit ctx: Context) = StringContextType.symbol.asClass
578+
lazy val StringContextSR = StringContextClass.requiredMethodRef(nme.s)
579+
def StringContextS(implicit ctx: Context) = StringContextSR.symbol
580+
lazy val StringContextRawR = StringContextClass.requiredMethodRef(nme.raw_)
581+
def StringContextRaw(implicit ctx: Context) = StringContextRawR.symbol
582+
def StringContextModule(implicit ctx: Context) = StringContextClass.companionModule
583+
lazy val StringContextModule_applyR = StringContextModule.requiredMethodRef(nme.apply)
584+
def StringContextModule_apply(implicit ctx: Context) = StringContextModule_applyR.symbol
585+
576586
lazy val PartialFunctionType: TypeRef = ctx.requiredClassRef("scala.PartialFunction")
577587
def PartialFunctionClass(implicit ctx: Context) = PartialFunctionType.symbol.asClass
578588
lazy val PartialFunction_isDefinedAtR = PartialFunctionClass.requiredMethodRef(nme.isDefinedAt)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@ object StdNames {
486486
val productElement: N = "productElement"
487487
val productIterator: N = "productIterator"
488488
val productPrefix: N = "productPrefix"
489+
val raw_ : N = "raw"
489490
val readResolve: N = "readResolve"
490491
val reflect : N = "reflect"
491492
val reflectiveSelectable: N = "reflectiveSelectable"
@@ -496,6 +497,7 @@ object StdNames {
496497
val runtime: N = "runtime"
497498
val runtimeClass: N = "runtimeClass"
498499
val runtimeMirror: N = "runtimeMirror"
500+
val s: N = "s"
499501
val sameElements: N = "sameElements"
500502
val scala_ : N = "scala"
501503
val scalaShadowing : N = "scalaShadowing"
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package dotty.tools.dotc.transform.localopt
2+
3+
import dotty.tools.dotc.ast.Trees._
4+
import dotty.tools.dotc.ast.tpd
5+
import dotty.tools.dotc.core.Constants.Constant
6+
import dotty.tools.dotc.core.Contexts.Context
7+
import dotty.tools.dotc.core.StdNames._
8+
import dotty.tools.dotc.core.Symbols._
9+
import dotty.tools.dotc.transform.MegaPhase.MiniPhase
10+
11+
/**
12+
* MiniPhase to transform s and raw string interpolators from using StringContext to string
13+
* concatenation. Since string concatenation uses the Java String builder, we get a performance
14+
* improvement in terms of these two interpolators.
15+
*
16+
* More info here:
17+
* https://medium.com/@dkomanov/scala-string-interpolation-performance-21dc85e83afd
18+
*/
19+
class StringInterpolatorOpt extends MiniPhase {
20+
import tpd._
21+
22+
override def phaseName: String = "stringInterpolatorOpt"
23+
24+
/** Matches a list of constant literals */
25+
private object Literals {
26+
def unapply(tree: SeqLiteral)(implicit ctx: Context): Option[List[Literal]] = {
27+
tree.elems match {
28+
case literals if literals.forall(_.isInstanceOf[Literal]) =>
29+
Some(literals.map(_.asInstanceOf[Literal]))
30+
case _ => None
31+
}
32+
}
33+
}
34+
35+
private object StringContextApply {
36+
def unapply(tree: Select)(implicit ctx: Context): Boolean = {
37+
tree.symbol.eq(defn.StringContextModule_apply) && {
38+
val qualifier = tree.qualifier
39+
qualifier.isInstanceOf[Ident] && qualifier.symbol.eq(defn.StringContextModule)
40+
}
41+
}
42+
}
43+
44+
/** Matches an s or raw string interpolator */
45+
private object SOrRawInterpolator {
46+
def unapply(tree: Tree)(implicit ctx: Context): Option[(List[Literal], List[Tree])] = {
47+
if (tree.symbol.eq(defn.StringContextRaw) || tree.symbol.eq(defn.StringContextS)) {
48+
tree match {
49+
case Apply(Select(Apply(StringContextApply(), List(Literals(strs))), _),
50+
List(SeqLiteral(elems, _))) if elems.length == strs.length - 1 =>
51+
Some(strs, elems)
52+
case _ => None
53+
}
54+
} else None
55+
}
56+
}
57+
58+
/**
59+
* Match trees that resemble s and raw string interpolations. In the case of the s
60+
* interpolator, escapes the string constants. Exposes the string constants as well as
61+
* the variable references.
62+
*/
63+
private object StringContextIntrinsic {
64+
def unapply(tree: Apply)(implicit ctx: Context): Option[(List[Literal], List[Tree])] = {
65+
tree match {
66+
case SOrRawInterpolator(strs, elems) =>
67+
if (tree.symbol == defn.StringContextRaw) Some(strs, elems)
68+
else { // tree.symbol == defn.StringContextS
69+
try {
70+
val escapedStrs = strs.map { str =>
71+
val escapedValue = StringContext.processEscapes(str.const.stringValue)
72+
cpy.Literal(str)(Constant(escapedValue))
73+
}
74+
Some(escapedStrs, elems)
75+
} catch {
76+
case _: StringContext.InvalidEscapeException => None
77+
}
78+
}
79+
case _ => None
80+
}
81+
}
82+
}
83+
84+
override def transformApply(tree: Apply)(implicit ctx: Context): Tree = {
85+
tree match {
86+
case StringContextIntrinsic(strs: List[Literal], elems: List[Tree]) =>
87+
val stri = strs.iterator
88+
val elemi = elems.iterator
89+
var result: Tree = stri.next
90+
def concat(tree: Tree): Unit = {
91+
result = result.select(defn.String_+).appliedTo(tree)
92+
}
93+
while (elemi.hasNext) {
94+
concat(elemi.next)
95+
val str = stri.next
96+
if (!str.const.stringValue.isEmpty) concat(str)
97+
}
98+
result
99+
case _ => tree
100+
}
101+
}
102+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package dotty.tools.backend.jvm
2+
3+
import org.junit.Assert._
4+
import org.junit.Test
5+
6+
class StringInterpolatorOptTest extends DottyBytecodeTest {
7+
import ASMConverters._
8+
9+
@Test def testRawInterpolator = {
10+
val source =
11+
"""
12+
|class Foo {
13+
| val one = 1
14+
| val two = "two"
15+
| val three = 3.0
16+
|
17+
| def meth1: String = raw"$one plus $two$three\n"
18+
| def meth2: String = "" + one + " plus " + two + three + "\\n"
19+
|}
20+
""".stripMargin
21+
22+
checkBCode(source) { dir =>
23+
val clsIn = dir.lookupName("Foo.class", directory = false).input
24+
val clsNode = loadClassNode(clsIn)
25+
val meth1 = getMethod(clsNode, "meth1")
26+
val meth2 = getMethod(clsNode, "meth2")
27+
28+
val instructions1 = instructionsFromMethod(meth1)
29+
val instructions2 = instructionsFromMethod(meth2)
30+
31+
assert(instructions1 == instructions2,
32+
"the `` string interpolator incorrectly converts to string concatenation\n" +
33+
diffInstructions(instructions1, instructions2))
34+
}
35+
}
36+
37+
@Test def testSInterpolator = {
38+
val source =
39+
"""
40+
|class Foo {
41+
| val one = 1
42+
| val two = "two"
43+
| val three = 3.0
44+
|
45+
| def meth1: String = s"$one plus $two$three\n"
46+
| def meth2: String = "" + one + " plus " + two + three + "\n"
47+
|}
48+
""".stripMargin
49+
50+
checkBCode(source) { dir =>
51+
val clsIn = dir.lookupName("Foo.class", directory = false).input
52+
val clsNode = loadClassNode(clsIn)
53+
val meth1 = getMethod(clsNode, "meth1")
54+
val meth2 = getMethod(clsNode, "meth2")
55+
56+
val instructions1 = instructionsFromMethod(meth1)
57+
val instructions2 = instructionsFromMethod(meth2)
58+
59+
assert(instructions1 == instructions2,
60+
"the `s` string interpolator incorrectly converts to string concatenation\n" +
61+
diffInstructions(instructions1, instructions2))
62+
}
63+
}
64+
}

tests/run/interpolation-opt.check

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
1 plus two\nis 3.0
2+
1 plus two
3+
is 3.0
4+
a1two3.0b
5+
a1two3.0b
6+
7+
8+
Hello World
9+
Side effect!
10+
Foo Bar
11+
Side effect n2!
12+
Titi Toto

tests/run/interpolation-opt.scala

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
object Test extends App {
2+
3+
val one = 1
4+
val two = "two"
5+
val three = 3.0
6+
7+
// Test escaping
8+
println(raw"$one plus $two\nis $three")
9+
println(s"$one plus $two\nis $three")
10+
11+
// Test empty strings between elements
12+
println(raw"a$one$two${three}b")
13+
println(s"a$one$two${three}b")
14+
15+
// Test empty string interpolators
16+
println(raw"")
17+
println(s"")
18+
19+
// Make sure that StringContext still works with idents
20+
val foo = "Hello"
21+
val bar = "World"
22+
println(StringContext(foo, bar).s(" "))
23+
24+
def myStringContext= { println("Side effect!"); StringContext }
25+
println(myStringContext("Foo", "Bar").s(" ")) // this shouldn't be optimised away
26+
27+
// this shouldn't be optimised away
28+
println({ println("Side effect n2!"); StringContext }.apply("Titi", "Toto").s(" "))
29+
}

0 commit comments

Comments
 (0)