package dotty.tools.backend.sjs import scala.collection.mutable import dotty.tools.FatalError import dotty.tools.dotc.core._ import Periods._ import SymDenotations._ import Contexts._ import Types._ import Symbols._ import Denotations._ import NameOps._ import StdNames._ import org.scalajs.core.ir import ir.{Trees => js, Types => jstpe} import ScopedVar.withScopedVars import JSDefinitions._ import JSInterop._ /** Encoding of symbol names for JavaScript * * Some issues that this encoding solves: * * Overloading: encode the full signature in the JS name * * Same scope for fields and methods of a class * * Global access to classes and modules (by their full name) * * @author Sébastien Doeraene */ object JSEncoding { /** Signature separator string (between parameter types) */ private final val SignatureSep = "__" /** Name given to the local Scala.js environment variable */ private final val ScalaJSEnvironmentName = "ScalaJS" implicit class SymOps(val self: Symbol) extends AnyVal { def unexpandedName(implicit ctx: Context): Names.Name = self.name.unexpandedName } implicit class MyNameOps(val self: Names.Name) extends AnyVal { def decoded: String = self.decode.toString } // Fresh local name generator ---------------------------------------------- class LocalNameGenerator { import LocalNameGenerator._ private val usedLocalNames = mutable.Set.empty[String] private val localSymbolNames = mutable.Map.empty[Symbol, String] def localSymbolName(sym: Symbol)(implicit ctx: Context): String = localSymbolNames.getOrElseUpdate(sym, freshName(sym.name.toString)) def freshLocalIdent()(implicit pos: ir.Position): js.Ident = js.Ident(freshName(), None) def freshLocalIdent(base: String)(implicit pos: ir.Position): js.Ident = js.Ident(freshName(base), Some(base)) private def freshName(base: String = "x"): String = { var suffix = 1 var longName = base while (usedLocalNames(longName) || isReserved(longName)) { suffix += 1 longName = base+"$"+suffix } usedLocalNames += longName mangleJSName(longName) } } private object LocalNameGenerator { private val isReserved = Set("arguments", "eval", ScalaJSEnvironmentName) } // Encoding methods ---------------------------------------------------------- def encodeLabelSym(sym: Symbol)( implicit ctx: Context, pos: ir.Position, localNames: LocalNameGenerator): js.Ident = { require(sym.is(Flags.Label), "encodeLabelSym called with non-label symbol: " + sym) js.Ident(localNames.localSymbolName(sym), Some(sym.unexpandedName.decoded)) } private def allRefClasses(implicit ctx: Context): Set[Symbol] = { //TODO /*(Set(ObjectRefClass, VolatileObjectRefClass) ++ refClass.values ++ volatileRefClass.values)*/ Set() } def encodeFieldSym(sym: Symbol)( implicit ctx: Context, pos: ir.Position): js.Ident = { require(sym.owner.isClass && sym.isTerm && !sym.is(Flags.Method) && !sym.is(Flags.Module), "encodeFieldSym called with non-field symbol: " + sym) val name0 = encodeMemberNameInternal(sym) val name = if (name0.charAt(name0.length()-1) != ' ') name0 else name0.substring(0, name0.length()-1) /* We have to special-case fields of Ref types (IntRef, ObjectRef, etc.) * because they are emitted as private by our .scala source files, but * they are considered public at use site since their symbols come from * Java-emitted .class files. */ val idSuffix = if (sym.is(Flags.Private) || allRefClasses.contains(sym.owner)) sym.owner.asClass.baseClasses.size.toString else "f" val encodedName = name + "$" + idSuffix js.Ident(mangleJSName(encodedName), Some(sym.unexpandedName.decoded)) } def encodeMethodSym(sym: Symbol, reflProxy: Boolean = false)( implicit ctx: Context, pos: ir.Position): js.Ident = { val (encodedName, paramsString) = encodeMethodNameInternal(sym, reflProxy) js.Ident(encodedName + paramsString, Some(sym.unexpandedName.decoded + paramsString)) } def encodeMethodName(sym: Symbol, reflProxy: Boolean = false)( implicit ctx: Context): String = { val (encodedName, paramsString) = encodeMethodNameInternal(sym, reflProxy) encodedName + paramsString } /** Encodes a method symbol of java.lang.String for use in RuntimeString. * * This basically means adding an initial parameter of type * java.lang.String, which is the `this` parameter. */ def encodeRTStringMethodSym(sym: Symbol)( implicit ctx: Context, pos: ir.Position): js.Ident = { require(sym.owner == defn.StringClass) require(!sym.isClassConstructor && !sym.is(Flags.Private)) val (encodedName, paramsString) = encodeMethodNameInternal(sym, inRTClass = true) js.Ident(encodedName + paramsString, Some(sym.unexpandedName.decoded + paramsString)) } /** Encodes a constructor symbol of java.lang.String for use in RuntimeString. * * - The name is rerouted to `newString` * - The result type is set to `java.lang.String` */ def encodeRTStringCtorSym(sym: Symbol)( implicit ctx: Context, pos: ir.Position): js.Ident = { require(sym.owner == defn.StringClass) require(sym.isClassConstructor && !sym.is(Flags.Private)) val paramTypeNames = sym.info.firstParamTypes.map(internalName(_)) val paramAndResultTypeNames = paramTypeNames :+ ir.Definitions.StringClass val paramsString = makeParamsString(paramAndResultTypeNames) js.Ident("newString" + paramsString, Some(sym.unexpandedName.decoded + paramsString)) } private def encodeMethodNameInternal(sym: Symbol, reflProxy: Boolean = false, inRTClass: Boolean = false)( implicit ctx: Context): (String, String) = { require(sym.is(Flags.Method), "encodeMethodSym called with non-method symbol: " + sym) def name = encodeMemberNameInternal(sym) val encodedName = { if (sym.isClassConstructor) { "init_" } else if (sym.is(Flags.Private)) { (mangleJSName(name) + SignatureSep + "p" + sym.owner.asClass.baseClasses.size.toString) } else { mangleJSName(name) } } val paramsString = makeParamsString(sym, reflProxy, inRTClass) (encodedName, paramsString) } def encodeStaticMemberSym(sym: Symbol)( implicit ctx: Context, pos: ir.Position): js.Ident = { require(sym.is(Flags.JavaStaticTerm), "encodeStaticMemberSym called with non-static symbol: " + sym) js.Ident( mangleJSName(encodeMemberNameInternal(sym)) + makeParamsString(List(internalName(sym.info))), Some(sym.unexpandedName.decoded)) } def encodeLocalSym(sym: Symbol)( implicit ctx: Context, pos: ir.Position, localNames: LocalNameGenerator): js.Ident = { require(!sym.owner.isClass && sym.isTerm && !sym.is(Flags.Method) && !sym.is(Flags.Module), "encodeLocalSym called with non-local symbol: " + sym) js.Ident(localNames.localSymbolName(sym), Some(sym.unexpandedName.decoded)) } def foreignIsImplClass(sym: Symbol)(implicit ctx: Context): Boolean = sym.name.isImplClassName def encodeClassType(sym: Symbol)(implicit ctx: Context): jstpe.Type = { if (sym == defn.ObjectClass) jstpe.AnyType else if (isJSType(sym)) jstpe.AnyType else { assert(sym != defn.ArrayClass, "encodeClassType() cannot be called with ArrayClass") jstpe.ClassType(encodeClassFullName(sym)) } } def encodeClassFullNameIdent(sym: Symbol)( implicit ctx: Context, pos: ir.Position): js.Ident = { js.Ident(encodeClassFullName(sym), Some(sym.fullName.toString)) } def encodeClassFullName(sym: Symbol)(implicit ctx: Context): String = { if (sym == defn.NothingClass) ir.Definitions.RuntimeNothingClass else if (sym == defn.NullClass) ir.Definitions.RuntimeNullClass else ir.Definitions.encodeClassName(sym.fullName.toString) } private def encodeMemberNameInternal(sym: Symbol)( implicit ctx: Context): String = { sym.name.toString.replace("_", "$und").replace("~", "$tilde") } def toIRType(tp: Type)(implicit ctx: Context): jstpe.Type = { val refType = toReferenceTypeInternal(tp) refType._1 match { case tpe: jstpe.ClassType => val sym = refType._2 if (sym.asClass.isPrimitiveValueClass) { if (sym == defn.BooleanClass) jstpe.BooleanType else if (sym == defn.FloatClass) jstpe.FloatType else if (sym == defn.DoubleClass) jstpe.DoubleType else if (sym == defn.LongClass) jstpe.LongType else if (sym == defn.UnitClass) jstpe.NoType else jstpe.IntType } else { if (sym == defn.ObjectClass || isJSType(sym)) jstpe.AnyType else if (sym == defn.NothingClass) jstpe.NothingType else if (sym == defn.NullClass) jstpe.NullType else tpe } case tpe: jstpe.ArrayType => tpe } } def toReferenceType(tp: Type)(implicit ctx: Context): jstpe.ReferenceType = toReferenceTypeInternal(tp)._1 private def toReferenceTypeInternal(tp: Type)( implicit ctx: Context): (jstpe.ReferenceType, Symbol) = { /** * Primitive types are represented as TypeRefs to the class symbol of, for example, scala.Int. * The `primitiveTypeMap` maps those class symbols to the corresponding PrimitiveBType. */ def primitiveOrClassToRefType(sym: Symbol): (jstpe.ReferenceType, Symbol) = { assert(sym.isClass, sym) //assert(sym != defn.ArrayClass || isCompilingArray, sym) (jstpe.ClassType(encodeClassFullName(sym)), sym) } /** * When compiling Array.scala, the type parameter T is not erased and shows up in method * signatures, e.g. `def apply(i: Int): T`. A TyperRef to T is replaced by ObjectReference. */ def nonClassTypeRefToRefType(sym: Symbol): (jstpe.ReferenceType, Symbol) = { //assert(sym.isType && isCompilingArray, sym) (jstpe.ClassType(ir.Definitions.ObjectClass), defn.ObjectClass) } tp.widenDealias match { // Array type such as Array[Int] (kept by erasure) case JavaArrayType(el) => val elRefType = toReferenceTypeInternal(el) (jstpe.ArrayType(elRefType._1), elRefType._2) case t: TypeRef => if (!t.symbol.isClass) nonClassTypeRefToRefType(t.symbol) // See comment on nonClassTypeRefToBType else primitiveOrClassToRefType(t.symbol) // Common reference to a type such as scala.Int or java.lang.String case Types.ClassInfo(_, sym, _, _, _) => /* We get here, for example, for genLoadModule, which invokes * toTypeKind(moduleClassSymbol.info) */ primitiveOrClassToRefType(sym) case t: MethodType => // triggers for LabelDefs toReferenceTypeInternal(t.resultType) /* AnnotatedType should (probably) be eliminated by erasure. However we know it happens for * meta-annotated annotations (@(ann @getter) val x = 0), so we don't emit a warning. * The type in the AnnotationInfo is an AnnotatedTpe. Tested in jvm/annotations.scala. */ case a @ AnnotatedType(t, _) => //debuglog(s"typeKind of annotated type $a") toReferenceTypeInternal(t) } } /** Patches the result type of a method symbol to sanitize it. * * For some reason, dotc thinks that the `info.resultType`of an * `isConstructor` method (for classes or traits) is the enclosing class * or trait, but the bodies and usages act as if the result type was `Unit`. * * This method returns `UnitType` for constructor methods, and otherwise * `sym.info.resultType`. */ def patchedResultType(sym: Symbol)(implicit ctx: Context): Type = if (sym.isConstructor) defn.UnitType else sym.info.resultType // Encoding of method signatures private def makeParamsString(sym: Symbol, reflProxy: Boolean, inRTClass: Boolean)( implicit ctx: Context): String = { val tpe = sym.info val paramTypeNames0 = tpe.firstParamTypes.map(internalName(_)) val hasExplicitThisParameter = inRTClass || isScalaJSDefinedJSClass(sym.owner) val paramTypeNames = if (!hasExplicitThisParameter) paramTypeNames0 else encodeClassFullName(sym.owner) :: paramTypeNames0 val paramAndResultTypeNames = { if (sym.isClassConstructor) paramTypeNames else if (reflProxy) paramTypeNames :+ "" else paramTypeNames :+ internalName(patchedResultType(sym)) } makeParamsString(paramAndResultTypeNames) } private def makeParamsString(paramAndResultTypeNames: List[String]) = paramAndResultTypeNames.mkString(SignatureSep, SignatureSep, "") /** Computes the internal name for a type. */ private def internalName(tpe: Type)(implicit ctx: Context): String = encodeReferenceType(toReferenceType(tpe)) /** Encodes a [[Types.ReferenceType]], such as in an encoded method signature. */ private def encodeReferenceType(refType: jstpe.ReferenceType): String = { refType match { case jstpe.ClassType(encodedName) => encodedName case jstpe.ArrayType(base, depth) => "A" * depth + base } } /** Mangles names that are illegal in JavaScript by prepending a `$`. * Also mangles names that would collide with these mangled names. */ private def mangleJSName(name: String) = if (js.isKeyword(name) || name(0).isDigit || name(0) == '$') "$" + name else name }