aboutsummaryrefslogtreecommitdiff
path: root/src/dotty/tools/backend/sjs/JSEncoding.scala
blob: e8ea3258bfa2b2c59f2258a570cf54c15feb994d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
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
}