From 39c3bc8893f604f152f1d7e4428758fa22b4df16 Mon Sep 17 00:00:00 2001 From: Wojtek Swiderski Date: Sat, 3 Feb 2018 18:30:39 -0500 Subject: [PATCH 01/10] String interpolation optimization miniphase --- compiler/src/dotty/tools/dotc/Compiler.scala | 10 ++-- .../src/dotty/tools/dotc/core/StdNames.scala | 2 + .../localopt/StringInterpolatorOpt.scala | 59 +++++++++++++++++++ tests/run/interpolation-opt.check | 5 ++ tests/run/interpolation-opt.scala | 14 +++++ 5 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala create mode 100644 tests/run/interpolation-opt.check create mode 100644 tests/run/interpolation-opt.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index fe1ce0f02742..f524a4e90531 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -7,16 +7,15 @@ import Periods._ import Symbols._ import Types._ import Scopes._ -import typer.{FrontEnd, Typer, ImportInfo, RefChecks} -import reporting.{Reporter, ConsoleReporter} +import typer.{FrontEnd, ImportInfo, RefChecks, Typer} +import reporting.{ConsoleReporter, Reporter} import Phases.Phase import transform._ import util.FreshNameCreator import core.DenotTransformers.DenotTransformer import core.Denotations.SingleDenotation - -import dotty.tools.backend.jvm.{LabelDefs, GenBCode, CollectSuperCalls} -import dotty.tools.dotc.transform.localopt.Simplify +import dotty.tools.backend.jvm.{CollectSuperCalls, GenBCode, LabelDefs} +import dotty.tools.dotc.transform.localopt.{Simplify, StringInterpolatorOpt} /** The central class of the dotc compiler. The job of a compiler is to create * runs, which process given `phases` in a given `rootContext`. @@ -78,6 +77,7 @@ class Compiler { new ExplicitOuter, // Add accessors to outer classes from nested ones. new ExplicitSelf, // Make references to non-trivial self types explicit as casts new ShortcutImplicits, // Allow implicit functions without creating closures + new StringInterpolatorOpt, // Optimizes raw and s string interpolators by rewriting them to string concatentations new CrossCastAnd, // Normalize selections involving intersection types. new Splitter) :: // Expand selections involving union types into conditionals List(new ErasedDecls, // Removes all erased defs and vals decls (except for parameters) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 3ba81450301e..567245cd2de5 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -485,6 +485,7 @@ object StdNames { val productElement: N = "productElement" val productIterator: N = "productIterator" val productPrefix: N = "productPrefix" + val raw_ : N = "raw" val readResolve: N = "readResolve" val reflect : N = "reflect" val reify : N = "reify" @@ -494,6 +495,7 @@ object StdNames { val runtime: N = "runtime" val runtimeClass: N = "runtimeClass" val runtimeMirror: N = "runtimeMirror" + val s: N = "s" val sameElements: N = "sameElements" val scala_ : N = "scala" val scalaShadowing : N = "scalaShadowing" diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala new file mode 100644 index 000000000000..8b5985be176e --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala @@ -0,0 +1,59 @@ +package dotty.tools.dotc.transform.localopt + +import dotty.tools.dotc.ast.Trees._ +import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.core.Constants.Constant +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.StdNames._ +import dotty.tools.dotc.core.Symbols._ +import dotty.tools.dotc.transform.MegaPhase.MiniPhase + +/** + * Created by wojtekswiderski on 2018-01-24. + */ +class StringInterpolatorOpt extends MiniPhase { + import tpd._ + + override def phaseName: String = "stringInterpolatorOpt" + + private object StringContextIntrinsic { + def unapply(tree: Apply)(implicit ctx: Context): Option[(List[Tree], List[Tree])] = { + tree match { + case Apply(Select(Apply(Select(Ident(nme.StringContext), nme.apply), + List(SeqLiteral(strs, _))), id), List(SeqLiteral(elems, _))) => + if (id == nme.raw_) Some(strs, elems) + else if (id == nme.s) { + try { + val escapedStrs = strs.mapConserve { str => + val strValue = str.asInstanceOf[Literal].const.stringValue + val escapedValue = StringContext.processEscapes(strValue) + cpy.Literal(str)(Constant(escapedValue)) + } + Some(escapedStrs, elems) + } catch { + case _: StringContext.InvalidEscapeException => None + } + } else None + case _ => None + } + } + } + + override def transformApply(tree: Apply)(implicit ctx: Context): Tree = { + tree match { + case StringContextIntrinsic(strs: List[Tree], elems: List[Tree]) => + val numLits = strs.length + strs.tail.foldLeft((0, strs.head)) { (acc: (Int, Tree), str: Tree) => + val (i, result) = acc + val resultWithElem = + if (i < numLits - 1) result.select(defn.String_+).appliedTo(elems(i)) + else result + val resultWithStr = + if (str.asInstanceOf[Literal].const.stringValue.isEmpty) resultWithElem + else resultWithElem.select(defn.String_+).appliedTo(str) + (i + 1, resultWithStr) + }._2 + case _ => tree + } + } +} diff --git a/tests/run/interpolation-opt.check b/tests/run/interpolation-opt.check new file mode 100644 index 000000000000..50d85afc3c7a --- /dev/null +++ b/tests/run/interpolation-opt.check @@ -0,0 +1,5 @@ +1 plus two\nis 3.0 +1 plus two +is 3.0 +a1two3.0b +a1two3.0b diff --git a/tests/run/interpolation-opt.scala b/tests/run/interpolation-opt.scala new file mode 100644 index 000000000000..88863020765b --- /dev/null +++ b/tests/run/interpolation-opt.scala @@ -0,0 +1,14 @@ +object Test extends App { + + val one = 1 + val two = "two" + val three = 3.0 + + // Test escaping + println(raw"$one plus $two\nis $three") + println(s"$one plus $two\nis $three") + + // Test empty strings between elements + println(raw"a$one$two${three}b") + println(s"a$one$two${three}b") +} From f7c2208cb7d4c47fdd48b746200b7f215f182d6a Mon Sep 17 00:00:00 2001 From: Wojtek Swiderski Date: Sun, 11 Feb 2018 14:43:08 -0500 Subject: [PATCH 02/10] Addressed comments --- .../dotty/tools/dotc/core/Definitions.scala | 3 ++ .../localopt/StringInterpolatorOpt.scala | 51 ++++++++++--------- tests/run/interpolation-opt.check | 3 ++ tests/run/interpolation-opt.scala | 9 ++++ 4 files changed, 42 insertions(+), 24 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 95c5aa94dd37..c281c028d6a5 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -583,6 +583,9 @@ class Definitions { lazy val StringAdd_plusR = StringAddClass.requiredMethodRef(nme.raw.PLUS) def StringAdd_+(implicit ctx: Context) = StringAdd_plusR.symbol + lazy val StringContextType: TypeRef = ctx.requiredClassRef("scala.StringContext") + def StringContextClass(implicit ctx: Context) = StringContextType.symbol.asClass + lazy val PartialFunctionType: TypeRef = ctx.requiredClassRef("scala.PartialFunction") def PartialFunctionClass(implicit ctx: Context) = PartialFunctionType.symbol.asClass lazy val AbstractPartialFunctionType: TypeRef = ctx.requiredClassRef("scala.runtime.AbstractPartialFunction") diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala index 8b5985be176e..40c529fa4533 100644 --- a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala +++ b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala @@ -19,20 +19,22 @@ class StringInterpolatorOpt extends MiniPhase { private object StringContextIntrinsic { def unapply(tree: Apply)(implicit ctx: Context): Option[(List[Tree], List[Tree])] = { tree match { - case Apply(Select(Apply(Select(Ident(nme.StringContext), nme.apply), - List(SeqLiteral(strs, _))), id), List(SeqLiteral(elems, _))) => - if (id == nme.raw_) Some(strs, elems) - else if (id == nme.s) { - try { - val escapedStrs = strs.mapConserve { str => - val strValue = str.asInstanceOf[Literal].const.stringValue - val escapedValue = StringContext.processEscapes(strValue) - cpy.Literal(str)(Constant(escapedValue)) + case Apply(Select(Apply(Select(ident, nme.apply), List(SeqLiteral(strs, _))), fn), + List(SeqLiteral(elems, _))) => + if (ident.symbol.eq(defn.StringContextClass) && strs.forall(_.isInstanceOf[Literal])) { + if (fn == nme.raw_) Some(strs, elems) + else if (fn == nme.s) { + try { + val escapedStrs = strs.mapConserve { str => + val strValue = str.asInstanceOf[Literal].const.stringValue + val escapedValue = StringContext.processEscapes(strValue) + cpy.Literal(str)(Constant(escapedValue)) + } + Some(escapedStrs, elems) + } catch { + case _: StringContext.InvalidEscapeException => None } - Some(escapedStrs, elems) - } catch { - case _: StringContext.InvalidEscapeException => None - } + } else None } else None case _ => None } @@ -42,17 +44,18 @@ class StringInterpolatorOpt extends MiniPhase { override def transformApply(tree: Apply)(implicit ctx: Context): Tree = { tree match { case StringContextIntrinsic(strs: List[Tree], elems: List[Tree]) => - val numLits = strs.length - strs.tail.foldLeft((0, strs.head)) { (acc: (Int, Tree), str: Tree) => - val (i, result) = acc - val resultWithElem = - if (i < numLits - 1) result.select(defn.String_+).appliedTo(elems(i)) - else result - val resultWithStr = - if (str.asInstanceOf[Literal].const.stringValue.isEmpty) resultWithElem - else resultWithElem.select(defn.String_+).appliedTo(str) - (i + 1, resultWithStr) - }._2 + val stri = strs.iterator + val elemi = elems.iterator + var result = stri.next + def concat(tree: Tree): Unit = { + result = result.select(defn.String_+).appliedTo(tree) + } + while (elemi.hasNext) { + concat(elemi.next) + val str = stri.next + if (!str.asInstanceOf[Literal].const.stringValue.isEmpty) concat(str) + } + result case _ => tree } } diff --git a/tests/run/interpolation-opt.check b/tests/run/interpolation-opt.check index 50d85afc3c7a..1c78b92caf0f 100644 --- a/tests/run/interpolation-opt.check +++ b/tests/run/interpolation-opt.check @@ -3,3 +3,6 @@ is 3.0 a1two3.0b a1two3.0b + + +Hello World \ No newline at end of file diff --git a/tests/run/interpolation-opt.scala b/tests/run/interpolation-opt.scala index 88863020765b..99ff075d3450 100644 --- a/tests/run/interpolation-opt.scala +++ b/tests/run/interpolation-opt.scala @@ -11,4 +11,13 @@ object Test extends App { // Test empty strings between elements println(raw"a$one$two${three}b") println(s"a$one$two${three}b") + + // Test empty string interpolators + println(raw"") + println(s"") + + // Make sure that StringContext still works with idents + val foo = "Hello" + val bar = "World" + println(StringContext(foo, bar).s(" ")) } From f19e12bea307e4ea82b13a91b6fdad2010d1bf9e Mon Sep 17 00:00:00 2001 From: Wojtek Swiderski Date: Sun, 11 Feb 2018 14:54:43 -0500 Subject: [PATCH 03/10] Use companion class --- .../tools/dotc/transform/localopt/StringInterpolatorOpt.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala index 40c529fa4533..508d7289f2e2 100644 --- a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala +++ b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala @@ -21,7 +21,8 @@ class StringInterpolatorOpt extends MiniPhase { tree match { case Apply(Select(Apply(Select(ident, nme.apply), List(SeqLiteral(strs, _))), fn), List(SeqLiteral(elems, _))) => - if (ident.symbol.eq(defn.StringContextClass) && strs.forall(_.isInstanceOf[Literal])) { + val clsSym = ident.symbol.companionClass + if (clsSym.eq(defn.StringContextClass) && strs.forall(_.isInstanceOf[Literal])) { if (fn == nme.raw_) Some(strs, elems) else if (fn == nme.s) { try { From 5fed68f0de26d3ea82ed4ce4739c54b13a950287 Mon Sep 17 00:00:00 2001 From: Wojtek Swiderski Date: Sun, 11 Feb 2018 19:31:58 -0500 Subject: [PATCH 04/10] fix test --- .../tools/dotc/transform/localopt/StringInterpolatorOpt.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala index 508d7289f2e2..5ec0574d1f1d 100644 --- a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala +++ b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala @@ -22,7 +22,8 @@ class StringInterpolatorOpt extends MiniPhase { case Apply(Select(Apply(Select(ident, nme.apply), List(SeqLiteral(strs, _))), fn), List(SeqLiteral(elems, _))) => val clsSym = ident.symbol.companionClass - if (clsSym.eq(defn.StringContextClass) && strs.forall(_.isInstanceOf[Literal])) { + if (clsSym.eq(defn.StringContextClass) && strs.forall(_.isInstanceOf[Literal]) + && elems.length == strs.length - 1) { if (fn == nme.raw_) Some(strs, elems) else if (fn == nme.s) { try { From 0b1e4ac58c0c507fe20f38cf867626d91d1b813b Mon Sep 17 00:00:00 2001 From: Wojtek Swiderski Date: Mon, 12 Mar 2018 23:11:37 +0000 Subject: [PATCH 05/10] StringContextClass -> Module --- compiler/src/dotty/tools/dotc/core/Definitions.scala | 1 + .../tools/dotc/transform/localopt/StringInterpolatorOpt.scala | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index c281c028d6a5..a39560c65ac6 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -585,6 +585,7 @@ class Definitions { lazy val StringContextType: TypeRef = ctx.requiredClassRef("scala.StringContext") def StringContextClass(implicit ctx: Context) = StringContextType.symbol.asClass + def StringContextModule(implicit ctx: Context) = StringContextClass.companionClass lazy val PartialFunctionType: TypeRef = ctx.requiredClassRef("scala.PartialFunction") def PartialFunctionClass(implicit ctx: Context) = PartialFunctionType.symbol.asClass diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala index 5ec0574d1f1d..d61723a4a9af 100644 --- a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala +++ b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala @@ -21,8 +21,7 @@ class StringInterpolatorOpt extends MiniPhase { tree match { case Apply(Select(Apply(Select(ident, nme.apply), List(SeqLiteral(strs, _))), fn), List(SeqLiteral(elems, _))) => - val clsSym = ident.symbol.companionClass - if (clsSym.eq(defn.StringContextClass) && strs.forall(_.isInstanceOf[Literal]) + if (ident.symbol.eq(defn.StringContextModule) && strs.forall(_.isInstanceOf[Literal]) && elems.length == strs.length - 1) { if (fn == nme.raw_) Some(strs, elems) else if (fn == nme.s) { From 11e6975ca3f5d61d2385b84ace77152f19a2cd12 Mon Sep 17 00:00:00 2001 From: Wojtek Swiderski Date: Mon, 12 Mar 2018 23:48:13 +0000 Subject: [PATCH 06/10] Added bytecode tests and fixed module defn --- .../dotty/tools/dotc/core/Definitions.scala | 2 +- .../jvm/StringInterpolatorOptTest.scala | 64 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 compiler/test/dotty/tools/backend/jvm/StringInterpolatorOptTest.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index a39560c65ac6..e83fa8de4546 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -585,7 +585,7 @@ class Definitions { lazy val StringContextType: TypeRef = ctx.requiredClassRef("scala.StringContext") def StringContextClass(implicit ctx: Context) = StringContextType.symbol.asClass - def StringContextModule(implicit ctx: Context) = StringContextClass.companionClass + def StringContextModule(implicit ctx: Context) = StringContextClass.companionModule lazy val PartialFunctionType: TypeRef = ctx.requiredClassRef("scala.PartialFunction") def PartialFunctionClass(implicit ctx: Context) = PartialFunctionType.symbol.asClass diff --git a/compiler/test/dotty/tools/backend/jvm/StringInterpolatorOptTest.scala b/compiler/test/dotty/tools/backend/jvm/StringInterpolatorOptTest.scala new file mode 100644 index 000000000000..56febbbc4e05 --- /dev/null +++ b/compiler/test/dotty/tools/backend/jvm/StringInterpolatorOptTest.scala @@ -0,0 +1,64 @@ +package dotty.tools.backend.jvm + +import org.junit.Assert._ +import org.junit.Test + +class StringInterpolatorOptTest extends DottyBytecodeTest { + import ASMConverters._ + + @Test def testRawInterpolator = { + val source = + """ + |class Foo { + | val one = 1 + | val two = "two" + | val three = 3.0 + | + | def meth1: String = raw"$one plus $two$three\n" + | def meth2: String = "" + one + " plus " + two + three + "\\n" + |} + """.stripMargin + + checkBCode(source) { dir => + val clsIn = dir.lookupName("Foo.class", directory = false).input + val clsNode = loadClassNode(clsIn) + val meth1 = getMethod(clsNode, "meth1") + val meth2 = getMethod(clsNode, "meth2") + + val instructions1 = instructionsFromMethod(meth1) + val instructions2 = instructionsFromMethod(meth2) + + assert(instructions1 == instructions2, + "the `` string interpolator incorrectly converts to string concatenation\n" + + diffInstructions(instructions1, instructions2)) + } + } + + @Test def testSInterpolator = { + val source = + """ + |class Foo { + | val one = 1 + | val two = "two" + | val three = 3.0 + | + | def meth1: String = s"$one plus $two$three\n" + | def meth2: String = "" + one + " plus " + two + three + "\n" + |} + """.stripMargin + + checkBCode(source) { dir => + val clsIn = dir.lookupName("Foo.class", directory = false).input + val clsNode = loadClassNode(clsIn) + val meth1 = getMethod(clsNode, "meth1") + val meth2 = getMethod(clsNode, "meth2") + + val instructions1 = instructionsFromMethod(meth1) + val instructions2 = instructionsFromMethod(meth2) + + assert(instructions1 == instructions2, + "the `s` string interpolator incorrectly converts to string concatenation\n" + + diffInstructions(instructions1, instructions2)) + } + } +} From 2da90a07a66b1b527e2d438c3d2aada4d7d55bac Mon Sep 17 00:00:00 2001 From: Wojtek Swiderski Date: Sun, 25 Mar 2018 15:19:30 -0400 Subject: [PATCH 07/10] Addressed comments --- .../dotty/tools/dotc/core/Definitions.scala | 6 ++ .../localopt/StringInterpolatorOpt.scala | 76 +++++++++++++------ .../jvm/StringInterpolatorOptTest.scala | 36 ++++----- 3 files changed, 78 insertions(+), 40 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index e83fa8de4546..2376ad02addc 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -585,7 +585,13 @@ class Definitions { lazy val StringContextType: TypeRef = ctx.requiredClassRef("scala.StringContext") def StringContextClass(implicit ctx: Context) = StringContextType.symbol.asClass + lazy val StringContextSR = StringContextClass.requiredMethodRef(nme.s) + def StringContextS(implicit ctx: Context) = StringContextSR.symbol + lazy val StringContextRawR = StringContextClass.requiredMethodRef(nme.raw_) + def StringContextRaw(implicit ctx: Context) = StringContextRawR.symbol def StringContextModule(implicit ctx: Context) = StringContextClass.companionModule + lazy val StringContextModule_applyR = StringContextModule.requiredMethodRef(nme.apply) + def StringContextModule_apply(implicit ctx: Context) = StringContextModule_applyR.symbol lazy val PartialFunctionType: TypeRef = ctx.requiredClassRef("scala.PartialFunction") def PartialFunctionClass(implicit ctx: Context) = PartialFunctionType.symbol.asClass diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala index d61723a4a9af..7676933a2751 100644 --- a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala +++ b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala @@ -9,34 +9,66 @@ import dotty.tools.dotc.core.Symbols._ import dotty.tools.dotc.transform.MegaPhase.MiniPhase /** - * Created by wojtekswiderski on 2018-01-24. + * MiniPhase to transform s and raw string interpolators from using StringContext to string + * concatenation. Since string concatenation uses the Java String builder, we get a performance + * improvement in terms of these two interpolators. + * + * More info here: + * https://medium.com/@dkomanov/scala-string-interpolation-performance-21dc85e83afd */ class StringInterpolatorOpt extends MiniPhase { import tpd._ override def phaseName: String = "stringInterpolatorOpt" + /** Matches a list of constant literals */ + private object Literals { + def unapply(tree: SeqLiteral)(implicit ctx: Context): Option[List[Literal]] = { + tree.elems match { + case literals if literals.forall(_.isInstanceOf[Literal]) => + Some(literals.map(_.asInstanceOf[Literal])) + case _ => None + } + } + } + + /** Matches an s or raw string interpolator */ + private object SOrRawInterpolator { + def unapply(tree: Tree)(implicit ctx: Context): Option[(List[Literal], List[Tree])] = { + if (tree.symbol.eq(defn.StringContextRaw) || tree.symbol.eq(defn.StringContextS)) { + tree match { + case Apply(Select(Apply(strContextApply, List(Literals(strs))), _), + List(SeqLiteral(elems, _))) + if strContextApply.symbol.eq(defn.StringContextModule_apply) && + elems.length == strs.length - 1 => + Some(strs, elems) + case _ => None + } + } else None + } + } + + /** + * Match trees that resemble s and raw string interpolations. In the case of the s + * interpolator, escapes the string constants. Exposes the string constants as well as + * the variable references. + */ private object StringContextIntrinsic { - def unapply(tree: Apply)(implicit ctx: Context): Option[(List[Tree], List[Tree])] = { + def unapply(tree: Apply)(implicit ctx: Context): Option[(List[Literal], List[Tree])] = { tree match { - case Apply(Select(Apply(Select(ident, nme.apply), List(SeqLiteral(strs, _))), fn), - List(SeqLiteral(elems, _))) => - if (ident.symbol.eq(defn.StringContextModule) && strs.forall(_.isInstanceOf[Literal]) - && elems.length == strs.length - 1) { - if (fn == nme.raw_) Some(strs, elems) - else if (fn == nme.s) { - try { - val escapedStrs = strs.mapConserve { str => - val strValue = str.asInstanceOf[Literal].const.stringValue - val escapedValue = StringContext.processEscapes(strValue) - cpy.Literal(str)(Constant(escapedValue)) - } - Some(escapedStrs, elems) - } catch { - case _: StringContext.InvalidEscapeException => None + case SOrRawInterpolator(strs, elems) => + if (tree.symbol == defn.StringContextRaw) Some(strs, elems) + else { // tree.symbol == defn.StringContextS + try { + val escapedStrs = strs.map { str => + val escapedValue = StringContext.processEscapes(str.const.stringValue) + cpy.Literal(str)(Constant(escapedValue)) } - } else None - } else None + Some(escapedStrs, elems) + } catch { + case _: StringContext.InvalidEscapeException => None + } + } case _ => None } } @@ -44,17 +76,17 @@ class StringInterpolatorOpt extends MiniPhase { override def transformApply(tree: Apply)(implicit ctx: Context): Tree = { tree match { - case StringContextIntrinsic(strs: List[Tree], elems: List[Tree]) => + case StringContextIntrinsic(strs: List[Literal], elems: List[Tree]) => val stri = strs.iterator val elemi = elems.iterator - var result = stri.next + var result: Tree = stri.next def concat(tree: Tree): Unit = { result = result.select(defn.String_+).appliedTo(tree) } while (elemi.hasNext) { concat(elemi.next) val str = stri.next - if (!str.asInstanceOf[Literal].const.stringValue.isEmpty) concat(str) + if (!str.const.stringValue.isEmpty) concat(str) } result case _ => tree diff --git a/compiler/test/dotty/tools/backend/jvm/StringInterpolatorOptTest.scala b/compiler/test/dotty/tools/backend/jvm/StringInterpolatorOptTest.scala index 56febbbc4e05..14bcf0d83e45 100644 --- a/compiler/test/dotty/tools/backend/jvm/StringInterpolatorOptTest.scala +++ b/compiler/test/dotty/tools/backend/jvm/StringInterpolatorOptTest.scala @@ -9,15 +9,15 @@ class StringInterpolatorOptTest extends DottyBytecodeTest { @Test def testRawInterpolator = { val source = """ - |class Foo { - | val one = 1 - | val two = "two" - | val three = 3.0 - | - | def meth1: String = raw"$one plus $two$three\n" - | def meth2: String = "" + one + " plus " + two + three + "\\n" - |} - """.stripMargin + |class Foo { + | val one = 1 + | val two = "two" + | val three = 3.0 + | + | def meth1: String = raw"$one plus $two$three\n" + | def meth2: String = "" + one + " plus " + two + three + "\\n" + |} + """.stripMargin checkBCode(source) { dir => val clsIn = dir.lookupName("Foo.class", directory = false).input @@ -37,15 +37,15 @@ class StringInterpolatorOptTest extends DottyBytecodeTest { @Test def testSInterpolator = { val source = """ - |class Foo { - | val one = 1 - | val two = "two" - | val three = 3.0 - | - | def meth1: String = s"$one plus $two$three\n" - | def meth2: String = "" + one + " plus " + two + three + "\n" - |} - """.stripMargin + |class Foo { + | val one = 1 + | val two = "two" + | val three = 3.0 + | + | def meth1: String = s"$one plus $two$three\n" + | def meth2: String = "" + one + " plus " + two + three + "\n" + |} + """.stripMargin checkBCode(source) { dir => val clsIn = dir.lookupName("Foo.class", directory = false).input From 7626e05173e364a51eb2b7a0e0570ae4385fab3e Mon Sep 17 00:00:00 2001 From: Wojtek Swiderski Date: Sat, 7 Apr 2018 22:03:05 -0400 Subject: [PATCH 08/10] Added test --- tests/run/interpolation-opt.check | 2 ++ tests/run/interpolation-opt.scala | 3 +++ 2 files changed, 5 insertions(+) diff --git a/tests/run/interpolation-opt.check b/tests/run/interpolation-opt.check index 1c78b92caf0f..ec5bed489bb3 100644 --- a/tests/run/interpolation-opt.check +++ b/tests/run/interpolation-opt.check @@ -5,4 +5,6 @@ a1two3.0b a1two3.0b +Hello World +Side effect! Hello World \ No newline at end of file diff --git a/tests/run/interpolation-opt.scala b/tests/run/interpolation-opt.scala index 99ff075d3450..a0dbcabf16a9 100644 --- a/tests/run/interpolation-opt.scala +++ b/tests/run/interpolation-opt.scala @@ -20,4 +20,7 @@ object Test extends App { val foo = "Hello" val bar = "World" println(StringContext(foo, bar).s(" ")) + + def myStringContext= { println("Side effect!"); StringContext } + println(myStringContext(foo, bar).s(" ")) // this shouldn't be optimised away } From ac7ab0a6a57a45963b3ebfbf69ea43915dc957eb Mon Sep 17 00:00:00 2001 From: Wojtek Swiderski Date: Sun, 8 Apr 2018 10:57:02 -0400 Subject: [PATCH 09/10] New tests and fix --- .../localopt/StringInterpolatorOpt.scala | 22 +++++++++++++++---- tests/run/interpolation-opt.check | 4 +++- tests/run/interpolation-opt.scala | 5 ++++- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala index 7676933a2751..f290a67f4739 100644 --- a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala +++ b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala @@ -32,15 +32,29 @@ class StringInterpolatorOpt extends MiniPhase { } } + private object StringContextIdent { + def unapply(tree: Ident)(implicit ctx: Context): Option[Ident] = { + if (tree.symbol.eq(defn.StringContextModule)) Some(tree) else None + } + } + + private object StringContextApply { + def unapply(tree: Select)(implicit ctx: Context): Option[Select] = { + tree match { + case Select(StringContextIdent(_), _) + if tree.symbol.eq(defn.StringContextModule_apply) => Some(tree) + case _ => None + } + } + } + /** Matches an s or raw string interpolator */ private object SOrRawInterpolator { def unapply(tree: Tree)(implicit ctx: Context): Option[(List[Literal], List[Tree])] = { if (tree.symbol.eq(defn.StringContextRaw) || tree.symbol.eq(defn.StringContextS)) { tree match { - case Apply(Select(Apply(strContextApply, List(Literals(strs))), _), - List(SeqLiteral(elems, _))) - if strContextApply.symbol.eq(defn.StringContextModule_apply) && - elems.length == strs.length - 1 => + case Apply(Select(Apply(StringContextApply(_), List(Literals(strs))), _), + List(SeqLiteral(elems, _))) if elems.length == strs.length - 1 => Some(strs, elems) case _ => None } diff --git a/tests/run/interpolation-opt.check b/tests/run/interpolation-opt.check index ec5bed489bb3..1330eb8ac2e7 100644 --- a/tests/run/interpolation-opt.check +++ b/tests/run/interpolation-opt.check @@ -7,4 +7,6 @@ a1two3.0b Hello World Side effect! -Hello World \ No newline at end of file +Foo Bar +Side effect n2! +Titi Toto diff --git a/tests/run/interpolation-opt.scala b/tests/run/interpolation-opt.scala index a0dbcabf16a9..e7ff07ec61ac 100644 --- a/tests/run/interpolation-opt.scala +++ b/tests/run/interpolation-opt.scala @@ -22,5 +22,8 @@ object Test extends App { println(StringContext(foo, bar).s(" ")) def myStringContext= { println("Side effect!"); StringContext } - println(myStringContext(foo, bar).s(" ")) // this shouldn't be optimised away + println(myStringContext("Foo", "Bar").s(" ")) // this shouldn't be optimised away + + // this shouldn't be optimised away + println({ println("Side effect n2!"); StringContext }.apply("Titi", "Toto").s(" ")) } From 449aff2adc01716a22d98e45fc7664ff6b725244 Mon Sep 17 00:00:00 2001 From: Wojtek Swiderski Date: Sun, 15 Apr 2018 15:40:29 -0400 Subject: [PATCH 10/10] Simplify extractors --- .../localopt/StringInterpolatorOpt.scala | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala index f290a67f4739..89681da9aeaa 100644 --- a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala +++ b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala @@ -32,18 +32,11 @@ class StringInterpolatorOpt extends MiniPhase { } } - private object StringContextIdent { - def unapply(tree: Ident)(implicit ctx: Context): Option[Ident] = { - if (tree.symbol.eq(defn.StringContextModule)) Some(tree) else None - } - } - private object StringContextApply { - def unapply(tree: Select)(implicit ctx: Context): Option[Select] = { - tree match { - case Select(StringContextIdent(_), _) - if tree.symbol.eq(defn.StringContextModule_apply) => Some(tree) - case _ => None + def unapply(tree: Select)(implicit ctx: Context): Boolean = { + tree.symbol.eq(defn.StringContextModule_apply) && { + val qualifier = tree.qualifier + qualifier.isInstanceOf[Ident] && qualifier.symbol.eq(defn.StringContextModule) } } } @@ -53,7 +46,7 @@ class StringInterpolatorOpt extends MiniPhase { def unapply(tree: Tree)(implicit ctx: Context): Option[(List[Literal], List[Tree])] = { if (tree.symbol.eq(defn.StringContextRaw) || tree.symbol.eq(defn.StringContextS)) { tree match { - case Apply(Select(Apply(StringContextApply(_), List(Literals(strs))), _), + case Apply(Select(Apply(StringContextApply(), List(Literals(strs))), _), List(SeqLiteral(elems, _))) if elems.length == strs.length - 1 => Some(strs, elems) case _ => None