summaryrefslogtreecommitdiff
path: root/tools/shared/src/main/scala/scala/scalajs/tools/javascript/ScalaJSClassEmitter.scala
diff options
context:
space:
mode:
Diffstat (limited to 'tools/shared/src/main/scala/scala/scalajs/tools/javascript/ScalaJSClassEmitter.scala')
-rw-r--r--tools/shared/src/main/scala/scala/scalajs/tools/javascript/ScalaJSClassEmitter.scala569
1 files changed, 569 insertions, 0 deletions
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)
+ }
+
+}