diff options
Diffstat (limited to 'tools/shared/src/main/scala/scala/scalajs')
61 files changed, 11935 insertions, 0 deletions
diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/classpath/CompleteClasspath.scala b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/CompleteClasspath.scala new file mode 100644 index 0000000..6646a7b --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/CompleteClasspath.scala @@ -0,0 +1,35 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.classpath + +import scala.collection.immutable.Seq + +import scala.scalajs.tools.io.VirtualJSFile +import scala.scalajs.tools.jsdep.ResolutionInfo + +/** A classpath where nothing is missing. Therefore: + * - All JS libraries are resolved and ordered + * - The CoreJSLibs are present + * - Nothing can be added anymore + */ +abstract class CompleteClasspath( + /** Resolved JS libraries */ + val jsLibs: Seq[ResolvedJSDependency], + val requiresDOM: Boolean, + val version: Option[String] +) { + + /** Fully linked Scala.js code */ + def scalaJSCode: VirtualJSFile + + /** All code in this complete classpath */ + final def allCode: Seq[VirtualJSFile] = jsLibs.map(_.lib) :+ scalaJSCode + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/classpath/ComplianceRequirement.scala b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/ComplianceRequirement.scala new file mode 100644 index 0000000..f6ec36f --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/ComplianceRequirement.scala @@ -0,0 +1,7 @@ +package scala.scalajs.tools.classpath + +import scala.scalajs.tools.jsdep.Origin + +/** Expresses a requirement for a given semantic to be compliant */ +final class ComplianceRequirement( + val semantics: String, val origins: List[Origin]) diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/classpath/Exceptions.scala b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/Exceptions.scala new file mode 100644 index 0000000..62bf75f --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/Exceptions.scala @@ -0,0 +1,42 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.classpath + +import scala.scalajs.tools.jsdep.ResolutionInfo + +class MissingJSLibException(val dependencies: List[ResolutionInfo]) + extends Exception(MissingJSLibException.mkMsg(dependencies)) + +object MissingJSLibException { + private def mkMsg(deps: List[ResolutionInfo]): String = { + val msg = new StringBuilder() + msg.append("Missing dependencies: \n") + for (d <- deps) { + msg.append(s"- ${d.resourceName}") + msg.append(s" originating from: ${d.origins.mkString(", ")}\n") + } + msg.toString() + } +} + +class BadComplianceException(val unmet: List[ComplianceRequirement]) + extends Exception(BadComplianceException.mkMsg(unmet)) + +object BadComplianceException { + private def mkMsg(unmets: List[ComplianceRequirement]): String = { + val msg = new StringBuilder() + msg.append("Unmet required semantic compliance(s): \n") + for (unmet <- unmets) { + msg.append(s"- ${unmet.semantics}") + msg.append(s" originating from: ${unmet.origins.mkString(", ")}\n") + } + msg.toString + } +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/classpath/IRClasspath.scala b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/IRClasspath.scala new file mode 100644 index 0000000..a92293b --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/IRClasspath.scala @@ -0,0 +1,69 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.classpath + +import scala.collection.immutable.{Seq, Traversable} + +import scala.scalajs.tools.sem.Semantics +import scala.scalajs.tools.io._ +import scala.scalajs.tools.logging._ +import scala.scalajs.tools.optimizer.ScalaJSOptimizer +import scala.scalajs.tools.jsdep.ResolutionInfo + +/** A [[CompleteClasspath]] that contains only IR as scalaJSCode */ +final class IRClasspath( + /** The JS libraries the IR code depends on */ + jsLibs: Seq[ResolvedJSDependency], + val requiredCompliance: Traversable[ComplianceRequirement], + /** The IR itself. Ancestor count is used for later ordering */ + val scalaJSIR: Traversable[VirtualScalaJSIRFile], + requiresDOM: Boolean, + version: Option[String] +) extends CompleteClasspath(jsLibs, requiresDOM, version) { + + /** Orders and optimizes the contained IR. + * + * Consider using [[ScalaJSOptimizer]] for a canonical way to do so. It + * allows to persist the resulting file and create a source map, as well as + * using non-default [[Semantics]]. + */ + override lazy val scalaJSCode: VirtualJSFile = { + import ScalaJSOptimizer._ + + val outName = "temporary-fastOpt.js" + + if (scalaJSIR.nonEmpty) { + val semantics = Semantics.compliantTo(requiredCompliance.map(_.semantics)) + val output = WritableMemVirtualJSFile(outName) + new ScalaJSOptimizer(semantics).optimizeCP( + Inputs(this), + OutputConfig(output), + NullLogger) + output + } else { + // We cannot run the optimizer without IR, because it will complain about + // java.lang.Object missing. However, an empty JS file is perfectly valid + // for no IR at all. + VirtualJSFile.empty(outName) + } + } + + /** Checks whether the given semantics are compliant with the requirements of + * this CompleteClasspath. Throws an exception otherwise. + */ + final def checkCompliance(semantics: Semantics): Unit = { + val unmet = requiredCompliance filterNot { compliance => + semantics.isCompliant(compliance.semantics) + } + + if (unmet.nonEmpty) + throw new BadComplianceException(unmet.toList) + } +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/classpath/LinkedClasspath.scala b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/LinkedClasspath.scala new file mode 100644 index 0000000..3ace785 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/LinkedClasspath.scala @@ -0,0 +1,26 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.classpath + +import scala.scalajs.tools.io.VirtualJSFile +import scala.scalajs.tools.jsdep.ResolutionInfo + +import scala.collection.immutable.Seq + +/** A [[CompleteClasspath]] that is fully linked (either with the + * [[ScalaJSOptimizer]] or the Closure Optimizer. It contains only a single + * file that is scalaJSCode. + */ +final class LinkedClasspath( + jsLibs: Seq[ResolvedJSDependency], + val scalaJSCode: VirtualJSFile, + requiresDOM: Boolean, + version: Option[String] +) extends CompleteClasspath(jsLibs, requiresDOM, version) diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/classpath/PartialClasspath.scala b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/PartialClasspath.scala new file mode 100644 index 0000000..949cd6e --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/PartialClasspath.scala @@ -0,0 +1,99 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.classpath + +import scala.collection.immutable.{Seq, Traversable} + +import scala.scalajs.tools.jsdep._ +import scala.scalajs.tools.io._ +import scala.scalajs.tools.corelib.CoreJSLibs + +/** A partial Scala.js classpath is a collection of: + * - Scala.js binary files *.sjsir + * - Native JavaScript libraries + * - Description of dependencies on other JavaScript libraries + * + * PartialClasspaths can be combined (using [[merge]]) and eventually resolved + * to a [[CompleteIRClasspath]] + */ +final class PartialClasspath( + /** Description of JS libraries the content of this classpath depends on */ + val dependencies: Traversable[JSDependencyManifest], + /** JS libraries this partial classpath provides */ + val availableLibs: Map[String, VirtualJSFile], + /** Scala.js IR contained in this PartialClasspath (unordered) */ + val scalaJSIR: Traversable[VirtualScalaJSIRFile], + val version: Option[String] +) { + import PartialClasspath.DependencyFilter + + /** Merges another [[PartialClasspath]] with this one. This means: + * - Concatenate/merge dependencies + * - Merge availableLibs (libs in that shadow libs in this) + * - Merge Scala.js IR + */ + def merge(that: PartialClasspath): PartialClasspath = { + new PartialClasspath( + this.dependencies ++ that.dependencies, + this.availableLibs ++ that.availableLibs, + this.scalaJSIR ++ that.scalaJSIR, + CacheUtils.joinVersions(this.version, that.version)) + } + + /** Construct a [[IRClasspath]] out of this [[PartialClasspath]] by + * resolving library dependencies (and failing if they are not met) + */ + def resolve(filter: DependencyFilter = identity): IRClasspath = { + new IRClasspath(resolveDependencies(filter), mergeCompliance(), scalaJSIR, + dependencies.exists(_.requiresDOM), version) + } + + /** Constructs an ordered list of JS libraries to include. Fails if: + * - Dependencies have cycles + * - Not all dependencies are available + */ + protected def resolveDependencies( + filter: DependencyFilter): List[ResolvedJSDependency] = { + val flatDeps = filter(dependencies.flatMap(_.flatten)) + val includeList = JSDependencyManifest.createIncludeList(flatDeps) + + val missingDeps = includeList.filterNot { info => + availableLibs.contains(info.resourceName) + } + + if (missingDeps.nonEmpty) + throw new MissingJSLibException(missingDeps) + + for (info <- includeList) + yield new ResolvedJSDependency(availableLibs(info.resourceName), info) + } + + protected def mergeCompliance(): Traversable[ComplianceRequirement] = { + val flatTups = for { + dependency <- dependencies + semantics <- dependency.compliantSemantics + } yield (semantics, dependency.origin) + + for { + (semantics, tups) <- flatTups.groupBy(_._1) + } yield new ComplianceRequirement(semantics, tups.map(_._2).toList) + } + +} + +object PartialClasspath { + + type DependencyFilter = + Traversable[FlatJSDependency] => Traversable[FlatJSDependency] + + /** Creates an empty PartialClasspath */ + def empty: PartialClasspath = + new PartialClasspath(Nil, Map.empty, Nil, Some("")) +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/classpath/ResolvedJSDependency.scala b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/ResolvedJSDependency.scala new file mode 100644 index 0000000..12ae8dc --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/ResolvedJSDependency.scala @@ -0,0 +1,10 @@ +package scala.scalajs.tools.classpath + +import scala.scalajs.tools.io._ +import scala.scalajs.tools.jsdep._ + +/** A dependency on a native JavaScript library that has been successfully + * resolved + */ +final class ResolvedJSDependency( + val lib: VirtualJSFile, val info: ResolutionInfo) diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/AbstractJarLibClasspathBuilder.scala b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/AbstractJarLibClasspathBuilder.scala new file mode 100644 index 0000000..77f8509 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/AbstractJarLibClasspathBuilder.scala @@ -0,0 +1,53 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.classpath.builder + +import scala.collection.mutable + +import scala.scalajs.tools.io._ +import scala.scalajs.tools.jsdep.JSDependencyManifest +import scala.scalajs.tools.classpath._ + +/** reads a ScalaJS library JAR into a CP + * - IR files go to scalaJSCode + * - JS files go to availableLibs + * - Reads a potential top-level JS_DEPENDENCIES file + */ +trait AbstractJarLibClasspathBuilder extends JarTraverser { + + private val irFiles = mutable.ListBuffer.empty[VirtualScalaJSIRFile] + private val jsFiles = mutable.Map.empty[String, VirtualJSFile] + private var dependency: Option[JSDependencyManifest] = None + + def build(jar: File): PartialClasspath = { + val v = traverseJar(jar) + new PartialClasspath(dependency.toList, + jsFiles.toMap, irFiles.toList, Some(v)) + } + + override protected def handleIR(relPath: String, + ir: => VirtualScalaJSIRFile): Unit = { + // We don't need to implement shadowing here: We have only a single JAR + irFiles += ir + } + + override protected def handleJS(js: => VirtualJSFile): Unit = { + val file = js + if (!jsFiles.contains(file.name)) + jsFiles += file.name -> file + } + + override protected def handleDepManifest(m: => JSDependencyManifest): Unit = { + if (dependency.isDefined) + sys.error("A JAR cannot have multiple JS dependency manifests") + dependency = Some(m) + } + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/AbstractPartialClasspathBuilder.scala b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/AbstractPartialClasspathBuilder.scala new file mode 100644 index 0000000..9889f4c --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/AbstractPartialClasspathBuilder.scala @@ -0,0 +1,47 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.classpath.builder + +import scala.scalajs.tools.jsdep.JSDependencyManifest +import scala.scalajs.tools.classpath._ +import scala.scalajs.tools.io._ + +import scala.collection.mutable +import scala.collection.immutable.Seq + +trait AbstractPartialClasspathBuilder extends ClasspathContentHandler + with ClasspathElementsTraverser { + + private val jsDepManifests = mutable.ListBuffer.empty[JSDependencyManifest] + private val irFiles = mutable.Map.empty[String, VirtualScalaJSIRFile] + private val otherJSFiles = mutable.Map.empty[String, VirtualJSFile] + + override protected def handleIR(relPath: String, + ir: => VirtualScalaJSIRFile): Unit = { + if (!irFiles.contains(relPath)) + irFiles += relPath -> ir + } + + override protected def handleJS(js: => VirtualJSFile): Unit = { + val file = js + if (!otherJSFiles.contains(file.name)) + otherJSFiles += file.name -> file + } + + override protected def handleDepManifest(m: => JSDependencyManifest): Unit = { + jsDepManifests += m + } + + def build(cp: Seq[File]): PartialClasspath = { + val version = traverseClasspathElements(cp) + new PartialClasspath(jsDepManifests.toList, otherJSFiles.toMap, + irFiles.values.toList, Some(version)) + } +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/ClasspathContentHandler.scala b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/ClasspathContentHandler.scala new file mode 100644 index 0000000..71106c2 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/ClasspathContentHandler.scala @@ -0,0 +1,25 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.classpath.builder + +import scala.scalajs.tools.io._ +import scala.scalajs.tools.jsdep._ +import scala.scalajs.tools.classpath._ + +import java.io._ + +import scala.collection.immutable.Seq + +/** Base-trait used by traversers to handle content with callbacks */ +trait ClasspathContentHandler { + protected def handleIR(relPath: String, ir: => VirtualScalaJSIRFile): Unit + protected def handleJS(js: => VirtualJSFile): Unit + protected def handleDepManifest(m: => JSDependencyManifest): Unit +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/ClasspathElementsTraverser.scala b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/ClasspathElementsTraverser.scala new file mode 100644 index 0000000..9403ce3 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/ClasspathElementsTraverser.scala @@ -0,0 +1,38 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.classpath.builder + +import scala.scalajs.tools.io._ + +/** A helper trait to traverse an arbitrary classpath element + * (i.e. a JAR or a directory). + */ +trait ClasspathElementsTraverser extends JarTraverser + with DirTraverser + with FileSystem { + + protected def traverseClasspathElements(cp: Seq[File]): String = + CacheUtils.joinVersions(cp.map(readEntriesInClasspathElement _): _*) + + /** Adds the Scala.js classpath entries in a directory or jar. + * Returns the accumulated version + */ + private def readEntriesInClasspathElement(element: File): String = { + if (!exists(element)) + getDummyVersion(element) + else if (isDirectory(element)) + traverseDir(element) + else if (isJARFile(element)) + traverseJar(element) + else + sys.error(s"$element (in classpath) exists and is neither JAR or directory") + } + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/DirTraverser.scala b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/DirTraverser.scala new file mode 100644 index 0000000..6609b29 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/DirTraverser.scala @@ -0,0 +1,60 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.classpath.builder + +import scala.scalajs.tools.io._ +import scala.scalajs.tools.jsdep.JSDependencyManifest + +import scala.collection.mutable + +trait DirTraverser extends ClasspathContentHandler with FileSystem { + + /** Traverses elements, returns a version string */ + protected def traverseDir(dir: File): String = { + val versions = mutable.SortedSet.empty[String] + + recurseDir(dir, "", versions) + + // Construct version + CacheUtils.joinVersions(versions.toSeq: _*) + } + + /** Recursively adds the Scala.js classpath entries in a directory */ + private def recurseDir(dir: File, dirPath: String, + versions: mutable.SortedSet[String]): Unit = { + val files = listFiles(dir) + for (file <- files) { + val name = getName(file) + if (isDirectory(file)) { + recurseDir(file, dirPath + name + "/", versions) + } else { + val path = dirPath + name + path match { + case JSDependencyManifest.ManifestFileName => + versions += getGlobalVersion(file) + val reader = toReader(file) + try handleDepManifest(JSDependencyManifest.read(reader)) + finally reader.close() + + case _ if isJSFile(file) => + versions += getGlobalVersion(file) + handleJS(toJSFile(file)) + + case _ if isIRFile(file) => + versions += getGlobalVersion(file) + handleIR(path, toIRFile(file)) + + case _ => // ignore other files + } + } + } + } + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/FileSystem.scala b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/FileSystem.scala new file mode 100644 index 0000000..99a8ca2 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/FileSystem.scala @@ -0,0 +1,57 @@ +package scala.scalajs.tools.classpath.builder + +import scala.scalajs.tools.io._ + +import scala.collection.immutable.Traversable + +import java.io.{InputStream, Reader} + +/** Abstraction of a FileSystem, so classpath builders can be used with virtual + * file systems + */ +trait FileSystem { + + type File + + /** Dummy version constant to identify files for which a version can not be + * found. + * This constant should never collide with the result of getVersion. + */ + val DummyVersion: String + + def isDirectory(f: File): Boolean + def isFile(f: File): Boolean + def isJSFile(f: File): Boolean + def isIRFile(f: File): Boolean + def isJARFile(f: File): Boolean + def exists(f: File): Boolean + + def getName(f: File): String + /** A string that uniquely identifies this file's location */ + def getAbsolutePath(f: File): String + /** A string that identifies the version of a file: If it equals the version + * of another file with the same absolute path, the two files must be equal. + * This is usually the lastModified date, but ordering is not required + */ + def getVersion(f: File): String + /** A string that globally identifies the version of a file: If it equals the + * global version of any other file, they must equal. + */ + def getGlobalVersion(f: File): String = + CacheUtils.joinVersions(getAbsolutePath(f), getVersion(f)) + + /** A string that globally identifies a file for which a version can not be + * found. Example: a file that does not exists. + */ + def getDummyVersion(f: File): String = + CacheUtils.joinVersions(getAbsolutePath(f), DummyVersion) + + /** List files in a directory */ + def listFiles(d: File): Traversable[File] + + def toJSFile(f: File): VirtualJSFile + def toIRFile(f: File): VirtualScalaJSIRFile + def toReader(f: File): Reader + def toInputStream(f: File): InputStream + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/JarTraverser.scala b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/JarTraverser.scala new file mode 100644 index 0000000..bbf5270 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/classpath/builder/JarTraverser.scala @@ -0,0 +1,85 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.classpath.builder + +import scala.collection.mutable +import scala.annotation.tailrec + +import java.util.zip._ +import java.io.{InputStream, InputStreamReader, Reader} + +import scala.scalajs.tools.io._ +import scala.scalajs.tools.jsdep.JSDependencyManifest + +trait JarTraverser extends ClasspathContentHandler with FileSystem { + + private val jsFiles = mutable.Map.empty[String, MemVirtualJSFile] + + /** Traverse a Jar and return a version */ + protected def traverseJar(jar: File): String = { + val zipStream = new ZipInputStream(toInputStream(jar)) + + try readEntries(zipStream, getAbsolutePath(jar)) + finally zipStream.close() + + for { + (_, jsFile) <- jsFiles + if jsFile.content != "" // drop if this is just a lone sourcemap + } handleJS(jsFile) + + getGlobalVersion(jar) + } + + private def getOrCreateJSFile(relPath: String, fullPath: String, fn: String) = + jsFiles.getOrElseUpdate(relPath, new MemVirtualJSFile(fullPath) { + override val name = fn + }) + + @tailrec + private def readEntries(in: ZipInputStream, jarPath: String): Unit = { + val entry = in.getNextEntry() + if (entry != null) { + readEntry(entry, in, jarPath) + readEntries(in, jarPath) + } + } + + private def readEntry(entry: ZipEntry, in: InputStream, jarPath: String) = { + val longName = entry.getName + val shortName = VirtualFile.nameFromPath(longName) + val fullPath = jarPath + "#" + longName + + def entryReader: Reader = new InputStreamReader(in, "UTF-8") + def entryContent: String = IO.readInputStreamToString(in) + def entryBinaryContent: Array[Byte] = IO.readInputStreamToByteArray(in) + def entryVersion: Option[String] = Some(entry.getTime.toString) + + if (longName == JSDependencyManifest.ManifestFileName) + handleDepManifest(JSDependencyManifest.read(entryReader)) + else if (longName.endsWith(".js")) { + val relPath = longName + getOrCreateJSFile(relPath, fullPath, shortName) + .withContent(entryContent) + .withVersion(entryVersion) + } else if (longName.endsWith(".js.map")) { + // assume the source map of a JS file + val relPath = longName.dropRight(".map".length) + getOrCreateJSFile(relPath, fullPath, shortName) + .withSourceMap(Some(entryContent)) + } else if (longName.endsWith(".sjsir")) { + val vf = new MemVirtualSerializedScalaJSIRFile(fullPath) { + override val name = shortName + }.withContent(entryBinaryContent) + .withVersion(entryVersion) + + handleIR(longName, vf) + } + } +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/corelib/CoreJSLibs.scala b/tools/shared/src/main/scala/scala/scalajs/tools/corelib/CoreJSLibs.scala new file mode 100644 index 0000000..ecbea1f --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/corelib/CoreJSLibs.scala @@ -0,0 +1,113 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.corelib + +import java.net.URI + +import scala.scalajs.ir.ScalaJSVersions +import scala.scalajs.tools.io._ + +import scala.scalajs.tools.sem._ + +import scala.collection.immutable.Seq +import scala.collection.mutable + +object CoreJSLibs { + + private val cachedLibBySemantics = + mutable.HashMap.empty[Semantics, VirtualJSFile] + + private val ScalaJSEnvLines = + ScalaJSEnvHolder.scalajsenv.split("\n|\r\n?") + + private val gitHubBaseURI = + new URI("https://raw.githubusercontent.com/scala-js/scala-js/") + + def libs(semantics: Semantics): Seq[VirtualJSFile] = synchronized { + Seq(cachedLibBySemantics.getOrElseUpdate(semantics, makeLib(semantics))) + } + + private def makeLib(semantics: Semantics): VirtualJSFile = { + new ScalaJSEnvVirtualJSFile(makeContent(semantics)) + } + + private def makeContent(semantics: Semantics): String = { + // This is a basic sort-of-C-style preprocessor + + def getOption(name: String): String = name match { + case "asInstanceOfs" => + semantics.asInstanceOfs.toString() + case "floats" => + if (semantics.strictFloats) "Strict" + else "Loose" + } + + var skipping = false + var skipDepth = 0 + val lines = for (line <- ScalaJSEnvLines) yield { + val includeThisLine = if (skipping) { + if (line == "//!else" && skipDepth == 1) { + skipping = false + skipDepth = 0 + } else if (line == "//!endif") { + skipDepth -= 1 + if (skipDepth == 0) + skipping = false + } else if (line.startsWith("//!if ")) { + skipDepth += 1 + } + false + } else { + if (line.startsWith("//!")) { + if (line.startsWith("//!if ")) { + val Array(_, option, op, value) = line.split(" ") + val optionValue = getOption(option) + val success = op match { + case "==" => optionValue == value + case "!=" => optionValue != value + } + if (!success) { + skipping = true + skipDepth = 1 + } + } else if (line == "//!else") { + skipping = true + skipDepth = 1 + } else if (line == "//!endif") { + // nothing to do + } else { + throw new MatchError(line) + } + false + } else { + true + } + } + if (includeThisLine) line + else "" // blank line preserves line numbers in source maps + } + + lines.mkString("", "\n", "\n") + } + + private class ScalaJSEnvVirtualJSFile(override val content: String) extends VirtualJSFile { + override def path: String = "scalajsenv.js" + override def version: Option[String] = Some("") + override def exists: Boolean = true + + override def toURI: URI = { + if (!ScalaJSVersions.currentIsSnapshot) + gitHubBaseURI.resolve(s"v${ScalaJSVersions.current}/tools/$path") + else + super.toURI + } + } + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSEnv.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSEnv.scala new file mode 100644 index 0000000..d439ae2 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSEnv.scala @@ -0,0 +1,19 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.env + +import scala.scalajs.tools.io._ +import scala.scalajs.tools.classpath._ +import scala.scalajs.tools.logging._ + +trait AsyncJSEnv extends JSEnv { + def asyncRunner(classpath: CompleteClasspath, code: VirtualJSFile, + logger: Logger, console: JSConsole): AsyncJSRunner +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala new file mode 100644 index 0000000..09e2dda --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala @@ -0,0 +1,19 @@ +package scala.scalajs.tools.env + +import scala.concurrent.Future + +trait AsyncJSRunner { + /** Start the associated run and returns a Future that completes when the run + * terminates. + */ + def start(): Future[Unit] + + /** Abort the associated run */ + def stop(): Unit + + /** Checks whether this async runner is still running */ + def isRunning(): Boolean + + /** Await completion of the started Run. Throws if the run failed */ + def await(): Unit +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSEnv.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSEnv.scala new file mode 100644 index 0000000..882e46a --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSEnv.scala @@ -0,0 +1,38 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.env + +import scala.scalajs.tools.io._ +import scala.scalajs.tools.classpath._ +import scala.scalajs.tools.logging._ + +/** An [[AsyncJSEnv]] that provides communication to and from the JS VM. + * + * Inside the VM there is a global JavaScript object named `scalajsCom` that + * can be used to control the message channel. It's operations are: + * {{{ + * // initialize com (with callback) + * scalajsCom.init(function(msg) { console.log("Received: " + msg); }); + * + * // send a message to host system + * scalajsCom.send("my message"); + * + * // close com (releases callback, allowing VM to terminate) + * scalajsCom.close(); + * }}} + */ +trait ComJSEnv extends AsyncJSEnv { + def comRunner(classpath: CompleteClasspath, code: VirtualJSFile, + logger: Logger, console: JSConsole): ComJSRunner +} + +object ComJSEnv { + class ComClosedException extends Exception("JSCom has been closed") +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala new file mode 100644 index 0000000..44302b8 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala @@ -0,0 +1,22 @@ +package scala.scalajs.tools.env + +trait ComJSRunner extends AsyncJSRunner { + + /** Send a message to the JS VM. Throws if the message cannot be sent. */ + def send(msg: String): Unit + + /** Block until a message is received. Throws a [[ComClosedExcpetion]] + * if the channel is closed before a message is received. + */ + def receive(): String + + /** Close the communication channel. Allows the VM to terminate if it is + * still waiting for callback. The JVM side **must** call close in + * order to be able to expect termination of the VM. + * + * Calling [[stop]] on a [ComJSRunner]] automatically closes the + * channel. + */ + def close(): Unit + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/ConsoleJSConsole.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/ConsoleJSConsole.scala new file mode 100644 index 0000000..5b3d055 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/ConsoleJSConsole.scala @@ -0,0 +1,17 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.env + +/** A JS console that prints on the console */ +object ConsoleJSConsole extends JSConsole { + override def log(msg: Any): Unit = { + Console.println(msg) + } +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/JSConsole.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/JSConsole.scala new file mode 100644 index 0000000..a93768f --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/JSConsole.scala @@ -0,0 +1,15 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.env + +/** Trait representing a JS console */ +trait JSConsole { + def log(msg: Any): Unit +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/JSEnv.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/JSEnv.scala new file mode 100644 index 0000000..f1fbf44 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/JSEnv.scala @@ -0,0 +1,20 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.env + +import scala.scalajs.tools.io._ +import scala.scalajs.tools.classpath._ +import scala.scalajs.tools.logging._ + +trait JSEnv { + /** Prepare a runner for the code in the virtual file. */ + def jsRunner(classpath: CompleteClasspath, code: VirtualJSFile, + logger: Logger, console: JSConsole): JSRunner +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/JSRunner.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/JSRunner.scala new file mode 100644 index 0000000..460fff0 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/JSRunner.scala @@ -0,0 +1,15 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.env + +trait JSRunner { + /** Run the associated JS code. Throw if an error occurs. */ + def run(): Unit +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/NullJSConsole.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/NullJSConsole.scala new file mode 100644 index 0000000..8147bbe --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/NullJSConsole.scala @@ -0,0 +1,5 @@ +package scala.scalajs.tools.env + +object NullJSConsole extends JSConsole { + def log(msg: Any): Unit = {} +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/io/CacheUtils.scala b/tools/shared/src/main/scala/scala/scalajs/tools/io/CacheUtils.scala new file mode 100644 index 0000000..14773f8 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/io/CacheUtils.scala @@ -0,0 +1,52 @@ +package scala.scalajs.tools.io + +object CacheUtils { + + def joinVersions(vs: Option[String]*): Option[String] = { + val bld = new StringBuilder + + @scala.annotation.tailrec + def loop(vs: Seq[Option[String]]): Option[String] = { + vs match { + case Some(v) :: vss => + bld.append(mangleVersionString(v)) + loop(vss) + case None :: _ => + None + case Nil => + Some(bld.toString) + } + } + + loop(vs.toList) + } + + def joinVersions(vs: String*): String = + vs.map(mangleVersionString _).mkString + + private def mangleVersionString(str: String) = s"${str.length}:$str" + + def cached(version: Option[String], output: VirtualFile, + cache: Option[WritableVirtualTextFile])(action: => Unit): Unit = { + + val upToDate = output.exists && ( + for { + v <- version + c <- cache if c.exists + } yield c.content == v + ).getOrElse(false) + + // Are we outdated? + if (!upToDate) { + action + + // Write cache + for (c <- cache; v <- version) { + val w = c.contentWriter + try w.write(v) + finally w.close() + } + } + } + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/io/IO.scala b/tools/shared/src/main/scala/scala/scalajs/tools/io/IO.scala new file mode 100644 index 0000000..b69b07c --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/io/IO.scala @@ -0,0 +1,116 @@ +package scala.scalajs.tools.io + +import scala.annotation.tailrec + +import scala.reflect.ClassTag + +import java.io._ + +object IO { + /** Returns the lines in an input stream. + * Lines do not contain the new line characters. + */ + def readLines(stream: InputStream): List[String] = + readLines(new InputStreamReader(stream)) + + /** Returns the lines in a string. + * Lines do not contain the new line characters. + */ + def readLines(content: String): List[String] = + readLines(new StringReader(content)) + + /** Returns the lines in a reader. + * Lines do not contain the new line characters. + */ + def readLines(reader: Reader): List[String] = { + val br = new BufferedReader(reader) + try { + val builder = List.newBuilder[String] + @tailrec + def loop(): Unit = { + val line = br.readLine() + if (line ne null) { + builder += line + loop() + } + } + loop() + builder.result() + } finally { + br.close() + } + } + + /** Reads the entire content of a reader as a string. */ + def readReaderToString(reader: Reader): String = { + val buffer = newBuffer[Char] + val builder = new StringBuilder + @tailrec + def loop(): Unit = { + val len = reader.read(buffer) + if (len > 0) { + builder.appendAll(buffer, 0, len) + loop() + } + } + loop() + builder.toString() + } + + /** Reads the entire content of an input stream as a UTF-8 string. */ + def readInputStreamToString(stream: InputStream): String = { + val reader = new BufferedReader(new InputStreamReader(stream, "UTF-8")) + readReaderToString(reader) + } + + /** Reads the entire content of an input stream as a byte array. */ + def readInputStreamToByteArray(stream: InputStream): Array[Byte] = { + val builder = new ByteArrayOutputStream() + val buffer = newBuffer[Byte] + @tailrec + def loop(): Unit = { + val size = stream.read(buffer) + if (size > 0) { + builder.write(buffer, 0, size) + loop() + } + } + loop() + builder.toByteArray() + } + + /** Concatenates a bunch of VirtualTextFiles to a WritableVirtualTextFile. + * Adds a '\n' after each file. + */ + def concatFiles(output: WritableVirtualTextFile, + files: Seq[VirtualTextFile]): Unit = { + val buffer = newBuffer[Char] + val out = output.contentWriter + + try { + for (file <- files) { + val reader = file.reader + + @tailrec + def loop(): Unit = { + val size = reader.read(buffer) + if (size > 0) { + out.write(buffer, 0, size) + loop() + } + } + + try loop() + finally reader.close() + + // New line after each file + out.write('\n') + } + } finally { + out.close() + } + } + + @inline + private def newBuffer[T : ClassTag] = new Array[T](4096) +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/io/MemFiles.scala b/tools/shared/src/main/scala/scala/scalajs/tools/io/MemFiles.scala new file mode 100644 index 0000000..68f66dc --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/io/MemFiles.scala @@ -0,0 +1,105 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.io + +import java.io._ + +/** A base class for simple in-memory mutable virtual files. */ +class MemVirtualFile(val path: String) extends VirtualFile { + private[this] var _version: Option[String] = None + + override def version: Option[String] = _version + def version_=(v: Option[String]): Unit = _version = v + + final def withVersion(v: Option[String]): this.type = { + version = v + this + } + + override def exists: Boolean = true +} + +/** A simple in-memory mutable virtual text file. */ +class MemVirtualTextFile(p: String) extends MemVirtualFile(p) + with VirtualTextFile { + private[this] var _content: String = "" + + override def content: String = _content + def content_=(v: String): Unit = _content = v + + final def withContent(v: String): this.type = { + content = v + this + } +} + +trait WritableMemVirtualTextFile extends MemVirtualTextFile + with WritableVirtualTextFile { + def contentWriter: Writer = new StringWriter { + override def close(): Unit = { + super.close() + WritableMemVirtualTextFile.this.content = this.toString + } + } +} + +object WritableMemVirtualTextFile { + def apply(path: String): WritableMemVirtualTextFile = + new MemVirtualTextFile(path) with WritableMemVirtualTextFile +} + +/** A simple in-memory mutable virtual binary file. */ +class MemVirtualBinaryFile(p: String) extends MemVirtualFile(p) + with VirtualBinaryFile { + private[this] var _content: Array[Byte] = new Array[Byte](0) + + override def content: Array[Byte] = _content + def content_=(v: Array[Byte]): Unit = _content = v + + final def withContent(v: Array[Byte]): this.type = { + content = v + this + } +} + +/** A simple in-memory mutable virtual JS file. */ +class MemVirtualJSFile(p: String) extends MemVirtualTextFile(p) + with VirtualJSFile { + private[this] var _sourceMap: Option[String] = None + + override def sourceMap: Option[String] = _sourceMap + def sourceMap_=(v: Option[String]): Unit = _sourceMap = v + + final def withSourceMap(v: Option[String]): this.type = { + sourceMap = v + this + } +} + +trait WritableMemVirtualJSFile extends MemVirtualJSFile + with WritableVirtualJSFile + with WritableMemVirtualTextFile { + + def sourceMapWriter: Writer = new StringWriter { + override def close(): Unit = { + super.close() + WritableMemVirtualJSFile.this.sourceMap = Some(this.toString) + } + } +} + +object WritableMemVirtualJSFile { + def apply(path: String): WritableMemVirtualJSFile = + new MemVirtualJSFile(path) with WritableMemVirtualJSFile +} + +/** A simple in-memory mutable virtual serialized Scala.js IR file. */ +class MemVirtualSerializedScalaJSIRFile(p: String) extends MemVirtualBinaryFile(p) + with VirtualSerializedScalaJSIRFile diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/io/VirtualFiles.scala b/tools/shared/src/main/scala/scala/scalajs/tools/io/VirtualFiles.scala new file mode 100644 index 0000000..c62ab5c --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/io/VirtualFiles.scala @@ -0,0 +1,169 @@ +package scala.scalajs.tools.io + +import java.io._ +import java.net.URI + +import scala.scalajs.ir +import scala.scalajs.tools.sourcemap._ + +/** A virtual input file. + */ +trait VirtualFile { + /** Path of the file, including everything. + * Unique if possible (used for lookup). */ + def path: String + + /** Name of the file/writer, including extension */ + def name: String = VirtualFile.nameFromPath(path) + + /** Optionally returns an implementation-dependent "version" token. + * Versions are compared with ==. + * If non-empty, a different version must be returned when the content + * changes. It should be equal if the content has not changed, but it is + * not mandatory. + * Such a token can be used by caches: the file need not be read and + * processed again if its version has not changed. + */ + def version: Option[String] = None + + /** Whether this file exists. Reading a non-existent file may fail */ + def exists: Boolean + + /** URI for this virtual file */ + def toURI: URI = { + new URI( + "virtualfile", // Pseudo-Scheme + path, // Scheme specific part + null // Fragment + ) + } +} + +object VirtualFile { + /** Splits at the last slash and returns remainder */ + def nameFromPath(path: String): String = { + val pos = path.lastIndexOf('/') + if (pos == -1) path + else path.substring(pos + 1) + } +} + +/** A virtual input file. + */ +trait VirtualTextFile extends VirtualFile { + /** Returns the content of the file. */ + def content: String + + /** Returns a new Reader of the file. */ + def reader: Reader = new StringReader(content) + + /** Returns the lines in the content. + * Lines do not contain the new line characters. + */ + def readLines(): List[String] = IO.readLines(reader) +} + +object VirtualTextFile { + def empty(path: String): VirtualTextFile = + new MemVirtualTextFile(path) +} + +trait WritableVirtualTextFile extends VirtualTextFile { + def contentWriter: Writer +} + +/** A virtual binary input file. + */ +trait VirtualBinaryFile extends VirtualFile { + /** Returns the content of the file. */ + def content: Array[Byte] + + /** Returns a new InputStream of the file. */ + def inputStream: InputStream = new ByteArrayInputStream(content) +} + +/** A virtual input file which contains JavaScript code. + * It may have a source map associated with it. + */ +trait VirtualJSFile extends VirtualTextFile { + /** Optionally, content of the source map file associated with this + * JavaScript source. + */ + def sourceMap: Option[String] = None +} + +object VirtualJSFile { + def empty(path: String): VirtualJSFile = + new MemVirtualJSFile(path).withVersion(Some(path)) +} + +trait WritableVirtualJSFile extends WritableVirtualTextFile with VirtualJSFile { + def sourceMapWriter: Writer +} + +/** A virtual Scala.js IR file. + * It contains the class info and the IR tree. + */ +trait VirtualScalaJSIRFile extends VirtualFile { + /** Rough class info of this file. */ + def roughInfo: ir.Infos.RoughClassInfo = info + + /** Class info of this file. */ + def info: ir.Infos.ClassInfo = + infoAndTree._1 + + /** IR Tree of this file. */ + def tree: ir.Trees.ClassDef = + infoAndTree._2 + + /** Class info and IR tree of this file. */ + def infoAndTree: (ir.Infos.ClassInfo, ir.Trees.ClassDef) +} + +/** Base trait for virtual Scala.js IR files that are serialized as binary file. + */ +trait VirtualSerializedScalaJSIRFile extends VirtualBinaryFile with VirtualScalaJSIRFile { + /** Rough class info of this file. */ + override def roughInfo: ir.Infos.RoughClassInfo = { + // Overridden to read only the necessary parts + val stream = inputStream + try { + ir.InfoSerializers.deserializeRoughInfo(stream) + } catch { + case e: IOException => + throw new IOException(s"Failed to deserialize rough info of $path", e) + } finally { + stream.close() + } + } + + /** Class info of this file. */ + override def info: ir.Infos.ClassInfo = { + // Overridden to read only the necessary parts + val stream = inputStream + try { + ir.InfoSerializers.deserializeFullInfo(stream) + } catch { + case e: IOException => + throw new IOException(s"Failed to deserialize info of $path", e) + } finally { + stream.close() + } + } + + /** Class info and IR tree of this file. */ + override def infoAndTree: (ir.Infos.ClassInfo, ir.Trees.ClassDef) = { + val stream = inputStream + try { + val (version, info) = ir.InfoSerializers.deserializeVersionFullInfo(stream) + val tree = ir.Serializers.deserialize( + stream, version).asInstanceOf[ir.Trees.ClassDef] + (info, tree) + } catch { + case e: IOException => + throw new IOException(s"Failed to deserialize $path", e) + } finally { + stream.close() + } + } +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/javascript/JSDesugaring.scala b/tools/shared/src/main/scala/scala/scalajs/tools/javascript/JSDesugaring.scala new file mode 100644 index 0000000..b4d4005 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/javascript/JSDesugaring.scala @@ -0,0 +1,1525 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.javascript + +import scala.language.implicitConversions + +import scala.annotation.switch + +import scala.scalajs.ir._ +import Position._ +import Transformers._ +import scala.scalajs.ir.Trees._ +import scala.scalajs.ir.Types._ + +import scala.scalajs.tools.sem._ +import CheckedBehavior._ + +import scala.scalajs.tools.javascript.{Trees => js} + +/** Desugaring of the IR to regular ES5 JavaScript. + * + * The major difference between the IR and JS is that most constructs can be + * used in expression position. The main work of the desugaring is to + * unnest complex constructs in expression position so that they become + * statements. + * + * The general idea is two-folded: + * 1) Unnest complex constructs in "argument position": + * When a complex construct is used in a non-rhs expression position + * (argument to a function, operand, condition of an if, etc.), that we + * call "argument position", declare a variable before the statement, + * assign the complex construct to it and then use that variable in the + * argument position instead. + * 2) Push LHS's inside complex RHS's: + * When an rhs is a complex construct, push the lhs inside the complex + * construct. Are considered lhs: + * * Assign, i.e., `x =` + * * VarDef, i.e., `var x =` + * * Return, i.e., `return` + * * (EmptyTree is also used as a trick for code reuse) + * In fact, think that, in this context, LHS means: what to do with the + * result of evaluating the RHS. + * + * -------------------------------------------------------------------------- + * + * Typical example, consider the method call: + * + * obj.meth({ + * var x = foo(42); + * x*x + * }); + * + * According to rule 1), the block that is passed as a parameter to obj.meth + * is first extracted in a synthetic var: + * + * var x\$1 = { + * var x = foo(42); + * x*x + * } + * obj.meth(x\$1); + * + * Then, according to rule 2), the lhs `var x\$1 =` is pushed inside the block: + * + * { + * var x = foo(42); + * var x\$1 = x*x; + * } + * obj.meth(x\$1); + * + * Because bare blocks are non-significant in JS, this is equivalent to + * + * var x = foo(42); + * var x\$1 = x*x; + * obj.meth(x\$1); + * + * -------------------------------------------------------------------------- + * + * JSDesugaring does all this in a single pass, but it helps to think that: + * * Rule 1) is implemented by unnest(), and used most notably in + * * transformStat() for statement-only constructs + * * pushLhsInto() for statement-or-expression constructs + * * Rule 2) is implemented by pushLhsInto() + * * Emitting the class structure is delegated to [[ScalaJSClassEmitter]]. + * + * There are a few other things that JSDesugaring takes care of: + * * Transform Scala expressions into their JS equivalent, taking the + * Scala.js class encoding into account. + * * And tiny details. + * + * @author Sébastien Doeraene + */ +object JSDesugaring { + + private final val ScalaJSEnvironmentName = "ScalaJS" + + /** Desugars a statement of the IR into ES5 JavaScript. */ + def desugarJavaScript(tree: Tree, semantics: Semantics): js.Tree = { + new JSDesugar(semantics).transformStat(tree) + } + + private[javascript] implicit def transformIdent(ident: Ident): js.Ident = + js.Ident(ident.name, ident.originalName)(ident.pos) + + private[javascript] def transformParamDef(paramDef: ParamDef): js.ParamDef = + js.ParamDef(paramDef.name, paramDef.mutable)(paramDef.pos) + + private class JSDesugar(semantics: Semantics) { + + // Synthetic variables + + var syntheticVarCounter: Int = 0 + + def newSyntheticVar()(implicit pos: Position): Ident = { + syntheticVarCounter += 1 + Ident("jsx$" + syntheticVarCounter, None) + } + + def resetSyntheticVarCounterIn[A](f: => A): A = { + val savedCounter = syntheticVarCounter + syntheticVarCounter = 0 + try f + finally syntheticVarCounter = savedCounter + } + + // Record names + + def makeRecordFieldIdent(recIdent: Ident, fieldIdent: Ident)( + implicit pos: Position): Ident = + makeRecordFieldIdent(recIdent.name, recIdent.originalName, + fieldIdent.name, fieldIdent.originalName) + + def makeRecordFieldIdent(recIdent: Ident, + fieldName: String, fieldOrigiName: Option[String])( + implicit pos: Position): Ident = + makeRecordFieldIdent(recIdent.name, recIdent.originalName, + fieldName, fieldOrigiName) + + def makeRecordFieldIdent(recName: String, recOrigName: Option[String], + fieldName: String, fieldOrigName: Option[String])( + implicit pos: Position): Ident = { + val name = recName + "_$_" + fieldName + val originalName = Some(recOrigName.getOrElse(recName) + "." + + fieldOrigName.getOrElse(fieldName)) + Ident(name, originalName) + } + + // LHS'es for labeled expressions + + var labeledExprLHSes: Map[Ident, Tree] = Map.empty + + // Now the work + + /** Desugar a statement of the IR into ES5 JS */ + def transformStat(tree: Tree): js.Tree = { + implicit val pos = tree.pos + + tree match { + // Statement-only language constructs + + case Skip() => + js.Skip() + + case VarDef(varIdent, RecordType(fields), recMutable, EmptyTree) => + js.Block(for { + RecordType.Field(fieldName, fieldOrigName, tpe, fieldMutable) <- fields + } yield { + transformStat { + VarDef(makeRecordFieldIdent(varIdent, fieldName, fieldOrigName), + tpe, recMutable || fieldMutable, EmptyTree) + } + }) + + case VarDef(name, _, mutable, EmptyTree) => + js.VarDef(name, mutable, js.EmptyTree) + + case VarDef(_, _, _, rhs) => + pushLhsInto(tree, rhs) + + case Assign(RecordFieldVarRef(lhs), rhs) => + pushLhsInto(Assign(lhs, EmptyTree), rhs) + + case Assign(select @ Select(qualifier, item, mutable), rhs) => + unnest(qualifier, rhs) { (newQualifier, newRhs) => + js.Assign( + js.DotSelect(transformExpr(newQualifier), item)(select.pos), + transformExpr(newRhs)) + } + + case Assign(select @ ArraySelect(array, index), rhs) => + unnest(List(array, index, rhs)) { + case List(newArray, newIndex, newRhs) => + js.Assign( + js.BracketSelect(js.DotSelect(transformExpr(newArray), + js.Ident("u"))(select.pos), + transformExpr(newIndex))(select.pos), + transformExpr(newRhs)) + } + + case Assign(select @ JSDotSelect(qualifier, item), rhs) => + unnest(qualifier, rhs) { (newQualifier, newRhs) => + js.Assign( + js.DotSelect(transformExpr(newQualifier), item)(select.pos), + transformExpr(newRhs)) + } + + case Assign(select @ JSBracketSelect(qualifier, item), rhs) => + unnest(List(qualifier, item, rhs)) { + case List(newQualifier, newItem, newRhs) => + js.Assign( + js.BracketSelect(transformExpr(newQualifier), + transformExpr(newItem))(select.pos), + transformExpr(newRhs)) + } + + case Assign(_ : VarRef, rhs) => + pushLhsInto(tree, rhs) + + case Assign(_, _) => + sys.error(s"Illegal Assign in transformStat: $tree") + + case StoreModule(cls, value) => + assert(cls.className.endsWith("$"), + s"Trying to store module for non-module class $cls") + val moduleName = cls.className.dropRight(1) + unnest(value) { newValue => + js.Assign( + js.DotSelect(envField("n"), Ident(moduleName)), + transformExpr(newValue)) + } + + case While(cond, body, label) => + /* We cannot simply unnest(cond) here, because that would eject the + * evaluation of the condition out of the loop. + */ + val newLabel = label.map(transformIdent) + if (isExpression(cond)) { + js.While(transformExpr(cond), transformStat(body), newLabel) + } else { + js.While(js.BooleanLiteral(true), { + unnest(cond) { newCond => + js.If(transformExpr(newCond), transformStat(body), js.Break()) + } + }, newLabel) + } + + case DoWhile(body, cond, label) => + /* We cannot simply unnest(cond) here, because that would eject the + * evaluation of the condition out of the loop. + */ + val newLabel = label.map(transformIdent) + if (isExpression(cond)) { + js.DoWhile(transformStat(body), transformExpr(cond), newLabel) + } else { + /* This breaks 'continue' statements for this loop, but we don't + * care because we never emit continue statements for do..while + * loops. + */ + js.While(js.BooleanLiteral(true), { + js.Block(transformStat(body), { + unnest(cond) { newCond => + js.If(transformExpr(newCond), js.Skip(), js.Break()) + } + }) + }, newLabel) + } + + case Debugger() => + js.Debugger() + + case JSDelete(JSDotSelect(obj, prop)) => + unnest(obj) { (newObj) => + js.Delete(js.DotSelect(transformExpr(newObj), prop)) + } + + case JSDelete(JSBracketSelect(obj, prop)) => + unnest(obj, prop) { (newObj, newProp) => + js.Delete(js.BracketSelect( + transformExpr(newObj), transformExpr(newProp))) + } + + // Treat 'return' as an LHS + + case Return(expr, label) => + pushLhsInto(tree, expr) + + /* Anything else is an expression => pushLhsInto(EmptyTree, _) + * In order not to duplicate all the code of pushLhsInto() here, we + * use a trick: EmptyTree is a dummy LHS that says "do nothing + * with the result of the rhs". + * This is exactly what an expression statement is doing: it evaluates + * the expression, but does nothing with its result. + */ + + case _ => + pushLhsInto(EmptyTree, tree) + } + } + + private object RecordFieldVarRef { + def unapply(tree: Tree): Option[VarRef] = { + tree match { + case Select(RecordVarRef(VarRef(recIdent, recMutable)), + fieldIdent, fieldMutable) => + implicit val pos = tree.pos + Some(VarRef(makeRecordFieldIdent(recIdent, fieldIdent), + recMutable || fieldMutable)(tree.tpe)) + case _ => + None + } + } + } + + private object RecordVarRef { + def unapply(tree: Tree): Option[VarRef] = { + if (!tree.tpe.isInstanceOf[RecordType]) None + else { + tree match { + case tree: VarRef => Some(tree) + case Select(RecordVarRef(VarRef(recIdent, recMutable)), + fieldIdent, fieldMutable) => + implicit val pos = tree.pos + Some(VarRef(makeRecordFieldIdent(recIdent, fieldIdent), + recMutable || fieldMutable)(tree.tpe)) + } + } + } + } + + /** Unnest complex constructs in argument position in temporary variables + * + * If all the arguments are JS expressions, there is nothing to do. + * Any argument that is not a JS expression must be unnested and stored + * in a temporary variable before the statement produced by `makeStat`. + * + * But *this changes the evaluation order!* In order not to lose it, it + * is necessary to also unnest arguments that are expressions but that + * are supposed to be evaluated before the argument-to-be-unnested and + * could have side-effects or even whose evaluation could be influenced + * by the side-effects of another unnested argument. + * + * Without deep effect analysis, which we do not do, we need to take + * a very pessimistic approach, and unnest any expression that contains + * an identifier (except those after the last non-expression argument). + * Hence the predicate `isPureExpressionWithoutIdent`. + */ + def unnest(args: List[Tree])( + makeStat: List[Tree] => js.Tree): js.Tree = { + if (args forall isExpression) makeStat(args) + else { + val extractedStatements = new scala.collection.mutable.ListBuffer[js.Tree] + + /* Attention! Everything must be processed recursively + * *right-to-left*! Indeed, the point is that noExtractYet will tell + * whether anything supposed to be evaluated *after* the currently + * being processed expression has been (at least partly) extracted + * in temporary variables (or simply statements, in the Block case). + * If nothing has, we can keep more in place without having to extract + * that expression in a temporary variable. + */ + + def rec(arg: Tree): Tree = { + def noExtractYet = extractedStatements.isEmpty + + if (if (noExtractYet) isExpression(arg) else isPureExpression(arg)) { + arg + } else { + implicit val pos = arg.pos + arg match { + case Block(stats :+ expr) => + val result = rec(expr) // right-to-left, remember? + // Put the stats in a Block because ++=: is not smart + js.Block(stats.map(transformStat)) +=: extractedStatements + result + + case UnaryOp(op, lhs) => + UnaryOp(op, rec(lhs)) + case BinaryOp(op, lhs, rhs) => + val newRhs = rec(rhs) + BinaryOp(op, rec(lhs), newRhs) + case JSBinaryOp(op, lhs, rhs) => + val newRhs = rec(rhs) + JSBinaryOp(op, rec(lhs), newRhs) + case JSUnaryOp(op, lhs) => + JSUnaryOp(op, rec(lhs)) + case IsInstanceOf(expr, tpe) => + IsInstanceOf(rec(expr), tpe) + + case AsInstanceOf(expr, tpe) + if noExtractYet || semantics.asInstanceOfs == Unchecked => + AsInstanceOf(rec(expr), tpe) + case Unbox(expr, tpe) + if noExtractYet || semantics.asInstanceOfs == Unchecked => + Unbox(rec(expr), tpe) + + case NewArray(tpe, lengths) => + NewArray(tpe, recs(lengths)) + case ArrayValue(tpe, elems) => + ArrayValue(tpe, recs(elems)) + case JSArrayConstr(items) => + JSArrayConstr(recs(items)) + case JSObjectConstr(items) => + val newValues = recs(items.map(_._2)) + JSObjectConstr(items.map(_._1) zip newValues) + case Closure(captureParams, params, body, captureValues) => + Closure(captureParams, params, body, recs(captureValues)) + + case New(cls, constr, args) if noExtractYet => + New(cls, constr, recs(args)) + case Select(qualifier, item, mutable) if noExtractYet => + Select(rec(qualifier), item, mutable)(arg.tpe) + case Apply(receiver, method, args) if noExtractYet => + val newArgs = recs(args) + Apply(rec(receiver), method, newArgs)(arg.tpe) + case StaticApply(receiver, cls, method, args) if noExtractYet => + val newArgs = recs(args) + StaticApply(rec(receiver), cls, method, newArgs)(arg.tpe) + case TraitImplApply(impl, method, args) if noExtractYet => + TraitImplApply(impl, method, recs(args))(arg.tpe) + case ArrayLength(array) if noExtractYet => + ArrayLength(rec(array)) + case ArraySelect(array, index) if noExtractYet => + val newIndex = rec(index) + ArraySelect(rec(array), newIndex)(arg.tpe) + case CallHelper(helper, args) if noExtractYet => + CallHelper(helper, recs(args))(arg.tpe) + + case If(cond, thenp, elsep) + if noExtractYet && isExpression(thenp) && isExpression(elsep) => + If(rec(cond), thenp, elsep)(arg.tpe) + + case _ => + val temp = newSyntheticVar() + val computeTemp = + pushLhsInto(VarDef(temp, arg.tpe, mutable = false, EmptyTree), arg) + computeTemp +=: extractedStatements + VarRef(temp, mutable = false)(arg.tpe) + } + } + } + + def recs(args: List[Tree]): List[Tree] = { + // This is a right-to-left map + args.foldRight[List[Tree]](Nil) { (arg, acc) => + rec(arg) :: acc + } + } + + val newArgs = recs(args) + + assert(extractedStatements.nonEmpty, + "Reached computeTemps with no temp to compute") + + val newStatement = makeStat(newArgs) + js.Block(extractedStatements.result() ::: List(newStatement))(newStatement.pos) + } + } + + /** Same as above, for a single argument */ + def unnest(arg: Tree)( + makeStat: Tree => js.Tree): js.Tree = { + unnest(List(arg)) { + case List(newArg) => makeStat(newArg) + } + } + + /** Same as above, for two arguments */ + def unnest(lhs: Tree, rhs: Tree)( + makeStat: (Tree, Tree) => js.Tree): js.Tree = { + unnest(List(lhs, rhs)) { + case List(newLhs, newRhs) => makeStat(newLhs, newRhs) + } + } + + /** Same as above, for one head argument and a list of arguments */ + def unnest(arg0: Tree, args: List[Tree])( + makeStat: (Tree, List[Tree]) => js.Tree): js.Tree = { + unnest(arg0 :: args) { newArgs => + makeStat(newArgs.head, newArgs.tail) + } + } + + /** Common implementation for the functions below. + * A pure expression can be moved around or executed twice, because it + * will always produce the same result and never have side-effects. + * A side-effect free expression can be elided if its result is not used. + */ + private def isExpressionInternal(tree: Tree, + allowUnpure: Boolean, allowSideEffects: Boolean): Boolean = { + + require(!allowSideEffects || allowUnpure) + + def test(tree: Tree): Boolean = tree match { + // Atomic expressions + case _: Literal => true + case _: This => true + case _: JSEnvInfo => true + + // Vars and fields (side-effect free, pure if immutable) + case VarRef(_, mutable) => + allowUnpure || !mutable + case Select(qualifier, item, mutable) => + (allowUnpure || !mutable) && test(qualifier) + + // Expressions preserving pureness + case Block(trees) => trees forall test + case If(cond, thenp, elsep) => test(cond) && test(thenp) && test(elsep) + case BinaryOp(_, lhs, rhs) => test(lhs) && test(rhs) + case UnaryOp(_, lhs) => test(lhs) + case JSBinaryOp(_, lhs, rhs) => test(lhs) && test(rhs) + case JSUnaryOp(_, lhs) => test(lhs) + case ArrayLength(array) => test(array) + case IsInstanceOf(expr, _) => test(expr) + + // Expressions preserving side-effect freedom + case NewArray(tpe, lengths) => + allowUnpure && (lengths forall test) + case ArrayValue(tpe, elems) => + allowUnpure && (elems forall test) + case ArraySelect(array, index) => + allowUnpure && test(array) && test(index) + case JSArrayConstr(items) => + allowUnpure && (items forall test) + case JSObjectConstr(items) => + allowUnpure && (items forall (item => test(item._2))) + case Closure(captureParams, params, body, captureValues) => + allowUnpure && (captureValues forall test) + + // Scala expressions that can always have side-effects + case New(cls, constr, args) => + allowSideEffects && (args forall test) + case LoadModule(cls) => // unfortunately + allowSideEffects + case Apply(receiver, method, args) => + allowSideEffects && test(receiver) && (args forall test) + case StaticApply(receiver, cls, method, args) => + allowSideEffects && test(receiver) && (args forall test) + case TraitImplApply(impl, method, args) => + allowSideEffects && (args forall test) + case GetClass(arg) => + allowSideEffects && test(arg) + case CallHelper(helper, args) => + allowSideEffects && (args forall test) + + // Casts + case AsInstanceOf(expr, _) => + (allowSideEffects || semantics.asInstanceOfs == Unchecked) && test(expr) + case Unbox(expr, _) => + (allowSideEffects || semantics.asInstanceOfs == Unchecked) && test(expr) + + // Because the env is a frozen object, env["global"] is pure + case JSBracketSelect(JSEnvInfo(), StringLiteral("global")) => true + + // JavaScript expressions that can always have side-effects + case JSNew(fun, args) => + allowSideEffects && test(fun) && (args forall test) + case JSDotSelect(qualifier, item) => + allowSideEffects && test(qualifier) + case JSBracketSelect(qualifier, item) => + allowSideEffects && test(qualifier) && test(item) + case JSFunctionApply(fun, args) => + allowSideEffects && test(fun) && (args forall test) + case JSDotMethodApply(receiver, method, args) => + allowSideEffects && test(receiver) && (args forall test) + case JSBracketMethodApply(receiver, method, args) => + allowSideEffects && test(receiver) && test(method) && (args forall test) + + // Non-expressions + case _ => false + } + test(tree) + } + + /** Test whether the given tree is a standard JS expression. + */ + def isExpression(tree: Tree): Boolean = + isExpressionInternal(tree, allowUnpure = true, allowSideEffects = true) + + /** Test whether the given tree is a side-effect-free standard JS expression. + */ + def isSideEffectFreeExpression(tree: Tree): Boolean = + isExpressionInternal(tree, allowUnpure = true, allowSideEffects = false) + + /** Test whether the given tree is a pure standard JS expression. + */ + def isPureExpression(tree: Tree): Boolean = + isExpressionInternal(tree, allowUnpure = false, allowSideEffects = false) + + def doVarDef(ident: Ident, tpe: Type, mutable: Boolean, rhs: Tree): js.Tree = { + implicit val pos = rhs.pos + tpe match { + case RecordType(fields) => + val elems = (rhs: @unchecked) match { + case RecordValue(_, elems) => + elems + case VarRef(rhsIdent, rhsMutable) => + for (RecordType.Field(fName, fOrigName, fTpe, fMutable) <- fields) + yield VarRef(makeRecordFieldIdent(rhsIdent, fName, fOrigName), + rhsMutable || fMutable)(fTpe) + } + js.Block(for { + (RecordType.Field(fName, fOrigName, fTpe, fMutable), + fRhs) <- fields zip elems + } yield { + doVarDef(makeRecordFieldIdent(ident, fName, fOrigName), + fTpe, mutable || fMutable, fRhs) + }) + + case _ => + js.VarDef(ident, mutable, transformExpr(rhs)) + } + } + + def doAssign(lhs: Tree, rhs: Tree): js.Tree = { + implicit val pos = rhs.pos + lhs.tpe match { + case RecordType(fields) => + val VarRef(ident, mutable) = lhs + val elems = (rhs: @unchecked) match { + case VarRef(rhsIdent, rhsMutable) => + for (RecordType.Field(fName, fOrigName, fTpe, fMutable) <- fields) + yield VarRef(makeRecordFieldIdent(rhsIdent, fName, fOrigName), + rhsMutable || fMutable)(fTpe) + } + js.Block(for { + (RecordType.Field(fName, fOrigName, fTpe, fMutable), + fRhs) <- fields zip elems + } yield { + doAssign(VarRef(makeRecordFieldIdent(ident, fName, fOrigName), + mutable || fMutable)(fTpe), fRhs) + }) + + case _ => + js.Assign(transformExpr(lhs), transformExpr(rhs)) + } + } + + /** Push an lhs into a (potentially complex) rhs + * lhs can be either a EmptyTree, a VarDef, a Assign or a + * Return + */ + def pushLhsInto(lhs: Tree, rhs: Tree): js.Tree = { + implicit val rhsPos = rhs.pos + + /** Push the current lhs further into a deeper rhs */ + @inline def redo(newRhs: Tree) = pushLhsInto(lhs, newRhs) + + if (rhs.tpe == NothingType && lhs != EmptyTree) { + /* A touch of peephole dead code elimination. + * Actually necessary to handle pushing an lhs into an infinite loop, + * for example. + */ + val transformedRhs = pushLhsInto(EmptyTree, rhs) + lhs match { + case VarDef(name, _, mutable, _) => + /* We still need to declare the var, in case it is used somewhere + * else in the function, where we can't dce it. + */ + js.Block(js.VarDef(name, true, js.EmptyTree), transformedRhs) + case _ => + transformedRhs + } + } else (rhs match { + // Handle the Block before testing whether it is an expression + + case Block(stats :+ expr) => + js.Block((stats map transformStat) :+ redo(expr)) + + // Base case, rhs is already a regular JS expression + + case _ if isExpression(rhs) => + (lhs: @unchecked) match { + case EmptyTree => + if (isSideEffectFreeExpression(rhs)) js.Skip() + else transformExpr(rhs) + case VarDef(name, tpe, mutable, _) => + doVarDef(name, tpe, mutable, rhs) + case Assign(lhs, _) => + doAssign(lhs, rhs) + case Return(_, None) => + js.Return(transformExpr(rhs)) + case Return(_, label @ Some(l)) => + labeledExprLHSes(l) match { + case newLhs @ Return(_, _) => + pushLhsInto(newLhs, rhs) // no need to break here + case newLhs => + js.Block(pushLhsInto(newLhs, rhs), + js.Break(label.map(transformIdent))) + } + } + + // Almost base case with RecordValue + + case RecordValue(recTpe, elems) => + (lhs: @unchecked) match { + case EmptyTree => + js.Block(elems map transformStat) + case VarDef(name, tpe, mutable, _) => + unnest(elems) { newElems => + doVarDef(name, tpe, mutable, RecordValue(recTpe, newElems)) + } + case Assign(lhs, _) => + unnest(elems) { newElems => + val temp = newSyntheticVar() + js.Block( + doVarDef(temp, recTpe, false, RecordValue(recTpe, newElems)), + doAssign(lhs, VarRef(temp, false)(recTpe))) + } + case Return(_, label @ Some(l)) => + val newLhs = labeledExprLHSes(l) + js.Block(pushLhsInto(newLhs, rhs), + js.Break(label.map(transformIdent))) + } + + // Control flow constructs + + case Labeled(label, tpe, body) => + val savedMap = labeledExprLHSes + labeledExprLHSes = labeledExprLHSes + (label -> lhs) + try { + lhs match { + case Return(_, _) => redo(body) + case _ => js.Labeled(label, redo(body)) + } + } finally { + labeledExprLHSes = savedMap + } + + case Return(expr, _) => + pushLhsInto(rhs, expr) + + case Continue(label) => + js.Continue(label.map(transformIdent)) + + case If(cond, thenp, elsep) => + unnest(cond) { newCond => + js.If(transformExpr(newCond), redo(thenp), redo(elsep)) + } + + case Try(block, errVar, handler, finalizer) => + val newHandler = + if (handler == EmptyTree) js.EmptyTree else redo(handler) + val newFinalizer = + if (finalizer == EmptyTree) js.EmptyTree else transformStat(finalizer) + + if (newHandler != js.EmptyTree && newFinalizer != js.EmptyTree) { + /* The Google Closure Compiler wrongly eliminates finally blocks, if + * the catch block throws an exception. + * Issues: #563, google/closure-compiler#186 + * + * Therefore, we desugar + * + * try { ... } catch { ... } finally { ... } + * + * into + * + * try { try { ... } catch { ... } } finally { ... } + */ + js.Try(js.Try(redo(block), errVar, newHandler, js.EmptyTree), + errVar, js.EmptyTree, newFinalizer) + } else + js.Try(redo(block), errVar, newHandler, newFinalizer) + + // TODO Treat throw as an LHS? + case Throw(expr) => + unnest(expr) { newExpr => + js.Throw(transformExpr(newExpr)) + } + + /** Matches are desugared into switches + * + * A match is different from a switch in two respects, both linked + * to match being designed to be used in expression position in + * Extended-JS. + * + * * There is no fall-through from one case to the next one, hence, + * no break statement. + * * Match supports _alternatives_ explicitly (with a switch, one + * would use the fall-through behavior to implement alternatives). + */ + case Match(selector, cases, default) => + unnest(selector) { newSelector => + val newCases = { + for { + (values, body) <- cases + newValues = (values map transformExpr) + // add the break statement + newBody = js.Block(redo(body), js.Break()) + // desugar alternatives into several cases falling through + caze <- (newValues.init map (v => (v, js.Skip()))) :+ (newValues.last, newBody) + } yield { + caze + } + } + val newDefault = + if (default == EmptyTree) js.EmptyTree + else redo(default) + js.Switch(transformExpr(newSelector), newCases, newDefault) + } + + // Scala expressions (if we reach here their arguments are not expressions) + + case New(cls, ctor, args) => + unnest(args) { newArgs => + redo(New(cls, ctor, newArgs)) + } + + case Select(qualifier, item, mutable) => + unnest(qualifier) { newQualifier => + redo(Select(newQualifier, item, mutable)(rhs.tpe)) + } + + case Apply(receiver, method, args) => + unnest(receiver, args) { (newReceiver, newArgs) => + redo(Apply(newReceiver, method, newArgs)(rhs.tpe)) + } + + case StaticApply(receiver, cls, method, args) => + unnest(receiver, args) { (newReceiver, newArgs) => + redo(StaticApply(newReceiver, cls, method, newArgs)(rhs.tpe)) + } + + case TraitImplApply(impl, method, args) => + unnest(args) { newArgs => + redo(TraitImplApply(impl, method, newArgs)(rhs.tpe)) + } + + case UnaryOp(op, lhs) => + unnest(lhs) { newLhs => + redo(UnaryOp(op, newLhs)) + } + + case BinaryOp(op, lhs, rhs) => + unnest(lhs, rhs) { (newLhs, newRhs) => + redo(BinaryOp(op, newLhs, newRhs)) + } + + case NewArray(tpe, lengths) => + unnest(lengths) { newLengths => + redo(NewArray(tpe, newLengths)) + } + + case ArrayValue(tpe, elems) => + unnest(elems) { newElems => + redo(ArrayValue(tpe, newElems)) + } + + case ArrayLength(array) => + unnest(array) { newArray => + redo(ArrayLength(newArray)) + } + + case ArraySelect(array, index) => + unnest(array, index) { (newArray, newIndex) => + redo(ArraySelect(newArray, newIndex)(rhs.tpe)) + } + + case IsInstanceOf(expr, cls) => + unnest(expr) { newExpr => + redo(IsInstanceOf(newExpr, cls)) + } + + case AsInstanceOf(expr, cls) => + if (semantics.asInstanceOfs == Unchecked) { + redo(expr) + } else { + unnest(expr) { newExpr => + redo(AsInstanceOf(newExpr, cls)) + } + } + + case Unbox(expr, charCode) => + unnest(expr) { newExpr => + redo(Unbox(newExpr, charCode)) + } + + case GetClass(expr) => + unnest(expr) { newExpr => + redo(GetClass(newExpr)) + } + + case CallHelper(helper, args) => + unnest(args) { newArgs => + redo(CallHelper(helper, newArgs)(rhs.tpe)) + } + + // JavaScript expressions (if we reach here their arguments are not expressions) + + case JSNew(ctor, args) => + unnest(ctor :: args) { newCtorAndArgs => + val newCtor :: newArgs = newCtorAndArgs + redo(JSNew(newCtor, newArgs)) + } + + case JSFunctionApply(fun, args) => + unnest(fun :: args) { newFunAndArgs => + val newFun :: newArgs = newFunAndArgs + redo(JSFunctionApply(newFun, newArgs)) + } + + case JSDotMethodApply(receiver, method, args) => + unnest(receiver :: args) { newReceiverAndArgs => + val newReceiver :: newArgs = newReceiverAndArgs + redo(JSDotMethodApply(newReceiver, method, newArgs)) + } + + case JSBracketMethodApply(receiver, method, args) => + unnest(receiver :: method :: args) { newReceiverAndArgs => + val newReceiver :: newMethod :: newArgs = newReceiverAndArgs + redo(JSBracketMethodApply(newReceiver, newMethod, newArgs)) + } + + case JSDotSelect(qualifier, item) => + unnest(qualifier) { newQualifier => + redo(JSDotSelect(newQualifier, item)) + } + + case JSBracketSelect(qualifier, item) => + unnest(qualifier, item) { (newQualifier, newItem) => + redo(JSBracketSelect(newQualifier, newItem)) + } + + case JSUnaryOp(op, lhs) => + unnest(lhs) { newLhs => + redo(JSUnaryOp(op, newLhs)) + } + + case JSBinaryOp("&&", lhs, rhs) => + if (lhs.tpe == BooleanType) { + redo(If(lhs, rhs, BooleanLiteral(false))(AnyType)) + } else { + unnest(lhs) { newLhs => + redo(If(newLhs, rhs, newLhs)(AnyType)) + } + } + + case JSBinaryOp("||", lhs, rhs) => + if (lhs.tpe == BooleanType) { + redo(If(lhs, BooleanLiteral(true), rhs)(AnyType)) + } else { + unnest(lhs) { newLhs => + redo(If(newLhs, newLhs, rhs)(AnyType)) + } + } + + case JSBinaryOp(op, lhs, rhs) => + unnest(lhs, rhs) { (newLhs, newRhs) => + redo(JSBinaryOp(op, newLhs, newRhs)) + } + + case JSArrayConstr(items) => + unnest(items) { newItems => + redo(JSArrayConstr(newItems)) + } + + case JSObjectConstr(fields) => + val names = fields map (_._1) + val items = fields map (_._2) + unnest(items) { newItems => + redo(JSObjectConstr(names.zip(newItems))) + } + + // Closures + + case Closure(captureParams, params, body, captureValues) => + unnest(captureValues) { newCaptureValues => + redo(Closure(captureParams, params, body, newCaptureValues)) + } + + case _ => + if (lhs == EmptyTree) { + /* Go "back" to transformStat() after having dived into + * expression statements. Remember that (lhs == EmptyTree) + * is a trick that we use to "add" all the code of pushLhsInto() + * to transformStat(). + */ + rhs match { + case _:Skip | _:VarDef | _:Assign | _:While | _:DoWhile | + _:Debugger | _:JSDelete | _:StoreModule | _:ClassDef => + transformStat(rhs) + case _ => + sys.error("Illegal tree in JSDesugar.pushLhsInto():\n" + + "lhs = " + lhs + "\n" + "rhs = " + rhs + + " of class " + rhs.getClass) + } + } else { + sys.error("Illegal tree in JSDesugar.pushLhsInto():\n" + + "lhs = " + lhs + "\n" + "rhs = " + rhs + + " of class " + rhs.getClass) + } + }) + } + + // Desugar Scala operations to JavaScript operations ----------------------- + + /** Desugar an expression of the IR into ES5 JS */ + def transformExpr(tree: Tree): js.Tree = { + import TreeDSL._ + + implicit val pos = tree.pos + + def or0(tree: js.Tree): js.Tree = + js.BinaryOp("|", tree, js.IntLiteral(0)) + + tree match { + // Control flow constructs + + case Block(stats :+ expr) => + js.Block((stats map transformStat) :+ transformExpr(expr)) + + // Note that these work even if thenp/elsep is not a BooleanType + case If(cond, BooleanLiteral(true), elsep) => + js.BinaryOp("||", transformExpr(cond), transformExpr(elsep)) + case If(cond, thenp, BooleanLiteral(false)) => + js.BinaryOp("&&", transformExpr(cond), transformExpr(thenp)) + + case If(cond, thenp, elsep) => + js.If(transformExpr(cond), transformExpr(thenp), transformExpr(elsep)) + + // Scala expressions + + case New(cls, ctor, args) => + js.Apply(js.New(encodeClassVar(cls.className), Nil) DOT ctor, + args map transformExpr) + + case LoadModule(cls) => + genLoadModule(cls.className) + + case RecordFieldVarRef(VarRef(name, mutable)) => + js.VarRef(name, mutable) + + case Select(qualifier, item, _) => + transformExpr(qualifier) DOT item + + case Apply(receiver, method, args) => + val newReceiver = transformExpr(receiver) + val newArgs = args map transformExpr + if (isMaybeHijackedClass(receiver.tpe) && + !Definitions.isReflProxyName(method.name)) { + val helperName = hijackedClassMethodToHelperName(method.name) + genCallHelper(helperName, newReceiver :: newArgs: _*) + } else { + js.Apply(newReceiver DOT method, newArgs) + } + + case StaticApply(receiver, cls, method, args) => + val fun = encodeClassVar(cls.className).prototype DOT method + js.Apply(fun DOT "call", (receiver :: args) map transformExpr) + + case TraitImplApply(impl, method, args) => + js.Apply(envField("i") DOT method, args map transformExpr) + + case UnaryOp(op, lhs) => + import UnaryOp._ + val newLhs = transformExpr(lhs) + (op: @switch) match { + case `typeof` => js.UnaryOp("typeof", newLhs) + case Boolean_! => js.UnaryOp("!", newLhs) + case DoubleToInt => js.BinaryOp("|", newLhs, js.IntLiteral(0)) + + case LongToInt => genLongMethodApply(newLhs, LongImpl.toInt) + case LongToDouble => genLongMethodApply(newLhs, LongImpl.toDouble) + + case DoubleToFloat => genFround(newLhs) + + case IntToLong => + genNewLong(LongImpl.initFromInt, newLhs) + case DoubleToLong => + genLongModuleApply(LongImpl.fromDouble, newLhs) + } + + case BinaryOp(op, lhs, rhs) => + import BinaryOp._ + val lhs1 = lhs match { + case UnaryOp(UnaryOp.DoubleToInt, inner) + if op == Int_& || op == Int_<< => + /* This case is emitted typically by conversions from + * Float/Double to Char/Byte/Short. We have to introduce an + * (int) cast in the IR so that it typechecks, but in JavaScript + * this is redundant because & and << already convert both their + * operands to ints. So we get rid of the conversion here. + */ + inner + case _ => + lhs + } + + val newLhs = transformExpr(lhs1) + val newRhs = transformExpr(rhs) + + (op: @switch) match { + case === | Num_== | Boolean_== => js.BinaryOp("===", newLhs, newRhs) + case !== | Num_!= | Boolean_!= => js.BinaryOp("!==", newLhs, newRhs) + + case String_+ => + if (lhs.tpe == StringType || rhs.tpe == StringType) + js.BinaryOp("+", newLhs, newRhs) + else + js.BinaryOp("+", js.BinaryOp("+", js.StringLiteral(""), newLhs), newRhs) + + case `in` => js.BinaryOp("in", newLhs, newRhs) + case `instanceof` => js.BinaryOp("instanceof", newLhs, newRhs) + + case Int_+ => or0(js.BinaryOp("+", newLhs, newRhs)) + case Int_- => + lhs match { + case IntLiteral(0) => or0(js.UnaryOp("-", newRhs)) + case _ => or0(js.BinaryOp("-", newLhs, newRhs)) + } + case Int_* => genCallHelper("imul", newLhs, newRhs) + case Int_/ => or0(js.BinaryOp("/", newLhs, newRhs)) + case Int_% => js.BinaryOp("%", newLhs, newRhs) + + case Int_| => js.BinaryOp("|", newLhs, newRhs) + case Int_& => js.BinaryOp("&", newLhs, newRhs) + case Int_^ => + lhs match { + case IntLiteral(-1) => js.UnaryOp("~", newRhs) + case _ => js.BinaryOp("^", newLhs, newRhs) + } + case Int_<< => js.BinaryOp("<<", newLhs, newRhs) + case Int_>>> => or0(js.BinaryOp(">>>", newLhs, newRhs)) + case Int_>> => js.BinaryOp(">>", newLhs, newRhs) + + case Float_+ => genFround(js.BinaryOp("+", newLhs, newRhs)) + case Float_- => + genFround(lhs match { + case DoubleLiteral(0.0) => js.UnaryOp("-", newRhs) + case _ => js.BinaryOp("-", newLhs, newRhs) + }) + case Float_* => genFround(js.BinaryOp("*", newLhs, newRhs)) + case Float_/ => genFround(js.BinaryOp("/", newLhs, newRhs)) + case Float_% => genFround(js.BinaryOp("%", newLhs, newRhs)) + + case Double_+ => js.BinaryOp("+", newLhs, newRhs) + case Double_- => + lhs match { + case DoubleLiteral(0.0) => js.UnaryOp("-", newRhs) + case _ => js.BinaryOp("-", newLhs, newRhs) + } + case Double_* => js.BinaryOp("*", newLhs, newRhs) + case Double_/ => js.BinaryOp("/", newLhs, newRhs) + case Double_% => js.BinaryOp("%", newLhs, newRhs) + + case Num_< => js.BinaryOp("<", newLhs, newRhs) + case Num_<= => js.BinaryOp("<=", newLhs, newRhs) + case Num_> => js.BinaryOp(">", newLhs, newRhs) + case Num_>= => js.BinaryOp(">=", newLhs, newRhs) + + case Long_+ => genLongMethodApply(newLhs, LongImpl.+, newRhs) + case Long_- => + lhs match { + case LongLiteral(0L) => genLongMethodApply(newRhs, LongImpl.UNARY_-) + case _ => genLongMethodApply(newLhs, LongImpl.-, newRhs) + } + case Long_* => genLongMethodApply(newLhs, LongImpl.*, newRhs) + case Long_/ => genLongMethodApply(newLhs, LongImpl./, newRhs) + case Long_% => genLongMethodApply(newLhs, LongImpl.%, newRhs) + + case Long_| => genLongMethodApply(newLhs, LongImpl.|, newRhs) + case Long_& => genLongMethodApply(newLhs, LongImpl.&, newRhs) + case Long_^ => + lhs match { + case LongLiteral(-1L) => genLongMethodApply(newRhs, LongImpl.UNARY_~) + case _ => genLongMethodApply(newLhs, LongImpl.^, newRhs) + } + case Long_<< => genLongMethodApply(newLhs, LongImpl.<<, newRhs) + case Long_>>> => genLongMethodApply(newLhs, LongImpl.>>>, newRhs) + case Long_>> => genLongMethodApply(newLhs, LongImpl.>>, newRhs) + + case Long_== => genLongMethodApply(newLhs, LongImpl.===, newRhs) + case Long_!= => genLongMethodApply(newLhs, LongImpl.!==, newRhs) + case Long_< => genLongMethodApply(newLhs, LongImpl.<, newRhs) + case Long_<= => genLongMethodApply(newLhs, LongImpl.<=, newRhs) + case Long_> => genLongMethodApply(newLhs, LongImpl.>, newRhs) + case Long_>= => genLongMethodApply(newLhs, LongImpl.>=, newRhs) + + case Boolean_| => !(!js.BinaryOp("|", newLhs, newRhs)) + case Boolean_& => !(!js.BinaryOp("&", newLhs, newRhs)) + } + + case NewArray(tpe, lengths) => + genCallHelper("newArrayObject", + genClassDataOf(tpe), js.ArrayConstr(lengths map transformExpr)) + + case ArrayValue(tpe, elems) => + genCallHelper("makeNativeArrayWrapper", + genClassDataOf(tpe), js.ArrayConstr(elems map transformExpr)) + + case ArrayLength(array) => + js.BracketSelect(js.DotSelect(transformExpr(array), + Ident("u")), js.StringLiteral("length")) + + case ArraySelect(array, index) => + js.BracketSelect(js.DotSelect(transformExpr(array), + Ident("u")), transformExpr(index)) + + case IsInstanceOf(expr, cls) => + genIsInstanceOf(transformExpr(expr), cls) + + case AsInstanceOf(expr, cls) => + val newExpr = transformExpr(expr) + if (semantics.asInstanceOfs == Unchecked) newExpr + else genAsInstanceOf(newExpr, cls) + + case Unbox(expr, charCode) => + val newExpr = transformExpr(expr) + + if (semantics.asInstanceOfs == Unchecked) { + (charCode: @switch) match { + case 'Z' => !(!newExpr) + case 'B' | 'S' | 'I' => js.BinaryOp("|", newExpr, js.IntLiteral(0)) + case 'J' => genCallHelper("uJ", newExpr) + case 'F' => genFround(newExpr) + case 'D' => js.UnaryOp("+", newExpr) + } + } else { + genCallHelper("u"+charCode, newExpr) + } + + case GetClass(expr) => + genCallHelper("objectGetClass", transformExpr(expr)) + + case CallHelper(helper, args) => + genCallHelper(helper, args map transformExpr: _*) + + // JavaScript expressions + + case JSBracketSelect(JSEnvInfo(), StringLiteral("global")) => + // Shortcut for this field which is heavily used + envField("g") + + case JSNew(constr, args) => + js.New(transformExpr(constr), args map transformExpr) + + case JSDotSelect(qualifier, item) => + js.DotSelect(transformExpr(qualifier), item) + + case JSBracketSelect(qualifier, item) => + js.BracketSelect(transformExpr(qualifier), transformExpr(item)) + + case JSFunctionApply(fun, args) => + /* Protect the fun so that if it is, e.g., + * path.f + * we emit + * (0, path.f)(args...) + * instead of + * path.f(args...) + * If we emit the latter, then `this` will be bound to `path` in + * `f`, which is sometimes extremely harmful (e.g., for builtin + * methods of `window`). + */ + val transformedFun = transformExpr(fun) + val protectedFun = transformedFun match { + case _:js.DotSelect | _:js.BracketSelect => + js.Block(js.IntLiteral(0), transformedFun) + case _ => + transformedFun + } + js.Apply(protectedFun, args map transformExpr) + + case JSDotMethodApply(receiver, method, args) => + js.Apply(js.DotSelect(transformExpr(receiver), method), + args map transformExpr) + + case JSBracketMethodApply(receiver, method, args) => + js.Apply(js.BracketSelect(transformExpr(receiver), + transformExpr(method)), args map transformExpr) + + case JSUnaryOp(op, lhs) => + js.UnaryOp(op, transformExpr(lhs)) + + case JSBinaryOp(op, lhs, rhs) => + js.BinaryOp(op, transformExpr(lhs), transformExpr(rhs)) + + case JSArrayConstr(items) => + js.ArrayConstr(items map transformExpr) + + case JSObjectConstr(fields) => + js.ObjectConstr(fields map { + case (name: Ident, value) => + (transformIdent(name), transformExpr(value)) + case (StringLiteral(name), value) => + (js.StringLiteral(name), transformExpr(value)) + }) + + case JSEnvInfo() => + envField("env") + + // Literals + + case Undefined() => js.Undefined() + case Null() => js.Null() + case BooleanLiteral(value) => js.BooleanLiteral(value) + case IntLiteral(value) => js.IntLiteral(value) + case FloatLiteral(value) => js.DoubleLiteral(value.toDouble) + case DoubleLiteral(value) => js.DoubleLiteral(value) + case StringLiteral(value) => js.StringLiteral(value) + + case LongLiteral(0L) => + genLongModuleApply(LongImpl.Zero) + case LongLiteral(value) => + val (l, m, h) = LongImpl.extractParts(value) + genNewLong(LongImpl.initFromParts, + js.IntLiteral(l), js.IntLiteral(m), js.IntLiteral(h)) + + case ClassOf(cls) => + js.Apply(js.DotSelect(genClassDataOf(cls), Ident("getClassOf")), Nil) + + // Atomic expressions + + case VarRef(name, mutable) => + js.VarRef(name, mutable) + + case This() => + js.This() + + case Closure(captureParams, params, body, captureValues) => + val transformedBody = { + val withReturn = Return(body, None) + transformStat(withReturn) match { + case js.Block(stats :+ js.Return(js.Undefined())) => js.Block(stats) + case other => other + } + } + + val innerFunction = + js.Function(params.map(transformParamDef), transformedBody) + + if (captureParams.isEmpty) { + innerFunction + } else { + js.Apply( + js.Function(captureParams.map(transformParamDef), { + js.Return(innerFunction) + }), + captureValues.map(transformExpr)) + } + + // Invalid trees + + case _ => + sys.error("Invalid tree in JSDesugar.transformExpr() "+ + s"of class ${tree.getClass}") + } + } + + def isMaybeHijackedClass(tpe: Type): Boolean = tpe match { + case ClassType(cls) => + Definitions.HijackedClasses.contains(cls) || + Definitions.AncestorsOfHijackedClasses.contains(cls) + case AnyType | UndefType | BooleanType | IntType | LongType | + FloatType | DoubleType | StringType => + true + case _ => + false + } + + val hijackedClassMethodToHelperName: Map[String, String] = Map( + "toString__T" -> "objectToString", + "clone__O" -> "objectClone", + "finalize__V" -> "objectFinalize", + "notify__V" -> "objectNotify", + "notifyAll__V" -> "objectNotifyAll", + "equals__O__Z" -> "objectEquals", + "hashCode__I" -> "objectHashCode", + + "length__I" -> "charSequenceLength", + "charAt__I__C" -> "charSequenceCharAt", + "subSequence__I__I__jl_CharSequence" -> "charSequenceSubSequence", + + "compareTo__O__I" -> "comparableCompareTo", + "compareTo__jl_Boolean__I" -> "comparableCompareTo", + "compareTo__jl_Byte__I" -> "comparableCompareTo", + "compareTo__jl_Short__I" -> "comparableCompareTo", + "compareTo__jl_Integer__I" -> "comparableCompareTo", + "compareTo__jl_Long__I" -> "comparableCompareTo", + "compareTo__jl_Float__I" -> "comparableCompareTo", + "compareTo__jl_Double__I" -> "comparableCompareTo", + "compareTo__jl_String__I" -> "comparableCompareTo", + + "booleanValue__Z" -> "booleanBooleanValue", + + "byteValue__B" -> "numberByteValue", + "shortValue__S" -> "numberShortValue", + "intValue__I" -> "numberIntValue", + "longValue__J" -> "numberLongValue", + "floatValue__F" -> "numberFloatValue", + "doubleValue__D" -> "numberDoubleValue", + + "isNaN__Z" -> "isNaN", + "isInfinite__Z" -> "isInfinite" + ) + + def genClassDataOf(cls: ReferenceType)(implicit pos: Position): js.Tree = { + cls match { + case ClassType(className) => + encodeClassField("d", className) + case ArrayType(base, dims) => + (1 to dims).foldLeft(encodeClassField("d", base)) { (prev, _) => + js.Apply(js.DotSelect(prev, js.Ident("getArrayOf")), Nil) + } + } + } + + private def genFround(arg: js.Tree)(implicit pos: Position): js.Tree = { + genCallHelper("fround", arg) + } + + private def genNewLong(ctor: String, args: js.Tree*)( + implicit pos: Position): js.Tree = { + import TreeDSL._ + js.Apply( + js.New(encodeClassVar(LongImpl.RuntimeLongClass), Nil) DOT ctor, + args.toList) + } + + private def genLongMethodApply(receiver: js.Tree, methodName: String, + args: js.Tree*)(implicit pos: Position): js.Tree = { + import TreeDSL._ + js.Apply(receiver DOT methodName, args.toList) + } + + private def genLongModuleApply(methodName: String, args: js.Tree*)( + implicit pos: Position): js.Tree = { + import TreeDSL._ + js.Apply( + genLoadModule(LongImpl.RuntimeLongModuleClass) DOT methodName, + args.toList) + } + + private def genLoadModule(moduleClass: String)( + implicit pos: Position): js.Tree = { + import TreeDSL._ + assert(moduleClass.endsWith("$"), + s"Trying to load module for non-module class $moduleClass") + val moduleName = moduleClass.dropRight(1) + js.Apply(envField("m") DOT moduleName, Nil) + } + + } + + // Helpers + + private[javascript] def genIsInstanceOf(expr: js.Tree, cls: ReferenceType)( + implicit pos: Position): js.Tree = + genIsAsInstanceOf(expr, cls, test = true) + + private def genAsInstanceOf(expr: js.Tree, cls: ReferenceType)( + implicit pos: Position): js.Tree = + genIsAsInstanceOf(expr, cls, test = false) + + private def genIsAsInstanceOf(expr: js.Tree, cls: ReferenceType, test: Boolean)( + implicit pos: Position): js.Tree = { + import Definitions._ + import TreeDSL._ + + cls match { + case ClassType(className0) => + val className = + if (className0 == BoxedLongClass) LongImpl.RuntimeLongClass + else className0 + + if (HijackedBoxedClasses.contains(className)) { + if (test) { + className match { + case BoxedUnitClass => expr === js.Undefined() + case BoxedBooleanClass => typeof(expr) === "boolean" + case BoxedByteClass => genCallHelper("isByte", expr) + case BoxedShortClass => genCallHelper("isShort", expr) + case BoxedIntegerClass => genCallHelper("isInt", expr) + case BoxedFloatClass => genCallHelper("isFloat", expr) + case BoxedDoubleClass => typeof(expr) === "number" + } + } else { + className match { + case BoxedUnitClass => genCallHelper("asUnit", expr) + case BoxedBooleanClass => genCallHelper("asBoolean", expr) + case BoxedByteClass => genCallHelper("asByte", expr) + case BoxedShortClass => genCallHelper("asShort", expr) + case BoxedIntegerClass => genCallHelper("asInt", expr) + case BoxedFloatClass => genCallHelper("asFloat", expr) + case BoxedDoubleClass => genCallHelper("asDouble", expr) + } + } + } else { + js.Apply( + envField(if (test) "is" else "as") DOT js.Ident(className), + List(expr)) + } + + case ArrayType(base, depth) => + js.Apply( + envField(if (test) "isArrayOf" else "asArrayOf") DOT js.Ident(base), + List(expr, js.IntLiteral(depth))) + } + } + + private[javascript] def genCallHelper(helperName: String, args: js.Tree*)( + implicit pos: Position): js.Tree = + js.Apply(envField(helperName), args.toList) + + private[javascript] def encodeClassVar(className: String)( + implicit pos: Position): js.Tree = + encodeClassField("c", className) + + private[javascript] def encodeClassField(field: String, className: String)( + implicit pos: Position): js.Tree = + js.DotSelect(envField(field), js.Ident(className)) + + private[javascript] def envField(field: String)(implicit pos: Position): js.Tree = + js.DotSelect(js.VarRef(js.Ident(ScalaJSEnvironmentName), false), + js.Ident(field)) + + private[javascript] implicit class MyTreeOps(val self: js.Tree) { + def prototype(implicit pos: Position): js.Tree = + js.DotSelect(self, js.Ident("prototype")) + } +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/javascript/LongImpl.scala b/tools/shared/src/main/scala/scala/scalajs/tools/javascript/LongImpl.scala new file mode 100644 index 0000000..70b81a3 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/javascript/LongImpl.scala @@ -0,0 +1,116 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.javascript + +object LongImpl { + final val RuntimeLongClass = "sjsr_RuntimeLong" + final val RuntimeLongModuleClass = "sjsr_RuntimeLong$" + + private final val SigUnary = "__sjsr_RuntimeLong" + private final val SigBinary = "__sjsr_RuntimeLong__sjsr_RuntimeLong" + private final val SigShift = "__I__sjsr_RuntimeLong" + private final val SigCompare = "__sjsr_RuntimeLong__Z" + + final val UNARY_- = "unary$und$minus" + SigUnary + final val UNARY_~ = "unary$und$tilde" + SigUnary + + final val + = "$$plus" + SigBinary + final val - = "$$minus" + SigBinary + final val * = "$$times" + SigBinary + final val / = "$$div" + SigBinary + final val % = "$$percent" + SigBinary + + final val | = "$$bar" + SigBinary + final val & = "$$amp" + SigBinary + final val ^ = "$$up" + SigBinary + + final val << = "$$less$less" + SigShift + final val >>> = "$$greater$greater$greater" + SigShift + final val >> = "$$greater$greater" + SigShift + + final val === = "equals" + SigCompare + final val !== = "notEquals" + SigCompare + final val < = "$$less" + SigCompare + final val <= = "$$less$eq" + SigCompare + final val > = "$$greater" + SigCompare + final val >= = "$$greater$eq" + SigCompare + + final val toInt = "toInt" + "__I" + final val toDouble = "toDouble" + "__D" + + final val byteValue = "byteValue__B" + final val shortValue = "shortValue__S" + final val intValue = "intValue__I" + final val longValue = "longValue__J" + final val floatValue = "floatValue__F" + final val doubleValue = "doubleValue__D" + + final val equals_ = "equals__O__Z" + final val hashCode_ = "hashCode__I" + final val compareTo = "compareTo__jl_Long__I" + final val compareToO = "compareTo__O__I" + + private val OperatorMethods = Set( + UNARY_-, UNARY_~, this.+, this.-, *, /, %, |, &, ^, <<, >>>, >>, + ===, !==, <, <=, >, >=, toInt, toDouble) + + private val BoxedLongMethods = Set( + byteValue, shortValue, intValue, longValue, floatValue, doubleValue, + equals_, hashCode_, compareTo, compareToO) + + val AllMethods = OperatorMethods ++ BoxedLongMethods + + // Methods used for intrinsics + + final val bitCount = "bitCount__I" + final val signum = "signum__sjsr_RuntimeLong" + final val numberOfLeadingZeros = "numberOfLeadingZeros__I" + final val numberOfTrailingZeros = "numberOfTrailingZeros__I" + final val toBinaryString = "toBinaryString__T" + final val toHexString = "toHexString__T" + final val toOctalString = "toOctalString__T" + + val AllIntrinsicMethods = Set( + bitCount, signum, numberOfLeadingZeros, numberOfTrailingZeros, + toBinaryString, toHexString, toOctalString) + + // Constructors + + final val initFromParts = "init___I__I__I" + final val initFromInt = "init___I" + + val AllConstructors = Set( + initFromParts, initFromInt) + + // Methods on the companion + + final val fromDouble = "fromDouble__D__sjsr_RuntimeLong" + + final val Zero = "Zero__sjsr_RuntimeLong" + + val AllModuleMethods = Set( + fromDouble, Zero) + + // Boldly copied from library/scala.scalajs.runtime.RuntimeLong + + /** Number of relevant bits in l and m each. */ + private final val BITS = 22 + /** Number of relevant bits in l and m together. */ + private final val BITS01 = 2 * BITS + /** Number of relevant bits in h. */ + private final val BITS2 = 64 - BITS01 + /** Bitmask for l and m. */ + private final val MASK = (1 << BITS) - 1 + /** Bitmask for h. */ + private final val MASK_2 = (1 << BITS2) - 1 + + def extractParts(value: Long): (Int, Int, Int) = + (value.toInt & MASK, (value >> BITS).toInt & MASK, (value >> BITS01).toInt & MASK_2) +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/javascript/Printers.scala b/tools/shared/src/main/scala/scala/scalajs/tools/javascript/Printers.scala new file mode 100644 index 0000000..264c548 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/javascript/Printers.scala @@ -0,0 +1,420 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.javascript + +import scala.annotation.switch + +import scala.util.control.Breaks + +import java.io.Writer +import java.net.URI + +import scala.scalajs.ir +import ir.Position +import ir.Position.NoPosition +import ir.Printers.IndentationManager +import ir.Utils.escapeJS + +import Trees._ + +import scala.scalajs.tools.sourcemap.SourceMapWriter + +object Printers { + + class JSTreePrinter(protected val out: Writer) extends IndentationManager { + def printTopLevelTree(tree: Tree) { + tree match { + case Skip() => + // do not print anything + case Block(stats) => + for (stat <- stats) + printTopLevelTree(stat) + case _ => + printStat(tree) + if (shouldPrintSepAfterTree(tree)) + print(";") + println() + } + } + + protected def shouldPrintSepAfterTree(tree: Tree): Boolean = + !tree.isInstanceOf[DocComment] + + protected def printBlock(tree: Tree): Unit = { + val trees = tree match { + case Block(trees) => trees + case _ => List(tree) + } + print("{"); indent(); println() + printSeq(trees) { x => + printStat(x) + } { x => + if (shouldPrintSepAfterTree(x)) + print(";") + println() + } + undent(); println(); print("}") + } + + protected def printSig(args: List[ParamDef]): Unit = { + printRow(args, "(", ", ", ")") + print(" ") + } + + protected def printArgs(args: List[Tree]): Unit = { + printRow(args, "(", ", ", ")") + } + + def printStat(tree: Tree): Unit = + printTree(tree, isStat = true) + + def printTree(tree: Tree, isStat: Boolean): Unit = { + tree match { + case EmptyTree => + print("<empty>") + + // Comments + + case DocComment(text) => + val lines = text.split("\n").toList + if (lines.tail.isEmpty) { + print("/** ", lines.head, " */") + } else { + print("/** ", lines.head); println() + for (line <- lines.tail) { + print(" * ", line); println() + } + print(" */") + } + + // Definitions + + case VarDef(ident, mutable, rhs) => + print("var ", ident) + if (rhs != EmptyTree) + print(" = ", rhs) + + case ParamDef(ident, mutable) => + print(ident) + + // Control flow constructs + + case Skip() => + print("/*<skip>*/") + + case tree @ Block(trees) => + if (isStat) + printBlock(tree) + else + printRow(trees, "(", ", ", ")") + + case Labeled(label, body) => + print(label, ": ") + printBlock(body) + + case Assign(lhs, rhs) => + print(lhs, " = ", rhs) + + case Return(expr) => + print("return ", expr) + + case If(cond, thenp, elsep) => + if (isStat) { + print("if (", cond, ") ") + printBlock(thenp) + elsep match { + case Skip() => () + case If(_, _, _) => + print(" else ") + printTree(elsep, isStat) + case _ => + print(" else ") + printBlock(elsep) + } + } else { + print("(", cond, " ? ", thenp, " : ", elsep, ")") + } + + case While(cond, body, label) => + if (label.isDefined) + print(label.get, ": ") + print("while (", cond, ") ") + printBlock(body) + + case DoWhile(body, cond, label) => + if (label.isDefined) + print(label.get, ": ") + print("do ") + printBlock(body) + print(" while (", cond, ")") + + case Try(block, errVar, handler, finalizer) => + print("try ") + printBlock(block) + if (handler != EmptyTree) { + print(" catch (", errVar, ") ") + printBlock(handler) + } + if (finalizer != EmptyTree) { + print(" finally ") + printBlock(finalizer) + } + + case Throw(expr) => + print("throw ", expr) + + case Break(label) => + if (label.isEmpty) print("break") + else print("break ", label.get) + + case Continue(label) => + if (label.isEmpty) print("continue") + else print("continue ", label.get) + + case Switch(selector, cases, default) => + print("switch (", selector, ") ") + print("{"); indent + for ((value, body) <- cases) { + println() + print("case ", value, ":"); indent; println() + printStat(body) + print(";") + undent + } + if (default != EmptyTree) { + println() + print("default:"); indent; println() + printStat(default) + print(";") + undent + } + undent; println(); print("}") + + case Debugger() => + print("debugger") + + // Expressions + + case New(ctor, args) => + def containsOnlySelectsFromAtom(tree: Tree): Boolean = tree match { + case DotSelect(qual, _) => containsOnlySelectsFromAtom(qual) + case BracketSelect(qual, _) => containsOnlySelectsFromAtom(qual) + case VarRef(_, _) => true + case This() => true + case _ => false // in particular, Apply + } + if (containsOnlySelectsFromAtom(ctor)) + print("new ", ctor) + else + print("new (", ctor, ")") + printArgs(args) + + case DotSelect(qualifier, item) => + print(qualifier, ".", item) + + case BracketSelect(qualifier, item) => + print(qualifier, "[", item, "]") + + case Apply(fun, args) => + print(fun) + printArgs(args) + + case Delete(prop) => + print("delete ", prop) + + case UnaryOp("typeof", lhs) => + print("typeof(", lhs, ")") + + case UnaryOp(op, lhs) => + print("(", op, lhs, ")") + + case BinaryOp(op, lhs, rhs) => + print("(", lhs, " ", op, " ", rhs, ")") + + case ArrayConstr(items) => + printRow(items, "[", ", ", "]") + + case ObjectConstr(Nil) => + print("{}") + + case ObjectConstr(fields) => + print("{"); indent; println() + printSeq(fields) { + case (name, value) => print(name, ": ", value) + } { _ => + print(",") + println() + } + undent; println(); print("}") + + // Literals + + case Undefined() => + print("(void 0)") + + case Null() => + print("null") + + case BooleanLiteral(value) => + print(if (value) "true" else "false") + + case IntLiteral(value) => + if (value >= 0) + print(value) + else + print("(", value, ")") + + case DoubleLiteral(value) => + if (value == 0 && 1 / value < 0) + print("(-0)") + else if (value >= 0) + print(value) + else + print("(", value, ")") + + case StringLiteral(value) => + print("\"", escapeJS(value), "\"") + + // Atomic expressions + + case VarRef(ident, _) => + print(ident) + + case This() => + print("this") + + case Function(args, body) => + print("(function") + printSig(args) + printBlock(body) + print(")") + + case _ => + print(s"<error, elem of class ${tree.getClass()}>") + } + } + + protected def printIdent(ident: Ident): Unit = + printString(escapeJS(ident.name)) + + def printOne(arg: Any): Unit = arg match { + case tree: Tree => + printTree(tree, isStat = false) + case ident: Ident => + printIdent(ident) + case arg => + printString(if (arg == null) "null" else arg.toString) + } + + protected def printString(s: String): Unit = { + out.write(s) + } + + // Make it public + override def println(): Unit = super.println() + + def complete(): Unit = () + } + + class JSTreePrinterWithSourceMap(_out: Writer, + sourceMap: SourceMapWriter) extends JSTreePrinter(_out) { + + private var column = 0 + + override def printTree(tree: Tree, isStat: Boolean): Unit = { + val pos = tree.pos + if (pos.isDefined) + sourceMap.startNode(column, pos) + + super.printTree(tree, isStat) + + if (pos.isDefined) + sourceMap.endNode(column) + } + + override protected def printIdent(ident: Ident): Unit = { + if (ident.pos.isDefined) + sourceMap.startNode(column, ident.pos, ident.originalName) + super.printIdent(ident) + if (ident.pos.isDefined) + sourceMap.endNode(column) + } + + override def println(): Unit = { + super.println() + sourceMap.nextLine() + column = this.indentMargin + } + + override protected def printString(s: String): Unit = { + // assume no EOL char in s, and assume s only has ASCII characters + super.printString(s) + column += s.length() + } + + override def complete(): Unit = { + sourceMap.complete() + super.complete() + } + } + + /** Prints a tree to find original locations based on line numbers. + * @param untilLine last 0-based line the positions should be recorded for + */ + class ReverseSourceMapPrinter(untilLine: Int) + extends JSTreePrinter(ReverseSourceMapPrinter.NullWriter) { + + private val positions = Array.fill(untilLine+1)(NoPosition) + private var curLine = 0 + + private val doneBreak = new Breaks + + def apply(x: Int): Position = positions(x) + + def reverseSourceMap(tree: Tree): Unit = doneBreak.breakable { + printTopLevelTree(tree) + } + + override def printTree(tree: Tree, isStat: Boolean): Unit = { + if (positions(curLine).isEmpty) + positions(curLine) = tree.pos + + super.printTree(tree, isStat) + } + + override protected def printIdent(ident: Ident): Unit = { + if (positions(curLine).isEmpty) + positions(curLine) = ident.pos + + super.printIdent(ident) + } + + override def println(): Unit = { + super.println() + curLine += 1 + if (curLine > untilLine) + doneBreak.break() + } + + override protected def printString(s: String): Unit = { + // assume no EOL char in s, and assume s only has ASCII characters + // therefore, we fully ignore the string + } + } + + object ReverseSourceMapPrinter { + private object NullWriter extends Writer { + def close(): Unit = () + def flush(): Unit = () + def write(buf: Array[Char], off: Int, len: Int): Unit = () + } + } + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/javascript/ScalaJSClassEmitter.scala b/tools/shared/src/main/scala/scala/scalajs/tools/javascript/ScalaJSClassEmitter.scala new file mode 100644 index 0000000..b249f88 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/javascript/ScalaJSClassEmitter.scala @@ -0,0 +1,569 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.javascript + +import scala.scalajs.ir._ +import Position._ +import Transformers._ +import scala.scalajs.ir.Trees._ +import Types._ + +import scala.scalajs.tools.sem._ +import CheckedBehavior.Unchecked + +import scala.scalajs.tools.javascript.{Trees => js} + +/** Defines methods to emit Scala.js classes to JavaScript code. + * The results are completely desugared. + */ +final class ScalaJSClassEmitter(semantics: Semantics) { + + import JSDesugaring._ + + /** Desugar a Scala.js class into ECMAScript 5 constructs */ + def genClassDef(tree: ClassDef): js.Tree = { + implicit val pos = tree.pos + val kind = tree.kind + + var reverseParts: List[js.Tree] = Nil + + if (kind == ClassKind.TraitImpl) { + reverseParts ::= genTraitImpl(tree) + } else { + if (kind.isClass) + reverseParts ::= genClass(tree) + if (kind.isClass || kind == ClassKind.Interface || + tree.name.name == Definitions.StringClass) + reverseParts ::= genInstanceTests(tree) + reverseParts ::= genArrayInstanceTests(tree) + reverseParts ::= genTypeData(tree) + if (kind.isClass) + reverseParts ::= genSetTypeData(tree) + if (kind == ClassKind.ModuleClass) + reverseParts ::= genModuleAccessor(tree) + if (kind.isClass) + reverseParts ::= genClassExports(tree) + } + + js.Block(reverseParts.reverse) + } + + def genClass(tree: ClassDef): js.Tree = { + val className = tree.name.name + val typeFunctionDef = genConstructor(tree) + val memberDefs = tree.defs collect { + case m: MethodDef => + genMethod(className, m) + case p: PropertyDef => + genProperty(className, p) + } + + js.Block(typeFunctionDef :: memberDefs)(tree.pos) + } + + /** Generates the JS constructor for a class. */ + def genConstructor(tree: ClassDef): js.Tree = { + assert(tree.kind.isClass) + + val classIdent = tree.name + val className = classIdent.name + val tpe = ClassType(className) + + assert(tree.parent.isDefined || className == Definitions.ObjectClass, + "Class $className is missing a parent class") + + val ctorFun = { + val superCtorCall = tree.parent.fold[js.Tree] { + js.Skip()(tree.pos) + } { parentIdent => + implicit val pos = tree.pos + js.Apply( + js.DotSelect(encodeClassVar(parentIdent.name), js.Ident("call")), + List(js.This())) + } + val fieldDefs = for { + field @ VarDef(name, vtpe, mutable, rhs) <- tree.defs + } yield { + implicit val pos = field.pos + desugarJavaScript( + Assign(Select(This()(tpe), name, mutable)(vtpe), rhs), + semantics) + } + js.Function(Nil, + js.Block(superCtorCall :: fieldDefs)(tree.pos))(tree.pos) + } + + { + implicit val pos = tree.pos + val typeVar = encodeClassVar(className) + val docComment = js.DocComment("@constructor") + val ctorDef = js.Assign(typeVar, ctorFun) + + val chainProto = tree.parent.fold[js.Tree] { + js.Skip() + } { parentIdent => + js.Block( + js.Assign(typeVar.prototype, + js.New(js.DotSelect(envField("h"), parentIdent), Nil)), + genAddToPrototype(className, js.Ident("constructor"), typeVar) + ) + } + + val inheritableCtorDef = { + val inheritableCtorVar = + js.DotSelect(envField("h"), classIdent) + js.Block( + js.DocComment("@constructor"), + js.Assign(inheritableCtorVar, js.Function(Nil, js.Skip())), + js.Assign(inheritableCtorVar.prototype, typeVar.prototype) + ) + } + + js.Block(docComment, ctorDef, chainProto, inheritableCtorDef) + } + } + + /** Generates a method. */ + def genMethod(className: String, method: MethodDef): js.Tree = { + implicit val pos = method.pos + val methodFun = js.Function(method.args.map(transformParamDef), + desugarBody(method.body, method.resultType == NoType)) + genAddToPrototype(className, method.name, methodFun) + } + + /** Generates a property. */ + def genProperty(className: String, property: PropertyDef): js.Tree = { + implicit val pos = property.pos + val classType = ClassType(className) + + // defineProperty method + val defProp = + js.BracketSelect(js.VarRef(js.Ident("Object"), false), + js.StringLiteral("defineProperty")) + + // class prototype + val proto = encodeClassVar(className).prototype + + // property name + val name = property.name match { + case StringLiteral(value) => + js.StringLiteral(value) + case id: Ident => + // We need to work around the closure compiler. Call propertyName to + // get a string representation of the optimized name + genCallHelper("propertyName", + js.ObjectConstr(transformIdent(id) -> js.IntLiteral(0) :: Nil)) + } + + // Options passed to the defineProperty method + val descriptor = js.ObjectConstr { + // Basic config + val base = + js.StringLiteral("enumerable") -> js.BooleanLiteral(true) :: Nil + + // Optionally add getter + val wget = + if (property.getterBody == EmptyTree) base + else js.StringLiteral("get") -> + js.Function(Nil, desugarBody(property.getterBody, isStat = false)) :: base + + // Optionally add setter + if (property.setterBody == EmptyTree) wget + else js.StringLiteral("set") -> + js.Function(transformParamDef(property.setterArg) :: Nil, + desugarBody(property.setterBody, isStat = true)) :: wget + } + + js.Apply(defProp, proto :: name :: descriptor :: Nil) + } + + /** Generate `classVar.prototype.name = value` */ + def genAddToPrototype(className: String, name: js.PropertyName, + value: js.Tree)(implicit pos: Position): js.Tree = { + val proto = encodeClassVar(className).prototype + val select = name match { + case name: js.Ident => js.DotSelect(proto, name) + case name: js.StringLiteral => js.BracketSelect(proto, name) + } + js.Assign(select, value) + } + + /** Generate `classVar.prototype.name = value` */ + def genAddToPrototype(className: String, name: PropertyName, + value: js.Tree)(implicit pos: Position): js.Tree = { + val newName = name match { + case ident: Ident => transformIdent(ident) + case StringLiteral(value) => js.StringLiteral(value) + } + genAddToPrototype(className, newName, value) + } + + def genInstanceTests(tree: ClassDef): js.Tree = { + import Definitions._ + import TreeDSL._ + + implicit val pos = tree.pos + + val classIdent = transformIdent(tree.name) + val className = classIdent.name + val displayName = decodeClassName(className) + + val isAncestorOfString = + AncestorsOfStringClass.contains(className) + val isAncestorOfHijackedNumberClass = + AncestorsOfHijackedNumberClasses.contains(className) + val isAncestorOfBoxedBooleanClass = + AncestorsOfBoxedBooleanClass.contains(className) + + val objParam = js.ParamDef(Ident("obj"), mutable = false) + val obj = objParam.ref + + val createIsStat = { + envField("is") DOT classIdent := + js.Function(List(objParam), js.Return(className match { + case Definitions.ObjectClass => + js.BinaryOp("!==", obj, js.Null()) + + case Definitions.StringClass => + js.UnaryOp("typeof", obj) === js.StringLiteral("string") + + case _ => + var test = (obj && (obj DOT "$classData") && + (obj DOT "$classData" DOT "ancestors" DOT classIdent)) + + if (isAncestorOfString) + test = test || ( + js.UnaryOp("typeof", obj) === js.StringLiteral("string")) + if (isAncestorOfHijackedNumberClass) + test = test || ( + js.UnaryOp("typeof", obj) === js.StringLiteral("number")) + if (isAncestorOfBoxedBooleanClass) + test = test || ( + js.UnaryOp("typeof", obj) === js.StringLiteral("boolean")) + + !(!test) + })) + } + + val createAsStat = if (semantics.asInstanceOfs == Unchecked) { + js.Skip() + } else { + envField("as") DOT classIdent := + js.Function(List(objParam), js.Return(className match { + case Definitions.ObjectClass => + obj + + case _ => + js.If(js.Apply(envField("is") DOT classIdent, List(obj)) || + (obj === js.Null()), { + obj + }, { + genCallHelper("throwClassCastException", + obj, js.StringLiteral(displayName)) + }) + })) + } + + js.Block(createIsStat, createAsStat) + } + + def genArrayInstanceTests(tree: ClassDef): js.Tree = { + import Definitions._ + import TreeDSL._ + + implicit val pos = tree.pos + + val classIdent = transformIdent(tree.name) + val className = classIdent.name + val displayName = decodeClassName(className) + + val objParam = js.ParamDef(Ident("obj"), mutable = false) + val obj = objParam.ref + + val depthParam = js.ParamDef(Ident("depth"), mutable = false) + val depth = depthParam.ref + + val createIsArrayOfStat = { + envField("isArrayOf") DOT classIdent := + js.Function(List(objParam, depthParam), className match { + case Definitions.ObjectClass => + val dataVarDef = js.VarDef(Ident("data"), false, { + obj && (obj DOT "$classData") + }) + val data = dataVarDef.ref + js.Block( + dataVarDef, + js.If(!data, { + js.Return(js.BooleanLiteral(false)) + }, { + val arrayDepthVarDef = js.VarDef(Ident("arrayDepth"), false, { + (data DOT "arrayDepth") || js.IntLiteral(0) + }) + val arrayDepth = arrayDepthVarDef.ref + js.Block( + arrayDepthVarDef, + js.Return { + // Array[A] </: Array[Array[A]] + !js.BinaryOp("<", arrayDepth, depth) && ( + // Array[Array[A]] <: Array[Object] + js.BinaryOp(">", arrayDepth, depth) || + // Array[Int] </: Array[Object] + !js.BracketSelect(data DOT "arrayBase", js.StringLiteral("isPrimitive")) + ) + }) + })) + + case _ => + js.Return(!(!(obj && (obj DOT "$classData") && + ((obj DOT "$classData" DOT "arrayDepth") === depth) && + (obj DOT "$classData" DOT "arrayBase" DOT "ancestors" DOT classIdent)))) + }) + } + + val createAsArrayOfStat = if (semantics.asInstanceOfs == Unchecked) { + js.Skip() + } else { + envField("asArrayOf") DOT classIdent := + js.Function(List(objParam, depthParam), js.Return { + js.If(js.Apply(envField("isArrayOf") DOT classIdent, List(obj, depth)) || + (obj === js.Null()), { + obj + }, { + genCallHelper("throwArrayCastException", + obj, js.StringLiteral("L"+displayName+";"), depth) + }) + }) + } + + js.Block(createIsArrayOfStat, createAsArrayOfStat) + } + + def genTypeData(tree: ClassDef): js.Tree = { + import Definitions._ + import TreeDSL._ + + implicit val pos = tree.pos + + val classIdent = transformIdent(tree.name) + val className = classIdent.name + val kind = tree.kind + assert(kind.isType) + + val isObjectClass = + className == ObjectClass + val isHijackedBoxedClass = + HijackedBoxedClasses.contains(className) + val isAncestorOfHijackedClass = + AncestorsOfHijackedClasses.contains(className) + + val parentData = tree.parent.fold[js.Tree] { + if (isObjectClass) js.Null() + else js.Undefined() + } { parent => + envField("d") DOT parent + } + + val ancestorsRecord = js.ObjectConstr( + for (ancestor <- classIdent :: tree.ancestors.map(transformIdent)) + yield (ancestor, js.IntLiteral(1))) + + val typeData = js.New(envField("ClassTypeData"), List( + js.ObjectConstr(List(classIdent -> js.IntLiteral(0))), + js.BooleanLiteral(kind == ClassKind.Interface), + js.StringLiteral(decodeClassName(className)), + parentData, + ancestorsRecord + ) ++ ( + // Optional parameter isInstance + if (isObjectClass) { + /* Object has special ScalaJS.is.O *and* ScalaJS.isArrayOf.O. */ + List( + envField("is") DOT classIdent, + envField("isArrayOf") DOT classIdent) + } else if (isHijackedBoxedClass) { + /* Hijacked boxed classes have a special isInstanceOf test. */ + val xParam = js.ParamDef(Ident("x"), mutable = false) + List(js.Function(List(xParam), js.Return { + genIsInstanceOf(xParam.ref, ClassType(className)) + })) + } else if (isAncestorOfHijackedClass || className == StringClass) { + /* java.lang.String and ancestors of hijacked classes have a normal + * ScalaJS.is.pack_Class test but with a non-standard behavior. */ + List(envField("is") DOT classIdent) + } else { + // For other classes, the isInstance function can be inferred. + Nil + } + )) + + envField("d") DOT classIdent := typeData + } + + def genSetTypeData(tree: ClassDef): js.Tree = { + import TreeDSL._ + + implicit val pos = tree.pos + + assert(tree.kind.isClass) + + encodeClassVar(tree.name.name).prototype DOT "$classData" := + envField("d") DOT tree.name + } + + def genModuleAccessor(tree: ClassDef): js.Tree = { + import TreeDSL._ + + implicit val pos = tree.pos + + val classIdent = transformIdent(tree.name) + val className = classIdent.name + val tpe = ClassType(className) + + require(tree.kind == ClassKind.ModuleClass, + s"genModuleAccessor called with non-module class: $className") + assert(className.endsWith("$")) + + val moduleName = className.dropRight(1) + val moduleIdent = Ident(moduleName) + + val moduleInstanceVar = envField("n") DOT moduleIdent + val accessorVar = envField("m") DOT moduleIdent + + val createModuleInstanceField = { + moduleInstanceVar := js.Undefined() + } + + val createAccessor = { + accessorVar := js.Function(Nil, js.Block( + js.If(!(moduleInstanceVar), { + moduleInstanceVar := + js.Apply(js.New(encodeClassVar(className), Nil) DOT js.Ident("init___"), Nil) + }, js.Skip()), + js.Return(moduleInstanceVar) + )) + } + + js.Block(createModuleInstanceField, createAccessor) + } + + def genClassExports(tree: ClassDef): js.Tree = { + val exports = tree.defs collect { + case e: ConstructorExportDef => + genConstructorExportDef(tree, e) + case e: ModuleExportDef => + genModuleExportDef(tree, e) + } + + js.Block(exports)(tree.pos) + } + + def genConstructorExportDef(cd: ClassDef, tree: ConstructorExportDef): js.Tree = { + import TreeDSL._ + + implicit val pos = tree.pos + val classType = ClassType(cd.name.name) + val ConstructorExportDef(fullName, args, body) = tree + + val baseCtor = envField("c") DOT cd.name + val (createNamespace, expCtorVar) = genCreateNamespaceInExports(fullName) + + js.Block( + createNamespace, + js.DocComment("@constructor"), + expCtorVar := js.Function(args.map(transformParamDef), js.Block( + js.Apply(js.DotSelect(baseCtor, js.Ident("call")), List(js.This())), + desugarBody(body, isStat = true) + )), + expCtorVar DOT "prototype" := baseCtor DOT "prototype" + ) + } + + def genModuleExportDef(cd: ClassDef, tree: ModuleExportDef): js.Tree = { + import TreeDSL._ + + implicit val pos = tree.pos + + val baseAccessor = + envField("m") DOT cd.name.name.dropRight(1) + val (createNamespace, expAccessorVar) = + genCreateNamespaceInExports(tree.fullName) + + js.Block( + createNamespace, + expAccessorVar := baseAccessor + ) + } + + def genTraitImpl(tree: ClassDef): js.Tree = { + val traitImplName = tree.name.name + val defs = tree.defs collect { + case m: MethodDef => + genTraitImplMethod(traitImplName, m) + } + js.Block(defs)(tree.pos) + } + + def genTraitImplMethod(traitImplName: String, tree: MethodDef): js.Tree = { + implicit val pos = tree.pos + val MethodDef(name: Ident, args, resultType, body) = tree + js.Assign( + js.DotSelect(envField("i"), name), + js.Function(args.map(transformParamDef), + desugarBody(body, resultType == NoType))) + } + + /** Generate a dummy parent. Used by ScalaJSOptimizer */ + def genDummyParent(name: String): js.Tree = { + implicit val pos = Position.NoPosition + + js.Block( + js.DocComment("@constructor (dummy parent)")) + js.Assign(js.DotSelect(envField("h"), js.Ident(name)), + js.Function(Nil, js.Skip()) + ) + } + + // Helpers + + /** Desugars a function body of the IR into ES5 JavaScript. */ + private def desugarBody(tree: Tree, isStat: Boolean): js.Tree = { + implicit val pos = tree.pos + val withReturn = + if (isStat) tree + else Return(tree) + desugarJavaScript(withReturn, semantics) match { + case js.Block(stats :+ js.Return(js.Undefined())) => js.Block(stats) + case other => other + } + } + + /** Gen JS code for assigning an rhs to a qualified name in the exports scope. + * For example, given the qualified name "foo.bar.Something", generates: + * + * ScalaJS.e["foo"] = ScalaJS.e["foo"] || {}; + * ScalaJS.e["foo"]["bar"] = ScalaJS.e["foo"]["bar"] || {}; + * + * Returns (statements, ScalaJS.e["foo"]["bar"]["Something"]) + */ + private def genCreateNamespaceInExports(qualName: String)( + implicit pos: Position): (js.Tree, js.Tree) = { + val parts = qualName.split("\\.") + val statements = List.newBuilder[js.Tree] + var namespace = envField("e") + for (i <- 0 until parts.length-1) { + namespace = js.BracketSelect(namespace, js.StringLiteral(parts(i))) + statements += + js.Assign(namespace, js.BinaryOp("||", namespace, js.ObjectConstr(Nil))) + } + val lhs = js.BracketSelect(namespace, js.StringLiteral(parts.last)) + (js.Block(statements.result()), lhs) + } + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/javascript/TreeDSL.scala b/tools/shared/src/main/scala/scala/scalajs/tools/javascript/TreeDSL.scala new file mode 100644 index 0000000..3ac54d8 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/javascript/TreeDSL.scala @@ -0,0 +1,50 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.javascript + +import scala.language.implicitConversions + +import scala.scalajs.ir.Position + +import Trees._ + +private[javascript] object TreeDSL { + implicit class TreeOps(val self: Tree) extends AnyVal { + /** Select a member */ + def DOT(field: Ident)(implicit pos: Position): DotSelect = + DotSelect(self, field) + + /** Select a member */ + def DOT(field: String)(implicit pos: Position): DotSelect = + DotSelect(self, Ident(field)) + + // Some operators that we use + + def ===(that: Tree)(implicit pos: Position): Tree = + BinaryOp("===", self, that) + def ===(that: String)(implicit pos: Position): Tree = + BinaryOp("===", self, StringLiteral(that)) + + def unary_!()(implicit pos: Position): Tree = + UnaryOp("!", self) + def &&(that: Tree)(implicit pos: Position): Tree = + BinaryOp("&&", self, that) + def ||(that: Tree)(implicit pos: Position): Tree = + BinaryOp("||", self, that) + + // Other constructs + + def :=(that: Tree)(implicit pos: Position): Tree = + Assign(self, that) + } + + def typeof(expr: Tree)(implicit pos: Position): Tree = + UnaryOp("typeof", expr) +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/javascript/Trees.scala b/tools/shared/src/main/scala/scala/scalajs/tools/javascript/Trees.scala new file mode 100644 index 0000000..0b86d1b --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/javascript/Trees.scala @@ -0,0 +1,194 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.javascript + +import scala.annotation.switch + +import scala.scalajs.ir +import ir.Position +import ir.Position.NoPosition + +object Trees { + import ir.Trees.requireValidIdent + + /** AST node of JavaScript. */ + abstract sealed class Tree { + val pos: Position + + def show: String = { + val writer = new java.io.StringWriter + val printer = new Printers.JSTreePrinter(writer) + printer.printTree(this, isStat = true) + writer.toString() + } + } + + case object EmptyTree extends Tree { + val pos = NoPosition + } + + // Comments + + case class DocComment(text: String)(implicit val pos: Position) extends Tree + + // Identifiers and properties + + sealed trait PropertyName { + def name: String + def pos: Position + } + + case class Ident(name: String, originalName: Option[String])( + implicit val pos: Position) extends PropertyName { + requireValidIdent(name) + } + + object Ident { + def apply(name: String)(implicit pos: Position): Ident = + new Ident(name, Some(name)) + } + + // Definitions + + case class VarDef(name: Ident, mutable: Boolean, rhs: Tree)(implicit val pos: Position) extends Tree { + def ref(implicit pos: Position): Tree = + VarRef(name, mutable = mutable) + } + + case class ParamDef(name: Ident, mutable: Boolean)(implicit val pos: Position) extends Tree { + def ref(implicit pos: Position): Tree = + VarRef(name, mutable = mutable) + } + + // Control flow constructs + + case class Skip()(implicit val pos: Position) extends Tree + + class Block private (val stats: List[Tree])(implicit val pos: Position) extends Tree { + override def toString(): String = + stats.mkString("Block(", ",", ")") + } + + object Block { + def apply(stats: List[Tree])(implicit pos: Position): Tree = { + val flattenedStats = stats flatMap { + case Skip() => Nil + case Block(subStats) => subStats + case other => other :: Nil + } + flattenedStats match { + case Nil => Skip() + case only :: Nil => only + case _ => new Block(flattenedStats) + } + } + + def apply(stats: Tree*)(implicit pos: Position): Tree = + apply(stats.toList) + + def unapply(block: Block): Some[List[Tree]] = Some(block.stats) + } + + case class Labeled(label: Ident, body: Tree)(implicit val pos: Position) extends Tree + + case class Assign(lhs: Tree, rhs: Tree)(implicit val pos: Position) extends Tree { + require(lhs match { + case _:VarRef | _:DotSelect | _:BracketSelect => true + case _ => false + }, s"Invalid lhs for Assign: $lhs") + } + + case class Return(expr: Tree)(implicit val pos: Position) extends Tree + + case class If(cond: Tree, thenp: Tree, elsep: Tree)(implicit val pos: Position) extends Tree + + case class While(cond: Tree, body: Tree, label: Option[Ident] = None)(implicit val pos: Position) extends Tree + + case class DoWhile(body: Tree, cond: Tree, label: Option[Ident] = None)(implicit val pos: Position) extends Tree + + case class Try(block: Tree, errVar: Ident, handler: Tree, finalizer: Tree)(implicit val pos: Position) extends Tree + + case class Throw(expr: Tree)(implicit val pos: Position) extends Tree + + case class Break(label: Option[Ident] = None)(implicit val pos: Position) extends Tree + + case class Continue(label: Option[Ident] = None)(implicit val pos: Position) extends Tree + + case class Switch(selector: Tree, cases: List[(Tree, Tree)], default: Tree)(implicit val pos: Position) extends Tree + + case class Debugger()(implicit val pos: Position) extends Tree + + // Expressions + + case class New(ctor: Tree, args: List[Tree])(implicit val pos: Position) extends Tree + + case class DotSelect(qualifier: Tree, item: Ident)(implicit val pos: Position) extends Tree + + case class BracketSelect(qualifier: Tree, item: Tree)(implicit val pos: Position) extends Tree + + /** Syntactic apply. + * It is a method call if fun is a dot-select or bracket-select. It is a + * function call otherwise. + */ + case class Apply(fun: Tree, args: List[Tree])(implicit val pos: Position) extends Tree + + case class Delete(prop: Tree)(implicit val pos: Position) extends Tree { + require(prop match { + case _:DotSelect | _:BracketSelect => true + case _ => false + }, s"Invalid prop for Delete: $prop") + } + + /** Unary operation (always preserves pureness). + * + * Operations which do not preserve pureness are not allowed in this tree. + * These are notably ++ and -- + */ + case class UnaryOp(op: String, lhs: Tree)(implicit val pos: Position) extends Tree + + /** Binary operation (always preserves pureness). + * + * Operations which do not preserve pureness are not allowed in this tree. + * These are notably +=, -=, *=, /= and %= + */ + case class BinaryOp(op: String, lhs: Tree, rhs: Tree)(implicit val pos: Position) extends Tree + + case class ArrayConstr(items: List[Tree])(implicit val pos: Position) extends Tree + + case class ObjectConstr(fields: List[(PropertyName, Tree)])(implicit val pos: Position) extends Tree + + // Literals + + /** Marker for literals. Literals are always pure. */ + sealed trait Literal extends Tree + + case class Undefined()(implicit val pos: Position) extends Literal + + case class Null()(implicit val pos: Position) extends Literal + + case class BooleanLiteral(value: Boolean)(implicit val pos: Position) extends Literal + + case class IntLiteral(value: Int)(implicit val pos: Position) extends Literal + + case class DoubleLiteral(value: Double)(implicit val pos: Position) extends Literal + + case class StringLiteral(value: String)( + implicit val pos: Position) extends Literal with PropertyName { + override def name = value + } + + // Atomic expressions + + case class VarRef(ident: Ident, mutable: Boolean)(implicit val pos: Position) extends Tree + + case class This()(implicit val pos: Position) extends Tree + + case class Function(args: List[ParamDef], body: Tree)(implicit val pos: Position) extends Tree +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/Exceptions.scala b/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/Exceptions.scala new file mode 100644 index 0000000..dd7f635 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/Exceptions.scala @@ -0,0 +1,59 @@ +package scala.scalajs.tools.jsdep + +abstract class DependencyException(msg: String) extends Exception(msg) + +class MissingDependencyException( + val originatingLib: FlatJSDependency, + val missingLib: String +) extends DependencyException( + s"The JS dependency ${originatingLib.resourceName} declared " + + s"from ${originatingLib.origin} has an unmet transitive " + + s"dependency $missingLib") + +class CyclicDependencyException( + val participants: List[ResolutionInfo] +) extends DependencyException( + CyclicDependencyException.mkMsg(participants)) + +object CyclicDependencyException { + private def mkMsg(parts: List[ResolutionInfo]) = { + val lookup = parts.map(p => (p.resourceName, p)).toMap + + val msg = new StringBuilder() + msg.append("There is a loop in the following JS dependencies:\n") + + def str(info: ResolutionInfo) = + s"${info.resourceName} from: ${info.origins.mkString(", ")}" + + for (dep <- parts) { + msg.append(s" ${str(dep)} which depends on\n") + for (name <- dep.dependencies) { + val rdep = lookup(name) + msg.append(s" - ${str(rdep)}\n") + } + } + + msg.toString() + } +} + +class ConflictingNameException( + val participants: List[FlatJSDependency] +) extends DependencyException( + ConflictingNameException.mkMsg(participants)) + +object ConflictingNameException { + private def mkMsg(parts: List[FlatJSDependency]) = { + val resName = parts.head.resourceName + + val msg = new StringBuilder() + msg.append(s"Name conflicts in:\n") + + for (p <- parts) { + msg.append(p) + msg.append('\n') + } + + sys.error(msg.toString()) + } +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/FlatJSDependency.scala b/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/FlatJSDependency.scala new file mode 100644 index 0000000..0c55e88 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/FlatJSDependency.scala @@ -0,0 +1,17 @@ +package scala.scalajs.tools.jsdep + +import scala.scalajs.ir.Trees.isValidIdentifier + +/** The same as a [[JSDependency]] but containing the origin from the containing + * JSDependencyManifest. This class is used for filtering of dependencies. + */ +final class FlatJSDependency( + val origin: Origin, + val resourceName: String, + val dependencies: List[String] = Nil, + val commonJSName: Option[String] = None) { + + require(commonJSName.forall(isValidIdentifier), + "commonJSName must be a valid JavaScript identifier") + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/JSDependency.scala b/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/JSDependency.scala new file mode 100644 index 0000000..2e6f8d1 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/JSDependency.scala @@ -0,0 +1,66 @@ +package scala.scalajs.tools.jsdep + +import scala.scalajs.tools.json._ + +import scala.scalajs.ir.Trees.isValidIdentifier + +/** Expresses a dependency on a raw JS library and the JS libraries this library + * itself depends on. + * + * Both the [[resourceName]] and each element of [[dependencies]] is the + * unqualified filename of the library (e.g. "jquery.js"). + * + * @param resourceName Filename of the JavaScript file to include + * (e.g. "jquery.js") + * @param dependencies Filenames of JavaScript files that must be included + * before this JavaScript file. + * @param commonJSName A JavaScript variable name this dependency should be + * required in a commonJS environment (n.b. Node.js). Should only be set if + * the JavaScript library will register its exports. + */ +final class JSDependency( + val resourceName: String, + val dependencies: List[String] = Nil, + val commonJSName: Option[String] = None) { + + require(commonJSName.forall(isValidIdentifier), + "commonJSName must be a valid JavaScript identifier") + + def dependsOn(names: String*): JSDependency = + copy(dependencies = dependencies ++ names) + def commonJSName(name: String): JSDependency = + copy(commonJSName = Some(name)) + def withOrigin(origin: Origin): FlatJSDependency = + new FlatJSDependency(origin, resourceName, dependencies, commonJSName) + + private def copy( + resourceName: String = this.resourceName, + dependencies: List[String] = this.dependencies, + commonJSName: Option[String] = this.commonJSName) = { + new JSDependency(resourceName, dependencies, commonJSName) + } +} + +object JSDependency { + + implicit object JSDepJSONSerializer extends JSONSerializer[JSDependency] { + def serialize(x: JSDependency): JSON = { + new JSONObjBuilder() + .fld("resourceName", x.resourceName) + .opt("dependencies", + if (x.dependencies.nonEmpty) Some(x.dependencies) else None) + .opt("commonJSName", x.commonJSName) + .toJSON + } + } + + implicit object JSDepJSONDeserializer extends JSONDeserializer[JSDependency] { + def deserialize(x: JSON): JSDependency = { + val obj = new JSONObjExtractor(x) + new JSDependency( + obj.fld[String] ("resourceName"), + obj.opt[List[String]]("dependencies").getOrElse(Nil), + obj.opt[String] ("commonJSName")) + } + } +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/JSDependencyManifest.scala b/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/JSDependencyManifest.scala new file mode 100644 index 0000000..24491b4 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/JSDependencyManifest.scala @@ -0,0 +1,130 @@ +package scala.scalajs.tools.jsdep + +import scala.scalajs.tools.json._ +import scala.scalajs.tools.io._ + +import scala.collection.immutable.{Seq, Traversable} + +import java.io.{Reader, Writer} + +/** The information written to a "JS_DEPENDENCIES" manifest file. */ +final class JSDependencyManifest( + val origin: Origin, + val libDeps: List[JSDependency], + val requiresDOM: Boolean, + val compliantSemantics: List[String]) { + def flatten: List[FlatJSDependency] = libDeps.map(_.withOrigin(origin)) +} + +object JSDependencyManifest { + + final val ManifestFileName = "JS_DEPENDENCIES" + + def createIncludeList( + flatDeps: Traversable[FlatJSDependency]): List[ResolutionInfo] = { + val jsDeps = mergeManifests(flatDeps) + + // Verify all dependencies are met + for { + lib <- flatDeps + dep <- lib.dependencies + if !jsDeps.contains(dep) + } throw new MissingDependencyException(lib, dep) + + // Sort according to dependencies and return + + // Very simple O(n²) topological sort for elements assumed to be distinct + // Copied :( from GenJSExports (but different exception) + @scala.annotation.tailrec + def loop(coll: List[ResolutionInfo], + acc: List[ResolutionInfo]): List[ResolutionInfo] = { + + if (coll.isEmpty) acc + else if (coll.tail.isEmpty) coll.head :: acc + else { + val (selected, pending) = coll.partition { x => + coll forall { y => (x eq y) || !y.dependencies.contains(x.resourceName) } + } + + if (selected.nonEmpty) + loop(pending, selected ::: acc) + else + throw new CyclicDependencyException(pending) + } + } + + loop(jsDeps.values.toList, Nil) + } + + /** Merges multiple JSDependencyManifests into a map of map: + * resourceName -> ResolutionInfo + */ + private def mergeManifests(flatDeps: Traversable[FlatJSDependency]) = { + @inline + def hasConflict(x: FlatJSDependency, y: FlatJSDependency) = ( + x.commonJSName.isDefined && + y.commonJSName.isDefined && + (x.resourceName == y.resourceName ^ + x.commonJSName == y.commonJSName) + ) + + val conflicts = flatDeps.filter(x => + flatDeps.exists(y => hasConflict(x,y))) + + if (conflicts.nonEmpty) + throw new ConflictingNameException(conflicts.toList) + + flatDeps.groupBy(_.resourceName).mapValues { sameName => + new ResolutionInfo( + resourceName = sameName.head.resourceName, + dependencies = sameName.flatMap(_.dependencies).toSet, + origins = sameName.map(_.origin).toList, + commonJSName = sameName.flatMap(_.commonJSName).headOption + ) + } + } + + implicit object JSDepManJSONSerializer extends JSONSerializer[JSDependencyManifest] { + @inline def optList[T](x: List[T]): Option[List[T]] = + if (x.nonEmpty) Some(x) else None + + def serialize(x: JSDependencyManifest): JSON = { + new JSONObjBuilder() + .fld("origin", x.origin) + .opt("libDeps", optList(x.libDeps)) + .opt("requiresDOM", if (x.requiresDOM) Some(true) else None) + .opt("compliantSemantics", optList(x.compliantSemantics)) + .toJSON + } + } + + implicit object JSDepManJSONDeserializer extends JSONDeserializer[JSDependencyManifest] { + def deserialize(x: JSON): JSDependencyManifest = { + val obj = new JSONObjExtractor(x) + new JSDependencyManifest( + obj.fld[Origin] ("origin"), + obj.opt[List[JSDependency]]("libDeps").getOrElse(Nil), + obj.opt[Boolean] ("requiresDOM").getOrElse(false), + obj.opt[List[String]] ("compliantSemantics").getOrElse(Nil)) + } + } + + def write(dep: JSDependencyManifest, output: WritableVirtualTextFile): Unit = { + val writer = output.contentWriter + try write(dep, writer) + finally writer.close() + } + + def write(dep: JSDependencyManifest, writer: Writer): Unit = + writeJSON(dep.toJSON, writer) + + def read(file: VirtualTextFile): JSDependencyManifest = { + val reader = file.reader + try read(reader) + finally reader.close() + } + + def read(reader: Reader): JSDependencyManifest = + fromJSON[JSDependencyManifest](readJSON(reader)) + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/Origin.scala b/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/Origin.scala new file mode 100644 index 0000000..a2c6b2d --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/Origin.scala @@ -0,0 +1,28 @@ +package scala.scalajs.tools.jsdep + +import scala.scalajs.tools.json._ + +/** The place a JSDependency originated from */ +final class Origin(val moduleName: String, val configuration: String) { + override def toString(): String = s"$moduleName:$configuration" +} + +object Origin { + implicit object OriginJSONSerializer extends JSONSerializer[Origin] { + def serialize(x: Origin): JSON = { + new JSONObjBuilder() + .fld("moduleName", x.moduleName) + .fld("configuration", x.configuration) + .toJSON + } + } + + implicit object OriginDeserializer extends JSONDeserializer[Origin] { + def deserialize(x: JSON): Origin = { + val obj = new JSONObjExtractor(x) + new Origin( + obj.fld[String]("moduleName"), + obj.fld[String]("configuration")) + } + } +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/ResolutionInfo.scala b/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/ResolutionInfo.scala new file mode 100644 index 0000000..2aa177e --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/ResolutionInfo.scala @@ -0,0 +1,21 @@ +package scala.scalajs.tools.jsdep + +import scala.scalajs.ir.Trees.isValidIdentifier + +/** Information about a resolved JSDependency + * + * @param resourceName Filename of the JavaScript file + * @param dependencies Filenames this dependency depends on + * @param origins Who declared this dependency + * @param commonJSName Variable name in commonJS environments + */ +final class ResolutionInfo( + val resourceName: String, + val dependencies: Set[String], + val origins: List[Origin], + val commonJSName: Option[String]) { + + require(commonJSName.forall(isValidIdentifier), + "commonJSName must be a valid JavaScript identifier") + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/json/AbstractJSONImpl.scala b/tools/shared/src/main/scala/scala/scalajs/tools/json/AbstractJSONImpl.scala new file mode 100644 index 0000000..ad5d79e --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/json/AbstractJSONImpl.scala @@ -0,0 +1,32 @@ +package scala.scalajs.tools.json + +import java.io.{Reader, Writer} + +/** A JSON implementation. Has a representation type and methods to convert + * this type to/from primitives, lists and maps. + * + * Further, it can write/read a value of this type to a string. + */ +private[json] trait AbstractJSONImpl { + + type Repr + + def fromString(x: String): Repr + def fromNumber(x: Number): Repr + def fromBoolean(x: Boolean): Repr + def fromList(x: List[Repr]): Repr + def fromMap(x: Map[String, Repr]): Repr + + def toString(x: Repr): String + def toNumber(x: Repr): Number + def toBoolean(x: Repr): Boolean + def toList(x: Repr): List[Repr] + def toMap(x: Repr): Map[String, Repr] + + def serialize(x: Repr): String + def serialize(x: Repr, writer: Writer): Unit + + def deserialize(str: String): Repr + def deserialize(reader: Reader): Repr + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/json/JSONDeserializer.scala b/tools/shared/src/main/scala/scala/scalajs/tools/json/JSONDeserializer.scala new file mode 100644 index 0000000..e854e9a --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/json/JSONDeserializer.scala @@ -0,0 +1,30 @@ +package scala.scalajs.tools.json + +trait JSONDeserializer[T] { + def deserialize(x: JSON): T +} + +object JSONDeserializer { + + implicit object stringJSON extends JSONDeserializer[String] { + def deserialize(x: JSON): String = Impl.toString(x) + } + + implicit object intJSON extends JSONDeserializer[Int] { + def deserialize(x: JSON): Int = Impl.toNumber(x).intValue() + } + + implicit object booleanJSON extends JSONDeserializer[Boolean] { + def deserialize(x: JSON): Boolean = Impl.toBoolean(x) + } + + implicit def listJSON[T : JSONDeserializer] = new JSONDeserializer[List[T]] { + def deserialize(x: JSON): List[T] = Impl.toList(x).map(fromJSON[T] _) + } + + implicit def mapJSON[V : JSONDeserializer] = new JSONDeserializer[Map[String, V]] { + def deserialize(x: JSON): Map[String, V] = + Impl.toMap(x).mapValues(fromJSON[V] _) + } + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/json/JSONObjBuilder.scala b/tools/shared/src/main/scala/scala/scalajs/tools/json/JSONObjBuilder.scala new file mode 100644 index 0000000..dd98f49 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/json/JSONObjBuilder.scala @@ -0,0 +1,20 @@ +package scala.scalajs.tools.json + +import scala.collection.mutable + +class JSONObjBuilder { + + private val flds = mutable.Map.empty[String, JSON] + + def fld[T : JSONSerializer](name: String, v: T): this.type = { + flds.put(name, v.toJSON) + this + } + + def opt[T : JSONSerializer](name: String, v: Option[T]): this.type = { + v.foreach(v => flds.put(name, v.toJSON)) + this + } + + def toJSON: JSON = Impl.fromMap(flds.toMap) +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/json/JSONObjExtractor.scala b/tools/shared/src/main/scala/scala/scalajs/tools/json/JSONObjExtractor.scala new file mode 100644 index 0000000..e49f7e4 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/json/JSONObjExtractor.scala @@ -0,0 +1,13 @@ +package scala.scalajs.tools.json + +import scala.collection.mutable + +class JSONObjExtractor(rawData: JSON) { + private val data = Impl.toMap(rawData) + + def fld[T : JSONDeserializer](name: String): T = + fromJSON[T](data(name)) + + def opt[T : JSONDeserializer](name: String): Option[T] = + data.get(name).map(fromJSON[T] _) +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/json/JSONSerializer.scala b/tools/shared/src/main/scala/scala/scalajs/tools/json/JSONSerializer.scala new file mode 100644 index 0000000..e26c92a --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/json/JSONSerializer.scala @@ -0,0 +1,32 @@ +package scala.scalajs.tools.json + +trait JSONSerializer[T] { + def serialize(x: T): JSON +} + +object JSONSerializer { + + implicit object stringJSON extends JSONSerializer[String] { + def serialize(x: String): JSON = Impl.fromString(x) + } + + implicit object intJSON extends JSONSerializer[Int] { + def serialize(x: Int): JSON = Impl.fromNumber(x) + } + + implicit object booleanJSON extends JSONSerializer[Boolean] { + def serialize(x: Boolean): JSON = Impl.fromBoolean(x) + } + + implicit def listJSON[T : JSONSerializer] = new JSONSerializer[List[T]] { + def serialize(x: List[T]): JSON = Impl.fromList(x.map(_.toJSON)) + } + + implicit def mapJSON[V : JSONSerializer] = { + new JSONSerializer[Map[String, V]] { + def serialize(x: Map[String, V]): JSON = + Impl.fromMap(x.mapValues(_.toJSON)) + } + } + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/json/package.scala b/tools/shared/src/main/scala/scala/scalajs/tools/json/package.scala new file mode 100644 index 0000000..551893a --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/json/package.scala @@ -0,0 +1,26 @@ +package scala.scalajs.tools + +import java.io.{Reader, Writer} + +/** Some type-class lightweight wrappers around simple-json. + * + * They allow to write [[xyz.toJSON]] to obtain classes that can be + * serialized by simple-json and [[fromJSON[T](xyz)]] to get an + * object back. + */ +package object json { + type JSON = Impl.Repr + + implicit class JSONPimp[T : JSONSerializer](x: T) { + def toJSON: JSON = implicitly[JSONSerializer[T]].serialize(x) + } + + def fromJSON[T](x: JSON)(implicit d: JSONDeserializer[T]): T = + d.deserialize(x) + + def writeJSON(x: JSON, writer: Writer): Unit = Impl.serialize(x, writer) + def jsonToString(x: JSON): String = Impl.serialize(x) + def readJSON(str: String): JSON = Impl.deserialize(str) + def readJSON(reader: Reader): JSON = Impl.deserialize(reader) + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/logging/Level.scala b/tools/shared/src/main/scala/scala/scalajs/tools/logging/Level.scala new file mode 100644 index 0000000..fbbf39d --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/logging/Level.scala @@ -0,0 +1,24 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.logging + +import scala.math.Ordered + +abstract sealed class Level extends Ordered[Level] { x => + protected val order: Int + def compare(y: Level) = x.order - y.order +} + +object Level { + case object Error extends Level { protected val order = 4 } + case object Warn extends Level { protected val order = 3 } + case object Info extends Level { protected val order = 2 } + case object Debug extends Level { protected val order = 1 } +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/logging/Logger.scala b/tools/shared/src/main/scala/scala/scalajs/tools/logging/Logger.scala new file mode 100644 index 0000000..3664f51 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/logging/Logger.scala @@ -0,0 +1,25 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.logging + +/** Abstract logger for our tools. Designed after sbt's Loggers. */ +trait Logger { + def log(level: Level, message: => String): Unit + def success(message: => String): Unit + def trace(t: => Throwable): Unit + + def error(message: => String): Unit = log(Level.Error, message) + def warn(message: => String): Unit = log(Level.Warn, message) + def info(message: => String): Unit = log(Level.Info, message) + def debug(message: => String): Unit = log(Level.Debug, message) + + def time(title: String, nanos: Long): Unit = + debug(s"$title: ${nanos / 1000} us") +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/logging/NullLogger.scala b/tools/shared/src/main/scala/scala/scalajs/tools/logging/NullLogger.scala new file mode 100644 index 0000000..0e36f89 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/logging/NullLogger.scala @@ -0,0 +1,7 @@ +package scala.scalajs.tools.logging + +object NullLogger extends Logger { + def log(level: Level, message: => String): Unit = {} + def success(message: => String): Unit = {} + def trace(t: => Throwable): Unit = {} +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/logging/ScalaConsoleLogger.scala b/tools/shared/src/main/scala/scala/scalajs/tools/logging/ScalaConsoleLogger.scala new file mode 100644 index 0000000..e2c9efc --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/logging/ScalaConsoleLogger.scala @@ -0,0 +1,15 @@ +package scala.scalajs.tools.logging + +class ScalaConsoleLogger(minLevel: Level = Level.Debug) extends Logger { + + def log(level: Level, message: =>String): Unit = if (level >= minLevel) { + if (level == Level.Warn || level == Level.Error) + scala.Console.err.println(message) + else + scala.Console.out.println(message) + } + def success(message: => String): Unit = info(message) + def trace(t: => Throwable): Unit = + // This is error level, so no checking + t.printStackTrace() +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/Analyzer.scala b/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/Analyzer.scala new file mode 100644 index 0000000..9cdd764 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/Analyzer.scala @@ -0,0 +1,587 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.optimizer + +import scala.annotation.tailrec + +import scala.collection.mutable + +import scala.scalajs.ir +import ir.{ClassKind, Definitions, Infos} + +import scala.scalajs.tools.sem._ +import scala.scalajs.tools.javascript.LongImpl +import scala.scalajs.tools.logging._ + +import ScalaJSOptimizer._ + +class Analyzer(logger0: Logger, semantics: Semantics, + allData: Seq[Infos.ClassInfo], globalWarnEnabled: Boolean, + isBeforeOptimizer: Boolean) { + /* Set this to true to debug the DCE analyzer. + * We don't rely on config to disable 'debug' messages because we want + * to use 'debug' for displaying more stack trace info that the user can + * see with the 'last' command. + */ + val DebugAnalyzer = false + + object logger extends Logger { + var indentation: String = "" + + def indent(): Unit = indentation += " " + def undent(): Unit = indentation = indentation.substring(2) + + def log(level: Level, message: => String) = + logger0.log(level, indentation+message) + def success(message: => String) = + logger0.success(indentation+message) + def trace(t: => Throwable) = + logger0.trace(t) + + def indented[A](body: => A): A = { + indent() + try body + finally undent() + } + + def debugIndent[A](message: => String)(body: => A): A = { + if (DebugAnalyzer) { + debug(message) + indented(body) + } else { + body + } + } + + def temporarilyNotIndented[A](body: => A): A = { + val savedIndent = indentation + indentation = "" + try body + finally indentation = savedIndent + } + } + + sealed trait From + case class FromMethod(methodInfo: MethodInfo) extends From + case object FromCore extends From + case object FromExports extends From + case object FromManual extends From + + var allAvailable: Boolean = true + + val classInfos: mutable.Map[String, ClassInfo] = { + val cs = for (classData <- allData) + yield (classData.encodedName, new ClassInfo(classData)) + mutable.Map.empty[String, ClassInfo] ++ cs + } + + def lookupClass(encodedName: String): ClassInfo = { + classInfos.get(encodedName) match { + case Some(info) => info + case None => + val c = new ClassInfo(createMissingClassInfo(encodedName)) + classInfos += encodedName -> c + c.nonExistent = true + c.linkClasses() + c + } + } + + def lookupModule(encodedName: String): ClassInfo = { + lookupClass(encodedName+"$") + } + + linkClasses() + + def linkClasses(): Unit = { + if (!classInfos.contains(ir.Definitions.ObjectClass)) + sys.error("Fatal error: could not find java.lang.Object on the classpath") + for (classInfo <- classInfos.values.toList) + classInfo.linkClasses() + } + + def computeReachability(manuallyReachable: Seq[ManualReachability], + noWarnMissing: Seq[NoWarnMissing]): Unit = { + // Stuff reachable from core symbols always should warn + reachCoreSymbols() + + // Disable warnings as requested + noWarnMissing.foreach(disableWarning _) + + // Reach all user stuff + manuallyReachable.foreach(reachManually _) + for (classInfo <- classInfos.values) + classInfo.reachExports() + } + + /** Reach symbols used directly by scalajsenv.js. */ + def reachCoreSymbols(): Unit = { + import semantics._ + import CheckedBehavior._ + + implicit val from = FromCore + + def instantiateClassWith(className: String, constructor: String): ClassInfo = { + val info = lookupClass(className) + info.instantiated() + info.callMethod(constructor) + info + } + + val ObjectClass = instantiateClassWith("O", "init___") + ObjectClass.callMethod("toString__T") + ObjectClass.callMethod("equals__O__Z") + + instantiateClassWith("jl_NullPointerException", "init___") + + if (asInstanceOfs != Unchecked) + instantiateClassWith("jl_ClassCastException", "init___T") + + if (asInstanceOfs == Fatal) + instantiateClassWith("sjsr_UndefinedBehaviorError", "init___jl_Throwable") + + instantiateClassWith("jl_Class", "init___jl_ScalaJSClassData") + + val RTStringModuleClass = lookupClass("sjsr_RuntimeString$") + RTStringModuleClass.accessModule() + RTStringModuleClass.callMethod("hashCode__T__I") + + val RTLongClass = lookupClass(LongImpl.RuntimeLongClass) + RTLongClass.instantiated() + for (method <- LongImpl.AllConstructors ++ LongImpl.AllMethods) + RTLongClass.callMethod(method) + + if (isBeforeOptimizer) { + for (method <- LongImpl.AllIntrinsicMethods) + RTLongClass.callMethod(method) + } + + val RTLongModuleClass = lookupClass(LongImpl.RuntimeLongModuleClass) + RTLongModuleClass.accessModule() + for (method <- LongImpl.AllModuleMethods) + RTLongModuleClass.callMethod(method) + + if (isBeforeOptimizer) { + for (hijacked <- Definitions.HijackedClasses) + lookupClass(hijacked).instantiated() + } else { + for (hijacked <- Definitions.HijackedClasses) + lookupClass(hijacked).accessData() + } + + if (semantics.strictFloats) { + val RuntimePackage = lookupClass("sjsr_package$") + RuntimePackage.accessModule() + RuntimePackage.callMethod("froundPolyfill__D__D") + } + + val BitsModuleClass = lookupClass("sjsr_Bits$") + BitsModuleClass.accessModule() + BitsModuleClass.callMethod("numberHashCode__D__I") + } + + def reachManually(info: ManualReachability) = { + implicit val from = FromManual + + // Don't lookupClass here, since we don't want to create any + // symbols. If a symbol doesn't exist, we fail. + info match { + case ReachObject(name) => classInfos(name + "$").accessModule() + case Instantiate(name) => classInfos(name).instantiated() + case ReachMethod(className, methodName, static) => + classInfos(className).callMethod(methodName, static) + } + } + + def disableWarning(noWarn: NoWarnMissing) = noWarn match { + case NoWarnClass(className) => + lookupClass(className).warnEnabled = false + case NoWarnMethod(className, methodName) => + lookupClass(className).lookupMethod(methodName).warnEnabled = false + } + + class ClassInfo(data: Infos.ClassInfo) { + val encodedName = data.encodedName + val ancestorCount = data.ancestorCount + val isStaticModule = data.kind == ClassKind.ModuleClass + val isInterface = data.kind == ClassKind.Interface + val isImplClass = data.kind == ClassKind.TraitImpl + val isRawJSType = data.kind == ClassKind.RawJSType + val isHijackedClass = data.kind == ClassKind.HijackedClass + val isClass = !isInterface && !isImplClass && !isRawJSType + val isExported = data.isExported + + val hasData = !isImplClass + val hasMoreThanData = isClass && !isHijackedClass + + var superClass: ClassInfo = _ + val ancestors = mutable.ListBuffer.empty[ClassInfo] + val descendants = mutable.ListBuffer.empty[ClassInfo] + + var nonExistent: Boolean = false + var warnEnabled: Boolean = true + + def linkClasses(): Unit = { + if (data.superClass != "") + superClass = lookupClass(data.superClass) + ancestors ++= data.ancestors.map(lookupClass) + for (ancestor <- ancestors) + ancestor.descendants += this + } + + lazy val descendentClasses = descendants.filter(_.isClass) + + def optimizerHints: Infos.OptimizerHints = data.optimizerHints + + var isInstantiated: Boolean = false + var isAnySubclassInstantiated: Boolean = false + var isModuleAccessed: Boolean = false + var isDataAccessed: Boolean = false + + var instantiatedFrom: Option[From] = None + + val delayedCalls = mutable.Map.empty[String, From] + + def isNeededAtAll = + isDataAccessed || + isAnySubclassInstantiated || + (isImplClass && methodInfos.values.exists(_.isReachable)) + + lazy val methodInfos: mutable.Map[String, MethodInfo] = { + val ms = for (methodData <- data.methods) + yield (methodData.encodedName, new MethodInfo(this, methodData)) + mutable.Map.empty[String, MethodInfo] ++ ms + } + + def lookupMethod(methodName: String): MethodInfo = { + tryLookupMethod(methodName).getOrElse { + val syntheticData = createMissingMethodInfo(methodName) + val m = new MethodInfo(this, syntheticData) + m.nonExistent = true + methodInfos += methodName -> m + m + } + } + + def tryLookupMethod(methodName: String): Option[MethodInfo] = { + assert(isClass || isImplClass, + s"Cannot call lookupMethod($methodName) on non-class $this") + @tailrec + def loop(ancestorInfo: ClassInfo): Option[MethodInfo] = { + if (ancestorInfo ne null) { + ancestorInfo.methodInfos.get(methodName) match { + case Some(m) if !m.isAbstract => Some(m) + case _ => loop(ancestorInfo.superClass) + } + } else { + None + } + } + loop(this) + } + + override def toString(): String = encodedName + + /** Start reachability algorithm with the exports for that class. */ + def reachExports(): Unit = { + implicit val from = FromExports + + // Myself + if (isExported) { + assert(!isImplClass, "An implementation class must not be exported") + if (isStaticModule) accessModule() + else instantiated() + } + + // My methods + for (methodInfo <- methodInfos.values) { + if (methodInfo.isExported) + callMethod(methodInfo.encodedName) + } + } + + def accessModule()(implicit from: From): Unit = { + assert(isStaticModule, s"Cannot call accessModule() on non-module $this") + if (!isModuleAccessed) { + logger.debugIndent(s"$this.isModuleAccessed = true") { + isModuleAccessed = true + instantiated() + callMethod("init___") + } + } + } + + def instantiated()(implicit from: From): Unit = { + if (!isInstantiated && isClass) { + logger.debugIndent(s"$this.isInstantiated = true") { + isInstantiated = true + instantiatedFrom = Some(from) + ancestors.foreach(_.subclassInstantiated()) + } + + for ((methodName, from) <- delayedCalls) + delayedCallMethod(methodName)(from) + } + } + + private def subclassInstantiated()(implicit from: From): Unit = { + if (!isAnySubclassInstantiated && isClass) { + logger.debugIndent(s"$this.isAnySubclassInstantiated = true") { + isAnySubclassInstantiated = true + if (instantiatedFrom.isEmpty) + instantiatedFrom = Some(from) + accessData() + methodInfos.get("__init__").foreach(_.reachStatic()) + } + } + } + + def accessData()(implicit from: From): Unit = { + if (!isDataAccessed && hasData) { + checkExistent() + if (DebugAnalyzer) + logger.debug(s"$this.isDataAccessed = true") + isDataAccessed = true + } + } + + def checkExistent()(implicit from: From): Unit = { + if (nonExistent) { + if (warnEnabled && globalWarnEnabled) { + logger.warn(s"Referring to non-existent class $encodedName") + warnCallStack() + } + nonExistent = false + allAvailable = false + } + } + + def callMethod(methodName: String, static: Boolean = false)( + implicit from: From): Unit = { + logger.debugIndent(s"calling${if (static) " static" else ""} $this.$methodName") { + if (isImplClass) { + // methods in impl classes are always implicitly called statically + lookupMethod(methodName).reachStatic() + } else if (isConstructorName(methodName)) { + // constructors are always implicitly called statically + lookupMethod(methodName).reachStatic() + } else if (static) { + assert(!isReflProxyName(methodName), + s"Trying to call statically refl proxy $this.$methodName") + lookupMethod(methodName).reachStatic() + } else { + for (descendentClass <- descendentClasses) { + if (descendentClass.isInstantiated) + descendentClass.delayedCallMethod(methodName) + else + descendentClass.delayedCalls += ((methodName, from)) + } + } + } + } + + private def delayedCallMethod(methodName: String)(implicit from: From): Unit = { + if (isReflProxyName(methodName)) { + tryLookupMethod(methodName).foreach(_.reach(this)) + } else { + lookupMethod(methodName).reach(this) + } + } + } + + class MethodInfo(val owner: ClassInfo, data: Infos.MethodInfo) { + + val encodedName = data.encodedName + val isAbstract = data.isAbstract + val isExported = data.isExported + val isReflProxy = isReflProxyName(encodedName) + + def optimizerHints: Infos.OptimizerHints = data.optimizerHints + + var isReachable: Boolean = false + + var calledFrom: Option[From] = None + var instantiatedSubclass: Option[ClassInfo] = None + + var nonExistent: Boolean = false + var warnEnabled: Boolean = true + + override def toString(): String = s"$owner.$encodedName" + + def reachStatic()(implicit from: From): Unit = { + assert(!isAbstract, + s"Trying to reach statically the abstract method $this") + + checkExistent() + + if (!isReachable) { + logger.debugIndent(s"$this.isReachable = true") { + isReachable = true + calledFrom = Some(from) + doReach() + } + } + } + + def reach(inClass: ClassInfo)(implicit from: From): Unit = { + assert(owner.isClass, + s"Trying to reach dynamically the non-class method $this") + assert(!isConstructorName(encodedName), + s"Trying to reach dynamically the constructor $this") + + checkExistent() + + if (!isReachable) { + logger.debugIndent(s"$this.isReachable = true") { + isReachable = true + calledFrom = Some(from) + instantiatedSubclass = Some(inClass) + doReach() + } + } + } + + private def checkExistent()(implicit from: From) = { + if (nonExistent) { + if (warnEnabled && owner.warnEnabled && globalWarnEnabled) { + logger.temporarilyNotIndented { + logger.warn(s"Referring to non-existent method $this") + warnCallStack() + } + } + allAvailable = false + } + } + + private[this] def doReach(): Unit = { + logger.debugIndent(s"$this.doReach()") { + implicit val from = FromMethod(this) + + if (owner.isImplClass) + owner.checkExistent() + + for (moduleName <- data.accessedModules) { + lookupModule(moduleName).accessModule() + } + + for (className <- data.instantiatedClasses) { + lookupClass(className).instantiated() + } + + for (className <- data.accessedClassData) { + lookupClass(className).accessData() + } + + for ((className, methods) <- data.calledMethods) { + val classInfo = lookupClass(className) + for (methodName <- methods) + classInfo.callMethod(methodName) + } + + for ((className, methods) <- data.calledMethodsStatic) { + val classInfo = lookupClass(className) + for (methodName <- methods) + classInfo.callMethod(methodName, static = true) + } + } + } + } + + def isReflProxyName(encodedName: String): Boolean = { + encodedName.endsWith("__") && + (encodedName != "init___") && (encodedName != "__init__") + } + + def isConstructorName(encodedName: String): Boolean = + encodedName.startsWith("init___") || (encodedName == "__init__") + + private def createMissingClassInfo(encodedName: String): Infos.ClassInfo = { + val kind = + if (encodedName.endsWith("$")) ClassKind.ModuleClass + else if (encodedName.endsWith("$class")) ClassKind.TraitImpl + else ClassKind.Class + Infos.ClassInfo( + name = s"<$encodedName>", + encodedName = encodedName, + isExported = false, + ancestorCount = if (kind.isClass) 1 else 0, + kind = kind, + superClass = if (kind.isClass) "O" else "", + ancestors = List(encodedName, "O"), + methods = List( + createMissingMethodInfo("__init__"), + createMissingMethodInfo("init___")) + ) + } + + private def createMissingMethodInfo(encodedName: String, + isAbstract: Boolean = false): Infos.MethodInfo = { + Infos.MethodInfo(encodedName = encodedName, isAbstract = isAbstract) + } + + def warnCallStack()(implicit from: From): Unit = { + val seenInfos = mutable.Set.empty[AnyRef] + + def rec(level: Level, optFrom: Option[From], + verb: String = "called"): Unit = { + val involvedClasses = new mutable.ListBuffer[ClassInfo] + + def onlyOnce(info: AnyRef): Boolean = { + if (seenInfos.add(info)) { + true + } else { + logger.log(level, " (already seen, not repeating call stack)") + false + } + } + + @tailrec + def loopTrace(optFrom: Option[From], verb: String = "called"): Unit = { + optFrom match { + case None => + logger.log(level, s"$verb from ... er ... nowhere!? (this is a bug in dce)") + case Some(from) => + from match { + case FromMethod(methodInfo) => + logger.log(level, s"$verb from $methodInfo") + if (onlyOnce(methodInfo)) { + methodInfo.instantiatedSubclass.foreach(involvedClasses += _) + loopTrace(methodInfo.calledFrom) + } + case FromCore => + logger.log(level, s"$verb from scalajs-corejslib.js") + case FromExports => + logger.log(level, "exported to JavaScript with @JSExport") + case FromManual => + logger.log(level, "manually made reachable") + } + } + } + + logger.indented { + loopTrace(optFrom, verb = verb) + } + + if (involvedClasses.nonEmpty) { + logger.log(level, "involving instantiated classes:") + logger.indented { + for (classInfo <- involvedClasses.result().distinct) { + logger.log(level, s"$classInfo") + if (onlyOnce(classInfo)) + rec(Level.Debug, classInfo.instantiatedFrom, verb = "instantiated") + // recurse with Debug log level not to overwhelm the user + } + } + } + } + + rec(Level.Warn, Some(from)) + } +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/GenIncOptimizer.scala b/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/GenIncOptimizer.scala new file mode 100644 index 0000000..47e1f87 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/GenIncOptimizer.scala @@ -0,0 +1,921 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.optimizer + +import language.higherKinds + +import scala.annotation.{switch, tailrec} + +import scala.collection.{GenMap, GenTraversableOnce, GenIterable, GenIterableLike} +import scala.collection.mutable + +import scala.scalajs.ir._ +import Definitions.isConstructorName +import Infos.OptimizerHints +import Trees._ +import Types._ + +import scala.scalajs.tools.sem._ + +import scala.scalajs.tools.javascript +import javascript.Trees.{Tree => JSTree} +import javascript.ScalaJSClassEmitter + +import scala.scalajs.tools.logging._ + +/** Incremental optimizer. + * An incremental optimizer consumes the reachability analysis produced by + * an [[Analyzer]], as well as trees for classes, trait impls, etc., and + * optimizes them in an incremental way. + * It maintains state between runs to do a minimal amount of work on every + * run, based on detecting what parts of the program must be re-optimized, + * and keeping optimized results from previous runs for the rest. + */ +abstract class GenIncOptimizer(semantics: Semantics) { + import GenIncOptimizer._ + + protected val CollOps: AbsCollOps + + private val classEmitter = new ScalaJSClassEmitter(semantics) + + private var logger: Logger = _ + + /** Are we in batch mode? I.e., are we running from scratch? + * Various parts of the algorithm can be skipped entirely when running in + * batch mode. + */ + private var batchMode: Boolean = false + + /** Should positions be considered when comparing tree hashes */ + private var considerPositions: Boolean = _ + + private var objectClass: Class = _ + private val classes = CollOps.emptyMap[String, Class] + private val traitImpls = CollOps.emptyParMap[String, TraitImpl] + + protected def getInterface(encodedName: String): InterfaceType + + /** Schedule a method for processing in the PROCESS PASS */ + protected def scheduleMethod(method: MethodImpl): Unit + + protected def newMethodImpl(owner: MethodContainer, + encodedName: String): MethodImpl + + def findTraitImpl(encodedName: String): TraitImpl = traitImpls(encodedName) + def findClass(encodedName: String): Class = classes(encodedName) + + def getTraitImpl(encodedName: String): Option[TraitImpl] = traitImpls.get(encodedName) + def getClass(encodedName: String): Option[Class] = classes.get(encodedName) + + type GetClassTreeIfChanged = + (String, Option[String]) => Option[(ClassDef, Option[String])] + + private def withLogger[A](logger: Logger)(body: => A): A = { + assert(this.logger == null) + this.logger = logger + try body + finally this.logger = null + } + + /** Update the incremental analyzer with a new run. */ + def update(analyzer: Analyzer, + getClassTreeIfChanged: GetClassTreeIfChanged, considerPositions: Boolean, + logger: Logger): Unit = withLogger(logger) { + + batchMode = objectClass == null + this.considerPositions = considerPositions + logger.debug(s"Optimizer batch mode: $batchMode") + + logTime(logger, "Incremental part of inc. optimizer") { + /* UPDATE PASS */ + updateAndTagEverything(analyzer, getClassTreeIfChanged) + } + + logTime(logger, "Optimizer part of inc. optimizer") { + /* PROCESS PASS */ + processAllTaggedMethods() + } + } + + /** Incremental part: update state and detect what needs to be re-optimized. + * UPDATE PASS ONLY. (This IS the update pass). + */ + private def updateAndTagEverything(analyzer: Analyzer, + getClassTreeIfChanged: GetClassTreeIfChanged): Unit = { + + val neededClasses = CollOps.emptyParMap[String, analyzer.ClassInfo] + val neededTraitImpls = CollOps.emptyParMap[String, analyzer.ClassInfo] + for { + classInfo <- analyzer.classInfos.values + if classInfo.isNeededAtAll + } { + if (classInfo.isClass && classInfo.isAnySubclassInstantiated) + CollOps.put(neededClasses, classInfo.encodedName, classInfo) + else if (classInfo.isImplClass) + CollOps.put(neededTraitImpls, classInfo.encodedName, classInfo) + } + + /* Remove deleted trait impls, and update existing trait impls. + * We don't even have to notify callers in case of additions or removals + * because callers have got to be invalidated by themselves. + * Only changed methods need to trigger notifications. + * + * Non-batch mode only. + */ + assert(!batchMode || traitImpls.isEmpty) + if (!batchMode) { + CollOps.retain(traitImpls) { (traitImplName, traitImpl) => + CollOps.remove(neededTraitImpls, traitImplName).fold { + /* Deleted trait impl. Mark all its methods as deleted, and remove it + * from known trait impls. + */ + traitImpl.methods.values.foreach(_.delete()) + + false + } { traitImplInfo => + /* Existing trait impl. Update it. */ + val (added, changed, removed) = + traitImpl.updateWith(traitImplInfo, getClassTreeIfChanged) + for (method <- changed) + traitImpl.myInterface.tagStaticCallersOf(method) + + true + } + } + } + + /* Add new trait impls. + * Easy, we don't have to notify anyone. + */ + for (traitImplInfo <- neededTraitImpls.values) { + val traitImpl = new TraitImpl(traitImplInfo.encodedName) + CollOps.put(traitImpls, traitImpl.encodedName, traitImpl) + traitImpl.updateWith(traitImplInfo, getClassTreeIfChanged) + } + + if (!batchMode) { + /* Class removals: + * * If a class is deleted or moved, delete its entire subtree (because + * all its descendants must also be deleted or moved). + * * If an existing class was instantiated but is no more, notify callers + * of its methods. + * + * Non-batch mode only. + */ + val objectClassStillExists = + objectClass.walkClassesForDeletions(neededClasses.get(_)) + assert(objectClassStillExists, "Uh oh, java.lang.Object was deleted!") + + /* Class changes: + * * Delete removed methods, update existing ones, add new ones + * * Update the list of ancestors + * * Class newly instantiated + * + * Non-batch mode only. + */ + objectClass.walkForChanges( + CollOps.remove(neededClasses, _).get, + getClassTreeIfChanged, + Set.empty) + } + + /* Class additions: + * * Add new classes (including those that have moved from elsewhere). + * In batch mode, we avoid doing notifications. + */ + + // Group children by (immediate) parent + val newChildrenByParent = CollOps.emptyAccMap[String, Analyzer#ClassInfo] + + for (classInfo <- neededClasses.values) { + val superInfo = classInfo.superClass + if (superInfo == null) { + assert(batchMode, "Trying to add java.lang.Object in incremental mode") + objectClass = new Class(None, classInfo.encodedName) + classes += classInfo.encodedName -> objectClass + objectClass.setupAfterCreation(classInfo, getClassTreeIfChanged) + } else { + CollOps.acc(newChildrenByParent, superInfo.encodedName, classInfo) + } + } + + val getNewChildren = + (name: String) => CollOps.getAcc(newChildrenByParent, name) + + // Walk the tree to add children + if (batchMode) { + objectClass.walkForAdditions(getNewChildren, getClassTreeIfChanged) + } else { + val existingParents = + CollOps.parFlatMapKeys(newChildrenByParent)(classes.get) + for (parent <- existingParents) + parent.walkForAdditions(getNewChildren, getClassTreeIfChanged) + } + + } + + /** Optimizer part: process all methods that need reoptimizing. + * PROCESS PASS ONLY. (This IS the process pass). + */ + protected def processAllTaggedMethods(): Unit + + protected def logProcessingMethods(count: Int): Unit = + logger.debug(s"Optimizing $count methods.") + + /** Base class for [[Class]] and [[TraitImpl]]. */ + abstract class MethodContainer(val encodedName: String) { + def thisType: Type + + val myInterface = getInterface(encodedName) + + val methods = mutable.Map.empty[String, MethodImpl] + + var lastVersion: Option[String] = None + + private def reachableMethodsOf(info: Analyzer#ClassInfo): Set[String] = { + (for { + methodInfo <- info.methodInfos.values + if methodInfo.isReachable && !methodInfo.isAbstract + } yield { + methodInfo.encodedName + }).toSet + } + + /** UPDATE PASS ONLY. Global concurrency safe but not on same instance */ + def updateWith(info: Analyzer#ClassInfo, + getClassTreeIfChanged: GetClassTreeIfChanged): (Set[String], Set[String], Set[String]) = { + myInterface.ancestors = info.ancestors.map(_.encodedName).toList + + val addedMethods = Set.newBuilder[String] + val changedMethods = Set.newBuilder[String] + val deletedMethods = Set.newBuilder[String] + + val reachableMethods = reachableMethodsOf(info) + val methodSetChanged = methods.keySet != reachableMethods + if (methodSetChanged) { + // Remove deleted methods + methods retain { (methodName, method) => + if (reachableMethods.contains(methodName)) { + true + } else { + deletedMethods += methodName + method.delete() + false + } + } + // Clear lastVersion if there are new methods + if (reachableMethods.exists(!methods.contains(_))) + lastVersion = None + } + for ((tree, version) <- getClassTreeIfChanged(encodedName, lastVersion)) { + lastVersion = version + this match { + case cls: Class => + cls.isModuleClass = tree.kind == ClassKind.ModuleClass + cls.fields = for (field @ VarDef(_, _, _, _) <- tree.defs) yield field + case _ => + } + tree.defs.foreach { + case methodDef: MethodDef if methodDef.name.isInstanceOf[Ident] && + reachableMethods.contains(methodDef.name.name) => + val methodName = methodDef.name.name + + val methodInfo = info.methodInfos(methodName) + methods.get(methodName).fold { + addedMethods += methodName + val method = newMethodImpl(this, methodName) + method.updateWith(methodInfo, methodDef) + methods(methodName) = method + method + } { method => + if (method.updateWith(methodInfo, methodDef)) + changedMethods += methodName + method + } + + case _ => // ignore + } + } + + (addedMethods.result(), changedMethods.result(), deletedMethods.result()) + } + } + + /** Class in the class hierarchy (not an interface). + * A class may be a module class. + * A class knows its superclass and the interfaces it implements. It also + * maintains a list of its direct subclasses, so that the instances of + * [[Class]] form a tree of the class hierarchy. + */ + class Class(val superClass: Option[Class], + _encodedName: String) extends MethodContainer(_encodedName) { + if (encodedName == Definitions.ObjectClass) { + assert(superClass.isEmpty) + assert(objectClass == null) + } else { + assert(superClass.isDefined) + } + + /** Parent chain from this to Object. */ + val parentChain: List[Class] = + this :: superClass.fold[List[Class]](Nil)(_.parentChain) + + /** Reverse parent chain from Object to this. */ + val reverseParentChain: List[Class] = + parentChain.reverse + + def thisType: Type = ClassType(encodedName) + + var interfaces: Set[InterfaceType] = Set.empty + var subclasses: CollOps.ParIterable[Class] = CollOps.emptyParIterable + var isInstantiated: Boolean = false + + var isModuleClass: Boolean = false + var hasElidableModuleAccessor: Boolean = false + + var fields: List[VarDef] = Nil + var isInlineable: Boolean = false + var tryNewInlineable: Option[RecordValue] = None + + override def toString(): String = + encodedName + + /** Walk the class hierarchy tree for deletions. + * This includes "deleting" classes that were previously instantiated but + * are no more. + * UPDATE PASS ONLY. Not concurrency safe on same instance. + */ + def walkClassesForDeletions( + getClassInfoIfNeeded: String => Option[Analyzer#ClassInfo]): Boolean = { + def sameSuperClass(info: Analyzer#ClassInfo): Boolean = + if (info.superClass == null) superClass.isEmpty + else superClass.exists(_.encodedName == info.superClass.encodedName) + + getClassInfoIfNeeded(encodedName) match { + case Some(classInfo) if sameSuperClass(classInfo) => + // Class still exists. Recurse. + subclasses = subclasses.filter( + _.walkClassesForDeletions(getClassInfoIfNeeded)) + if (isInstantiated && !classInfo.isInstantiated) + notInstantiatedAnymore() + true + case _ => + // Class does not exist or has been moved. Delete the entire subtree. + deleteSubtree() + false + } + } + + /** Delete this class and all its subclasses. UPDATE PASS ONLY. */ + def deleteSubtree(): Unit = { + delete() + for (subclass <- subclasses) + subclass.deleteSubtree() + } + + /** UPDATE PASS ONLY. */ + private def delete(): Unit = { + if (isInstantiated) + notInstantiatedAnymore() + for (method <- methods.values) + method.delete() + classes -= encodedName + /* Note: no need to tag methods that call *statically* one of the methods + * of the deleted classes, since they've got to be invalidated by + * themselves. + */ + } + + /** UPDATE PASS ONLY. */ + def notInstantiatedAnymore(): Unit = { + assert(isInstantiated) + isInstantiated = false + for (intf <- interfaces) { + intf.removeInstantiatedSubclass(this) + for (methodName <- allMethods().keys) + intf.tagDynamicCallersOf(methodName) + } + } + + /** UPDATE PASS ONLY. */ + def walkForChanges( + getClassInfo: String => Analyzer#ClassInfo, + getClassTreeIfChanged: GetClassTreeIfChanged, + parentMethodAttributeChanges: Set[String]): Unit = { + + val classInfo = getClassInfo(encodedName) + + val (addedMethods, changedMethods, deletedMethods) = + updateWith(classInfo, getClassTreeIfChanged) + + val oldInterfaces = interfaces + val newInterfaces = + classInfo.ancestors.map(info => getInterface(info.encodedName)).toSet + interfaces = newInterfaces + + val methodAttributeChanges = + (parentMethodAttributeChanges -- methods.keys ++ + addedMethods ++ changedMethods ++ deletedMethods) + + // Tag callers with dynamic calls + val wasInstantiated = isInstantiated + isInstantiated = classInfo.isInstantiated + assert(!(wasInstantiated && !isInstantiated), + "(wasInstantiated && !isInstantiated) should have been handled "+ + "during deletion phase") + + if (isInstantiated) { + if (wasInstantiated) { + val existingInterfaces = oldInterfaces.intersect(newInterfaces) + for { + intf <- existingInterfaces + methodName <- methodAttributeChanges + } { + intf.tagDynamicCallersOf(methodName) + } + if (newInterfaces.size != oldInterfaces.size || + newInterfaces.size != existingInterfaces.size) { + val allMethodNames = allMethods().keys + for { + intf <- oldInterfaces ++ newInterfaces -- existingInterfaces + methodName <- allMethodNames + } { + intf.tagDynamicCallersOf(methodName) + } + } + } else { + val allMethodNames = allMethods().keys + for (intf <- interfaces) { + intf.addInstantiatedSubclass(this) + for (methodName <- allMethodNames) + intf.tagDynamicCallersOf(methodName) + } + } + } + + // Tag callers with static calls + for (methodName <- methodAttributeChanges) + myInterface.tagStaticCallersOf(methodName) + + // Module class specifics + updateHasElidableModuleAccessor() + + // Inlineable class + if (updateIsInlineable(classInfo)) { + for (method <- methods.values; if isConstructorName(method.encodedName)) + myInterface.tagStaticCallersOf(method.encodedName) + } + + // Recurse in subclasses + for (cls <- subclasses) + cls.walkForChanges(getClassInfo, getClassTreeIfChanged, + methodAttributeChanges) + } + + /** UPDATE PASS ONLY. */ + def walkForAdditions( + getNewChildren: String => GenIterable[Analyzer#ClassInfo], + getClassTreeIfChanged: GetClassTreeIfChanged): Unit = { + + val subclassAcc = CollOps.prepAdd(subclasses) + + for (classInfo <- getNewChildren(encodedName)) { + val cls = new Class(Some(this), classInfo.encodedName) + CollOps.add(subclassAcc, cls) + classes += classInfo.encodedName -> cls + cls.setupAfterCreation(classInfo, getClassTreeIfChanged) + cls.walkForAdditions(getNewChildren, getClassTreeIfChanged) + } + + subclasses = CollOps.finishAdd(subclassAcc) + } + + /** UPDATE PASS ONLY. */ + def updateHasElidableModuleAccessor(): Unit = { + hasElidableModuleAccessor = + isAdHocElidableModuleAccessor(encodedName) || + (isModuleClass && lookupMethod("init___").exists(isElidableModuleConstructor)) + } + + /** UPDATE PASS ONLY. */ + def updateIsInlineable(classInfo: Analyzer#ClassInfo): Boolean = { + val oldTryNewInlineable = tryNewInlineable + isInlineable = classInfo.optimizerHints.hasInlineAnnot + if (!isInlineable) { + tryNewInlineable = None + } else { + val allFields = reverseParentChain.flatMap(_.fields) + val (fieldValues, fieldTypes) = (for { + VarDef(Ident(name, originalName), tpe, mutable, rhs) <- allFields + } yield { + (rhs, RecordType.Field(name, originalName, tpe, mutable)) + }).unzip + tryNewInlineable = Some( + RecordValue(RecordType(fieldTypes), fieldValues)(Position.NoPosition)) + } + tryNewInlineable != oldTryNewInlineable + } + + /** UPDATE PASS ONLY. */ + def setupAfterCreation(classInfo: Analyzer#ClassInfo, + getClassTreeIfChanged: GetClassTreeIfChanged): Unit = { + + updateWith(classInfo, getClassTreeIfChanged) + interfaces = + classInfo.ancestors.map(info => getInterface(info.encodedName)).toSet + + isInstantiated = classInfo.isInstantiated + + if (batchMode) { + if (isInstantiated) { + /* Only add the class to all its ancestor interfaces */ + for (intf <- interfaces) + intf.addInstantiatedSubclass(this) + } + } else { + val allMethodNames = allMethods().keys + + if (isInstantiated) { + /* Add the class to all its ancestor interfaces + notify all callers + * of any of the methods. + * TODO: be more selective on methods that are notified: it is not + * necessary to modify callers of methods defined in a parent class + * that already existed in the previous run. + */ + for (intf <- interfaces) { + intf.addInstantiatedSubclass(this) + for (methodName <- allMethodNames) + intf.tagDynamicCallersOf(methodName) + } + } + + /* Tag static callers because the class could have been *moved*, + * not just added. + */ + for (methodName <- allMethodNames) + myInterface.tagStaticCallersOf(methodName) + } + + updateHasElidableModuleAccessor() + updateIsInlineable(classInfo) + } + + /** UPDATE PASS ONLY. */ + private def isElidableModuleConstructor(impl: MethodImpl): Boolean = { + def isTriviallySideEffectFree(tree: Tree): Boolean = tree match { + case _:VarRef | _:Literal | _:This => true + case _ => false + } + def isElidableStat(tree: Tree): Boolean = tree match { + case Block(stats) => + stats.forall(isElidableStat) + case Assign(Select(This(), _, _), rhs) => + isTriviallySideEffectFree(rhs) + case TraitImplApply(ClassType(traitImpl), methodName, List(This())) => + traitImpls(traitImpl).methods(methodName.name).originalDef.body match { + case Skip() => true + case _ => false + } + case StaticApply(This(), ClassType(cls), methodName, args) => + Definitions.isConstructorName(methodName.name) && + args.forall(isTriviallySideEffectFree) && + impl.owner.asInstanceOf[Class].superClass.exists { superCls => + superCls.encodedName == cls && + superCls.lookupMethod(methodName.name).exists(isElidableModuleConstructor) + } + case StoreModule(_, _) => + true + case _ => + isTriviallySideEffectFree(tree) + } + isElidableStat(impl.originalDef.body) + } + + /** All the methods of this class, including inherited ones. + * It has () so we remember this is an expensive operation. + * UPDATE PASS ONLY. + */ + def allMethods(): scala.collection.Map[String, MethodImpl] = { + val result = mutable.Map.empty[String, MethodImpl] + for (parent <- reverseParentChain) + result ++= parent.methods + result + } + + /** BOTH PASSES. */ + @tailrec + final def lookupMethod(methodName: String): Option[MethodImpl] = { + methods.get(methodName) match { + case Some(impl) => Some(impl) + case none => + superClass match { + case Some(p) => p.lookupMethod(methodName) + case none => None + } + } + } + } + + /** Trait impl. */ + class TraitImpl(_encodedName: String) extends MethodContainer(_encodedName) { + def thisType: Type = NoType + } + + /** Thing from which a [[MethodImpl]] can unregister itself from. */ + trait Unregisterable { + /** UPDATE PASS ONLY. */ + def unregisterDependee(dependee: MethodImpl): Unit + } + + /** Type of a class or interface. + * Types are created on demand when a method is called on a given + * [[ClassType]]. + * + * Fully concurrency safe unless otherwise noted. + */ + abstract class InterfaceType(val encodedName: String) extends Unregisterable { + + override def toString(): String = + s"intf $encodedName" + + /** PROCESS PASS ONLY. Concurrency safe except with + * [[addInstantiatedSubclass]] and [[removeInstantiatedSubclass]] + */ + def instantiatedSubclasses: Iterable[Class] + + /** UPDATE PASS ONLY. Concurrency safe except with + * [[instantiatedSubclasses]] + */ + def addInstantiatedSubclass(x: Class): Unit + + /** UPDATE PASS ONLY. Concurrency safe except with + * [[instantiatedSubclasses]] + */ + def removeInstantiatedSubclass(x: Class): Unit + + /** PROCESS PASS ONLY. Concurrency safe except with [[ancestors_=]] */ + def ancestors: List[String] + + /** UPDATE PASS ONLY. Not concurrency safe. */ + def ancestors_=(v: List[String]): Unit + + /** PROCESS PASS ONLY. Concurrency safe except with [[ancestors_=]]. */ + def registerAskAncestors(asker: MethodImpl): Unit + + /** PROCESS PASS ONLY. */ + def registerDynamicCaller(methodName: String, caller: MethodImpl): Unit + + /** PROCESS PASS ONLY. */ + def registerStaticCaller(methodName: String, caller: MethodImpl): Unit + + /** UPDATE PASS ONLY. */ + def tagDynamicCallersOf(methodName: String): Unit + + /** UPDATE PASS ONLY. */ + def tagStaticCallersOf(methodName: String): Unit + } + + /** A method implementation. + * It must be concrete, and belong either to a [[Class]] or a [[TraitImpl]]. + * + * A single instance is **not** concurrency safe (unless otherwise noted in + * a method comment). However, the global state modifications are + * concurrency safe. + */ + abstract class MethodImpl(val owner: MethodContainer, + val encodedName: String) extends OptimizerCore.MethodImpl + with OptimizerCore.AbstractMethodID + with Unregisterable { + private[this] var _deleted: Boolean = false + + var optimizerHints: OptimizerHints = OptimizerHints.empty + var originalDef: MethodDef = _ + var desugaredDef: JSTree = _ + var preciseInfo: Infos.MethodInfo = _ + + def thisType: Type = owner.thisType + def deleted: Boolean = _deleted + + override def toString(): String = + s"$owner.$encodedName" + + /** PROCESS PASS ONLY. */ + def registerBodyAsker(asker: MethodImpl): Unit + + /** UPDATE PASS ONLY. */ + def tagBodyAskers(): Unit + + /** PROCESS PASS ONLY. */ + private def registerAskAncestors(intf: InterfaceType): Unit = { + intf.registerAskAncestors(this) + registeredTo(intf) + } + + /** PROCESS PASS ONLY. */ + private def registerDynamicCall(intf: InterfaceType, + methodName: String): Unit = { + intf.registerDynamicCaller(methodName, this) + registeredTo(intf) + } + + /** PROCESS PASS ONLY. */ + private def registerStaticCall(intf: InterfaceType, + methodName: String): Unit = { + intf.registerStaticCaller(methodName, this) + registeredTo(intf) + } + + /** PROCESS PASS ONLY. */ + def registerAskBody(target: MethodImpl): Unit = { + target.registerBodyAsker(this) + registeredTo(target) + } + + /** PROCESS PASS ONLY. */ + protected def registeredTo(intf: Unregisterable): Unit + + /** UPDATE PASS ONLY. */ + protected def unregisterFromEverywhere(): Unit + + /** Return true iff this is the first time this method is called since the + * last reset (via [[resetTag]]). + * UPDATE PASS ONLY. + */ + protected def protectTag(): Boolean + + /** PROCESS PASS ONLY. */ + protected def resetTag(): Unit + + /** Returns true if the method's attributes changed. + * Attributes are whether it is inlineable, and whether it is a trait + * impl forwarder. Basically this is what is declared in + * [[OptimizerCore.AbstractMethodID]]. + * In the process, tags all the body askers if the body changes. + * UPDATE PASS ONLY. Not concurrency safe on same instance. + */ + def updateWith(methodInfo: Analyzer#MethodInfo, + methodDef: MethodDef): Boolean = { + assert(!_deleted, "updateWith() called on a deleted method") + + val bodyChanged = { + originalDef == null || + (methodDef.hash zip originalDef.hash).forall { + case (h1, h2) => !Hashers.hashesEqual(h1, h2, considerPositions) + } + } + + if (bodyChanged) + tagBodyAskers() + + val hints = methodInfo.optimizerHints + val changed = hints != optimizerHints || bodyChanged + if (changed) { + val oldAttributes = (inlineable, isTraitImplForwarder) + + optimizerHints = hints + originalDef = methodDef + desugaredDef = null + preciseInfo = null + updateInlineable() + tag() + + val newAttributes = (inlineable, isTraitImplForwarder) + newAttributes != oldAttributes + } else { + false + } + } + + /** UPDATE PASS ONLY. Not concurrency safe on same instance. */ + def delete(): Unit = { + assert(!_deleted, "delete() called twice") + _deleted = true + if (protectTag()) + unregisterFromEverywhere() + } + + /** Concurrency safe with itself and [[delete]] on the same instance + * + * [[tag]] can be called concurrently with [[delete]] when methods in + * traits/classes are updated. + * + * UPDATE PASS ONLY. + */ + def tag(): Unit = if (protectTag()) { + scheduleMethod(this) + unregisterFromEverywhere() + } + + /** PROCESS PASS ONLY. */ + def process(): Unit = if (!_deleted) { + val (optimizedDef, info) = new Optimizer().optimize(thisType, originalDef) + desugaredDef = + if (owner.isInstanceOf[Class]) + classEmitter.genMethod(owner.encodedName, optimizedDef) + else + classEmitter.genTraitImplMethod(owner.encodedName, optimizedDef) + preciseInfo = info + resetTag() + } + + /** All methods are PROCESS PASS ONLY */ + private class Optimizer extends OptimizerCore(semantics) { + type MethodID = MethodImpl + + val myself: MethodImpl.this.type = MethodImpl.this + + protected def getMethodBody(method: MethodID): MethodDef = { + MethodImpl.this.registerAskBody(method) + method.originalDef + } + + protected def dynamicCall(intfName: String, + methodName: String): List[MethodID] = { + val intf = getInterface(intfName) + MethodImpl.this.registerDynamicCall(intf, methodName) + intf.instantiatedSubclasses.flatMap(_.lookupMethod(methodName)).toList + } + + protected def staticCall(className: String, + methodName: String): Option[MethodID] = { + val clazz = classes(className) + MethodImpl.this.registerStaticCall(clazz.myInterface, methodName) + clazz.lookupMethod(methodName) + } + + protected def traitImplCall(traitImplName: String, + methodName: String): Option[MethodID] = { + val traitImpl = traitImpls(traitImplName) + registerStaticCall(traitImpl.myInterface, methodName) + traitImpl.methods.get(methodName) + } + + protected def getAncestorsOf(intfName: String): List[String] = { + val intf = getInterface(intfName) + registerAskAncestors(intf) + intf.ancestors + } + + protected def hasElidableModuleAccessor(moduleClassName: String): Boolean = + classes(moduleClassName).hasElidableModuleAccessor + + protected def tryNewInlineableClass(className: String): Option[RecordValue] = + classes(className).tryNewInlineable + } + } + +} + +object GenIncOptimizer { + + private val isAdHocElidableModuleAccessor = + Set("s_Predef$") + + private[optimizer] def logTime[A](logger: Logger, + title: String)(body: => A): A = { + val startTime = System.nanoTime() + val result = body + val endTime = System.nanoTime() + val elapsedTime = endTime - startTime + logger.time(title, elapsedTime) + result + } + + private[optimizer] trait AbsCollOps { + type Map[K, V] <: mutable.Map[K, V] + type ParMap[K, V] <: GenMap[K, V] + type AccMap[K, V] + type ParIterable[V] <: GenIterableLike[V, ParIterable[V]] + type Addable[V] + + def emptyAccMap[K, V]: AccMap[K, V] + def emptyMap[K, V]: Map[K, V] + def emptyParMap[K, V]: ParMap[K, V] + def emptyParIterable[V]: ParIterable[V] + + // Operations on ParMap + def put[K, V](map: ParMap[K, V], k: K, v: V): Unit + def remove[K, V](map: ParMap[K, V], k: K): Option[V] + def retain[K, V](map: ParMap[K, V])(p: (K, V) => Boolean): Unit + + // Operations on AccMap + def acc[K, V](map: AccMap[K, V], k: K, v: V): Unit + def getAcc[K, V](map: AccMap[K, V], k: K): GenIterable[V] + def parFlatMapKeys[A, B](map: AccMap[A, _])( + f: A => GenTraversableOnce[B]): GenIterable[B] + + // Operations on ParIterable + def prepAdd[V](it: ParIterable[V]): Addable[V] + def add[V](addable: Addable[V], v: V): Unit + def finishAdd[V](addable: Addable[V]): ParIterable[V] + + } + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/IRChecker.scala b/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/IRChecker.scala new file mode 100644 index 0000000..6329826 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/IRChecker.scala @@ -0,0 +1,854 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.optimizer + +import scala.language.implicitConversions + +import scala.annotation.switch + +import scala.collection.mutable + +import scala.scalajs.ir._ +import Definitions._ +import Trees._ +import Types._ + +import scala.scalajs.tools.logging._ + +/** Checker for the validity of the IR. */ +class IRChecker(analyzer: Analyzer, allClassDefs: Seq[ClassDef], logger: Logger) { + import IRChecker._ + + private var _errorCount: Int = 0 + def errorCount: Int = _errorCount + + private val classes: mutable.Map[String, CheckedClass] = { + mutable.Map.empty[String, CheckedClass] ++= + allClassDefs.map(new CheckedClass(_)).map(c => c.name -> c) + } + + def check(): Boolean = { + for { + classDef <- allClassDefs + if analyzer.classInfos(classDef.name.name).isNeededAtAll + } { + classDef.kind match { + case ClassKind.Class | ClassKind.ModuleClass => checkClass(classDef) + case ClassKind.TraitImpl => checkTraitImpl(classDef) + case _ => + } + } + errorCount == 0 + } + + def checkClass(classDef: ClassDef): Unit = { + if (!analyzer.classInfos(classDef.name.name).isAnySubclassInstantiated) + return + + for (member <- classDef.defs) { + implicit val ctx = ErrorContext(member) + member match { + // Scala declarations + case v @ VarDef(_, _, _, _) => + checkFieldDef(v, classDef) + case m: MethodDef if m.name.isInstanceOf[Ident] => + checkMethodDef(m, classDef) + + // Exports + case m: MethodDef if m.name.isInstanceOf[StringLiteral] => + checkExportedMethodDef(m, classDef) + case member @ PropertyDef(_: StringLiteral, _, _, _) => + checkExportedPropertyDef(member, classDef) + case member @ ConstructorExportDef(_, _, _) => + checkConstructorExportDef(member, classDef) + case member @ ModuleExportDef(_) => + checkModuleExportDef(member, classDef) + + // Anything else is illegal + case _ => + reportError(s"Illegal class member of type ${member.getClass.getName}") + } + } + } + + def checkTraitImpl(classDef: ClassDef): Unit = { + for (member <- classDef.defs) { + implicit val ctx = ErrorContext(member) + member match { + case m: MethodDef => + checkMethodDef(m, classDef) + case _ => + reportError(s"Invalid member for a TraitImpl") + } + } + } + + def checkFieldDef(fieldDef: VarDef, classDef: ClassDef): Unit = { + val VarDef(name, tpe, mutable, rhs) = fieldDef + implicit val ctx = ErrorContext(fieldDef) + + if (tpe == NoType) + reportError(s"VarDef cannot have type NoType") + else + typecheckExpect(rhs, Env.empty, tpe) + } + + def checkMethodDef(methodDef: MethodDef, classDef: ClassDef): Unit = { + val MethodDef(Ident(name, _), params, resultType, body) = methodDef + implicit val ctx = ErrorContext(methodDef) + + if (!analyzer.classInfos(classDef.name.name).methodInfos(name).isReachable) + return + + for (ParamDef(name, tpe, _) <- params) + if (tpe == NoType) + reportError(s"Parameter $name has type NoType") + + val resultTypeForSig = + if (isConstructorName(name)) NoType + else resultType + + val advertizedSig = (params.map(_.ptpe), resultTypeForSig) + val sigFromName = inferMethodType(name, + inTraitImpl = classDef.kind == ClassKind.TraitImpl) + if (advertizedSig != sigFromName) { + reportError( + s"The signature of ${classDef.name.name}.$name, which is "+ + s"$advertizedSig, does not match its name (should be $sigFromName).") + } + + val thisType = + if (!classDef.kind.isClass) NoType + else ClassType(classDef.name.name) + val bodyEnv = Env.fromSignature(thisType, params, resultType) + if (resultType == NoType) + typecheckStat(body, bodyEnv) + else + typecheckExpect(body, bodyEnv, resultType) + } + + def checkExportedMethodDef(methodDef: MethodDef, classDef: ClassDef): Unit = { + val MethodDef(_, params, resultType, body) = methodDef + implicit val ctx = ErrorContext(methodDef) + + if (!classDef.kind.isClass) { + reportError(s"Exported method def can only appear in a class") + return + } + + for (ParamDef(name, tpe, _) <- params) { + if (tpe == NoType) + reportError(s"Parameter $name has type NoType") + else if (tpe != AnyType) + reportError(s"Parameter $name of exported method def has type $tpe, "+ + "but must be Any") + } + + if (resultType != AnyType) { + reportError(s"Result type of exported method def is $resultType, "+ + "but must be Any") + } + + val thisType = ClassType(classDef.name.name) + val bodyEnv = Env.fromSignature(thisType, params, resultType) + .withArgumentsVar(methodDef.pos) + typecheckExpect(body, bodyEnv, resultType) + } + + def checkExportedPropertyDef(propDef: PropertyDef, classDef: ClassDef): Unit = { + val PropertyDef(_, getterBody, setterArg, setterBody) = propDef + implicit val ctx = ErrorContext(propDef) + + if (!classDef.kind.isClass) { + reportError(s"Exported property def can only appear in a class") + return + } + + val thisType = ClassType(classDef.name.name) + + if (getterBody != EmptyTree) { + val getterBodyEnv = Env.fromSignature(thisType, Nil, AnyType) + typecheckExpect(getterBody, getterBodyEnv, AnyType) + } + + if (setterBody != EmptyTree) { + if (setterArg.ptpe != AnyType) + reportError("Setter argument of exported property def has type "+ + s"${setterArg.ptpe}, but must be Any") + + val setterBodyEnv = Env.fromSignature(thisType, List(setterArg), NoType) + typecheckStat(setterBody, setterBodyEnv) + } + } + + def checkConstructorExportDef(ctorDef: ConstructorExportDef, + classDef: ClassDef): Unit = { + val ConstructorExportDef(_, params, body) = ctorDef + implicit val ctx = ErrorContext(ctorDef) + + if (!classDef.kind.isClass) { + reportError(s"Exported constructor def can only appear in a class") + return + } + + for (ParamDef(name, tpe, _) <- params) { + if (tpe == NoType) + reportError(s"Parameter $name has type NoType") + else if (tpe != AnyType) + reportError(s"Parameter $name of exported constructor def has type "+ + s"$tpe, but must be Any") + } + + val thisType = ClassType(classDef.name.name) + val bodyEnv = Env.fromSignature(thisType, params, NoType) + .withArgumentsVar(ctorDef.pos) + typecheckStat(body, bodyEnv) + } + + def checkModuleExportDef(moduleDef: ModuleExportDef, + classDef: ClassDef): Unit = { + implicit val ctx = ErrorContext(moduleDef) + + if (classDef.kind != ClassKind.ModuleClass) + reportError(s"Exported module def can only appear in a module class") + } + + def typecheckStat(tree: Tree, env: Env): Env = { + implicit val ctx = ErrorContext(tree) + + tree match { + case VarDef(ident, vtpe, mutable, rhs) => + typecheckExpect(rhs, env, vtpe) + env.withLocal(LocalDef(ident.name, vtpe, mutable)(tree.pos)) + + case Skip() => + env + + case Assign(select, rhs) => + select match { + case Select(_, Ident(name, _), false) => + /* TODO In theory this case would verify that we never assign to + * an immutable field. But we cannot do that because we *do* emit + * such assigns in constructors. + * In the future we might want to check that only these legal + * special cases happen, and nothing else. But it seems non-trivial + * to do so, so currently we trust scalac not to make us emit + * illegal assigns. + */ + //reportError(s"Assignment to immutable field $name.") + case VarRef(Ident(name, _), false) => + reportError(s"Assignment to immutable variable $name.") + case _ => + } + val lhsTpe = typecheckExpr(select, env) + val expectedRhsTpe = select match { + case _:JSDotSelect | _:JSBracketSelect => AnyType + case _ => lhsTpe + } + typecheckExpect(rhs, env, expectedRhsTpe) + env + + case StoreModule(cls, value) => + if (!cls.className.endsWith("$")) + reportError("StoreModule of non-module class $cls") + typecheckExpect(value, env, ClassType(cls.className)) + env + + case Block(stats) => + (env /: stats) { (prevEnv, stat) => + typecheckStat(stat, prevEnv) + } + env + + case Labeled(label, NoType, body) => + typecheckStat(body, env.withLabeledReturnType(label.name, AnyType)) + env + + case If(cond, thenp, elsep) => + typecheckExpect(cond, env, BooleanType) + typecheckStat(thenp, env) + typecheckStat(elsep, env) + env + + case While(cond, body, label) => + typecheckExpect(cond, env, BooleanType) + typecheckStat(body, env) + env + + case DoWhile(body, cond, label) => + typecheckStat(body, env) + typecheckExpect(cond, env, BooleanType) + env + + case Try(block, errVar, handler, finalizer) => + typecheckStat(block, env) + if (handler != EmptyTree) { + val handlerEnv = + env.withLocal(LocalDef(errVar.name, AnyType, false)(errVar.pos)) + typecheckStat(handler, handlerEnv) + } + if (finalizer != EmptyTree) { + typecheckStat(finalizer, env) + } + env + + case Match(selector, cases, default) => + typecheckExpr(selector, env) + for ((alts, body) <- cases) { + alts.foreach(typecheckExpr(_, env)) + typecheckStat(body, env) + } + typecheckStat(default, env) + env + + case Debugger() => + env + + case JSDelete(JSDotSelect(obj, prop)) => + typecheckExpr(obj, env) + env + + case JSDelete(JSBracketSelect(obj, prop)) => + typecheckExpr(obj, env) + typecheckExpr(prop, env) + env + + case _ => + typecheck(tree, env) + env + } + } + + def typecheckExpect(tree: Tree, env: Env, expectedType: Type)( + implicit ctx: ErrorContext): Unit = { + val tpe = typecheckExpr(tree, env) + if (!isSubtype(tpe, expectedType)) + reportError(s"$expectedType expected but $tpe found "+ + s"for tree of type ${tree.getClass.getName}") + } + + def typecheckExpr(tree: Tree, env: Env): Type = { + implicit val ctx = ErrorContext(tree) + if (tree.tpe == NoType) + reportError(s"Expression tree has type NoType") + typecheck(tree, env) + } + + def typecheck(tree: Tree, env: Env): Type = { + implicit val ctx = ErrorContext(tree) + + def checkApplyGeneric(methodName: String, methodFullName: String, + args: List[Tree], inTraitImpl: Boolean): Unit = { + val (methodParams, resultType) = inferMethodType(methodName, inTraitImpl) + if (args.size != methodParams.size) + reportError(s"Arity mismatch: ${methodParams.size} expected but "+ + s"${args.size} found") + for ((actual, formal) <- args zip methodParams) { + typecheckExpect(actual, env, formal) + } + if (!isConstructorName(methodName) && tree.tpe != resultType) + reportError(s"Call to $methodFullName of type $resultType "+ + s"typed as ${tree.tpe}") + } + + tree match { + // Control flow constructs + + case Block(statsAndExpr) => + val stats :+ expr = statsAndExpr + val envAfterStats = (env /: stats) { (prevEnv, stat) => + typecheckStat(stat, prevEnv) + } + typecheckExpr(expr, envAfterStats) + + case Labeled(label, tpe, body) => + typecheckExpect(body, env.withLabeledReturnType(label.name, tpe), tpe) + + case Return(expr, label) => + env.returnTypes.get(label.map(_.name)).fold[Unit] { + reportError(s"Cannot return to label $label.") + typecheckExpr(expr, env) + } { returnType => + typecheckExpect(expr, env, returnType) + } + + case If(cond, thenp, elsep) => + val tpe = tree.tpe + typecheckExpect(cond, env, BooleanType) + typecheckExpect(thenp, env, tpe) + typecheckExpect(elsep, env, tpe) + + case While(BooleanLiteral(true), body, label) if tree.tpe == NothingType => + typecheckStat(body, env) + + case Try(block, errVar, handler, finalizer) => + val tpe = tree.tpe + typecheckExpect(block, env, tpe) + if (handler != EmptyTree) { + val handlerEnv = + env.withLocal(LocalDef(errVar.name, AnyType, false)(errVar.pos)) + typecheckExpect(handler, handlerEnv, tpe) + } + if (finalizer != EmptyTree) { + typecheckStat(finalizer, env) + } + + case Throw(expr) => + typecheckExpr(expr, env) + + case Continue(label) => + /* Here we could check that it is indeed legal to break to the + * specified label. However, if we do anything illegal here, it will + * result in a SyntaxError in JavaScript anyway, so we do not really + * care. + */ + + case Match(selector, cases, default) => + val tpe = tree.tpe + typecheckExpr(selector, env) + for ((alts, body) <- cases) { + alts.foreach(typecheckExpr(_, env)) + typecheckExpect(body, env, tpe) + } + typecheckExpect(default, env, tpe) + + // Scala expressions + + case New(cls, ctor, args) => + val clazz = lookupClass(cls) + if (!clazz.kind.isClass) + reportError(s"new $cls which is not a class") + checkApplyGeneric(ctor.name, s"$cls.$ctor", args, + inTraitImpl = false) + + case LoadModule(cls) => + if (!cls.className.endsWith("$")) + reportError("LoadModule of non-module class $cls") + + case Select(qualifier, Ident(item, _), mutable) => + val qualType = typecheckExpr(qualifier, env) + qualType match { + case ClassType(cls) => + val clazz = lookupClass(cls) + if (!clazz.kind.isClass) { + reportError(s"Cannot select $item of non-class $cls") + } else { + clazz.lookupField(item).fold[Unit] { + reportError(s"Class $cls does not have a field $item") + } { fieldDef => + if (fieldDef.tpe != tree.tpe) + reportError(s"Select $cls.$item of type "+ + s"${fieldDef.tpe} typed as ${tree.tpe}") + if (fieldDef.mutable != mutable) + reportError(s"Select $cls.$item with "+ + s"mutable=${fieldDef.mutable} marked as mutable=$mutable") + } + } + case NullType | NothingType => + // always ok + case _ => + reportError(s"Cannot select $item of non-class type $qualType") + } + + case Apply(receiver, Ident(method, _), args) => + val receiverType = typecheckExpr(receiver, env) + checkApplyGeneric(method, s"$receiverType.$method", args, + inTraitImpl = false) + + case StaticApply(receiver, cls, Ident(method, _), args) => + typecheckExpect(receiver, env, cls) + checkApplyGeneric(method, s"$cls.$method", args, inTraitImpl = false) + + case TraitImplApply(impl, Ident(method, _), args) => + val clazz = lookupClass(impl) + if (clazz.kind != ClassKind.TraitImpl) + reportError(s"Cannot trait-impl apply method of non-trait-impl $impl") + checkApplyGeneric(method, s"$impl.$method", args, inTraitImpl = true) + + case UnaryOp(op, lhs) => + import UnaryOp._ + (op: @switch) match { + case `typeof` => + typecheckExpr(lhs, env) + case IntToLong => + typecheckExpect(lhs, env, IntType) + case LongToInt | LongToDouble => + typecheckExpect(lhs, env, LongType) + case DoubleToInt | DoubleToFloat | DoubleToLong => + typecheckExpect(lhs, env, DoubleType) + case Boolean_! => + typecheckExpect(lhs, env, BooleanType) + } + + case BinaryOp(op, lhs, rhs) => + import BinaryOp._ + (op: @switch) match { + case === | !== | String_+ => + typecheckExpr(lhs, env) + typecheckExpr(rhs, env) + case `in` => + typecheckExpect(lhs, env, ClassType(StringClass)) + typecheckExpr(rhs, env) + case `instanceof` => + typecheckExpr(lhs, env) + typecheckExpr(rhs, env) + case Int_+ | Int_- | Int_* | Int_/ | Int_% | + Int_| | Int_& | Int_^ | Int_<< | Int_>>> | Int_>> => + typecheckExpect(lhs, env, IntType) + typecheckExpect(rhs, env, IntType) + case Float_+ | Float_- | Float_* | Float_/ | Float_% => + typecheckExpect(lhs, env, FloatType) + typecheckExpect(lhs, env, FloatType) + case Long_+ | Long_- | Long_* | Long_/ | Long_% | + Long_| | Long_& | Long_^ | + Long_== | Long_!= | Long_< | Long_<= | Long_> | Long_>= => + typecheckExpect(lhs, env, LongType) + typecheckExpect(rhs, env, LongType) + case Long_<< | Long_>>> | Long_>> => + typecheckExpect(lhs, env, LongType) + typecheckExpect(rhs, env, IntType) + case Double_+ | Double_- | Double_* | Double_/ | Double_% | + Num_== | Num_!= | Num_< | Num_<= | Num_> | Num_>= => + typecheckExpect(lhs, env, DoubleType) + typecheckExpect(lhs, env, DoubleType) + case Boolean_== | Boolean_!= | Boolean_| | Boolean_& => + typecheckExpect(lhs, env, BooleanType) + typecheckExpect(rhs, env, BooleanType) + } + + case NewArray(tpe, lengths) => + for (length <- lengths) + typecheckExpect(length, env, IntType) + + case ArrayValue(tpe, elems) => + val elemType = arrayElemType(tpe) + for (elem <- elems) + typecheckExpect(elem, env, elemType) + + case ArrayLength(array) => + val arrayType = typecheckExpr(array, env) + if (!arrayType.isInstanceOf[ArrayType]) + reportError(s"Array type expected but $arrayType found") + + case ArraySelect(array, index) => + typecheckExpect(index, env, IntType) + typecheckExpr(array, env) match { + case arrayType: ArrayType => + if (tree.tpe != arrayElemType(arrayType)) + reportError(s"Array select of array type $arrayType typed as ${tree.tpe}") + case arrayType => + reportError(s"Array type expected but $arrayType found") + } + + case IsInstanceOf(expr, cls) => + typecheckExpr(expr, env) + + case AsInstanceOf(expr, cls) => + typecheckExpr(expr, env) + + case Unbox(expr, _) => + typecheckExpr(expr, env) + + case GetClass(expr) => + typecheckExpr(expr, env) + + // JavaScript expressions + + case JSNew(ctor, args) => + typecheckExpr(ctor, env) + for (arg <- args) + typecheckExpr(arg, env) + + case JSDotSelect(qualifier, item) => + typecheckExpr(qualifier, env) + + case JSBracketSelect(qualifier, item) => + typecheckExpr(qualifier, env) + typecheckExpr(item, env) + + case JSFunctionApply(fun, args) => + typecheckExpr(fun, env) + for (arg <- args) + typecheckExpr(arg, env) + + case JSDotMethodApply(receiver, method, args) => + typecheckExpr(receiver, env) + for (arg <- args) + typecheckExpr(arg, env) + + case JSBracketMethodApply(receiver, method, args) => + typecheckExpr(receiver, env) + typecheckExpr(method, env) + for (arg <- args) + typecheckExpr(arg, env) + + case JSUnaryOp(op, lhs) => + typecheckExpr(lhs, env) + + case JSBinaryOp(op, lhs, rhs) => + typecheckExpr(lhs, env) + typecheckExpr(rhs, env) + + case JSArrayConstr(items) => + for (item <- items) + typecheckExpr(item, env) + + case JSObjectConstr(fields) => + for ((_, value) <- fields) + typecheckExpr(value, env) + + case JSEnvInfo() => + + // Literals + + case _: Literal => + + // Atomic expressions + + case VarRef(Ident(name, _), mutable) => + env.locals.get(name).fold[Unit] { + reportError(s"Cannot find variable $name in scope") + } { localDef => + if (tree.tpe != localDef.tpe) + reportError(s"Variable $name of type ${localDef.tpe} "+ + s"typed as ${tree.tpe}") + if (mutable != localDef.mutable) + reportError(s"Variable $name with mutable=${localDef.mutable} "+ + s"marked as mutable=$mutable") + } + + case This() => + if (!isSubtype(env.thisTpe, tree.tpe)) + reportError(s"this of type ${env.thisTpe} typed as ${tree.tpe}") + + case Closure(captureParams, params, body, captureValues) => + if (captureParams.size != captureValues.size) + reportError("Mismatched size for captures: "+ + s"${captureParams.size} params vs ${captureValues.size} values") + + for ((ParamDef(name, ctpe, mutable), value) <- captureParams zip captureValues) { + if (mutable) + reportError(s"Capture parameter $name cannot be mutable") + if (ctpe == NoType) + reportError(s"Parameter $name has type NoType") + else + typecheckExpect(value, env, ctpe) + } + + for (ParamDef(name, ptpe, mutable) <- params) { + if (ptpe == NoType) + reportError(s"Parameter $name has type NoType") + else if (ptpe != AnyType) + reportError(s"Closure parameter $name has type $ptpe instead of any") + } + + val bodyEnv = Env.fromSignature( + AnyType, captureParams ++ params, AnyType) + typecheckExpect(body, bodyEnv, AnyType) + + case _ => + reportError(s"Invalid expression tree") + } + + tree.tpe + } + + def inferMethodType(encodedName: String, inTraitImpl: Boolean)( + implicit ctx: ErrorContext): (List[Type], Type) = { + def dropPrivateMarker(params: List[String]): List[String] = + if (params.nonEmpty && params.head.startsWith("p")) params.tail + else params + + if (isConstructorName(encodedName)) { + assert(!inTraitImpl, "Trait impl should not have a constructor") + val params = dropPrivateMarker( + encodedName.stripPrefix("init___").split("__").toList) + if (params == List("")) (Nil, NoType) + else (params.map(decodeType), NoType) + } else if (isReflProxyName(encodedName)) { + assert(!inTraitImpl, "Trait impl should not have refl proxy methods") + val params = dropPrivateMarker(encodedName.split("__").toList.tail) + (params.map(decodeType), AnyType) + } else { + val paramsAndResult0 = + encodedName.split("__").toList.tail + val paramsAndResult1 = + if (inTraitImpl) paramsAndResult0.tail + else paramsAndResult0 + val paramsAndResult = + dropPrivateMarker(paramsAndResult1) + (paramsAndResult.init.map(decodeType), decodeType(paramsAndResult.last)) + } + } + + def decodeType(encodedName: String)(implicit ctx: ErrorContext): Type = { + if (encodedName.isEmpty) NoType + else if (encodedName.charAt(0) == 'A') { + // array type + val dims = encodedName.indexWhere(_ != 'A') + val base = encodedName.substring(dims) + ArrayType(base, dims) + } else if (encodedName.length == 1) { + (encodedName.charAt(0): @switch) match { + case 'V' => NoType + case 'Z' => BooleanType + case 'C' | 'B' | 'S' | 'I' => IntType + case 'J' => LongType + case 'F' => FloatType + case 'D' => DoubleType + case 'O' => AnyType + case 'T' => ClassType(StringClass) // NOT StringType + } + } else if (encodedName == "sr_Nothing$") { + NothingType + } else if (encodedName == "sr_Null$") { + NullType + } else { + val clazz = lookupClass(encodedName) + if (clazz.kind == ClassKind.RawJSType) AnyType + else ClassType(encodedName) + } + } + + def arrayElemType(arrayType: ArrayType)(implicit ctx: ErrorContext): Type = { + if (arrayType.dimensions == 1) decodeType(arrayType.baseClassName) + else ArrayType(arrayType.baseClassName, arrayType.dimensions-1) + } + + def reportError(msg: String)(implicit ctx: ErrorContext): Unit = { + logger.error(s"$ctx: $msg") + _errorCount += 1 + } + + def lookupClass(className: String)(implicit ctx: ErrorContext): CheckedClass = { + classes.getOrElseUpdate(className, { + reportError(s"Cannot find class $className") + new CheckedClass(className, ClassKind.Class, + Some(ObjectClass), Set(ObjectClass)) + }) + } + + def lookupClass(classType: ClassType)(implicit ctx: ErrorContext): CheckedClass = + lookupClass(classType.className) + + def isSubclass(lhs: String, rhs: String)(implicit ctx: ErrorContext): Boolean = { + lookupClass(lhs).isSubclass(lookupClass(rhs)) + } + + def isSubtype(lhs: Type, rhs: Type)(implicit ctx: ErrorContext): Boolean = { + Types.isSubtype(lhs, rhs)(isSubclass) + } + + class Env( + /** Type of `this`. Can be NoType. */ + val thisTpe: Type, + /** Local variables in scope (including through closures). */ + val locals: Map[String, LocalDef], + /** Return types by label. */ + val returnTypes: Map[Option[String], Type] + ) { + def withThis(thisTpe: Type): Env = + new Env(thisTpe, this.locals, this.returnTypes) + + def withLocal(localDef: LocalDef): Env = + new Env(thisTpe, locals + (localDef.name -> localDef), returnTypes) + + def withLocals(localDefs: TraversableOnce[LocalDef]): Env = + new Env(thisTpe, locals ++ localDefs.map(d => d.name -> d), returnTypes) + + def withReturnType(returnType: Type): Env = + new Env(this.thisTpe, this.locals, returnTypes + (None -> returnType)) + + def withLabeledReturnType(label: String, returnType: Type): Env = + new Env(this.thisTpe, this.locals, returnTypes + (Some(label) -> returnType)) + + def withArgumentsVar(pos: Position): Env = + withLocal(LocalDef("arguments", AnyType, mutable = false)(pos)) + } + + object Env { + val empty: Env = new Env(NoType, Map.empty, Map.empty) + + def fromSignature(thisType: Type, params: List[ParamDef], + resultType: Type): Env = { + val paramLocalDefs = + for (p @ ParamDef(name, tpe, mutable) <- params) yield + name.name -> LocalDef(name.name, tpe, mutable)(p.pos) + new Env(thisType, paramLocalDefs.toMap, + Map(None -> (if (resultType == NoType) AnyType else resultType))) + } + } + + class CheckedClass( + val name: String, + val kind: ClassKind, + val superClassName: Option[String], + val ancestors: Set[String], + _fields: TraversableOnce[CheckedField] = Nil) { + + val fields = _fields.map(f => f.name -> f).toMap + + lazy val superClass = superClassName.map(classes) + + def this(classDef: ClassDef) = { + this(classDef.name.name, classDef.kind, + classDef.parent.map(_.name), + classDef.ancestors.map(_.name).toSet, + CheckedClass.collectFields(classDef)) + } + + def isSubclass(that: CheckedClass): Boolean = + this == that || ancestors.contains(that.name) + + def isAncestorOfHijackedClass: Boolean = + AncestorsOfHijackedClasses.contains(name) + + def lookupField(name: String): Option[CheckedField] = + fields.get(name).orElse(superClass.flatMap(_.lookupField(name))) + } + + object CheckedClass { + private def collectFields(classDef: ClassDef) = { + classDef.defs collect { + case VarDef(Ident(name, _), tpe, mutable, _) => + new CheckedField(name, tpe, mutable) + } + } + } + + class CheckedField(val name: String, val tpe: Type, val mutable: Boolean) +} + +object IRChecker { + private final class ErrorContext(val tree: Tree) extends AnyVal { + override def toString(): String = { + val pos = tree.pos + s"${pos.source}(${pos.line+1}:${pos.column+1}:${tree.getClass.getSimpleName})" + } + + def pos: Position = tree.pos + } + + private object ErrorContext { + implicit def tree2errorContext(tree: Tree): ErrorContext = + ErrorContext(tree) + + def apply(tree: Tree): ErrorContext = + new ErrorContext(tree) + } + + private def isConstructorName(name: String): Boolean = + name.startsWith("init___") + + private def isReflProxyName(name: String): Boolean = + name.endsWith("__") && !isConstructorName(name) + + case class LocalDef(name: String, tpe: Type, mutable: Boolean)(val pos: Position) +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/IncOptimizer.scala b/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/IncOptimizer.scala new file mode 100644 index 0000000..d115618 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/IncOptimizer.scala @@ -0,0 +1,158 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.optimizer + +import scala.collection.{GenTraversableOnce, GenIterable} +import scala.collection.mutable + +import scala.scalajs.tools.sem.Semantics + +class IncOptimizer(semantics: Semantics) extends GenIncOptimizer(semantics) { + + protected object CollOps extends GenIncOptimizer.AbsCollOps { + type Map[K, V] = mutable.Map[K, V] + type ParMap[K, V] = mutable.Map[K, V] + type AccMap[K, V] = mutable.Map[K, mutable.ListBuffer[V]] + type ParIterable[V] = mutable.ListBuffer[V] + type Addable[V] = mutable.ListBuffer[V] + + def emptyAccMap[K, V]: AccMap[K, V] = mutable.Map.empty + def emptyMap[K, V]: Map[K, V] = mutable.Map.empty + def emptyParMap[K, V]: ParMap[K, V] = mutable.Map.empty + def emptyParIterable[V]: ParIterable[V] = mutable.ListBuffer.empty + + // Operations on ParMap + def put[K, V](map: ParMap[K, V], k: K, v: V): Unit = map.put(k, v) + def remove[K, V](map: ParMap[K, V], k: K): Option[V] = map.remove(k) + + def retain[K, V](map: ParMap[K, V])(p: (K, V) => Boolean): Unit = + map.retain(p) + + // Operations on AccMap + def acc[K, V](map: AccMap[K, V], k: K, v: V): Unit = + map.getOrElseUpdate(k, mutable.ListBuffer.empty) += v + + def getAcc[K, V](map: AccMap[K, V], k: K): GenIterable[V] = + map.getOrElse(k, Nil) + + def parFlatMapKeys[A, B](map: AccMap[A, _])( + f: A => GenTraversableOnce[B]): GenIterable[B] = + map.keys.flatMap(f).toList + + // Operations on ParIterable + def prepAdd[V](it: ParIterable[V]): Addable[V] = it + def add[V](addable: Addable[V], v: V): Unit = addable += v + def finishAdd[V](addable: Addable[V]): ParIterable[V] = addable + } + + private val _interfaces = mutable.Map.empty[String, InterfaceType] + protected def getInterface(encodedName: String): InterfaceType = + _interfaces.getOrElseUpdate(encodedName, new SeqInterfaceType(encodedName)) + + private val methodsToProcess = mutable.ListBuffer.empty[MethodImpl] + protected def scheduleMethod(method: MethodImpl): Unit = + methodsToProcess += method + + protected def newMethodImpl(owner: MethodContainer, + encodedName: String): MethodImpl = new SeqMethodImpl(owner, encodedName) + + protected def processAllTaggedMethods(): Unit = { + logProcessingMethods(methodsToProcess.count(!_.deleted)) + for (method <- methodsToProcess) + method.process() + methodsToProcess.clear() + } + + private class SeqInterfaceType(encName: String) extends InterfaceType(encName) { + private val ancestorsAskers = mutable.Set.empty[MethodImpl] + private val dynamicCallers = mutable.Map.empty[String, mutable.Set[MethodImpl]] + private val staticCallers = mutable.Map.empty[String, mutable.Set[MethodImpl]] + + private var _ancestors: List[String] = encodedName :: Nil + + private var _instantiatedSubclasses: Set[Class] = Set.empty + + def instantiatedSubclasses: Iterable[Class] = _instantiatedSubclasses + + def addInstantiatedSubclass(x: Class): Unit = + _instantiatedSubclasses += x + + def removeInstantiatedSubclass(x: Class): Unit = + _instantiatedSubclasses -= x + + def ancestors: List[String] = _ancestors + + def ancestors_=(v: List[String]): Unit = { + if (v != _ancestors) { + _ancestors = v + ancestorsAskers.foreach(_.tag()) + ancestorsAskers.clear() + } + } + + def registerAskAncestors(asker: MethodImpl): Unit = + ancestorsAskers += asker + + def registerDynamicCaller(methodName: String, caller: MethodImpl): Unit = + dynamicCallers.getOrElseUpdate(methodName, mutable.Set.empty) += caller + + def registerStaticCaller(methodName: String, caller: MethodImpl): Unit = + staticCallers.getOrElseUpdate(methodName, mutable.Set.empty) += caller + + def unregisterDependee(dependee: MethodImpl): Unit = { + ancestorsAskers -= dependee + dynamicCallers.values.foreach(_ -= dependee) + staticCallers.values.foreach(_ -= dependee) + } + + def tagDynamicCallersOf(methodName: String): Unit = + dynamicCallers.remove(methodName).foreach(_.foreach(_.tag())) + + def tagStaticCallersOf(methodName: String): Unit = + staticCallers.remove(methodName).foreach(_.foreach(_.tag())) + } + + private class SeqMethodImpl(owner: MethodContainer, + encodedName: String) extends MethodImpl(owner, encodedName) { + + private val bodyAskers = mutable.Set.empty[MethodImpl] + + def registerBodyAsker(asker: MethodImpl): Unit = + bodyAskers += asker + + def unregisterDependee(dependee: MethodImpl): Unit = + bodyAskers -= dependee + + def tagBodyAskers(): Unit = { + bodyAskers.foreach(_.tag()) + bodyAskers.clear() + } + + private var _registeredTo: List[Unregisterable] = Nil + private var tagged = false + + protected def registeredTo(intf: Unregisterable): Unit = + _registeredTo ::= intf + + protected def unregisterFromEverywhere(): Unit = { + _registeredTo.foreach(_.unregisterDependee(this)) + _registeredTo = Nil + } + + protected def protectTag(): Boolean = { + val res = !tagged + tagged = true + res + } + protected def resetTag(): Unit = tagged = false + + } + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/JSTreeBuilder.scala b/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/JSTreeBuilder.scala new file mode 100644 index 0000000..3d37a56 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/JSTreeBuilder.scala @@ -0,0 +1,16 @@ +package scala.scalajs.tools.optimizer + +import scala.scalajs.ir +import scala.scalajs.tools.javascript + +/** An abstract builder taking IR or JSTrees */ +trait JSTreeBuilder { + /** Add a JavaScript tree representing a statement. + * The tree must be a valid JavaScript tree (typically obtained by + * desugaring a full-fledged IR tree). + */ + def addJSTree(tree: javascript.Trees.Tree): Unit + + /** Completes the builder. */ + def complete(): Unit = () +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/OptimizerCore.scala b/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/OptimizerCore.scala new file mode 100644 index 0000000..364038b --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/OptimizerCore.scala @@ -0,0 +1,3572 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.optimizer + +import scala.language.implicitConversions + +import scala.annotation.{switch, tailrec} + +import scala.collection.mutable + +import scala.util.control.{NonFatal, ControlThrowable, TailCalls} +import scala.util.control.TailCalls.{done => _, _} // done is a too generic term + +import scala.scalajs.ir._ +import Definitions.{ObjectClass, isConstructorName, isReflProxyName} +import Infos.OptimizerHints +import Trees._ +import Types._ + +import scala.scalajs.tools.sem.Semantics +import scala.scalajs.tools.javascript.LongImpl +import scala.scalajs.tools.logging._ + +/** Optimizer core. + * Designed to be "mixed in" [[IncOptimizer#MethodImpl#Optimizer]]. + * This is the core of the optimizer. It contains all the smart things the + * optimizer does. To perform inlining, it relies on abstract protected + * methods to identify the target of calls. + */ +private[optimizer] abstract class OptimizerCore(semantics: Semantics) { + import OptimizerCore._ + + type MethodID <: AbstractMethodID + + val myself: MethodID + + /** Returns the body of a method. */ + protected def getMethodBody(method: MethodID): MethodDef + + /** Returns the list of possible targets for a dynamically linked call. */ + protected def dynamicCall(intfName: String, + methodName: String): List[MethodID] + + /** Returns the target of a static call. */ + protected def staticCall(className: String, + methodName: String): Option[MethodID] + + /** Returns the target of a trait impl call. */ + protected def traitImplCall(traitImplName: String, + methodName: String): Option[MethodID] + + /** Returns the list of ancestors of a class or interface. */ + protected def getAncestorsOf(encodedName: String): List[String] + + /** Tests whether the given module class has an elidable accessor. + * In other words, whether it is safe to discard a LoadModule of that + * module class which is not used. + */ + protected def hasElidableModuleAccessor(moduleClassName: String): Boolean + + /** Tests whether the given class is inlineable. + * @return None if the class is not inlineable, Some(value) if it is, where + * value is a RecordValue with the initial value of its fields. + */ + protected def tryNewInlineableClass(className: String): Option[RecordValue] + + private val usedLocalNames = mutable.Set.empty[String] + private val usedLabelNames = mutable.Set.empty[String] + private var statesInUse: List[State[_]] = Nil + + private var disableOptimisticOptimizations: Boolean = false + private var rollbacksCount: Int = 0 + + private val attemptedInlining = mutable.ListBuffer.empty[MethodID] + + private var curTrampolineId = 0 + + def optimize(thisType: Type, originalDef: MethodDef): (MethodDef, Infos.MethodInfo) = { + try { + val MethodDef(name, params, resultType, body) = originalDef + val (newParams, newBody) = try { + transformIsolatedBody(Some(myself), thisType, params, resultType, body) + } catch { + case _: TooManyRollbacksException => + usedLocalNames.clear() + usedLabelNames.clear() + statesInUse = Nil + disableOptimisticOptimizations = true + transformIsolatedBody(Some(myself), thisType, params, resultType, body) + } + val m = MethodDef(name, newParams, resultType, newBody)(None)(originalDef.pos) + val info = recreateInfo(m) + (m, info) + } catch { + case NonFatal(cause) => + throw new OptimizeException(myself, attemptedInlining.distinct.toList, cause) + case e: Throwable => + // This is a fatal exception. Don't wrap, just output debug info error + Console.err.println(exceptionMsg(myself, attemptedInlining.distinct.toList)) + throw e + } + } + + private def withState[A, B](state: State[A])(body: => B): B = { + statesInUse ::= state + try body + finally statesInUse = statesInUse.tail + } + + private def freshLocalName(base: String): String = + freshNameGeneric(usedLocalNames, base) + + private def freshLabelName(base: String): String = + freshNameGeneric(usedLabelNames, base) + + private val isReserved = isKeyword ++ Seq("arguments", "eval", "ScalaJS") + + private def freshNameGeneric(usedNames: mutable.Set[String], base: String): String = { + val result = if (!usedNames.contains(base) && !isReserved(base)) { + base + } else { + var i = 1 + while (usedNames.contains(base + "$" + i)) + i += 1 + base + "$" + i + } + usedNames += result + result + } + + private def tryOrRollback(body: CancelFun => TailRec[Tree])( + fallbackFun: () => TailRec[Tree]): TailRec[Tree] = { + if (disableOptimisticOptimizations) { + fallbackFun() + } else { + val trampolineId = curTrampolineId + val savedUsedLocalNames = usedLocalNames.toSet + val savedUsedLabelNames = usedLabelNames.toSet + val savedStates = statesInUse.map(_.makeBackup()) + + body { () => + throw new RollbackException(trampolineId, savedUsedLocalNames, + savedUsedLabelNames, savedStates, fallbackFun) + } + } + } + + private def isSubclass(lhs: String, rhs: String): Boolean = + getAncestorsOf(lhs).contains(rhs) + + private val isSubclassFun = isSubclass _ + private def isSubtype(lhs: Type, rhs: Type): Boolean = + Types.isSubtype(lhs, rhs)(isSubclassFun) + + /** Transforms a statement. + * + * For valid expression trees, it is always the case that + * {{{ + * transformStat(tree) + * === + * pretransformExpr(tree)(finishTransformStat) + * }}} + */ + private def transformStat(tree: Tree)(implicit scope: Scope): Tree = + transform(tree, isStat = true) + + /** Transforms an expression. + * + * It is always the case that + * {{{ + * transformExpr(tree) + * === + * pretransformExpr(tree)(finishTransformExpr) + * }}} + */ + private def transformExpr(tree: Tree)(implicit scope: Scope): Tree = + transform(tree, isStat = false) + + /** Transforms a tree. */ + private def transform(tree: Tree, isStat: Boolean)( + implicit scope: Scope): Tree = { + + @inline implicit def pos = tree.pos + val result = tree match { + // Definitions + + case VarDef(_, _, _, rhs) => + /* A local var that is last (or alone) in its block is not terribly + * useful. Get rid of it. + * (Non-last VarDefs in blocks are handled in transformBlock.) + */ + transformStat(rhs) + + // Control flow constructs + + case tree: Block => + transformBlock(tree, isStat) + + case Labeled(ident @ Ident(label, _), tpe, body) => + trampoline { + returnable(label, if (isStat) NoType else tpe, body, isStat, + usePreTransform = false)(finishTransform(isStat)) + } + + case Assign(lhs, rhs) => + val cont = { (preTransLhs: PreTransform) => + resolveLocalDef(preTransLhs) match { + case PreTransRecordTree(lhsTree, lhsOrigType, lhsCancelFun) => + val recordType = lhsTree.tpe.asInstanceOf[RecordType] + pretransformNoLocalDef(rhs) { + case PreTransRecordTree(rhsTree, rhsOrigType, rhsCancelFun) => + if (rhsTree.tpe != recordType || rhsOrigType != lhsOrigType) + lhsCancelFun() + TailCalls.done(Assign(lhsTree, rhsTree)) + case _ => + lhsCancelFun() + } + case PreTransTree(lhsTree, _) => + TailCalls.done(Assign(lhsTree, transformExpr(rhs))) + } + } + trampoline { + lhs match { + case lhs: Select => + pretransformSelectCommon(lhs, isLhsOfAssign = true)(cont) + case _ => + pretransformExpr(lhs)(cont) + } + } + + case Return(expr, optLabel) => + val optInfo = optLabel match { + case Some(Ident(label, _)) => + Some(scope.env.labelInfos(label)) + case None => + scope.env.labelInfos.get("") + } + optInfo.fold[Tree] { + Return(transformExpr(expr), None) + } { info => + val newOptLabel = Some(Ident(info.newName, None)) + if (!info.acceptRecords) { + val newExpr = transformExpr(expr) + info.returnedTypes.value ::= (newExpr.tpe, RefinedType(newExpr.tpe)) + Return(newExpr, newOptLabel) + } else trampoline { + pretransformNoLocalDef(expr) { texpr => + texpr match { + case PreTransRecordTree(newExpr, origType, cancelFun) => + info.returnedTypes.value ::= (newExpr.tpe, origType) + TailCalls.done(Return(newExpr, newOptLabel)) + case PreTransTree(newExpr, tpe) => + info.returnedTypes.value ::= (newExpr.tpe, tpe) + TailCalls.done(Return(newExpr, newOptLabel)) + } + } + } + } + + case If(cond, thenp, elsep) => + val newCond = transformExpr(cond) + newCond match { + case BooleanLiteral(condValue) => + if (condValue) transform(thenp, isStat) + else transform(elsep, isStat) + case _ => + val newThenp = transform(thenp, isStat) + val newElsep = transform(elsep, isStat) + val refinedType = + constrainedLub(newThenp.tpe, newElsep.tpe, tree.tpe) + foldIf(newCond, newThenp, newElsep)(refinedType) + } + + case While(cond, body, optLabel) => + val newCond = transformExpr(cond) + newCond match { + case BooleanLiteral(false) => Skip() + case _ => + optLabel match { + case None => + While(newCond, transformStat(body), None) + + case Some(labelIdent @ Ident(label, _)) => + val newLabel = freshLabelName(label) + val info = new LabelInfo(newLabel, acceptRecords = false) + While(newCond, { + val bodyScope = scope.withEnv( + scope.env.withLabelInfo(label, info)) + transformStat(body)(bodyScope) + }, Some(Ident(newLabel, None)(labelIdent.pos))) + } + } + + case DoWhile(body, cond, None) => + val newBody = transformStat(body) + val newCond = transformExpr(cond) + newCond match { + case BooleanLiteral(false) => newBody + case _ => DoWhile(newBody, newCond, None) + } + + case Try(block, errVar, EmptyTree, finalizer) => + val newBlock = transform(block, isStat) + val newFinalizer = transformStat(finalizer) + Try(newBlock, errVar, EmptyTree, newFinalizer)(newBlock.tpe) + + case Try(block, errVar @ Ident(name, originalName), handler, finalizer) => + val newBlock = transform(block, isStat) + + val newName = freshLocalName(name) + val newOriginalName = originalName.orElse(Some(name)) + val localDef = LocalDef(RefinedType(AnyType), true, + ReplaceWithVarRef(newName, newOriginalName, new SimpleState(true), None)) + val newHandler = { + val handlerScope = scope.withEnv(scope.env.withLocalDef(name, localDef)) + transform(handler, isStat)(handlerScope) + } + + val newFinalizer = transformStat(finalizer) + + val refinedType = constrainedLub(newBlock.tpe, newHandler.tpe, tree.tpe) + Try(newBlock, Ident(newName, newOriginalName)(errVar.pos), + newHandler, newFinalizer)(refinedType) + + case Throw(expr) => + Throw(transformExpr(expr)) + + case Continue(optLabel) => + val newOptLabel = optLabel map { label => + Ident(scope.env.labelInfos(label.name).newName, None)(label.pos) + } + Continue(newOptLabel) + + case Match(selector, cases, default) => + val newSelector = transformExpr(selector) + newSelector match { + case newSelector: Literal => + val body = cases collectFirst { + case (alts, body) if alts.exists(literal_===(_, newSelector)) => body + } getOrElse default + transform(body, isStat) + case _ => + Match(newSelector, + cases map (c => (c._1, transform(c._2, isStat))), + transform(default, isStat))(tree.tpe) + } + + // Scala expressions + + case New(cls, ctor, args) => + New(cls, ctor, args map transformExpr) + + case StoreModule(cls, value) => + StoreModule(cls, transformExpr(value)) + + case tree: Select => + trampoline { + pretransformSelectCommon(tree, isLhsOfAssign = false)( + finishTransform(isStat = false)) + } + + case tree: Apply => + trampoline { + pretransformApply(tree, isStat, usePreTransform = false)( + finishTransform(isStat)) + } + + case tree: StaticApply => + trampoline { + pretransformStaticApply(tree, isStat, usePreTransform = false)( + finishTransform(isStat)) + } + + case tree: TraitImplApply => + trampoline { + pretransformTraitImplApply(tree, isStat, usePreTransform = false)( + finishTransform(isStat)) + } + + case tree @ UnaryOp(_, arg) => + if (isStat) transformStat(arg) + else transformUnaryOp(tree) + + case tree @ BinaryOp(op, lhs, rhs) => + if (isStat) Block(transformStat(lhs), transformStat(rhs)) + else transformBinaryOp(tree) + + case NewArray(tpe, lengths) => + NewArray(tpe, lengths map transformExpr) + + case ArrayValue(tpe, elems) => + ArrayValue(tpe, elems map transformExpr) + + case ArrayLength(array) => + ArrayLength(transformExpr(array)) + + case ArraySelect(array, index) => + ArraySelect(transformExpr(array), transformExpr(index))(tree.tpe) + + case RecordValue(tpe, elems) => + RecordValue(tpe, elems map transformExpr) + + case IsInstanceOf(expr, ClassType(ObjectClass)) => + transformExpr(BinaryOp(BinaryOp.!==, expr, Null())) + + case IsInstanceOf(expr, tpe) => + trampoline { + pretransformExpr(expr) { texpr => + val result = { + if (isSubtype(texpr.tpe.base, tpe)) { + if (texpr.tpe.isNullable) + BinaryOp(BinaryOp.!==, finishTransformExpr(texpr), Null()) + else + Block(finishTransformStat(texpr), BooleanLiteral(true)) + } else { + if (texpr.tpe.isExact) + Block(finishTransformStat(texpr), BooleanLiteral(false)) + else + IsInstanceOf(finishTransformExpr(texpr), tpe) + } + } + TailCalls.done(result) + } + } + + case AsInstanceOf(expr, ClassType(ObjectClass)) => + transformExpr(expr) + + case AsInstanceOf(expr, cls) => + trampoline { + pretransformExpr(tree)(finishTransform(isStat)) + } + + case Unbox(arg, charCode) => + trampoline { + pretransformExpr(arg) { targ => + foldUnbox(targ, charCode)(finishTransform(isStat)) + } + } + + case GetClass(expr) => + GetClass(transformExpr(expr)) + + // JavaScript expressions + + case JSNew(ctor, args) => + JSNew(transformExpr(ctor), args map transformExpr) + + case JSDotSelect(qualifier, item) => + JSDotSelect(transformExpr(qualifier), item) + + case JSBracketSelect(qualifier, item) => + JSBracketSelect(transformExpr(qualifier), transformExpr(item)) + + case tree: JSFunctionApply => + trampoline { + pretransformJSFunctionApply(tree, isStat, usePreTransform = false)( + finishTransform(isStat)) + } + + case JSDotMethodApply(receiver, method, args) => + JSDotMethodApply(transformExpr(receiver), method, + args map transformExpr) + + case JSBracketMethodApply(receiver, method, args) => + JSBracketMethodApply(transformExpr(receiver), transformExpr(method), + args map transformExpr) + + case JSDelete(JSDotSelect(obj, prop)) => + JSDelete(JSDotSelect(transformExpr(obj), prop)) + + case JSDelete(JSBracketSelect(obj, prop)) => + JSDelete(JSBracketSelect(transformExpr(obj), transformExpr(prop))) + + case JSUnaryOp(op, lhs) => + JSUnaryOp(op, transformExpr(lhs)) + + case JSBinaryOp(op, lhs, rhs) => + JSBinaryOp(op, transformExpr(lhs), transformExpr(rhs)) + + case JSArrayConstr(items) => + JSArrayConstr(items map transformExpr) + + case JSObjectConstr(fields) => + JSObjectConstr(fields map { + case (name, value) => (name, transformExpr(value)) + }) + + // Atomic expressions + + case _:VarRef | _:This => + trampoline { + pretransformExpr(tree)(finishTransform(isStat)) + } + + case Closure(captureParams, params, body, captureValues) => + transformClosureCommon(captureParams, params, body, + captureValues.map(transformExpr)) + + // Trees that need not be transformed + + case _:Skip | _:Debugger | _:LoadModule | + _:JSEnvInfo | _:Literal | EmptyTree => + tree + } + + if (isStat) keepOnlySideEffects(result) + else result + } + + private def transformClosureCommon(captureParams: List[ParamDef], + params: List[ParamDef], body: Tree, newCaptureValues: List[Tree])( + implicit pos: Position): Closure = { + + val (allNewParams, newBody) = + transformIsolatedBody(None, AnyType, captureParams ++ params, AnyType, body) + val (newCaptureParams, newParams) = + allNewParams.splitAt(captureParams.size) + + Closure(newCaptureParams, newParams, newBody, newCaptureValues) + } + + private def transformBlock(tree: Block, isStat: Boolean)( + implicit scope: Scope): Tree = { + def transformList(stats: List[Tree])( + implicit scope: Scope): Tree = stats match { + case last :: Nil => + transform(last, isStat) + + case (VarDef(Ident(name, originalName), vtpe, mutable, rhs)) :: rest => + trampoline { + pretransformExpr(rhs) { trhs => + withBinding(Binding(name, originalName, vtpe, mutable, trhs)) { + (restScope, cont1) => + val newRest = transformList(rest)(restScope) + cont1(PreTransTree(newRest, RefinedType(newRest.tpe))) + } (finishTransform(isStat)) + } + } + + case stat :: rest => + val transformedStat = transformStat(stat) + if (transformedStat.tpe == NothingType) transformedStat + else Block(transformedStat, transformList(rest))(stat.pos) + + case Nil => // silence the exhaustivity warning in a sensible way + Skip()(tree.pos) + } + transformList(tree.stats)(scope) + } + + /** Pretransforms a list of trees as a list of [[PreTransform]]s. + * This is a convenience method to use pretransformExpr on a list. + */ + private def pretransformExprs(trees: List[Tree])( + cont: List[PreTransform] => TailRec[Tree])( + implicit scope: Scope): TailRec[Tree] = { + trees match { + case first :: rest => + pretransformExpr(first) { tfirst => + pretransformExprs(rest) { trest => + cont(tfirst :: trest) + } + } + + case Nil => + cont(Nil) + } + } + + /** Pretransforms two trees as a pair of [[PreTransform]]s. + * This is a convenience method to use pretransformExpr on two trees. + */ + private def pretransformExprs(tree1: Tree, tree2: Tree)( + cont: (PreTransform, PreTransform) => TailRec[Tree])( + implicit scope: Scope): TailRec[Tree] = { + pretransformExpr(tree1) { ttree1 => + pretransformExpr(tree2) { ttree2 => + cont(ttree1, ttree2) + } + } + } + + /** Pretransforms a tree and a list of trees as [[PreTransform]]s. + * This is a convenience method to use pretransformExpr. + */ + private def pretransformExprs(first: Tree, rest: List[Tree])( + cont: (PreTransform, List[PreTransform]) => TailRec[Tree])( + implicit scope: Scope): TailRec[Tree] = { + pretransformExpr(first) { tfirst => + pretransformExprs(rest) { trest => + cont(tfirst, trest) + } + } + } + + /** Pretransforms a tree to get a refined type while avoiding to force + * things we might be able to optimize by folding and aliasing. + */ + private def pretransformExpr(tree: Tree)(cont: PreTransCont)( + implicit scope: Scope): TailRec[Tree] = tailcall { + @inline implicit def pos = tree.pos + + tree match { + case tree: Block => + pretransformBlock(tree)(cont) + + case VarRef(Ident(name, _), _) => + val localDef = scope.env.localDefs.getOrElse(name, + sys.error(s"Cannot find local def '$name' at $pos\n" + + s"While optimizing $myself\n" + + s"Env is ${scope.env}\nInlining ${scope.implsBeingInlined}")) + cont(PreTransLocalDef(localDef)) + + case This() => + val localDef = scope.env.localDefs.getOrElse("this", + sys.error(s"Found invalid 'this' at $pos\n" + + s"While optimizing $myself\n" + + s"Env is ${scope.env}\nInlining ${scope.implsBeingInlined}")) + cont(PreTransLocalDef(localDef)) + + case If(cond, thenp, elsep) => + val newCond = transformExpr(cond) + newCond match { + case BooleanLiteral(condValue) => + if (condValue) pretransformExpr(thenp)(cont) + else pretransformExpr(elsep)(cont) + case _ => + tryOrRollback { cancelFun => + pretransformNoLocalDef(thenp) { tthenp => + pretransformNoLocalDef(elsep) { telsep => + (tthenp, telsep) match { + case (PreTransRecordTree(thenTree, thenOrigType, thenCancelFun), + PreTransRecordTree(elseTree, elseOrigType, elseCancelFun)) => + val commonType = + if (thenTree.tpe == elseTree.tpe && + thenOrigType == elseOrigType) thenTree.tpe + else cancelFun() + val refinedOrigType = + constrainedLub(thenOrigType, elseOrigType, tree.tpe) + cont(PreTransRecordTree( + If(newCond, thenTree, elseTree)(commonType), + refinedOrigType, + cancelFun)) + + case (PreTransRecordTree(thenTree, thenOrigType, thenCancelFun), _) + if telsep.tpe.isNothingType => + cont(PreTransRecordTree( + If(newCond, thenTree, finishTransformExpr(telsep))(thenTree.tpe), + thenOrigType, + thenCancelFun)) + + case (_, PreTransRecordTree(elseTree, elseOrigType, elseCancelFun)) + if tthenp.tpe.isNothingType => + cont(PreTransRecordTree( + If(newCond, finishTransformExpr(tthenp), elseTree)(elseTree.tpe), + elseOrigType, + elseCancelFun)) + + case _ => + val newThenp = finishTransformExpr(tthenp) + val newElsep = finishTransformExpr(telsep) + val refinedType = + constrainedLub(newThenp.tpe, newElsep.tpe, tree.tpe) + cont(PreTransTree( + foldIf(newCond, newThenp, newElsep)(refinedType))) + } + } + } + } { () => + val newThenp = transformExpr(thenp) + val newElsep = transformExpr(elsep) + val refinedType = + constrainedLub(newThenp.tpe, newElsep.tpe, tree.tpe) + cont(PreTransTree( + foldIf(newCond, newThenp, newElsep)(refinedType))) + } + } + + case Match(selector, cases, default) => + val newSelector = transformExpr(selector) + newSelector match { + case newSelector: Literal => + val body = cases collectFirst { + case (alts, body) if alts.exists(literal_===(_, newSelector)) => body + } getOrElse default + pretransformExpr(body)(cont) + case _ => + cont(PreTransTree(Match(newSelector, + cases map (c => (c._1, transformExpr(c._2))), + transformExpr(default))(tree.tpe))) + } + + case Labeled(ident @ Ident(label, _), tpe, body) => + returnable(label, tpe, body, isStat = false, usePreTransform = true)(cont) + + case New(cls @ ClassType(className), ctor, args) => + tryNewInlineableClass(className) match { + case Some(initialValue) => + pretransformExprs(args) { targs => + tryOrRollback { cancelFun => + inlineClassConstructor( + new AllocationSite(tree), + cls, initialValue, ctor, targs, cancelFun)(cont) + } { () => + cont(PreTransTree( + New(cls, ctor, targs.map(finishTransformExpr)), + RefinedType(cls, isExact = true, isNullable = false))) + } + } + case None => + cont(PreTransTree( + New(cls, ctor, args.map(transformExpr)), + RefinedType(cls, isExact = true, isNullable = false))) + } + + case tree: Select => + pretransformSelectCommon(tree, isLhsOfAssign = false)(cont) + + case tree: Apply => + pretransformApply(tree, isStat = false, + usePreTransform = true)(cont) + + case tree: StaticApply => + pretransformStaticApply(tree, isStat = false, + usePreTransform = true)(cont) + + case tree: TraitImplApply => + pretransformTraitImplApply(tree, isStat = false, + usePreTransform = true)(cont) + + case tree: JSFunctionApply => + pretransformJSFunctionApply(tree, isStat = false, + usePreTransform = true)(cont) + + case AsInstanceOf(expr, tpe) => + pretransformExpr(expr) { texpr => + tpe match { + case ClassType(ObjectClass) => + cont(texpr) + case _ => + if (isSubtype(texpr.tpe.base, tpe)) { + cont(texpr) + } else { + cont(PreTransTree( + AsInstanceOf(finishTransformExpr(texpr), tpe))) + } + } + } + + case Closure(captureParams, params, body, captureValues) => + pretransformExprs(captureValues) { tcaptureValues => + tryOrRollback { cancelFun => + val captureBindings = for { + (ParamDef(Ident(name, origName), tpe, mutable), value) <- + captureParams zip tcaptureValues + } yield { + Binding(name, origName, tpe, mutable, value) + } + withNewLocalDefs(captureBindings) { (captureLocalDefs, cont1) => + val alreadyUsedState = new SimpleState[Boolean](false) + withState(alreadyUsedState) { + val replacement = TentativeClosureReplacement( + captureParams, params, body, captureLocalDefs, + alreadyUsedState, cancelFun) + val localDef = LocalDef( + RefinedType(AnyType, isExact = false, isNullable = false), + mutable = false, + replacement) + cont1(PreTransLocalDef(localDef)) + } + } (cont) + } { () => + val newClosure = transformClosureCommon(captureParams, params, body, + tcaptureValues.map(finishTransformExpr)) + cont(PreTransTree( + newClosure, + RefinedType(AnyType, isExact = false, isNullable = false))) + } + } + + case _ => + val result = transformExpr(tree) + cont(PreTransTree(result, RefinedType(result.tpe))) + } + } + + private def pretransformBlock(tree: Block)( + cont: PreTransCont)( + implicit scope: Scope): TailRec[Tree] = { + def pretransformList(stats: List[Tree])( + cont: PreTransCont)( + implicit scope: Scope): TailRec[Tree] = stats match { + case last :: Nil => + pretransformExpr(last)(cont) + + case (VarDef(Ident(name, originalName), vtpe, mutable, rhs)) :: rest => + pretransformExpr(rhs) { trhs => + withBinding(Binding(name, originalName, vtpe, mutable, trhs)) { + (restScope, cont1) => + pretransformList(rest)(cont1)(restScope) + } (cont) + } + + case stat :: rest => + implicit val pos = tree.pos + val transformedStat = transformStat(stat) + transformedStat match { + case Skip() => + pretransformList(rest)(cont) + case _ => + if (transformedStat.tpe == NothingType) + cont(PreTransTree(transformedStat, RefinedType.Nothing)) + else { + pretransformList(rest) { trest => + cont(PreTransBlock(transformedStat :: Nil, trest)) + } + } + } + + case Nil => // silence the exhaustivity warning in a sensible way + TailCalls.done(Skip()(tree.pos)) + } + pretransformList(tree.stats)(cont)(scope) + } + + private def pretransformSelectCommon(tree: Select, isLhsOfAssign: Boolean)( + cont: PreTransCont)( + implicit scope: Scope): TailRec[Tree] = { + val Select(qualifier, item, mutable) = tree + pretransformExpr(qualifier) { preTransQual => + pretransformSelectCommon(tree.tpe, preTransQual, item, mutable, + isLhsOfAssign)(cont)(scope, tree.pos) + } + } + + private def pretransformSelectCommon(expectedType: Type, + preTransQual: PreTransform, item: Ident, mutable: Boolean, + isLhsOfAssign: Boolean)( + cont: PreTransCont)( + implicit scope: Scope, pos: Position): TailRec[Tree] = { + preTransQual match { + case PreTransLocalDef(LocalDef(_, _, + InlineClassBeingConstructedReplacement(fieldLocalDefs, cancelFun))) => + val fieldLocalDef = fieldLocalDefs(item.name) + if (!isLhsOfAssign || fieldLocalDef.mutable) { + cont(PreTransLocalDef(fieldLocalDef)) + } else { + /* This is an assignment to an immutable field of a inlineable class + * being constructed, but that does not appear at the "top-level" of + * one of its constructors. We cannot handle those, so we cancel. + * (Assignments at the top-level are normal initializations of these + * fields, and are transformed as vals in inlineClassConstructor.) + */ + cancelFun() + } + case PreTransLocalDef(LocalDef(_, _, + InlineClassInstanceReplacement(_, fieldLocalDefs, cancelFun))) => + val fieldLocalDef = fieldLocalDefs(item.name) + if (!isLhsOfAssign || fieldLocalDef.mutable) { + cont(PreTransLocalDef(fieldLocalDef)) + } else { + /* In an ideal world, this should not happen (assigning to an + * immutable field of an already constructed object). However, since + * we cannot IR-check that this does not happen (see #1021), this is + * effectively allowed by the IR spec. We are therefore not allowed + * to crash. We cancel instead. This will become an actual field + * (rather than an optimized local val) which is not considered pure + * (for that same reason). + */ + cancelFun() + } + case _ => + resolveLocalDef(preTransQual) match { + case PreTransRecordTree(newQual, origType, cancelFun) => + val recordType = newQual.tpe.asInstanceOf[RecordType] + val field = recordType.findField(item.name) + val sel = Select(newQual, item, mutable)(field.tpe) + sel.tpe match { + case _: RecordType => + cont(PreTransRecordTree(sel, RefinedType(expectedType), cancelFun)) + case _ => + cont(PreTransTree(sel, RefinedType(sel.tpe))) + } + + case PreTransTree(newQual, _) => + cont(PreTransTree(Select(newQual, item, mutable)(expectedType), + RefinedType(expectedType))) + } + } + } + + /** Resolves any LocalDef in a [[PreTransform]]. */ + private def resolveLocalDef(preTrans: PreTransform): PreTransGenTree = { + implicit val pos = preTrans.pos + preTrans match { + case PreTransBlock(stats, result) => + resolveLocalDef(result) match { + case PreTransRecordTree(tree, tpe, cancelFun) => + PreTransRecordTree(Block(stats :+ tree), tpe, cancelFun) + case PreTransTree(tree, tpe) => + PreTransTree(Block(stats :+ tree), tpe) + } + + case PreTransLocalDef(localDef @ LocalDef(tpe, mutable, replacement)) => + replacement match { + case ReplaceWithRecordVarRef(name, originalName, + recordType, used, cancelFun) => + used.value = true + PreTransRecordTree( + VarRef(Ident(name, originalName), mutable)(recordType), + tpe, cancelFun) + + case InlineClassInstanceReplacement(recordType, fieldLocalDefs, cancelFun) => + if (!isImmutableType(recordType)) + cancelFun() + PreTransRecordTree( + RecordValue(recordType, recordType.fields.map( + f => fieldLocalDefs(f.name).newReplacement)), + tpe, cancelFun) + + case _ => + PreTransTree(localDef.newReplacement, localDef.tpe) + } + + case preTrans: PreTransGenTree => + preTrans + } + } + + /** Combines pretransformExpr and resolveLocalDef in one convenience method. */ + private def pretransformNoLocalDef(tree: Tree)( + cont: PreTransGenTree => TailRec[Tree])( + implicit scope: Scope): TailRec[Tree] = { + pretransformExpr(tree) { ttree => + cont(resolveLocalDef(ttree)) + } + } + + /** Finishes a pretransform, either a statement or an expression. */ + private def finishTransform(isStat: Boolean): PreTransCont = { preTrans => + TailCalls.done { + if (isStat) finishTransformStat(preTrans) + else finishTransformExpr(preTrans) + } + } + + /** Finishes an expression pretransform to get a normal [[Tree]]. + * This method (together with finishTransformStat) must not be called more + * than once per pretransform and per translation. + * By "per translation", we mean in an alternative path through + * `tryOrRollback`. It could still be called several times as long as + * it is once in the 'try' part and once in the 'fallback' part. + */ + private def finishTransformExpr(preTrans: PreTransform): Tree = { + implicit val pos = preTrans.pos + preTrans match { + case PreTransBlock(stats, result) => + Block(stats :+ finishTransformExpr(result)) + case PreTransLocalDef(localDef) => + localDef.newReplacement + case PreTransRecordTree(_, _, cancelFun) => + cancelFun() + case PreTransTree(tree, _) => + tree + } + } + + /** Finishes a statement pretransform to get a normal [[Tree]]. + * This method (together with finishTransformExpr) must not be called more + * than once per pretransform and per translation. + * By "per translation", we mean in an alternative path through + * `tryOrRollback`. It could still be called several times as long as + * it is once in the 'try' part and once in the 'fallback' part. + */ + private def finishTransformStat(stat: PreTransform): Tree = stat match { + case PreTransBlock(stats, result) => + Block(stats :+ finishTransformStat(result))(stat.pos) + case PreTransLocalDef(_) => + Skip()(stat.pos) + case PreTransRecordTree(tree, _, _) => + keepOnlySideEffects(tree) + case PreTransTree(tree, _) => + keepOnlySideEffects(tree) + } + + /** Keeps only the side effects of a Tree (overapproximation). */ + private def keepOnlySideEffects(stat: Tree): Tree = stat match { + case _:VarRef | _:This | _:Literal => + Skip()(stat.pos) + case Block(init :+ last) => + Block(init :+ keepOnlySideEffects(last))(stat.pos) + case LoadModule(ClassType(moduleClassName)) => + if (hasElidableModuleAccessor(moduleClassName)) Skip()(stat.pos) + else stat + case Select(LoadModule(ClassType(moduleClassName)), _, _) => + if (hasElidableModuleAccessor(moduleClassName)) Skip()(stat.pos) + else stat + case Closure(_, _, _, captureValues) => + Block(captureValues.map(keepOnlySideEffects))(stat.pos) + case UnaryOp(_, arg) => + keepOnlySideEffects(arg) + case If(cond, thenp, elsep) => + (keepOnlySideEffects(thenp), keepOnlySideEffects(elsep)) match { + case (Skip(), Skip()) => keepOnlySideEffects(cond) + case (newThenp, newElsep) => If(cond, newThenp, newElsep)(NoType)(stat.pos) + } + case BinaryOp(_, lhs, rhs) => + Block(keepOnlySideEffects(lhs), keepOnlySideEffects(rhs))(stat.pos) + case RecordValue(_, elems) => + Block(elems.map(keepOnlySideEffects))(stat.pos) + case _ => + stat + } + + private def pretransformApply(tree: Apply, isStat: Boolean, + usePreTransform: Boolean)( + cont: PreTransCont)( + implicit scope: Scope): TailRec[Tree] = { + val Apply(receiver, methodIdent @ Ident(methodName, _), args) = tree + implicit val pos = tree.pos + + pretransformExpr(receiver) { treceiver => + def treeNotInlined0(transformedArgs: List[Tree]) = + cont(PreTransTree(Apply(finishTransformExpr(treceiver), methodIdent, + transformedArgs)(tree.tpe)(tree.pos), RefinedType(tree.tpe))) + + def treeNotInlined = treeNotInlined0(args.map(transformExpr)) + + treceiver.tpe.base match { + case NothingType => + cont(treceiver) + case NullType => + cont(PreTransTree(Block( + finishTransformStat(treceiver), + CallHelper("throwNullPointerException")(NothingType)))) + case _ => + if (isReflProxyName(methodName)) { + // Never inline reflective proxies + treeNotInlined + } else { + val cls = boxedClassForType(treceiver.tpe.base) + val impls = + if (treceiver.tpe.isExact) staticCall(cls, methodName).toList + else dynamicCall(cls, methodName) + val allocationSite = treceiver.tpe.allocationSite + if (impls.isEmpty || impls.exists(impl => + scope.implsBeingInlined((allocationSite, impl)))) { + // isEmpty could happen, have to leave it as is for the TypeError + treeNotInlined + } else if (impls.size == 1) { + val target = impls.head + pretransformExprs(args) { targs => + val intrinsicCode = getIntrinsicCode(target) + if (intrinsicCode >= 0) { + callIntrinsic(intrinsicCode, Some(treceiver), targs, + isStat, usePreTransform)(cont) + } else if (target.inlineable || shouldInlineBecauseOfArgs(treceiver :: targs)) { + inline(allocationSite, Some(treceiver), targs, target, + isStat, usePreTransform)(cont) + } else { + treeNotInlined0(targs.map(finishTransformExpr)) + } + } + } else { + if (impls.forall(_.isTraitImplForwarder)) { + val reference = impls.head + val TraitImplApply(ClassType(traitImpl), Ident(methodName, _), _) = + getMethodBody(reference).body + if (!impls.tail.forall(getMethodBody(_).body match { + case TraitImplApply(ClassType(`traitImpl`), + Ident(`methodName`, _), _) => true + case _ => false + })) { + // Not all calling the same method in the same trait impl + treeNotInlined + } else { + pretransformExprs(args) { targs => + inline(allocationSite, Some(treceiver), targs, reference, + isStat, usePreTransform)(cont) + } + } + } else { + // TODO? Inline multiple non-trait-impl-forwarder with the exact same body? + treeNotInlined + } + } + } + } + } + } + + private def boxedClassForType(tpe: Type): String = (tpe: @unchecked) match { + case ClassType(cls) => cls + case AnyType => Definitions.ObjectClass + case UndefType => Definitions.BoxedUnitClass + case BooleanType => Definitions.BoxedBooleanClass + case IntType => Definitions.BoxedIntegerClass + case LongType => Definitions.BoxedLongClass + case FloatType => Definitions.BoxedFloatClass + case DoubleType => Definitions.BoxedDoubleClass + case StringType => Definitions.StringClass + case ArrayType(_, _) => Definitions.ObjectClass + } + + private def pretransformStaticApply(tree: StaticApply, isStat: Boolean, + usePreTransform: Boolean)( + cont: PreTransCont)( + implicit scope: Scope): TailRec[Tree] = { + val StaticApply(receiver, clsType @ ClassType(cls), + methodIdent @ Ident(methodName, _), args) = tree + implicit val pos = tree.pos + + def treeNotInlined0(transformedReceiver: Tree, transformedArgs: List[Tree]) = + cont(PreTransTree(StaticApply(transformedReceiver, clsType, + methodIdent, transformedArgs)(tree.tpe), RefinedType(tree.tpe))) + + def treeNotInlined = + treeNotInlined0(transformExpr(receiver), args.map(transformExpr)) + + if (isReflProxyName(methodName)) { + // Never inline reflective proxies + treeNotInlined + } else { + val optTarget = staticCall(cls, methodName) + if (optTarget.isEmpty) { + // just in case + treeNotInlined + } else { + val target = optTarget.get + pretransformExprs(receiver, args) { (treceiver, targs) => + val intrinsicCode = getIntrinsicCode(target) + if (intrinsicCode >= 0) { + callIntrinsic(intrinsicCode, Some(treceiver), targs, + isStat, usePreTransform)(cont) + } else { + val shouldInline = + target.inlineable || shouldInlineBecauseOfArgs(treceiver :: targs) + val allocationSite = treceiver.tpe.allocationSite + val beingInlined = + scope.implsBeingInlined((allocationSite, target)) + + if (shouldInline && !beingInlined) { + inline(allocationSite, Some(treceiver), targs, target, + isStat, usePreTransform)(cont) + } else { + treeNotInlined0(finishTransformExpr(treceiver), + targs.map(finishTransformExpr)) + } + } + } + } + } + } + + private def pretransformTraitImplApply(tree: TraitImplApply, isStat: Boolean, + usePreTransform: Boolean)( + cont: PreTransCont)( + implicit scope: Scope): TailRec[Tree] = { + val TraitImplApply(implType @ ClassType(impl), + methodIdent @ Ident(methodName, _), args) = tree + implicit val pos = tree.pos + + def treeNotInlined0(transformedArgs: List[Tree]) = + cont(PreTransTree(TraitImplApply(implType, methodIdent, + transformedArgs)(tree.tpe), RefinedType(tree.tpe))) + + def treeNotInlined = treeNotInlined0(args.map(transformExpr)) + + val optTarget = traitImplCall(impl, methodName) + if (optTarget.isEmpty) { + // just in case + treeNotInlined + } else { + val target = optTarget.get + pretransformExprs(args) { targs => + val intrinsicCode = getIntrinsicCode(target) + if (intrinsicCode >= 0) { + callIntrinsic(intrinsicCode, None, targs, + isStat, usePreTransform)(cont) + } else { + val shouldInline = + target.inlineable || shouldInlineBecauseOfArgs(targs) + val allocationSite = targs.headOption.flatMap(_.tpe.allocationSite) + val beingInlined = + scope.implsBeingInlined((allocationSite, target)) + + if (shouldInline && !beingInlined) { + inline(allocationSite, None, targs, target, + isStat, usePreTransform)(cont) + } else { + treeNotInlined0(targs.map(finishTransformExpr)) + } + } + } + } + } + + private def pretransformJSFunctionApply(tree: JSFunctionApply, + isStat: Boolean, usePreTransform: Boolean)( + cont: PreTransCont)( + implicit scope: Scope, pos: Position): TailRec[Tree] = { + val JSFunctionApply(fun, args) = tree + implicit val pos = tree.pos + + pretransformExpr(fun) { tfun => + tfun match { + case PreTransLocalDef(LocalDef(_, false, + closure @ TentativeClosureReplacement( + captureParams, params, body, captureLocalDefs, + alreadyUsed, cancelFun))) if !alreadyUsed.value => + alreadyUsed.value = true + pretransformExprs(args) { targs => + inlineBody( + Some(PreTransTree(Undefined())), // `this` is `undefined` + captureParams ++ params, AnyType, body, + captureLocalDefs.map(PreTransLocalDef(_)) ++ targs, isStat, + usePreTransform)(cont) + } + + case _ => + cont(PreTransTree( + JSFunctionApply(finishTransformExpr(tfun), args.map(transformExpr)))) + } + } + } + + private def shouldInlineBecauseOfArgs( + receiverAndArgs: List[PreTransform]): Boolean = { + def isLikelyOptimizable(arg: PreTransform): Boolean = arg match { + case PreTransBlock(_, result) => + isLikelyOptimizable(result) + + case PreTransLocalDef(localDef) => + localDef.replacement match { + case TentativeClosureReplacement(_, _, _, _, _, _) => true + case ReplaceWithRecordVarRef(_, _, _, _, _) => true + case InlineClassBeingConstructedReplacement(_, _) => true + case InlineClassInstanceReplacement(_, _, _) => true + case _ => false + } + + case PreTransRecordTree(_, _, _) => + true + + case _ => + arg.tpe.base match { + case ClassType("s_Predef$$less$colon$less" | "s_Predef$$eq$colon$eq") => + true + case _ => + false + } + } + receiverAndArgs.exists(isLikelyOptimizable) + } + + private def inline(allocationSite: Option[AllocationSite], + optReceiver: Option[PreTransform], + args: List[PreTransform], target: MethodID, isStat: Boolean, + usePreTransform: Boolean)( + cont: PreTransCont)( + implicit scope: Scope, pos: Position): TailRec[Tree] = { + + attemptedInlining += target + + val MethodDef(_, formals, resultType, body) = getMethodBody(target) + + body match { + case Skip() => + assert(isStat, "Found Skip() in expression position") + cont(PreTransTree( + Block((optReceiver ++: args).map(finishTransformStat)), + RefinedType.NoRefinedType)) + + case _: Literal => + cont(PreTransTree( + Block((optReceiver ++: args).map(finishTransformStat) :+ body), + RefinedType(body.tpe))) + + case This() if args.isEmpty => + assert(optReceiver.isDefined, + "There was a This(), there should be a receiver") + cont(optReceiver.get) + + case Select(This(), field, mutable) if formals.isEmpty => + assert(optReceiver.isDefined, + "There was a This(), there should be a receiver") + pretransformSelectCommon(body.tpe, optReceiver.get, field, mutable, + isLhsOfAssign = false)(cont) + + case Assign(lhs @ Select(This(), field, mutable), VarRef(Ident(rhsName, _), _)) + if formals.size == 1 && formals.head.name.name == rhsName => + assert(isStat, "Found Assign in expression position") + assert(optReceiver.isDefined, + "There was a This(), there should be a receiver") + pretransformSelectCommon(lhs.tpe, optReceiver.get, field, mutable, + isLhsOfAssign = true) { preTransLhs => + // TODO Support assignment of record + cont(PreTransTree( + Assign(finishTransformExpr(preTransLhs), + finishTransformExpr(args.head)), + RefinedType.NoRefinedType)) + } + + case _ => + val targetID = (allocationSite, target) + inlineBody(optReceiver, formals, resultType, body, args, isStat, + usePreTransform)(cont)(scope.inlining(targetID), pos) + } + } + + private def inlineBody(optReceiver: Option[PreTransform], + formals: List[ParamDef], resultType: Type, body: Tree, + args: List[PreTransform], isStat: Boolean, + usePreTransform: Boolean)( + cont: PreTransCont)( + implicit scope: Scope, pos: Position): TailRec[Tree] = tailcall { + + val optReceiverBinding = optReceiver map { receiver => + Binding("this", None, receiver.tpe.base, false, receiver) + } + + val argsBindings = for { + (ParamDef(Ident(name, originalName), tpe, mutable), arg) <- formals zip args + } yield { + Binding(name, originalName, tpe, mutable, arg) + } + + withBindings(optReceiverBinding ++: argsBindings) { (bodyScope, cont1) => + returnable("", resultType, body, isStat, usePreTransform)( + cont1)(bodyScope, pos) + } (cont) (scope.withEnv(OptEnv.Empty)) + } + + private def callIntrinsic(code: Int, optTReceiver: Option[PreTransform], + targs: List[PreTransform], isStat: Boolean, usePreTransform: Boolean)( + cont: PreTransCont)( + implicit pos: Position): TailRec[Tree] = { + + import Intrinsics._ + + implicit def string2ident(s: String): Ident = Ident(s, None) + + lazy val newArgs = targs.map(finishTransformExpr) + + @inline def contTree(result: Tree) = cont(PreTransTree(result)) + + @inline def StringClassType = ClassType(Definitions.StringClass) + + def asRTLong(arg: Tree): Tree = + AsInstanceOf(arg, ClassType(LongImpl.RuntimeLongClass)) + def firstArgAsRTLong: Tree = + asRTLong(newArgs.head) + + (code: @switch) match { + // java.lang.System + + case ArrayCopy => + assert(isStat, "System.arraycopy must be used in statement position") + contTree(CallHelper("systemArraycopy", newArgs)(NoType)) + case IdentityHashCode => + contTree(CallHelper("systemIdentityHashCode", newArgs)(IntType)) + + // scala.scalajs.runtime package object + + case PropertiesOf => + contTree(CallHelper("propertiesOf", newArgs)(AnyType)) + + // java.lang.Long + + case LongToString => + contTree(Apply(firstArgAsRTLong, "toString__T", Nil)(StringClassType)) + case LongCompare => + contTree(Apply(firstArgAsRTLong, "compareTo__sjsr_RuntimeLong__I", + List(asRTLong(newArgs(1))))(IntType)) + case LongBitCount => + contTree(Apply(firstArgAsRTLong, LongImpl.bitCount, Nil)(IntType)) + case LongSignum => + contTree(Apply(firstArgAsRTLong, LongImpl.signum, Nil)(LongType)) + case LongLeading0s => + contTree(Apply(firstArgAsRTLong, LongImpl.numberOfLeadingZeros, Nil)(IntType)) + case LongTrailing0s => + contTree(Apply(firstArgAsRTLong, LongImpl.numberOfTrailingZeros, Nil)(IntType)) + case LongToBinStr => + contTree(Apply(firstArgAsRTLong, LongImpl.toBinaryString, Nil)(StringClassType)) + case LongToHexStr => + contTree(Apply(firstArgAsRTLong, LongImpl.toHexString, Nil)(StringClassType)) + case LongToOctalStr => + contTree(Apply(firstArgAsRTLong, LongImpl.toOctalString, Nil)(StringClassType)) + + // TypedArray conversions + + case ByteArrayToInt8Array => + contTree(CallHelper("byteArray2TypedArray", newArgs)(AnyType)) + case ShortArrayToInt16Array => + contTree(CallHelper("shortArray2TypedArray", newArgs)(AnyType)) + case CharArrayToUint16Array => + contTree(CallHelper("charArray2TypedArray", newArgs)(AnyType)) + case IntArrayToInt32Array => + contTree(CallHelper("intArray2TypedArray", newArgs)(AnyType)) + case FloatArrayToFloat32Array => + contTree(CallHelper("floatArray2TypedArray", newArgs)(AnyType)) + case DoubleArrayToFloat64Array => + contTree(CallHelper("doubleArray2TypedArray", newArgs)(AnyType)) + + case Int8ArrayToByteArray => + contTree(CallHelper("typedArray2ByteArray", newArgs)(AnyType)) + case Int16ArrayToShortArray => + contTree(CallHelper("typedArray2ShortArray", newArgs)(AnyType)) + case Uint16ArrayToCharArray => + contTree(CallHelper("typedArray2CharArray", newArgs)(AnyType)) + case Int32ArrayToIntArray => + contTree(CallHelper("typedArray2IntArray", newArgs)(AnyType)) + case Float32ArrayToFloatArray => + contTree(CallHelper("typedArray2FloatArray", newArgs)(AnyType)) + case Float64ArrayToDoubleArray => + contTree(CallHelper("typedArray2DoubleArray", newArgs)(AnyType)) + } + } + + private def inlineClassConstructor(allocationSite: AllocationSite, + cls: ClassType, initialValue: RecordValue, + ctor: Ident, args: List[PreTransform], cancelFun: CancelFun)( + cont: PreTransCont)( + implicit scope: Scope, pos: Position): TailRec[Tree] = { + + val RecordValue(recordType, initialFieldValues) = initialValue + + pretransformExprs(initialFieldValues) { tinitialFieldValues => + val initialFieldBindings = for { + (RecordType.Field(name, originalName, tpe, mutable), value) <- + recordType.fields zip tinitialFieldValues + } yield { + Binding(name, originalName, tpe, mutable, value) + } + + withNewLocalDefs(initialFieldBindings) { (initialFieldLocalDefList, cont1) => + val fieldNames = initialValue.tpe.fields.map(_.name) + val initialFieldLocalDefs = + Map(fieldNames zip initialFieldLocalDefList: _*) + + inlineClassConstructorBody(allocationSite, initialFieldLocalDefs, + cls, cls, ctor, args, cancelFun) { (finalFieldLocalDefs, cont2) => + cont2(PreTransLocalDef(LocalDef( + RefinedType(cls, isExact = true, isNullable = false, + allocationSite = Some(allocationSite)), + mutable = false, + InlineClassInstanceReplacement(recordType, finalFieldLocalDefs, cancelFun)))) + } (cont1) + } (cont) + } + } + + private def inlineClassConstructorBody( + allocationSite: AllocationSite, + inputFieldsLocalDefs: Map[String, LocalDef], cls: ClassType, + ctorClass: ClassType, ctor: Ident, args: List[PreTransform], + cancelFun: CancelFun)( + buildInner: (Map[String, LocalDef], PreTransCont) => TailRec[Tree])( + cont: PreTransCont)( + implicit scope: Scope): TailRec[Tree] = tailcall { + + val target = staticCall(ctorClass.className, ctor.name).getOrElse(cancelFun()) + val targetID = (Some(allocationSite), target) + if (scope.implsBeingInlined.contains(targetID)) + cancelFun() + + val MethodDef(_, formals, _, BlockOrAlone(stats, This())) = + getMethodBody(target) + + val argsBindings = for { + (ParamDef(Ident(name, originalName), tpe, mutable), arg) <- formals zip args + } yield { + Binding(name, originalName, tpe, mutable, arg) + } + + withBindings(argsBindings) { (bodyScope, cont1) => + val thisLocalDef = LocalDef( + RefinedType(cls, isExact = true, isNullable = false), false, + InlineClassBeingConstructedReplacement(inputFieldsLocalDefs, cancelFun)) + val statsScope = bodyScope.inlining(targetID).withEnv( + bodyScope.env.withLocalDef("this", thisLocalDef)) + inlineClassConstructorBodyList(allocationSite, thisLocalDef, + inputFieldsLocalDefs, cls, stats, cancelFun)( + buildInner)(cont1)(statsScope) + } (cont) (scope.withEnv(OptEnv.Empty)) + } + + private def inlineClassConstructorBodyList( + allocationSite: AllocationSite, + thisLocalDef: LocalDef, inputFieldsLocalDefs: Map[String, LocalDef], + cls: ClassType, stats: List[Tree], cancelFun: CancelFun)( + buildInner: (Map[String, LocalDef], PreTransCont) => TailRec[Tree])( + cont: PreTransCont)( + implicit scope: Scope): TailRec[Tree] = { + stats match { + case This() :: rest => + inlineClassConstructorBodyList(allocationSite, thisLocalDef, + inputFieldsLocalDefs, cls, rest, cancelFun)(buildInner)(cont) + + case Assign(s @ Select(ths: This, + Ident(fieldName, fieldOrigName), false), value) :: rest => + pretransformExpr(value) { tvalue => + withNewLocalDef(Binding(fieldName, fieldOrigName, s.tpe, false, + tvalue)) { (localDef, cont1) => + if (localDef.contains(thisLocalDef)) { + /* Uh oh, there is a `val x = ...this...`. We can't keep it, + * because this field will not be updated with `newThisLocalDef`. + */ + cancelFun() + } + val newFieldsLocalDefs = + inputFieldsLocalDefs.updated(fieldName, localDef) + val newThisLocalDef = LocalDef( + RefinedType(cls, isExact = true, isNullable = false), false, + InlineClassBeingConstructedReplacement(newFieldsLocalDefs, cancelFun)) + val restScope = scope.withEnv(scope.env.withLocalDef( + "this", newThisLocalDef)) + inlineClassConstructorBodyList(allocationSite, + newThisLocalDef, newFieldsLocalDefs, cls, rest, cancelFun)( + buildInner)(cont1)(restScope) + } (cont) + } + + /* if (cond) + * throw e + * else + * this.outer = value + * + * becomes + * + * this.outer = + * if (cond) throw e + * else value + * + * Typical shape of initialization of outer pointer of inner classes. + */ + case If(cond, th: Throw, + Assign(Select(This(), _, false), value)) :: rest => + // work around a bug of the compiler (these should be @-bindings) + val stat = stats.head.asInstanceOf[If] + val ass = stat.elsep.asInstanceOf[Assign] + val lhs = ass.lhs + inlineClassConstructorBodyList(allocationSite, thisLocalDef, + inputFieldsLocalDefs, cls, + Assign(lhs, If(cond, th, value)(lhs.tpe)(stat.pos))(ass.pos) :: rest, + cancelFun)(buildInner)(cont) + + case StaticApply(ths: This, superClass, superCtor, args) :: rest + if isConstructorName(superCtor.name) => + pretransformExprs(args) { targs => + inlineClassConstructorBody(allocationSite, inputFieldsLocalDefs, + cls, superClass, superCtor, targs, + cancelFun) { (outputFieldsLocalDefs, cont1) => + val newThisLocalDef = LocalDef( + RefinedType(cls, isExact = true, isNullable = false), false, + InlineClassBeingConstructedReplacement(outputFieldsLocalDefs, cancelFun)) + val restScope = scope.withEnv(scope.env.withLocalDef( + "this", newThisLocalDef)) + inlineClassConstructorBodyList(allocationSite, + newThisLocalDef, outputFieldsLocalDefs, + cls, rest, cancelFun)(buildInner)(cont1)(restScope) + } (cont) + } + + case VarDef(Ident(name, originalName), tpe, mutable, rhs) :: rest => + pretransformExpr(rhs) { trhs => + withBinding(Binding(name, originalName, tpe, mutable, trhs)) { (restScope, cont1) => + inlineClassConstructorBodyList(allocationSite, + thisLocalDef, inputFieldsLocalDefs, + cls, rest, cancelFun)(buildInner)(cont1)(restScope) + } (cont) + } + + case stat :: rest => + val transformedStat = transformStat(stat) + transformedStat match { + case Skip() => + inlineClassConstructorBodyList(allocationSite, + thisLocalDef, inputFieldsLocalDefs, + cls, rest, cancelFun)(buildInner)(cont) + case _ => + if (transformedStat.tpe == NothingType) + cont(PreTransTree(transformedStat, RefinedType.Nothing)) + else { + inlineClassConstructorBodyList(allocationSite, + thisLocalDef, inputFieldsLocalDefs, + cls, rest, cancelFun) { (outputFieldsLocalDefs, cont1) => + buildInner(outputFieldsLocalDefs, { tinner => + cont1(PreTransBlock(transformedStat :: Nil, tinner)) + }) + }(cont) + } + } + + case Nil => + buildInner(inputFieldsLocalDefs, cont) + } + } + + private def foldIf(cond: Tree, thenp: Tree, elsep: Tree)(tpe: Type)( + implicit pos: Position): Tree = { + import BinaryOp._ + + @inline def default = If(cond, thenp, elsep)(tpe) + cond match { + case BooleanLiteral(v) => + if (v) thenp + else elsep + + case _ => + @inline def negCond = foldUnaryOp(UnaryOp.Boolean_!, cond) + if (thenp.tpe == BooleanType && elsep.tpe == BooleanType) { + (cond, thenp, elsep) match { + case (_, BooleanLiteral(t), BooleanLiteral(e)) => + if (t == e) Block(keepOnlySideEffects(cond), thenp) + else if (t) cond + else negCond + + case (_, BooleanLiteral(false), _) => + foldIf(negCond, elsep, BooleanLiteral(false))(tpe) // canonical && form + case (_, _, BooleanLiteral(true)) => + foldIf(negCond, BooleanLiteral(true), thenp)(tpe) // canonical || form + + /* if (lhs === null) rhs === null else lhs === rhs + * -> lhs === rhs + * This is the typical shape of a lhs == rhs test where + * the equals() method has been inlined as a reference + * equality test. + */ + case (BinaryOp(BinaryOp.===, VarRef(lhsIdent, _), Null()), + BinaryOp(BinaryOp.===, VarRef(rhsIdent, _), Null()), + BinaryOp(BinaryOp.===, VarRef(lhsIdent2, _), VarRef(rhsIdent2, _))) + if lhsIdent2 == lhsIdent && rhsIdent2 == rhsIdent => + elsep + + // Example: (x > y) || (x == y) -> (x >= y) + case (BinaryOp(op1 @ (Num_== | Num_!= | Num_< | Num_<= | Num_> | Num_>=), l1, r1), + BooleanLiteral(true), + BinaryOp(op2 @ (Num_== | Num_!= | Num_< | Num_<= | Num_> | Num_>=), l2, r2)) + if ((l1.isInstanceOf[Literal] || l1.isInstanceOf[VarRef]) && + (r1.isInstanceOf[Literal] || r1.isInstanceOf[VarRef]) && + (l1 == l2 && r1 == r2)) => + val canBeEqual = + ((op1 == Num_==) || (op1 == Num_<=) || (op1 == Num_>=)) || + ((op2 == Num_==) || (op2 == Num_<=) || (op2 == Num_>=)) + val canBeLessThan = + ((op1 == Num_!=) || (op1 == Num_<) || (op1 == Num_<=)) || + ((op2 == Num_!=) || (op2 == Num_<) || (op2 == Num_<=)) + val canBeGreaterThan = + ((op1 == Num_!=) || (op1 == Num_>) || (op1 == Num_>=)) || + ((op2 == Num_!=) || (op2 == Num_>) || (op2 == Num_>=)) + + fold3WayComparison(canBeEqual, canBeLessThan, canBeGreaterThan, l1, r1) + + // Example: (x >= y) && (x <= y) -> (x == y) + case (BinaryOp(op1 @ (Num_== | Num_!= | Num_< | Num_<= | Num_> | Num_>=), l1, r1), + BinaryOp(op2 @ (Num_== | Num_!= | Num_< | Num_<= | Num_> | Num_>=), l2, r2), + BooleanLiteral(false)) + if ((l1.isInstanceOf[Literal] || l1.isInstanceOf[VarRef]) && + (r1.isInstanceOf[Literal] || r1.isInstanceOf[VarRef]) && + (l1 == l2 && r1 == r2)) => + val canBeEqual = + ((op1 == Num_==) || (op1 == Num_<=) || (op1 == Num_>=)) && + ((op2 == Num_==) || (op2 == Num_<=) || (op2 == Num_>=)) + val canBeLessThan = + ((op1 == Num_!=) || (op1 == Num_<) || (op1 == Num_<=)) && + ((op2 == Num_!=) || (op2 == Num_<) || (op2 == Num_<=)) + val canBeGreaterThan = + ((op1 == Num_!=) || (op1 == Num_>) || (op1 == Num_>=)) && + ((op2 == Num_!=) || (op2 == Num_>) || (op2 == Num_>=)) + + fold3WayComparison(canBeEqual, canBeLessThan, canBeGreaterThan, l1, r1) + + case _ => default + } + } else { + (thenp, elsep) match { + case (Skip(), Skip()) => keepOnlySideEffects(cond) + case (Skip(), _) => foldIf(negCond, elsep, thenp)(tpe) + + case _ => default + } + } + } + } + + private def transformUnaryOp(tree: UnaryOp)(implicit scope: Scope): Tree = { + import UnaryOp._ + + implicit val pos = tree.pos + val UnaryOp(op, arg) = tree + + (op: @switch) match { + case LongToInt => + trampoline { + pretransformExpr(arg) { (targ) => + TailCalls.done { + foldUnaryOp(op, finishTransformOptLongExpr(targ)) + } + } + } + + case _ => + foldUnaryOp(op, transformExpr(arg)) + } + } + + private def transformBinaryOp(tree: BinaryOp)(implicit scope: Scope): Tree = { + import BinaryOp._ + + implicit val pos = tree.pos + val BinaryOp(op, lhs, rhs) = tree + + (op: @switch) match { + case === | !== => + trampoline { + pretransformExprs(lhs, rhs) { (tlhs, trhs) => + TailCalls.done(foldReferenceEquality(tlhs, trhs, op == ===)) + } + } + + case Long_== | Long_!= | Long_< | Long_<= | Long_> | Long_>= => + trampoline { + pretransformExprs(lhs, rhs) { (tlhs, trhs) => + TailCalls.done { + if (isLiteralOrOptimizableLong(tlhs) && + isLiteralOrOptimizableLong(trhs)) { + foldBinaryOp(op, finishTransformOptLongExpr(tlhs), + finishTransformOptLongExpr(trhs)) + } else { + foldBinaryOp(op, finishTransformExpr(tlhs), + finishTransformExpr(trhs)) + } + } + } + } + + case _ => + foldBinaryOp(op, transformExpr(lhs), transformExpr(rhs)) + } + } + + private def isLiteralOrOptimizableLong(texpr: PreTransform): Boolean = { + texpr match { + case PreTransTree(LongLiteral(_), _) => + true + case PreTransLocalDef(LocalDef(_, _, replacement)) => + replacement match { + case ReplaceWithVarRef(_, _, _, Some(_)) => true + case ReplaceWithConstant(LongLiteral(_)) => true + case _ => false + } + case _ => + false + } + } + + private def finishTransformOptLongExpr(targ: PreTransform): Tree = targ match { + case PreTransLocalDef(LocalDef(tpe, false, + ReplaceWithVarRef(_, _, _, Some(argValue)))) => + argValue() + case _ => + finishTransformExpr(targ) + } + + private def foldUnaryOp(op: UnaryOp.Code, arg: Tree)( + implicit pos: Position): Tree = { + import UnaryOp._ + @inline def default = UnaryOp(op, arg) + (op: @switch) match { + case Boolean_! => + arg match { + case BooleanLiteral(v) => BooleanLiteral(!v) + case UnaryOp(Boolean_!, x) => x + + case BinaryOp(innerOp, l, r) => + val newOp = (innerOp: @switch) match { + case BinaryOp.=== => BinaryOp.!== + case BinaryOp.!== => BinaryOp.=== + + case BinaryOp.Num_== => BinaryOp.Num_!= + case BinaryOp.Num_!= => BinaryOp.Num_== + case BinaryOp.Num_< => BinaryOp.Num_>= + case BinaryOp.Num_<= => BinaryOp.Num_> + case BinaryOp.Num_> => BinaryOp.Num_<= + case BinaryOp.Num_>= => BinaryOp.Num_< + + case BinaryOp.Long_== => BinaryOp.Long_!= + case BinaryOp.Long_!= => BinaryOp.Long_== + case BinaryOp.Long_< => BinaryOp.Long_>= + case BinaryOp.Long_<= => BinaryOp.Long_> + case BinaryOp.Long_> => BinaryOp.Long_<= + case BinaryOp.Long_>= => BinaryOp.Long_< + + case BinaryOp.Boolean_== => BinaryOp.Boolean_!= + case BinaryOp.Boolean_!= => BinaryOp.Boolean_== + + case _ => -1 + } + if (newOp == -1) default + else BinaryOp(newOp, l, r) + + case _ => default + } + + case IntToLong => + arg match { + case IntLiteral(v) => LongLiteral(v.toLong) + case _ => default + } + + case LongToInt => + arg match { + case LongLiteral(v) => IntLiteral(v.toInt) + case UnaryOp(IntToLong, x) => x + + case BinaryOp(BinaryOp.Long_+, x, y) => + foldBinaryOp(BinaryOp.Int_+, + foldUnaryOp(LongToInt, x), + foldUnaryOp(LongToInt, y)) + case BinaryOp(BinaryOp.Long_-, x, y) => + foldBinaryOp(BinaryOp.Int_-, + foldUnaryOp(LongToInt, x), + foldUnaryOp(LongToInt, y)) + + case _ => default + } + + case LongToDouble => + arg match { + case LongLiteral(v) => DoubleLiteral(v.toDouble) + case _ => default + } + case DoubleToInt => + arg match { + case _ if arg.tpe == IntType => arg + case NumberLiteral(v) => IntLiteral(v.toInt) + case _ => default + } + case DoubleToFloat => + arg match { + case _ if arg.tpe == FloatType => arg + case NumberLiteral(v) => FloatLiteral(v.toFloat) + case _ => default + } + case DoubleToLong => + arg match { + case _ if arg.tpe == IntType => foldUnaryOp(IntToLong, arg) + case NumberLiteral(v) => LongLiteral(v.toLong) + case _ => default + } + case _ => + default + } + } + + /** Performs === for two literals. + * The result is always known statically. + */ + private def literal_===(lhs: Literal, rhs: Literal): Boolean = { + (lhs, rhs) match { + case (IntLiteral(l), IntLiteral(r)) => l == r + case (FloatLiteral(l), FloatLiteral(r)) => l == r + case (NumberLiteral(l), NumberLiteral(r)) => l == r + case (LongLiteral(l), LongLiteral(r)) => l == r + case (BooleanLiteral(l), BooleanLiteral(r)) => l == r + case (StringLiteral(l), StringLiteral(r)) => l == r + case (Undefined(), Undefined()) => true + case (Null(), Null()) => true + case _ => false + } + } + + private def foldBinaryOp(op: BinaryOp.Code, lhs: Tree, rhs: Tree)( + implicit pos: Position): Tree = { + import BinaryOp._ + @inline def default = BinaryOp(op, lhs, rhs) + (op: @switch) match { + case === | !== => + val positive = (op == ===) + (lhs, rhs) match { + case (lhs: Literal, rhs: Literal) => + BooleanLiteral(literal_===(lhs, rhs) == positive) + + case (_: Literal, _) => foldBinaryOp(op, rhs, lhs) + case _ => default + } + + case Int_+ => + (lhs, rhs) match { + case (IntLiteral(l), IntLiteral(r)) => IntLiteral(l + r) + case (_, IntLiteral(_)) => foldBinaryOp(Int_+, rhs, lhs) + case (IntLiteral(0), _) => rhs + + case (IntLiteral(x), + BinaryOp(innerOp @ (Int_+ | Int_-), IntLiteral(y), z)) => + foldBinaryOp(innerOp, IntLiteral(x+y), z) + + case _ => default + } + + case Int_- => + (lhs, rhs) match { + case (_, IntLiteral(r)) => foldBinaryOp(Int_+, lhs, IntLiteral(-r)) + + case (IntLiteral(x), BinaryOp(Int_+, IntLiteral(y), z)) => + foldBinaryOp(Int_-, IntLiteral(x-y), z) + case (IntLiteral(x), BinaryOp(Int_-, IntLiteral(y), z)) => + foldBinaryOp(Int_+, IntLiteral(x-y), z) + + case (_, BinaryOp(Int_-, IntLiteral(0), x)) => + foldBinaryOp(Int_+, lhs, x) + + case _ => default + } + + case Int_* => + (lhs, rhs) match { + case (IntLiteral(l), IntLiteral(r)) => IntLiteral(l * r) + case (_, IntLiteral(_)) => foldBinaryOp(Int_*, rhs, lhs) + + case (IntLiteral(1), _) => rhs + case (IntLiteral(-1), _) => foldBinaryOp(Int_-, IntLiteral(0), lhs) + + case _ => default + } + + case Int_/ => + (lhs, rhs) match { + case (IntLiteral(l), IntLiteral(r)) if r != 0 => IntLiteral(l / r) + + case (_, IntLiteral(1)) => lhs + case (_, IntLiteral(-1)) => foldBinaryOp(Int_-, IntLiteral(0), lhs) + + case _ => default + } + + case Int_% => + (lhs, rhs) match { + case (IntLiteral(l), IntLiteral(r)) if r != 0 => IntLiteral(l % r) + case (_, IntLiteral(1 | -1)) => + Block(keepOnlySideEffects(lhs), IntLiteral(0)) + case _ => default + } + + case Int_| => + (lhs, rhs) match { + case (IntLiteral(l), IntLiteral(r)) => IntLiteral(l | r) + case (_, IntLiteral(_)) => foldBinaryOp(Int_|, rhs, lhs) + case (IntLiteral(0), _) => rhs + + case (IntLiteral(x), BinaryOp(Int_|, IntLiteral(y), z)) => + foldBinaryOp(Int_|, IntLiteral(x | y), z) + + case _ => default + } + + case Int_& => + (lhs, rhs) match { + case (IntLiteral(l), IntLiteral(r)) => IntLiteral(l & r) + case (_, IntLiteral(_)) => foldBinaryOp(Int_&, rhs, lhs) + case (IntLiteral(-1), _) => rhs + + case (IntLiteral(x), BinaryOp(Int_&, IntLiteral(y), z)) => + foldBinaryOp(Int_&, IntLiteral(x & y), z) + + case _ => default + } + + case Int_^ => + (lhs, rhs) match { + case (IntLiteral(l), IntLiteral(r)) => IntLiteral(l ^ r) + case (_, IntLiteral(_)) => foldBinaryOp(Int_^, rhs, lhs) + case (IntLiteral(0), _) => rhs + + case (IntLiteral(x), BinaryOp(Int_^, IntLiteral(y), z)) => + foldBinaryOp(Int_^, IntLiteral(x ^ y), z) + + case _ => default + } + + case Int_<< => + (lhs, rhs) match { + case (IntLiteral(l), IntLiteral(r)) => IntLiteral(l << r) + case (_, IntLiteral(x)) if x % 32 == 0 => lhs + case _ => default + } + + case Int_>>> => + (lhs, rhs) match { + case (IntLiteral(l), IntLiteral(r)) => IntLiteral(l >>> r) + case (_, IntLiteral(x)) if x % 32 == 0 => lhs + case _ => default + } + + case Int_>> => + (lhs, rhs) match { + case (IntLiteral(l), IntLiteral(r)) => IntLiteral(l >> r) + case (_, IntLiteral(x)) if x % 32 == 0 => lhs + case _ => default + } + + case Long_+ => + (lhs, rhs) match { + case (LongLiteral(l), LongLiteral(r)) => LongLiteral(l + r) + case (_, LongLiteral(_)) => foldBinaryOp(Long_+, rhs, lhs) + case (LongLiteral(0), _) => rhs + + case (LongLiteral(x), + BinaryOp(innerOp @ (Long_+ | Long_-), LongLiteral(y), z)) => + foldBinaryOp(innerOp, LongLiteral(x+y), z) + + case _ => default + } + + case Long_- => + (lhs, rhs) match { + case (_, LongLiteral(r)) => foldBinaryOp(Long_+, LongLiteral(-r), lhs) + + case (LongLiteral(x), BinaryOp(Long_+, LongLiteral(y), z)) => + foldBinaryOp(Long_-, LongLiteral(x-y), z) + case (LongLiteral(x), BinaryOp(Long_-, LongLiteral(y), z)) => + foldBinaryOp(Long_+, LongLiteral(x-y), z) + + case (_, BinaryOp(BinaryOp.Long_-, LongLiteral(0L), x)) => + foldBinaryOp(Long_+, lhs, x) + + case _ => default + } + + case Long_* => + (lhs, rhs) match { + case (LongLiteral(l), LongLiteral(r)) => LongLiteral(l * r) + case (_, LongLiteral(_)) => foldBinaryOp(Long_*, rhs, lhs) + + case (LongLiteral(1), _) => rhs + case (LongLiteral(-1), _) => foldBinaryOp(Long_-, LongLiteral(0), lhs) + + case _ => default + } + + case Long_/ => + (lhs, rhs) match { + case (_, LongLiteral(0)) => default + case (LongLiteral(l), LongLiteral(r)) => LongLiteral(l / r) + + case (_, LongLiteral(1)) => lhs + case (_, LongLiteral(-1)) => foldBinaryOp(Long_-, LongLiteral(0), lhs) + + case (LongFromInt(x), LongFromInt(y: IntLiteral)) if y.value != -1 => + LongFromInt(foldBinaryOp(Int_/, x, y)) + + case _ => default + } + + case Long_% => + (lhs, rhs) match { + case (_, LongLiteral(0)) => default + case (LongLiteral(l), LongLiteral(r)) => LongLiteral(l % r) + + case (_, LongLiteral(1L | -1L)) => + Block(keepOnlySideEffects(lhs), LongLiteral(0L)) + + case (LongFromInt(x), LongFromInt(y)) => + LongFromInt(foldBinaryOp(Int_%, x, y)) + + case _ => default + } + + case Long_| => + (lhs, rhs) match { + case (LongLiteral(l), LongLiteral(r)) => LongLiteral(l | r) + case (_, LongLiteral(_)) => foldBinaryOp(Long_|, rhs, lhs) + case (LongLiteral(0), _) => rhs + + case (LongLiteral(x), BinaryOp(Long_|, LongLiteral(y), z)) => + foldBinaryOp(Long_|, LongLiteral(x | y), z) + + case _ => default + } + + case Long_& => + (lhs, rhs) match { + case (LongLiteral(l), LongLiteral(r)) => LongLiteral(l & r) + case (_, LongLiteral(_)) => foldBinaryOp(Long_&, rhs, lhs) + case (LongLiteral(-1), _) => rhs + + case (LongLiteral(x), BinaryOp(Long_&, LongLiteral(y), z)) => + foldBinaryOp(Long_&, LongLiteral(x & y), z) + + case _ => default + } + + case Long_^ => + (lhs, rhs) match { + case (LongLiteral(l), LongLiteral(r)) => LongLiteral(l ^ r) + case (_, LongLiteral(_)) => foldBinaryOp(Long_^, rhs, lhs) + case (LongLiteral(0), _) => rhs + + case (LongLiteral(x), BinaryOp(Long_^, LongLiteral(y), z)) => + foldBinaryOp(Long_^, LongLiteral(x ^ y), z) + + case _ => default + } + + case Long_<< => + (lhs, rhs) match { + case (LongLiteral(l), IntLiteral(r)) => LongLiteral(l << r) + case (_, IntLiteral(x)) if x % 64 == 0 => lhs + case _ => default + } + + case Long_>>> => + (lhs, rhs) match { + case (LongLiteral(l), IntLiteral(r)) => LongLiteral(l >>> r) + case (_, IntLiteral(x)) if x % 64 == 0 => lhs + case _ => default + } + + case Long_>> => + (lhs, rhs) match { + case (LongLiteral(l), IntLiteral(r)) => LongLiteral(l >> r) + case (_, IntLiteral(x)) if x % 64 == 0 => lhs + case _ => default + } + + case Long_== | Long_!= => + val positive = (op == Long_==) + (lhs, rhs) match { + case (LongLiteral(l), LongLiteral(r)) => + BooleanLiteral((l == r) == positive) + + case (LongFromInt(x), LongFromInt(y)) => + foldBinaryOp(if (positive) === else !==, x, y) + case (LongFromInt(x), LongLiteral(y)) => + assert(y > Int.MaxValue || y < Int.MinValue) + Block(keepOnlySideEffects(x), BooleanLiteral(!positive)) + + case (BinaryOp(Long_+, LongLiteral(x), y), LongLiteral(z)) => + foldBinaryOp(op, y, LongLiteral(z-x)) + case (BinaryOp(Long_-, LongLiteral(x), y), LongLiteral(z)) => + foldBinaryOp(op, y, LongLiteral(x-z)) + + case (LongLiteral(_), _) => foldBinaryOp(op, rhs, lhs) + case _ => default + } + + case Long_< | Long_<= | Long_> | Long_>= => + def flippedOp = (op: @switch) match { + case Long_< => Long_> + case Long_<= => Long_>= + case Long_> => Long_< + case Long_>= => Long_<= + } + + def intOp = (op: @switch) match { + case Long_< => Num_< + case Long_<= => Num_<= + case Long_> => Num_> + case Long_>= => Num_>= + } + + (lhs, rhs) match { + case (LongLiteral(l), LongLiteral(r)) => + val result = (op: @switch) match { + case Long_< => l < r + case Long_<= => l <= r + case Long_> => l > r + case Long_>= => l >= r + } + BooleanLiteral(result) + + case (_, LongLiteral(Long.MinValue)) => + if (op == Long_< || op == Long_>=) + Block(keepOnlySideEffects(lhs), BooleanLiteral(op == Long_>=)) + else + foldBinaryOp(if (op == Long_<=) Long_== else Long_!=, lhs, rhs) + + case (_, LongLiteral(Long.MaxValue)) => + if (op == Long_> || op == Long_<=) + Block(keepOnlySideEffects(lhs), BooleanLiteral(op == Long_<=)) + else + foldBinaryOp(if (op == Long_>=) Long_== else Long_!=, lhs, rhs) + + case (LongFromInt(x), LongFromInt(y)) => + foldBinaryOp(intOp, x, y) + case (LongFromInt(x), LongLiteral(y)) => + assert(y > Int.MaxValue || y < Int.MinValue) + val result = + if (y > Int.MaxValue) op == Long_< || op == Long_<= + else op == Long_> || op == Long_>= + Block(keepOnlySideEffects(x), BooleanLiteral(result)) + + /* x + y.toLong > z + * -x on both sides + * requires x + y.toLong not to overflow, and z - x likewise + * y.toLong > z - x + */ + case (BinaryOp(Long_+, LongLiteral(x), y @ LongFromInt(_)), LongLiteral(z)) + if canAddLongs(x, Int.MinValue) && + canAddLongs(x, Int.MaxValue) && + canSubtractLongs(z, x) => + foldBinaryOp(op, y, LongLiteral(z-x)) + + /* x - y.toLong > z + * -x on both sides + * requires x - y.toLong not to overflow, and z - x likewise + * -(y.toLong) > z - x + */ + case (BinaryOp(Long_-, LongLiteral(x), y @ LongFromInt(_)), LongLiteral(z)) + if canSubtractLongs(x, Int.MinValue) && + canSubtractLongs(x, Int.MaxValue) && + canSubtractLongs(z, x) => + if (z-x != Long.MinValue) { + // Since -(y.toLong) does not overflow, we can negate both sides + foldBinaryOp(flippedOp, y, LongLiteral(-(z-x))) + } else { + /* -(y.toLong) > Long.MinValue + * Depending on the operator, this is either always true or + * always false. + */ + val result = (op == Long_>) || (op == Long_>=) + Block(keepOnlySideEffects(y), BooleanLiteral(result)) + } + + /* x.toLong + y.toLong > Int.MaxValue.toLong + * + * This is basically testing whether x+y overflows in positive. + * If x <= 0 or y <= 0, this cannot happen -> false. + * If x > 0 and y > 0, this can be detected with x+y < 0. + * Therefore, we rewrite as: + * + * x > 0 && y > 0 && x+y < 0. + * + * This requires to evaluate x and y once. + */ + case (BinaryOp(Long_+, LongFromInt(x), LongFromInt(y)), + LongLiteral(Int.MaxValue)) => + trampoline { + withNewLocalDefs(List( + Binding("x", None, IntType, false, PreTransTree(x)), + Binding("y", None, IntType, false, PreTransTree(y)))) { + (tempsLocalDefs, cont) => + val List(tempXDef, tempYDef) = tempsLocalDefs + val tempX = tempXDef.newReplacement + val tempY = tempYDef.newReplacement + cont(PreTransTree( + AndThen(AndThen( + BinaryOp(Num_>, tempX, IntLiteral(0)), + BinaryOp(Num_>, tempY, IntLiteral(0))), + BinaryOp(Num_<, BinaryOp(Int_+, tempX, tempY), IntLiteral(0))))) + } (finishTransform(isStat = false)) + } + + case (LongLiteral(_), _) => foldBinaryOp(flippedOp, rhs, lhs) + case _ => default + } + + case Float_+ => + (lhs, rhs) match { + case (FloatLiteral(l), FloatLiteral(r)) => FloatLiteral(l + r) + case (FloatLiteral(0), _) => rhs + case (_, FloatLiteral(_)) => foldBinaryOp(Float_+, rhs, lhs) + + case (FloatLiteral(x), + BinaryOp(innerOp @ (Float_+ | Float_-), FloatLiteral(y), z)) => + foldBinaryOp(innerOp, FloatLiteral(x+y), z) + + case _ => default + } + + case Float_- => + (lhs, rhs) match { + case (_, FloatLiteral(r)) => foldBinaryOp(Float_+, lhs, FloatLiteral(-r)) + + case (FloatLiteral(x), BinaryOp(Float_+, FloatLiteral(y), z)) => + foldBinaryOp(Float_-, FloatLiteral(x-y), z) + case (FloatLiteral(x), BinaryOp(Float_-, FloatLiteral(y), z)) => + foldBinaryOp(Float_+, FloatLiteral(x-y), z) + + case (_, BinaryOp(BinaryOp.Float_-, FloatLiteral(0), x)) => + foldBinaryOp(Float_+, lhs, x) + + case _ => default + } + + case Float_* => + (lhs, rhs) match { + case (FloatLiteral(l), FloatLiteral(r)) => FloatLiteral(l * r) + case (_, FloatLiteral(_)) => foldBinaryOp(Float_*, rhs, lhs) + + case (FloatLiteral(1), _) => rhs + case (FloatLiteral(-1), _) => foldBinaryOp(Float_-, FloatLiteral(0), lhs) + + case _ => default + } + + case Float_/ => + (lhs, rhs) match { + case (FloatLiteral(l), FloatLiteral(r)) => FloatLiteral(l / r) + + case (_, FloatLiteral(1)) => lhs + case (_, FloatLiteral(-1)) => foldBinaryOp(Float_-, FloatLiteral(0), lhs) + + case _ => default + } + + case Float_% => + (lhs, rhs) match { + case (FloatLiteral(l), FloatLiteral(r)) => FloatLiteral(l % r) + case _ => default + } + + case Double_+ => + (lhs, rhs) match { + case (NumberLiteral(l), NumberLiteral(r)) => DoubleLiteral(l + r) + case (NumberLiteral(0), _) => rhs + case (_, NumberLiteral(_)) => foldBinaryOp(Double_+, rhs, lhs) + + case (NumberLiteral(x), + BinaryOp(innerOp @ (Double_+ | Double_-), NumberLiteral(y), z)) => + foldBinaryOp(innerOp, DoubleLiteral(x+y), z) + + case _ => default + } + + case Double_- => + (lhs, rhs) match { + case (_, NumberLiteral(r)) => foldBinaryOp(Double_+, lhs, DoubleLiteral(-r)) + + case (NumberLiteral(x), BinaryOp(Double_+, NumberLiteral(y), z)) => + foldBinaryOp(Double_-, DoubleLiteral(x-y), z) + case (NumberLiteral(x), BinaryOp(Double_-, NumberLiteral(y), z)) => + foldBinaryOp(Double_+, DoubleLiteral(x-y), z) + + case (_, BinaryOp(BinaryOp.Double_-, NumberLiteral(0), x)) => + foldBinaryOp(Double_+, lhs, x) + + case _ => default + } + + case Double_* => + (lhs, rhs) match { + case (NumberLiteral(l), NumberLiteral(r)) => DoubleLiteral(l * r) + case (_, NumberLiteral(_)) => foldBinaryOp(Double_*, rhs, lhs) + + case (NumberLiteral(1), _) => rhs + case (NumberLiteral(-1), _) => foldBinaryOp(Double_-, DoubleLiteral(0), lhs) + + case _ => default + } + + case Double_/ => + (lhs, rhs) match { + case (NumberLiteral(l), NumberLiteral(r)) => DoubleLiteral(l / r) + + case (_, NumberLiteral(1)) => lhs + case (_, NumberLiteral(-1)) => foldBinaryOp(Double_-, DoubleLiteral(0), lhs) + + case _ => default + } + + case Double_% => + (lhs, rhs) match { + case (NumberLiteral(l), NumberLiteral(r)) => DoubleLiteral(l % r) + case _ => default + } + + case Boolean_== | Boolean_!= => + val positive = (op == Boolean_==) + (lhs, rhs) match { + case (BooleanLiteral(l), _) => + if (l == positive) rhs + else foldUnaryOp(UnaryOp.Boolean_!, rhs) + case (_, BooleanLiteral(r)) => + if (r == positive) lhs + else foldUnaryOp(UnaryOp.Boolean_!, lhs) + case _ => + default + } + + case Boolean_| => + (lhs, rhs) match { + case (_, BooleanLiteral(false)) => lhs + case (BooleanLiteral(false), _) => rhs + case _ => default + } + + case Boolean_& => + (lhs, rhs) match { + case (_, BooleanLiteral(true)) => lhs + case (BooleanLiteral(true), _) => rhs + case _ => default + } + + case Num_== | Num_!= => + val positive = (op == Num_==) + (lhs, rhs) match { + case (lhs: Literal, rhs: Literal) => + BooleanLiteral(literal_===(lhs, rhs) == positive) + + case (BinaryOp(Int_+, IntLiteral(x), y), IntLiteral(z)) => + foldBinaryOp(op, y, IntLiteral(z-x)) + case (BinaryOp(Int_-, IntLiteral(x), y), IntLiteral(z)) => + foldBinaryOp(op, y, IntLiteral(x-z)) + + case (_: Literal, _) => foldBinaryOp(op, rhs, lhs) + case _ => default + } + + case Num_< | Num_<= | Num_> | Num_>= => + def flippedOp = (op: @switch) match { + case Num_< => Num_> + case Num_<= => Num_>= + case Num_> => Num_< + case Num_>= => Num_<= + } + + if (lhs.tpe == IntType && rhs.tpe == IntType) { + (lhs, rhs) match { + case (IntLiteral(l), IntLiteral(r)) => + val result = (op: @switch) match { + case Num_< => l < r + case Num_<= => l <= r + case Num_> => l > r + case Num_>= => l >= r + } + BooleanLiteral(result) + + case (_, IntLiteral(Int.MinValue)) => + if (op == Num_< || op == Num_>=) + Block(keepOnlySideEffects(lhs), BooleanLiteral(op == Num_>=)) + else + foldBinaryOp(if (op == Num_<=) Num_== else Num_!=, lhs, rhs) + + case (_, IntLiteral(Int.MaxValue)) => + if (op == Num_> || op == Num_<=) + Block(keepOnlySideEffects(lhs), BooleanLiteral(op == Num_<=)) + else + foldBinaryOp(if (op == Num_>=) Num_== else Num_!=, lhs, rhs) + + case (IntLiteral(_), _) => foldBinaryOp(flippedOp, rhs, lhs) + case _ => default + } + } else { + (lhs, rhs) match { + case (NumberLiteral(l), NumberLiteral(r)) => + val result = (op: @switch) match { + case Num_< => l < r + case Num_<= => l <= r + case Num_> => l > r + case Num_>= => l >= r + } + BooleanLiteral(result) + + case _ => default + } + } + + case _ => + default + } + } + + private def fold3WayComparison(canBeEqual: Boolean, canBeLessThan: Boolean, + canBeGreaterThan: Boolean, lhs: Tree, rhs: Tree)( + implicit pos: Position): Tree = { + import BinaryOp._ + if (canBeEqual) { + if (canBeLessThan) { + if (canBeGreaterThan) + Block(keepOnlySideEffects(lhs), keepOnlySideEffects(rhs), BooleanLiteral(true)) + else + foldBinaryOp(Num_<=, lhs, rhs) + } else { + if (canBeGreaterThan) + foldBinaryOp(Num_>=, lhs, rhs) + else + foldBinaryOp(Num_==, lhs, rhs) + } + } else { + if (canBeLessThan) { + if (canBeGreaterThan) + foldBinaryOp(Num_!=, lhs, rhs) + else + foldBinaryOp(Num_<, lhs, rhs) + } else { + if (canBeGreaterThan) + foldBinaryOp(Num_>, lhs, rhs) + else + Block(keepOnlySideEffects(lhs), keepOnlySideEffects(rhs), BooleanLiteral(false)) + } + } + } + + private def foldUnbox(arg: PreTransform, charCode: Char)( + cont: PreTransCont): TailRec[Tree] = { + (charCode: @switch) match { + case 'Z' if arg.tpe.base == BooleanType => cont(arg) + case 'I' if arg.tpe.base == IntType => cont(arg) + case 'F' if arg.tpe.base == FloatType => cont(arg) + case 'J' if arg.tpe.base == LongType => cont(arg) + case 'D' if arg.tpe.base == DoubleType || + arg.tpe.base == IntType || arg.tpe.base == FloatType => cont(arg) + case _ => + cont(PreTransTree(Unbox(finishTransformExpr(arg), charCode)(arg.pos))) + } + } + + private def foldReferenceEquality(tlhs: PreTransform, trhs: PreTransform, + positive: Boolean = true)(implicit pos: Position): Tree = { + (tlhs, trhs) match { + case (_, PreTransTree(Null(), _)) if !tlhs.tpe.isNullable => + Block( + finishTransformStat(tlhs), + BooleanLiteral(!positive)) + case (PreTransTree(Null(), _), _) if !trhs.tpe.isNullable => + Block( + finishTransformStat(trhs), + BooleanLiteral(!positive)) + case _ => + foldBinaryOp(if (positive) BinaryOp.=== else BinaryOp.!==, + finishTransformExpr(tlhs), finishTransformExpr(trhs)) + } + } + + private def finishTransformCheckNull(preTrans: PreTransform)( + implicit pos: Position): Tree = { + if (preTrans.tpe.isNullable) { + val transformed = finishTransformExpr(preTrans) + CallHelper("checkNonNull", transformed)(transformed.tpe) + } else { + finishTransformExpr(preTrans) + } + } + + def transformIsolatedBody(optTarget: Option[MethodID], + thisType: Type, params: List[ParamDef], resultType: Type, + body: Tree): (List[ParamDef], Tree) = { + val (paramLocalDefs, newParamDefs) = (for { + p @ ParamDef(ident @ Ident(name, originalName), ptpe, mutable) <- params + } yield { + val newName = freshLocalName(name) + val newOriginalName = originalName.orElse(Some(newName)) + val localDef = LocalDef(RefinedType(ptpe), mutable, + ReplaceWithVarRef(newName, newOriginalName, new SimpleState(true), None)) + val newParamDef = ParamDef( + Ident(newName, newOriginalName)(ident.pos), ptpe, mutable)(p.pos) + ((name -> localDef), newParamDef) + }).unzip + + val thisLocalDef = + if (thisType == NoType) None + else { + Some("this" -> LocalDef( + RefinedType(thisType, isExact = false, isNullable = false), + false, ReplaceWithThis())) + } + + val allLocalDefs = thisLocalDef ++: paramLocalDefs + + val scope0 = optTarget.fold(Scope.Empty)( + target => Scope.Empty.inlining((None, target))) + val scope = scope0.withEnv(OptEnv.Empty.withLocalDefs(allLocalDefs)) + val newBody = + transform(body, resultType == NoType)(scope) + + (newParamDefs, newBody) + } + + private def returnable(oldLabelName: String, resultType: Type, + body: Tree, isStat: Boolean, usePreTransform: Boolean)( + cont: PreTransCont)( + implicit scope: Scope, pos: Position): TailRec[Tree] = tailcall { + val newLabel = freshLabelName( + if (oldLabelName.isEmpty) "inlinereturn" else oldLabelName) + + def doMakeTree(newBody: Tree, returnedTypes: List[Type]): Tree = { + val refinedType = + returnedTypes.reduce(constrainedLub(_, _, resultType)) + val returnCount = returnedTypes.size - 1 + + tryOptimizePatternMatch(oldLabelName, refinedType, + returnCount, newBody) getOrElse { + Labeled(Ident(newLabel, None), refinedType, newBody) + } + } + + val info = new LabelInfo(newLabel, acceptRecords = usePreTransform) + withState(info.returnedTypes) { + val bodyScope = scope.withEnv(scope.env.withLabelInfo(oldLabelName, info)) + + if (usePreTransform) { + assert(!isStat, "Cannot use pretransform in statement position") + tryOrRollback { cancelFun => + pretransformExpr(body) { tbody0 => + val returnedTypes0 = info.returnedTypes.value + if (returnedTypes0.isEmpty) { + // no return to that label, we can eliminate it + cont(tbody0) + } else { + val tbody = resolveLocalDef(tbody0) + val (newBody, returnedTypes) = tbody match { + case PreTransRecordTree(bodyTree, origType, _) => + (bodyTree, (bodyTree.tpe, origType) :: returnedTypes0) + case PreTransTree(bodyTree, tpe) => + (bodyTree, (bodyTree.tpe, tpe) :: returnedTypes0) + } + val (actualTypes, origTypes) = returnedTypes.unzip + val refinedOrigType = + origTypes.reduce(constrainedLub(_, _, resultType)) + actualTypes.collectFirst { + case actualType: RecordType => actualType + }.fold[TailRec[Tree]] { + // None of the returned types are records + cont(PreTransTree( + doMakeTree(newBody, actualTypes), refinedOrigType)) + } { recordType => + if (actualTypes.exists(t => t != recordType && t != NothingType)) + cancelFun() + + val resultTree = doMakeTree(newBody, actualTypes) + + if (origTypes.exists(t => t != refinedOrigType && !t.isNothingType)) + cancelFun() + + cont(PreTransRecordTree(resultTree, refinedOrigType, cancelFun)) + } + } + } (bodyScope) + } { () => + returnable(oldLabelName, resultType, body, isStat, + usePreTransform = false)(cont) + } + } else { + val newBody = transform(body, isStat)(bodyScope) + val returnedTypes0 = info.returnedTypes.value.map(_._1) + if (returnedTypes0.isEmpty) { + // no return to that label, we can eliminate it + cont(PreTransTree(newBody, RefinedType(newBody.tpe))) + } else { + val returnedTypes = newBody.tpe :: returnedTypes0 + val tree = doMakeTree(newBody, returnedTypes) + cont(PreTransTree(tree, RefinedType(tree.tpe))) + } + } + } + } + + def tryOptimizePatternMatch(oldLabelName: String, refinedType: Type, + returnCount: Int, newBody: Tree): Option[Tree] = { + if (!oldLabelName.startsWith("matchEnd")) None + else { + newBody match { + case Block(stats) => + @tailrec + def createRevAlts(xs: List[Tree], acc: List[(Tree, Tree)]): List[(Tree, Tree)] = xs match { + case If(cond, body, Skip()) :: xr => + createRevAlts(xr, (cond, body) :: acc) + case remaining => + (EmptyTree, Block(remaining)(remaining.head.pos)) :: acc + } + val revAlts = createRevAlts(stats, Nil) + + if (revAlts.size == returnCount) { + @tailrec + def constructOptimized(revAlts: List[(Tree, Tree)], elsep: Tree): Option[Tree] = { + revAlts match { + case (cond, body) :: revAltsRest => + body match { + case BlockOrAlone(prep, + Return(result, Some(Ident(newLabel, _)))) => + val result1 = + if (refinedType == NoType) keepOnlySideEffects(result) + else result + val prepAndResult = Block(prep :+ result1)(body.pos) + if (cond == EmptyTree) { + assert(elsep == EmptyTree) + constructOptimized(revAltsRest, prepAndResult) + } else { + assert(elsep != EmptyTree) + constructOptimized(revAltsRest, + foldIf(cond, prepAndResult, elsep)(refinedType)(cond.pos)) + } + case _ => + None + } + case Nil => + Some(elsep) + } + } + constructOptimized(revAlts, EmptyTree) + } else None + case _ => + None + } + } + } + + private def withBindings(bindings: List[Binding])( + buildInner: (Scope, PreTransCont) => TailRec[Tree])( + cont: PreTransCont)( + implicit scope: Scope): TailRec[Tree] = { + withNewLocalDefs(bindings) { (localDefs, cont1) => + val newMappings = for { + (binding, localDef) <- bindings zip localDefs + } yield { + binding.name -> localDef + } + buildInner(scope.withEnv(scope.env.withLocalDefs(newMappings)), cont1) + } (cont) + } + + private def withBinding(binding: Binding)( + buildInner: (Scope, PreTransCont) => TailRec[Tree])( + cont: PreTransCont)( + implicit scope: Scope): TailRec[Tree] = { + withNewLocalDef(binding) { (localDef, cont1) => + buildInner(scope.withEnv(scope.env.withLocalDef(binding.name, localDef)), + cont1) + } (cont) + } + + private def withNewLocalDefs(bindings: List[Binding])( + buildInner: (List[LocalDef], PreTransCont) => TailRec[Tree])( + cont: PreTransCont): TailRec[Tree] = { + bindings match { + case first :: rest => + withNewLocalDef(first) { (firstLocalDef, cont1) => + withNewLocalDefs(rest) { (restLocalDefs, cont2) => + buildInner(firstLocalDef :: restLocalDefs, cont2) + } (cont1) + } (cont) + + case Nil => + buildInner(Nil, cont) + } + } + + private def isImmutableType(tpe: Type): Boolean = tpe match { + case RecordType(fields) => + fields.forall(f => !f.mutable && isImmutableType(f.tpe)) + case _ => + true + } + + private def withNewLocalDef(binding: Binding)( + buildInner: (LocalDef, PreTransCont) => TailRec[Tree])( + cont: PreTransCont): TailRec[Tree] = tailcall { + val Binding(name, originalName, declaredType, mutable, value) = binding + implicit val pos = value.pos + + def withDedicatedVar(tpe: RefinedType): TailRec[Tree] = { + val newName = freshLocalName(name) + val newOriginalName = originalName.orElse(Some(name)) + + val used = new SimpleState(false) + withState(used) { + def doBuildInner(localDef: LocalDef)(varDef: => VarDef)( + cont: PreTransCont): TailRec[Tree] = { + buildInner(localDef, { tinner => + if (used.value) { + cont(PreTransBlock(varDef :: Nil, tinner)) + } else { + tinner match { + case PreTransLocalDef(`localDef`) => + cont(value) + case _ if tinner.contains(localDef) => + cont(PreTransBlock(varDef :: Nil, tinner)) + case _ => + val rhsSideEffects = finishTransformStat(value) + rhsSideEffects match { + case Skip() => + cont(tinner) + case _ => + if (rhsSideEffects.tpe == NothingType) + cont(PreTransTree(rhsSideEffects, RefinedType.Nothing)) + else + cont(PreTransBlock(rhsSideEffects :: Nil, tinner)) + } + } + } + }) + } + + resolveLocalDef(value) match { + case PreTransRecordTree(valueTree, valueTpe, cancelFun) => + val recordType = valueTree.tpe.asInstanceOf[RecordType] + if (!isImmutableType(recordType)) + cancelFun() + val localDef = LocalDef(valueTpe, mutable, + ReplaceWithRecordVarRef(newName, newOriginalName, recordType, + used, cancelFun)) + doBuildInner(localDef) { + VarDef(Ident(newName, newOriginalName), recordType, mutable, + valueTree) + } (cont) + + case PreTransTree(valueTree, valueTpe) => + def doDoBuildInner(optValueTree: Option[() => Tree])( + cont1: PreTransCont) = { + val localDef = LocalDef(tpe, mutable, ReplaceWithVarRef( + newName, newOriginalName, used, optValueTree)) + doBuildInner(localDef) { + VarDef(Ident(newName, newOriginalName), tpe.base, mutable, + optValueTree.fold(valueTree)(_())) + } (cont1) + } + if (mutable) { + doDoBuildInner(None)(cont) + } else (valueTree match { + case LongFromInt(arg) => + withNewLocalDef( + Binding("x", None, IntType, false, PreTransTree(arg))) { + (intLocalDef, cont1) => + doDoBuildInner(Some( + () => LongFromInt(intLocalDef.newReplacement)))( + cont1) + } (cont) + + case BinaryOp(op @ (BinaryOp.Long_+ | BinaryOp.Long_-), + LongFromInt(intLhs), LongFromInt(intRhs)) => + withNewLocalDefs(List( + Binding("x", None, IntType, false, PreTransTree(intLhs)), + Binding("y", None, IntType, false, PreTransTree(intRhs)))) { + (intLocalDefs, cont1) => + val List(lhsLocalDef, rhsLocalDef) = intLocalDefs + doDoBuildInner(Some( + () => BinaryOp(op, + LongFromInt(lhsLocalDef.newReplacement), + LongFromInt(rhsLocalDef.newReplacement))))( + cont1) + } (cont) + + case _ => + doDoBuildInner(None)(cont) + }) + } + } + } + + if (value.tpe.isNothingType) { + cont(value) + } else if (mutable) { + withDedicatedVar(RefinedType(declaredType)) + } else { + val refinedType = value.tpe + value match { + case PreTransBlock(stats, result) => + withNewLocalDef(binding.copy(value = result))(buildInner) { tresult => + cont(PreTransBlock(stats, tresult)) + } + + case PreTransLocalDef(localDef) if !localDef.mutable => + buildInner(localDef, cont) + + case PreTransTree(literal: Literal, _) => + buildInner(LocalDef(refinedType, false, + ReplaceWithConstant(literal)), cont) + + case PreTransTree(VarRef(Ident(refName, refOriginalName), false), _) => + buildInner(LocalDef(refinedType, false, + ReplaceWithVarRef(refName, refOriginalName, + new SimpleState(true), None)), cont) + + case _ => + withDedicatedVar(refinedType) + } + } + } + + /** Finds a type as precise as possible which is a supertype of lhs and rhs + * but still a subtype of upperBound. + * Requires that lhs and rhs be subtypes of upperBound, obviously. + */ + private def constrainedLub(lhs: RefinedType, rhs: RefinedType, + upperBound: Type): RefinedType = { + if (upperBound == NoType) RefinedType(upperBound) + else if (lhs == rhs) lhs + else if (lhs.isNothingType) rhs + else if (rhs.isNothingType) lhs + else { + RefinedType(constrainedLub(lhs.base, rhs.base, upperBound), + false, lhs.isNullable || rhs.isNullable) + } + } + + /** Finds a type as precise as possible which is a supertype of lhs and rhs + * but still a subtype of upperBound. + * Requires that lhs and rhs be subtypes of upperBound, obviously. + */ + private def constrainedLub(lhs: Type, rhs: Type, upperBound: Type): Type = { + // TODO Improve this + if (upperBound == NoType) upperBound + else if (lhs == rhs) lhs + else if (lhs == NothingType) rhs + else if (rhs == NothingType) lhs + else upperBound + } + + /** Trampolines a pretransform */ + private def trampoline(tailrec: => TailRec[Tree]): Tree = { + curTrampolineId += 1 + + val myTrampolineId = curTrampolineId + + try { + var rec = () => tailrec + + while (true) { + try { + return rec().result + } catch { + case e: RollbackException if e.trampolineId == myTrampolineId => + rollbacksCount += 1 + if (rollbacksCount > MaxRollbacksPerMethod) + throw new TooManyRollbacksException + + usedLocalNames.clear() + usedLocalNames ++= e.savedUsedLocalNames + usedLabelNames.clear() + usedLabelNames ++= e.savedUsedLabelNames + for ((state, backup) <- statesInUse zip e.savedStates) + state.asInstanceOf[State[Any]].restore(backup) + + rec = e.cont + } + } + + sys.error("Reached end of infinite loop") + } finally { + curTrampolineId -= 1 + } + } +} + +private[optimizer] object OptimizerCore { + + private final val MaxRollbacksPerMethod = 256 + + private final class TooManyRollbacksException + extends scala.util.control.ControlThrowable + + private val AnonFunctionClassPrefix = "sjsr_AnonFunction" + + private type CancelFun = () => Nothing + private type PreTransCont = PreTransform => TailRec[Tree] + + private case class RefinedType private (base: Type, isExact: Boolean, + isNullable: Boolean)( + val allocationSite: Option[AllocationSite], dummy: Int = 0) { + + def isNothingType: Boolean = base == NothingType + } + + private object RefinedType { + def apply(base: Type, isExact: Boolean, isNullable: Boolean, + allocationSite: Option[AllocationSite]): RefinedType = + new RefinedType(base, isExact, isNullable)(allocationSite) + + def apply(base: Type, isExact: Boolean, isNullable: Boolean): RefinedType = + RefinedType(base, isExact, isNullable, None) + + def apply(tpe: Type): RefinedType = tpe match { + case BooleanType | IntType | FloatType | DoubleType | StringType | + UndefType | NothingType | _:RecordType | NoType => + RefinedType(tpe, isExact = true, isNullable = false) + case NullType => + RefinedType(tpe, isExact = true, isNullable = true) + case _ => + RefinedType(tpe, isExact = false, isNullable = true) + } + + val NoRefinedType = RefinedType(NoType) + val Nothing = RefinedType(NothingType) + } + + private class AllocationSite(private val node: Tree) { + override def equals(that: Any): Boolean = that match { + case that: AllocationSite => this.node eq that.node + case _ => false + } + + override def hashCode(): Int = + System.identityHashCode(node) + + override def toString(): String = + s"AllocationSite($node)" + } + + private case class LocalDef( + tpe: RefinedType, + mutable: Boolean, + replacement: LocalDefReplacement) { + + def newReplacement(implicit pos: Position): Tree = replacement match { + case ReplaceWithVarRef(name, originalName, used, _) => + used.value = true + VarRef(Ident(name, originalName), mutable)(tpe.base) + + case ReplaceWithRecordVarRef(_, _, _, _, cancelFun) => + cancelFun() + + case ReplaceWithThis() => + This()(tpe.base) + + case ReplaceWithConstant(value) => + value + + case TentativeClosureReplacement(_, _, _, _, _, cancelFun) => + cancelFun() + + case InlineClassBeingConstructedReplacement(_, cancelFun) => + cancelFun() + + case InlineClassInstanceReplacement(_, _, cancelFun) => + cancelFun() + } + + def contains(that: LocalDef): Boolean = { + (this eq that) || (replacement match { + case TentativeClosureReplacement(_, _, _, captureLocalDefs, _, _) => + captureLocalDefs.exists(_.contains(that)) + case InlineClassInstanceReplacement(_, fieldLocalDefs, _) => + fieldLocalDefs.valuesIterator.exists(_.contains(that)) + case _ => + false + }) + } + } + + private sealed abstract class LocalDefReplacement + + private final case class ReplaceWithVarRef(name: String, + originalName: Option[String], + used: SimpleState[Boolean], + longOpTree: Option[() => Tree]) extends LocalDefReplacement + + private final case class ReplaceWithRecordVarRef(name: String, + originalName: Option[String], + recordType: RecordType, + used: SimpleState[Boolean], + cancelFun: CancelFun) extends LocalDefReplacement + + private final case class ReplaceWithThis() extends LocalDefReplacement + + private final case class ReplaceWithConstant( + value: Tree) extends LocalDefReplacement + + private final case class TentativeClosureReplacement( + captureParams: List[ParamDef], params: List[ParamDef], body: Tree, + captureValues: List[LocalDef], + alreadyUsed: SimpleState[Boolean], + cancelFun: CancelFun) extends LocalDefReplacement + + private final case class InlineClassBeingConstructedReplacement( + fieldLocalDefs: Map[String, LocalDef], + cancelFun: CancelFun) extends LocalDefReplacement + + private final case class InlineClassInstanceReplacement( + recordType: RecordType, + fieldLocalDefs: Map[String, LocalDef], + cancelFun: CancelFun) extends LocalDefReplacement + + private final class LabelInfo( + val newName: String, + val acceptRecords: Boolean, + /** (actualType, originalType), actualType can be a RecordType. */ + val returnedTypes: SimpleState[List[(Type, RefinedType)]] = new SimpleState(Nil)) + + private class OptEnv( + val localDefs: Map[String, LocalDef], + val labelInfos: Map[String, LabelInfo]) { + + def withLocalDef(oldName: String, rep: LocalDef): OptEnv = + new OptEnv(localDefs + (oldName -> rep), labelInfos) + + def withLocalDefs(reps: List[(String, LocalDef)]): OptEnv = + new OptEnv(localDefs ++ reps, labelInfos) + + def withLabelInfo(oldName: String, info: LabelInfo): OptEnv = + new OptEnv(localDefs, labelInfos + (oldName -> info)) + + def withinFunction(paramLocalDefs: List[(String, LocalDef)]): OptEnv = + new OptEnv(localDefs ++ paramLocalDefs, Map.empty) + + override def toString(): String = { + "localDefs:"+localDefs.mkString("\n ", "\n ", "\n") + + "labelInfos:"+labelInfos.mkString("\n ", "\n ", "") + } + } + + private object OptEnv { + val Empty: OptEnv = new OptEnv(Map.empty, Map.empty) + } + + private class Scope(val env: OptEnv, + val implsBeingInlined: Set[(Option[AllocationSite], AbstractMethodID)]) { + def withEnv(env: OptEnv): Scope = + new Scope(env, implsBeingInlined) + + def inlining(impl: (Option[AllocationSite], AbstractMethodID)): Scope = { + assert(!implsBeingInlined(impl), s"Circular inlining of $impl") + new Scope(env, implsBeingInlined + impl) + } + } + + private object Scope { + val Empty: Scope = new Scope(OptEnv.Empty, Set.empty) + } + + /** The result of pretransformExpr(). + * It has a `tpe` as precisely refined as if a full transformExpr() had + * been performed. + * It is also not dependent on the environment anymore. In some sense, it + * has "captured" its environment at definition site. + */ + private sealed abstract class PreTransform { + def pos: Position + val tpe: RefinedType + + def contains(localDef: LocalDef): Boolean = this match { + case PreTransBlock(_, result) => + result.contains(localDef) + case PreTransLocalDef(thisLocalDef) => + thisLocalDef.contains(localDef) + case _ => + false + } + } + + private final class PreTransBlock private (val stats: List[Tree], + val result: PreTransLocalDef) extends PreTransform { + def pos = result.pos + val tpe = result.tpe + + assert(stats.nonEmpty) + + override def toString(): String = + s"PreTransBlock($stats,$result)" + } + + private object PreTransBlock { + def apply(stats: List[Tree], result: PreTransform): PreTransform = { + if (stats.isEmpty) result + else { + result match { + case PreTransBlock(innerStats, innerResult) => + new PreTransBlock(stats ++ innerStats, innerResult) + case result: PreTransLocalDef => + new PreTransBlock(stats, result) + case PreTransRecordTree(tree, tpe, cancelFun) => + PreTransRecordTree(Block(stats :+ tree)(tree.pos), tpe, cancelFun) + case PreTransTree(tree, tpe) => + PreTransTree(Block(stats :+ tree)(tree.pos), tpe) + } + } + } + + def unapply(preTrans: PreTransBlock): Some[(List[Tree], PreTransLocalDef)] = + Some(preTrans.stats, preTrans.result) + } + + private sealed abstract class PreTransNoBlock extends PreTransform + + private final case class PreTransLocalDef(localDef: LocalDef)( + implicit val pos: Position) extends PreTransNoBlock { + val tpe: RefinedType = localDef.tpe + } + + private sealed abstract class PreTransGenTree extends PreTransNoBlock + + private final case class PreTransRecordTree(tree: Tree, + tpe: RefinedType, cancelFun: CancelFun) extends PreTransGenTree { + def pos = tree.pos + + assert(tree.tpe.isInstanceOf[RecordType], + s"Cannot create a PreTransRecordTree with non-record type ${tree.tpe}") + } + + private final case class PreTransTree(tree: Tree, + tpe: RefinedType) extends PreTransGenTree { + def pos: Position = tree.pos + + assert(!tree.tpe.isInstanceOf[RecordType], + s"Cannot create a Tree with record type ${tree.tpe}") + } + + private object PreTransTree { + def apply(tree: Tree): PreTransTree = + PreTransTree(tree, RefinedType(tree.tpe)) + } + + private final case class Binding(name: String, originalName: Option[String], + declaredType: Type, mutable: Boolean, value: PreTransform) + + private object NumberLiteral { + def unapply(tree: Literal): Option[Double] = tree match { + case DoubleLiteral(v) => Some(v) + case IntLiteral(v) => Some(v.toDouble) + case FloatLiteral(v) => Some(v.toDouble) + case _ => None + } + } + + private object LongFromInt { + def apply(x: Tree)(implicit pos: Position): Tree = x match { + case IntLiteral(v) => LongLiteral(v) + case _ => UnaryOp(UnaryOp.IntToLong, x) + } + + def unapply(tree: Tree): Option[Tree] = tree match { + case LongLiteral(v) if v.toInt == v => Some(IntLiteral(v.toInt)(tree.pos)) + case UnaryOp(UnaryOp.IntToLong, x) => Some(x) + case _ => None + } + } + + private object AndThen { + def apply(lhs: Tree, rhs: Tree)(implicit pos: Position): Tree = + If(lhs, rhs, BooleanLiteral(false))(BooleanType) + } + + /** Tests whether `x + y` is valid without falling out of range. */ + private def canAddLongs(x: Long, y: Long): Boolean = + if (y >= 0) x+y >= x + else x+y < x + + /** Tests whether `x - y` is valid without falling out of range. */ + private def canSubtractLongs(x: Long, y: Long): Boolean = + if (y >= 0) x-y <= x + else x-y > x + + /** Tests whether `-x` is valid without falling out of range. */ + private def canNegateLong(x: Long): Boolean = + x != Long.MinValue + + private object Intrinsics { + final val ArrayCopy = 1 + final val IdentityHashCode = ArrayCopy + 1 + + final val PropertiesOf = IdentityHashCode + 1 + + final val LongToString = PropertiesOf + 1 + final val LongCompare = LongToString + 1 + final val LongBitCount = LongCompare + 1 + final val LongSignum = LongBitCount + 1 + final val LongLeading0s = LongSignum + 1 + final val LongTrailing0s = LongLeading0s + 1 + final val LongToBinStr = LongTrailing0s + 1 + final val LongToHexStr = LongToBinStr + 1 + final val LongToOctalStr = LongToHexStr + 1 + + final val ByteArrayToInt8Array = LongToOctalStr + 1 + final val ShortArrayToInt16Array = ByteArrayToInt8Array + 1 + final val CharArrayToUint16Array = ShortArrayToInt16Array + 1 + final val IntArrayToInt32Array = CharArrayToUint16Array + 1 + final val FloatArrayToFloat32Array = IntArrayToInt32Array + 1 + final val DoubleArrayToFloat64Array = FloatArrayToFloat32Array + 1 + + final val Int8ArrayToByteArray = DoubleArrayToFloat64Array + 1 + final val Int16ArrayToShortArray = Int8ArrayToByteArray + 1 + final val Uint16ArrayToCharArray = Int16ArrayToShortArray + 1 + final val Int32ArrayToIntArray = Uint16ArrayToCharArray + 1 + final val Float32ArrayToFloatArray = Int32ArrayToIntArray + 1 + final val Float64ArrayToDoubleArray = Float32ArrayToFloatArray + 1 + + val intrinsics: Map[String, Int] = Map( + "jl_System$.arraycopy__O__I__O__I__I__V" -> ArrayCopy, + "jl_System$.identityHashCode__O__I" -> IdentityHashCode, + + "sjsr_package$.propertiesOf__sjs_js_Any__sjs_js_Array" -> PropertiesOf, + + "jl_Long$.toString__J__T" -> LongToString, + "jl_Long$.compare__J__J__I" -> LongCompare, + "jl_Long$.bitCount__J__I" -> LongBitCount, + "jl_Long$.signum__J__J" -> LongSignum, + "jl_Long$.numberOfLeadingZeros__J__I" -> LongLeading0s, + "jl_Long$.numberOfTrailingZeros__J__I" -> LongTrailing0s, + "jl_long$.toBinaryString__J__T" -> LongToBinStr, + "jl_Long$.toHexString__J__T" -> LongToHexStr, + "jl_Long$.toOctalString__J__T" -> LongToOctalStr, + + "sjs_js_typedarray_package$.byteArray2Int8Array__AB__sjs_js_typedarray_Int8Array" -> ByteArrayToInt8Array, + "sjs_js_typedarray_package$.shortArray2Int16Array__AS__sjs_js_typedarray_Int16Array" -> ShortArrayToInt16Array, + "sjs_js_typedarray_package$.charArray2Uint16Array__AC__sjs_js_typedarray_Uint16Array" -> CharArrayToUint16Array, + "sjs_js_typedarray_package$.intArray2Int32Array__AI__sjs_js_typedarray_Int32Array" -> IntArrayToInt32Array, + "sjs_js_typedarray_package$.floatArray2Float32Array__AF__sjs_js_typedarray_Float32Array" -> FloatArrayToFloat32Array, + "sjs_js_typedarray_package$.doubleArray2Float64Array__AD__sjs_js_typedarray_Float64Array" -> DoubleArrayToFloat64Array, + + "sjs_js_typedarray_package$.int8Array2ByteArray__sjs_js_typedarray_Int8Array__AB" -> Int8ArrayToByteArray, + "sjs_js_typedarray_package$.int16Array2ShortArray__sjs_js_typedarray_Int16Array__AS" -> Int16ArrayToShortArray, + "sjs_js_typedarray_package$.uint16Array2CharArray__sjs_js_typedarray_Uint16Array__AC" -> Uint16ArrayToCharArray, + "sjs_js_typedarray_package$.int32Array2IntArray__sjs_js_typedarray_Int32Array__AI" -> Int32ArrayToIntArray, + "sjs_js_typedarray_package$.float32Array2FloatArray__sjs_js_typedarray_Float32Array__AF" -> Float32ArrayToFloatArray, + "sjs_js_typedarray_package$.float64Array2DoubleArray__sjs_js_typedarray_Float64Array__AD" -> Float64ArrayToDoubleArray + ).withDefaultValue(-1) + } + + private def getIntrinsicCode(target: AbstractMethodID): Int = + Intrinsics.intrinsics(target.toString) + + private trait State[A] { + def makeBackup(): A + def restore(backup: A): Unit + } + + private class SimpleState[A](var value: A) extends State[A] { + def makeBackup(): A = value + def restore(backup: A): Unit = value = backup + } + + trait AbstractMethodID { + def inlineable: Boolean + def isTraitImplForwarder: Boolean + } + + /** Parts of [[GenIncOptimizer#MethodImpl]] with decisions about optimizations. */ + abstract class MethodImpl { + def encodedName: String + def optimizerHints: OptimizerHints + def originalDef: MethodDef + def thisType: Type + + var inlineable: Boolean = false + var isTraitImplForwarder: Boolean = false + + protected def updateInlineable(): Unit = { + val MethodDef(Ident(methodName, _), params, _, body) = originalDef + + isTraitImplForwarder = body match { + // Shape of forwarders to trait impls + case TraitImplApply(impl, method, args) => + ((args.size == params.size + 1) && + (args.head.isInstanceOf[This]) && + (args.tail.zip(params).forall { + case (VarRef(Ident(aname, _), _), + ParamDef(Ident(pname, _), _, _)) => aname == pname + case _ => false + })) + + case _ => false + } + + inlineable = optimizerHints.hasInlineAnnot || isTraitImplForwarder || { + val MethodDef(_, params, _, body) = originalDef + body match { + case _:Skip | _:This | _:Literal => true + + // Shape of accessors + case Select(This(), _, _) if params.isEmpty => true + case Assign(Select(This(), _, _), VarRef(_, _)) + if params.size == 1 => true + + // Shape of trivial call-super constructors + case Block(stats) + if params.isEmpty && isConstructorName(encodedName) && + stats.forall(isTrivialConstructorStat) => true + + // Simple method + case SimpleMethodBody() => true + + case _ => false + } + } + } + } + + private def isTrivialConstructorStat(stat: Tree): Boolean = stat match { + case This() => + true + case StaticApply(This(), _, _, Nil) => + true + case TraitImplApply(_, Ident(methodName, _), This() :: Nil) => + methodName.contains("__$init$__") + case _ => + false + } + + private object SimpleMethodBody { + @tailrec + def unapply(body: Tree): Boolean = body match { + case New(_, _, args) => areSimpleArgs(args) + case Apply(receiver, _, args) => areSimpleArgs(receiver :: args) + case StaticApply(receiver, _, _, args) => areSimpleArgs(receiver :: args) + case TraitImplApply(_, _, args) => areSimpleArgs(args) + case Select(qual, _, _) => isSimpleArg(qual) + case IsInstanceOf(inner, _) => isSimpleArg(inner) + + case Block(List(inner, Undefined())) => + unapply(inner) + + case Unbox(inner, _) => unapply(inner) + case AsInstanceOf(inner, _) => unapply(inner) + + case _ => isSimpleArg(body) + } + + private def areSimpleArgs(args: List[Tree]): Boolean = + args.forall(isSimpleArg) + + @tailrec + private def isSimpleArg(arg: Tree): Boolean = arg match { + case New(_, _, Nil) => true + case Apply(receiver, _, Nil) => isTrivialArg(receiver) + case StaticApply(receiver, _, _, Nil) => isTrivialArg(receiver) + case TraitImplApply(_, _, Nil) => true + + case ArrayLength(array) => isTrivialArg(array) + case ArraySelect(array, index) => isTrivialArg(array) && isTrivialArg(index) + + case Unbox(inner, _) => isSimpleArg(inner) + case AsInstanceOf(inner, _) => isSimpleArg(inner) + + case _ => + isTrivialArg(arg) + } + + private def isTrivialArg(arg: Tree): Boolean = arg match { + case _:VarRef | _:This | _:Literal | _:LoadModule => + true + case _ => + false + } + } + + private object BlockOrAlone { + def unapply(tree: Tree): Some[(List[Tree], Tree)] = Some(tree match { + case Block(init :+ last) => (init, last) + case _ => (Nil, tree) + }) + } + + /** Recreates precise [[Infos.MethodInfo]] from the optimized [[MethodDef]]. */ + private def recreateInfo(methodDef: MethodDef): Infos.MethodInfo = { + new RecreateInfoTraverser().recreateInfo(methodDef) + } + + private final class RecreateInfoTraverser extends Traversers.Traverser { + import RecreateInfoTraverser._ + + private val calledMethods = mutable.Map.empty[String, mutable.Set[String]] + private val calledMethodsStatic = mutable.Map.empty[String, mutable.Set[String]] + private val instantiatedClasses = mutable.Set.empty[String] + private val accessedModules = mutable.Set.empty[String] + private val accessedClassData = mutable.Set.empty[String] + + def recreateInfo(methodDef: MethodDef): Infos.MethodInfo = { + traverse(methodDef.body) + Infos.MethodInfo( + encodedName = methodDef.name.name, + calledMethods = calledMethods.toMap.mapValues(_.toList), + calledMethodsStatic = calledMethodsStatic.toMap.mapValues(_.toList), + instantiatedClasses = instantiatedClasses.toList, + accessedModules = accessedModules.toList, + accessedClassData = accessedClassData.toList) + } + + private def addCalledMethod(container: String, methodName: String): Unit = + calledMethods.getOrElseUpdate(container, mutable.Set.empty) += methodName + + private def addCalledMethodStatic(container: String, methodName: String): Unit = + calledMethodsStatic.getOrElseUpdate(container, mutable.Set.empty) += methodName + + private def refTypeToClassData(tpe: ReferenceType): String = tpe match { + case ClassType(cls) => cls + case ArrayType(base, _) => base + } + + def addAccessedClassData(encodedName: String): Unit = { + if (!AlwaysPresentClassData.contains(encodedName)) + accessedClassData += encodedName + } + + def addAccessedClassData(tpe: ReferenceType): Unit = + addAccessedClassData(refTypeToClassData(tpe)) + + override def traverse(tree: Tree): Unit = { + tree match { + case New(ClassType(cls), ctor, _) => + instantiatedClasses += cls + addCalledMethodStatic(cls, ctor.name) + + case Apply(receiver, method, _) => + receiver.tpe match { + case ClassType(cls) if !Definitions.HijackedClasses.contains(cls) => + addCalledMethod(cls, method.name) + case AnyType => + addCalledMethod(Definitions.ObjectClass, method.name) + case ArrayType(_, _) if method.name != "clone__O" => + /* clone__O is overridden in the pseudo Array classes and is + * always kept anyway, because it is in scalajsenv.js. + * Other methods delegate to Object, which we can model with + * a static call to Object.method. + */ + addCalledMethodStatic(Definitions.ObjectClass, method.name) + case _ => + // Nothing to do + } + + case StaticApply(_, ClassType(cls), method, _) => + addCalledMethodStatic(cls, method.name) + case TraitImplApply(ClassType(impl), method, _) => + addCalledMethodStatic(impl, method.name) + + case LoadModule(ClassType(cls)) => + accessedModules += cls.stripSuffix("$") + + case NewArray(tpe, _) => + addAccessedClassData(tpe) + case ArrayValue(tpe, _) => + addAccessedClassData(tpe) + case IsInstanceOf(_, cls) => + addAccessedClassData(cls) + case AsInstanceOf(_, cls) => + addAccessedClassData(cls) + case ClassOf(cls) => + addAccessedClassData(cls) + + case _ => + } + super.traverse(tree) + } + } + + private object RecreateInfoTraverser { + /** Class data that are never eliminated by dce, so we don't need to + * record them. + */ + private val AlwaysPresentClassData = { + import Definitions._ + Set("V", "Z", "C", "B", "S", "I", "J", "F", "D", + ObjectClass, StringClass) + } + } + + private def exceptionMsg(myself: AbstractMethodID, + attemptedInlining: List[AbstractMethodID]) = { + val buf = new StringBuilder() + + buf.append("The Scala.js optimizer crashed while optimizing " + myself) + + buf.append("\nMethods attempted to inline:\n") + + for (m <- attemptedInlining) { + buf.append("* ") + buf.append(m) + buf.append('\n') + } + + buf.toString + } + + private class RollbackException(val trampolineId: Int, + val savedUsedLocalNames: Set[String], + val savedUsedLabelNames: Set[String], + val savedStates: List[Any], + val cont: () => TailRec[Tree]) extends ControlThrowable + + class OptimizeException(val myself: AbstractMethodID, + val attemptedInlining: List[AbstractMethodID], cause: Throwable + ) extends Exception(exceptionMsg(myself, attemptedInlining), cause) + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/ScalaJSOptimizer.scala b/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/ScalaJSOptimizer.scala new file mode 100644 index 0000000..646484b --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/ScalaJSOptimizer.scala @@ -0,0 +1,552 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.optimizer + +import scala.annotation.{switch, tailrec} + +import scala.collection.mutable +import scala.collection.immutable.{Seq, Traversable} + +import java.net.URI + +import scala.scalajs.ir +import ir.Infos +import ir.ClassKind + +import scala.scalajs.tools.logging._ +import scala.scalajs.tools.io._ +import scala.scalajs.tools.classpath._ +import scala.scalajs.tools.sourcemap._ +import scala.scalajs.tools.corelib._ + +import scala.scalajs.tools.sem.Semantics + +import scala.scalajs.tools.javascript +import javascript.{Trees => js} + +/** Scala.js optimizer: does type-aware global dce. */ +class ScalaJSOptimizer( + semantics: Semantics, + optimizerFactory: (Semantics) => GenIncOptimizer) { + import ScalaJSOptimizer._ + + private val classEmitter = new javascript.ScalaJSClassEmitter(semantics) + + private[this] var persistentState: PersistentState = new PersistentState + private[this] var optimizer: GenIncOptimizer = optimizerFactory(semantics) + + def this(semantics: Semantics) = this(semantics, new IncOptimizer(_)) + + /** Applies Scala.js-specific optimizations to a CompleteIRClasspath. + * See [[ScalaJSOptimizer.Inputs]] for details about the required and + * optional inputs. + * See [[ScalaJSOptimizer.OutputConfig]] for details about the configuration + * for the output of this method. + * Returns a [[CompleteCIClasspath]] containing the result of the + * optimizations. + * + * analyzes, dead code eliminates and concatenates IR content + * - Maintains/establishes order + * - No IR in result + * - CoreJSLibs in result (since they are implicitly in the CompleteIRCP) + */ + def optimizeCP(inputs: Inputs[IRClasspath], outCfg: OutputConfig, + logger: Logger): LinkedClasspath = { + + val cp = inputs.input + + CacheUtils.cached(cp.version, outCfg.output, outCfg.cache) { + logger.info(s"Fast optimizing ${outCfg.output.path}") + optimizeIR(inputs.copy(input = inputs.input.scalaJSIR), outCfg, logger) + } + + new LinkedClasspath(cp.jsLibs, outCfg.output, cp.requiresDOM, cp.version) + } + + def optimizeIR(inputs: Inputs[Traversable[VirtualScalaJSIRFile]], + outCfg: OutputConfig, logger: Logger): Unit = { + + val builder = { + import outCfg._ + if (wantSourceMap) + new JSFileBuilderWithSourceMap(output.name, + output.contentWriter, + output.sourceMapWriter, + relativizeSourceMapBase) + else + new JSFileBuilder(output.name, output.contentWriter) + } + + builder.addLine("'use strict';") + CoreJSLibs.libs(semantics).foreach(builder.addFile _) + + optimizeIR(inputs, outCfg, builder, logger) + + builder.complete() + builder.closeWriters() + } + + def optimizeIR(inputs: Inputs[Traversable[VirtualScalaJSIRFile]], + outCfg: OptimizerConfig, builder: JSTreeBuilder, logger: Logger): Unit = { + + /* Handle tree equivalence: If we handled source maps so far, positions are + still up-to-date. Otherwise we need to flush the state if proper + positions are requested now. + */ + if (outCfg.wantSourceMap && !persistentState.wasWithSourceMap) + clean() + + persistentState.wasWithSourceMap = outCfg.wantSourceMap + + persistentState.startRun() + try { + import inputs._ + val allData = + GenIncOptimizer.logTime(logger, "Read info") { + readAllData(inputs.input, logger) + } + val (useOptimizer, refinedAnalyzer) = GenIncOptimizer.logTime( + logger, "Optimizations part") { + val analyzer = + GenIncOptimizer.logTime(logger, "Compute reachability") { + val analyzer = new Analyzer(logger, semantics, allData, + globalWarnEnabled = true, + isBeforeOptimizer = !outCfg.disableOptimizer) + analyzer.computeReachability(manuallyReachable, noWarnMissing) + analyzer + } + if (outCfg.checkIR) { + GenIncOptimizer.logTime(logger, "Check IR") { + if (analyzer.allAvailable) + checkIR(analyzer, logger) + else if (inputs.noWarnMissing.isEmpty) + sys.error("Could not check IR because there where linking errors.") + } + } + def getClassTreeIfChanged(encodedName: String, + lastVersion: Option[String]): Option[(ir.Trees.ClassDef, Option[String])] = { + val persistentFile = persistentState.encodedNameToPersistentFile(encodedName) + persistentFile.treeIfChanged(lastVersion) + } + + val useOptimizer = analyzer.allAvailable && !outCfg.disableOptimizer + + if (outCfg.batchMode) + optimizer = optimizerFactory(semantics) + + val refinedAnalyzer = if (useOptimizer) { + GenIncOptimizer.logTime(logger, "Inliner") { + optimizer.update(analyzer, getClassTreeIfChanged, + outCfg.wantSourceMap, logger) + } + GenIncOptimizer.logTime(logger, "Refined reachability analysis") { + val refinedData = computeRefinedData(allData, optimizer) + val refinedAnalyzer = new Analyzer(logger, semantics, refinedData, + globalWarnEnabled = false, + isBeforeOptimizer = false) + refinedAnalyzer.computeReachability(manuallyReachable, noWarnMissing) + refinedAnalyzer + } + } else { + if (inputs.noWarnMissing.isEmpty && !outCfg.disableOptimizer) + logger.warn("Not running the inliner because there where linking errors.") + analyzer + } + (useOptimizer, refinedAnalyzer) + } + GenIncOptimizer.logTime(logger, "Write DCE'ed output") { + buildDCEedOutput(builder, refinedAnalyzer, useOptimizer) + } + } finally { + persistentState.endRun(outCfg.unCache) + logger.debug( + s"Inc. opt stats: reused: ${persistentState.statsReused} -- "+ + s"invalidated: ${persistentState.statsInvalidated} -- "+ + s"trees read: ${persistentState.statsTreesRead}") + } + } + + /** Resets all persistent state of this optimizer */ + def clean(): Unit = { + persistentState = new PersistentState + optimizer = optimizerFactory(semantics) + } + + private def readAllData(ir: Traversable[VirtualScalaJSIRFile], + logger: Logger): scala.collection.Seq[Infos.ClassInfo] = { + ir.map(persistentState.getPersistentIRFile(_).info).toSeq + } + + private def checkIR(analyzer: Analyzer, logger: Logger): Unit = { + val allClassDefs = for { + classInfo <- analyzer.classInfos.values + persistentIRFile <- persistentState.encodedNameToPersistentFile.get( + classInfo.encodedName) + } yield persistentIRFile.tree + val checker = new IRChecker(analyzer, allClassDefs.toSeq, logger) + if (!checker.check()) + sys.error(s"There were ${checker.errorCount} IR checking errors.") + } + + private def computeRefinedData( + allData: scala.collection.Seq[Infos.ClassInfo], + optimizer: GenIncOptimizer): scala.collection.Seq[Infos.ClassInfo] = { + + def refineMethodInfo(container: optimizer.MethodContainer, + methodInfo: Infos.MethodInfo): Infos.MethodInfo = { + container.methods.get(methodInfo.encodedName).fold(methodInfo) { + methodImpl => methodImpl.preciseInfo + } + } + + def refineMethodInfos(container: optimizer.MethodContainer, + methodInfos: List[Infos.MethodInfo]): List[Infos.MethodInfo] = { + methodInfos.map(m => refineMethodInfo(container, m)) + } + + def refineClassInfo(container: optimizer.MethodContainer, + info: Infos.ClassInfo): Infos.ClassInfo = { + val refinedMethods = refineMethodInfos(container, info.methods) + Infos.ClassInfo(info.name, info.encodedName, info.isExported, + info.ancestorCount, info.kind, info.superClass, info.ancestors, + Infos.OptimizerHints.empty, refinedMethods) + } + + for { + info <- allData + } yield { + info.kind match { + case ClassKind.Class | ClassKind.ModuleClass => + optimizer.getClass(info.encodedName).fold(info) { + cls => refineClassInfo(cls, info) + } + + case ClassKind.TraitImpl => + optimizer.getTraitImpl(info.encodedName).fold(info) { + impl => refineClassInfo(impl, info) + } + + case _ => + info + } + } + } + + private def buildDCEedOutput(builder: JSTreeBuilder, + analyzer: Analyzer, useInliner: Boolean): Unit = { + + def compareClassInfo(lhs: analyzer.ClassInfo, rhs: analyzer.ClassInfo) = { + if (lhs.ancestorCount != rhs.ancestorCount) lhs.ancestorCount < rhs.ancestorCount + else lhs.encodedName.compareTo(rhs.encodedName) < 0 + } + + def addPersistentFile(classInfo: analyzer.ClassInfo, + persistentFile: PersistentIRFile) = { + import ir.Trees._ + import javascript.JSDesugaring.{desugarJavaScript => desugar} + + val d = persistentFile.desugared + lazy val classDef = { + persistentState.statsTreesRead += 1 + persistentFile.tree + } + + def addTree(tree: js.Tree): Unit = + builder.addJSTree(tree) + + def addReachableMethods(emitFun: (String, MethodDef) => js.Tree): Unit = { + /* This is a bit convoluted because we have to: + * * avoid to use classDef at all if we already know all the needed methods + * * if any new method is needed, better to go through the defs once + */ + val methodNames = d.methodNames.getOrElseUpdate( + classDef.defs collect { + case MethodDef(Ident(encodedName, _), _, _, _) => encodedName + }) + val reachableMethods = methodNames.filter( + name => classInfo.methodInfos(name).isReachable) + if (reachableMethods.forall(d.methods.contains(_))) { + for (encodedName <- reachableMethods) { + addTree(d.methods(encodedName)) + } + } else { + classDef.defs.foreach { + case m: MethodDef if m.name.isInstanceOf[Ident] => + if (classInfo.methodInfos(m.name.name).isReachable) { + addTree(d.methods.getOrElseUpdate(m.name.name, + emitFun(classInfo.encodedName, m))) + } + case _ => + } + } + } + + if (classInfo.isImplClass) { + if (useInliner) { + for { + method <- optimizer.findTraitImpl(classInfo.encodedName).methods.values + if (classInfo.methodInfos(method.encodedName).isReachable) + } { + addTree(method.desugaredDef) + } + } else { + addReachableMethods(classEmitter.genTraitImplMethod) + } + } else if (!classInfo.hasMoreThanData) { + // there is only the data anyway + addTree(d.wholeClass.getOrElseUpdate( + classEmitter.genClassDef(classDef))) + } else { + if (classInfo.isAnySubclassInstantiated) { + addTree(d.constructor.getOrElseUpdate( + classEmitter.genConstructor(classDef))) + if (useInliner) { + for { + method <- optimizer.findClass(classInfo.encodedName).methods.values + if (classInfo.methodInfos(method.encodedName).isReachable) + } { + addTree(method.desugaredDef) + } + } else { + addReachableMethods(classEmitter.genMethod) + } + addTree(d.exportedMembers.getOrElseUpdate(js.Block { + classDef.defs collect { + case m: MethodDef if m.name.isInstanceOf[StringLiteral] => + classEmitter.genMethod(classInfo.encodedName, m) + case p: PropertyDef => + classEmitter.genProperty(classInfo.encodedName, p) + } + }(classDef.pos))) + } + if (classInfo.isDataAccessed) { + addTree(d.typeData.getOrElseUpdate(js.Block( + classEmitter.genInstanceTests(classDef), + classEmitter.genArrayInstanceTests(classDef), + classEmitter.genTypeData(classDef) + )(classDef.pos))) + } + if (classInfo.isAnySubclassInstantiated) + addTree(d.setTypeData.getOrElseUpdate( + classEmitter.genSetTypeData(classDef))) + if (classInfo.isModuleAccessed) + addTree(d.moduleAccessor.getOrElseUpdate( + classEmitter.genModuleAccessor(classDef))) + addTree(d.classExports.getOrElseUpdate( + classEmitter.genClassExports(classDef))) + } + } + + + for { + classInfo <- analyzer.classInfos.values.toSeq.sortWith(compareClassInfo) + if classInfo.isNeededAtAll + } { + val optPersistentFile = + persistentState.encodedNameToPersistentFile.get(classInfo.encodedName) + + // if we have a persistent file, this is not a dummy class + optPersistentFile.fold { + if (classInfo.isAnySubclassInstantiated) { + // Subclass will emit constructor that references this dummy class. + // Therefore, we need to emit a dummy parent. + builder.addJSTree( + classEmitter.genDummyParent(classInfo.encodedName)) + } + } { pf => addPersistentFile(classInfo, pf) } + } + } +} + +object ScalaJSOptimizer { + /** Inputs of the Scala.js optimizer. */ + final case class Inputs[T]( + /** The CompleteNCClasspath or the IR files to be packaged. */ + input: T, + /** Manual additions to reachability */ + manuallyReachable: Seq[ManualReachability] = Nil, + /** Elements we won't warn even if they don't exist */ + noWarnMissing: Seq[NoWarnMissing] = Nil + ) + + sealed abstract class ManualReachability + final case class ReachObject(name: String) extends ManualReachability + final case class Instantiate(name: String) extends ManualReachability + final case class ReachMethod(className: String, methodName: String, + static: Boolean) extends ManualReachability + + sealed abstract class NoWarnMissing + final case class NoWarnClass(className: String) extends NoWarnMissing + final case class NoWarnMethod(className: String, methodName: String) + extends NoWarnMissing + + /** Configurations relevant to the optimizer */ + trait OptimizerConfig { + /** Ask to produce source map for the output. Is used in the incremental + * optimizer to decide whether a position change should trigger re-inlining + */ + val wantSourceMap: Boolean + /** If true, performs expensive checks of the IR for the used parts. */ + val checkIR: Boolean + /** If true, the optimizer removes trees that have not been used in the + * last run from the cache. Otherwise, all trees that has been used once, + * are kept in memory. */ + val unCache: Boolean + /** If true, no optimizations are performed */ + val disableOptimizer: Boolean + /** If true, nothing is performed incrementally */ + val batchMode: Boolean + } + + /** Configuration for the output of the Scala.js optimizer. */ + final case class OutputConfig( + /** Writer for the output. */ + output: WritableVirtualJSFile, + /** Cache file */ + cache: Option[WritableVirtualTextFile] = None, + /** Ask to produce source map for the output */ + wantSourceMap: Boolean = false, + /** Base path to relativize paths in the source map. */ + relativizeSourceMapBase: Option[URI] = None, + /** If true, performs expensive checks of the IR for the used parts. */ + checkIR: Boolean = false, + /** If true, the optimizer removes trees that have not been used in the + * last run from the cache. Otherwise, all trees that has been used once, + * are kept in memory. */ + unCache: Boolean = true, + /** If true, no optimizations are performed */ + disableOptimizer: Boolean = false, + /** If true, nothing is performed incrementally */ + batchMode: Boolean = false + ) extends OptimizerConfig + + // Private helpers ----------------------------------------------------------- + + private final class PersistentState { + val files = mutable.Map.empty[String, PersistentIRFile] + val encodedNameToPersistentFile = + mutable.Map.empty[String, PersistentIRFile] + + var statsReused: Int = 0 + var statsInvalidated: Int = 0 + var statsTreesRead: Int = 0 + + var wasWithSourceMap: Boolean = true + + def startRun(): Unit = { + statsReused = 0 + statsInvalidated = 0 + statsTreesRead = 0 + for (file <- files.values) + file.startRun() + } + + def getPersistentIRFile(irFile: VirtualScalaJSIRFile): PersistentIRFile = { + val file = files.getOrElseUpdate(irFile.path, + new PersistentIRFile(irFile.path)) + if (file.updateFile(irFile)) + statsReused += 1 + else + statsInvalidated += 1 + encodedNameToPersistentFile += ((file.info.encodedName, file)) + file + } + + def endRun(unCache: Boolean): Unit = { + // "Garbage-collect" persisted versions of files that have disappeared + files.retain((_, f) => f.cleanAfterRun(unCache)) + encodedNameToPersistentFile.clear() + } + } + + private final class PersistentIRFile(val path: String) { + import ir.Trees._ + + private[this] var existedInThisRun: Boolean = false + private[this] var desugaredUsedInThisRun: Boolean = false + + private[this] var irFile: VirtualScalaJSIRFile = null + private[this] var version: Option[String] = None + private[this] var _info: Infos.ClassInfo = null + private[this] var _tree: ClassDef = null + private[this] var _desugared: Desugared = null + + def startRun(): Unit = { + existedInThisRun = false + desugaredUsedInThisRun = false + } + + def updateFile(irFile: VirtualScalaJSIRFile): Boolean = { + existedInThisRun = true + this.irFile = irFile + if (version.isDefined && version == irFile.version) { + // yeepeeh, nothing to do + true + } else { + version = irFile.version + _info = irFile.info + _tree = null + _desugared = null + false + } + } + + def info: Infos.ClassInfo = _info + + def desugared: Desugared = { + desugaredUsedInThisRun = true + if (_desugared == null) + _desugared = new Desugared + _desugared + } + + def tree: ClassDef = { + if (_tree == null) + _tree = irFile.tree + _tree + } + + def treeIfChanged(lastVersion: Option[String]): Option[(ClassDef, Option[String])] = { + if (lastVersion.isDefined && lastVersion == version) None + else Some((tree, version)) + } + + /** Returns true if this file should be kept for the next run at all. */ + def cleanAfterRun(unCache: Boolean): Boolean = { + irFile = null + if (unCache && !desugaredUsedInThisRun) + _desugared = null // free desugared if unused in this run + existedInThisRun + } + } + + private final class Desugared { + // for class kinds that are not decomposed + val wholeClass = new OneTimeCache[js.Tree] + + val constructor = new OneTimeCache[js.Tree] + val methodNames = new OneTimeCache[List[String]] + val methods = mutable.Map.empty[String, js.Tree] + val exportedMembers = new OneTimeCache[js.Tree] + val typeData = new OneTimeCache[js.Tree] + val setTypeData = new OneTimeCache[js.Tree] + val moduleAccessor = new OneTimeCache[js.Tree] + val classExports = new OneTimeCache[js.Tree] + } + + private final class OneTimeCache[A >: Null] { + private[this] var value: A = null + def getOrElseUpdate(v: => A): A = { + if (value == null) + value = v + value + } + } +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/sem/CheckedBehavior.scala b/tools/shared/src/main/scala/scala/scalajs/tools/sem/CheckedBehavior.scala new file mode 100644 index 0000000..4668b3c --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/sem/CheckedBehavior.scala @@ -0,0 +1,24 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.sem + +sealed abstract class CheckedBehavior { + import CheckedBehavior._ + def optimized: CheckedBehavior = this match { + case Fatal => Unchecked + case _ => this + } +} + +object CheckedBehavior { + case object Compliant extends CheckedBehavior + case object Fatal extends CheckedBehavior + case object Unchecked extends CheckedBehavior +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/sem/Semantics.scala b/tools/shared/src/main/scala/scala/scalajs/tools/sem/Semantics.scala new file mode 100644 index 0000000..9d17b06 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/sem/Semantics.scala @@ -0,0 +1,97 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.sem + +import scala.collection.immutable.Traversable + +final class Semantics private ( + val asInstanceOfs: CheckedBehavior, + val strictFloats: Boolean) { + + import Semantics._ + + def withAsInstanceOfs(behavior: CheckedBehavior): Semantics = + copy(asInstanceOfs = behavior) + + def withStrictFloats(strictFloats: Boolean): Semantics = + copy(strictFloats = strictFloats) + + def optimized: Semantics = + copy(asInstanceOfs = this.asInstanceOfs.optimized) + + override def equals(that: Any): Boolean = that match { + case that: Semantics => + this.asInstanceOfs == that.asInstanceOfs && + this.strictFloats == that.strictFloats + case _ => + false + } + + override def hashCode(): Int = { + import scala.util.hashing.MurmurHash3._ + var acc = HashSeed + acc = mix(acc, asInstanceOfs.hashCode) + acc = mixLast(acc, strictFloats.##) + finalizeHash(acc, 1) + } + + override def toString(): String = { + s"""Semantics( + | asInstanceOfs = $asInstanceOfs, + | strictFloats = $strictFloats + |)""".stripMargin + } + + /** Checks whether the given semantics setting is Java compliant */ + def isCompliant(name: String): Boolean = name match { + case "asInstanceOfs" => asInstanceOfs == CheckedBehavior.Compliant + case "strictFloats" => strictFloats + case _ => false + } + + /** Retrieve a list of semantics which are set to compliant */ + def compliants: List[String] = { + def cl(name: String, cond: Boolean) = if (cond) List(name) else Nil + + cl("asInstanceOfs", asInstanceOfs == CheckedBehavior.Compliant) ++ + cl("strictFloats", strictFloats) + } + + private def copy( + asInstanceOfs: CheckedBehavior = this.asInstanceOfs, + strictFloats: Boolean = this.strictFloats): Semantics = { + new Semantics( + asInstanceOfs = asInstanceOfs, + strictFloats = strictFloats) + } +} + +object Semantics { + private val HashSeed = + scala.util.hashing.MurmurHash3.stringHash(classOf[Semantics].getName) + + val Defaults: Semantics = new Semantics( + asInstanceOfs = CheckedBehavior.Fatal, + strictFloats = false) + + def compliantTo(semantics: Traversable[String]): Semantics = { + import Defaults._ + import CheckedBehavior._ + + val semsSet = semantics.toSet + + def sw[T](name: String, compliant: T, default: T): T = + if (semsSet.contains(name)) compliant else default + + new Semantics( + asInstanceOfs = sw("asInstanceOfs", Compliant, asInstanceOfs), + strictFloats = sw("strictFloats", true, strictFloats)) + } +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/sourcemap/JSFileBuilder.scala b/tools/shared/src/main/scala/scala/scalajs/tools/sourcemap/JSFileBuilder.scala new file mode 100644 index 0000000..1bf2254 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/sourcemap/JSFileBuilder.scala @@ -0,0 +1,144 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.sourcemap + +import scala.annotation.tailrec + +import scala.collection.mutable + +import java.io._ +import java.util.regex.Pattern +import java.net.{ URI, URISyntaxException } + +import scala.scalajs.ir.Position +import scala.scalajs.tools.{javascript => js} +import scala.scalajs.tools.io._ +import scala.scalajs.tools.optimizer.JSTreeBuilder + +class JSFileBuilder(val name: String, + protected val outputWriter: Writer) extends JSTreeBuilder { + def addLine(line: String): Unit = { + outputWriter.write(line) + outputWriter.write('\n') + } + + def addLines(lines: Seq[String]): Unit = + lines.foreach(addLine) + + def addFile(file: VirtualJSFile): Unit = + addPartsOfFile(file)(!_.startsWith("//# sourceMappingURL=")) + + def addPartsOfFile(file: VirtualJSFile)(selector: String => Boolean): Unit = { + for (line <- file.readLines() if selector(line)) + addLine(line) + } + + /** Add a JavaScript tree representing a statement. + * The tree must be a valid JavaScript tree (typically obtained by + * desugaring a full-fledged IR tree). + */ + def addJSTree(tree: js.Trees.Tree): Unit = { + val printer = new js.Printers.JSTreePrinter(outputWriter) + printer.printTopLevelTree(tree) + // Do not close the printer: we do not have ownership of the writers + } + + /** Closes the underlying writer(s). + */ + def closeWriters(): Unit = { + outputWriter.close() + } +} + +class JSFileBuilderWithSourceMapWriter(n: String, ow: Writer, + protected val sourceMapWriter: SourceMapWriter) + extends JSFileBuilder(n, ow) { + + override def addLine(line: String): Unit = { + super.addLine(line) + sourceMapWriter.nextLine() + } + + private final val NotSelected = -1 + + override def addPartsOfFile(file: VirtualJSFile)( + selector: String => Boolean): Unit = { + val br = new BufferedReader(file.reader) + try { + // Select lines, and remember offsets + val offsets = new mutable.ArrayBuffer[Int] // (maybe NotSelected) + val selectedLineLengths = new mutable.ArrayBuffer[Int] + var line: String = br.readLine() + var selectedCount = 0 + while (line != null) { + if (selector(line)) { + super.addLine(line) // super call not to advance line in source map + offsets += selectedCount + selectedLineLengths += line.length + selectedCount += 1 + } else { + offsets += NotSelected + } + line = br.readLine() + } + + /* We ignore a potential source map. + * This happens typically for corejslib.js and other helper files + * written directly in JS. + * We generate a fake line-by-line source map for these on the fly + */ + val sourceFile = file.toURI + + for (lineNumber <- 0 until offsets.size) { + val offset = offsets(lineNumber) + if (offset != NotSelected) { + val originalPos = Position(sourceFile, lineNumber, 0) + sourceMapWriter.startNode(0, originalPos, None) + sourceMapWriter.endNode(selectedLineLengths(offset)) + sourceMapWriter.nextLine() + } + } + } finally { + br.close() + } + } + + override def addJSTree(tree: js.Trees.Tree): Unit = { + val printer = new js.Printers.JSTreePrinterWithSourceMap( + outputWriter, sourceMapWriter) + printer.printTopLevelTree(tree) + // Do not close the printer: we do not have ownership of the writers + } + + override def complete(): Unit = { + super.complete() + sourceMapWriter.complete() + } + +} + +class JSFileBuilderWithSourceMap(n: String, ow: Writer, + sourceMapOutputWriter: Writer, + relativizeSourceMapBasePath: Option[URI] = None) + extends JSFileBuilderWithSourceMapWriter( + n, ow, + new SourceMapWriter(sourceMapOutputWriter, n, + relativizeSourceMapBasePath)) { + + override def complete(): Unit = { + addLine("//# sourceMappingURL=" + name + ".map") + super.complete() + } + + override def closeWriters(): Unit = { + super.closeWriters() + sourceMapOutputWriter.close() + } +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/sourcemap/SourceMapWriter.scala b/tools/shared/src/main/scala/scala/scalajs/tools/sourcemap/SourceMapWriter.scala new file mode 100644 index 0000000..5d8bdb1 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/sourcemap/SourceMapWriter.scala @@ -0,0 +1,213 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.sourcemap + +import java.io.Writer +import java.net.URI + +import scala.collection.mutable.{ ListBuffer, HashMap, Stack, StringBuilder } + +import scala.scalajs.ir +import ir.Position +import ir.Position._ +import ir.Utils + +object SourceMapWriter { + private val Base64Map = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789+/" + + // Some constants for writeBase64VLQ + // Each base-64 digit covers 6 bits, but 1 is used for the continuation + private final val VLQBaseShift = 5 + private final val VLQBase = 1 << VLQBaseShift + private final val VLQBaseMask = VLQBase - 1 + private final val VLQContinuationBit = VLQBase + + private def jsonString(s: String) = + "\"" + Utils.escapeJS(s) + "\"" +} + +class SourceMapWriter( + val out: Writer, + val generatedFile: String, + val relativizeBaseURI: Option[URI] = None) { + + import SourceMapWriter._ + + private val sources = new ListBuffer[String] + private val _srcToIndex = new HashMap[SourceFile, Int] + + private val names = new ListBuffer[String] + private val _nameToIndex = new HashMap[String, Int] + + private val nodePosStack = new Stack[(Position, Option[String])] + nodePosStack.push((NoPosition, None)) + + private var lineCountInGenerated = 0 + private var lastColumnInGenerated = 0 + private var firstSegmentOfLine = true + private var lastSource: SourceFile = null + private var lastSourceIndex = 0 + private var lastLine: Int = 0 + private var lastColumn: Int = 0 + private var lastNameIndex: Int = 0 + + private var pendingColumnInGenerated: Int = -1 + private var pendingPos: Position = NoPosition + private var pendingName: Option[String] = None + + writeHeader() + + private def sourceToIndex(source: SourceFile) = + _srcToIndex.getOrElseUpdate(source, + (sources += sourceToURI(source)).size-1) + + /** Relatively hacky way to get a Web-friendly URI to the source file */ + private def sourceToURI(source: SourceFile): String = { + val uri = source + val relURI = relativizeBaseURI.fold(uri)(Utils.relativize(_, uri)) + + Utils.fixFileURI(relURI).toASCIIString + } + + private def nameToIndex(name: String) = + _nameToIndex.getOrElseUpdate(name, (names += name).size-1) + + private def writeHeader(): Unit = { + out.write("{\n") + out.write("\"version\": 3,\n") + out.write("\"file\": " + jsonString(generatedFile) + ",\n") + out.write("\"mappings\": \"") + } + + def nextLine(): Unit = { + writePendingSegment() + out.write(';') + lineCountInGenerated += 1 + lastColumnInGenerated = 0 + firstSegmentOfLine = true + pendingColumnInGenerated = -1 + pendingPos = nodePosStack.top._1 + pendingName = nodePosStack.top._2 + } + + def startNode(column: Int, originalPos: Position, + originalName: Option[String] = None): Unit = { + nodePosStack.push((originalPos, originalName)) + startSegment(column, originalPos, originalName) + } + + def endNode(column: Int): Unit = { + nodePosStack.pop() + startSegment(column, nodePosStack.top._1, nodePosStack.top._2) + } + + private def startSegment(startColumn: Int, originalPos: Position, + originalName: Option[String]): Unit = { + // There is no point in outputting a segment with the same information + if ((originalPos == pendingPos) && (originalName == pendingName)) + return + + // Write pending segment if it covers a non-empty range + if (startColumn != pendingColumnInGenerated) + writePendingSegment() + + // New pending + pendingColumnInGenerated = startColumn + pendingPos = originalPos + pendingName = + if (startColumn != pendingColumnInGenerated) originalName + else pendingName orElse originalName + } + + private def writePendingSegment() { + if (pendingColumnInGenerated < 0) + return + + // Segments of a line are separated by ',' + if (firstSegmentOfLine) firstSegmentOfLine = false + else out.write(',') + + // Generated column field + writeBase64VLQ(pendingColumnInGenerated-lastColumnInGenerated) + lastColumnInGenerated = pendingColumnInGenerated + + // If the position is NoPosition, stop here + if (!pendingPos.isDefined) + return + + // Extract relevant properties of pendingPos + val source = pendingPos.source + val line = pendingPos.line + val column = pendingPos.column + + // Source index field + if (source == lastSource) { // highly likely + writeBase64VLQ0() + } else { + val sourceIndex = sourceToIndex(source) + writeBase64VLQ(sourceIndex-lastSourceIndex) + lastSource = source + lastSourceIndex = sourceIndex + } + + // Line field + writeBase64VLQ(line - lastLine) + lastLine = line + + // Column field + writeBase64VLQ(column - lastColumn) + lastColumn = column + + // Name field + if (pendingName.isDefined) { + val nameIndex = nameToIndex(pendingName.get) + writeBase64VLQ(nameIndex-lastNameIndex) + lastNameIndex = nameIndex + } + } + + def complete(): Unit = { + writePendingSegment() + + out.write("\",\n") + out.write( + sources.map(jsonString(_)).mkString("\"sources\": [", ", ", "],\n")) + out.write( + names.map(jsonString(_)).mkString("\"names\": [", ", ", "],\n")) + out.write("\"lineCount\": "+lineCountInGenerated+"\n") + out.write("}\n") + } + + /** Write the Base 64 VLQ of an integer to the mappings + * Inspired by the implementation in Closure Compiler: + * http://code.google.com/p/closure-compiler/source/browse/src/com/google/debugging/sourcemap/Base64VLQ.java + */ + private def writeBase64VLQ(value0: Int): Unit = { + // Sign is encoded in the least significant bit + var value = + if (value0 < 0) ((-value0) << 1) + 1 + else value0 << 1 + + // Write as many base-64 digits as necessary to encode value + do { + var digit = value & VLQBaseMask + value = value >>> VLQBaseShift + if (value != 0) + digit |= VLQContinuationBit + out.write(Base64Map.charAt(digit)) + } while (value != 0) + } + + private def writeBase64VLQ0(): Unit = + out.write('A') +} |