diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala index 478af8920b..7765ae0dc0 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala @@ -81,6 +81,217 @@ object OptimizerTest extends JasmineTest { } + describe("+[string] constant folding") { + it("must not break when folding two constant strings") { + @inline def str: String = "I am " + expect(str + "constant").toEqual("I am constant") + } + + it("must not break when folding the empty string when associated with a string") { + @noinline def str: String = "hello" + expect(str + "").toEqual("hello") + expect("" + str).toEqual("hello") + } + + it("must not break when folding 1.4f and a stringLit") { + expect(1.4f + "hello").toEqual("1.399999976158142hello") + expect("hello" + 1.4f).toEqual("hello1.399999976158142") + } + + it("must not break when folding cascading +[string]") { + @noinline def str: String = "awesome! 10/10" + expect("Scala.js" + (" is " + str)).toEqual("Scala.js is awesome! 10/10") + expect((str + " is ") + "Scala.js").toEqual("awesome! 10/10 is Scala.js") + } + + it("must not break when folding a chain of +[string]") { + @inline def b: String = "b" + @inline def d: String = "d" + @inline def f: String = "f" + expect("a" + b + "c" + d + "e" + f + "g").toEqual("abcdefg") + } + + it("must not break when folding integer in double and stringLit") { + expect(1.0 + "hello").toEqual("1hello") + expect("hello" + 1.0).toEqual("hello1") + } + + it("must not break when folding zero and stringLit") { + expect(0.0 + "hello").toEqual("0hello") + expect("hello" + 0.0).toEqual("hello0") + expect(-0.0 + "hello").toEqual("0hello") + expect("hello" + (-0.0)).toEqual("hello0") + } + + it("must not break when folding Infinities and stringLit") { + expect(Double.PositiveInfinity + "hello").toEqual("Infinityhello") + expect("hello" + Double.PositiveInfinity).toEqual("helloInfinity") + expect(Double.NegativeInfinity + "hello").toEqual("-Infinityhello") + expect("hello" + Double.NegativeInfinity).toEqual("hello-Infinity") + } + + it("must not break when folding NaN and stringLit") { + expect(Double.NaN + "hello").toEqual("NaNhello") + expect("hello" + Double.NaN).toEqual("helloNaN") + } + + unless("fullopt-stage"). + it("must not break when folding double with decimal and stringLit") { + expect(1.2323919403474454E21 + "hello") + .toEqual("1.2323919403474454e+21hello") + expect("hello" + 1.2323919403474454E21) + .toEqual("hello1.2323919403474454e+21") + } + + unless("fullopt-stage"). + it("must not break when folding double that JVM would print in scientific notation and stringLit") { + expect(123456789012345d + "hello") + .toEqual("123456789012345hello") + expect("hello" + 123456789012345d) + .toEqual("hello123456789012345") + } + + unless("fullopt-stage"). + it("must not break when folding doubles to String"){ + @noinline def toStringNoInline(v: Double): String = v.toString + @inline def test(v: Double): Unit = + expect(v.toString).toEqual(toStringNoInline(v)) + + // Special cases + test(0.0) + test(-0.0) + test(Double.NaN) + test(Double.PositiveInfinity) + test(Double.NegativeInfinity) + + // k <= n <= 21 + test(1.0) + test(12.0) + test(123.0) + test(1234.0) + test(12345.0) + test(123456.0) + test(1234567.0) + test(12345678.0) + test(123456789.0) + test(1234567890.0) + test(12345678901.0) + test(123456789012.0) + test(1234567890123.0) + test(12345678901234.0) + test(123456789012345.0) + test(1234567890123456.0) + test(12345678901234657.0) + test(123456789012345678.0) + test(1234567890123456789.0) + test(12345678901234567890.0) + test(123456789012345678901.0) + + // 0 < n <= 21 + test(1.42) + test(12.42) + test(123.42) + test(1234.42) + test(12345.42) + test(123456.42) + test(1234567.42) + test(12345678.42) + test(123456789.42) + test(1234567890.42) + test(12345678901.42) + test(123456789012.42) + test(1234567890123.42) + test(12345678901234.42) + test(123456789012345.42) + test(1234567890123456.42) + test(12345678901234657.42) + test(123456789012345678.42) + test(1234567890123456789.42) + test(12345678901234567890.42) + test(123456789012345678901.42) + + // -6 < n <= 0 + test(0.1) + test(0.01) + test(0.001) + test(0.0001) + test(0.00001) + test(0.000001) + + // k == 1 + test(1e22) + test(2e25) + test(3e50) + test(4e100) + test(5e200) + test(6e300) + test(7e307) + test(1e-22) + test(2e-25) + test(3e-50) + test(4e-100) + test(5e-200) + test(6e-300) + test(7e-307) + + // else + test(1.42e22) + test(2.42e25) + test(3.42e50) + test(4.42e100) + test(5.42e200) + test(6.42e300) + test(7.42e307) + test(1.42e-22) + test(2.42e-25) + test(3.42e-50) + test(4.42e-100) + test(5.42e-200) + test(6.42e-300) + test(7.42e-307) + + // special cases when ulp > 1 + test(18271179521433728.0) + test(1.15292150460684685E18) + test(1234567890123456770.0) + test(2234567890123456770.0) + test(4234567890123450000.0) + test(149170297077708820000.0) + test(296938164846899230000.0) + test(607681513323520000000.0) + } + + it("must not break when folding long and stringLit") { + expect(1L + "hello").toEqual("1hello") + expect("hello" + 1L).toEqual("hello1") + } + + it("must not break when folding integer and stringLit") { + expect(42 + "hello").toEqual("42hello") + expect("hello" + 42).toEqual("hello42") + } + + it("must not break when folding boolean and stringLit") { + expect(false + " is not true").toEqual("false is not true") + expect("false is not " + true).toEqual("false is not true") + } + + it("must not break when folding unit and stringLit") { + expect(() + " is undefined?").toEqual("undefined is undefined?") + expect("undefined is " + ()).toEqual("undefined is undefined") + } + + it("must not break when folding null and stringLit") { + expect("Damien is not " + null).toEqual("Damien is not null") + } + + it("must not break when folding char and stringLit") { + expect('S' + "cala.js").toEqual("Scala.js") + expect("Scala.j" + 's').toEqual("Scala.js") + } + + } + @inline class InlineClassDependentFields(val x: Int) { val b = x > 3 diff --git a/tools/shared/src/main/scala/org/scalajs/core/tools/optimizer/OptimizerCore.scala b/tools/shared/src/main/scala/org/scalajs/core/tools/optimizer/OptimizerCore.scala index 6e655f8a1a..3545fa5b75 100644 --- a/tools/shared/src/main/scala/org/scalajs/core/tools/optimizer/OptimizerCore.scala +++ b/tools/shared/src/main/scala/org/scalajs/core/tools/optimizer/OptimizerCore.scala @@ -2165,6 +2165,39 @@ private[optimizer] abstract class OptimizerCore( } } + /** Translate literals to their Scala.js String representation. */ + private def foldToStringForString_+(tree: Tree)(implicit pos : Position): Tree = tree match { + case FloatLiteral(value) => + foldToStringForString_+(DoubleLiteral(value.toDouble)) + + case DoubleLiteral(value) => + jsNumberToString(value).fold(tree)(StringLiteral(_)) + + case LongLiteral(value) => StringLiteral(value.toString) + case IntLiteral(value) => StringLiteral(value.toString) + case BooleanLiteral(value) => StringLiteral(value.toString) + case Null() => StringLiteral("null") + case Undefined() => StringLiteral("undefined") + case _ => tree + } + + /* Following the ECMAScript 6 specification */ + private def jsNumberToString(value: Double): Option[String] = { + if (1.0.toString == "1") { + // We are in a JS environment, so the host .toString() is the correct one. + Some(value.toString) + } else { + value match { + case _ if value.isNaN => Some("NaN") + case 0 => Some("0") + case _ if value < 0 => jsNumberToString(-value).map("-" + _) + case _ if value.isInfinity => Some("Infinity") + case _ if value.isValidInt => Some(value.toInt.toString) + case _ => None + } + } + } + private def foldBinaryOp(op: BinaryOp.Code, lhs: Tree, rhs: Tree)( implicit pos: Position): Tree = { import BinaryOp._ @@ -2180,6 +2213,27 @@ private[optimizer] abstract class OptimizerCore( case _ => default } + case String_+ => + val lhs1 = foldToStringForString_+(lhs) + val rhs1 = foldToStringForString_+(rhs) + @inline def stringDefault = BinaryOp(String_+, lhs1, rhs1) + (lhs1, rhs1) match { + case (StringLiteral(s1), StringLiteral(s2)) => + StringLiteral(s1 + s2) + case (_, StringLiteral("")) => + foldBinaryOp(op, rhs1, lhs1) + case (StringLiteral(""), _) if rhs1.tpe == StringType => + rhs1 + case (_, BinaryOp(String_+, rl, rr)) => + foldBinaryOp(String_+, BinaryOp(String_+, lhs1, rl), rr) + case (BinaryOp(String_+, ll, StringLiteral(lr)), StringLiteral(r)) => + BinaryOp(String_+, ll, StringLiteral(lr + r)) + case (BinaryOp(String_+, StringLiteral(""), lr), _) => + BinaryOp(String_+, lr, rhs1) + case _ => + stringDefault + } + case Int_+ => (lhs, rhs) match { case (IntLiteral(l), IntLiteral(r)) => IntLiteral(l + r)