From 2c4b142503bd2d871e6818b5cab8c38627d9e4a0 Mon Sep 17 00:00:00 2001 From: Haoyi Li Date: Wed, 26 Nov 2014 00:45:31 -0800 Subject: Squashed 'examples/scala-js/' content from commit 47311ba git-subtree-dir: examples/scala-js git-subtree-split: 47311ba693f949f204f27ea9475bb63425fbd4f3 --- .../classpath/builder/JarLibClasspathBuilder.scala | 13 + .../builder/PartialClasspathBuilder.scala | 38 ++ .../classpath/builder/PhysicalFileSystem.scala | 41 +++ .../scala/scalajs/tools/io/FileVirtualFiles.scala | 157 ++++++++ .../main/scala/scala/scalajs/tools/json/Impl.scala | 38 ++ .../tools/optimizer/ClosureAstBuilder.scala | 47 +++ .../tools/optimizer/ClosureAstTransformer.scala | 397 +++++++++++++++++++++ .../scalajs/tools/optimizer/ConcurrencyUtils.scala | 74 ++++ .../tools/optimizer/LoggerErrorManager.scala | 38 ++ .../scalajs/tools/optimizer/ParIncOptimizer.scala | 188 ++++++++++ .../tools/optimizer/ScalaJSClosureOptimizer.scala | 216 +++++++++++ .../scalajs/tools/sourcemap/SourceMapper.scala | 88 +++++ tools/jvm/src/test/resources/test.jar | 0 .../test/ClasspathElementsTraverserTest.scala | 42 +++ .../classpath/builder/test/JarBuilderTest.scala | 74 ++++ 15 files changed, 1451 insertions(+) create mode 100644 tools/jvm/src/main/scala/scala/scalajs/tools/classpath/builder/JarLibClasspathBuilder.scala create mode 100644 tools/jvm/src/main/scala/scala/scalajs/tools/classpath/builder/PartialClasspathBuilder.scala create mode 100644 tools/jvm/src/main/scala/scala/scalajs/tools/classpath/builder/PhysicalFileSystem.scala create mode 100644 tools/jvm/src/main/scala/scala/scalajs/tools/io/FileVirtualFiles.scala create mode 100644 tools/jvm/src/main/scala/scala/scalajs/tools/json/Impl.scala create mode 100644 tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ClosureAstBuilder.scala create mode 100644 tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ClosureAstTransformer.scala create mode 100644 tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ConcurrencyUtils.scala create mode 100644 tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/LoggerErrorManager.scala create mode 100644 tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ParIncOptimizer.scala create mode 100644 tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ScalaJSClosureOptimizer.scala create mode 100644 tools/jvm/src/main/scala/scala/scalajs/tools/sourcemap/SourceMapper.scala create mode 100644 tools/jvm/src/test/resources/test.jar create mode 100644 tools/jvm/src/test/scala/scala/scalajs/tools/classpath/builder/test/ClasspathElementsTraverserTest.scala create mode 100644 tools/jvm/src/test/scala/scala/scalajs/tools/classpath/builder/test/JarBuilderTest.scala (limited to 'tools/jvm/src') diff --git a/tools/jvm/src/main/scala/scala/scalajs/tools/classpath/builder/JarLibClasspathBuilder.scala b/tools/jvm/src/main/scala/scala/scalajs/tools/classpath/builder/JarLibClasspathBuilder.scala new file mode 100644 index 0000000..5bc488c --- /dev/null +++ b/tools/jvm/src/main/scala/scala/scalajs/tools/classpath/builder/JarLibClasspathBuilder.scala @@ -0,0 +1,13 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.classpath.builder + +class JarLibClasspathBuilder extends AbstractJarLibClasspathBuilder + with PhysicalFileSystem diff --git a/tools/jvm/src/main/scala/scala/scalajs/tools/classpath/builder/PartialClasspathBuilder.scala b/tools/jvm/src/main/scala/scala/scalajs/tools/classpath/builder/PartialClasspathBuilder.scala new file mode 100644 index 0000000..626c74c --- /dev/null +++ b/tools/jvm/src/main/scala/scala/scalajs/tools/classpath/builder/PartialClasspathBuilder.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.classpath._ +import scala.collection.immutable.Seq + +/** + * Allows to create a PartialClasspathBuilder from a (filesystem) classpath + * + * Rules for classpath reading: + * - IR goes to scalaJSIR + * - Descends into JARs + * - Entries stay in order of ‘cp‘, IR remains unordered + * - Earlier IR entries shadow later IR entries with the same relative path + * - JS goes to availableLibs (earlier libs take precedence) + * - JS_DEPENDENCIES are added to dependencies + */ +class PartialClasspathBuilder extends AbstractPartialClasspathBuilder + with PhysicalFileSystem + +object PartialClasspathBuilder { + /** Convenience method. The same as + * + * {{{ + * (new PartialClasspathBuilder).build(cp) + * }}} + */ + def build(cp: Seq[java.io.File]): PartialClasspath = + (new PartialClasspathBuilder).build(cp) +} diff --git a/tools/jvm/src/main/scala/scala/scalajs/tools/classpath/builder/PhysicalFileSystem.scala b/tools/jvm/src/main/scala/scala/scalajs/tools/classpath/builder/PhysicalFileSystem.scala new file mode 100644 index 0000000..a0dd7a5 --- /dev/null +++ b/tools/jvm/src/main/scala/scala/scalajs/tools/classpath/builder/PhysicalFileSystem.scala @@ -0,0 +1,41 @@ +package scala.scalajs.tools.classpath.builder + +import scala.scalajs.tools.io._ + +import scala.collection.immutable.Traversable + +import java.io._ + +/** FileSystem implementation using java.io._ */ +trait PhysicalFileSystem extends FileSystem { + + type File = java.io.File + + val DummyVersion: String = "DUMMY_FILE" + + def isDirectory(f: File): Boolean = f.isDirectory + def isFile(f: File): Boolean = f.isFile + def isJSFile(f: File): Boolean = f.isFile && f.getName.endsWith(".js") + def isIRFile(f: File): Boolean = f.isFile && f.getName.endsWith(".sjsir") + def isJARFile(f: File): Boolean = f.isFile && f.getName.endsWith(".jar") + def exists(f: File): Boolean = f.exists + + def getName(f: File): String = f.getName + def getAbsolutePath(f: File): String = f.getAbsolutePath + def getVersion(f: File): String = f.lastModified.toString + + def listFiles(d: File): Traversable[File] = { + require(d.isDirectory) + Option(d.listFiles).map(_.toList).getOrElse { + throw new IOException(s"Couldn't list files in $d") + } + } + + def toJSFile(f: File): VirtualJSFile = FileVirtualJSFile(f) + def toIRFile(f: File): VirtualScalaJSIRFile = FileVirtualScalaJSIRFile(f) + def toReader(f: File): Reader = + new BufferedReader(new FileReader(f)) + def toInputStream(f: File): InputStream = + new BufferedInputStream(new FileInputStream(f)) + +} diff --git a/tools/jvm/src/main/scala/scala/scalajs/tools/io/FileVirtualFiles.scala b/tools/jvm/src/main/scala/scala/scalajs/tools/io/FileVirtualFiles.scala new file mode 100644 index 0000000..da29225 --- /dev/null +++ b/tools/jvm/src/main/scala/scala/scalajs/tools/io/FileVirtualFiles.scala @@ -0,0 +1,157 @@ +package scala.scalajs.tools.io + +import scala.annotation.tailrec + +import java.io._ +import java.net.URI + +/** A [[VirtualFile]] implemented by an actual file on the file system. */ +class FileVirtualFile(val file: File) extends VirtualFile { + import FileVirtualFile._ + + override def path = file.getPath + + override def name = file.getName + + override def version: Option[String] = { + if (!file.isFile) None + else Some(file.lastModified.toString) + } + + override def exists: Boolean = file.exists + + override def toURI: URI = file.toURI +} + +object FileVirtualFile extends (File => FileVirtualFile) { + def apply(f: File): FileVirtualFile = + new FileVirtualFile(f) + + /** Tests whether the given file has the specified extension. + * Extension contain the '.', so a typical value for `ext` would be ".js". + * The comparison is case-sensitive. + */ + def hasExtension(file: File, ext: String): Boolean = + file.getName.endsWith(ext) + + /** Returns a new file with the same parent as the given file but a different + * name. + */ + def withName(file: File, newName: String): File = + new File(file.getParentFile(), newName) + + /** Returns a new file with the same path as the given file but a different + * extension. + * Extension contain the '.', so a typical value for `ext` would be ".js". + * Precondition: hasExtension(file, oldExt) + */ + def withExtension(file: File, oldExt: String, newExt: String): File = { + require(hasExtension(file, oldExt), + s"File $file does not have extension '$oldExt'") + withName(file, file.getName.stripSuffix(oldExt) + newExt) + } +} + +/** A [[VirtualTextFile]] implemented by an actual file on the file system. */ +class FileVirtualTextFile(f: File) extends FileVirtualFile(f) + with VirtualTextFile { + import FileVirtualTextFile._ + + override def content: String = readFileToString(file) + override def reader: Reader = new BufferedReader(new FileReader(f)) +} + +object FileVirtualTextFile extends (File => FileVirtualTextFile) { + def apply(f: File): FileVirtualTextFile = + new FileVirtualTextFile(f) + + /** Reads the entire content of a file as a UTF-8 string. */ + def readFileToString(file: File): String = { + val stream = new FileInputStream(file) + try IO.readInputStreamToString(stream) + finally stream.close() + } +} + +trait WritableFileVirtualTextFile extends FileVirtualTextFile + with WritableVirtualTextFile { + override def contentWriter: Writer = { + new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(file), "UTF-8")) + } +} + +object WritableFileVirtualTextFile { + def apply(f: File): WritableFileVirtualTextFile = + new FileVirtualTextFile(f) with WritableFileVirtualTextFile +} + +/** A [[VirtualBinaryFile]] implemented by an actual file on the file system. */ +class FileVirtualBinaryFile(f: File) extends FileVirtualFile(f) + with VirtualBinaryFile { + import FileVirtualBinaryFile._ + + override def inputStream: InputStream = + new BufferedInputStream(new FileInputStream(file)) + + override def content: Array[Byte] = + readFileToByteArray(file) +} + +object FileVirtualBinaryFile extends (File => FileVirtualBinaryFile) { + def apply(f: File): FileVirtualBinaryFile = + new FileVirtualBinaryFile(f) + + /** Reads the entire content of a file as byte array. */ + def readFileToByteArray(file: File): Array[Byte] = { + val stream = new FileInputStream(file) + try IO.readInputStreamToByteArray(stream) + finally stream.close() + } +} + +class FileVirtualJSFile(f: File) extends FileVirtualTextFile(f) + with VirtualJSFile { + import FileVirtualFile._ + import FileVirtualTextFile._ + + val sourceMapFile: File = withExtension(file, ".js", ".js.map") + + override def sourceMap: Option[String] = { + if (sourceMapFile.exists) Some(readFileToString(sourceMapFile)) + else None + } +} + +object FileVirtualJSFile extends (File => FileVirtualJSFile) { + def apply(f: File): FileVirtualJSFile = + new FileVirtualJSFile(f) +} + +trait WritableFileVirtualJSFile extends FileVirtualJSFile + with WritableFileVirtualTextFile + with WritableVirtualJSFile { + + override def sourceMapWriter: Writer = { + new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(sourceMapFile), "UTF-8")) + } +} + +object WritableFileVirtualJSFile { + def apply(f: File): WritableFileVirtualJSFile = + new FileVirtualJSFile(f) with WritableFileVirtualJSFile +} + +class FileVirtualScalaJSIRFile(f: File) + extends FileVirtualBinaryFile(f) with VirtualSerializedScalaJSIRFile + +object FileVirtualScalaJSIRFile extends (File => FileVirtualScalaJSIRFile) { + import FileVirtualFile._ + + def apply(f: File): FileVirtualScalaJSIRFile = + new FileVirtualScalaJSIRFile(f) + + def isScalaJSIRFile(file: File): Boolean = + hasExtension(file, ".sjsir") +} diff --git a/tools/jvm/src/main/scala/scala/scalajs/tools/json/Impl.scala b/tools/jvm/src/main/scala/scala/scalajs/tools/json/Impl.scala new file mode 100644 index 0000000..ea847e3 --- /dev/null +++ b/tools/jvm/src/main/scala/scala/scalajs/tools/json/Impl.scala @@ -0,0 +1,38 @@ +package scala.scalajs.tools.json + +import org.json.simple.JSONValue + +import scala.collection.JavaConverters._ + +import java.io.{Writer, Reader} + +private[json] object Impl extends AbstractJSONImpl { + + type Repr = Object + + def fromString(x: String): Repr = x + def fromNumber(x: Number): Repr = x + def fromBoolean(x: Boolean): Repr = java.lang.Boolean.valueOf(x) + def fromList(x: List[Repr]): Repr = x.asJava + def fromMap(x: Map[String, Repr]): Repr = x.asJava + + def toString(x: Repr): String = x.asInstanceOf[String] + def toNumber(x: Repr): Number = x.asInstanceOf[Number] + def toBoolean(x: Repr): Boolean = + x.asInstanceOf[java.lang.Boolean].booleanValue() + def toList(x: Repr): List[Repr] = + x.asInstanceOf[java.util.List[Repr]].asScala.toList + def toMap(x: Repr): Map[String, Repr] = + x.asInstanceOf[java.util.Map[String, Repr]].asScala.toMap + + def serialize(x: Repr): String = + JSONValue.toJSONString(x) + + def serialize(x: Repr, writer: Writer): Unit = + JSONValue.writeJSONString(x, writer) + + def deserialize(str: String): Repr = JSONValue.parse(str) + + def deserialize(reader: Reader): Repr = JSONValue.parse(reader) + +} diff --git a/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ClosureAstBuilder.scala b/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ClosureAstBuilder.scala new file mode 100644 index 0000000..8d2eb2b --- /dev/null +++ b/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ClosureAstBuilder.scala @@ -0,0 +1,47 @@ +package scala.scalajs.tools.optimizer + +import scala.scalajs.ir +import ir.Position.NoPosition + +import scala.scalajs.tools.javascript.Trees.Tree + +import com.google.javascript.rhino._ +import com.google.javascript.rhino.jstype.{StaticSourceFile, SimpleSourceFile} +import com.google.javascript.jscomp._ + +import scala.collection.mutable + +import java.net.URI + +class ClosureAstBuilder( + relativizeBaseURI: Option[URI] = None) extends JSTreeBuilder { + + private val transformer = new ClosureAstTransformer(relativizeBaseURI) + private val treeBuf = mutable.ListBuffer.empty[Node] + + def addJSTree(tree: Tree): Unit = + treeBuf += transformer.transformStat(tree)(NoPosition) + + lazy val closureAST: SourceAst = { + val root = transformer.setNodePosition(IR.script(treeBuf: _*), NoPosition) + + treeBuf.clear() + + new ClosureAstBuilder.ScalaJSSourceAst(root) + } + +} + +object ClosureAstBuilder { + // Dummy Source AST class + + class ScalaJSSourceAst(root: Node) extends SourceAst { + def getAstRoot(compiler: AbstractCompiler): Node = root + def clearAst(): Unit = () // Just for GC. Nonsensical here. + def getInputId(): InputId = root.getInputId() + def getSourceFile(): SourceFile = + root.getStaticSourceFile().asInstanceOf[SourceFile] + def setSourceFile(file: SourceFile): Unit = + if (getSourceFile() ne file) throw new IllegalStateException + } +} diff --git a/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ClosureAstTransformer.scala b/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ClosureAstTransformer.scala new file mode 100644 index 0000000..af22501 --- /dev/null +++ b/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ClosureAstTransformer.scala @@ -0,0 +1,397 @@ +package scala.scalajs.tools.optimizer + +import scala.scalajs.ir +import ir.Position +import ir.Position.NoPosition + +import scala.scalajs.tools.javascript.Trees._ + +import com.google.javascript.rhino._ +import com.google.javascript.jscomp._ + +import scala.collection.mutable +import scala.annotation.tailrec + +import java.net.URI + +class ClosureAstTransformer(val relativizeBaseURI: Option[URI] = None) { + + private val inputId = new InputId("Scala.js IR") + + private val dummySourceName = new java.net.URI("virtualfile:scala.js-ir") + + def transformStat(tree: Tree)(implicit parentPos: Position): Node = + innerTransformStat(tree, tree.pos orElse parentPos) + + private def innerTransformStat(tree: Tree, pos_in: Position): Node = { + implicit val pos = pos_in + + wrapTransform(tree) { + case VarDef(ident, _, EmptyTree) => + new Node(Token.VAR, transformName(ident)) + case VarDef(ident, _, rhs) => + val node = transformName(ident) + node.addChildToFront(transformExpr(rhs)) + new Node(Token.VAR, node) + case Skip() => + new Node(Token.EMPTY) + case Block(stats) => + transformBlock(stats, pos) + case Labeled(label, body) => + new Node(Token.LABEL, transformLabel(label), transformBlock(body)) + case Return(EmptyTree) => + new Node(Token.RETURN) + case Return(expr) => + new Node(Token.RETURN, transformExpr(expr)) + case If(cond, thenp, Skip()) => + new Node(Token.IF, transformExpr(cond), transformBlock(thenp)) + case If(cond, thenp, elsep) => + new Node(Token.IF, transformExpr(cond), + transformBlock(thenp), transformBlock(elsep)) + case While(cond, body, None) => + new Node(Token.WHILE, transformExpr(cond), transformBlock(body)) + case While(cond, body, Some(label)) => + val whileNode = + new Node(Token.WHILE, transformExpr(cond), transformBlock(body)) + new Node(Token.LABEL, transformLabel(label), + setNodePosition(whileNode, pos)) + case DoWhile(body, cond, None) => + new Node(Token.DO, transformBlock(body), transformExpr(cond)) + case DoWhile(body, cond, Some(label)) => + val doNode = + new Node(Token.DO, transformBlock(body), transformExpr(cond)) + new Node(Token.LABEL, transformLabel(label), + setNodePosition(doNode, pos)) + case Try(block, errVar, handler, EmptyTree) => + val catchPos = handler.pos orElse pos + val catchNode = + new Node(Token.CATCH, transformName(errVar), transformBlock(handler)) + val blockNode = + new Node(Token.BLOCK, setNodePosition(catchNode, catchPos)) + new Node(Token.TRY, transformBlock(block), + setNodePosition(blockNode, catchPos)) + case Try(block, _, EmptyTree, finalizer) => + val blockNode = setNodePosition(new Node(Token.BLOCK), pos) + new Node(Token.TRY, transformBlock(block), blockNode, + transformBlock(finalizer)) + case Try(block, errVar, handler, finalizer) => + val catchPos = handler.pos orElse pos + val catchNode = + new Node(Token.CATCH, transformName(errVar), transformBlock(handler)) + val blockNode = + new Node(Token.BLOCK, setNodePosition(catchNode, catchPos)) + new Node(Token.TRY, transformBlock(block), + setNodePosition(blockNode, catchPos), transformBlock(finalizer)) + case Throw(expr) => + new Node(Token.THROW, transformExpr(expr)) + case Break(None) => + new Node(Token.BREAK) + case Break(Some(label)) => + new Node(Token.BREAK, transformLabel(label)) + case Continue(None) => + new Node(Token.CONTINUE) + case Continue(Some(label)) => + new Node(Token.CONTINUE, transformLabel(label)) + + case Switch(selector, cases, default) => + val switchNode = new Node(Token.SWITCH, transformExpr(selector)) + + for ((expr, body) <- cases) { + val bodyNode = transformBlock(body) + bodyNode.putBooleanProp(Node.SYNTHETIC_BLOCK_PROP, true) + val caseNode = new Node(Token.CASE, transformExpr(expr), bodyNode) + switchNode.addChildToBack( + setNodePosition(caseNode, expr.pos orElse pos)) + } + + if (default != EmptyTree) { + val bodyNode = transformBlock(default) + bodyNode.putBooleanProp(Node.SYNTHETIC_BLOCK_PROP, true) + val caseNode = new Node(Token.DEFAULT_CASE, bodyNode) + switchNode.addChildToBack( + setNodePosition(caseNode, default.pos orElse pos)) + } + + switchNode + + case Debugger() => + new Node(Token.DEBUGGER) + case _ => + // We just assume it is an expression + new Node(Token.EXPR_RESULT, transformExpr(tree)) + } + } + + def transformExpr(tree: Tree)(implicit parentPos: Position): Node = + innerTransformExpr(tree, tree.pos orElse parentPos) + + private def innerTransformExpr(tree: Tree, pos_in: Position): Node = { + implicit val pos = pos_in + + wrapTransform(tree) { + case Block(exprs) => + exprs.map(transformExpr).reduceRight { (expr1, expr2) => + setNodePosition(new Node(Token.COMMA, expr1, expr2), pos) + } + case If(cond, thenp, elsep) => + new Node(Token.HOOK, transformExpr(cond), + transformExpr(thenp), transformExpr(elsep)) + case Assign(lhs, rhs) => + new Node(Token.ASSIGN, transformExpr(lhs), transformExpr(rhs)) + case New(ctor, args) => + val node = new Node(Token.NEW, transformExpr(ctor)) + args.foreach(arg => node.addChildToBack(transformExpr(arg))) + node + case DotSelect(qualifier, item) => + new Node(Token.GETPROP, transformExpr(qualifier), transformString(item)) + case BracketSelect(qualifier, item) => + new Node(Token.GETELEM, transformExpr(qualifier), transformExpr(item)) + + case Apply(fun, args) => + val node = new Node(Token.CALL, transformExpr(fun)) + args.foreach(arg => node.addChildToBack(transformExpr(arg))) + + // Closure needs to know (from the parser), if the call has a bound + // `this` or not. Since JSDesugar inserts protects calls if necessary, + // it is sufficient to check if we have a select as target + if (!fun.isInstanceOf[DotSelect] && + !fun.isInstanceOf[BracketSelect]) + node.putBooleanProp(Node.FREE_CALL, true) + + node + + case Delete(prop) => + new Node(Token.DELPROP, transformExpr(prop)) + case UnaryOp(op, lhs) => + mkUnaryOp(op, transformExpr(lhs)) + case BinaryOp(op, lhs, rhs) => + mkBinaryOp(op, transformExpr(lhs), transformExpr(rhs)) + case ArrayConstr(items) => + val node = new Node(Token.ARRAYLIT) + items.foreach(i => node.addChildToBack(transformExpr(i))) + node + + case ObjectConstr(fields) => + val node = new Node(Token.OBJECTLIT) + + for ((name, expr) <- fields) { + val fldNode = transformStringKey(name) + fldNode.addChildToBack(transformExpr(expr)) + node.addChildToBack(fldNode) + } + + node + + case Undefined() => + new Node(Token.VOID, setNodePosition(Node.newNumber(0.0), pos)) + case Null() => + new Node(Token.NULL) + case BooleanLiteral(value) => + if (value) new Node(Token.TRUE) else new Node(Token.FALSE) + case IntLiteral(value) => + Node.newNumber(value) + case DoubleLiteral(value) => + Node.newNumber(value) + case StringLiteral(value) => + Node.newString(value) + case VarRef(ident, _) => + transformName(ident) + case This() => + new Node(Token.THIS) + + case Function(args, body) => + // Note that a Function may also be a statement (when it is named), + // but Scala.js does not have such an IR node + val paramList = new Node(Token.PARAM_LIST) + args.foreach(arg => paramList.addChildToBack(transformParam(arg))) + + val emptyName = setNodePosition(Node.newString(Token.NAME, ""), pos) + + new Node(Token.FUNCTION, emptyName, paramList, transformBlock(body)) + + case _ => + throw new TransformException(s"Unknown tree of class ${tree.getClass()}") + } + } + + def transformParam(param: ParamDef)(implicit parentPos: Position): Node = + transformName(param.name) + + def transformName(ident: Ident)(implicit parentPos: Position): Node = + setNodePosition(Node.newString(Token.NAME, ident.name), + ident.pos orElse parentPos) + + def transformLabel(ident: Ident)(implicit parentPos: Position): Node = + setNodePosition(Node.newString(Token.LABEL_NAME, ident.name), + ident.pos orElse parentPos) + + def transformString(pName: PropertyName)(implicit parentPos: Position): Node = + setNodePosition(Node.newString(pName.name), pName.pos orElse parentPos) + + def transformStringKey(pName: PropertyName)( + implicit parentPos: Position): Node = { + val node = Node.newString(Token.STRING_KEY, pName.name) + + if (pName.isInstanceOf[StringLiteral]) + node.setQuotedString() + + setNodePosition(node, pName.pos orElse parentPos) + } + + def transformBlock(tree: Tree)(implicit parentPos: Position): Node = { + val pos = if (tree.pos.isDefined) tree.pos else parentPos + wrapTransform(tree) { + case Block(stats) => + transformBlock(stats, pos) + case tree => + transformBlock(List(tree), pos) + } (pos) + } + + def transformBlock(stats: List[Tree], blockPos: Position): Node = { + @inline def ctorDoc(node: Node) = { + val b = new JSDocInfoBuilder(false) + b.recordConstructor() + b.build(node) + } + + val block = new Node(Token.BLOCK) + + // The Rhino IR attaches DocComments to the following nodes (rather than + // having individual nodes). We preprocess these here. + @tailrec + def loop(ts: List[Tree], nextIsCtor: Boolean = false): Unit = ts match { + case DocComment(text) :: tss if text.startsWith("@constructor") => + loop(tss, nextIsCtor = true) + case DocComment(text) :: tss => + loop(tss) + case t :: tss => + val node = transformStat(t)(blockPos) + if (nextIsCtor) { + // The @constructor must be propagated through an ExprResult node + val trg = + if (node.isExprResult()) node.getChildAtIndex(0) + else node + + trg.setJSDocInfo(ctorDoc(trg)) + } + + block.addChildToBack(node) + + loop(tss) + case Nil => + } + + loop(stats) + + block + } + + @inline + private def wrapTransform(tree: Tree)(body: Tree => Node)( + implicit pos: Position): Node = { + try { + setNodePosition(body(tree), pos) + } catch { + case e: TransformException => + throw e // pass through + case e: RuntimeException => + throw new TransformException(tree, e) + } + } + + def setNodePosition(node: Node, pos: ir.Position): node.type = { + if (pos != ir.Position.NoPosition) { + attachSourceFile(node, pos.source) + node.setLineno(pos.line+1) + node.setCharno(pos.column) + } else { + attachSourceFile(node, dummySourceName) + } + node + } + + private def attachSourceFile(node: Node, source: URI): node.type = { + val str = sourceUriToString(source) + + node.setInputId(inputId) + node.setStaticSourceFile(new SourceFile(str)) + + node + } + + private def sourceUriToString(uri: URI): String = { + val relURI = relativizeBaseURI.fold(uri)(ir.Utils.relativize(_, uri)) + ir.Utils.fixFileURI(relURI).toASCIIString + } + + // Helpers for IR + @inline + private def mkUnaryOp(op: String, lhs: Node): Node = { + val tok = op match { + case "!" => Token.NOT + case "~" => Token.BITNOT + case "+" => Token.POS + case "-" => Token.NEG + case "typeof" => Token.TYPEOF + case _ => throw new TransformException(s"Unhandled unary op: $op") + } + + new Node(tok, lhs) + } + + @inline + private def mkBinaryOp(op: String, lhs: Node, rhs: Node): Node = { + val tok = op match { + case "|" => Token.BITOR + case "&" => Token.BITAND + case "^" => Token.BITXOR + case "==" => Token.EQ + case "!=" => Token.NE + case "<" => Token.LT + case "<=" => Token.LE + case ">" => Token.GT + case ">=" => Token.GE + case "<<" => Token.LSH + case ">>" => Token.RSH + case ">>>" => Token.URSH + case "+" => Token.ADD + case "-" => Token.SUB + case "*" => Token.MUL + case "/" => Token.DIV + case "%" => Token.MOD + case "===" => Token.SHEQ + case "!==" => Token.SHNE + case "in" => Token.IN + case "instanceof" => Token.INSTANCEOF + case "||" => Token.OR + case "&&" => Token.AND + case _ => + throw new TransformException(s"Unhandled binary op: $op") + } + + new Node(tok, lhs, rhs) + } + + // Exception wrapper in transforms + + class TransformException private (msg: String, e: Throwable) + extends RuntimeException(msg, e) { + + def this(tree: Tree, e: Throwable) = + this(TransformException.mkMsg(tree), e) + + def this(msg: String) = this(msg, null) + } + + object TransformException { + import ir.Printers._ + import java.io._ + + private def mkMsg(tree: Tree): String = { + "Exception while translating Scala.js JS tree to GCC IR at tree:\n" + + tree.show + } + } + +} diff --git a/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ConcurrencyUtils.scala b/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ConcurrencyUtils.scala new file mode 100644 index 0000000..471ed65 --- /dev/null +++ b/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ConcurrencyUtils.scala @@ -0,0 +1,74 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.optimizer + +import scala.annotation.tailrec + +import scala.collection.concurrent.TrieMap + +import java.util.concurrent.atomic._ + +private[optimizer] object ConcurrencyUtils { + + /** An atomic accumulator supports adding single elements and retrieving and + * deleting all contained elements */ + type AtomicAcc[T] = AtomicReference[List[T]] + + object AtomicAcc { + @inline final def empty[T]: AtomicAcc[T] = + new AtomicReference[List[T]](Nil) + @inline final def apply[T](l: List[T]): AtomicAcc[T] = + new AtomicReference(l) + } + + implicit class AtomicAccOps[T](val acc: AtomicAcc[T]) extends AnyVal { + @inline final def size: Int = acc.get.size + + @inline + final def +=(x: T): Unit = AtomicAccOps.append(acc, x) + + @inline + final def removeAll(): List[T] = AtomicAccOps.removeAll(acc) + } + + object AtomicAccOps { + @inline + @tailrec + private final def append[T](acc: AtomicAcc[T], x: T): Boolean = { + val oldV = acc.get + val newV = x :: oldV + acc.compareAndSet(oldV, newV) || append(acc, x) + } + + @inline + private final def removeAll[T](acc: AtomicAcc[T]): List[T] = + acc.getAndSet(Nil) + } + + type TrieSet[T] = TrieMap[T, Null] + + implicit class TrieSetOps[T](val set: TrieSet[T]) extends AnyVal { + @inline final def +=(x: T): Unit = set.put(x, null) + } + + object TrieSet { + @inline final def empty[T]: TrieSet[T] = TrieMap.empty + } + + implicit class TrieMapOps[K,V](val map: TrieMap[K,V]) extends AnyVal { + @inline final def getOrPut(k: K, default: => V): V = { + map.get(k).getOrElse { + val v = default + map.putIfAbsent(k, v).getOrElse(v) + } + } + } + +} diff --git a/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/LoggerErrorManager.scala b/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/LoggerErrorManager.scala new file mode 100644 index 0000000..d51dd7b --- /dev/null +++ b/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/LoggerErrorManager.scala @@ -0,0 +1,38 @@ +package scala.scalajs.tools.optimizer + +import com.google.javascript.jscomp.{ BasicErrorManager, CheckLevel, JSError } + +import scala.scalajs.tools.logging.Logger + +/** A Google Closure Error Manager that forwards to a tools.logging.Logger */ +class LoggerErrorManager(private val log: Logger) extends BasicErrorManager { + + /** Ugly hack to disable FRACTIONAL_BITWISE_OPERAND warnings. Since its + * DiagnosticType (PeepholeFoldConstants.FRACTIONAL_BITWISE_OPERAND) is + * package private. + */ + override def report(level: CheckLevel, error: JSError): Unit = { + if (error.getType.key == "JSC_FRACTIONAL_BITWISE_OPERAND") + super.report(CheckLevel.OFF, error) + else + super.report(level, error) + } + + def println(level: CheckLevel, error: JSError): Unit = level match { + case CheckLevel.WARNING => log.warn (s"Closure: ${error}") + case CheckLevel.ERROR => log.error(s"Closure: ${error}") + case CheckLevel.OFF => + } + + protected def printSummary(): Unit = { + val msg = s"Closure: ${getErrorCount} error(s), ${getWarningCount} warning(s)" + + if (getErrorCount > 0) + log.error(msg) + else if (getWarningCount > 0) + log.warn(msg) + else + log.info(msg) + } + +} diff --git a/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ParIncOptimizer.scala b/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ParIncOptimizer.scala new file mode 100644 index 0000000..422238e --- /dev/null +++ b/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ParIncOptimizer.scala @@ -0,0 +1,188 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ 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.concurrent.TrieMap +import scala.collection.parallel.mutable.{ParTrieMap, ParArray} + +import java.util.concurrent.atomic._ + +import scala.scalajs.tools.sem.Semantics + +import ConcurrencyUtils._ + +class ParIncOptimizer(semantics: Semantics) extends GenIncOptimizer(semantics) { + + protected object CollOps extends GenIncOptimizer.AbsCollOps { + type Map[K, V] = TrieMap[K, V] + type ParMap[K, V] = ParTrieMap[K, V] + type AccMap[K, V] = TrieMap[K, AtomicAcc[V]] + type ParIterable[V] = ParArray[V] + type Addable[V] = AtomicAcc[V] + + def emptyAccMap[K, V]: AccMap[K, V] = TrieMap.empty + def emptyMap[K, V]: Map[K, V] = TrieMap.empty + def emptyParMap[K, V]: ParMap[K, V] = ParTrieMap.empty + def emptyParIterable[V]: ParIterable[V] = ParArray.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.foreach { case (k, v) => + if (!p(k, v)) + map.remove(k) + } + } + + // Operations on AccMap + def acc[K, V](map: AccMap[K, V], k: K, v: V): Unit = + map.getOrPut(k, AtomicAcc.empty) += v + + def getAcc[K, V](map: AccMap[K, V], k: K): GenIterable[V] = + map.get(k).fold[Iterable[V]](Nil)(_.removeAll()).toParArray + + def parFlatMapKeys[A, B](map: AccMap[A, _])( + f: A => GenTraversableOnce[B]): GenIterable[B] = + map.keys.flatMap(f).toParArray + + // Operations on ParIterable + def prepAdd[V](it: ParIterable[V]): Addable[V] = + AtomicAcc(it.toList) + + def add[V](addable: Addable[V], v: V): Unit = + addable += v + + def finishAdd[V](addable: Addable[V]): ParIterable[V] = + addable.removeAll().toParArray + } + + private val _interfaces = TrieMap.empty[String, InterfaceType] + protected def getInterface(encodedName: String): InterfaceType = + _interfaces.getOrPut(encodedName, new ParInterfaceType(encodedName)) + + private val methodsToProcess: AtomicAcc[MethodImpl] = AtomicAcc.empty + protected def scheduleMethod(method: MethodImpl): Unit = + methodsToProcess += method + + protected def newMethodImpl(owner: MethodContainer, + encodedName: String): MethodImpl = new ParMethodImpl(owner, encodedName) + + protected def processAllTaggedMethods(): Unit = { + val methods = methodsToProcess.removeAll().toParArray + logProcessingMethods(methods.count(!_.deleted)) + for (method <- methods) + method.process() + } + + private class ParInterfaceType(encName: String) extends InterfaceType(encName) { + private val ancestorsAskers = TrieSet.empty[MethodImpl] + private val dynamicCallers = TrieMap.empty[String, TrieSet[MethodImpl]] + private val staticCallers = TrieMap.empty[String, TrieSet[MethodImpl]] + + private var _ancestors: List[String] = encodedName :: Nil + + private val _instantiatedSubclasses: TrieSet[Class] = TrieSet.empty + + /** PROCESS PASS ONLY. Concurrency safe except with + * [[addInstantiatedSubclass]] and [[removeInstantiatedSubclass]] + */ + def instantiatedSubclasses: Iterable[Class] = + _instantiatedSubclasses.keys + + /** UPDATE PASS ONLY. Concurrency safe except with + * [[instantiatedSubclasses]] + */ + def addInstantiatedSubclass(x: Class): Unit = + _instantiatedSubclasses += x + + /** UPDATE PASS ONLY. Concurrency safe except with + * [[instantiatedSubclasses]] + */ + def removeInstantiatedSubclass(x: Class): Unit = + _instantiatedSubclasses -= x + + /** PROCESS PASS ONLY. Concurrency safe except with [[ancestors_=]] */ + def ancestors: List[String] = _ancestors + + /** UPDATE PASS ONLY. Not concurrency safe. */ + def ancestors_=(v: List[String]): Unit = { + if (v != _ancestors) { + _ancestors = v + ancestorsAskers.keysIterator.foreach(_.tag()) + ancestorsAskers.clear() + } + } + + /** PROCESS PASS ONLY. Concurrency safe except with [[ancestors_=]]. */ + def registerAskAncestors(asker: MethodImpl): Unit = + ancestorsAskers += asker + + /** PROCESS PASS ONLY. */ + def registerDynamicCaller(methodName: String, caller: MethodImpl): Unit = + dynamicCallers.getOrPut(methodName, TrieSet.empty) += caller + + /** PROCESS PASS ONLY. */ + def registerStaticCaller(methodName: String, caller: MethodImpl): Unit = + staticCallers.getOrPut(methodName, TrieSet.empty) += caller + + /** UPDATE PASS ONLY. */ + def unregisterDependee(dependee: MethodImpl): Unit = { + ancestorsAskers -= dependee + dynamicCallers.valuesIterator.foreach(_ -= dependee) + staticCallers.valuesIterator.foreach(_ -= dependee) + } + + /** UPDATE PASS ONLY. */ + def tagDynamicCallersOf(methodName: String): Unit = + dynamicCallers.remove(methodName).foreach(_.keysIterator.foreach(_.tag())) + + /** UPDATE PASS ONLY. */ + def tagStaticCallersOf(methodName: String): Unit = + staticCallers.remove(methodName).foreach(_.keysIterator.foreach(_.tag())) + } + + private class ParMethodImpl(owner: MethodContainer, + encodedName: String) extends MethodImpl(owner, encodedName) { + + private val bodyAskers = TrieSet.empty[MethodImpl] + + /** PROCESS PASS ONLY. */ + def registerBodyAsker(asker: MethodImpl): Unit = + bodyAskers += asker + + /** UPDATE PASS ONLY. */ + def unregisterDependee(dependee: MethodImpl): Unit = + bodyAskers -= dependee + + /** UPDATE PASS ONLY. */ + def tagBodyAskers(): Unit = { + bodyAskers.keysIterator.foreach(_.tag()) + bodyAskers.clear() + } + + private val _registeredTo = AtomicAcc.empty[Unregisterable] + private val tagged = new AtomicBoolean(false) + + protected def registeredTo(intf: Unregisterable): Unit = + _registeredTo += intf + + protected def unregisterFromEverywhere(): Unit = { + _registeredTo.removeAll().foreach(_.unregisterDependee(this)) + } + + protected def protectTag(): Boolean = !tagged.getAndSet(true) + protected def resetTag(): Unit = tagged.set(false) + + } + +} diff --git a/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ScalaJSClosureOptimizer.scala b/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ScalaJSClosureOptimizer.scala new file mode 100644 index 0000000..146b2b8 --- /dev/null +++ b/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ScalaJSClosureOptimizer.scala @@ -0,0 +1,216 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.optimizer + +import scala.scalajs.tools.sem.Semantics +import scala.scalajs.tools.classpath._ +import scala.scalajs.tools.logging._ +import scala.scalajs.tools.io._ +import scala.scalajs.tools.corelib.CoreJSLibs + +import com.google.javascript.jscomp.{ + SourceFile => ClosureSource, + Compiler => ClosureCompiler, + CompilerOptions => ClosureOptions, + _ +} +import scala.collection.JavaConverters._ +import scala.collection.immutable.{Seq, Traversable} + +import java.net.URI + +/** Scala.js Closure optimizer: does advanced optimizations with Closure. */ +class ScalaJSClosureOptimizer(semantics: Semantics) { + import ScalaJSClosureOptimizer._ + + private def toClosureSource(file: VirtualJSFile) = + ClosureSource.fromReader(file.toURI.toString(), file.reader) + + private def toClosureInput(file: VirtualJSFile) = + new CompilerInput(toClosureSource(file)) + + /** Fully optimizes an [[IRClasspath]] by asking the ScalaJSOptimizer to + * emit a closure AST and then compiling this AST directly + */ + def optimizeCP(optimizer: ScalaJSOptimizer, + inputs: Inputs[ScalaJSOptimizer.Inputs[IRClasspath]], + outCfg: OutputConfig, logger: Logger): LinkedClasspath = { + + val cp = inputs.input.input + + CacheUtils.cached(cp.version, outCfg.output, outCfg.cache) { + logger.info(s"Full Optimizing ${outCfg.output.path}") + + val irFastOptInput = + inputs.input.copy(input = inputs.input.input.scalaJSIR) + val irFullOptInput = inputs.copy(input = irFastOptInput) + + optimizeIR(optimizer, irFullOptInput, outCfg, logger) + } + + new LinkedClasspath(cp.jsLibs, outCfg.output, cp.requiresDOM, cp.version) + } + + def optimizeIR(optimizer: ScalaJSOptimizer, + inputs: Inputs[ScalaJSOptimizer.Inputs[Traversable[VirtualScalaJSIRFile]]], + outCfg: OutputConfig, logger: Logger): Unit = { + + // Build Closure IR via ScalaJSOptimizer + val builder = new ClosureAstBuilder(outCfg.relativizeSourceMapBase) + + optimizer.optimizeIR(inputs.input, outCfg, builder, logger) + + // Build a Closure JSModule which includes the core libs + val module = new JSModule("Scala.js") + + for (lib <- CoreJSLibs.libs(semantics)) + module.add(toClosureInput(lib)) + + val ast = builder.closureAST + module.add(new CompilerInput(ast, ast.getInputId(), false)) + + for (export <- inputs.additionalExports) + module.add(toClosureInput(export)) + + // Compile the module + val closureExterns = + (ScalaJSExternsFile +: inputs.additionalExterns).map(toClosureSource) + + val options = closureOptions(outCfg) + val compiler = closureCompiler(logger) + + val result = GenIncOptimizer.logTime(logger, "Closure Compiler pass") { + compiler.compileModules( + closureExterns.asJava, List(module).asJava, options) + } + + GenIncOptimizer.logTime(logger, "Write Closure result") { + writeResult(result, compiler, outCfg.output) + } + } + + private def writeResult(result: Result, compiler: ClosureCompiler, + output: WritableVirtualJSFile): Unit = { + + val outputContent = if (result.errors.nonEmpty) "" + else "(function(){'use strict';" + compiler.toSource + "}).call(this);\n" + + val sourceMap = Option(compiler.getSourceMap()) + + // Write optimized code + val w = output.contentWriter + try { + w.write(outputContent) + if (sourceMap.isDefined) + w.write("//# sourceMappingURL=" + output.name + ".map\n") + } finally w.close() + + // Write source map (if available) + sourceMap.foreach { sm => + val w = output.sourceMapWriter + try sm.appendTo(w, output.name) + finally w.close() + } + } + + private def closureOptions(optConfig: OptimizerConfig, + noSourceMap: Boolean = false) = { + + val options = new ClosureOptions + options.prettyPrint = optConfig.prettyPrint + CompilationLevel.ADVANCED_OPTIMIZATIONS.setOptionsForCompilationLevel(options) + options.setLanguageIn(ClosureOptions.LanguageMode.ECMASCRIPT5) + options.setCheckGlobalThisLevel(CheckLevel.OFF) + + if (!noSourceMap && optConfig.wantSourceMap) { + options.setSourceMapOutputPath(optConfig.output.name + ".map") + options.setSourceMapDetailLevel(SourceMap.DetailLevel.ALL) + } + + options + } + + private def closureCompiler(logger: Logger) = { + val compiler = new ClosureCompiler + compiler.setErrorManager(new LoggerErrorManager(logger)) + compiler + } +} + +object ScalaJSClosureOptimizer { + /** Inputs of the Scala.js Closure optimizer. */ + final case class Inputs[T]( + /** Input to optimize (classpath or file-list) */ + input: T, + /** Additional externs files to be given to Closure. */ + additionalExterns: Seq[VirtualJSFile] = Nil, + /** Additional exports to be given to Closure. + * These files are just appended to the classpath, given to Closure, + * but not used in the Scala.js optimizer pass when direct optimizing + */ + additionalExports: Seq[VirtualJSFile] = Nil + ) + + /** Configuration the closure part of the optimizer needs. + * See [[OutputConfig]] for a description of the fields. + */ + trait OptimizerConfig { + val output: WritableVirtualJSFile + val cache: Option[WritableVirtualTextFile] + val wantSourceMap: Boolean + val prettyPrint: Boolean + val relativizeSourceMapBase: Option[URI] + } + + /** Configuration for the output of the Scala.js Closure optimizer */ + final case class OutputConfig( + /** Writer for the output */ + output: WritableVirtualJSFile, + /** Cache file */ + cache: Option[WritableVirtualTextFile] = 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, + /** Ask to produce source map for the output */ + wantSourceMap: Boolean = false, + /** Pretty-print the output. */ + prettyPrint: Boolean = false, + /** Base path to relativize paths in the source map */ + relativizeSourceMapBase: Option[URI] = None + ) extends OptimizerConfig with ScalaJSOptimizer.OptimizerConfig + + /** Minimal set of externs to compile Scala.js-emitted code with Closure. */ + val ScalaJSExterns = """ + /** @constructor */ + function Object() {} + Object.protoype.toString = function() {}; + /** @constructor */ + function Array() {} + Array.prototype.length = 0; + /** @constructor */ + function Function() {} + Function.prototype.constructor = function() {}; + Function.prototype.call = function() {}; + Function.prototype.apply = function() {}; + var global = {}; + var __ScalaJSEnv = {}; + """ + + val ScalaJSExternsFile = new MemVirtualJSFile("ScalaJSExterns.js"). + withContent(ScalaJSExterns) + +} diff --git a/tools/jvm/src/main/scala/scala/scalajs/tools/sourcemap/SourceMapper.scala b/tools/jvm/src/main/scala/scala/scalajs/tools/sourcemap/SourceMapper.scala new file mode 100644 index 0000000..6f856a9 --- /dev/null +++ b/tools/jvm/src/main/scala/scala/scalajs/tools/sourcemap/SourceMapper.scala @@ -0,0 +1,88 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.sourcemap + +import scala.scalajs.tools.classpath._ + +import com.google.debugging.sourcemap._ + +class SourceMapper(classpath: CompleteClasspath) { + + def map(ste: StackTraceElement, columnNumber: Int): StackTraceElement = { + val mapped = for { + sourceMap <- findSourceMap(ste.getFileName) + } yield map(ste, columnNumber, sourceMap) + + mapped.getOrElse(ste) + } + + def map(ste: StackTraceElement, columnNumber: Int, + sourceMap: String): StackTraceElement = { + + val sourceMapConsumer = + SourceMapConsumerFactory + .parse(sourceMap) + .asInstanceOf[SourceMapConsumerV3] + + /* **Attention** + * - StackTrace positions are 1-based + * - SourceMapConsumer.getMappingForLine() is 1-based + */ + + val lineNumber = ste.getLineNumber + val column = + if (columnNumber == -1) getFirstColumn(sourceMapConsumer, lineNumber) + else columnNumber + + val originalMapping = + sourceMapConsumer.getMappingForLine(lineNumber, column) + + if (originalMapping != null) { + new StackTraceElement( + ste.getClassName, + ste.getMethodName, + originalMapping.getOriginalFile, + originalMapping.getLineNumber) + } else ste + } + + /** both `lineNumber` and the resulting column are 1-based */ + private def getFirstColumn(sourceMapConsumer: SourceMapConsumerV3, + lineNumber1Based: Int) = { + + /* **Attention** + * - SourceMapConsumerV3.EntryVisitor is 0-based!!! + */ + + val lineNumber = lineNumber1Based - 1 + + var column: Option[Int] = None + + sourceMapConsumer.visitMappings( + new SourceMapConsumerV3.EntryVisitor { + def visit(sourceName: String, + symbolName: String, + sourceStartPosition: FilePosition, + startPosition: FilePosition, + endPosition: FilePosition): Unit = + if (!column.isDefined && startPosition.getLine == lineNumber) + column = Some(startPosition.getColumn) + }) + + val column0Based = column.getOrElse(0) + column0Based + 1 + } + + private def findSourceMap(path: String) = { + val candidates = classpath.allCode.filter(_.path == path) + if (candidates.size != 1) None // better no sourcemap than a wrong one + else candidates.head.sourceMap + } +} diff --git a/tools/jvm/src/test/resources/test.jar b/tools/jvm/src/test/resources/test.jar new file mode 100644 index 0000000..e69de29 diff --git a/tools/jvm/src/test/scala/scala/scalajs/tools/classpath/builder/test/ClasspathElementsTraverserTest.scala b/tools/jvm/src/test/scala/scala/scalajs/tools/classpath/builder/test/ClasspathElementsTraverserTest.scala new file mode 100644 index 0000000..72d01e4 --- /dev/null +++ b/tools/jvm/src/test/scala/scala/scalajs/tools/classpath/builder/test/ClasspathElementsTraverserTest.scala @@ -0,0 +1,42 @@ +package scala.scalajs.tools.classpath.builder.test + +import scala.scalajs.tools.classpath.builder._ +import scala.scalajs.tools.jsdep.JSDependencyManifest +import scala.scalajs.tools.io.VirtualScalaJSIRFile +import scala.scalajs.tools.io.VirtualJSFile + +import java.io.File + +import org.junit.Test +import org.junit.Assert._ + +class ClasspathElementsTraverserTest { + class TestElementTraverser extends ClasspathElementsTraverser with PhysicalFileSystem { + protected def handleDepManifest(m: => JSDependencyManifest): Unit = ??? + protected def handleIR(relPath: String, ir: => VirtualScalaJSIRFile): Unit = ??? + protected def handleJS(js: => VirtualJSFile): Unit = ??? + + def test(f: File): String = traverseClasspathElements(Seq(f)) + } + + val cpElementTraverser = new TestElementTraverser() + + @Test + def testNotExistingJarFile(): Unit = { + val res = cpElementTraverser.test(new File("dummy.jar")) + assertTrue(res.endsWith(cpElementTraverser.DummyVersion)) + } + + @Test + def testExistingDirectory(): Unit = { + val res = cpElementTraverser.test(new File("tools/jvm/src/test/resources")) + assertTrue(res.indexOf(cpElementTraverser.DummyVersion) < 0) + } + + @Test + def testExistingJarFile(): Unit = { + val res = cpElementTraverser.test(new File("tools/jvm/src/test/resources/test.jar")) + assertTrue(res.indexOf(cpElementTraverser.DummyVersion) < 0) + } +} + diff --git a/tools/jvm/src/test/scala/scala/scalajs/tools/classpath/builder/test/JarBuilderTest.scala b/tools/jvm/src/test/scala/scala/scalajs/tools/classpath/builder/test/JarBuilderTest.scala new file mode 100644 index 0000000..6ed68fc --- /dev/null +++ b/tools/jvm/src/test/scala/scala/scalajs/tools/classpath/builder/test/JarBuilderTest.scala @@ -0,0 +1,74 @@ +package scala.scalajs.tools.classpath.builder.test + +import scala.scalajs.tools.classpath.builder._ + +import org.junit.Test +import org.junit.Assert._ + +class JarBuilderTest { + + val jsFileContent = "window.alert('Hello World');" + val jsFileName = "lib.js" + + private def getJARInputStream = { + // Keep imports local so we don't accidentally use something we shouldn't + import java.util.zip._ + import java.io._ + + // Write a ZIP to a byte array + val outBuf = new ByteArrayOutputStream + val out = new ZipOutputStream(outBuf) + + out.putNextEntry(new ZipEntry(jsFileName)) + val w = new OutputStreamWriter(out, "UTF-8") + w.write(jsFileContent) + w.flush() + + out.close() + + new ByteArrayInputStream(outBuf.toByteArray) + } + + /** Trivial FS that has only one jar */ + trait TrivialFS extends FileSystem { + // Keep imports local so we don't accidentally use something we shouldn't + import java.io.{InputStream, Reader} + import scala.scalajs.tools.io._ + import scala.collection.immutable.Traversable + + type File = Unit + + val DummyVersion: String = "DUMMY" + + def isDirectory(f: File): Boolean = false + def isFile(f: File): Boolean = true + def isJSFile(f: File): Boolean = false + def isIRFile(f: File): Boolean = false + def isJARFile(f: File): Boolean = true + def exists(f: File): Boolean = true + + def getName(f: File): String = "jar" + def getAbsolutePath(f: File): String = "jar" + def getVersion(f: File): String = "" + + 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 = getJARInputStream + } + + class TestJARBuilder extends AbstractJarLibClasspathBuilder with TrivialFS + + @Test + def readInMemoryJarClasspath { + val builder = new TestJARBuilder + val cp = builder.build(Seq(())) + + assertArrayEquals( + Array[Object](jsFileName -> jsFileContent), + cp.availableLibs.mapValues(_.content).toArray[Object]) + } + +} -- cgit v1.2.3