/* __ *\
** ________ ___ / / ___ __ ____ 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)
}
}