summaryrefslogtreecommitdiff
path: root/examples/scala-js/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ClosureAstTransformer.scala
blob: af225014c136c3ec3c87e245fd77600f6ef06328 (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
390
391
392
393
394
395
396
397
package scala.scalajs.tools.optimizer

import scala.scalajs.ir
import ir.Position
import ir.Position.NoPosition

import scala.scalajs.tools.javascript.Trees._

import com.google.javascript.rhino._
import com.google.javascript.jscomp._

import scala.collection.mutable
import scala.annotation.tailrec

import java.net.URI

class ClosureAstTransformer(val relativizeBaseURI: Option[URI] = None) {

  private val inputId = new InputId("Scala.js IR")

  private val dummySourceName = new java.net.URI("virtualfile:scala.js-ir")

  def transformStat(tree: Tree)(implicit parentPos: Position): Node =
    innerTransformStat(tree, tree.pos orElse parentPos)

  private def innerTransformStat(tree: Tree, pos_in: Position): Node = {
    implicit val pos = pos_in

    wrapTransform(tree) {
      case VarDef(ident, _, EmptyTree) =>
        new Node(Token.VAR, transformName(ident))
      case VarDef(ident, _, rhs) =>
        val node = transformName(ident)
        node.addChildToFront(transformExpr(rhs))
        new Node(Token.VAR, node)
      case Skip() =>
        new Node(Token.EMPTY)
      case Block(stats) =>
        transformBlock(stats, pos)
      case Labeled(label, body) =>
        new Node(Token.LABEL, transformLabel(label), transformBlock(body))
      case Return(EmptyTree) =>
        new Node(Token.RETURN)
      case Return(expr) =>
        new Node(Token.RETURN, transformExpr(expr))
      case If(cond, thenp, Skip()) =>
        new Node(Token.IF, transformExpr(cond), transformBlock(thenp))
      case If(cond, thenp, elsep) =>
        new Node(Token.IF, transformExpr(cond),
            transformBlock(thenp), transformBlock(elsep))
      case While(cond, body, None) =>
        new Node(Token.WHILE, transformExpr(cond), transformBlock(body))
      case While(cond, body, Some(label)) =>
        val whileNode =
          new Node(Token.WHILE, transformExpr(cond), transformBlock(body))
        new Node(Token.LABEL, transformLabel(label),
            setNodePosition(whileNode, pos))
      case DoWhile(body, cond, None) =>
        new Node(Token.DO, transformBlock(body), transformExpr(cond))
      case DoWhile(body, cond, Some(label)) =>
        val doNode =
          new Node(Token.DO, transformBlock(body), transformExpr(cond))
        new Node(Token.LABEL, transformLabel(label),
            setNodePosition(doNode, pos))
      case Try(block, errVar, handler, EmptyTree) =>
        val catchPos = handler.pos orElse pos
        val catchNode =
          new Node(Token.CATCH, transformName(errVar), transformBlock(handler))
        val blockNode =
          new Node(Token.BLOCK, setNodePosition(catchNode, catchPos))
        new Node(Token.TRY, transformBlock(block),
            setNodePosition(blockNode, catchPos))
      case Try(block, _, EmptyTree, finalizer) =>
        val blockNode = setNodePosition(new Node(Token.BLOCK), pos)
        new Node(Token.TRY, transformBlock(block), blockNode,
            transformBlock(finalizer))
      case Try(block, errVar, handler, finalizer) =>
        val catchPos = handler.pos orElse pos
        val catchNode =
          new Node(Token.CATCH, transformName(errVar), transformBlock(handler))
        val blockNode =
          new Node(Token.BLOCK, setNodePosition(catchNode, catchPos))
        new Node(Token.TRY, transformBlock(block),
            setNodePosition(blockNode, catchPos), transformBlock(finalizer))
      case Throw(expr) =>
        new Node(Token.THROW, transformExpr(expr))
      case Break(None) =>
        new Node(Token.BREAK)
      case Break(Some(label)) =>
        new Node(Token.BREAK, transformLabel(label))
      case Continue(None) =>
        new Node(Token.CONTINUE)
      case Continue(Some(label)) =>
       new Node(Token.CONTINUE, transformLabel(label))

      case Switch(selector, cases, default) =>
        val switchNode = new Node(Token.SWITCH, transformExpr(selector))

        for ((expr, body) <- cases) {
          val bodyNode = transformBlock(body)
          bodyNode.putBooleanProp(Node.SYNTHETIC_BLOCK_PROP, true)
          val caseNode = new Node(Token.CASE, transformExpr(expr), bodyNode)
          switchNode.addChildToBack(
              setNodePosition(caseNode, expr.pos orElse pos))
        }

        if (default != EmptyTree) {
          val bodyNode = transformBlock(default)
          bodyNode.putBooleanProp(Node.SYNTHETIC_BLOCK_PROP, true)
          val caseNode = new Node(Token.DEFAULT_CASE, bodyNode)
          switchNode.addChildToBack(
              setNodePosition(caseNode, default.pos orElse pos))
        }

        switchNode

      case Debugger() =>
        new Node(Token.DEBUGGER)
      case _ =>
        // We just assume it is an expression
        new Node(Token.EXPR_RESULT, transformExpr(tree))
    }
  }

  def transformExpr(tree: Tree)(implicit parentPos: Position): Node =
    innerTransformExpr(tree, tree.pos orElse parentPos)

  private def innerTransformExpr(tree: Tree, pos_in: Position): Node = {
    implicit val pos = pos_in

    wrapTransform(tree) {
      case Block(exprs) =>
        exprs.map(transformExpr).reduceRight { (expr1, expr2) =>
          setNodePosition(new Node(Token.COMMA, expr1, expr2), pos)
        }
      case If(cond, thenp, elsep) =>
        new Node(Token.HOOK, transformExpr(cond),
            transformExpr(thenp), transformExpr(elsep))
      case Assign(lhs, rhs) =>
        new Node(Token.ASSIGN, transformExpr(lhs), transformExpr(rhs))
      case New(ctor, args) =>
        val node = new Node(Token.NEW, transformExpr(ctor))
        args.foreach(arg => node.addChildToBack(transformExpr(arg)))
        node
      case DotSelect(qualifier, item) =>
        new Node(Token.GETPROP, transformExpr(qualifier), transformString(item))
      case BracketSelect(qualifier, item) =>
        new Node(Token.GETELEM, transformExpr(qualifier), transformExpr(item))

      case Apply(fun, args) =>
        val node = new Node(Token.CALL, transformExpr(fun))
        args.foreach(arg => node.addChildToBack(transformExpr(arg)))

        // Closure needs to know (from the parser), if the call has a bound
        // `this` or not. Since JSDesugar inserts protects calls if necessary,
        // it is sufficient to check if we have a select as target
        if (!fun.isInstanceOf[DotSelect] &&
            !fun.isInstanceOf[BracketSelect])
          node.putBooleanProp(Node.FREE_CALL, true)

        node

      case Delete(prop) =>
        new Node(Token.DELPROP, transformExpr(prop))
      case UnaryOp(op, lhs) =>
        mkUnaryOp(op, transformExpr(lhs))
      case BinaryOp(op, lhs, rhs) =>
        mkBinaryOp(op, transformExpr(lhs), transformExpr(rhs))
      case ArrayConstr(items) =>
        val node = new Node(Token.ARRAYLIT)
        items.foreach(i => node.addChildToBack(transformExpr(i)))
        node

      case ObjectConstr(fields) =>
        val node = new Node(Token.OBJECTLIT)

        for ((name, expr) <- fields) {
          val fldNode = transformStringKey(name)
          fldNode.addChildToBack(transformExpr(expr))
          node.addChildToBack(fldNode)
        }

        node

      case Undefined() =>
        new Node(Token.VOID, setNodePosition(Node.newNumber(0.0), pos))
      case Null() =>
        new Node(Token.NULL)
      case BooleanLiteral(value) =>
        if (value) new Node(Token.TRUE) else new Node(Token.FALSE)
      case IntLiteral(value) =>
        Node.newNumber(value)
      case DoubleLiteral(value) =>
        Node.newNumber(value)
      case StringLiteral(value) =>
        Node.newString(value)
      case VarRef(ident, _) =>
        transformName(ident)
      case This() =>
        new Node(Token.THIS)

      case Function(args, body) =>
        // Note that a Function may also be a statement (when it is named),
        // but Scala.js does not have such an IR node
        val paramList = new Node(Token.PARAM_LIST)
        args.foreach(arg => paramList.addChildToBack(transformParam(arg)))

        val emptyName = setNodePosition(Node.newString(Token.NAME, ""), pos)

        new Node(Token.FUNCTION, emptyName, paramList, transformBlock(body))

      case _ =>
        throw new TransformException(s"Unknown tree of class ${tree.getClass()}")
    }
  }

  def transformParam(param: ParamDef)(implicit parentPos: Position): Node =
    transformName(param.name)

  def transformName(ident: Ident)(implicit parentPos: Position): Node =
    setNodePosition(Node.newString(Token.NAME, ident.name),
        ident.pos orElse parentPos)

  def transformLabel(ident: Ident)(implicit parentPos: Position): Node =
    setNodePosition(Node.newString(Token.LABEL_NAME, ident.name),
        ident.pos orElse parentPos)

  def transformString(pName: PropertyName)(implicit parentPos: Position): Node =
    setNodePosition(Node.newString(pName.name), pName.pos orElse parentPos)

  def transformStringKey(pName: PropertyName)(
      implicit parentPos: Position): Node = {
    val node = Node.newString(Token.STRING_KEY, pName.name)

    if (pName.isInstanceOf[StringLiteral])
      node.setQuotedString()

    setNodePosition(node, pName.pos orElse parentPos)
  }

  def transformBlock(tree: Tree)(implicit parentPos: Position): Node = {
    val pos = if (tree.pos.isDefined) tree.pos else parentPos
    wrapTransform(tree) {
      case Block(stats) =>
        transformBlock(stats, pos)
      case tree =>
        transformBlock(List(tree), pos)
    } (pos)
  }

  def transformBlock(stats: List[Tree], blockPos: Position): Node = {
    @inline def ctorDoc(node: Node) = {
      val b = new JSDocInfoBuilder(false)
      b.recordConstructor()
      b.build(node)
    }

    val block = new Node(Token.BLOCK)

    // The Rhino IR attaches DocComments to the following nodes (rather than
    // having individual nodes). We preprocess these here.
    @tailrec
    def loop(ts: List[Tree], nextIsCtor: Boolean = false): Unit = ts match {
      case DocComment(text) :: tss if text.startsWith("@constructor") =>
        loop(tss, nextIsCtor = true)
      case DocComment(text) :: tss =>
        loop(tss)
      case t :: tss =>
        val node = transformStat(t)(blockPos)
        if (nextIsCtor) {
          // The @constructor must be propagated through an ExprResult node
          val trg =
            if (node.isExprResult()) node.getChildAtIndex(0)
            else node

          trg.setJSDocInfo(ctorDoc(trg))
        }

        block.addChildToBack(node)

        loop(tss)
      case Nil =>
    }

    loop(stats)

    block
  }

  @inline
  private def wrapTransform(tree: Tree)(body: Tree => Node)(
      implicit pos: Position): Node = {
    try {
      setNodePosition(body(tree), pos)
    } catch {
      case e: TransformException =>
        throw e // pass through
      case e: RuntimeException =>
        throw new TransformException(tree, e)
    }
  }

  def setNodePosition(node: Node, pos: ir.Position): node.type = {
    if (pos != ir.Position.NoPosition) {
      attachSourceFile(node, pos.source)
      node.setLineno(pos.line+1)
      node.setCharno(pos.column)
    } else {
      attachSourceFile(node, dummySourceName)
    }
    node
  }

  private def attachSourceFile(node: Node, source: URI): node.type = {
    val str = sourceUriToString(source)

    node.setInputId(inputId)
    node.setStaticSourceFile(new SourceFile(str))

    node
  }

  private def sourceUriToString(uri: URI): String = {
    val relURI = relativizeBaseURI.fold(uri)(ir.Utils.relativize(_, uri))
    ir.Utils.fixFileURI(relURI).toASCIIString
  }

  // Helpers for IR
  @inline
  private def mkUnaryOp(op: String, lhs: Node): Node = {
    val tok = op match {
      case "!"      => Token.NOT
      case "~"      => Token.BITNOT
      case "+"      => Token.POS
      case "-"      => Token.NEG
      case "typeof" => Token.TYPEOF
      case _        => throw new TransformException(s"Unhandled unary op: $op")
    }

    new Node(tok, lhs)
  }

  @inline
  private def mkBinaryOp(op: String, lhs: Node, rhs: Node): Node = {
    val tok = op match {
      case "|"          => Token.BITOR
      case "&"          => Token.BITAND
      case "^"          => Token.BITXOR
      case "=="         => Token.EQ
      case "!="         => Token.NE
      case "<"          => Token.LT
      case "<="         => Token.LE
      case ">"          => Token.GT
      case ">="         => Token.GE
      case "<<"         => Token.LSH
      case ">>"         => Token.RSH
      case ">>>"        => Token.URSH
      case "+"          => Token.ADD
      case "-"          => Token.SUB
      case "*"          => Token.MUL
      case "/"          => Token.DIV
      case "%"          => Token.MOD
      case "==="        => Token.SHEQ
      case "!=="        => Token.SHNE
      case "in"         => Token.IN
      case "instanceof" => Token.INSTANCEOF
      case "||"         => Token.OR
      case "&&"         => Token.AND
      case _ =>
        throw new TransformException(s"Unhandled binary op: $op")
    }

    new Node(tok, lhs, rhs)
  }

  // Exception wrapper in transforms

  class TransformException private (msg: String, e: Throwable)
      extends RuntimeException(msg, e) {

    def this(tree: Tree, e: Throwable) =
      this(TransformException.mkMsg(tree), e)

    def this(msg: String) = this(msg, null)
  }

  object TransformException {
    import ir.Printers._
    import java.io._

    private def mkMsg(tree: Tree): String = {
      "Exception while translating Scala.js JS tree to GCC IR at tree:\n" +
        tree.show
    }
  }

}