diff --git a/Jenkinsfile b/Jenkinsfile index 158b73cc4e..1c9dc60c29 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -185,6 +185,9 @@ def Tasks = [ reversi$v/fastLinkJS \ reversi$v/fullLinkJS \ reversi$v/checksizes && + sbtretry ++$scala \ + 'set Global/enableMinifyEverywhere := true' \ + reversi$v/checksizes && sbtretry ++$scala javalibintf/compile:doc compiler$v/compile:doc library$v/compile:doc \ testInterface$v/compile:doc testBridge$v/compile:doc && sbtretry ++$scala headerCheck && @@ -199,68 +202,84 @@ def Tasks = [ "test-suite-default-esversion": ''' setJavaVersion $java npm install && - sbtretry ++$scala jUnitTestOutputsJVM$v/test jUnitTestOutputsJS$v/test testBridge$v/test \ + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + jUnitTestOutputsJVM$v/test jUnitTestOutputsJS$v/test testBridge$v/test \ 'set scalaJSStage in Global := FullOptStage' jUnitTestOutputsJS$v/test testBridge$v/test && - sbtretry ++$scala $testSuite$v/test $testSuite$v/testHtmlJSDom && - sbtretry 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test \ + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + $testSuite$v/test $testSuite$v/testHtmlJSDom && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSStage in Global := FullOptStage' \ + $testSuite$v/test \ $testSuite$v/testHtmlJSDom && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAllowBigIntsForLongs(true)))' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAllowBigIntsForLongs(true)).withOptimizer(false))' \ - ++$scala $testSuite$v/test && - sbtretry \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAllowBigIntsForLongs(true)))' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAllowBigIntsForLongs(true)).withOptimizer(false))' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAvoidLetsAndConsts(false).withAvoidClasses(false)))' \ - ++$scala $testSuite$v/test && - sbtretry \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAvoidLetsAndConsts(false).withAvoidClasses(false)))' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.CommonJSModule))' \ - ++$scala $testSuite$v/test && - sbtretry \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.CommonJSModule))' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.CommonJSModule))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallestModules))' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.CommonJSModule))' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.CommonJSModule))' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test && - sbtretry \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ - ++$scala $testSuite$v/test && - sbtretry \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallestModules))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ - ++$scala $testSuite$v/test && - sbtretry \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallModulesFor(List("org.scalajs.testsuite"))))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ - ++$scala $testSuite$v/test && - sbtretry \ + $testSuite$v/test && + # The following tests the same thing whether testMinify is true or false; we also set it for regularity. + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test + $testSuite$v/test ''', "test-suite-custom-esversion-force-polyfills": ''' @@ -504,9 +523,10 @@ mainScalaVersions.each { scalaVersion -> quickMatrix.add([task: "main", scala: scalaVersion, java: javaVersion]) quickMatrix.add([task: "tools", scala: scalaVersion, java: javaVersion]) } - quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testSuite: "testSuite"]) + quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "testSuite"]) + quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testMinify: "true", testSuite: "testSuite"]) quickMatrix.add([task: "test-suite-custom-esversion", scala: scalaVersion, java: mainJavaVersion, esVersion: "ES5_1", testSuite: "testSuite"]) - quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testSuite: "scalaTestSuite"]) + quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "scalaTestSuite"]) quickMatrix.add([task: "test-suite-custom-esversion", scala: scalaVersion, java: mainJavaVersion, esVersion: "ES5_1", testSuite: "scalaTestSuite"]) quickMatrix.add([task: "bootstrap", scala: scalaVersion, java: mainJavaVersion]) quickMatrix.add([task: "partest-fastopt", scala: scalaVersion, java: mainJavaVersion]) @@ -527,7 +547,7 @@ otherScalaVersions.each { scalaVersion -> } mainScalaVersions.each { scalaVersion -> otherJavaVersions.each { javaVersion -> - quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: javaVersion, testSuite: "testSuite"]) + quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: javaVersion, testMinify: "false", testSuite: "testSuite"]) } fullMatrix.add([task: "partest-noopt", scala: scalaVersion, java: mainJavaVersion]) fullMatrix.add([task: "partest-fullopt", scala: scalaVersion, java: mainJavaVersion]) diff --git a/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala b/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala index d867c1d1bf..40644b5b9f 100644 --- a/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala +++ b/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala @@ -46,6 +46,19 @@ final class StandardConfig private ( val relativizeSourceMapBase: Option[URI], /** Name patterns for output. */ val outputPatterns: OutputPatterns, + /** Apply Scala.js-specific minification of the produced .js files. + * + * When enabled, the linker more aggressively reduces the size of the + * generated code, at the cost of readability and debuggability. It does + * not perform size optimizations that would negatively impact run-time + * performance. + * + * The focus is on optimizations that general-purpose JavaScript minifiers + * cannot do on their own. For the best results, we expect the Scala.js + * minifier to be used in conjunction with a general-purpose JavaScript + * minifier. + */ + val minify: Boolean, /** Whether to use the Google Closure Compiler pass, if it is available. * On the JavaScript platform, this does not have any effect. */ @@ -80,6 +93,7 @@ final class StandardConfig private ( sourceMap = true, relativizeSourceMapBase = None, outputPatterns = OutputPatterns.Defaults, + minify = false, closureCompilerIfAvailable = false, prettyPrint = false, batchMode = false, @@ -148,6 +162,9 @@ final class StandardConfig private ( def withOutputPatterns(f: OutputPatterns => OutputPatterns): StandardConfig = copy(outputPatterns = f(outputPatterns)) + def withMinify(minify: Boolean): StandardConfig = + copy(minify = minify) + def withClosureCompilerIfAvailable(closureCompilerIfAvailable: Boolean): StandardConfig = copy(closureCompilerIfAvailable = closureCompilerIfAvailable) @@ -173,6 +190,7 @@ final class StandardConfig private ( | sourceMap = $sourceMap, | relativizeSourceMapBase = $relativizeSourceMapBase, | outputPatterns = $outputPatterns, + | minify = $minify, | closureCompilerIfAvailable = $closureCompilerIfAvailable, | prettyPrint = $prettyPrint, | batchMode = $batchMode, @@ -192,6 +210,7 @@ final class StandardConfig private ( sourceMap: Boolean = sourceMap, outputPatterns: OutputPatterns = outputPatterns, relativizeSourceMapBase: Option[URI] = relativizeSourceMapBase, + minify: Boolean = minify, closureCompilerIfAvailable: Boolean = closureCompilerIfAvailable, prettyPrint: Boolean = prettyPrint, batchMode: Boolean = batchMode, @@ -209,6 +228,7 @@ final class StandardConfig private ( sourceMap, relativizeSourceMapBase, outputPatterns, + minify, closureCompilerIfAvailable, prettyPrint, batchMode, @@ -237,6 +257,7 @@ object StandardConfig { .addField("relativizeSourceMapBase", config.relativizeSourceMapBase.map(_.toASCIIString())) .addField("outputPatterns", config.outputPatterns) + .addField("minify", config.minify) .addField("closureCompilerIfAvailable", config.closureCompilerIfAvailable) .addField("prettyPrint", config.prettyPrint) @@ -264,6 +285,7 @@ object StandardConfig { * - `sourceMap`: `true` * - `relativizeSourceMapBase`: `None` * - `outputPatterns`: [[OutputPatterns.Defaults]] + * - `minify`: `false` * - `closureCompilerIfAvailable`: `false` * - `prettyPrint`: `false` * - `batchMode`: `false` diff --git a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureAstTransformer.scala b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureAstTransformer.scala index 79ad4562ec..cad0fd9434 100644 --- a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureAstTransformer.scala +++ b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureAstTransformer.scala @@ -210,9 +210,9 @@ private class ClosureAstTransformer(featureSet: FeatureSet, private def transformClassMember(member: Tree): Node = { implicit val pos = member.pos - def newFixedPropNode(token: Token, static: Boolean, name: Ident, + def newFixedPropNode(token: Token, static: Boolean, name: MaybeDelayedIdent, function: Node): Node = { - val node = Node.newString(token, name.name) + val node = Node.newString(token, name.resolveName()) node.addChildToBack(function) node.setStaticMember(static) node @@ -258,7 +258,7 @@ private class ClosureAstTransformer(featureSet: FeatureSet, val node = newComputedPropNode(static, nameExpr, function) node.putBooleanProp(Node.COMPUTED_PROP_METHOD, true) node - case name: Ident => + case name: MaybeDelayedIdent => newFixedPropNode(Token.MEMBER_FUNCTION_DEF, static, name, function) } @@ -274,7 +274,7 @@ private class ClosureAstTransformer(featureSet: FeatureSet, val node = newComputedPropNode(static, nameExpr, function) node.putBooleanProp(Node.COMPUTED_PROP_GETTER, true) node - case name: Ident => + case name: MaybeDelayedIdent => newFixedPropNode(Token.GETTER_DEF, static, name, function) } @@ -290,7 +290,7 @@ private class ClosureAstTransformer(featureSet: FeatureSet, val node = newComputedPropNode(static, nameExpr, function) node.putBooleanProp(Node.COMPUTED_PROP_SETTER, true) node - case name: Ident => + case name: MaybeDelayedIdent => newFixedPropNode(Token.SETTER_DEF, static, name, function) } @@ -321,7 +321,7 @@ private class ClosureAstTransformer(featureSet: FeatureSet, args.foreach(arg => node.addChildToBack(transformExpr(arg))) node case DotSelect(qualifier, item) => - val node = Node.newString(Token.GETPROP, item.name) + val node = Node.newString(Token.GETPROP, item.resolveName()) node.addChildToBack(transformExpr(qualifier)) setNodePosition(node, item.pos.orElse(pos)) case BracketSelect(qualifier, item) => @@ -435,8 +435,8 @@ private class ClosureAstTransformer(featureSet: FeatureSet, val transformedValue = transformExpr(value) val node = name match { - case Ident(name, _) => - Node.newString(Token.STRING_KEY, name) + case name: MaybeDelayedIdent => + Node.newString(Token.STRING_KEY, name.resolveName()) case StringLiteral(name) => val node = Node.newString(Token.STRING_KEY, name) diff --git a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala index 64160204ac..7532e0be47 100644 --- a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala +++ b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala @@ -54,13 +54,19 @@ final class ClosureLinkerBackend(config: LinkerBackendImpl.Config) s"Cannot use module kind $moduleKind with the Closure Compiler") private[this] val emitter = { + // Note that we do not transfer `minify` -- Closure will do its own thing anyway val emitterConfig = Emitter.Config(config.commonConfig.coreSpec) .withJSHeader(config.jsHeader) .withOptimizeBracketSelects(false) .withTrackAllGlobalRefs(true) .withInternalModulePattern(m => OutputPatternsImpl.moduleName(config.outputPatterns, m.id)) - new Emitter(emitterConfig, ClosureLinkerBackend.PostTransformer) + // Do not apply ClosureAstTransformer eagerly: + // The ASTs used by closure are highly mutable, so re-using them is non-trivial. + // Since closure is slow anyways, we haven't built the optimization. + val postTransformer = Emitter.PostTransformer.Identity + + new Emitter(emitterConfig, postTransformer) } val symbolRequirements: SymbolRequirement = emitter.symbolRequirements @@ -296,11 +302,4 @@ private object ClosureLinkerBackend { Function.prototype.apply; var NaN = 0.0/0.0, Infinity = 1.0/0.0, undefined = void 0; """ - - private object PostTransformer extends Emitter.PostTransformer[js.Tree] { - // Do not apply ClosureAstTransformer eagerly: - // The ASTs used by closure are highly mutable, so re-using them is non-trivial. - // Since closure is slow anyways, we haven't built the optimization. - def transformStats(trees: List[js.Tree], indent: Int): List[js.Tree] = trees - } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala index e07e31597b..fa7e616880 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala @@ -41,16 +41,19 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) private[this] var totalModules = 0 private[this] val rewrittenModules = new AtomicInteger(0) - private[this] val emitter = { + private[this] val bodyPrinter: BodyPrinter = { + if (config.minify) IdentityPostTransformerBasedBodyPrinter + else if (config.sourceMap) PrintedTreeWithSourceMapBodyPrinter + else PrintedTreeWithoutSourceMapBodyPrinter + } + + private[this] val emitter: Emitter[bodyPrinter.TreeType] = { val emitterConfig = Emitter.Config(config.commonConfig.coreSpec) .withJSHeader(config.jsHeader) .withInternalModulePattern(m => OutputPatternsImpl.moduleName(config.outputPatterns, m.id)) + .withMinify(config.minify) - val postTransformer = - if (config.sourceMap) PostTransformerWithSourceMap - else PostTransformerWithoutSourceMap - - new Emitter(emitterConfig, postTransformer) + new Emitter(emitterConfig, bodyPrinter.postTransformer) } val symbolRequirements: SymbolRequirement = emitter.symbolRequirements @@ -82,13 +85,14 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) val skipContentCheck = !isFirstRun isFirstRun = false - val allChanged = + val allChanged0 = printedModuleSetCache.updateGlobal(emitterResult.header, emitterResult.footer) + val allChanged = allChanged0 || config.minify val writer = new OutputWriter(output, config, skipContentCheck) { protected def writeModuleWithoutSourceMap(moduleID: ModuleID, force: Boolean): Option[ByteBuffer] = { val cache = printedModuleSetCache.getModuleCache(moduleID) - val (printedTrees, changed) = emitterResult.body(moduleID) + val (trees, changed) = emitterResult.body(moduleID) if (force || changed || allChanged) { rewrittenModules.incrementAndGet() @@ -98,8 +102,7 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) jsFileWriter.write(printedModuleSetCache.headerBytes) jsFileWriter.writeASCIIString("'use strict';\n") - for (printedTree <- printedTrees) - jsFileWriter.write(printedTree.jsCode) + bodyPrinter.printWithoutSourceMap(trees, jsFileWriter) jsFileWriter.write(printedModuleSetCache.footerBytes) @@ -112,7 +115,7 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) protected def writeModuleWithSourceMap(moduleID: ModuleID, force: Boolean): Option[(ByteBuffer, ByteBuffer)] = { val cache = printedModuleSetCache.getModuleCache(moduleID) - val (printedTrees, changed) = emitterResult.body(moduleID) + val (trees, changed) = emitterResult.body(moduleID) if (force || changed || allChanged) { rewrittenModules.incrementAndGet() @@ -133,10 +136,7 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) jsFileWriter.writeASCIIString("'use strict';\n") smWriter.nextLine() - for (printedTree <- printedTrees) { - jsFileWriter.write(printedTree.jsCode) - smWriter.insertFragment(printedTree.sourceMapFragment) - } + bodyPrinter.printWithSourceMap(trees, jsFileWriter, smWriter) jsFileWriter.write(printedModuleSetCache.footerBytes) jsFileWriter.write(("//# sourceMappingURL=" + sourceMapURI + "\n").getBytes(StandardCharsets.UTF_8)) @@ -240,6 +240,57 @@ private object BasicLinkerBackend { } } + private abstract class BodyPrinter { + type TreeType >: Null <: js.Tree + + val postTransformer: Emitter.PostTransformer[TreeType] + + def printWithoutSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter): Unit + def printWithSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter, smWriter: SourceMapWriter): Unit + } + + private object IdentityPostTransformerBasedBodyPrinter extends BodyPrinter { + type TreeType = js.Tree + + val postTransformer: Emitter.PostTransformer[TreeType] = Emitter.PostTransformer.Identity + + def printWithoutSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter): Unit = { + val printer = new Printers.JSTreePrinter(jsFileWriter) + for (tree <- trees) + printer.printStat(tree) + } + + def printWithSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter, smWriter: SourceMapWriter): Unit = { + val printer = new Printers.JSTreePrinterWithSourceMap(jsFileWriter, smWriter, initIndent = 0) + for (tree <- trees) + printer.printStat(tree) + } + } + + private abstract class PrintedTreeBasedBodyPrinter( + val postTransformer: Emitter.PostTransformer[js.PrintedTree] + ) extends BodyPrinter { + type TreeType = js.PrintedTree + + def printWithoutSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter): Unit = { + for (tree <- trees) + jsFileWriter.write(tree.jsCode) + } + + def printWithSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter, smWriter: SourceMapWriter): Unit = { + for (tree <- trees) { + jsFileWriter.write(tree.jsCode) + smWriter.insertFragment(tree.sourceMapFragment) + } + } + } + + private object PrintedTreeWithoutSourceMapBodyPrinter + extends PrintedTreeBasedBodyPrinter(PostTransformerWithoutSourceMap) + + private object PrintedTreeWithSourceMapBodyPrinter + extends PrintedTreeBasedBodyPrinter(PostTransformerWithSourceMap) + private object PostTransformerWithoutSourceMap extends Emitter.PostTransformer[js.PrintedTree] { def transformStats(trees: List[js.Tree], indent: Int): List[js.PrintedTree] = { if (trees.isEmpty) { @@ -248,7 +299,7 @@ private object BasicLinkerBackend { val jsCodeWriter = new ByteArrayWriter() val printer = new Printers.JSTreePrinter(jsCodeWriter, indent) - trees.map(printer.printStat(_)) + trees.foreach(printer.printStat(_)) js.PrintedTree(jsCodeWriter.toByteArray(), SourceMapWriter.Fragment.Empty) :: Nil } @@ -264,7 +315,7 @@ private object BasicLinkerBackend { val smFragmentBuilder = new SourceMapWriter.FragmentBuilder() val printer = new Printers.JSTreePrinterWithSourceMap(jsCodeWriter, smFragmentBuilder, indent) - trees.map(printer.printStat(_)) + trees.foreach(printer.printStat(_)) smFragmentBuilder.complete() js.PrintedTree(jsCodeWriter.toByteArray(), smFragmentBuilder.result()) :: Nil diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/LinkerBackendImpl.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/LinkerBackendImpl.scala index e1795d4493..0fc8f5169b 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/LinkerBackendImpl.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/LinkerBackendImpl.scala @@ -53,6 +53,8 @@ object LinkerBackendImpl { val outputPatterns: OutputPatterns, /** Base path to relativize paths in the source map. */ val relativizeSourceMapBase: Option[URI], + /** Whether to use Scala.js' minifier for property names. */ + val minify: Boolean, /** Whether to use the Google Closure Compiler pass, if it is available. * On the JavaScript platform, this does not have any effect. */ @@ -69,6 +71,7 @@ object LinkerBackendImpl { sourceMap = true, outputPatterns = OutputPatterns.Defaults, relativizeSourceMapBase = None, + minify = false, closureCompilerIfAvailable = false, prettyPrint = false, maxConcurrentWrites = 50) @@ -91,6 +94,9 @@ object LinkerBackendImpl { def withRelativizeSourceMapBase(relativizeSourceMapBase: Option[URI]): Config = copy(relativizeSourceMapBase = relativizeSourceMapBase) + def withMinify(minify: Boolean): Config = + copy(minify = minify) + def withClosureCompilerIfAvailable(closureCompilerIfAvailable: Boolean): Config = copy(closureCompilerIfAvailable = closureCompilerIfAvailable) @@ -106,12 +112,21 @@ object LinkerBackendImpl { sourceMap: Boolean = sourceMap, outputPatterns: OutputPatterns = outputPatterns, relativizeSourceMapBase: Option[URI] = relativizeSourceMapBase, + minify: Boolean = minify, closureCompilerIfAvailable: Boolean = closureCompilerIfAvailable, prettyPrint: Boolean = prettyPrint, maxConcurrentWrites: Int = maxConcurrentWrites): Config = { - new Config(commonConfig, jsHeader, sourceMap, outputPatterns, - relativizeSourceMapBase, closureCompilerIfAvailable, prettyPrint, - maxConcurrentWrites) + new Config( + commonConfig, + jsHeader, + sourceMap, + outputPatterns, + relativizeSourceMapBase, + minify, + closureCompilerIfAvailable, + prettyPrint, + maxConcurrentWrites + ) } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ArrayClassProperty.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ArrayClassProperty.scala new file mode 100644 index 0000000000..c33d0a99e7 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ArrayClassProperty.scala @@ -0,0 +1,46 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.linker.backend.emitter + +import org.scalajs.ir.OriginalName + +/** Represents a property of one of the special `ArrayClass`es. + * + * These properties live in the same namespace as Scala field and method + * names, because the `ArrayClass`es extend `j.l.Object`. Therefore, they + * must take part in the global property minification algorithm. + */ +final class ArrayClassProperty(val nonMinifiedName: String) + extends Comparable[ArrayClassProperty] { + + val originalName: OriginalName = OriginalName(nonMinifiedName) + + def compareTo(that: ArrayClassProperty): Int = + this.nonMinifiedName.compareTo(that.nonMinifiedName) + + override def toString(): String = s"ArrayClassProperty($nonMinifiedName)" +} + +object ArrayClassProperty { + /** `ArrayClass.u`: the underlying array of typed array. */ + val u: ArrayClassProperty = new ArrayClassProperty("u") + + /** `ArrayClass.get()`: gets one element. */ + val get: ArrayClassProperty = new ArrayClassProperty("get") + + /** `ArrayClass.set()`: sets one element. */ + val set: ArrayClassProperty = new ArrayClassProperty("set") + + /** `ArrayClass.copyTo()`: copies from that array to another array. */ + val copyTo: ArrayClassProperty = new ArrayClassProperty("copyTo") +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala index 211b335e29..11f8c3df33 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala @@ -355,7 +355,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } yield { val field = anyField.asInstanceOf[FieldDef] implicit val pos = field.pos - js.Assign(genSelect(js.This(), field.name, field.originalName), + js.Assign(genSelectForDef(js.This(), field.name, field.originalName), genZeroOf(field.ftpe)) } } @@ -422,8 +422,11 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val zero = genBoxedZeroOf(field.ftpe) field match { case FieldDef(_, name, originalName, _) => + /* TODO This seems to be dead code, which is somehow reassuring + * because I don't know what it is supposed to achieve. + */ WithGlobals( - js.Assign(js.DotSelect(classVarRef, genMemberFieldIdent(name, originalName)), zero)) + js.Assign(genSelectForDef(classVarRef, name, originalName), zero)) case JSFieldDef(_, name, _) => for (propName <- genMemberNameTree(name)) yield js.Assign(genPropSelect(classVarRef, propName), zero) @@ -472,7 +475,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { for { methodFun <- desugarToFunction(className, method.args, method.body.get, method.resultType) } yield { - val jsMethodName = genMemberMethodIdent(method.name, method.originalName) + val jsMethodName = genMethodIdentForDef(method.name, method.originalName) if (useESClass) { js.MethodDef(static = false, jsMethodName, methodFun.args, methodFun.restParam, methodFun.body) @@ -659,20 +662,6 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } } - private def genMemberFieldIdent(ident: FieldIdent, - originalName: OriginalName): js.Ident = { - val jsName = genName(ident.name) - js.Ident(jsName, genOriginalName(ident.name, originalName, jsName))( - ident.pos) - } - - private def genMemberMethodIdent(ident: MethodIdent, - originalName: OriginalName): js.Ident = { - val jsName = genMethodName(ident.name) - js.Ident(jsName, genOriginalName(ident.name, originalName, jsName))( - ident.pos) - } - def needInstanceTests(tree: LinkedClass)( implicit globalKnowledge: GlobalKnowledge): Boolean = { tree.hasInstanceTests || (tree.hasRuntimeTypeInfo && @@ -716,7 +705,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { !(!( genIsScalaJSObject(obj) && genIsClassNameInAncestors(className, - obj DOT "$classData" DOT "ancestors") + obj DOT cpn.classData DOT cpn.ancestors) )) } @@ -792,9 +781,9 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { globalFunctionDef(VarField.isArrayOf, className, List(objParam, depthParam), None, { js.Return(!(!({ genIsScalaJSObject(obj) && - ((obj DOT "$classData" DOT "arrayDepth") === depth) && + ((obj DOT cpn.classData DOT cpn.arrayDepth) === depth) && genIsClassNameInAncestors(className, - obj DOT "$classData" DOT "arrayBase" DOT "ancestors") + obj DOT cpn.classData DOT cpn.arrayBase DOT cpn.ancestors) }))) }) } @@ -825,14 +814,14 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { private def genIsScalaJSObject(obj: js.Tree)(implicit pos: Position): js.Tree = { import TreeDSL._ - obj && (obj DOT "$classData") + obj && (obj DOT cpn.classData) } private def genIsClassNameInAncestors(className: ClassName, ancestors: js.Tree)( implicit pos: Position): js.Tree = { import TreeDSL._ - ancestors DOT genName(className) + ancestors DOT genAncestorIdent(className) } def genTypeData(className: ClassName, kind: ClassKind, @@ -863,7 +852,8 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } val ancestorsRecord = js.ObjectConstr( - ancestors.withFilter(_ != ObjectClass).map(ancestor => (js.Ident(genName(ancestor)), js.IntLiteral(1)))) + ancestors.withFilter(_ != ObjectClass).map(ancestor => (genAncestorIdent(ancestor), js.IntLiteral(1))) + ) val isInstanceFunWithGlobals: WithGlobals[js.Tree] = { if (globalKnowledge.isAncestorOfHijackedClass(className)) { @@ -912,7 +902,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { isInstanceFunWithGlobals.flatMap { isInstanceFun => val allParams = List( - js.ObjectConstr(List(js.Ident(genName(className)) -> js.IntLiteral(0))), + js.ObjectConstr(List(genAncestorIdent(className) -> js.IntLiteral(0))), js.BooleanLiteral(kind == ClassKind.Interface), js.StringLiteral(RuntimeClassNameMapperImpl.map( semantics.runtimeClassNameMapper, className.nameString)), @@ -925,7 +915,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val prunedParams = allParams.reverse.dropWhile(_.isInstanceOf[js.Undefined]).reverse - val typeData = js.Apply(js.New(globalVar(VarField.TypeData, CoreVar), Nil) DOT "initClass", + val typeData = js.Apply(js.New(globalVar(VarField.TypeData, CoreVar), Nil) DOT cpn.initClass, prunedParams) globalVarDef(VarField.d, className, typeData) @@ -937,7 +927,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { globalKnowledge: GlobalKnowledge, pos: Position): js.Tree = { import TreeDSL._ - globalVar(VarField.c, className).prototype DOT "$classData" := globalVar(VarField.d, className) + globalVar(VarField.c, className).prototype DOT cpn.classData := globalVar(VarField.d, className) } def genModuleAccessor(className: ClassName, isJSClass: Boolean)( diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala index b36e35783d..6dc814cd87 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala @@ -106,7 +106,7 @@ private[emitter] object CoreJSLib { // Conditional global references that we often use private def ReflectRef = globalRef("Reflect") - private val classData = Ident("$classData") + private val classData = Ident(cpn.classData) private val orderedPrimRefsWithoutVoid = { List(BooleanRef, CharRef, ByteRef, ShortRef, IntRef, LongRef, @@ -522,7 +522,7 @@ private[emitter] object CoreJSLib { condDefs(!allowBigIntsForLongs)(List( globalVar(VarField.L0, CoreVar) := genScalaClassNew( LongImpl.RuntimeLongClass, LongImpl.initFromParts, 0, 0), - genClassDataOf(LongRef) DOT "zero" := globalVar(VarField.L0, CoreVar) + genClassDataOf(LongRef) DOT cpn.zero := globalVar(VarField.L0, CoreVar) )) } @@ -547,14 +547,14 @@ private[emitter] object CoreJSLib { val ctor = { val c = varRef("c") MethodDef(static = false, Ident("constructor"), paramList(c), None, { - This() DOT "c" := c + This() DOT cpn.c := c }) } val toStr = { MethodDef(static = false, Ident("toString"), Nil, None, { Return(Apply(genIdentBracketSelect(StringRef, "fromCharCode"), - (This() DOT "c") :: Nil)) + (This() DOT cpn.c) :: Nil)) }) } @@ -594,7 +594,7 @@ private[emitter] object CoreJSLib { str("char") }, { If(genIsScalaJSObject(value), { - genIdentBracketSelect(value DOT classData, "name") + genIdentBracketSelect(value DOT classData, cpn.name) }, { typeof(value) }) @@ -693,10 +693,10 @@ private[emitter] object CoreJSLib { val i = varRef("i") Block( - const(result, New(arrayClassData DOT "constr", + const(result, New(arrayClassData DOT cpn.constr, BracketSelect(lengths, lengthIndex) :: Nil)), If(lengthIndex < (lengths.length - 1), Block( - const(subArrayClassData, arrayClassData DOT "componentData"), + const(subArrayClassData, arrayClassData DOT cpn.componentData), const(subLengthIndex, lengthIndex + 1), const(underlying, result.u), For(let(i, 0), i < underlying.length, i.++, { @@ -719,7 +719,7 @@ private[emitter] object CoreJSLib { defineFunction1(VarField.objectOrArrayClone) { instance => // return instance.$classData.isArrayClass ? instance.clone__O() : $objectClone(instance); - Return(If(genIdentBracketSelect(instance DOT classData, "isArrayClass"), + Return(If(genIdentBracketSelect(instance DOT classData, cpn.isArrayClass), genApply(instance, cloneMethodName, Nil), genCallHelper(VarField.objectClone, instance))) } @@ -804,7 +804,7 @@ private[emitter] object CoreJSLib { condDefs(globalKnowledge.isClassClassInstantiated)( defineObjectGetClassBasedFun(VarField.objectGetClass, className => genClassOf(className), - instance => Apply(instance DOT classData DOT "getClassOf", Nil), + instance => Apply(instance DOT classData DOT cpn.getClassOf, Nil), Null() ) ) ::: @@ -813,7 +813,7 @@ private[emitter] object CoreJSLib { StringLiteral(RuntimeClassNameMapperImpl.map( semantics.runtimeClassNameMapper, className.nameString)) }, - instance => genIdentBracketSelect(instance DOT classData, "name"), + instance => genIdentBracketSelect(instance DOT classData, cpn.name), { if (nullPointers == CheckedBehavior.Unchecked) genApply(Null(), getNameMethodName, Nil) @@ -877,7 +877,7 @@ private[emitter] object CoreJSLib { if (implementedInObject) { val staticObjectCall: Tree = { - val fun = globalVar(VarField.c, ObjectClass).prototype DOT genMethodName(methodName) + val fun = globalVar(VarField.c, ObjectClass).prototype DOT genMethodIdent(methodName) Return(Apply(fun DOT "call", instance :: args)) } @@ -1131,13 +1131,13 @@ private[emitter] object CoreJSLib { ) ::: condDefs(esVersion >= ESVersion.ES2015 && nullPointers != CheckedBehavior.Unchecked)( defineFunction5(VarField.systemArraycopy) { (src, srcPos, dest, destPos, length) => - Apply(src DOT "copyTo", List(srcPos, dest, destPos, length)) + genArrayClassPropApply(src, ArrayClassProperty.copyTo, srcPos, dest, destPos, length) } ) ::: condDefs(arrayStores != CheckedBehavior.Unchecked)( defineFunction5(VarField.systemArraycopyRefs) { (src, srcPos, dest, destPos, length) => - If(Apply(genIdentBracketSelect(dest DOT classData, "isAssignableFrom"), List(src DOT classData)), { + If(Apply(genIdentBracketSelect(dest DOT classData, cpn.isAssignableFrom), List(src DOT classData)), { /* Fast-path, no need for array store checks. This always applies * for arrays of the same type, and a fortiori, when `src eq dest`. */ @@ -1155,7 +1155,8 @@ private[emitter] object CoreJSLib { srcPos, dest.u.length, destPos, length) }, For(let(i, 0), i < length, i := ((i + 1) | 0), { - Apply(dest DOT "set", List((destPos + i) | 0, BracketSelect(srcArray, (srcPos + i) | 0))) + genArrayClassPropApply(dest, ArrayClassProperty.set, + (destPos + i) | 0, BracketSelect(srcArray, (srcPos + i) | 0)) }) ) }) @@ -1169,9 +1170,12 @@ private[emitter] object CoreJSLib { const(srcData, src && (src DOT classData)), If(srcData === (dest && (dest DOT classData)), { // Both values have the same "data" (could also be falsy values) - If(srcData && genIdentBracketSelect(srcData, "isArrayClass"), { + If(srcData && genIdentBracketSelect(srcData, cpn.isArrayClass), { // Fast path: the values are array of the same type - genUncheckedArraycopy(List(src, srcPos, dest, destPos, length)) + if (esVersion >= ESVersion.ES2015 && nullPointers == CheckedBehavior.Unchecked) + genArrayClassPropApply(src, ArrayClassProperty.copyTo, srcPos, dest, destPos, length) + else + genCallHelper(VarField.systemArraycopy, src, srcPos, dest, destPos, length) }, { genCallHelper(VarField.throwArrayStoreException, Null()) }) @@ -1383,7 +1387,7 @@ private[emitter] object CoreJSLib { ( defineUnbox(VarField.uV, BoxedUnitClass, _ => Undefined()) ::: defineUnbox(VarField.uZ, BoxedBooleanClass, v => !(!v)) ::: - defineUnbox(VarField.uC, BoxedCharacterClass, v => If(v === Null(), 0, v DOT "c")) ::: + defineUnbox(VarField.uC, BoxedCharacterClass, v => If(v === Null(), 0, v DOT cpn.c)) ::: defineUnbox(VarField.uB, BoxedByteClass, _ | 0) ::: defineUnbox(VarField.uS, BoxedShortClass, _ | 0) ::: defineUnbox(VarField.uI, BoxedIntegerClass, _ | 0) ::: @@ -1401,7 +1405,7 @@ private[emitter] object CoreJSLib { // Unboxes for Chars and Longs ( defineFunction1(VarField.uC) { v => - Return(If(v === Null(), 0, v DOT "c")) + Return(If(v === Null(), 0, v DOT cpn.c)) } ::: defineFunction1(VarField.uJ) { v => Return(If(v === Null(), genLongZero(), v)) @@ -1441,14 +1445,17 @@ private[emitter] object CoreJSLib { genCallHelper(VarField.throwArrayIndexOutOfBoundsException, i)) } + val getName = genArrayClassPropertyForDef(ArrayClassProperty.get) + val setName = genArrayClassPropertyForDef(ArrayClassProperty.set) + List( - MethodDef(static = false, Ident("get"), paramList(i), None, { + MethodDef(static = false, getName, paramList(i), None, { Block( boundsCheck, Return(BracketSelect(This().u, i)) ) }), - MethodDef(static = false, Ident("set"), paramList(i, v), None, { + MethodDef(static = false, setName, paramList(i, v), None, { Block( boundsCheck, BracketSelect(This().u, i) := v @@ -1462,8 +1469,10 @@ private[emitter] object CoreJSLib { val i = varRef("i") val v = varRef("v") + val setName = genArrayClassPropertyForDef(ArrayClassProperty.set) + List( - MethodDef(static = false, Ident("set"), paramList(i, v), None, { + MethodDef(static = false, setName, paramList(i, v), None, { BracketSelect(This().u, i) := v }) ) @@ -1476,7 +1485,10 @@ private[emitter] object CoreJSLib { val dest = varRef("dest") val destPos = varRef("destPos") val length = varRef("length") - val methodDef = MethodDef(static = false, Ident("copyTo"), + + val copyToName = genArrayClassPropertyForDef(ArrayClassProperty.copyTo) + + val methodDef = MethodDef(static = false, copyToName, paramList(srcPos, dest, destPos, length), None, { if (isTypedArray) { Block( @@ -1501,7 +1513,8 @@ private[emitter] object CoreJSLib { Nil } - val clone = MethodDef(static = false, Ident(genMethodName(cloneMethodName)), Nil, None, { + val cloneMethodIdent = genMethodIdentForDef(cloneMethodName, NoOriginalName) + val clone = MethodDef(static = false, cloneMethodIdent, Nil, None, { Return(New(ArrayClass, Apply(genIdentBracketSelect(This().u, "slice"), Nil) :: Nil)) }) @@ -1572,34 +1585,34 @@ private[emitter] object CoreJSLib { val ctor = { MethodDef(static = false, Ident("constructor"), Nil, None, { Block( - privateFieldSet("constr", Undefined()), + privateFieldSet(cpn.constr, Undefined()), if (globalKnowledge.isParentDataAccessed) - privateFieldSet("parentData", Undefined()) + privateFieldSet(cpn.parentData, Undefined()) else Skip(), - privateFieldSet("ancestors", Null()), - privateFieldSet("componentData", Null()), - privateFieldSet("arrayBase", Null()), - privateFieldSet("arrayDepth", int(0)), - privateFieldSet("zero", Null()), - privateFieldSet("arrayEncodedName", str("")), - privateFieldSet("_classOf", Undefined()), - privateFieldSet("_arrayOf", Undefined()), + privateFieldSet(cpn.ancestors, Null()), + privateFieldSet(cpn.componentData, Null()), + privateFieldSet(cpn.arrayBase, Null()), + privateFieldSet(cpn.arrayDepth, int(0)), + privateFieldSet(cpn.zero, Null()), + privateFieldSet(cpn.arrayEncodedName, str("")), + privateFieldSet(cpn._classOf, Undefined()), + privateFieldSet(cpn._arrayOf, Undefined()), /* A lambda for the logic of the public `isAssignableFrom`, * without its fast-path. See the comment on the definition of * `isAssignableFrom` for the rationale of this decomposition. */ - privateFieldSet("isAssignableFromFun", Undefined()), + privateFieldSet(cpn.isAssignableFromFun, Undefined()), - privateFieldSet("wrapArray", Undefined()), - privateFieldSet("isJSType", bool(false)), + privateFieldSet(cpn.wrapArray, Undefined()), + privateFieldSet(cpn.isJSType, bool(false)), - publicFieldSet("name", str("")), - publicFieldSet("isPrimitive", bool(false)), - publicFieldSet("isInterface", bool(false)), - publicFieldSet("isArrayClass", bool(false)), - publicFieldSet("isInstance", Undefined()) + publicFieldSet(cpn.name, str("")), + publicFieldSet(cpn.isPrimitive, bool(false)), + publicFieldSet(cpn.isInterface, bool(false)), + publicFieldSet(cpn.isArrayClass, bool(false)), + publicFieldSet(cpn.isInstance, Undefined()) ) }) } @@ -1614,22 +1627,22 @@ private[emitter] object CoreJSLib { val that = varRef("that") val depth = varRef("depth") val obj = varRef("obj") - MethodDef(static = false, Ident("initPrim"), + MethodDef(static = false, Ident(cpn.initPrim), paramList(zero, arrayEncodedName, displayName, arrayClass, typedArrayClass), None, { Block( - privateFieldSet("ancestors", ObjectConstr(Nil)), - privateFieldSet("zero", zero), - privateFieldSet("arrayEncodedName", arrayEncodedName), + privateFieldSet(cpn.ancestors, ObjectConstr(Nil)), + privateFieldSet(cpn.zero, zero), + privateFieldSet(cpn.arrayEncodedName, arrayEncodedName), const(self, This()), // capture `this` for use in arrow fun - privateFieldSet("isAssignableFromFun", + privateFieldSet(cpn.isAssignableFromFun, genArrowFunction(paramList(that), Return(that === self))), - publicFieldSet("name", displayName), - publicFieldSet("isPrimitive", bool(true)), - publicFieldSet("isInstance", + publicFieldSet(cpn.name, displayName), + publicFieldSet(cpn.isPrimitive, bool(true)), + publicFieldSet(cpn.isInstance, genArrowFunction(paramList(obj), Return(bool(false)))), If(arrayClass !== Undefined(), { // it is undefined for void - privateFieldSet("_arrayOf", - Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT "initSpecializedArray", + privateFieldSet(cpn._arrayOf, + Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT cpn.initSpecializedArray, List(This(), arrayClass, typedArrayClass))) }), Return(This()) @@ -1649,29 +1662,29 @@ private[emitter] object CoreJSLib { val that = varRef("that") val depth = varRef("depth") val obj = varRef("obj") - MethodDef(static = false, Ident("initClass"), + MethodDef(static = false, Ident(cpn.initClass), paramList(internalNameObj, isInterface, fullName, ancestors, isJSType, parentData, isInstance), None, { Block( const(internalName, genCallHelper(VarField.propertyName, internalNameObj)), if (globalKnowledge.isParentDataAccessed) - privateFieldSet("parentData", parentData) + privateFieldSet(cpn.parentData, parentData) else Skip(), - privateFieldSet("ancestors", ancestors), - privateFieldSet("arrayEncodedName", str("L") + fullName + str(";")), - privateFieldSet("isAssignableFromFun", { + privateFieldSet(cpn.ancestors, ancestors), + privateFieldSet(cpn.arrayEncodedName, str("L") + fullName + str(";")), + privateFieldSet(cpn.isAssignableFromFun, { genArrowFunction(paramList(that), { - Return(!(!(BracketSelect(that DOT "ancestors", internalName)))) + Return(!(!(BracketSelect(that DOT cpn.ancestors, internalName)))) }) }), - privateFieldSet("isJSType", !(!isJSType)), - publicFieldSet("name", fullName), - publicFieldSet("isInterface", isInterface), - publicFieldSet("isInstance", isInstance || { + privateFieldSet(cpn.isJSType, !(!isJSType)), + publicFieldSet(cpn.name, fullName), + publicFieldSet(cpn.isInterface, isInterface), + publicFieldSet(cpn.isInstance, isInstance || { genArrowFunction(paramList(obj), { Return(!(!(obj && (obj DOT classData) && - BracketSelect(obj DOT classData DOT "ancestors", internalName)))) + BracketSelect(obj DOT classData DOT cpn.ancestors, internalName)))) }) }), Return(This()) @@ -1685,22 +1698,22 @@ private[emitter] object CoreJSLib { Block( arrayClass.prototype DOT classData := This(), - const(name, str("[") + (componentData DOT "arrayEncodedName")), - privateFieldSet("constr", arrayClass), + const(name, str("[") + (componentData DOT cpn.arrayEncodedName)), + privateFieldSet(cpn.constr, arrayClass), if (globalKnowledge.isParentDataAccessed) - privateFieldSet("parentData", genClassDataOf(ObjectClass)) + privateFieldSet(cpn.parentData, genClassDataOf(ObjectClass)) else Skip(), - privateFieldSet("ancestors", ObjectConstr(List( - Ident(genName(CloneableClass)) -> 1, - Ident(genName(SerializableClass)) -> 1 + privateFieldSet(cpn.ancestors, ObjectConstr(List( + genAncestorIdent(CloneableClass) -> 1, + genAncestorIdent(SerializableClass) -> 1 ))), - privateFieldSet("componentData", componentData), - privateFieldSet("arrayBase", arrayBase), - privateFieldSet("arrayDepth", arrayDepth), - privateFieldSet("arrayEncodedName", name), - publicFieldSet("name", name), - publicFieldSet("isArrayClass", bool(true)) + privateFieldSet(cpn.componentData, componentData), + privateFieldSet(cpn.arrayBase, arrayBase), + privateFieldSet(cpn.arrayDepth, arrayDepth), + privateFieldSet(cpn.arrayEncodedName, name), + publicFieldSet(cpn.name, name), + publicFieldSet(cpn.isArrayClass, bool(true)) ) } @@ -1713,15 +1726,15 @@ private[emitter] object CoreJSLib { val that = varRef("that") val obj = varRef("obj") val array = varRef("array") - MethodDef(static = false, Ident("initSpecializedArray"), + MethodDef(static = false, Ident(cpn.initSpecializedArray), paramList(componentData, arrayClass, typedArrayClass, isAssignableFromFun), None, { Block( initArrayCommonBody(arrayClass, componentData, componentData, 1), const(self, This()), // capture `this` for use in arrow fun - privateFieldSet("isAssignableFromFun", isAssignableFromFun || { + privateFieldSet(cpn.isAssignableFromFun, isAssignableFromFun || { genArrowFunction(paramList(that), Return(self === that)) }), - privateFieldSet("wrapArray", { + privateFieldSet(cpn.wrapArray, { If(typedArrayClass, { genArrowFunction(paramList(array), { Return(New(arrayClass, New(typedArrayClass, array :: Nil) :: Nil)) @@ -1732,7 +1745,7 @@ private[emitter] object CoreJSLib { }) }) }), - publicFieldSet("isInstance", + publicFieldSet(cpn.isInstance, genArrowFunction(paramList(obj), Return(obj instanceof arrayClass))), Return(This()) ) @@ -1749,7 +1762,7 @@ private[emitter] object CoreJSLib { val self = varRef("self") val obj = varRef("obj") val array = varRef("array") - MethodDef(static = false, Ident("initArray"), + MethodDef(static = false, Ident(cpn.initArray), paramList(componentData), None, { val ArrayClassDef = { val ctor = { @@ -1767,19 +1780,21 @@ private[emitter] object CoreJSLib { val i = varRef("i") val v = varRef("v") + val setName = genArrayClassPropertyForDef(ArrayClassProperty.set) + val boundsCheck = condTree(arrayIndexOutOfBounds != CheckedBehavior.Unchecked) { If((i < 0) || (i >= This().u.length), genCallHelper(VarField.throwArrayIndexOutOfBoundsException, i)) } val storeCheck = { - If((v !== Null()) && !(componentData DOT "isJSType") && - !Apply(genIdentBracketSelect(componentData, "isInstance"), v :: Nil), + If((v !== Null()) && !(componentData DOT cpn.isJSType) && + !Apply(genIdentBracketSelect(componentData, cpn.isInstance), v :: Nil), genCallHelper(VarField.throwArrayStoreException, v)) } List( - MethodDef(static = false, Ident("set"), paramList(i, v), None, { + MethodDef(static = false, setName, paramList(i, v), None, { Block( boundsCheck, storeCheck, @@ -1796,7 +1811,10 @@ private[emitter] object CoreJSLib { val dest = varRef("dest") val destPos = varRef("destPos") val length = varRef("length") - val methodDef = MethodDef(static = false, Ident("copyTo"), + + val copyToName = genArrayClassPropertyForDef(ArrayClassProperty.copyTo) + + val methodDef = MethodDef(static = false, copyToName, paramList(srcPos, dest, destPos, length), None, { genCallHelper(VarField.arraycopyGeneric, This().u, srcPos, dest.u, destPos, length) @@ -1806,7 +1824,8 @@ private[emitter] object CoreJSLib { Nil } - val clone = MethodDef(static = false, Ident(genMethodName(cloneMethodName)), Nil, None, { + val cloneMethodIdent = genMethodIdentForDef(cloneMethodName, NoOriginalName) + val clone = MethodDef(static = false, cloneMethodIdent, Nil, None, { Return(New(ArrayClass, Apply(genIdentBracketSelect(This().u, "slice"), Nil) :: Nil)) }) @@ -1828,28 +1847,28 @@ private[emitter] object CoreJSLib { Block( ArrayClassDef, - const(arrayBase, (componentData DOT "arrayBase") || componentData), - const(arrayDepth, (componentData DOT "arrayDepth") + 1), + const(arrayBase, (componentData DOT cpn.arrayBase) || componentData), + const(arrayDepth, (componentData DOT cpn.arrayDepth) + 1), initArrayCommonBody(ArrayClass, componentData, arrayBase, arrayDepth), const(isAssignableFromFun, { genArrowFunction(paramList(that), { val thatDepth = varRef("thatDepth") Block( - const(thatDepth, that DOT "arrayDepth"), + const(thatDepth, that DOT cpn.arrayDepth), Return(If(thatDepth === arrayDepth, { - Apply(arrayBase DOT "isAssignableFromFun", (that DOT "arrayBase") :: Nil) + Apply(arrayBase DOT cpn.isAssignableFromFun, (that DOT cpn.arrayBase) :: Nil) }, { (thatDepth > arrayDepth) && (arrayBase === genClassDataOf(ObjectClass)) })) ) }) }), - privateFieldSet("isAssignableFromFun", isAssignableFromFun), - privateFieldSet("wrapArray", genArrowFunction(paramList(array), { + privateFieldSet(cpn.isAssignableFromFun, isAssignableFromFun), + privateFieldSet(cpn.wrapArray, genArrowFunction(paramList(array), { Return(New(ArrayClass, array :: Nil)) })), const(self, This()), // don't rely on the lambda being called with `this` as receiver - publicFieldSet("isInstance", genArrowFunction(paramList(obj), { + publicFieldSet(cpn.isInstance, genArrowFunction(paramList(obj), { val data = varRef("data") Block( const(data, obj && (obj DOT classData)), @@ -1865,24 +1884,24 @@ private[emitter] object CoreJSLib { } val getArrayOf = { - MethodDef(static = false, Ident("getArrayOf"), Nil, None, { + MethodDef(static = false, Ident(cpn.getArrayOf), Nil, None, { Block( - If(!(This() DOT "_arrayOf"), - This() DOT "_arrayOf" := - Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT "initArray", This() :: Nil), + If(!(This() DOT cpn._arrayOf), + This() DOT cpn._arrayOf := + Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT cpn.initArray, This() :: Nil), Skip()), - Return(This() DOT "_arrayOf") + Return(This() DOT cpn._arrayOf) ) }) } def getClassOf = { - MethodDef(static = false, Ident("getClassOf"), Nil, None, { + MethodDef(static = false, Ident(cpn.getClassOf), Nil, None, { Block( - If(!(This() DOT "_classOf"), - This() DOT "_classOf" := genScalaClassNew(ClassClass, ObjectArgConstructorName, This()), + If(!(This() DOT cpn._classOf), + This() DOT cpn._classOf := genScalaClassNew(ClassClass, ObjectArgConstructorName, This()), Skip()), - Return(This() DOT "_classOf") + Return(This() DOT cpn._classOf) ) }) } @@ -1898,21 +1917,21 @@ private[emitter] object CoreJSLib { * We only need a polymorphic dispatch in the slow path. */ val that = varRef("that") - MethodDef(static = false, StringLiteral("isAssignableFrom"), + MethodDef(static = false, StringLiteral(cpn.isAssignableFrom), paramList(that), None, { Return( (This() === that) || // fast path - Apply(This() DOT "isAssignableFromFun", that :: Nil)) + Apply(This() DOT cpn.isAssignableFromFun, that :: Nil)) }) } def checkCast = { val obj = varRef("obj") - MethodDef(static = false, StringLiteral("checkCast"), paramList(obj), None, + MethodDef(static = false, StringLiteral(cpn.checkCast), paramList(obj), None, if (asInstanceOfs != CheckedBehavior.Unchecked) { - If((obj !== Null()) && !(This() DOT "isJSType") && - !Apply(genIdentBracketSelect(This(), "isInstance"), obj :: Nil), - genCallHelper(VarField.throwClassCastException, obj, genIdentBracketSelect(This(), "name")), + If((obj !== Null()) && !(This() DOT cpn.isJSType) && + !Apply(genIdentBracketSelect(This(), cpn.isInstance), obj :: Nil), + genCallHelper(VarField.throwClassCastException, obj, genIdentBracketSelect(This(), cpn.name)), Skip()) } else { Skip() @@ -1921,17 +1940,17 @@ private[emitter] object CoreJSLib { } def getSuperclass = { - MethodDef(static = false, StringLiteral("getSuperclass"), Nil, None, { - Return(If(This() DOT "parentData", - Apply(This() DOT "parentData" DOT "getClassOf", Nil), + MethodDef(static = false, StringLiteral(cpn.getSuperclass), Nil, None, { + Return(If(This() DOT cpn.parentData, + Apply(This() DOT cpn.parentData DOT cpn.getClassOf, Nil), Null())) }) } def getComponentType = { - MethodDef(static = false, StringLiteral("getComponentType"), Nil, None, { - Return(If(This() DOT "componentData", - Apply(This() DOT "componentData" DOT "getClassOf", Nil), + MethodDef(static = false, StringLiteral(cpn.getComponentType), Nil, None, { + Return(If(This() DOT cpn.componentData, + Apply(This() DOT cpn.componentData DOT cpn.getClassOf, Nil), Null())) }) } @@ -1940,12 +1959,12 @@ private[emitter] object CoreJSLib { val lengths = varRef("lengths") val arrayClassData = varRef("arrayClassData") val i = varRef("i") - MethodDef(static = false, StringLiteral("newArrayOfThisClass"), + MethodDef(static = false, StringLiteral(cpn.newArrayOfThisClass), paramList(lengths), None, { Block( let(arrayClassData, This()), For(let(i, 0), i < lengths.length, i.++, { - arrayClassData := Apply(arrayClassData DOT "getArrayOf", Nil) + arrayClassData := Apply(arrayClassData DOT cpn.getArrayOf, Nil) }), Return(genCallHelper(VarField.newArrayObject, arrayClassData, lengths)) ) @@ -1994,14 +2013,14 @@ private[emitter] object CoreJSLib { val forObj = extractWithGlobals(globalFunctionDef(VarField.isArrayOf, ObjectClass, paramList(obj, depth), None, { Block( - const(data, obj && (obj DOT "$classData")), + const(data, obj && (obj DOT cpn.classData)), If(!data, { Return(BooleanLiteral(false)) }, { Block( - const(arrayDepth, data DOT "arrayDepth"), + const(arrayDepth, data DOT cpn.arrayDepth), Return(If(arrayDepth === depth, { - !genIdentBracketSelect(data DOT "arrayBase", "isPrimitive") + !genIdentBracketSelect(data DOT cpn.arrayBase, cpn.isPrimitive) }, { arrayDepth > depth })) @@ -2015,8 +2034,8 @@ private[emitter] object CoreJSLib { val depth = varRef("depth") extractWithGlobals(globalFunctionDef(VarField.isArrayOf, primRef, paramList(obj, depth), None, { Return(!(!(obj && (obj DOT classData) && - ((obj DOT classData DOT "arrayDepth") === depth) && - ((obj DOT classData DOT "arrayBase") === genClassDataOf(primRef))))) + ((obj DOT classData DOT cpn.arrayDepth) === depth) && + ((obj DOT classData DOT cpn.arrayBase) === genClassDataOf(primRef))))) })) } @@ -2070,27 +2089,27 @@ private[emitter] object CoreJSLib { extractWithGlobals( globalVarDef(VarField.d, ObjectClass, New(globalVar(VarField.TypeData, CoreVar), Nil))) ::: List( - privateFieldSet("ancestors", ObjectConstr(Nil)), - privateFieldSet("arrayEncodedName", str("L" + fullName + ";")), - privateFieldSet("isAssignableFromFun", { + privateFieldSet(cpn.ancestors, ObjectConstr(Nil)), + privateFieldSet(cpn.arrayEncodedName, str("L" + fullName + ";")), + privateFieldSet(cpn.isAssignableFromFun, { genArrowFunction(paramList(that), { - Return(!genIdentBracketSelect(that, "isPrimitive")) + Return(!genIdentBracketSelect(that, cpn.isPrimitive)) }) }), - publicFieldSet("name", str(fullName)), - publicFieldSet("isInstance", + publicFieldSet(cpn.name, str(fullName)), + publicFieldSet(cpn.isInstance, genArrowFunction(paramList(obj), Return(obj !== Null()))), - privateFieldSet("_arrayOf", { - Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT "initSpecializedArray", List( + privateFieldSet(cpn._arrayOf, { + Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT cpn.initSpecializedArray, List( typeDataVar, globalVar(VarField.ac, ObjectClass), Undefined(), // typedArray genArrowFunction(paramList(that), { val thatDepth = varRef("thatDepth") Block( - const(thatDepth, that DOT "arrayDepth"), + const(thatDepth, that DOT cpn.arrayDepth), Return(If(thatDepth === 1, { - !genIdentBracketSelect(that DOT "arrayBase", "isPrimitive") + !genIdentBracketSelect(that DOT cpn.arrayBase, cpn.isPrimitive) }, { (thatDepth > 1) })) @@ -2098,7 +2117,7 @@ private[emitter] object CoreJSLib { }) )) }), - globalVar(VarField.c, ObjectClass).prototype DOT "$classData" := typeDataVar + globalVar(VarField.c, ObjectClass).prototype DOT cpn.classData := typeDataVar ) } @@ -2124,7 +2143,7 @@ private[emitter] object CoreJSLib { } extractWithGlobals(globalVarDef(VarField.d, primRef, { - Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT "initPrim", + Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT cpn.initPrim, List(zero, str(primRef.charCode.toString()), str(primRef.displayName), if (primRef == VoidRef) Undefined() @@ -2232,5 +2251,10 @@ private[emitter] object CoreJSLib { private def double(d: Double): DoubleLiteral = DoubleLiteral(d) private def bigInt(i: Long): BigIntLiteral = BigIntLiteral(i) + + // cannot extend AnyVal because this is not a static class + private implicit class CustomTreeOps(private val self: Tree) { + def u: Tree = genArrayClassPropSelect(self, ArrayClassProperty.u) + } } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index 1a8b9bc517..506dec4d4a 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -39,6 +39,9 @@ final class Emitter[E >: Null <: js.Tree]( import Emitter._ import config._ + require(!config.minify || postTransformer == PostTransformer.Identity, + "When using the 'minify' option, the postTransformer must be Identity.") + private implicit val globalRefTracking: GlobalRefTracking = config.topLevelGlobalRefTracking @@ -49,10 +52,14 @@ final class Emitter[E >: Null <: js.Tree]( private val nameGen: NameGen = new NameGen private class State(val lastMentionedDangerousGlobalRefs: Set[String]) { + val nameCompressor = + if (minify) Some(new NameCompressor(config)) + else None + val sjsGen: SJSGen = { val jsGen = new JSGen(config) val varGen = new VarGen(jsGen, nameGen, lastMentionedDangerousGlobalRefs) - new SJSGen(jsGen, nameGen, varGen) + new SJSGen(jsGen, nameGen, varGen, nameCompressor) } val classEmitter: ClassEmitter = new ClassEmitter(sjsGen) @@ -87,7 +94,7 @@ final class Emitter[E >: Null <: js.Tree]( def emit(moduleSet: ModuleSet, logger: Logger): Result[E] = { val WithGlobals(body, globalRefs) = emitInternal(moduleSet, logger) - moduleKind match { + val result = moduleKind match { case ModuleKind.NoModule => assert(moduleSet.modules.size <= 1) val topLevelVars = moduleSet.modules @@ -112,6 +119,19 @@ final class Emitter[E >: Null <: js.Tree]( case ModuleKind.ESModule | ModuleKind.CommonJSModule => new Result(config.jsHeader, body, "", Nil, globalRefs) } + + for (compressor <- state.nameCompressor) { + compressor.allocateNames(moduleSet, logger) + + /* Throw away the whole state, but keep the mentioned dangerous global refs. + * Note that instances of the name compressor's entries are still alive + * at this point, since they are referenced from `DelayedIdent` nodes in + * the result trees. + */ + state = new State(state.lastMentionedDangerousGlobalRefs) + } + + result } private def emitInternal(moduleSet: ModuleSet, @@ -1084,7 +1104,8 @@ object Emitter { val jsHeader: String, val internalModulePattern: ModuleID => String, val optimizeBracketSelects: Boolean, - val trackAllGlobalRefs: Boolean + val trackAllGlobalRefs: Boolean, + val minify: Boolean ) { private def this( semantics: Semantics, @@ -1097,7 +1118,9 @@ object Emitter { jsHeader = "", internalModulePattern = "./" + _.id, optimizeBracketSelects = true, - trackAllGlobalRefs = false) + trackAllGlobalRefs = false, + minify = false + ) } private[emitter] val topLevelGlobalRefTracking: GlobalRefTracking = @@ -1127,6 +1150,9 @@ object Emitter { def withTrackAllGlobalRefs(trackAllGlobalRefs: Boolean): Config = copy(trackAllGlobalRefs = trackAllGlobalRefs) + def withMinify(minify: Boolean): Config = + copy(minify = minify) + private def copy( semantics: Semantics = semantics, moduleKind: ModuleKind = moduleKind, @@ -1134,9 +1160,12 @@ object Emitter { jsHeader: String = jsHeader, internalModulePattern: ModuleID => String = internalModulePattern, optimizeBracketSelects: Boolean = optimizeBracketSelects, - trackAllGlobalRefs: Boolean = trackAllGlobalRefs): Config = { + trackAllGlobalRefs: Boolean = trackAllGlobalRefs, + minify: Boolean = minify + ): Config = { new Config(semantics, moduleKind, esFeatures, jsHeader, - internalModulePattern, optimizeBracketSelects, trackAllGlobalRefs) + internalModulePattern, optimizeBracketSelects, trackAllGlobalRefs, + minify) } } @@ -1149,6 +1178,12 @@ object Emitter { def transformStats(trees: List[js.Tree], indent: Int): List[E] } + object PostTransformer { + object Identity extends PostTransformer[js.Tree] { + def transformStats(trees: List[js.Tree], indent: Int): List[js.Tree] = trees + } + } + private final class DesugaredClassCache[E >: Null] { val privateJSFields = new OneTimeCache[WithGlobals[E]] val storeJSSuperClass = new OneTimeCache[WithGlobals[E]] diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala index 32b8baca4f..a518948b97 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala @@ -632,12 +632,11 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { } if (checked) { - js.Apply(js.DotSelect(genArray, js.Ident("set")), - List(genIndex, genRhs)) + genArrayClassPropApply(genArray, ArrayClassProperty.set, genIndex, genRhs) } else { js.Assign( js.BracketSelect( - js.DotSelect(genArray, js.Ident("u"))(lhs.pos), + genArrayClassPropSelect(genArray, ArrayClassProperty.u)(lhs.pos), genIndex)(lhs.pos), genRhs) } @@ -875,12 +874,19 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { implicit val env = env0 val jsArgs = newArgs.map(transformExprNoChar(_)) + def genUnchecked(): js.Tree = { + if (esFeatures.esVersion >= ESVersion.ES2015 && semantics.nullPointers == CheckedBehavior.Unchecked) + genArrayClassPropApply(jsArgs.head, ArrayClassProperty.copyTo, jsArgs.tail) + else + genCallHelper(VarField.systemArraycopy, jsArgs: _*) + } + if (semantics.arrayStores == Unchecked) { - genUncheckedArraycopy(jsArgs) + genUnchecked() } else { (src.tpe, dest.tpe) match { case (PrimArray(srcPrimRef), PrimArray(destPrimRef)) if srcPrimRef == destPrimRef => - genUncheckedArraycopy(jsArgs) + genUnchecked() case (RefArray(), RefArray()) => genCallHelper(VarField.systemArraycopyRefs, jsArgs: _*) case _ => @@ -2245,7 +2251,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { val newArgs = transformTypedArgs(method.name, args) def genNormalApply(): js.Tree = - js.Apply(newReceiver(false) DOT transformMethodIdent(method), newArgs) + js.Apply(newReceiver(false) DOT genMethodIdent(method), newArgs) def genDispatchApply(): js.Tree = js.Apply(globalVar(VarField.dp, methodName), newReceiver(false) :: newArgs) @@ -2308,7 +2314,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { genApplyStaticLike(VarField.f, className, method, transformedArgs) } else { val fun = - globalVar(VarField.c, className).prototype DOT transformMethodIdent(method) + globalVar(VarField.c, className).prototype DOT genMethodIdent(method) js.Apply(fun DOT "call", transformedArgs) } @@ -2650,17 +2656,19 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { genArrayValue(typeRef, elems.map(transformExpr(_, preserveChar)))) case ArrayLength(array) => - genIdentBracketSelect(js.DotSelect(transformExprNoChar(checkNotNull(array)), - js.Ident("u")), "length") + val newArray = transformExprNoChar(checkNotNull(array)) + genIdentBracketSelect( + genArrayClassPropSelect(newArray, ArrayClassProperty.u), + "length") case ArraySelect(array, index) => val newArray = transformExprNoChar(checkNotNull(array)) val newIndex = transformExprNoChar(index) semantics.arrayIndexOutOfBounds match { case CheckedBehavior.Compliant | CheckedBehavior.Fatal => - js.Apply(js.DotSelect(newArray, js.Ident("get")), List(newIndex)) + genArrayClassPropApply(newArray, ArrayClassProperty.get, newIndex) case CheckedBehavior.Unchecked => - js.BracketSelect(js.DotSelect(newArray, js.Ident("u")), newIndex) + js.BracketSelect(genArrayClassPropSelect(newArray, ArrayClassProperty.u), newIndex) } case tree: RecordSelect => @@ -2736,7 +2744,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { js.DotSelect( genSelect(transformExprNoChar(checkNotNull(runtimeClass)), FieldIdent(dataFieldName)), - js.Ident("zero")) + js.Ident(cpn.zero)) case Transient(NativeArrayWrapper(elemClass, nativeArray)) => val newNativeArray = transformExprNoChar(nativeArray) @@ -2750,8 +2758,8 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { transformExprNoChar(checkNotNull(elemClass)), FieldIdent(dataFieldName)) val arrayClassData = js.Apply( - js.DotSelect(elemClassData, js.Ident("getArrayOf")), Nil) - js.Apply(arrayClassData DOT "wrapArray", newNativeArray :: Nil) + js.DotSelect(elemClassData, js.Ident(cpn.getArrayOf)), Nil) + js.Apply(arrayClassData DOT cpn.wrapArray, newNativeArray :: Nil) } case Transient(ObjectClassName(obj)) => @@ -2759,12 +2767,13 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case Transient(ArrayToTypedArray(expr, primRef)) => val value = transformExprNoChar(checkNotNull(expr)) + val valueUnderlying = genArrayClassPropSelect(value, ArrayClassProperty.u) if (es2015) { - js.Apply(genIdentBracketSelect(value.u, "slice"), Nil) + js.Apply(genIdentBracketSelect(valueUnderlying, "slice"), Nil) } else { val typedArrayClass = extractWithGlobals(typedArrayRef(primRef).get) - js.New(typedArrayClass, value.u :: Nil) + js.New(typedArrayClass, valueUnderlying :: Nil) } case Transient(TypedArrayToArray(expr, primRef)) => @@ -3200,9 +3209,6 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { private def transformLabelIdent(ident: LabelIdent): js.Ident = js.Ident(genName(ident.name))(ident.pos) - private def transformMethodIdent(ident: MethodIdent): js.Ident = - js.Ident(genMethodName(ident.name))(ident.pos) - private def transformLocalVarIdent(ident: LocalIdent): js.Ident = js.Ident(transformLocalName(ident.name))(ident.pos) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala index 9d0150c2c9..4da09323c5 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala @@ -148,9 +148,9 @@ private[emitter] final class JSGen(val config: Emitter.Config) { def genPropSelect(qual: Tree, item: PropertyName)( implicit pos: Position): Tree = { item match { - case item: Ident => DotSelect(qual, item) - case item: StringLiteral => genBracketSelect(qual, item) - case ComputedName(tree) => genBracketSelect(qual, tree) + case item: MaybeDelayedIdent => DotSelect(qual, item) + case item: StringLiteral => genBracketSelect(qual, item) + case ComputedName(tree) => genBracketSelect(qual, tree) } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameCompressor.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameCompressor.scala new file mode 100644 index 0000000000..0db5ead5bf --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameCompressor.scala @@ -0,0 +1,256 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.linker.backend.emitter + +import scala.annotation.{switch, tailrec} + +import java.util.Comparator + +import scala.collection.mutable +import scala.reflect.ClassTag + +import org.scalajs.ir.Names._ +import org.scalajs.linker.backend.javascript.Trees.DelayedIdent.Resolver +import org.scalajs.linker.standard.ModuleSet +import org.scalajs.logging.Logger + +private[emitter] final class NameCompressor(config: Emitter.Config) { + import NameCompressor._ + + private val entries: EntryMap = mutable.AnyRefMap.empty + private val ancestorEntries: AncestorEntryMap = mutable.AnyRefMap.empty + + private var namesAllocated: Boolean = false + + def allocateNames(moduleSet: ModuleSet, logger: Logger): Unit = { + assert(!namesAllocated, "Cannot allocate names a second time") + + val propertyNamesToAvoid = logger.time("Name compressor: Collect property names to avoid") { + collectPropertyNamesToAvoid(moduleSet) + } + + logger.time("Name compressor: Allocate property names") { + allocatePropertyNames(entries, propertyNamesToAvoid) + } + + logger.time("Name compressor: Allocate ancestor names") { + allocatePropertyNames(ancestorEntries, BasePropertyNamesToAvoid) + } + + namesAllocated = true + } + + def genResolverFor(fieldName: FieldName): Resolver = + entries.getOrElseUpdate(fieldName, new FieldNameEntry(fieldName)).genResolver() + + def genResolverFor(methodName: MethodName): Resolver = + entries.getOrElseUpdate(methodName, new MethodNameEntry(methodName)).genResolver() + + def genResolverFor(prop: ArrayClassProperty): Resolver = + entries.getOrElseUpdate(prop, new ArrayClassPropEntry(prop)).genResolver() + + def genResolverForAncestor(ancestor: ClassName): Resolver = + ancestorEntries.getOrElseUpdate(ancestor, new AncestorNameEntry(ancestor)).genResolver() + + /** Collects the property names to avoid for Scala instance members. + * + * We collect the names of exported members in Scala classes. These live in + * the same namespace as Scala methods and fields. Therefore, we must avoid + * them when allocating names for that namespace. + */ + private def collectPropertyNamesToAvoid(moduleSet: ModuleSet): Set[String] = { + import org.scalajs.ir.Trees._ + + val builder = Set.newBuilder[String] + + builder ++= BasePropertyNamesToAvoid + + for { + module <- moduleSet.modules + linkedClass <- module.classDefs + if linkedClass.kind.isClass + exportedMember <- linkedClass.exportedMembers + } { + (exportedMember: @unchecked) match { + case JSMethodDef(_, StringLiteral(name), _, _, _) => + builder += name + case JSPropertyDef(_, StringLiteral(name), _, _) => + builder += name + } + } + + builder.result() + } +} + +private[emitter] object NameCompressor { + /** Base set of names that should be avoided when allocating property names + * in any namespace. + * + * This set contains: + * + * - the reserved JS identifiers (not technically invalid by spec, but JS + * minifiers tend to avoid them anyway: `foo.if` is playing with fire), + * - the `"then"` name, because it is used to identify `Thenable`s by + * spec and therefore lives in the same namespace as the properties of + * *all* objects, + */ + private val BasePropertyNamesToAvoid: Set[String] = + NameGen.ReservedJSIdentifierNames + "then" + + private def allocatePropertyNames[K <: AnyRef, E <: BaseEntry with Comparable[E]: ClassTag]( + entries: mutable.AnyRefMap[K, E], namesToAvoid: collection.Set[String]): Unit = { + val comparator: Comparator[E] = + Comparator.comparingInt[E](_.occurrences).reversed() // by decreasing order of occurrences + .thenComparing(Comparator.naturalOrder[E]()) // tie-break + + val orderedEntries = entries.values.toArray + java.util.Arrays.sort(orderedEntries, comparator) + + val generator = new NameGenerator(namesToAvoid) + + for (entry <- orderedEntries) + entry.allocatedName = generator.nextString() + } + + /** Keys of this map are `FieldName | MethodName | ArrayClassProperty`. */ + private type EntryMap = mutable.AnyRefMap[AnyRef, PropertyNameEntry] + + private type AncestorEntryMap = mutable.AnyRefMap[ClassName, AncestorNameEntry] + + private sealed abstract class BaseEntry { + var occurrences: Int = 0 + var allocatedName: String = null + + protected def debugString: String + + private object resolver extends Resolver { + def resolve(): String = { + if (allocatedName == null) + throw new IllegalStateException(s"Cannot resolve name before it was allocated, for $this") + allocatedName + } + + def debugString: String = BaseEntry.this.debugString + + override def toString(): String = debugString + } + + private def incOccurrences(): Unit = { + if (allocatedName != null) + throw new IllegalStateException(s"Cannot increase occurrences after name was allocated for $this") + occurrences += 1 + } + + def genResolver(): Resolver = { + incOccurrences() + resolver + } + } + + private sealed abstract class PropertyNameEntry + extends BaseEntry with Comparable[PropertyNameEntry] { + + def compareTo(that: PropertyNameEntry): Int = (this, that) match { + case (x: FieldNameEntry, y: FieldNameEntry) => + x.fieldName.compareTo(y.fieldName) + + case (x: MethodNameEntry, y: MethodNameEntry) => + x.methodName.compareTo(y.methodName) + + case (x: ArrayClassPropEntry, y: ArrayClassPropEntry) => + x.property.compareTo(y.property) + + case _ => + def ordinalFor(x: PropertyNameEntry): Int = x match { + case _: FieldNameEntry => 1 + case _: MethodNameEntry => 2 + case _: ArrayClassPropEntry => 3 + } + ordinalFor(this) - ordinalFor(that) + } + } + + private final class FieldNameEntry(val fieldName: FieldName) + extends PropertyNameEntry { + protected def debugString: String = fieldName.nameString + + override def toString(): String = s"FieldNameEntry(${fieldName.nameString})" + } + + private final class MethodNameEntry(val methodName: MethodName) + extends PropertyNameEntry { + protected def debugString: String = methodName.nameString + + override def toString(): String = s"MethodNameEntry(${methodName.nameString})" + } + + private final class ArrayClassPropEntry(val property: ArrayClassProperty) + extends PropertyNameEntry { + protected def debugString: String = property.nonMinifiedName + + override def toString(): String = s"ArrayClassPropEntry(${property.nonMinifiedName})" + } + + private final class AncestorNameEntry(val ancestor: ClassName) + extends BaseEntry with Comparable[AncestorNameEntry] { + + def compareTo(that: AncestorNameEntry): Int = + this.ancestor.compareTo(that.ancestor) + + protected def debugString: String = ancestor.nameString + + override def toString(): String = s"AncestorNameEntry(${ancestor.nameString})" + } + + // private[emitter] for tests + private[emitter] final class NameGenerator(namesToAvoid: collection.Set[String]) { + /* 6 because 52 * (62**5) > Int.MaxValue + * i.e., to exceed this size we would need more than Int.MaxValue different names. + */ + private val charArray = new Array[Char](6) + charArray(0) = 'a' + private var charCount = 1 + + @tailrec + private def incAtIndex(idx: Int): Unit = { + (charArray(idx): @switch) match { + case '9' => + charArray(idx) = 'a' + case 'z' => + charArray(idx) = 'A' + case 'Z' => + if (idx > 0) { + charArray(idx) = '0' + incAtIndex(idx - 1) + } else { + java.util.Arrays.fill(charArray, '0') + charArray(0) = 'a' + charCount += 1 + } + case c => + charArray(idx) = (c + 1).toChar + } + } + + @tailrec + final def nextString(): String = { + val s = new String(charArray, 0, charCount) + incAtIndex(charCount - 1) + if (namesToAvoid.contains(s)) + nextString() + else + s + } + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala index 552dd545bd..ae8502211d 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala @@ -301,7 +301,7 @@ private[emitter] final class NameGen { } } -private object NameGen { +private[emitter] object NameGen { private final val FullwidthSpacingUnderscore = '\uff3f' private final val GreekSmallLetterDelta = '\u03b4' @@ -371,7 +371,7 @@ private object NameGen { * not actually mean `void 0`, and who knows what JS engine performance * cliffs we can trigger with that. */ - private final val ReservedJSIdentifierNames: Set[String] = Set( + private[emitter] final val ReservedJSIdentifierNames: Set[String] = Set( "arguments", "await", "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do", "else", "enum", "eval", "export", "extends", "false", "finally", "for", "function", "if", diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SJSGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SJSGen.scala index 4541d3a292..b52be22348 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SJSGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SJSGen.scala @@ -32,7 +32,8 @@ import PolyfillableBuiltin._ private[emitter] final class SJSGen( val jsGen: JSGen, val nameGen: NameGen, - val varGen: VarGen + val varGen: VarGen, + val nameCompressor: Option[NameCompressor] ) { import jsGen._ @@ -42,6 +43,108 @@ private[emitter] final class SJSGen( val useBigIntForLongs = esFeatures.allowBigIntsForLongs + /** Core Property Names. */ + object cpn { + // --- Scala.js objects --- + + /** The class-wide classData field of Scala.js objects, which references their TypeData. */ + val classData = "$classData" // always in full; it is used as identification of Scala.js objects + + // --- Class --- + + /** `Char.c`: the int value of the character. */ + val c = "c" + + // --- TypeData private fields --- + + /** `TypeData.constr`: the run-time constructor of the class. */ + val constr = if (minify) "C" else "constr" + + /** `TypeData.parentData`: the super class data. */ + val parentData = if (minify) "P" else "parentData" + + /** `TypeData.ancestors`: dictionary where keys are the ancestor names of all ancestors. */ + val ancestors = if (minify) "n" else "ancestors" + + /** `TypeData.componentData`: the `TypeData` of the component type of an array type. */ + val componentData = if (minify) "O" else "componentData" + + /** `TypeData.arrayBase`: the `TypeData` of the base type of an array type. */ + val arrayBase = if (minify) "B" else "arrayBase" + + /** `TypeData.arrayDepth`: the depth of an array type. */ + val arrayDepth = if (minify) "D" else "arrayDepth" + + /** `TypeData.zero`: the zero value of the type. */ + val zero = if (minify) "z" else "zero" + + /** `TypeData.arrayEncodedName`: the name of the type as it appears in its array type's name. */ + val arrayEncodedName = if (minify) "E" else "arrayEncodedName" + + /** `TypeData._classOf`: the field storing the `jl.Class` instance for that type. */ + val _classOf = if (minify) "L" else "_classOf" + + /** `TypeData._arrayOf`: the field storing the `TypeData` for that type's array type. */ + val _arrayOf = if (minify) "A" else "_arrayOf" + + /** `TypeData.isAssignableFromFun`: the implementation of `jl.Class.isAssignableFrom` without fast path. */ + val isAssignableFromFun = if (minify) "F" else "isAssignableFromFun" + + /** `TypeData.wrapArray`: the function to create an ArrayClass instance from a JS array of its elements. */ + val wrapArray = if (minify) "w" else "wrapArray" + + /** `TypeData.isJSType`: whether it is a JS type. */ + val isJSType = if (minify) "J" else "isJSType" + + // --- TypeData constructors --- + + val initPrim = if (minify) "p" else "initPrim" + + val initClass = if (minify) "i" else "initClass" + + val initSpecializedArray = if (minify) "y" else "initSpecializedArray" + + val initArray = if (minify) "a" else "initArray" + + // --- TypeData private methods --- + + /** `TypeData.getArrayOf()`: the `Type` instance for that type's array type. */ + val getArrayOf = if (minify) "r" else "getArrayOf" + + /** `TypeData.getClassOf()`: the `jl.Class` instance for that type. */ + val getClassOf = if (minify) "l" else "getClassOf" + + // --- TypeData public fields --- never minified + + /** `TypeData.name`: public, the user name of the class (the result of `jl.Class.getName()`). */ + val name = "name" + + /** `TypeData.isPrimitive`: public, whether it is a primitive type. */ + val isPrimitive = "isPrimitive" + + /** `TypeData.isInterface`: public, whether it is an interface type. */ + val isInterface = "isInterface" + + /** `TypeData.isArrayClass`: public, whether it is an array type. */ + val isArrayClass = "isArrayClass" + + /** `TypeData.isInstance()`: public, implementation of `jl.Class.isInstance`. */ + val isInstance = "isInstance" + + /** `TypeData.isAssignableFrom()`: public, implementation of `jl.Class.isAssignableFrom`. */ + val isAssignableFrom = "isAssignableFrom" + + // --- TypeData public methods --- never minified + + val checkCast = "checkCast" + + val getSuperclass = "getSuperclass" + + val getComponentType = "getComponentType" + + val newArrayOfThisClass = "newArrayOfThisClass" + } + def genZeroOf(tpe: Type)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): Tree = { @@ -149,36 +252,43 @@ private[emitter] final class SJSGen( } } - def genUncheckedArraycopy(args: List[Tree])( - implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, - pos: Position): Tree = { - import TreeDSL._ - - assert(args.lengthCompare(5) == 0, - s"wrong number of args for genUncheckedArrayCopy: $args") - - if (esFeatures.esVersion >= ESVersion.ES2015 && semantics.nullPointers == CheckedBehavior.Unchecked) - Apply(args.head DOT "copyTo", args.tail) - else - genCallHelper(VarField.systemArraycopy, args: _*) - } - def genSelect(receiver: Tree, field: irt.FieldIdent)( implicit pos: Position): Tree = { - DotSelect(receiver, Ident(genName(field.name))(field.pos)) + DotSelect(receiver, genFieldIdent(field.name)(field.pos)) } - def genSelect(receiver: Tree, field: irt.FieldIdent, + def genSelectForDef(receiver: Tree, field: irt.FieldIdent, originalName: OriginalName)( implicit pos: Position): Tree = { - val jsName = genName(field.name) - val jsOrigName = genOriginalName(field.name, originalName, jsName) - DotSelect(receiver, Ident(jsName, jsOrigName)(field.pos)) + DotSelect(receiver, genFieldIdentForDef(field.name, originalName)(field.pos)) + } + + private def genFieldIdent(fieldName: FieldName)( + implicit pos: Position): MaybeDelayedIdent = { + nameCompressor match { + case None => + Ident(genName(fieldName)) + case Some(compressor) => + DelayedIdent(compressor.genResolverFor(fieldName)) + } + } + + private def genFieldIdentForDef(fieldName: FieldName, + originalName: OriginalName)( + implicit pos: Position): MaybeDelayedIdent = { + nameCompressor match { + case None => + val jsName = genName(fieldName) + val jsOrigName = genOriginalName(fieldName, originalName, jsName) + Ident(jsName, jsOrigName) + case Some(compressor) => + DelayedIdent(compressor.genResolverFor(fieldName), originalName.orElse(fieldName)) + } } def genApply(receiver: Tree, methodName: MethodName, args: List[Tree])( implicit pos: Position): Tree = { - Apply(DotSelect(receiver, Ident(genMethodName(methodName))), args) + Apply(DotSelect(receiver, genMethodIdent(methodName)), args) } def genApply(receiver: Tree, methodName: MethodName, args: Tree*)( @@ -186,8 +296,68 @@ private[emitter] final class SJSGen( genApply(receiver, methodName, args.toList) } - def genMethodName(methodName: MethodName): String = - genName(methodName) + def genMethodIdent(methodIdent: irt.MethodIdent): MaybeDelayedIdent = + genMethodIdent(methodIdent.name)(methodIdent.pos) + + def genMethodIdentForDef(methodIdent: irt.MethodIdent, + originalName: OriginalName): MaybeDelayedIdent = { + genMethodIdentForDef(methodIdent.name, originalName)(methodIdent.pos) + } + + def genMethodIdent(methodName: MethodName)(implicit pos: Position): MaybeDelayedIdent = { + nameCompressor match { + case None => Ident(genName(methodName)) + case Some(compressor) => DelayedIdent(compressor.genResolverFor(methodName)) + } + } + + def genMethodIdentForDef(methodName: MethodName, originalName: OriginalName)( + implicit pos: Position): MaybeDelayedIdent = { + nameCompressor match { + case None => + val jsName = genName(methodName) + val jsOrigName = genOriginalName(methodName, originalName, jsName) + Ident(jsName, jsOrigName) + case Some(compressor) => + DelayedIdent(compressor.genResolverFor(methodName), originalName.orElse(methodName)) + } + } + + def genArrayClassPropApply(receiver: Tree, prop: ArrayClassProperty, args: Tree*)( + implicit pos: Position): Tree = { + genArrayClassPropApply(receiver, prop, args.toList) + } + + def genArrayClassPropApply(receiver: Tree, prop: ArrayClassProperty, args: List[Tree])( + implicit pos: Position): Tree = { + Apply(genArrayClassPropSelect(receiver, prop), args) + } + + def genArrayClassPropSelect(qualifier: Tree, prop: ArrayClassProperty)( + implicit pos: Position): Tree = { + DotSelect(qualifier, genArrayClassProperty(prop)) + } + + def genArrayClassProperty(prop: ArrayClassProperty)(implicit pos: Position): MaybeDelayedIdent = { + nameCompressor match { + case None => Ident(prop.nonMinifiedName) + case Some(compressor) => DelayedIdent(compressor.genResolverFor(prop)) + } + } + + def genArrayClassPropertyForDef(prop: ArrayClassProperty)(implicit pos: Position): MaybeDelayedIdent = { + nameCompressor match { + case None => Ident(prop.nonMinifiedName) + case Some(compressor) => DelayedIdent(compressor.genResolverFor(prop), prop.originalName) + } + } + + def genAncestorIdent(ancestor: ClassName)(implicit pos: Position): MaybeDelayedIdent = { + nameCompressor match { + case None => Ident(genName(ancestor)) + case Some(compressor) => DelayedIdent(compressor.genResolverForAncestor(ancestor)) + } + } def genJSPrivateSelect(receiver: Tree, field: irt.FieldIdent)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, @@ -559,14 +729,14 @@ private[emitter] final class SJSGen( case ArrayTypeRef(ClassRef(ObjectClass), 1) => globalVar(VarField.ac, ObjectClass) case _ => - genClassDataOf(arrayTypeRef) DOT "constr" + genClassDataOf(arrayTypeRef) DOT cpn.constr } } def genClassOf(typeRef: TypeRef)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): Tree = { - Apply(DotSelect(genClassDataOf(typeRef), Ident("getClassOf")), Nil) + Apply(DotSelect(genClassDataOf(typeRef), Ident(cpn.getClassOf)), Nil) } def genClassOf(className: ClassName)( @@ -585,7 +755,7 @@ private[emitter] final class SJSGen( case ArrayTypeRef(base, dims) => val baseData = genClassDataOf(base) (1 to dims).foldLeft[Tree](baseData) { (prev, _) => - Apply(DotSelect(prev, Ident("getArrayOf")), Nil) + Apply(DotSelect(prev, Ident(cpn.getArrayOf)), Nil) } } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/TreeDSL.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/TreeDSL.scala index 0063f17d20..540936dc78 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/TreeDSL.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/TreeDSL.scala @@ -24,7 +24,7 @@ private[emitter] object TreeDSL { extends AnyVal { /** Select a member */ - def DOT(field: Ident)(implicit pos: Position): DotSelect = + def DOT(field: MaybeDelayedIdent)(implicit pos: Position): DotSelect = DotSelect(self, field) /** Select a member */ @@ -112,7 +112,6 @@ private[emitter] object TreeDSL { def prototype(implicit pos: Position): Tree = self DOT "prototype" def length(implicit pos: Position): Tree = self DOT "length" - def u(implicit pos: Position): Tree = self DOT "u" } def typeof(expr: Tree)(implicit pos: Position): Tree = diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala index a6d632a1cd..09a3b8648a 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala @@ -139,7 +139,7 @@ object Printers { * - No leading indent. * - No trailing newline. */ - def printTree(tree: Tree, isStat: Boolean): Unit = { + protected def printTree(tree: Tree, isStat: Boolean): Unit = { def printSeparatorIfStat() = { if (isStat) print(';') @@ -747,9 +747,13 @@ object Printers { protected def print(ident: Ident): Unit = printEscapeJS(ident.name) + protected def print(ident: DelayedIdent): Unit = + printEscapeJS(ident.resolveName()) + private final def print(propName: PropertyName): Unit = propName match { - case lit: StringLiteral => print(lit: Tree) - case ident: Ident => print(ident) + case lit: StringLiteral => print(lit: Tree) + case ident: Ident => print(ident) + case ident: DelayedIdent => print(ident) case ComputedName(tree) => print("[") @@ -777,7 +781,7 @@ object Printers { private var column = 0 - override def printTree(tree: Tree, isStat: Boolean): Unit = { + override protected def printTree(tree: Tree, isStat: Boolean): Unit = { val pos = tree.pos if (pos.isDefined) sourceMap.startNode(column, pos) @@ -799,6 +803,14 @@ object Printers { sourceMap.endNode(column) } + override protected def print(ident: DelayedIdent): Unit = { + if (ident.pos.isDefined) + sourceMap.startIdentNode(column, ident.pos, ident.originalName) + printEscapeJS(ident.resolveName()) + if (ident.pos.isDefined) + sourceMap.endNode(column) + } + override protected def print(printedTree: PrintedTree): Unit = { super.print(printedTree) sourceMap.insertFragment(printedTree.sourceMapFragment) @@ -830,4 +842,24 @@ object Printers { } } + /** Shows a `Tree` for debugging purposes, not for pretty-printing. */ + private[javascript] def showTree(tree: Tree): String = { + val writer = new ByteArrayWriter() + val printer = new Printers.JSTreeShowPrinter(writer) + printer.printTreeForShow(tree) + new String(writer.toByteArray(), StandardCharsets.US_ASCII) + } + + /** A printer that shows `Tree`s for debugging, not for pretty-printing. */ + private class JSTreeShowPrinter(_out: ByteArrayWriter, initIndent: Int = 0) + extends JSTreePrinter(_out, initIndent) { + def printTreeForShow(tree: Tree): Unit = + printTree(tree, isStat = true) + + override protected def print(ident: DelayedIdent): Unit = { + print("") + } + } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala index ec5b72e850..0c0b820e82 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala @@ -31,12 +31,7 @@ object Trees { abstract sealed class Tree { val pos: Position - def show: String = { - val writer = new ByteArrayWriter() - val printer = new Printers.JSTreePrinter(writer) - printer.printTree(this, isStat = true) - new String(writer.toByteArray(), StandardCharsets.US_ASCII) - } + def show: String = Printers.showTree(this) } // Constructor comment / annotation. @@ -50,16 +45,26 @@ object Trees { def pos: Position } + sealed trait MaybeDelayedIdent extends PropertyName { + def resolveName(): String + } + sealed case class Ident(name: String, originalName: OriginalName)( - implicit val pos: Position) extends PropertyName { - require(Ident.isValidJSIdentifierName(name), - s"'$name' is not a valid JS identifier name") + implicit val pos: Position) extends MaybeDelayedIdent { + Ident.requireValidJSIdentifierName(name) + + def resolveName(): String = name } object Ident { def apply(name: String)(implicit pos: Position): Ident = new Ident(name, NoOriginalName) + def requireValidJSIdentifierName(name: String): Unit = { + require(isValidJSIdentifierName(name), + s"'$name' is not a valid JS identifier name") + } + /** Tests whether the given string is a valid `IdentifierName` for the * ECMAScript language specification. * @@ -87,6 +92,42 @@ object Trees { } } + /** An ident whose real name will be resolved later. */ + sealed case class DelayedIdent(resolver: DelayedIdent.Resolver, originalName: OriginalName)( + implicit val pos: Position) + extends MaybeDelayedIdent { + + def resolveName(): String = { + val name = resolver.resolve() + Ident.requireValidJSIdentifierName(name) + name + } + } + + object DelayedIdent { + def apply(resolver: DelayedIdent.Resolver)(implicit pos: Position): DelayedIdent = + new DelayedIdent(resolver, NoOriginalName) + + /** Resolver for the eventual name of a `DelayedIdent`. */ + trait Resolver { + /** Resolves the eventual name of the delayed ident. + * + * @throws java.lang.IllegalStateException + * if this resolver is not yet ready to resolve a name + */ + def resolve(): String + + /** A string representing this resolver for debugging purposes. + * + * The result of this method is used when calling `show` on the + * associated `DelayedIdent`. Once the resolver is ready, this method is + * encouraged to return the same string as `resolve()`, but it is not + * mandatory to do so. + */ + def debugString: String + } + } + sealed case class ComputedName(tree: Tree) extends PropertyName { def pos: Position = tree.pos } @@ -231,7 +272,7 @@ object Trees { implicit val pos: Position) extends Tree - sealed case class DotSelect(qualifier: Tree, item: Ident)( + sealed case class DotSelect(qualifier: Tree, item: MaybeDelayedIdent)( implicit val pos: Position) extends Tree diff --git a/linker/shared/src/main/scala/org/scalajs/linker/standard/StandardLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/standard/StandardLinkerBackend.scala index 4ea98e8679..ff27b8452f 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/standard/StandardLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/standard/StandardLinkerBackend.scala @@ -23,6 +23,7 @@ object StandardLinkerBackend { .withSourceMap(config.sourceMap) .withOutputPatterns(config.outputPatterns) .withRelativizeSourceMapBase(config.relativizeSourceMapBase) + .withMinify(config.minify) .withClosureCompilerIfAvailable(config.closureCompilerIfAvailable) .withPrettyPrint(config.prettyPrint) .withMaxConcurrentWrites(config.maxConcurrentWrites) diff --git a/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala index 1f7884c0f1..50e726106e 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala @@ -63,7 +63,7 @@ class EmitterTest { config = config) fullContent <- linkToContent(classDefs, moduleInitializers = MainTestModuleInitializers, - config = config.withClosureCompilerIfAvailable(true)) + config = config.withClosureCompilerIfAvailable(true).withMinify(true)) } yield { def testContent(content: String): Unit = { if (!content.startsWith(header)) { diff --git a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala index aa851ca438..dca2b88b47 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -71,7 +71,7 @@ class LibrarySizeTest { testLinkedSizes( expectedFastLinkSize = 150063, - expectedFullLinkSizeWithoutClosure = 130664, + expectedFullLinkSizeWithoutClosure = 92648, expectedFullLinkSizeWithClosure = 21325, classDefs, moduleInitializers = MainTestModuleInitializers @@ -98,6 +98,7 @@ object LibrarySizeTest { val fullLinkConfig = config .withSemantics(_.optimized) .withClosureCompilerIfAvailable(true) + .withMinify(true) val fastLinker = StandardImpl.linker(config) val fullLinker = StandardImpl.linker(fullLinkConfig) diff --git a/linker/shared/src/test/scala/org/scalajs/linker/backend/emitter/NameCompressorTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/backend/emitter/NameCompressorTest.scala new file mode 100644 index 0000000000..db2f5e722b --- /dev/null +++ b/linker/shared/src/test/scala/org/scalajs/linker/backend/emitter/NameCompressorTest.scala @@ -0,0 +1,53 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.linker.backend.emitter + +import org.junit.Test +import org.junit.Assert._ + +class NameCompressorTest { + @Test def testNameGenerator(): Unit = { + // all the one-letter strings + val letterStrings = (('a' to 'z') ++ ('A' to 'Z')).map(_.toString()) + + // all the one-letter-or-digit strings + val letterOrDigitStrings = ('0' to '9').map(_.toString()) ++ letterStrings + + val expectedOneCharIdents = letterStrings + + val expectedTwoCharIdents = for { + firstChar <- letterStrings + secondChar <- letterOrDigitStrings + ident = firstChar + secondChar + if ident != "do" && ident != "if" && ident != "in" // reserved JS identifiers that will be avoided + } yield { + ident + } + + val firstFewExpectedThreeCharIdents = { + letterOrDigitStrings.map("a0" + _) ++ + letterOrDigitStrings.map("a1" + _) + } + + val expectedSequenceStart = + expectedOneCharIdents ++ expectedTwoCharIdents ++ firstFewExpectedThreeCharIdents + + // Now actually test + + val namesToAvoid = NameGen.ReservedJSIdentifierNames + val generator = new NameCompressor.NameGenerator(namesToAvoid) + + for (expected <- expectedSequenceStart) + assertEquals(expected, generator.nextString()) + } +} diff --git a/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala index ba4848f668..a2a8108fa8 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala @@ -30,12 +30,16 @@ class PrintersTest { private implicit def str2ident(name: String): Ident = Ident(name, ir.OriginalName.NoOriginalName) - private def assertPrintEquals(expected: String, tree: Tree): Unit = { + private def printTree(tree: Tree): String = { val out = new ByteArrayWriter val printer = new Printers.JSTreePrinter(out) printer.printStat(tree) - assertEquals(expected.stripMargin.trim + "\n", - new String(out.toByteArray(), UTF_8)) + new String(out.toByteArray(), UTF_8) + } + + private def assertPrintEquals(expected: String, tree: Tree): Unit = { + val printResult = printTree(tree) + assertEquals(expected.stripMargin.trim + "\n", printResult) } @Test def printFunctionDef(): Unit = { @@ -159,6 +163,33 @@ class PrintersTest { ) } + @Test def delayedIdentPrintVersusShow(): Unit = { + locally { + object resolver extends DelayedIdent.Resolver { + def resolve(): String = "foo" + def debugString: String = "bar" + } + + val tree = DotSelect(VarRef("x"), DelayedIdent(resolver)) + + assertPrintEquals("x.foo;", tree) + assertEquals("x.;", tree.show) + } + + // Even when `resolve()` throws, `show` still succeeds based on `debugString`. + locally { + object resolver extends DelayedIdent.Resolver { + def resolve(): String = throw new IllegalStateException("not ready") + def debugString: String = "bar" + } + + val tree = DotSelect(VarRef("x"), DelayedIdent(resolver)) + + assertThrows(classOf[IllegalStateException], () => printTree(tree)) + assertEquals("x.;", tree.show) + } + } + @Test def showPrintedTree(): Unit = { val tree = PrintedTree("test".getBytes(UTF_8), SourceMapWriter.Fragment.Empty) diff --git a/project/Build.scala b/project/Build.scala index a14202f7f9..c6ae6168e5 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -53,6 +53,9 @@ object ExposedValues extends AutoPlugin { val default213ScalaVersion: SettingKey[String] = settingKey("the default Scala 2.13.x version for this build (derived from cross213ScalaVersions)") + val enableMinifyEverywhere: SettingKey[Boolean] = + settingKey("force usage of the `minify` option of the linker in all contexts (fast and full)") + // set scalaJSLinkerConfig in someProject ~= makeCompliant val makeCompliant: StandardConfig => StandardConfig = { _.withSemantics { semantics => @@ -102,6 +105,8 @@ object ExposedValues extends AutoPlugin { } } +import ExposedValues.autoImport.enableMinifyEverywhere + final case class ExpectedSizes(fastLink: Range, fullLink: Range, fastLinkGz: Range, fullLinkGz: Range) @@ -143,6 +148,15 @@ object MyScalaJSPlugin extends AutoPlugin { } override def globalSettings: Seq[Setting[_]] = Def.settings( + // can be overridden with a 'set' command + enableMinifyEverywhere := false, + + scalaJSLinkerConfig := { + scalaJSLinkerConfig.value + .withCheckIR(true) + .withMinify(enableMinifyEverywhere.value) + }, + fullClasspath in scalaJSLinkerImpl := { (fullClasspath in (Build.linker.v2_12, Runtime)).value }, @@ -201,10 +215,24 @@ object MyScalaJSPlugin extends AutoPlugin { libDeps.filterNot(dep => blacklist.contains(dep.name)) }, - scalaJSLinkerConfig ~= (_.withCheckIR(true)), - wantSourceMaps := true, + // If `enableMinifyEverywhere` is used, make sure to deactive GCC in fullLinkJS + Compile / fullLinkJS / scalaJSLinkerConfig := { + val prev = (Compile / fullLinkJS / scalaJSLinkerConfig).value + if (enableMinifyEverywhere.value) + prev.withClosureCompiler(false) + else + prev + }, + Test / fullLinkJS / scalaJSLinkerConfig := { + val prev = (Test / fullLinkJS / scalaJSLinkerConfig).value + if (enableMinifyEverywhere.value) + prev.withClosureCompiler(false) + else + prev + }, + jsEnv := new NodeJSEnv( NodeJSEnv.Config().withSourceMap(wantSourceMaps.value)), @@ -231,6 +259,7 @@ object MyScalaJSPlugin extends AutoPlugin { checksizes := { val logger = streams.value.log + val useMinifySizes = enableMinifyEverywhere.value val maybeExpected = expectedSizes.value /* The deprecated tasks do exactly what we want in terms of module / @@ -239,7 +268,7 @@ object MyScalaJSPlugin extends AutoPlugin { val fast = (fastOptJS in Compile).value.data val full = (fullOptJS in Compile).value.data - val desc = s"${thisProject.value.id} Scala ${scalaVersion.value}" + val desc = s"${thisProject.value.id} Scala ${scalaVersion.value}, useMinifySizes = $useMinifySizes" maybeExpected.fold { logger.info(s"Ignoring checksizes for " + desc) @@ -1963,23 +1992,42 @@ object Build { MyScalaJSPlugin.expectedSizes := { val default212Version = default212ScalaVersion.value val default213Version = default213ScalaVersion.value + val useMinifySizes = enableMinifyEverywhere.value scalaVersion.value match { case `default212Version` => - Some(ExpectedSizes( - fastLink = 640000 to 641000, - fullLink = 101000 to 102000, - fastLinkGz = 77000 to 78000, - fullLinkGz = 26000 to 27000, - )) + if (!useMinifySizes) { + Some(ExpectedSizes( + fastLink = 640000 to 641000, + fullLink = 101000 to 102000, + fastLinkGz = 77000 to 78000, + fullLinkGz = 26000 to 27000, + )) + } else { + Some(ExpectedSizes( + fastLink = 494000 to 495000, + fullLink = 337000 to 338000, + fastLinkGz = 69000 to 70000, + fullLinkGz = 50000 to 51000, + )) + } case `default213Version` => - Some(ExpectedSizes( - fastLink = 462000 to 463000, - fullLink = 99000 to 100000, - fastLinkGz = 60000 to 61000, - fullLinkGz = 26000 to 27000, - )) + if (!useMinifySizes) { + Some(ExpectedSizes( + fastLink = 462000 to 463000, + fullLink = 99000 to 100000, + fastLinkGz = 60000 to 61000, + fullLinkGz = 26000 to 27000, + )) + } else { + Some(ExpectedSizes( + fastLink = 347000 to 348000, + fullLink = 307000 to 308000, + fastLinkGz = 54000 to 55000, + fullLinkGz = 49000 to 50000, + )) + } case _ => None @@ -2227,7 +2275,8 @@ object Build { "isNoModule" -> (moduleKind == ModuleKind.NoModule), "isESModule" -> (moduleKind == ModuleKind.ESModule), "isCommonJSModule" -> (moduleKind == ModuleKind.CommonJSModule), - "isFullOpt" -> (stage == Stage.FullOpt), + "usesClosureCompiler" -> linkerConfig.closureCompiler, + "hasMinifiedNames" -> (linkerConfig.closureCompiler || linkerConfig.minify), "compliantAsInstanceOfs" -> (sems.asInstanceOfs == CheckedBehavior.Compliant), "compliantArrayIndexOutOfBounds" -> (sems.arrayIndexOutOfBounds == CheckedBehavior.Compliant), "compliantArrayStores" -> (sems.arrayStores == CheckedBehavior.Compliant), diff --git a/sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPluginInternal.scala b/sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPluginInternal.scala index 6f1d6f66a4..02d4eb89f1 100644 --- a/sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPluginInternal.scala +++ b/sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPluginInternal.scala @@ -470,6 +470,7 @@ private[sbtplugin] object ScalaJSPluginInternal { prevConfig .withSemantics(_.optimized) .withClosureCompiler(useClosure) + .withMinify(true) // ignored if we actually use Closure .withCheckIR(true) // for safety, fullOpt is slow anyways. }, diff --git a/test-suite/js/src/main/scala-ide-stubs/org/scalajs/testsuite/utils/BuildInfo.scala b/test-suite/js/src/main/scala-ide-stubs/org/scalajs/testsuite/utils/BuildInfo.scala index cbd1a4f46d..d577790522 100644 --- a/test-suite/js/src/main/scala-ide-stubs/org/scalajs/testsuite/utils/BuildInfo.scala +++ b/test-suite/js/src/main/scala-ide-stubs/org/scalajs/testsuite/utils/BuildInfo.scala @@ -22,7 +22,8 @@ private[utils] object BuildInfo { final val isNoModule = false final val isESModule = false final val isCommonJSModule = false - final val isFullOpt = false + final val usesClosureCompiler = false + final val hasMinifiedNames = false final val compliantAsInstanceOfs = false final val compliantArrayIndexOutOfBounds = false final val compliantArrayStores = false diff --git a/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala b/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala index cbf49e2d92..ac1c4132b3 100644 --- a/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala +++ b/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala @@ -68,7 +68,10 @@ object Platform { def sourceMaps: Boolean = BuildInfo.hasSourceMaps && executingInNodeJS - def isInFullOpt: Boolean = BuildInfo.isFullOpt + def usesClosureCompiler: Boolean = BuildInfo.usesClosureCompiler + + def hasMinifiedNames: Boolean = BuildInfo.hasMinifiedNames + def isInProductionMode: Boolean = BuildInfo.productionMode def hasCompliantAsInstanceOfs: Boolean = BuildInfo.compliantAsInstanceOfs 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 2e8878cef9..833ae38e64 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 @@ -321,19 +321,22 @@ class OptimizerTest { } @Test def foldingDoubleWithDecimalAndString(): Unit = { - assumeFalse("Assumed not executing in FullOpt", isInFullOpt) + assumeFalse("GCC wrongly optimizes this code", usesClosureCompiler) + assertEquals("1.2323919403474454e+21hello", 1.2323919403474454E21 + "hello") assertEquals("hello1.2323919403474454e+21", "hello" + 1.2323919403474454E21) } @Test def foldingDoubleThatJVMWouldPrintInScientificNotationAndString(): Unit = { - assumeFalse("Assumed not executing in FullOpt", isInFullOpt) + assumeFalse("GCC wrongly optimizes this code", usesClosureCompiler) + assertEquals("123456789012345hello", 123456789012345d + "hello") assertEquals("hello123456789012345", "hello" + 123456789012345d) } @Test def foldingDoublesToString(): Unit = { - assumeFalse("Assumed not executing in FullOpt", isInFullOpt) + assumeFalse("GCC wrongly optimizes this code", usesClosureCompiler) + @noinline def toStringNoInline(v: Double): String = v.toString @inline def test(v: Double): Unit = assertEquals(toStringNoInline(v), v.toString) diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/MiscInteropTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/MiscInteropTest.scala index 2877c74ac4..f477cffcc6 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/MiscInteropTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/MiscInteropTest.scala @@ -42,7 +42,7 @@ class MiscInteropTest { assumeFalse( "GCC wrongly optimizes this code, " + "see https://github.com/google/closure-compiler/issues/3498", - isInFullOpt) + usesClosureCompiler) @noinline def nonExistentGlobalVarNoInline(): Any = js.Dynamic.global.thisGlobalVarDoesNotExist @@ -197,7 +197,7 @@ class MiscInteropTest { // Emitted classes @Test def meaningfulNameProperty(): Unit = { - assumeFalse("Assumed not executing in FullOpt", isInFullOpt) + assumeFalse("Need non-minified names", hasMinifiedNames) def nameOf(obj: Any): js.Any = obj.asInstanceOf[js.Dynamic].constructor.name diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/library/StackTraceTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/library/StackTraceTest.scala index d931b4bb0d..03315cf014 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/library/StackTraceTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/library/StackTraceTest.scala @@ -52,7 +52,7 @@ class StackTraceTest { @Test def decodeClassNameAndMethodName(): Unit = { assumeTrue("Assume Node.js", executingInNodeJS) - assumeFalse("Assume fullopt-stage", isInFullOpt) + assumeFalse("Assume non-minified names", hasMinifiedNames) val Error = js.constructorOf[js.Error] val oldStackTraceLimit = Error.stackTraceLimit diff --git a/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala b/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala index a3d908bf8e..6b0a9c412a 100644 --- a/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala +++ b/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala @@ -40,7 +40,9 @@ object Platform { else Integer.parseInt(v.takeWhile(_.isDigit)) } - def isInFullOpt: Boolean = false + def usesClosureCompiler: Boolean = false + + def hasMinifiedNames: Boolean = false def hasCompliantAsInstanceOfs: Boolean = true def hasCompliantArrayIndexOutOfBounds: Boolean = true diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/LongTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/LongTest.scala index a21f8ff802..dd9ed7a5a6 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/LongTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/LongTest.scala @@ -620,7 +620,7 @@ class LongTest { test(-1, lg(-1)) // Closure seems to incorrectly rewrite the constant on the right :-( - val epsilon = if (isInFullOpt) 1E4f else 0.0f + val epsilon = if (usesClosureCompiler) 1E4f else 0.0f test(9.223372E18f, MaxVal, epsilon) test(-9.223372E18f, MinVal, epsilon) @@ -674,7 +674,7 @@ class LongTest { test(-1, lg(-1)) // Closure seems to incorrectly rewrite the constant on the right :-( - val epsilon = if (isInFullOpt) 1E4 else 0.0 + val epsilon = if (usesClosureCompiler) 1E4 else 0.0 test(9.223372036854776E18, MaxVal, epsilon) test(-9.223372036854776E18, MinVal, epsilon) @@ -722,7 +722,7 @@ class LongTest { test(lg(0), -Double.MinPositiveValue) test(MaxVal, twoPow63) test(MaxVal, twoPow63NextUp) - if (!isInFullOpt) { + if (!usesClosureCompiler) { // GCC incorrectly rewrites the Double constants on the rhs test(lg(-1024, 2147483647), twoPow63NextDown) test(MinVal, -twoPow63) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/ReflectiveCallTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/ReflectiveCallTest.scala index 1fe0250028..98bd0f275e 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/ReflectiveCallTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/ReflectiveCallTest.scala @@ -346,7 +346,7 @@ class ReflectiveCallTest { assumeFalse( "GCC is a bit too eager in its optimizations in this error case", - Platform.isInFullOpt) + Platform.usesClosureCompiler) type ObjWithAnyRefPrimitives = Any { def eq(that: AnyRef): Boolean