/* Scala.js compiler * Copyright 2013 LAMP/EPFL * @author Sébastien Doeraene */ package scala.scalajs.compiler import scala.collection.mutable import scala.tools.nsc._ import scala.math.PartialOrdering import scala.reflect.internal.Flags import scala.scalajs.ir import ir.{Trees => js, Types => jstpe} import util.ScopedVar import ScopedVar.withScopedVars /** Generation of exports for JavaScript * * @author Sébastien Doeraene */ trait GenJSExports extends SubComponent { self: GenJSCode => import global._ import jsAddons._ import definitions._ import jsDefinitions._ trait JSExportsPhase { this: JSCodePhase => /** * Generate exporter methods for a class * @param classSym symbol of class we export for * @param decldExports symbols exporter methods that have been encountered in * the class' tree. This is not the same as classSym.info.delcs since * inherited concrete methods from traits should be in this param, too */ def genMemberExports( classSym: Symbol, decldExports: List[Symbol]): List[js.Tree] = { val newlyDecldExports = decldExports.filterNot { isOverridingExport _ } val newlyDecldExportNames = newlyDecldExports.map(_.name.toTermName).toList.distinct newlyDecldExportNames map { genMemberExport(classSym, _) } } def genConstructorExports(classSym: Symbol): List[js.ConstructorExportDef] = { val constructors = classSym.tpe.member(nme.CONSTRUCTOR).alternatives // Generate exports from constructors and their annotations val ctorExports = for { ctor <- constructors exp <- jsInterop.exportsOf(ctor) } yield (exp, ctor) val exports = for { (jsName, specs) <- ctorExports.groupBy(_._1.jsName) // group by exported name } yield { val (namedExports, normalExports) = specs.partition(_._1.isNamed) val normalCtors = normalExports.map(s => ExportedSymbol(s._2)) val namedCtors = for { (exp, ctor) <- namedExports } yield { implicit val pos = exp.pos ExportedBody(List(JSAnyTpe), genNamedExporterBody(ctor, genFormalArg(1).ref), nme.CONSTRUCTOR.toString, pos) } val ctors = normalCtors ++ namedCtors implicit val pos = ctors.head.pos val js.MethodDef(_, args, _, body) = withNewLocalNameScope(genExportMethod(ctors, jsName)) js.ConstructorExportDef(jsName, args, body) } exports.toList } def genModuleAccessorExports(classSym: Symbol): List[js.ModuleExportDef] = { for { exp <- jsInterop.exportsOf(classSym) } yield { implicit val pos = exp.pos if (exp.isNamed) reporter.error(pos, "You may not use @JSNamedExport on an object") js.ModuleExportDef(exp.jsName) } } /** Generate the exporter proxy for a named export */ def genNamedExporterDef(dd: DefDef): js.MethodDef = { implicit val pos = dd.pos val sym = dd.symbol val Block(Apply(fun, _) :: Nil, _) = dd.rhs val trgSym = fun.symbol val inArg = js.ParamDef(js.Ident("namedParams"), jstpe.AnyType, mutable = false) val inArgRef = inArg.ref val methodIdent = encodeMethodSym(sym) withScopedVars( currentMethodInfoBuilder := currentClassInfoBuilder.addMethod(methodIdent.name) ) { js.MethodDef(methodIdent, List(inArg), toIRType(sym.tpe.resultType), genNamedExporterBody(trgSym, inArg.ref))(None) } } private def genNamedExporterBody(trgSym: Symbol, inArg: js.Tree)( implicit pos: Position) = { if (hasRepeatedParam(trgSym)) { reporter.error(pos, "You may not name-export a method with a *-parameter") } val jsArgs = for { (pSym, index) <- trgSym.info.params.zipWithIndex } yield { val rhs = js.JSBracketSelect(inArg, js.StringLiteral(pSym.name.decoded)) js.VarDef(js.Ident("namedArg$" + index), jstpe.AnyType, mutable = false, rhs = rhs) } val jsArgRefs = jsArgs.map(_.ref) // Generate JS code to prepare arguments (default getters and unboxes) val jsArgPrep = genPrepareArgs(jsArgRefs, trgSym) val jsResult = genResult(trgSym, jsArgPrep.map(_.ref)) js.Block(jsArgs ++ jsArgPrep :+ jsResult) } private def genMemberExport(classSym: Symbol, name: TermName): js.Tree = { val alts = classSym.info.member(name).alternatives assert(!alts.isEmpty, s"Ended up with no alternatives for ${classSym.fullName}::$name. " + s"Original set was ${alts} with types ${alts.map(_.tpe)}") val (jsName, isProp) = jsInterop.jsExportInfo(name) // Check if we have a conflicting export of the other kind val conflicting = classSym.info.member(jsInterop.scalaExportName(jsName, !isProp)) if (conflicting != NoSymbol) { val kind = if (isProp) "property" else "method" val alts = conflicting.alternatives reporter.error(alts.head.pos, s"Exported $kind $jsName conflicts with ${alts.head.fullName}") } withNewLocalNameScope { if (isProp) genExportProperty(alts, jsName) else genExportMethod(alts.map(ExportedSymbol), jsName) } } private def genExportProperty(alts: List[Symbol], jsName: String) = { assert(!alts.isEmpty) implicit val pos = alts.head.pos // Separate getters and setters. Somehow isJSGetter doesn't work here. Hence // we just check the parameter list length. val (getter, setters) = alts.partition(_.tpe.params.isEmpty) // if we have more than one getter, something went horribly wrong assert(getter.size <= 1, s"Found more than one getter to export for name ${jsName}.") val getTree = if (getter.isEmpty) js.EmptyTree else genApplyForSym(getter.head) val setTree = if (setters.isEmpty) js.EmptyTree else genExportSameArgc(setters.map(ExportedSymbol), 0) // we only have 1 argument js.PropertyDef(js.StringLiteral(jsName), getTree, genFormalArg(1), setTree) } /** generates the exporter function (i.e. exporter for non-properties) for * a given name */ private def genExportMethod(alts0: List[Exported], jsName: String) = { assert(alts0.nonEmpty, "need at least one alternative to generate exporter method") implicit val pos = alts0.head.pos val alts = { // toString() is always exported. We might need to add it here // to get correct overloading. if (jsName == "toString" && alts0.forall(_.params.nonEmpty)) ExportedSymbol(Object_toString) :: alts0 else alts0 } // Factor out methods with variable argument lists. Note that they can // only be at the end of the lists as enforced by PrepJSExports val (varArgMeths, normalMeths) = alts.partition(_.hasRepeatedParam) // Highest non-repeated argument count val maxArgc = ( // We have argc - 1, since a repeated parameter list may also be empty // (unlike a normal parameter) varArgMeths.map(_.params.size - 1) ++ normalMeths.map(_.params.size) ).max val formalArgs = genFormalArgs(maxArgc) // Calculates possible arg counts for normal method def argCounts(ex: Exported) = ex match { case ExportedSymbol(sym) => val params = sym.tpe.params // Find default param val dParam = params.indexWhere { _.hasFlag(Flags.DEFAULTPARAM) } if (dParam == -1) Seq(params.size) else dParam to params.size case ex: ExportedBody => List(ex.params.size) } // Generate tuples (argc, method) val methodArgCounts = { // Normal methods for { method <- normalMeths argc <- argCounts(method) } yield (argc, method) } ++ { // Repeated parameter methods for { method <- varArgMeths argc <- method.params.size - 1 to maxArgc } yield (argc, method) } // Create a map: argCount -> methods (methods may appear multiple times) val methodByArgCount = methodArgCounts.groupBy(_._1).mapValues(_.map(_._2).toSet) // Create tuples: (methods, argCounts). This will be the cases we generate val caseDefinitions = methodByArgCount.groupBy(_._2).mapValues(_.keySet) // Verify stuff about caseDefinitions assert({ val argcs = caseDefinitions.values.flatten.toList argcs == argcs.distinct && argcs.forall(_ <= maxArgc) }, "every argc should appear only once and be lower than max") // Generate a case block for each (methods, argCounts) tuple val cases = for { (methods, argcs) <- caseDefinitions if methods.nonEmpty && argcs.nonEmpty // exclude default case we're generating anyways for varargs if methods != varArgMeths.toSet // body of case to disambiguates methods with current count caseBody = genExportSameArgc(methods.toList, 0, Some(argcs.min)) // argc in reverse order argcList = argcs.toList.sortBy(- _) } yield (argcList.map(js.IntLiteral(_)), caseBody) val hasVarArg = varArgMeths.nonEmpty def defaultCase = { if (!hasVarArg) genThrowTypeError() else genExportSameArgc(varArgMeths, 0) } val body = { if (cases.isEmpty) defaultCase else if (cases.size == 1 && !hasVarArg) cases.head._2 else { js.Match( js.Unbox(js.JSBracketSelect( js.VarRef(js.Ident("arguments"), false)(jstpe.AnyType), js.StringLiteral("length")), 'I'), cases.toList, defaultCase)(jstpe.AnyType) } } js.MethodDef(js.StringLiteral(jsName), formalArgs, jstpe.AnyType, body)(None) } /** * Resolve method calls to [[alts]] while assuming they have the same * parameter count. * @param alts Alternative methods * @param paramIndex Index where to start disambiguation * @param maxArgc only use that many arguments */ private def genExportSameArgc(alts: List[Exported], paramIndex: Int, maxArgc: Option[Int] = None): js.Tree = { implicit val pos = alts.head.pos if (alts.size == 1) alts.head.body else if (maxArgc.exists(_ <= paramIndex) || !alts.exists(_.params.size > paramIndex)) { // We reach here in three cases: // 1. The parameter list has been exhausted // 2. The optional argument count restriction has triggered // 3. We only have (more than once) repeated parameters left // Therefore, we should fail reporter.error(pos, s"""Cannot disambiguate overloads for exported method ${alts.head.name} with types | ${alts.map(_.typeInfo).mkString("\n ")}""".stripMargin) js.Undefined() } else { val altsByTypeTest = groupByWithoutHashCode(alts) { case ExportedSymbol(alt) => // get parameter type while resolving repeated params val tpe = enteringPhase(currentRun.uncurryPhase) { val ps = alt.paramss.flatten if (ps.size <= paramIndex || isRepeated(ps(paramIndex))) { assert(isRepeated(ps.last)) repeatedToSingle(ps.last.tpe) } else { enteringPhase(currentRun.posterasurePhase) { ps(paramIndex).tpe } } } typeTestForTpe(tpe) case ex: ExportedBody => typeTestForTpe(ex.params(paramIndex)) } if (altsByTypeTest.size == 1) { // Testing this parameter is not doing any us good genExportSameArgc(alts, paramIndex+1, maxArgc) } else { // Sort them so that, e.g., isInstanceOf[String] // comes before isInstanceOf[Object] val sortedAltsByTypeTest = topoSortDistinctsBy( altsByTypeTest)(_._1)(RTTypeTest.Ordering) val defaultCase = genThrowTypeError() sortedAltsByTypeTest.foldRight[js.Tree](defaultCase) { (elem, elsep) => val (typeTest, subAlts) = elem implicit val pos = subAlts.head.pos val param = genFormalArg(paramIndex+1) val genSubAlts = genExportSameArgc(subAlts, paramIndex+1, maxArgc) def hasDefaultParam = subAlts.exists { case ExportedSymbol(p) => val params = p.tpe.params params.size > paramIndex && params(paramIndex).hasFlag(Flags.DEFAULTPARAM) case _: ExportedBody => false } val optCond = typeTest match { case HijackedTypeTest(boxedClassName, _) => Some(js.IsInstanceOf(param.ref, jstpe.ClassType(boxedClassName))) case InstanceOfTypeTest(tpe) => Some(genIsInstanceOf(param.ref, tpe)) case NoTypeTest => None } optCond.fold[js.Tree] { genSubAlts // note: elsep is discarded, obviously } { cond => val condOrUndef = if (!hasDefaultParam) cond else { js.If(cond, js.BooleanLiteral(true), js.BinaryOp(js.BinaryOp.===, param.ref, js.Undefined()))( jstpe.BooleanType) } js.If(condOrUndef, genSubAlts, elsep)(jstpe.AnyType) } } } } } /** * Generate a call to the method [[sym]] while using the formalArguments * and potentially the argument array. Also inserts default parameters if * required. */ private def genApplyForSym(sym: Symbol): js.Tree = { implicit val pos = sym.pos // the (single) type of the repeated parameter if any val repeatedTpe = enteringPhase(currentRun.uncurryPhase) { for { param <- sym.paramss.flatten.lastOption if isRepeated(param) } yield repeatedToSingle(param.tpe) } val normalArgc = sym.tpe.params.size - (if (repeatedTpe.isDefined) 1 else 0) // optional repeated parameter list val jsVarArg = repeatedTpe map { tpe => // Copy arguments that go to vararg into an array, put it in a wrapper val countIdent = freshLocalIdent("count") val count = js.VarRef(countIdent, mutable = false)(jstpe.IntType) val counterIdent = freshLocalIdent("i") val counter = js.VarRef(counterIdent, mutable = true)(jstpe.IntType) val arrayIdent = freshLocalIdent("varargs") val array = js.VarRef(arrayIdent, mutable = false)(jstpe.AnyType) val arguments = js.VarRef(js.Ident("arguments"), mutable = false)(jstpe.AnyType) val argLen = js.Unbox( js.JSBracketSelect(arguments, js.StringLiteral("length")), 'I') val argOffset = js.IntLiteral(normalArgc) val jsArrayCtor = js.JSBracketSelect( js.JSBracketSelect(js.JSEnvInfo(), js.StringLiteral("global")), js.StringLiteral("Array")) js.Block( // var i = 0 js.VarDef(counterIdent, jstpe.IntType, mutable = true, rhs = js.IntLiteral(0)), // val count = arguments.length - js.VarDef(countIdent, jstpe.IntType, mutable = false, rhs = js.BinaryOp(js.BinaryOp.Int_-, argLen, argOffset)), // val varargs = new Array(count) js.VarDef(arrayIdent, jstpe.AnyType, mutable = false, rhs = js.JSNew(jsArrayCtor, List(count))), // while (i < count) js.While(js.BinaryOp(js.BinaryOp.Num_<, counter, count), js.Block( // varargs[i] = arguments[ + i]; js.Assign( js.JSBracketSelect(array, counter), js.JSBracketSelect(arguments, js.BinaryOp(js.BinaryOp.Int_+, argOffset, counter))), // i = i + 1 (++i won't work, desugar eliminates it) js.Assign(counter, js.BinaryOp(js.BinaryOp.Int_+, counter, js.IntLiteral(1))) )), // new WrappedArray(varargs) genNew(WrappedArrayClass, WrappedArray_ctor, List(array)) ) } // normal arguments val jsArgs = genFormalArgs(normalArgc) val jsArgRefs = jsArgs.map(_.ref) // Generate JS code to prepare arguments (default getters and unboxes) val jsArgPrep = genPrepareArgs(jsArgRefs, sym) val jsResult = genResult(sym, jsArgPrep.map(_.ref) ++ jsVarArg) js.Block(jsArgPrep :+ jsResult) } /** Generate the necessary JavaScript code to prepare the arguments of an * exported method (unboxing and default parameter handling) */ private def genPrepareArgs(jsArgs: List[js.VarRef], sym: Symbol)( implicit pos: Position): List[js.VarDef] = { val result = new mutable.ListBuffer[js.VarDef] val funTpe = enteringPhase(currentRun.posterasurePhase)(sym.tpe) for { (jsArg, (param, i)) <- jsArgs zip funTpe.params.zipWithIndex } yield { // Code to verify the type of the argument (if it is defined) val verifiedArg = { val tpePosterasure = enteringPhase(currentRun.posterasurePhase)(param.tpe) tpePosterasure match { case tpe if isPrimitiveValueType(tpe) => val unboxed = makePrimitiveUnbox(jsArg, tpe) // Ensure we don't convert null to a primitive value type js.If(js.BinaryOp(js.BinaryOp.===, jsArg, js.Null()), genThrowTypeError(s"Found null, expected $tpe"), unboxed)(unboxed.tpe) case tpe: ErasedValueType => val boxedClass = tpe.valueClazz val unboxMethod = boxedClass.derivedValueClassUnbox genApplyMethod( genAsInstanceOf(jsArg, tpe), boxedClass, unboxMethod, Nil) case tpe => genAsInstanceOf(jsArg, tpe) } } // If argument is undefined and there is a default getter, call it val verifiedOrDefault = if (param.hasFlag(Flags.DEFAULTPARAM)) { js.If(js.BinaryOp(js.BinaryOp.===, jsArg, js.Undefined()), { val trgSym = { if (sym.isClassConstructor) sym.owner.companionModule.moduleClass else sym.owner } val defaultGetter = trgSym.tpe.member( nme.defaultGetterName(sym.name, i+1)) assert(defaultGetter.exists, s"need default getter for method ${sym.fullName}") assert(!defaultGetter.isOverloaded) val trgTree = { if (sym.isClassConstructor) genLoadModule(trgSym) else js.This()(encodeClassType(trgSym)) } // Pass previous arguments to defaultGetter genApplyMethod(trgTree, trgSym, defaultGetter, result.take(defaultGetter.tpe.params.size).toList.map(_.ref)) }, { // Otherwise, unbox the argument verifiedArg })(verifiedArg.tpe) } else { // Otherwise, it is always the unboxed argument verifiedArg } result += js.VarDef(js.Ident("prep"+jsArg.ident.name, jsArg.ident.originalName), verifiedOrDefault.tpe, mutable = false, verifiedOrDefault) } result.toList } /** Generate the final forwarding call to the exported method. * Attention: This method casts the arguments to the right type. The IR * checker will not detect if you pass in a wrongly typed argument. */ private def genResult(sym: Symbol, args: List[js.Tree])(implicit pos: Position) = { val thisType = if (sym.owner == ObjectClass) jstpe.ClassType(ir.Definitions.ObjectClass) else encodeClassType(sym.owner) val call = genApplyMethod(js.This()(thisType), sym.owner, sym, args) ensureBoxed(call, enteringPhase(currentRun.posterasurePhase)(sym.tpe.resultType)) } private sealed abstract class Exported { def pos: Position def params: List[Type] def body: js.Tree def name: String def typeInfo: String def hasRepeatedParam: Boolean } private case class ExportedSymbol(sym: Symbol) extends Exported { def pos: Position = sym.pos def params: List[Type] = sym.tpe.params.map(_.tpe) def body: js.Tree = genApplyForSym(sym) def name: String = sym.name.toString def typeInfo: String = sym.tpe.toString def hasRepeatedParam: Boolean = GenJSExports.this.hasRepeatedParam(sym) } private case class ExportedBody(params: List[Type], body: js.Tree, name: String, pos: Position) extends Exported { def typeInfo: String = params.mkString("(", ", ", ")") val hasRepeatedParam: Boolean = false } } private def isOverridingExport(sym: Symbol): Boolean = { lazy val osym = sym.nextOverriddenSymbol sym.isOverridingSymbol && !osym.owner.isInterface } private sealed abstract class RTTypeTest private final case class HijackedTypeTest( boxedClassName: String, rank: Int) extends RTTypeTest private final case class InstanceOfTypeTest(tpe: Type) extends RTTypeTest { override def equals(that: Any): Boolean = { that match { case InstanceOfTypeTest(thatTpe) => tpe =:= thatTpe case _ => false } } } private case object NoTypeTest extends RTTypeTest private object RTTypeTest { implicit object Ordering extends PartialOrdering[RTTypeTest] { override def tryCompare(lhs: RTTypeTest, rhs: RTTypeTest): Option[Int] = { if (lteq(lhs, rhs)) if (lteq(rhs, lhs)) Some(0) else Some(-1) else if (lteq(rhs, lhs)) Some(1) else None } override def lteq(lhs: RTTypeTest, rhs: RTTypeTest): Boolean = { (lhs, rhs) match { // NoTypeTest is always last case (_, NoTypeTest) => true case (NoTypeTest, _) => false case (HijackedTypeTest(_, rank1), HijackedTypeTest(_, rank2)) => rank1 <= rank2 case (InstanceOfTypeTest(t1), InstanceOfTypeTest(t2)) => t1 <:< t2 case (_:HijackedTypeTest, _:InstanceOfTypeTest) => true case (_:InstanceOfTypeTest, _:HijackedTypeTest) => false } } override def equiv(lhs: RTTypeTest, rhs: RTTypeTest): Boolean = { lhs == rhs } } } // Very simple O(n²) topological sort for elements assumed to be distinct private def topoSortDistinctsBy[A <: AnyRef, B](coll: List[A])(f: A => B)( implicit ord: PartialOrdering[B]): List[A] = { @scala.annotation.tailrec def loop(coll: List[A], acc: List[A]): List[A] = { if (coll.isEmpty) acc else if (coll.tail.isEmpty) coll.head :: acc else { val (lhs, rhs) = coll.span(x => !coll.forall( y => (x eq y) || !ord.lteq(f(x), f(y)))) assert(!rhs.isEmpty, s"cycle while ordering $coll") loop(lhs ::: rhs.tail, rhs.head :: acc) } } loop(coll, Nil) } private def typeTestForTpe(tpe: Type): RTTypeTest = { tpe match { case tpe: ErasedValueType => InstanceOfTypeTest(tpe.valueClazz.typeConstructor) case _ => import ir.{Definitions => Defs} (toTypeKind(tpe): @unchecked) match { case VoidKind => HijackedTypeTest(Defs.BoxedUnitClass, 0) case BooleanKind => HijackedTypeTest(Defs.BoxedBooleanClass, 1) case ByteKind => HijackedTypeTest(Defs.BoxedByteClass, 2) case ShortKind => HijackedTypeTest(Defs.BoxedShortClass, 3) case IntKind => HijackedTypeTest(Defs.BoxedIntegerClass, 4) case FloatKind => HijackedTypeTest(Defs.BoxedFloatClass, 5) case DoubleKind => HijackedTypeTest(Defs.BoxedDoubleClass, 6) case CharKind => InstanceOfTypeTest(boxedClass(CharClass).tpe) case LongKind => InstanceOfTypeTest(boxedClass(LongClass).tpe) case REFERENCE(cls) => if (cls == StringClass) HijackedTypeTest(Defs.StringClass, 7) else if (cls == ObjectClass) NoTypeTest else if (isRawJSType(tpe)) { cls match { case JSUndefinedClass => HijackedTypeTest(Defs.BoxedUnitClass, 0) case JSBooleanClass => HijackedTypeTest(Defs.BoxedBooleanClass, 1) case JSNumberClass => HijackedTypeTest(Defs.BoxedDoubleClass, 6) case JSStringClass => HijackedTypeTest(Defs.StringClass, 7) case _ => NoTypeTest } } else InstanceOfTypeTest(tpe) case ARRAY(_) => InstanceOfTypeTest(tpe) } } } // Group-by that does not rely on hashCode(), only equals() - O(n²) private def groupByWithoutHashCode[A, B]( coll: List[A])(f: A => B): List[(B, List[A])] = { import scala.collection.mutable.ArrayBuffer val m = new ArrayBuffer[(B, List[A])] m.sizeHint(coll.length) for (elem <- coll) { val key = f(elem) val index = m.indexWhere(_._1 == key) if (index < 0) m += ((key, List(elem))) else m(index) = (key, elem :: m(index)._2) } m.toList } private def genThrowTypeError(msg: String = "No matching overload")( implicit pos: Position): js.Tree = { js.Throw(js.StringLiteral(msg)) } private def genFormalArgs(count: Int)(implicit pos: Position): List[js.ParamDef] = (1 to count map genFormalArg).toList private def genFormalArg(index: Int)(implicit pos: Position): js.ParamDef = js.ParamDef(js.Ident("arg$" + index), jstpe.AnyType, mutable = false) private def hasRepeatedParam(sym: Symbol) = enteringPhase(currentRun.uncurryPhase) { sym.paramss.flatten.lastOption.exists(isRepeated _) } }