All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.scalajs.linker.backend.emitter.FunctionEmitter.scala Maven / Gradle / Ivy

The newest version!
/*
 * Scala.js (https://www.scala-js.org/)
 *
 * Copyright EPFL.
 *
 * Licensed under Apache License 2.0
 * (https://www.apache.org/licenses/LICENSE-2.0).
 *
 * See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 */

package org.scalajs.linker.backend.emitter

import scala.annotation.{switch, tailrec}

import scala.collection.mutable

import org.scalajs.ir._
import org.scalajs.ir.Names._
import org.scalajs.ir.OriginalName.NoOriginalName
import org.scalajs.ir.Position._
import org.scalajs.ir.Printers.IRTreePrinter
import org.scalajs.ir.Transformers._
import org.scalajs.ir.Traversers._
import org.scalajs.ir.Trees._
import org.scalajs.ir.Types._

import org.scalajs.linker.interface._
import org.scalajs.linker.interface.CheckedBehavior._
import org.scalajs.linker.backend.javascript.{Trees => js}

import java.io.StringWriter

import EmitterNames._
import PolyfillableBuiltin._
import Transients._

/** Desugaring of the IR to JavaScript functions.
 *
 *  The general shape and compliance to standards is chosen with
 *  [[ESFeatures]].
 *
 *  The major difference between the IR and JS is that most constructs can be
 *  used in expression position. The main work of the desugaring is to
 *  unnest complex constructs in expression position so that they become
 *  statements.
 *
 *  The general idea is two-folded:
 *  1) Unnest complex constructs in "argument position":
 *     When a complex construct is used in a non-rhs expression position
 *     (argument to a function, operand, condition of an if, etc.), that we
 *     call "argument position", declare a variable before the statement,
 *     assign the complex construct to it and then use that variable in the
 *     argument position instead.
 *  2) Push LHS's inside complex RHS's:
 *     When an rhs is a complex construct, push the lhs inside the complex
 *     construct. Are considered lhs:
 *     * Assign, i.e., `x =`
 *     * VarDef, i.e., `val x =` or `var x =`
 *     * Return, i.e., `return`
 *     * Throw, i.e., `throw`
 *     * Discard, i.e. just evaluate and discard
 *     In fact, think that, in this context, LHS means: what to do with the
 *     result of evaluating the RHS.
 *
 *  When VarDefs are emitted as Lets (i.e., in ES 6 mode), they cannot be
 *  pushed in all complex constructs, since that would alter their scope.
 *  In those cases, they are first declared without an initial value, then
 *  an Assign is pushed instead.
 *
 *  --------------------------------------------------------------------------
 *
 *  Typical example, consider the method call:
 *
 *  obj.meth({
 *    var x = foo(42);
 *    x*x
 *  });
 *
 *  According to rule 1), the block that is passed as a parameter to obj.meth
 *  is first extracted in a synthetic var:
 *
 *  var x\$1 = {
 *    var x = foo(42);
 *    x*x
 *  }
 *  obj.meth(x\$1);
 *
 *  Then, according to rule 2), the lhs `var x\$1 =` is pushed inside the block:
 *
 *  {
 *    var x = foo(42);
 *    var x\$1 = x*x;
 *  }
 *  obj.meth(x\$1);
 *
 *  Because bare blocks are non-significant in JS, this is equivalent to
 *
 *  var x = foo(42);
 *  var x\$1 = x*x;
 *  obj.meth(x\$1);
 *
 *  --------------------------------------------------------------------------
 *
 *  FunctionEmitter does all this in a single pass, but it helps to think that:
 *  * Rule 1) is implemented by unnest(), and used most notably in
 *    * transformStat() for statement-only constructs
 *    * pushLhsInto() for statement-or-expression constructs
 *  * Rule 2) is implemented by pushLhsInto()
 *  * Emitting the class structure is delegated to [[ScalaJSClassEmitter]].
 *
 *  There are a few other things that FunctionEmitter takes care of:
 *  * Transform Scala expressions into their JS equivalent, taking the
 *    Scala.js class encoding into account.
 *  * And tiny details.
 *
 *  --------------------------------------------------------------------------
 *
 *  About `Labeled` blocks, this phase maintains two sets of label names.
 *
 *  First, `tailPosLabels`, a set of labels for which we are in "tail
 *  position", i.e., breaking to that label is equivalent to no-op. `Break`s to
 *  labels in that set can be replaced by `Skip()`, allowing the removal of
 *  the label altogether. For example, in
 *
 *  {{{
 *  var y;
 *  lbl: {
 *    var x = 5;
 *    if (b) {
 *      y = x + 3;
 *      break lbl;
 *    } else {
 *      y = x + 5;
 *    }
 *  }
 *  }}}
 *
 *  the `break lbl` is in tail position for the label `lbl`, and can therefore
 *  be removed. After removal of the labeled block itself, the snippet rewrites
 *  as:
 *
 *  {{{
 *  var y;
 *  var x = 5;
 *  if (b) {
 *    y = x + 3;
 *  } else {
 *    y = x + 5;
 *  }
 *  }}}
 *
 *  `tailPosLabels` is a property of the *evaluation order*, and is therefore
 *  passed explicitly in `transform` methods (as opposed to being stored in the
 *  environment).
 *
 *  Second, `defaultBreakTargets`, a set of "default" labels, for which `break`
 *  is equivalent to `break lbl`. This is the set of tail-position labels of the
 *  closest enclosing `while/do..while/switch` "loop". `Break`s to labels in
 *  that set can be replaced by `break` without label, also allowing the removal
 *  of the labeled block. For example, in
 *
 *  {{{
 *  var y;
 *  lbl: {
 *    var i = 0;
 *    while (true) {
 *      if (x == 5) {
 *        y = 3;
 *        break lbl;
 *      }
 *      i = i - 1;
 *    }
 *  }
 *  }}}
 *
 *  the `break lbl` is in "default" position of the label `lbl`, since the
 *  closest enclosing `while/do..while/switch` is in tail position of `lbl`.
 *  The snippet can therefore be rewritten as:
 *
 *  {{{
 *  var y;
 *  var i = 0;
 *  while (true) {
 *    if (x == 5) {
 *      y = 3;
 *      break;
 *    }
 *    i = i - 1;
 *  }
 *  }}}
 *
 *  This is particularly interesting for `Range.foreach`, because, when inlined,
 *  its `return` statement becomes a `break` to an `inlinereturn` label, which
 *  is a default label at the point of the `return`. We can therefore avoid the
 *  useless labeled block for this common case.
 *
 *  `defaultBreakTargets` is property of the *scope*, and is therefore stored
 *  in the environment.
 *
 *  Finally, we also recover JavaScript `continue` statements out of labeled
 *  blocks that are immediately nested in the body of `while` loops. When we
 *  have:
 *
 *  {{{
 *  while (cond) {
 *    lab: {
 *      ...
 *      if (c)
 *        return(lab) expr;
 *      ...
 *    }
 *  }
 *  }}}
 *
 *  we make the `while` loop "acquire" the label as its own, and instruct the
 *  environment that, should an instruction want to return to that label, it
 *  should use `continue` instead of `break`. This produces:
 *
 *  {{{
 *  lab: while (cond) {
 *    ...
 *    if (c)
 *      { expr; continue lab }
 *    ...
 *  }
 *  }}}
 *
 *  Note that the result of `expr` is always discarded, in that case, since the
 *  labeled block was in statement position (by construction).
 *
 *  Similarly to the treatment with `defaultBreakTargets`, we keep track of
 *  `defaultContinueTargets`, the set of labels for which we can simply write
 *  `continue` instead of `continue lab`. This allows the above code to finally
 *  become:
 *
 *  {{{
 *  while (cond) {
 *    ...
 *    if (c)
 *      { expr; continue }
 *    ...
 *  }
 *  }}}
 *
 *  @author Sébastien Doeraene
 */
private[emitter] class FunctionEmitter(sjsGen: SJSGen) {
  import FunctionEmitter._
  import sjsGen._
  import jsGen._
  import config._
  import nameGen._
  import varGen._

  /** Desugars parameters and body to a JS function.
   */
  def desugarToFunction(enclosingClassName: ClassName, params: List[ParamDef],
      body: Tree, resultType: Type)(
      implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge,
      globalRefTracking: GlobalRefTracking, pos: Position): WithGlobals[js.Function] = {
    desugarToFunction(enclosingClassName, params, restParam = None, body,
        resultType)
  }

  /** Desugars parameters and body to a JS function (JS constructor variant).
   */
  def desugarToFunction(enclosingClassName: ClassName, params: List[ParamDef],
      restParam: Option[ParamDef], body: JSConstructorBody)(
      implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge,
      globalRefTracking: GlobalRefTracking, pos: Position): WithGlobals[js.Function] = {
    val bodyBlock = Block(body.allStats)(body.pos)
    new JSDesugar(globalRefTracking).desugarToFunction(
        params, restParam, bodyBlock, isStat = false,
        Env.empty(AnyType).withEnclosingClassName(Some(enclosingClassName)))
  }

  /** Desugars parameters and body to a JS function.
   */
  def desugarToFunction(enclosingClassName: ClassName, params: List[ParamDef],
      restParam: Option[ParamDef], body: Tree, resultType: Type)(
      implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge,
      globalRefTracking: GlobalRefTracking, pos: Position): WithGlobals[js.Function] = {
    new JSDesugar(globalRefTracking).desugarToFunction(
        params, restParam, body, isStat = resultType == NoType,
        Env.empty(resultType).withEnclosingClassName(Some(enclosingClassName)))
  }

  /** Desugars parameters and body to a JS function where `this` is given as
   *  an explicit normal parameter.
   */
  def desugarToFunctionWithExplicitThis(enclosingClassName: ClassName,
      params: List[ParamDef], body: Tree, resultType: Type)(
      implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge,
      globalRefTracking: GlobalRefTracking, pos: Position): WithGlobals[js.Function] = {
    new JSDesugar(globalRefTracking).desugarToFunctionWithExplicitThis(
        params, body, isStat = resultType == NoType,
        Env.empty(resultType).withEnclosingClassName(Some(enclosingClassName)))
  }

  /** Desugars parameters and body to a JS function.
   */
  def desugarToFunction(params: List[ParamDef], restParam: Option[ParamDef],
      body: Tree, resultType: Type)(
      implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge,
      globalRefTracking: GlobalRefTracking, pos: Position): WithGlobals[js.Function] = {
    new JSDesugar(globalRefTracking).desugarToFunction(
        params, restParam, body, isStat = resultType == NoType,
        Env.empty(resultType))
  }

  /** Desugars a class-level expression. */
  def desugarExpr(expr: Tree, resultType: Type)(
      implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge,
      globalRefTracking: GlobalRefTracking): WithGlobals[js.Tree] = {
    implicit val pos = expr.pos

    for (fun <- desugarToFunction(Nil, None, expr, resultType)) yield {
      fun match {
        case js.Function(_, Nil, None, js.Return(newExpr)) =>
          // no need for an IIFE, we can just use `newExpr` directly
          newExpr
        case _ =>
          js.Apply(fun, Nil)
      }
    }
  }

  private class JSDesugar(outerGlobalRefTracking: GlobalRefTracking)(
      implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge) {

    // Inside JSDesugar, we always track everything
    private implicit val globalRefTracking: GlobalRefTracking =
      GlobalRefTracking.All

    // For convenience
    private val es2015 = esFeatures.esVersion >= ESVersion.ES2015

    // Name management

    /** Whether we are running in the "optimistic naming" run.
     *
     *  In theory, `JSDesugar` works in two passes: the optimistic run,
     *  followed by the pessimistic run should the first one fail.
     *
     *  The optimistic run assumes that there is no clash between a local
     *  variable name and a global variable name (i.e., a `JSGlobalRef`). This
     *  allows it to straightforwardly translate IR identifiers to JS
     *  identifiers. While it does that, it records all the local variable and
     *  global variable names that are used in the method.
     *
     *  At the end of the translation, we check whether there was a clash by
     *  testing if the two sets intersect. If they do not, we are lucky, and
     *  can completely by-pass the pessimistic run. If there is a clash, then
     *  we need to restart everything in pessimistic mode.
     *
     *  In the pessimistic run, we use the set of global variable names that
     *  was collected during the optimistic run to *prevent* clashes from
     *  happening. This requires that we maintain a map of IR identifiers to
     *  allocated JS identifiers, as some IR identifiers need to be renamed.
     */
    private var isOptimisticNamingRun: Boolean = true

    private val globalVarNames = mutable.Set.empty[String]
    private val localVarNames = mutable.Set.empty[String]

    private lazy val localVarAllocs = mutable.Map.empty[LocalName, String]

    private def referenceGlobalName(name: String): Unit =
      globalVarNames += name

    private def extractWithGlobals[A](withGlobals: WithGlobals[A]): A = {
      for (globalRef <- withGlobals.globalVarNames)
        referenceGlobalName(globalRef)
      withGlobals.value
    }

    private def transformLocalName(name: LocalName): String = {
      if (isOptimisticNamingRun) {
        val jsName = genName(name)
        localVarNames += jsName
        jsName
      } else {
        // Slow path in a different `def` to keep it out of the JIT's way
        def slowPath(): String = {
          localVarAllocs.getOrElseUpdate(name, {
            var suffix = 0
            val baseJSName = genName(name)
            var result: String = baseJSName
            while (globalVarNames.contains(result) ||
                localVarNames.contains(result)) {
              suffix += 1
              result = baseJSName + "$" + suffix
            }
            localVarNames += result
            result
          })
        }
        slowPath()
      }
    }

    private var syntheticVarCounter: Int = 0

    private def newSyntheticVar()(implicit pos: Position): js.Ident = {
      syntheticVarCounter += 1
      fileLevelVarIdent(VarField.x, syntheticVarCounter.toString())
    }

    @inline
    @tailrec
    private def performOptimisticThenPessimisticRuns[A](
        body: => A): WithGlobals[A] = {
      val result = body
      if (!isOptimisticNamingRun || !globalVarNames.exists(localVarNames)) {
        /* At this point, filter out the global refs that do not need to be
         * tracked in the outer context.
         *
         * By default, only dangerous global refs need to be tracked outside of
         * functions, to power `mentionedDangerousGlobalRefs` In that case, the
         * set is hopefully already emptied at this point for the large majority
         * of methods, if not all.
         *
         * However, when integrating with GCC, we must tell it a list of all the
         * global variables that are accessed in an externs file. In that case, we
         * need to track all global variables across functions and classes. This is
         * slower, but running GCC will take most of the time anyway in that case.
         */
        val outerGlobalRefs =
          outerGlobalRefTracking.refineFrom(globalRefTracking, globalVarNames.toSet)

        WithGlobals(result, outerGlobalRefs)
      } else {
        /* Clear the local var names, but *not* the global var names.
         * In the pessimistic run, we will use the knowledge gathered during
         * the optimistic run about the set of global variable names that are
         * used.
         */
        localVarNames.clear()
        isOptimisticNamingRun = false
        performOptimisticThenPessimisticRuns(body)
      }
    }

    // Record names

    def makeRecordFieldIdentForVarRef(tree: RecordSelect)(
        implicit pos: Position): js.Ident = {

      val recIdent = (tree.record: @unchecked) match {
        case VarRef(ident)                 => transformLocalVarIdent(ident)
        case Transient(JSVarRef(ident, _)) => ident
        case record: RecordSelect          => makeRecordFieldIdentForVarRef(record)
      }

      // Since this is only used for VarRefs, we never need an original name
      makeRecordFieldIdent(recIdent, tree.field.name, NoOriginalName)
    }

    def makeRecordFieldIdent(recIdent: js.Ident,
        fieldName: SimpleFieldName, fieldOrigName: OriginalName)(
        implicit pos: Position): js.Ident = {

      /* "__" is a safe separator for generated names because JSGen avoids it
       * when generating `LocalName`s and `SimpleFieldName`s.
       */
      val name = recIdent.name + "__" + genName(fieldName)
      val originalName = OriginalName(
          recIdent.originalName.getOrElse(recIdent.name) ++ UTF8Period ++
          fieldOrigName.getOrElse(fieldName))
      js.Ident(name, originalName)
    }

    // LHS'es for labeled expressions

    val usedLabels = mutable.Set.empty[LabelName]

    // Now the work

    /** Desugars parameters and body to a JS function where `this` is given as
     *  a normal parameter.
     */
    def desugarToFunctionWithExplicitThis(
        params: List[ParamDef], body: Tree, isStat: Boolean, env0: Env)(
        implicit pos: Position): WithGlobals[js.Function] = {

      performOptimisticThenPessimisticRuns {
        val thisIdent = fileLevelVarIdent(VarField.thiz, thisOriginalName)
        val env = env0.withExplicitThis()
        val js.Function(jsArrow, jsParams, restParam, jsBody) =
          desugarToFunctionInternal(arrow = false, params, None, body, isStat, env)
        js.Function(jsArrow, js.ParamDef(thisIdent) :: jsParams, restParam, jsBody)
      }
    }

    /** Desugars parameters and body to a JS function.
     */
    def desugarToFunction(params: List[ParamDef], restParam: Option[ParamDef],
        body: Tree, isStat: Boolean, env0: Env)(
        implicit pos: Position): WithGlobals[js.Function] = {
      performOptimisticThenPessimisticRuns {
        desugarToFunctionInternal(arrow = false, params, restParam, body, isStat, env0)
      }
    }

    /** Desugars parameters and body to a JS function.
     */
    private def desugarToFunctionInternal(arrow: Boolean,
        params: List[ParamDef], restParam: Option[ParamDef], body: Tree,
        isStat: Boolean, env0: Env)(
        implicit pos: Position): js.Function = {

      val env = env0.withParams(params ++ restParam)

      val newBody = if (isStat) {
        body match {
          // Necessary to optimize away top-level _return: {} blocks
          case Labeled(label, _, body) =>
            transformStat(body, Set.empty)(
                env.withLabeledExprLHS(label, Lhs.ReturnFromFunction))
          case _ =>
            transformStat(body, Set.empty)(env)
        }
      } else {
        pushLhsInto(Lhs.ReturnFromFunction, body, Set.empty)(env)
      }

      val cleanedNewBody = newBody match {
        case js.Block(stats :+ js.Return(js.Undefined())) => js.Block(stats)
        case other                                        => other
      }

      val actualArrowFun = arrow && esFeatures.useECMAScript2015Semantics
      val jsParams = params.map(transformParamDef(_))

      if (es2015) {
        val jsRestParam = restParam.map(transformParamDef(_))
        js.Function(actualArrowFun, jsParams, jsRestParam, cleanedNewBody)
      } else {
        val patchedBody = restParam.fold {
          cleanedNewBody
        } { restParam =>
          js.Block(makeExtractRestParam(restParam, jsParams.size), cleanedNewBody)
        }

        js.Function(actualArrowFun, jsParams, None, patchedBody)
      }
    }

    private def makeExtractRestParam(restParamDef: ParamDef, offset: Int)(
        implicit pos: Position): js.Tree = {
      val lenIdent = newSyntheticVar()
      val len = js.VarRef(lenIdent)

      val counterIdent = newSyntheticVar()
      val counter = js.VarRef(counterIdent)

      val restParamIdent = transformLocalVarIdent(restParamDef.name,
          restParamDef.originalName)
      val restParam = js.VarRef(restParamIdent)

      val arguments = js.VarRef(js.Ident("arguments"))

      def or0(tree: js.Tree): js.Tree =
        js.BinaryOp(JSBinaryOp.|, tree, js.IntLiteral(0)(tree.pos))(tree.pos)

      js.Block(
        // const len = arguments.length | 0
        genLet(lenIdent, mutable = false,
            or0(genIdentBracketSelect(arguments, "length"))),
        // let i = 
        genLet(counterIdent, mutable = true, js.IntLiteral(offset)),
        // const restParam = []
        genLet(restParamIdent, mutable = false, js.ArrayConstr(Nil)),
        // while (i < len)
        js.While(js.BinaryOp(JSBinaryOp.<, counter, len), js.Block(
          // restParam.push(arguments[i]);
          js.Apply(
              genIdentBracketSelect(restParam, "push"), List(
              js.BracketSelect(arguments, counter))),
          // i = (i + 1) | 0
          js.Assign(counter, or0(js.BinaryOp(JSBinaryOp.+,
              counter, js.IntLiteral(1))))
        ))
      )
    }

    /** Desugar a statement of the IR into ES5 JS */
    def transformStat(tree: Tree, tailPosLabels: Set[LabelName])(
        implicit env: Env): js.Tree = {
      import TreeDSL._

      implicit val pos = tree.pos

      tree match {
        // VarDefs at the end of block. Normal VarDefs are handled in
        // transformBlockStats

        case VarDef(_, _, _, _, rhs) =>
          pushLhsInto(Lhs.Discard, rhs, tailPosLabels)

        // Statement-only language constructs

        case Skip() =>
          js.Skip()

        case Assign(lhs, rhs) =>
          lhs match {
            case Select(qualifier, field) =>
              unnest(checkNotNull(qualifier), rhs) { (newQualifier, newRhs, env0) =>
                implicit val env = env0
                js.Assign(
                    genSelect(transformExprNoChar(newQualifier), field)(lhs.pos),
                    transformExpr(newRhs, lhs.tpe))
              }

            case ArraySelect(array, index) =>
              unnest(checkNotNull(array), index, rhs) { (newArray, newIndex, newRhs, env0) =>
                implicit val env = env0
                val genArray = transformExprNoChar(newArray)
                val genIndex = transformExprNoChar(newIndex)
                val genRhs = transformExpr(newRhs, lhs.tpe)

                /* We need to use a checked 'set' if at least one of the following applies:
                 * - Array index out of bounds are checked, or
                 * - Array stores are checked and the array is an array of reference types.
                 */
                val checked = {
                  (semantics.arrayIndexOutOfBounds != CheckedBehavior.Unchecked) ||
                  ((semantics.arrayStores != CheckedBehavior.Unchecked) && RefArray.is(array.tpe))
                }

                if (checked) {
                  genSyntheticPropApply(genArray, SyntheticProperty.set, genIndex, genRhs)
                } else {
                  js.Assign(
                      js.BracketSelect(
                          genSyntheticPropSelect(genArray, SyntheticProperty.u)(lhs.pos),
                          genIndex)(lhs.pos),
                      genRhs)
                }
              }

            case lhs: RecordSelect =>
              val newLhs = Transient(JSVarRef(makeRecordFieldIdentForVarRef(lhs),
                  mutable = true)(lhs.tpe))
              pushLhsInto(Lhs.Assign(newLhs), rhs, tailPosLabels)

            case JSPrivateSelect(qualifier, field) =>
              unnest(qualifier, rhs) { (newQualifier, newRhs, env0) =>
                implicit val env = env0
                js.Assign(
                    genJSPrivateSelect(transformExprNoChar(newQualifier), field)(
                        moduleContext, globalKnowledge, lhs.pos),
                    transformExprNoChar(newRhs))
              }

            case JSSelect(qualifier, item) =>
              unnest(qualifier, item, rhs) {
                (newQualifier, newItem, newRhs, env0) =>
                  implicit val env = env0
                  js.Assign(
                      genBracketSelect(transformExprNoChar(newQualifier),
                          transformExprNoChar(newItem))(lhs.pos),
                      transformExprNoChar(newRhs))
              }

            case JSSuperSelect(superClass, qualifier, item) =>
              unnest(superClass, qualifier, item, rhs) {
                (newSuperClass, newQualifier, newItem, newRhs, env0) =>
                  implicit val env = env0
                  genCallHelper(VarField.superSet, transformExprNoChar(newSuperClass),
                      transformExprNoChar(newQualifier), transformExprNoChar(item),
                      transformExprNoChar(rhs))
              }

            case SelectStatic(item) =>
              if (needToUseGloballyMutableVarSetter(item.name)) {
                unnest(rhs) { (rhs, env0) =>
                  implicit val env = env0
                  js.Apply(globalVar(VarField.u, item.name), transformExpr(rhs, lhs.tpe) :: Nil)
                }
              } else {
                // Assign normally.
                pushLhsInto(Lhs.Assign(lhs), rhs, tailPosLabels)
              }

            case _:VarRef | _:JSGlobalRef =>
              pushLhsInto(Lhs.Assign(lhs), rhs, tailPosLabels)
          }

        case StoreModule() =>
          val enclosingClassName = env.enclosingClassName.getOrElse {
            throw new AssertionError(
                "Need enclosing class for StoreModule().")
          }
          js.Assign(globalVar(VarField.n, enclosingClassName), js.This())

        case While(cond, body) =>
          val loopEnv = env.withInLoopForVarCapture(true)

          /* If there is a Labeled block immediately enclosed within the body
           * of this while loop, acquire its label and use it as the while's
           * label, turning it into a `continue` label.
           */
          val (optLabel, newBody) = {
            body match {
              case Labeled(label, _, innerBody) =>
                val innerBodyEnv = loopEnv
                  .withLabeledExprLHS(label, Lhs.Discard)
                  .withTurnLabelIntoContinue(label.name)
                  .withDefaultBreakTargets(tailPosLabels)
                  .withDefaultContinueTargets(Set(label.name))
                val newBody =
                  pushLhsInto(Lhs.Discard, innerBody, Set(label.name))(innerBodyEnv)
                val optLabel = if (usedLabels.contains(label.name))
                  Some(transformLabelIdent(label))
                else
                  None
                (optLabel, newBody)

              case _ =>
                val bodyEnv = loopEnv
                  .withDefaultBreakTargets(tailPosLabels)
                  .withDefaultContinueTargets(Set.empty)
                val newBody = transformStat(body, Set.empty)(bodyEnv)
                (None, newBody)
            }
          }

          /* We cannot simply unnest(cond) here, because that would eject the
           * evaluation of the condition out of the loop.
           */
          if (isExpression(cond)) {
            js.While(transformExprNoChar(cond)(loopEnv), newBody, optLabel)
          } else {
            js.While(js.BooleanLiteral(true), {
              unnest(cond) { (newCond, env0) =>
                implicit val env = env0
                js.If(transformExprNoChar(newCond), newBody, js.Break())
              } (loopEnv)
            }, optLabel)
          }

        case ForIn(obj, keyVar, keyVarOriginalName, body) =>
          unnest(obj) { (newObj, env0) =>
            implicit val env = env0

            val lhs = genEmptyImmutableLet(
                transformLocalVarIdent(keyVar, keyVarOriginalName))
            val bodyEnv = env
              .withDef(keyVar, mutable = false)
              .withInLoopForVarCapture(true)

            js.ForIn(lhs, transformExprNoChar(newObj), {
              transformStat(body, Set.empty)(bodyEnv)
            })
          }

        case Debugger() =>
          js.Debugger()

        case JSSuperConstructorCall(args) =>
          unnestOrSpread(args) { (newArgs, env0) =>
            implicit val env = env0

            val enclosingClassName = env.enclosingClassName.getOrElse {
              throw new AssertionError(
                  "Need enclosing class for super constructor call.")
            }

            val superCtorCall = if (useClassesForJSClassesAndThrowables) {
              js.Apply(js.Super(), newArgs.map(transformJSArg))
            } else {
              val superCtor = {
                if (globalKnowledge.hasStoredSuperClass(enclosingClassName)) {
                  fileLevelVar(VarField.superClass)
                } else {
                  val superClass =
                    globalKnowledge.getSuperClassOfJSClass(enclosingClassName)
                  extractWithGlobals(genJSClassConstructor(superClass))
                }
              }

              if (needsToTranslateAnySpread(newArgs)) {
                val argArray = spreadToArgArray(newArgs)
                js.Apply(
                    genIdentBracketSelect(superCtor, "apply"),
                    List(js.This(), transformExprNoChar(argArray)))
              } else {
                js.Apply(
                    genIdentBracketSelect(superCtor, "call"),
                    js.This() :: newArgs.map(transformJSArg))
              }
            }

            val enclosingClassFieldDefs =
              globalKnowledge.getFieldDefs(enclosingClassName)

            val fieldDefs = for {
              field <- enclosingClassFieldDefs
              if !field.flags.namespace.isStatic
            } yield {
              implicit val pos = field.pos
              /* Here, a naive translation would emit something like this:
               *
               *   this["field"] = 0;
               *
               * However, this won't work if we override a getter from the
               * superclass with a val in this class, because the assignment
               * would try to set the property which has a getter but no
               * setter.
               * Instead, we must force the creation of a field on the object,
               * irrespective of the presence of a getter/setter in the
               * prototype chain. This is why we use `defineProperty`:
               *
               *   Object.defineProperty(this, "field", {
               *     "configurable": true,
               *     "enumerable": true,
               *     "writable": true,
               *     "value": 0
               *   });
               *
               * which has all the same semantics as the assignment, except
               * it disregards the prototype chain.
               *
               * For private fields, we can use a normal assignment, since they
               * cannot clash with anything else in the prototype chain anyway.
               */

              val zero = genBoxedZeroOf(field.ftpe)

              field match {
                case FieldDef(_, name, _, _) =>
                  js.Assign(
                      genJSPrivateSelect(js.This(), name),
                      zero)

                case JSFieldDef(_, name, _) =>
                  unnest(name) { (newName, env0) =>
                    implicit val env = env0

                    val descriptor = List(
                        "configurable" -> js.BooleanLiteral(true),
                        "enumerable" -> js.BooleanLiteral(true),
                        "writable" -> js.BooleanLiteral(true),
                        "value" -> zero
                    )

                    extractWithGlobals(
                        genDefineProperty(js.This(), transformExprNoChar(newName), descriptor))
                  }
              }
            }

            js.Block(superCtorCall :: fieldDefs)
          }

        case JSDelete(qualifier, item) =>
          unnest(qualifier, item) { (newQual, newItem, env0) =>
            implicit val env = env0
            js.Delete(genBracketSelect(
                transformExprNoChar(newQual), transformExprNoChar(newItem)))
          }

        // Treat 'return' as an LHS

        case Return(expr, label) =>
          pushLhsInto(Lhs.Return(label), expr, tailPosLabels)

        case Transient(SystemArrayCopy(src, srcPos, dest, destPos, length)) =>
          unnest(List(src, srcPos, dest, destPos, length)) { (newArgs, env0) =>
            implicit val env = env0
            val jsArgs = newArgs.map(transformExprNoChar(_))

            def genUnchecked(): js.Tree = {
              if (esFeatures.esVersion >= ESVersion.ES2015 && semantics.nullPointers == CheckedBehavior.Unchecked)
                genSyntheticPropApply(jsArgs.head, SyntheticProperty.copyTo, jsArgs.tail)
              else
                genCallHelper(VarField.systemArraycopy, jsArgs: _*)
            }

            if (semantics.arrayStores == Unchecked) {
              genUnchecked()
            } else {
              (src.tpe, dest.tpe) match {
                case (PrimArray(srcPrimRef), PrimArray(destPrimRef)) if srcPrimRef == destPrimRef =>
                  genUnchecked()
                case (RefArray(), RefArray()) =>
                  genCallHelper(VarField.systemArraycopyRefs, jsArgs: _*)
                case _ =>
                  genCallHelper(VarField.systemArraycopyFull, jsArgs: _*)
              }
            }
          }

        /* Anything else is an expression => pushLhsInto(Lhs.Discard, _)
         * In order not to duplicate all the code of pushLhsInto() here, we
         * use a trick: Lhs.Discard is a dummy LHS that says "do nothing
         * with the result of the rhs".
         * This is exactly what an expression statement is doing: it evaluates
         * the expression, but does nothing with its result.
         */

        case _ =>
          pushLhsInto(Lhs.Discard, tree, tailPosLabels)
      }
    }

    def transformBlockStats(trees: List[Tree])(
        implicit env: Env): (List[js.Tree], Env) = {

      @tailrec
      def transformLoop(trees: List[Tree], env: Env,
          acc: List[js.Tree]): (List[js.Tree], Env) = trees match {
        case VarDef(ident, originalName, tpe, mutable, rhs) :: ts =>
          val newEnv = env.withDef(ident, mutable)
          val lhs = Lhs.VarDef(transformLocalVarIdent(ident, originalName),
              tpe, mutable)
          val newTree = pushLhsInto(lhs, rhs, Set.empty)(env)
          transformLoop(ts, newEnv, newTree :: acc)

        case tree :: ts =>
          transformLoop(ts, env, transformStat(tree, Set.empty)(env) :: acc)

        case Nil =>
          (acc.reverse, env)
      }

      transformLoop(trees, env, Nil)
    }

    /** Same as `unnest`, but allows (and preserves) [[JSSpread]]s at the
     *  top-level.
     */
    def unnestOrSpread(args: List[TreeOrJSSpread])(
        makeStat: (List[TreeOrJSSpread], Env) => js.Tree)(
        implicit env: Env): js.Tree = {
      unnestOrSpread(Nil, args) { (newNil, newArgs, env) =>
        assert(newNil.isEmpty)
        makeStat(newArgs, env)
      }
    }

    /** Same as `unnest`, but allows (and preserves) [[JSSpread]]s at the
     *  top-level.
     */
    def unnestOrSpread(nonSpreadArgs: List[Tree], args: List[TreeOrJSSpread])(
        makeStat: (List[Tree], List[TreeOrJSSpread], Env) => js.Tree)(
        implicit env: Env): js.Tree = {
      val (argsNoSpread, argsWereSpread) = args.map {
        case JSSpread(items) => (items, true)
        case arg: Tree       => (arg, false)
      }.unzip

      unnest(nonSpreadArgs ::: argsNoSpread) { (newAllArgs, env) =>
        val (newNonSpreadArgs, newArgsNoSpread) =
          newAllArgs.splitAt(nonSpreadArgs.size)
        val newArgs = newArgsNoSpread.zip(argsWereSpread).map {
          case (newItems, true) => JSSpread(newItems)(newItems.pos)
          case (newArg, false)  => newArg
        }
        makeStat(newNonSpreadArgs, newArgs, env)
      }
    }

    /** Unnest complex constructs in argument position in temporary variables
     *
     *  If all the arguments are JS expressions, there is nothing to do.
     *  Any argument that is not a JS expression must be unnested and stored
     *  in a temporary variable before the statement produced by `makeStat`.
     *
     *  But *this changes the evaluation order!* In order not to lose it, it
     *  is necessary to also unnest arguments that are expressions but that
     *  are supposed to be evaluated before the argument-to-be-unnested and
     *  could have side-effects or even whose evaluation could be influenced
     *  by the side-effects of another unnested argument.
     *
     *  Without deep effect analysis, which we do not do, we need to take
     *  a very pessimistic approach, and unnest any expression that contains
     *  an identifier (except those after the last non-expression argument).
     *  Hence the predicate `isPureExpressionWithoutIdent`.
     */
    def unnest(args: List[Tree])(
        makeStat: (List[Tree], Env) => js.Tree)(
        implicit env: Env): js.Tree = {
      if (args forall isExpression) makeStat(args, env)
      else {
        val extractedStatements = new scala.collection.mutable.ListBuffer[js.Tree]
        var innerEnv = env

        /* Attention! Everything must be processed recursively
         * *right-to-left*! Indeed, the point is that noExtractYet will tell
         * whether anything supposed to be evaluated *after* the currently
         * being processed expression has been (at least partly) extracted
         * in temporary variables (or simply statements, in the Block case).
         * If nothing has, we can keep more in place without having to extract
         * that expression in a temporary variable.
         *
         * Also note that environments are handled the wrong way around. This is
         * ok, since the same local name may not appear multiple times inside a
         * single method.
         */

        def rec(arg: Tree)(implicit env: Env): Tree = {
          def noExtractYet = extractedStatements.isEmpty

          val keepAsIs =
            if (noExtractYet) isExpression(arg)
            else isPureExpression(arg)

          if (keepAsIs) {
            arg
          } else {
            implicit val pos = arg.pos
            arg match {
              case Block(stats :+ expr) =>
                val (jsStats, newEnv) = transformBlockStats(stats)
                val result = rec(expr)(newEnv)
                // Put the stats in a Block because ++=: is not smart
                js.Block(jsStats) +=: extractedStatements
                innerEnv = stats.foldLeft(innerEnv) { (prev, stat) =>
                  stat match {
                    case VarDef(name, _, _, mutable, _) =>
                      prev.withDef(name, mutable)
                    case _ =>
                      prev
                  }
                }
                result

              case UnaryOp(op, lhs) if op != UnaryOp.CheckNotNull || noExtractYet =>
                UnaryOp(op, rec(lhs))
              case BinaryOp(op, lhs, rhs) =>
                val newRhs = rec(rhs)
                BinaryOp(op, rec(lhs), newRhs)
              case JSBinaryOp(op, lhs, rhs) =>
                val newRhs = rec(rhs)
                JSBinaryOp(op, rec(lhs), newRhs)
              case JSUnaryOp(op, lhs) =>
                JSUnaryOp(op, rec(lhs))
              case IsInstanceOf(expr, testType) =>
                IsInstanceOf(rec(expr), testType)

              case AsInstanceOf(expr, tpe)
                  if noExtractYet || semantics.asInstanceOfs == Unchecked =>
                AsInstanceOf(rec(expr), tpe)

              case NewArray(tpe, length) =>
                NewArray(tpe, rec(length))
              case ArrayValue(tpe, elems) =>
                ArrayValue(tpe, recs(elems))
              case JSArrayConstr(items) if !needsToTranslateAnySpread(items) =>
                JSArrayConstr(recsOrSpread(items))

              case arg @ JSObjectConstr(items)
                  if !doesObjectConstrRequireDesugaring(arg) =>
                // We need to properly interleave keys and values here
                val newItems = items.foldRight[List[(Tree, Tree)]](Nil) {
                  case ((key, value), acc) =>
                    val newValue = rec(value) // value first!
                    val newKey = rec(key)
                    (newKey, newValue) :: acc
                }
                JSObjectConstr(newItems)

              case Closure(arrow, captureParams, params, restParam, body, captureValues) =>
                Closure(arrow, captureParams, params, restParam, body, recs(captureValues))

              case New(className, constr, args) if noExtractYet =>
                New(className, constr, recs(args))
              case Select(qualifier, item) if noExtractYet =>
                Select(rec(qualifier), item)(arg.tpe)
              case Apply(flags, receiver, method, args) if noExtractYet =>
                val newArgs = recs(args)
                Apply(flags, rec(receiver), method, newArgs)(arg.tpe)
              case ApplyStatically(flags, receiver, className, method, args) if noExtractYet =>
                val newArgs = recs(args)
                ApplyStatically(flags, rec(receiver), className, method, newArgs)(arg.tpe)
              case ApplyStatic(flags, className, method, args) if noExtractYet =>
                ApplyStatic(flags, className, method, recs(args))(arg.tpe)
              case ApplyDynamicImport(flags, className, method, args) if noExtractYet =>
                ApplyDynamicImport(flags, className, method, recs(args))
              case ArrayLength(array) if noExtractYet =>
                ArrayLength(rec(array))
              case ArraySelect(array, index) if noExtractYet =>
                val newIndex = rec(index)
                ArraySelect(rec(array), newIndex)(arg.tpe)
              case RecordSelect(record, field) if noExtractYet =>
                RecordSelect(rec(record), field)(arg.tpe)

              case Transient(Cast(expr, tpe)) =>
                Transient(Cast(rec(expr), tpe))
              case Transient(ZeroOf(runtimeClass)) =>
                Transient(ZeroOf(rec(runtimeClass)))
              case Transient(ObjectClassName(obj)) =>
                Transient(ObjectClassName(rec(obj)))

              case Transient(NativeArrayWrapper(elemClass, nativeArray)) if noExtractYet =>
                val newNativeArray = rec(nativeArray)
                val newElemClass = rec(elemClass)
                Transient(NativeArrayWrapper(newElemClass, newNativeArray)(arg.tpe))
              case Transient(ArrayToTypedArray(expr, primRef)) if noExtractYet =>
                Transient(ArrayToTypedArray(rec(expr), primRef))
              case Transient(TypedArrayToArray(expr, primRef)) if noExtractYet =>
                Transient(TypedArrayToArray(rec(expr), primRef))

              case If(cond, thenp, elsep)
                  if noExtractYet && isExpression(thenp) && isExpression(elsep) =>
                If(rec(cond), thenp, elsep)(arg.tpe)

              case _ =>
                val temp = newSyntheticVar()
                val computeTemp = pushLhsInto(
                    Lhs.VarDef(temp, arg.tpe, mutable = false), arg,
                    Set.empty)
                computeTemp +=: extractedStatements
                Transient(JSVarRef(temp, mutable = false)(arg.tpe))
            }
          }
        }

        def recs(args: List[Tree])(implicit env: Env): List[Tree] = {
          // This is a right-to-left map
          args.foldRight[List[Tree]](Nil) { (arg, acc) =>
            rec(arg) :: acc
          }
        }

        def recsOrSpread(args: List[TreeOrJSSpread])(
            implicit env: Env): List[TreeOrJSSpread] = {
          args.foldRight[List[TreeOrJSSpread]](Nil) { (arg, acc) =>
            val newArg = arg match {
              case JSSpread(items) => JSSpread(rec(items))(arg.pos)
              case arg: Tree       => rec(arg)
            }
            newArg :: acc
          }
        }

        val newArgs = recs(args)

        assert(extractedStatements.nonEmpty,
            "Reached computeTemps with no temp to compute")

        val newStatement = makeStat(newArgs, innerEnv)
        js.Block(extractedStatements.result() ::: List(newStatement))(newStatement.pos)
      }
    }

    /** Same as above, for a single argument */
    def unnest(arg: Tree)(makeStat: (Tree, Env) => js.Tree)(
        implicit env: Env): js.Tree = {
      unnest(List(arg)) { (newArgs, env) =>
        val newArg :: Nil = newArgs
        makeStat(newArg, env)
      }
    }

    /** Same as above, for two arguments */
    def unnest(arg1: Tree, arg2: Tree)(
        makeStat: (Tree, Tree, Env) => js.Tree)(
        implicit env: Env): js.Tree = {
      unnest(List(arg1, arg2)) { (newArgs, env) =>
        val newArg1 :: newArg2 :: Nil = newArgs
        makeStat(newArg1, newArg2, env)
      }
    }

    /** Same as above, for 3 arguments */
    def unnest(arg1: Tree, arg2: Tree, arg3: Tree)(
        makeStat: (Tree, Tree, Tree, Env) => js.Tree)(
        implicit env: Env): js.Tree = {
      unnest(List(arg1, arg2, arg3)) { (newArgs, env) =>
        val newArg1 :: newArg2 :: newArg3 :: Nil = newArgs
        makeStat(newArg1, newArg2, newArg3, env)
      }
    }

    /** Same as above, for 4 arguments */
    def unnest(arg1: Tree, arg2: Tree, arg3: Tree, arg4: Tree)(
        makeStat: (Tree, Tree, Tree, Tree, Env) => js.Tree)(
        implicit env: Env): js.Tree = {
      unnest(List(arg1, arg2, arg3, arg4)) { (newArgs, env) =>
        val newArg1 :: newArg2 :: newArg3 :: newArg4 :: Nil = newArgs
        makeStat(newArg1, newArg2, newArg3, newArg4, env)
      }
    }

    /** Same as above, for one head argument and a list of arguments */
    def unnest(arg0: Tree, args: List[Tree])(
        makeStat: (Tree, List[Tree], Env) => js.Tree)(
        implicit env: Env): js.Tree = {
      unnest(arg0 :: args) { (newArgs, env) =>
        makeStat(newArgs.head, newArgs.tail, env)
      }
    }

    /** Unnest for the fields of a `JSObjectConstr`. */
    def unnestJSObjectConstrFields(fields: List[(Tree, Tree)])(
        makeStat: (List[(Tree, Tree)], Env) => js.Tree)(
        implicit env: Env): js.Tree = {

      // Collect all the trees that need unnesting, in evaluation order
      val trees = fields.flatMap { field =>
        List(field._1, field._2)
      }

      unnest(trees) { (newTrees, env) =>
        // Re-decompose all the trees into pairs of (key, value)
        val newTreesIterator = newTrees.iterator
        val newFields = List.newBuilder[(Tree, Tree)]
        while (newTreesIterator.hasNext)
          newFields += ((newTreesIterator.next(), newTreesIterator.next()))

        makeStat(newFields.result(), env)
      }
    }

    /** Common implementation for the functions below.
     *  A pure expression can be moved around or executed twice, because it
     *  will always produce the same result and never have side-effects.
     *  A side-effect free expression can be elided if its result is not used.
     */
    private def isExpressionInternal(tree: Tree, allowUnpure: Boolean,
        allowSideEffects: Boolean)(implicit env: Env): Boolean = {

      require(!allowSideEffects || allowUnpure)

      def allowBehavior(behavior: CheckedBehavior): Boolean =
        allowSideEffects || behavior == Unchecked

      def testJSArg(tree: TreeOrJSSpread): Boolean = tree match {
        case JSSpread(items) => es2015 && test(items)
        case tree: Tree      => test(tree)
      }

      def testNPE(tree: Tree): Boolean = {
        val npeOK = allowBehavior(semantics.nullPointers) || !tree.tpe.isNullable
        npeOK && test(tree)
      }

      def test(tree: Tree): Boolean = tree match {
        // Atomic expressions
        case _: Literal       => true
        case _: This          => true
        case _: JSNewTarget   => true
        case _: JSLinkingInfo => true

        // Vars (side-effect free, pure if immutable)
        case VarRef(name) =>
          allowUnpure || !env.isLocalMutable(name)
        case Transient(JSVarRef(_, mutable)) =>
          allowUnpure || !mutable

        // Division and modulo, preserve pureness unless they can divide by 0
        case BinaryOp(BinaryOp.Int_/ | BinaryOp.Int_%, lhs, rhs) if !allowSideEffects =>
          rhs match {
            case IntLiteral(r) if r != 0 => test(lhs)
            case _                       => false
          }
        case BinaryOp(BinaryOp.Long_/ | BinaryOp.Long_%, lhs, rhs) if !allowSideEffects =>
          rhs match {
            case LongLiteral(r) if r != 0L => test(lhs)
            case _                         => false
          }

        // String_charAt preserves pureness iff the semantics for stringIndexOutOfBounds are unchecked
        case BinaryOp(BinaryOp.String_charAt, lhs, rhs) =>
          allowBehavior(semantics.stringIndexOutOfBounds) && test(lhs) && test(rhs)

        // Binary Class_x operations that can have side effects
        case BinaryOp(BinaryOp.Class_cast, lhs, rhs) =>
          allowBehavior(semantics.asInstanceOfs) && test(lhs) && test(rhs)
        case BinaryOp(BinaryOp.Class_newArray, lhs, rhs) =>
          allowSideEffects && test(lhs) && test(rhs)

        // Expressions preserving pureness (modulo NPE)
        case Block(trees)            => trees forall test
        case If(cond, thenp, elsep)  => test(cond) && test(thenp) && test(elsep)
        case BinaryOp(_, lhs, rhs)   => test(lhs) && test(rhs)
        case UnaryOp(op, lhs)        => if (op == UnaryOp.CheckNotNull) testNPE(lhs) else test(lhs)
        case ArrayLength(array)      => testNPE(array)
        case RecordSelect(record, _) => test(record)
        case IsInstanceOf(expr, _)   => test(expr)
        case IdentityHashCode(expr)  => test(expr)
        case GetClass(arg)           => testNPE(arg)

        // Expressions preserving pureness (modulo NPE) but requiring that expr be a var
        case WrapAsThrowable(expr @ (VarRef(_) | Transient(JSVarRef(_, _))))     => test(expr)
        case UnwrapFromThrowable(expr @ (VarRef(_) | Transient(JSVarRef(_, _)))) => testNPE(expr)

        // Transients preserving pureness (modulo NPE)
        case Transient(Cast(expr, _)) =>
          test(expr)
        case Transient(ZeroOf(runtimeClass)) =>
          test(runtimeClass) // ZeroOf *assumes* that `runtimeClass ne null`
        case Transient(ObjectClassName(obj)) =>
          testNPE(obj)

        // Expressions preserving side-effect freedom (modulo NPE)
        case Select(qualifier, _) =>
          allowUnpure && testNPE(qualifier)
        case SelectStatic(_) =>
          allowUnpure
        case ArrayValue(tpe, elems) =>
          allowUnpure && (elems forall test)
        case Clone(arg) =>
          allowUnpure && testNPE(arg)
        case JSArrayConstr(items) =>
          allowUnpure && (items.forall(testJSArg))
        case tree @ JSObjectConstr(items) =>
          allowUnpure &&
          !doesObjectConstrRequireDesugaring(tree) &&
          items.forall { item =>
            test(item._1) && test(item._2)
          }
        case Closure(arrow, captureParams, params, restParam, body, captureValues) =>
          allowUnpure && (captureValues forall test)

        // Transients preserving side-effect freedom (modulo NPE)
        case Transient(NativeArrayWrapper(elemClass, nativeArray)) =>
          allowUnpure && testNPE(elemClass) && test(nativeArray)
        case Transient(ArrayToTypedArray(expr, primRef)) =>
          allowUnpure && testNPE(expr)

        // Scala expressions that can always have side-effects
        case New(className, constr, args) =>
          allowSideEffects && (args forall test)
        case LoadModule(className) => // unfortunately
          allowSideEffects
        case Apply(_, receiver, method, args) =>
          allowSideEffects && test(receiver) && (args forall test)
        case ApplyStatically(_, receiver, className, method, args) =>
          allowSideEffects && test(receiver) && (args forall test)
        case ApplyStatic(_, className, method, args) =>
          allowSideEffects && (args forall test)
        case ApplyDynamicImport(_, _, _, args) =>
          allowSideEffects && args.forall(test)

        // Transients with side effects.
        case Transient(TypedArrayToArray(expr, primRef)) =>
          allowSideEffects && test(expr) // may TypeError

        // Array operations with conditional exceptions
        case NewArray(tpe, length) =>
          allowBehavior(semantics.negativeArraySizes) && allowUnpure && test(length)
        case ArraySelect(array, index) =>
          allowBehavior(semantics.arrayIndexOutOfBounds) && allowUnpure && testNPE(array) && test(index)

        // Casts
        case AsInstanceOf(expr, _) =>
          allowBehavior(semantics.asInstanceOfs) && test(expr)

        // JavaScript expressions that can always have side-effects
        case SelectJSNativeMember(_, _) =>
          allowSideEffects
        case JSNew(fun, args) =>
          allowSideEffects && test(fun) && (args.forall(testJSArg))
        case Transient(JSNewVararg(ctor, argArray)) =>
          allowSideEffects && test(ctor) && test(argArray)
        case JSPrivateSelect(qualifier, _) =>
          allowSideEffects && test(qualifier)
        case JSSelect(qualifier, item) =>
          allowSideEffects && test(qualifier) && test(item)
        case JSFunctionApply(fun, args) =>
          allowSideEffects && test(fun) && (args.forall(testJSArg))
        case JSMethodApply(receiver, method, args) =>
          allowSideEffects && test(receiver) && test(method) && (args.forall(testJSArg))
        case JSSuperSelect(superClass, qualifier, item) =>
          allowSideEffects && test(superClass) && test(qualifier) && test(item)
        case JSImportCall(arg) =>
          allowSideEffects && test(arg)
        case JSImportMeta() =>
          allowSideEffects
        case LoadJSModule(_) =>
          allowSideEffects
        case JSBinaryOp(_, lhs, rhs) =>
          allowSideEffects && test(lhs) && test(rhs)
        case JSUnaryOp(_, lhs) =>
          allowSideEffects && test(lhs)
        case JSGlobalRef(_) =>
          allowSideEffects
        case JSTypeOfGlobalRef(_) =>
          allowSideEffects
        case CreateJSClass(_, captureValues) =>
          allowSideEffects && captureValues.forall(test)

        /* LoadJSConstructor is pure only for non-native JS classes,
         * which do not have a native load spec. Note that this test makes
         * sense per se, as the actual desugaring of `LoadJSConstructor` is
         * based on the jsNativeLoadSpec of the class.
         */
        case LoadJSConstructor(className) =>
          allowUnpure || globalKnowledge.getJSNativeLoadSpec(className).isEmpty

        // Non-expressions
        case _ => false
      }
      test(tree)
    }

    /** Test whether the given tree is a standard JS expression.
     */
    def isExpression(tree: Tree)(implicit env: Env): Boolean =
      isExpressionInternal(tree, allowUnpure = true, allowSideEffects = true)

    /** Test whether the given tree is a side-effect-free standard JS expression.
     */
    def isSideEffectFreeExpression(tree: Tree)(implicit env: Env): Boolean =
      isExpressionInternal(tree, allowUnpure = true, allowSideEffects = false)

    /** Test whether the given tree is a pure standard JS expression.
     */
    def isPureExpression(tree: Tree)(implicit env: Env): Boolean =
      isExpressionInternal(tree, allowUnpure = false, allowSideEffects = false)

    def doVarDef(ident: js.Ident, tpe: Type, mutable: Boolean, rhs: Tree)(
        implicit env: Env): js.Tree = {
      implicit val pos = rhs.pos
      tpe match {
        case RecordType(fields) =>
          val elems = extractRecordElems(rhs)
          js.Block(for {
            (RecordType.Field(fName, fOrigName, fTpe, fMutable),
                fRhs) <- fields zip elems
          } yield {
            doVarDef(makeRecordFieldIdent(ident, fName, fOrigName), fTpe,
                mutable || fMutable, fRhs)
          })

        case _ =>
          genLet(ident, mutable, transformExpr(rhs, tpe))
      }
    }

    def doEmptyVarDef(ident: js.Ident, tpe: Type)(
        implicit pos: Position, env: Env): js.Tree = {
      tpe match {
        case RecordType(fields) =>
          js.Block(for {
            RecordType.Field(fName, fOrigName, fTpe, fMutable) <- fields
          } yield {
            doEmptyVarDef(makeRecordFieldIdent(ident, fName, fOrigName), fTpe)
          })

        case _ =>
          genEmptyMutableLet(ident)
      }
    }

    def doAssign(lhs: Tree, rhs: Tree)(implicit env: Env): js.Tree = {
      implicit val pos = rhs.pos
      lhs.tpe match {
        case RecordType(fields) =>
          val ident = (lhs: @unchecked) match {
            case VarRef(ident)                 => transformLocalVarIdent(ident)
            case Transient(JSVarRef(ident, _)) => ident
          }
          val elems = extractRecordElems(rhs)
          js.Block(for {
            (RecordType.Field(fName, fOrigName, fTpe, fMutable),
                fRhs) <- fields zip elems
          } yield {
            doAssign(
                Transient(JSVarRef(makeRecordFieldIdent(ident, fName, fOrigName),
                    mutable = true)(fTpe)),
                fRhs)
          })

        case _ =>
          val base = js.Assign(transformExpr(lhs, preserveChar = true),
              transformExpr(rhs, lhs.tpe))
          lhs match {
            case SelectStatic(FieldIdent(field))
                if moduleKind == ModuleKind.NoModule =>
              val mirrors = globalKnowledge.getStaticFieldMirrors(field)
              mirrors.foldLeft(base) { (prev, mirror) =>
                js.Assign(genGlobalVarRef(mirror), prev)
              }
            case _ =>
              base
          }
      }
    }

    private def extractRecordElems(recordTree: Tree)(
        implicit pos: Position, env: Env): List[Tree] = {

      val recordType = recordTree.tpe.asInstanceOf[RecordType]

      (recordTree: @unchecked) match {
        case RecordValue(_, elems) =>
          elems

        case VarRef(ident) =>
          val jsIdent = transformLocalVarIdent(ident)
          val mutable = env.isLocalMutable(ident)
          for (RecordType.Field(fName, fOrigName, fTpe, _) <- recordType.fields)
            yield Transient(JSVarRef(makeRecordFieldIdent(jsIdent, fName, fOrigName), mutable)(fTpe))

        case Transient(JSVarRef(ident, mutable)) =>
          for (RecordType.Field(fName, fOrigName, fTpe, _) <- recordType.fields)
            yield Transient(JSVarRef(makeRecordFieldIdent(ident, fName, fOrigName), mutable)(fTpe))
      }
    }

    /** Push an lhs into a (potentially complex) rhs */
    def pushLhsInto(lhs: Lhs, rhs: Tree, tailPosLabels: Set[LabelName])(
        implicit env: Env): js.Tree = {
      implicit val pos = rhs.pos

      /** Push the current lhs further into a deeper rhs. */
      @inline def redo(newRhs: Tree)(implicit env: Env) =
        pushLhsInto(lhs, newRhs, tailPosLabels)

      /** Extract a definition of the lhs if it is a VarDef, to avoid changing
       *  its scope.
       *  This only matters when we emit lets and consts.
       */
      def extractLet(inner: Lhs => js.Tree)(
          implicit env: Env): js.Tree = {
        if (useLets) {
          lhs match {
            case Lhs.VarDef(name, tpe, mutable) =>
              js.Block(
                  doEmptyVarDef(name, tpe),
                  inner(Lhs.Assign(Transient(JSVarRef(name, mutable)(tpe)))))
            case _ =>
              inner(lhs)
          }
        } else {
          inner(lhs)
        }
      }

      def doReturnToLabel(l: LabelIdent): js.Tree = {
        val newLhs = env.lhsForLabeledExpr(l)
        val body = pushLhsInto(newLhs, rhs, Set.empty)
        if (newLhs.hasNothingType) {
          /* A touch of peephole dead code elimination.
           * This is actually necessary to avoid dangling breaks to eliminated
           * labels, as in issue #2307.
           */
          body
        } else if (tailPosLabels.contains(l.name)) {
          body
        } else if (env.isDefaultBreakTarget(l.name)) {
          js.Block(body, js.Break(None))
        } else if (env.isDefaultContinueTarget(l.name)) {
          js.Block(body, js.Continue(None))
        } else {
          usedLabels += l.name
          val transformedLabel = Some(transformLabelIdent(l))
          val jump =
            if (env.isLabelTurnedIntoContinue(l.name)) js.Continue(transformedLabel)
            else js.Break(transformedLabel)
          js.Block(body, jump)
        }
      }

      if (rhs.tpe == NothingType && lhs != Lhs.Discard) {
        /* A touch of peephole dead code elimination.
         * Actually necessary to handle pushing an lhs into an infinite loop,
         * for example.
         */
        val transformedRhs = pushLhsInto(Lhs.Discard, rhs, tailPosLabels)
        lhs match {
          case Lhs.VarDef(name, tpe, _) =>
            /* We still need to declare the var, in case it is used somewhere
             * else in the function, where we can't dce it.
             */
            js.Block(doEmptyVarDef(name, tpe), transformedRhs)

          case _ =>
            transformedRhs
        }
      } else (rhs match {
        // Handle the Block before testing whether it is an expression

        case Block(stats0 :+ expr0) =>
          val (stats1, env0) = transformBlockStats(stats0)
          val expr1 = redo(expr0)(env0)
          js.Block(stats1 :+ expr1)

        // Base case, rhs is already a regular JS expression

        case _ if isExpression(rhs) =>
          lhs match {
            case Lhs.Discard =>
              if (isSideEffectFreeExpression(rhs)) js.Skip()
              else transformExpr(rhs, preserveChar = true)
            case Lhs.VarDef(name, tpe, mutable) =>
              doVarDef(name, tpe, mutable, rhs)
            case Lhs.Assign(lhs) =>
              doAssign(lhs, rhs)
            case Lhs.ReturnFromFunction =>
              js.Return(transformExpr(rhs, env.expectedReturnType))
            case Lhs.Return(l) =>
              doReturnToLabel(l)
            case Lhs.Throw =>
              js.Throw(transformExprNoChar(rhs))
          }

        // Almost base case with RecordValue

        case RecordValue(recTpe, elems) =>
          lhs match {
            case Lhs.Discard =>
              val (newStat, _) = transformBlockStats(elems)
              js.Block(newStat)

            case Lhs.VarDef(name, tpe, mutable) =>
              unnest(elems) { (newElems, env) =>
                doVarDef(name, tpe, mutable, RecordValue(recTpe, newElems))(env)
              }

            case Lhs.Assign(lhs) =>
              unnest(elems) { (newElems, env0) =>
                implicit val env = env0
                val temp = newSyntheticVar()
                val varDef = doVarDef(temp, recTpe, mutable = false,
                    RecordValue(recTpe, newElems))
                val assign = doAssign(lhs,
                    Transient(JSVarRef(temp, mutable = false)(recTpe)))
                js.Block(varDef, assign)
              }

            case Lhs.Return(l) =>
              doReturnToLabel(l)

            case Lhs.ReturnFromFunction | Lhs.Throw =>
              throw new AssertionError("Cannot return or throw a record value.")
          }

        // Control flow constructs

        case Labeled(label, tpe, body) =>
          extractLet { newLhs =>
            val bodyEnv = env.withLabeledExprLHS(label, newLhs)
            val newBody =
              pushLhsInto(newLhs, body, tailPosLabels + label.name)(bodyEnv)
            if (usedLabels.contains(label.name))
              js.Labeled(transformLabelIdent(label), newBody)
            else
              newBody
          }

        case Return(expr, label) =>
          pushLhsInto(Lhs.Return(label), expr, tailPosLabels)

        case If(cond, thenp, elsep) =>
          unnest(cond) { (newCond, env0) =>
            implicit val env = env0
            extractLet { newLhs =>
              js.If(transformExprNoChar(newCond),
                  pushLhsInto(newLhs, thenp, tailPosLabels),
                  pushLhsInto(newLhs, elsep, tailPosLabels))
            }
          }

        case TryCatch(block, errVar, errVarOriginalName, handler) =>
          extractLet { newLhs =>
            val newBlock = pushLhsInto(newLhs, block, tailPosLabels)(env)
            val newHandler = pushLhsInto(newLhs, handler, tailPosLabels)(
                env.withDef(errVar, mutable = false))
            js.TryCatch(newBlock,
                transformLocalVarIdent(errVar, errVarOriginalName), newHandler)
          }

        case TryFinally(block, finalizer) =>
          extractLet { newLhs =>
            val newBlock = pushLhsInto(newLhs, block, tailPosLabels)
            val newFinalizer = transformStat(finalizer, Set.empty)
            js.TryFinally(newBlock, newFinalizer)
          }

        case Throw(expr) =>
          pushLhsInto(Lhs.Throw, expr, tailPosLabels)

        /** Matches are desugared into switches
         *
         *  A match is different from a switch in two respects, both linked
         *  to match being designed to be used in expression position in
         *  Extended-JS.
         *
         *  * There is no fall-through from one case to the next one, hence,
         *    no break statement.
         *  * Match supports _alternatives_ explicitly (with a switch, one
         *    would use the fall-through behavior to implement alternatives).
         */
        case Match(selector, cases, default) =>
          unnest(selector) { (newSelector, env0) =>
            implicit val env = env0.withDefaultBreakTargets(tailPosLabels)
            extractLet { newLhs =>
              val newCases = {
                for {
                  (values, body) <- cases
                  newValues = values.map(transformExprNoChar(_))
                  // add the break statement
                  newBody = js.Block(
                      pushLhsInto(newLhs, body, tailPosLabels),
                      js.Break())
                  // desugar alternatives into several cases falling through
                  caze <- (newValues.init map (v => (v, js.Skip()))) :+ (newValues.last, newBody)
                } yield {
                  caze
                }
              }
              val newDefault = pushLhsInto(newLhs, default, tailPosLabels)
              js.Switch(transformExpr(newSelector, preserveChar = true),
                  newCases, newDefault)
            }
          }

        // Scala expressions (if we reach here their arguments are not expressions)

        case New(className, ctor, args) =>
          unnest(args) { (newArgs, env) =>
            redo(New(className, ctor, newArgs))(env)
          }

        case Select(qualifier, item) =>
          unnest(qualifier) { (newQualifier, env) =>
            redo(Select(newQualifier, item)(rhs.tpe))(env)
          }

        case Apply(flags, receiver, method, args) =>
          unnest(checkNotNull(receiver), args) { (newReceiver, newArgs, env) =>
            redo(Apply(flags, newReceiver, method, newArgs)(rhs.tpe))(env)
          }

        case ApplyStatically(flags, receiver, className, method, args) =>
          unnest(checkNotNull(receiver), args) { (newReceiver, newArgs, env) =>
            redo(ApplyStatically(flags, newReceiver, className, method,
                newArgs)(rhs.tpe))(env)
          }

        case ApplyStatic(flags, className, method, args) =>
          unnest(args) { (newArgs, env) =>
            redo(ApplyStatic(flags, className, method, newArgs)(rhs.tpe))(env)
          }

        case ApplyDynamicImport(flags, className, method, args) =>
          unnest(args) { (newArgs, env) =>
            redo(ApplyDynamicImport(flags, className, method, newArgs))(env)
          }

        case UnaryOp(op, lhs) =>
          unnest(lhs) { (newLhs, env) =>
            redo(UnaryOp(op, newLhs))(env)
          }

        case BinaryOp(op, lhs, rhs) =>
          unnest(lhs, rhs) { (newLhs, newRhs, env) =>
            redo(BinaryOp(op, newLhs, newRhs))(env)
          }

        case NewArray(tpe, length) =>
          unnest(length) { (newLength, env) =>
            redo(NewArray(tpe, newLength))(env)
          }

        case ArrayValue(tpe, elems) =>
          unnest(elems) { (newElems, env) =>
            redo(ArrayValue(tpe, newElems))(env)
          }

        case ArrayLength(array) =>
          unnest(array) { (newArray, env) =>
            redo(ArrayLength(newArray))(env)
          }

        case ArraySelect(array, index) =>
          unnest(checkNotNull(array), index) { (newArray, newIndex, env) =>
            redo(ArraySelect(newArray, newIndex)(rhs.tpe))(env)
          }

        case RecordSelect(record, field) =>
          unnest(record) { (newRecord, env) =>
            redo(RecordSelect(newRecord, field)(rhs.tpe))(env)
          }

        case IsInstanceOf(expr, testType) =>
          unnest(expr) { (newExpr, env) =>
            redo(IsInstanceOf(newExpr, testType))(env)
          }

        case AsInstanceOf(expr, tpe) =>
          unnest(expr) { (newExpr, env) =>
            redo(AsInstanceOf(newExpr, tpe))(env)
          }

        case GetClass(expr) =>
          unnest(expr) { (newExpr, env) =>
            redo(GetClass(newExpr))(env)
          }

        case Clone(expr) =>
          unnest(expr) { (newExpr, env) =>
            redo(Clone(newExpr))(env)
          }

        case IdentityHashCode(expr) =>
          unnest(expr) { (newExpr, env) =>
            redo(IdentityHashCode(newExpr))(env)
          }

        case WrapAsThrowable(expr) =>
          unnest(expr) { (newExpr, newEnv) =>
            implicit val env = newEnv
            withTempJSVar(newExpr) { varRef =>
              redo(WrapAsThrowable(varRef))
            }
          }

        case UnwrapFromThrowable(expr) =>
          unnest(expr) { (newExpr, newEnv) =>
            implicit val env = newEnv
            withTempJSVar(newExpr) { varRef =>
              redo(UnwrapFromThrowable(varRef))
            }
          }

        case Transient(Cast(expr, tpe)) =>
          unnest(expr) { (newExpr, env) =>
            redo(Transient(Cast(newExpr, tpe)))(env)
          }

        case Transient(ZeroOf(runtimeClass)) =>
          unnest(runtimeClass) { (newRuntimeClass, env) =>
            redo(Transient(ZeroOf(newRuntimeClass)))(env)
          }

        case Transient(NativeArrayWrapper(elemClass, nativeArray)) =>
          unnest(elemClass, nativeArray) { (newElemClass, newNativeArray, env) =>
            redo(Transient(NativeArrayWrapper(newElemClass, newNativeArray)(rhs.tpe)))(env)
          }

        case Transient(ObjectClassName(obj)) =>
          unnest(obj) { (newObj, env) =>
            redo(Transient(ObjectClassName(newObj)))(env)
          }

        case Transient(ArrayToTypedArray(expr, primRef)) =>
          unnest(expr) { (newExpr, env) =>
            redo(Transient(ArrayToTypedArray(newExpr, primRef)))(env)
          }

        case Transient(TypedArrayToArray(expr, primRef)) =>
          unnest(expr) { (newExpr, env) =>
            redo(Transient(TypedArrayToArray(newExpr, primRef)))(env)
          }

        // JavaScript expressions (if we reach here their arguments are not expressions)

        case JSNew(ctor, args) =>
          if (needsToTranslateAnySpread(args)) {
            redo {
              Transient(JSNewVararg(ctor, spreadToArgArray(args)))
            }
          } else {
            unnestOrSpread(ctor :: Nil, args) { (newCtor0, newArgs, env) =>
              val newCtor :: Nil = newCtor0
              redo(JSNew(newCtor, newArgs))(env)
            }
          }

        case Transient(JSNewVararg(ctor, argArray)) =>
          unnest(ctor, argArray) { (newCtor, newArgArray, env) =>
            redo(Transient(JSNewVararg(newCtor, newArgArray)))(env)
          }

        case JSFunctionApply(fun, args) =>
          if (needsToTranslateAnySpread(args)) {
            redo {
              JSMethodApply(fun, StringLiteral("apply"),
                  List(Undefined(), spreadToArgArray(args)))
            }
          } else {
            unnestOrSpread(fun :: Nil, args) { (newFun0, newArgs, env) =>
              val newFun :: Nil = newFun0
              redo(JSFunctionApply(newFun, newArgs))(env)
            }
          }

        case JSMethodApply(receiver, method, args) =>
          if (needsToTranslateAnySpread(args)) {
            withTempVar(receiver) { newReceiver =>
              redo {
                JSMethodApply(
                    JSSelect(newReceiver, method),
                    StringLiteral("apply"),
                    List(newReceiver, spreadToArgArray(args)))
              }
            }
          } else {
            unnestOrSpread(receiver :: method :: Nil, args) {
              (newReceiverAndMethod, newArgs, env) =>
                val newReceiver :: newMethod :: Nil = newReceiverAndMethod
                redo(JSMethodApply(newReceiver, newMethod, newArgs))(env)
            }
          }

        case JSSuperSelect(superClass, qualifier, item) =>
          unnest(superClass, qualifier, item) {
            (newSuperClass, newQualifier, newItem, env) =>
              redo(JSSuperSelect(newSuperClass, newQualifier, newItem))(env)
          }

        case JSSuperMethodCall(superClass, receiver, method, args) =>
          redo {
            JSMethodApply(
                JSSelect(
                    JSSelect(superClass, StringLiteral("prototype")),
                    method),
                StringLiteral("call"),
                receiver :: args)
          }

        case JSImportCall(arg) =>
          unnest(arg) { (newArg, env) =>
            redo(JSImportCall(newArg))(env)
          }

        case JSPrivateSelect(qualifier, field) =>
          unnest(qualifier) { (newQualifier, env) =>
            redo(JSPrivateSelect(newQualifier, field))(env)
          }

        case JSSelect(qualifier, item) =>
          unnest(qualifier, item) { (newQualifier, newItem, env) =>
            redo(JSSelect(newQualifier, newItem))(env)
          }

        case JSUnaryOp(op, lhs) =>
          unnest(lhs) { (newLhs, env) =>
            redo(JSUnaryOp(op, newLhs))(env)
          }

        case JSBinaryOp(JSBinaryOp.&&, lhs, rhs) =>
          if (lhs.tpe == BooleanType) {
            redo(If(lhs, rhs, BooleanLiteral(false))(AnyType))
          } else {
            unnest(lhs) { (newLhs, env) =>
              redo(If(newLhs, rhs, newLhs)(AnyType))(env)
            }
          }

        case JSBinaryOp(JSBinaryOp.||, lhs, rhs) =>
          if (lhs.tpe == BooleanType) {
            redo(If(lhs, BooleanLiteral(true), rhs)(AnyType))
          } else {
            unnest(lhs) { (newLhs, env) =>
              redo(If(newLhs, newLhs, rhs)(AnyType))(env)
            }
          }

        case JSBinaryOp(op, lhs, rhs) =>
          unnest(lhs, rhs) { (newLhs, newRhs, env) =>
            redo(JSBinaryOp(op, newLhs, newRhs))(env)
          }

        case JSArrayConstr(items) =>
          if (needsToTranslateAnySpread(items)) {
            redo {
              spreadToArgArray(items)
            }
          } else {
            unnestOrSpread(items) { (newItems, env) =>
              redo(JSArrayConstr(newItems))(env)
            }
          }

        case rhs @ JSObjectConstr(fields) =>
          if (doesObjectConstrRequireDesugaring(rhs)) {
            val objVarIdent = newSyntheticVar()

            def objVarRef(implicit pos: Position): Tree =
              Transient(JSVarRef(objVarIdent, mutable = false)(AnyType))

            val objVarDef =
              genLet(objVarIdent, mutable = false, js.ObjectConstr(Nil))

            val assignFields = for {
              (key, value) <- fields
            } yield {
              implicit val pos = value.pos
              Assign(JSSelect(objVarRef, key), value)
            }

            js.Block(
                objVarDef,
                redo {
                  Block(assignFields ::: objVarRef :: Nil)
                }
            )
          } else {
            unnestJSObjectConstrFields(fields) { (newFields, env) =>
              redo(JSObjectConstr(newFields))(env)
            }
          }

        // Closures

        case Closure(arrow, captureParams, params, restParam, body, captureValues) =>
          unnest(captureValues) { (newCaptureValues, env) =>
            redo(Closure(arrow, captureParams, params, restParam, body, newCaptureValues))(
                env)
          }

        case CreateJSClass(className, captureValues) =>
          unnest(captureValues) { (newCaptureValues, env) =>
            redo(CreateJSClass(className, newCaptureValues))(env)
          }

        case _ =>
          if (lhs == Lhs.Discard) {
            /* Go "back" to transformStat() after having dived into
             * expression statements. Remember that Lhs.Discard is a trick that
             * we use to "add" all the code of pushLhsInto() to transformStat().
             */
            rhs match {
              case _:Skip | _:VarDef | _:Assign | _:While |
                  _:Debugger | _:JSSuperConstructorCall | _:JSDelete |
                  _:StoreModule | Transient(_:SystemArrayCopy) =>
                transformStat(rhs, tailPosLabels)
              case _ =>
                throw new IllegalArgumentException(
                    "Illegal tree in JSDesugar.pushLhsInto():\n" +
                    "lhs = " + lhs + "\n" +
                    "rhs = " + rhs + " of class " + rhs.getClass)
            }
          } else {
            throw new IllegalArgumentException(
                "Illegal tree in JSDesugar.pushLhsInto():\n" +
                "lhs = " + lhs + "\n" +
                "rhs = " + rhs + " of class " + rhs.getClass)
          }
      })
    }

    private def withTempJSVar(value: Tree)(makeBody: Transient => js.Tree)(
        implicit env: Env, pos: Position): js.Tree = {
      withTempJSVar(transformExpr(value, value.tpe), value.tpe)(makeBody)
    }

    private def withTempJSVar(value: js.Tree, tpe: Type)(
        makeBody: Transient => js.Tree)(
        implicit pos: Position): js.Tree = {
      val varIdent = newSyntheticVar()
      val varDef = genLet(varIdent, mutable = false, value)
      val body = makeBody(Transient(JSVarRef(varIdent, mutable = false)(tpe)))
      js.Block(varDef, body)
    }

    private def needsToTranslateAnySpread(args: List[TreeOrJSSpread]): Boolean =
      !es2015 && args.exists(_.isInstanceOf[JSSpread])

    private def spreadToArgArray(args: List[TreeOrJSSpread])(
        implicit env: Env, pos: Position): Tree = {
      var reversedParts: List[Tree] = Nil
      var reversedPartUnderConstruction: List[Tree] = Nil

      def closeReversedPartUnderConstruction() = {
        if (!reversedPartUnderConstruction.isEmpty) {
          val part = reversedPartUnderConstruction.reverse
          reversedParts ::= JSArrayConstr(part)(part.head.pos)
          reversedPartUnderConstruction = Nil
        }
      }

      for (arg <- args) {
        arg match {
          case JSSpread(spreadArray) =>
            closeReversedPartUnderConstruction()
            reversedParts ::= spreadArray
          case arg: Tree =>
            reversedPartUnderConstruction ::= arg
        }
      }
      closeReversedPartUnderConstruction()

      reversedParts match {
        case Nil        => JSArrayConstr(Nil)
        case List(part) => part
        case _          =>
          val partHead :: partTail = reversedParts.reverse
          JSMethodApply(partHead, StringLiteral("concat"), partTail)
      }
    }

    /** Tests whether a [[JSObjectConstr]] must be desugared. */
    private def doesObjectConstrRequireDesugaring(
        tree: JSObjectConstr): Boolean = {
      def computedNamesAllowed: Boolean =
        es2015

      def hasComputedName: Boolean =
        tree.fields.exists(!_._1.isInstanceOf[StringLiteral])

      def hasDuplicateNonComputedProp: Boolean = {
        val names = tree.fields.collect {
          case (StringLiteral(name), _) => name
        }
        names.toSet.size != names.size
      }

      (!computedNamesAllowed && hasComputedName) || hasDuplicateNonComputedProp
    }

    /** Evaluates `expr` and stores the result in a temp, then evaluates the
     *  result of `makeTree(temp)`.
     */
    private def withTempVar(expr: Tree)(makeTree: Tree => js.Tree)(
        implicit env: Env): js.Tree = {
      expr match {
        case VarRef(ident) if !env.isLocalMutable(ident) =>
          makeTree(expr)
        case Transient(JSVarRef(_, false)) =>
          makeTree(expr)
        case _ =>
          implicit val pos = expr.pos
          val temp = newSyntheticVar()
          val computeTemp = pushLhsInto(
              Lhs.VarDef(temp, expr.tpe, mutable = false), expr, Set.empty)
          js.Block(computeTemp,
              makeTree(Transient(JSVarRef(temp, mutable = false)(expr.tpe))))
      }
    }

    def transformJSArg(tree: TreeOrJSSpread)(implicit env: Env): js.Tree = {
      tree match {
        case JSSpread(items) =>
          assert(es2015)
          js.Spread(transformExprNoChar(items))(tree.pos)
        case tree: Tree =>
          transformExprNoChar(tree)
      }
    }

    def transformExprNoChar(tree: Tree)(implicit env: Env): js.Tree =
      transformExpr(tree, preserveChar = false)

    def transformExpr(tree: Tree, expectedType: Type)(
        implicit env: Env): js.Tree = {
      transformExpr(tree, preserveChar = expectedType == CharType)
    }

    def transformTypedArgs(methodName: MethodName, args: List[Tree])(
        implicit env: Env): List[js.Tree] = {
      if (args.forall(_.tpe != CharType)) {
        // Fast path
        args.map(transformExpr(_, preserveChar = true))
      } else {
        args.zip(methodName.paramTypeRefs).map {
          case (arg, CharRef) => transformExpr(arg, preserveChar = true)
          case (arg, _)       => transformExpr(arg, preserveChar = false)
        }
      }
    }

    /** Desugar an expression of the IR into JavaScript. */
    def transformExpr(tree: Tree, preserveChar: Boolean)(
        implicit env: Env): js.Tree = {

      import TreeDSL._

      implicit val pos = tree.pos

      def or0(tree: js.Tree): js.Tree =
        js.BinaryOp(JSBinaryOp.|, tree, js.IntLiteral(0))

      def bigIntShiftRhs(tree: js.Tree): js.Tree = {
        tree match {
          case js.IntLiteral(v) =>
            js.BigIntLiteral(v & 63)
          case _ =>
            js.Apply(genGlobalVarRef("BigInt"),
                List(js.BinaryOp(JSBinaryOp.&, tree, js.IntLiteral(63))))
        }
      }

      val baseResult: js.Tree = tree match {
        // Control flow constructs

        case Block(stats :+ expr) =>
          val (newStats, newEnv) = transformBlockStats(stats)
          js.Block(newStats :+ transformExpr(expr, tree.tpe)(newEnv))

        // Note that these work even if thenp/elsep is not a BooleanType
        case If(cond, BooleanLiteral(true), elsep) =>
          js.BinaryOp(JSBinaryOp.||, transformExprNoChar(cond),
              transformExpr(elsep, tree.tpe))
        case If(cond, thenp, BooleanLiteral(false)) =>
          js.BinaryOp(JSBinaryOp.&&, transformExprNoChar(cond),
              transformExpr(thenp, tree.tpe))

        case If(cond, thenp, elsep) =>
          js.If(transformExprNoChar(cond), transformExpr(thenp, tree.tpe),
              transformExpr(elsep, tree.tpe))

        // Scala expressions

        case New(className, ctor, args) =>
          val newArgs = transformTypedArgs(ctor.name, args)
          genScalaClassNew(className, ctor.name, newArgs: _*)

        case LoadModule(className) =>
          genLoadModule(className)

        case Select(qualifier, field) =>
          genSelect(transformExprNoChar(checkNotNull(qualifier)), field)

        case SelectStatic(item) =>
          globalVar(VarField.t, item.name)

        case SelectJSNativeMember(className, member) =>
          val jsNativeLoadSpec =
            globalKnowledge.getJSNativeLoadSpec(className, member.name)
          extractWithGlobals(genLoadJSFromSpec(jsNativeLoadSpec))

        case Apply(_, receiver, method, args) =>
          val methodName = method.name

          def newReceiver(asChar: Boolean): js.Tree = {
            if (asChar) {
              /* When statically calling a (hijacked) method of j.l.Character,
               * the receiver must be passed as a primitive CharType. If it is
               * not already a CharType, we must introduce a cast to unbox the
               * value.
               */
              if (receiver.tpe == CharType)
                transformExpr(receiver, preserveChar = true)
              else
                transformExpr(AsInstanceOf(checkNotNull(receiver), CharType), preserveChar = true)
            } else {
              /* For other primitive types, unboxes/casts are not necessary,
               * because they would only convert `null` to the zero value of
               * the type. However, `null` is ruled out by `checkNotNull` (or
               * because it is UB).
               */
              transformExprNoChar(checkNotNull(receiver))
            }
          }

          val newArgs = transformTypedArgs(method.name, args)

          def genNormalApply(): js.Tree =
            js.Apply(newReceiver(false) DOT genMethodIdent(method), newArgs)

          def genDispatchApply(): js.Tree =
            js.Apply(globalVar(VarField.dp, methodName), newReceiver(false) :: newArgs)

          def genHijackedMethodApply(className: ClassName): js.Tree =
            genApplyStaticLike(VarField.f, className, method, newReceiver(className == BoxedCharacterClass) :: newArgs)

          if (isMaybeHijackedClass(receiver.tpe) &&
              !methodName.isReflectiveProxy) {
            receiver.tpe match {
              case AnyType | AnyNotNullType =>
                genDispatchApply()

              case LongType | ClassType(BoxedLongClass, _) if !useBigIntForLongs =>
                // All methods of java.lang.Long are also in RuntimeLong
                genNormalApply()

              case _ if hijackedMethodsInheritedFromObject.contains(methodName) =>
                /* Methods inherited from j.l.Object do not have a dedicated
                 * hijacked method that we can call, even when we know the
                 * precise type of the receiver. Therefore, we always have to
                 * go through the dispatcher in those cases.
                 *
                 * Note that when the optimizer is enabled, if the receiver
                 * had a precise type, the method would have been inlined
                 * anyway (because all the affected methods are @inline).
                 * Therefore this roundabout of dealing with this does not
                 * prevent any optimization.
                 */
                genDispatchApply()

              case ClassType(className, _) if !HijackedClasses.contains(className) =>
                /* This is a strict ancestor of a hijacked class. We need to
                 * use the dispatcher available in the helper method.
                 */
                genDispatchApply()

              case tpe =>
                /* This is a concrete hijacked class or its corresponding
                 * primitive type. Directly call the hijacked method. Note that
                 * there might not even be a dispatcher for this method, so
                 * this is important.
                 */
                genHijackedMethodApply(typeToBoxedHijackedClass(tpe))
            }
          } else {
            genNormalApply()
          }

        case ApplyStatically(flags, receiver, className, method, args) =>
          val newReceiver = transformExprNoChar(checkNotNull(receiver))
          val newArgs = transformTypedArgs(method.name, args)
          val transformedArgs = newReceiver :: newArgs

          if (flags.isConstructor) {
            genApplyStaticLike(VarField.ct, className, method, transformedArgs)
          } else if (flags.isPrivate) {
            genApplyStaticLike(VarField.p, className, method, transformedArgs)
          } else if (globalKnowledge.isInterface(className)) {
            genApplyStaticLike(VarField.f, className, method, transformedArgs)
          } else {
            val fun =
              globalVar(VarField.c, className).prototype DOT genMethodIdent(method)
            js.Apply(fun DOT "call", transformedArgs)
          }

        case ApplyStatic(flags, className, method, args) =>
          genApplyStaticLike(
              if (flags.isPrivate) VarField.ps else VarField.s,
              className,
              method,
              transformTypedArgs(method.name, args))

        case tree: ApplyDynamicImport =>
          transformApplyDynamicImport(tree)

        case UnaryOp(op, lhs) =>
          import UnaryOp._
          val newLhs = transformExpr(lhs, preserveChar = (op == CharToInt || op == CheckNotNull))
          (op: @switch) match {
            case Boolean_! => js.UnaryOp(JSUnaryOp.!, newLhs)

            // Widening conversions
            case CharToInt | ByteToInt | ShortToInt | IntToDouble |
                FloatToDouble =>
              newLhs
            case IntToLong =>
              if (useBigIntForLongs)
                js.Apply(genGlobalVarRef("BigInt"), List(newLhs))
              else
                genLongModuleApply(LongImpl.fromInt, newLhs)

            // Narrowing conversions
            case IntToChar =>
              js.BinaryOp(JSBinaryOp.&, js.IntLiteral(0xffff), newLhs)
            case IntToByte =>
              js.BinaryOp(JSBinaryOp.>>,
                  js.BinaryOp(JSBinaryOp.<<, newLhs, js.IntLiteral(24)),
                  js.IntLiteral(24))
            case IntToShort =>
              js.BinaryOp(JSBinaryOp.>>,
                  js.BinaryOp(JSBinaryOp.<<, newLhs, js.IntLiteral(16)),
                  js.IntLiteral(16))
            case LongToInt =>
              if (useBigIntForLongs)
                js.Apply(genGlobalVarRef("Number"), List(wrapBigInt32(newLhs)))
              else
                genApply(newLhs, LongImpl.toInt)
            case DoubleToInt =>
              genCallHelper(VarField.doubleToInt, newLhs)
            case DoubleToFloat =>
              genFround(newLhs)

            // Long <-> Double (neither widening nor narrowing)
            case LongToDouble =>
              if (useBigIntForLongs)
                js.Apply(genGlobalVarRef("Number"), List(newLhs))
              else
                genApply(newLhs, LongImpl.toDouble)
            case DoubleToLong =>
              if (useBigIntForLongs)
                genCallHelper(VarField.doubleToLong, newLhs)
              else
                genLongModuleApply(LongImpl.fromDouble, newLhs)

            // Long -> Float (neither widening nor narrowing)
            case LongToFloat =>
              if (useBigIntForLongs)
                genCallHelper(VarField.longToFloat, newLhs)
              else
                genApply(newLhs, LongImpl.toFloat)

            // String.length
            case String_length =>
              genIdentBracketSelect(newLhs, "length")

            // Null check
            case CheckNotNull =>
              if (semantics.nullPointers == CheckedBehavior.Unchecked)
                newLhs
              else
                genCallHelper(VarField.n, newLhs)

            // Class operations
            case Class_name =>
              genGetDataOf(newLhs) DOT cpn.name
            case Class_isPrimitive =>
              genGetDataOf(newLhs) DOT cpn.isPrimitive
            case Class_isInterface =>
              genGetDataOf(newLhs) DOT cpn.isInterface
            case Class_isArray =>
              genGetDataOf(newLhs) DOT cpn.isArrayClass
            case Class_componentType =>
              js.Apply(genGetDataOf(newLhs) DOT cpn.getComponentType, Nil)
            case Class_superClass =>
              js.Apply(genGetDataOf(newLhs) DOT cpn.getSuperclass, Nil)
          }

        case BinaryOp(op, lhs, rhs) =>
          import BinaryOp._
          val newLhs = transformExpr(lhs, preserveChar = (op == String_+))
          val newRhs = transformExpr(rhs, preserveChar = (op == String_+))

          def extractClassData(origTree: Tree, jsTree: js.Tree): js.Tree = origTree match {
            case ClassOf(typeRef) => genClassDataOf(typeRef)(implicitly, implicitly, origTree.pos)
            case _                => genGetDataOf(jsTree)
          }

          (op: @switch) match {
            case === | !== =>
              /* Semantically, this is an `Object.is` test in JS. However, we
               * optimize it as a primitive JS strict equality (`===`) when
               * possible.
               *
               * The optimizer does not do this optimization because:
               *
               * - it partly relies on the specifics of how hijacked classes are encoded in JS
               *   (see the `ClassType(ObjectClass)` case in `canBePrimitiveNum`),
               * - if handled in the optimizer, `JSBinaryOp`s become frequent
               *   and require additional infrastructure to optimize common patterns, and
               * - it is very specific to *JavaScript*, and is actually detrimental in Wasm.
               */

              def canBePrimitiveNum(tree: Tree): Boolean = tree.tpe match {
                case AnyType | ByteType | ShortType | IntType | FloatType | DoubleType =>
                  true
                case ClassType(ObjectClass, _) =>
                  /* Due to how hijacked classes are encoded in JS, we know
                   * that in `java.lang.Object` itself, `this` can never be a
                   * primitive. It will always be a proper Scala.js object.
                   *
                   * Exempting `this` in `java.lang.Object` is important so
                   * that the body of `Object.equals__O__Z` can be compiled as
                   * `this === that` instead of `Object.is(this, that)`.
                   */
                  !tree.isInstanceOf[This]
                case ClassType(BoxedByteClass | BoxedShortClass |
                    BoxedIntegerClass | BoxedFloatClass | BoxedDoubleClass, _) =>
                  true
                case ClassType(className, _) =>
                  globalKnowledge.isAncestorOfHijackedClass(BoxedDoubleClass)
                case _ =>
                  false
              }

              def isWhole(tree: Tree): Boolean = tree.tpe match {
                case ByteType | ShortType | IntType =>
                  true
                case ClassType(className, _) =>
                  className == BoxedByteClass ||
                  className == BoxedShortClass ||
                  className == BoxedIntegerClass
                case _ =>
                  false
              }

              val canOptimizeAsJSStrictEq = {
                !canBePrimitiveNum(lhs) ||
                !canBePrimitiveNum(rhs) ||
                (isWhole(lhs) && isWhole(rhs))
              }

              if (canOptimizeAsJSStrictEq) {
                js.BinaryOp(if (op == ===) JSBinaryOp.=== else JSBinaryOp.!==,
                    newLhs, newRhs)
              } else {
                val objectIsCall =
                  genCallPolyfillableBuiltin(ObjectIsBuiltin, newLhs, newRhs)
                if (op == ===) objectIsCall
                else js.UnaryOp(JSUnaryOp.!, objectIsCall)
              }

            case Int_== | Double_== | Boolean_== =>
              js.BinaryOp(JSBinaryOp.===, newLhs, newRhs)
            case Int_!= | Double_!= | Boolean_!= =>
              js.BinaryOp(JSBinaryOp.!==, newLhs, newRhs)

            case String_+ =>
              def charToString(t: js.Tree): js.Tree =
                genCallHelper(VarField.charToString, t)

              (lhs.tpe, rhs.tpe) match {
                case (CharType, CharType) => charToString(newLhs) + charToString(newRhs)
                case (CharType, _)        => charToString(newLhs) + newRhs
                case (_, CharType)        => newLhs + charToString(newRhs)
                case (StringType, _)      => newLhs + newRhs
                case (_, StringType)      => newLhs + newRhs
                case _                    => (js.StringLiteral("") + newLhs) + newRhs
              }

            case Int_+ => or0(js.BinaryOp(JSBinaryOp.+, newLhs, newRhs))
            case Int_- =>
              lhs match {
                case IntLiteral(0) => or0(js.UnaryOp(JSUnaryOp.-, newRhs))
                case _             => or0(js.BinaryOp(JSBinaryOp.-, newLhs, newRhs))
              }
            case Int_* =>
              genCallPolyfillableBuiltin(ImulBuiltin, newLhs, newRhs)
            case Int_/ =>
              rhs match {
                case IntLiteral(r) if r != 0 =>
                  or0(js.BinaryOp(JSBinaryOp./, newLhs, newRhs))
                case _ =>
                  genCallHelper(VarField.intDiv, newLhs, newRhs)
              }
            case Int_% =>
              rhs match {
                case IntLiteral(r) if r != 0 =>
                  or0(js.BinaryOp(JSBinaryOp.%, newLhs, newRhs))
                case _ =>
                  genCallHelper(VarField.intMod, newLhs, newRhs)
              }

            case Int_|   => js.BinaryOp(JSBinaryOp.|, newLhs, newRhs)
            case Int_&   => js.BinaryOp(JSBinaryOp.&, newLhs, newRhs)
            case Int_^   =>
              lhs match {
                case IntLiteral(-1) => js.UnaryOp(JSUnaryOp.~, newRhs)
                case _              => js.BinaryOp(JSBinaryOp.^, newLhs, newRhs)
              }
            case Int_<<  => js.BinaryOp(JSBinaryOp.<<, newLhs, newRhs)
            case Int_>>> => or0(js.BinaryOp(JSBinaryOp.>>>, newLhs, newRhs))
            case Int_>>  => js.BinaryOp(JSBinaryOp.>>, newLhs, newRhs)

            case Int_<  => js.BinaryOp(JSBinaryOp.<, newLhs, newRhs)
            case Int_<= => js.BinaryOp(JSBinaryOp.<=, newLhs, newRhs)
            case Int_>  => js.BinaryOp(JSBinaryOp.>, newLhs, newRhs)
            case Int_>= => js.BinaryOp(JSBinaryOp.>=, newLhs, newRhs)

            case Long_+ =>
              if (useBigIntForLongs)
                wrapBigInt64(js.BinaryOp(JSBinaryOp.+, newLhs, newRhs))
              else
                genApply(newLhs, LongImpl.+, newRhs)
            case Long_- =>
              lhs match {
                case LongLiteral(0L) =>
                  if (useBigIntForLongs)
                    wrapBigInt64(js.UnaryOp(JSUnaryOp.-, newRhs))
                  else
                    genApply(newRhs, LongImpl.UNARY_-)
                case _ =>
                  if (useBigIntForLongs)
                    wrapBigInt64(js.BinaryOp(JSBinaryOp.-, newLhs, newRhs))
                  else
                    genApply(newLhs, LongImpl.-, newRhs)
              }
            case Long_* =>
              if (useBigIntForLongs)
                wrapBigInt64(js.BinaryOp(JSBinaryOp.*, newLhs, newRhs))
              else
                genApply(newLhs, LongImpl.*, newRhs)
            case Long_/ =>
              if (useBigIntForLongs) {
                rhs match {
                  case LongLiteral(r) if r != 0L =>
                    wrapBigInt64(js.BinaryOp(JSBinaryOp./, newLhs, newRhs))
                  case _ =>
                    genCallHelper(VarField.longDiv, newLhs, newRhs)
                }
              } else {
                genApply(newLhs, LongImpl./, newRhs)
              }
            case Long_% =>
              if (useBigIntForLongs) {
                rhs match {
                  case LongLiteral(r) if r != 0L =>
                    wrapBigInt64(js.BinaryOp(JSBinaryOp.%, newLhs, newRhs))
                  case _ =>
                    genCallHelper(VarField.longMod, newLhs, newRhs)
                }
              } else {
                genApply(newLhs, LongImpl.%, newRhs)
              }

            case Long_| =>
              if (useBigIntForLongs)
                wrapBigInt64(js.BinaryOp(JSBinaryOp.|, newLhs, newRhs))
              else
                genApply(newLhs, LongImpl.|, newRhs)
            case Long_& =>
              if (useBigIntForLongs)
                wrapBigInt64(js.BinaryOp(JSBinaryOp.&, newLhs, newRhs))
              else
                genApply(newLhs, LongImpl.&, newRhs)
            case Long_^ =>
              lhs match {
                case LongLiteral(-1L) =>
                  if (useBigIntForLongs)
                    wrapBigInt64(js.UnaryOp(JSUnaryOp.~, newRhs))
                  else
                    genApply(newRhs, LongImpl.UNARY_~)
                case _ =>
                  if (useBigIntForLongs)
                    wrapBigInt64(js.BinaryOp(JSBinaryOp.^, newLhs, newRhs))
                  else
                    genApply(newLhs, LongImpl.^, newRhs)
              }
            case Long_<< =>
              if (useBigIntForLongs)
                wrapBigInt64(js.BinaryOp(JSBinaryOp.<<, newLhs, bigIntShiftRhs(newRhs)))
              else
                genApply(newLhs, LongImpl.<<, newRhs)
            case Long_>>> =>
              if (useBigIntForLongs)
                wrapBigInt64(js.BinaryOp(JSBinaryOp.>>, wrapBigIntU64(newLhs), bigIntShiftRhs(newRhs)))
              else
                genApply(newLhs, LongImpl.>>>, newRhs)
            case Long_>> =>
              if (useBigIntForLongs)
                wrapBigInt64(js.BinaryOp(JSBinaryOp.>>, newLhs, bigIntShiftRhs(newRhs)))
              else
                genApply(newLhs, LongImpl.>>, newRhs)

            case Long_== =>
              if (useBigIntForLongs)
                js.BinaryOp(JSBinaryOp.===, newLhs, newRhs)
              else
                genApply(newLhs, LongImpl.===, newRhs)
            case Long_!= =>
              if (useBigIntForLongs)
                js.BinaryOp(JSBinaryOp.!==, newLhs, newRhs)
              else
                genApply(newLhs, LongImpl.!==, newRhs)
            case Long_< =>
              if (useBigIntForLongs)
                js.BinaryOp(JSBinaryOp.<, newLhs, newRhs)
              else
                genApply(newLhs, LongImpl.<, newRhs)
            case Long_<= =>
              if (useBigIntForLongs)
                js.BinaryOp(JSBinaryOp.<=, newLhs, newRhs)
              else
                genApply(newLhs, LongImpl.<=, newRhs)
            case Long_> =>
              if (useBigIntForLongs)
                js.BinaryOp(JSBinaryOp.>, newLhs, newRhs)
              else
                genApply(newLhs, LongImpl.>, newRhs)
            case Long_>= =>
              if (useBigIntForLongs)
                js.BinaryOp(JSBinaryOp.>=, newLhs, newRhs)
              else
                genApply(newLhs, LongImpl.>=, newRhs)

            case Float_+ => genFround(js.BinaryOp(JSBinaryOp.+, newLhs, newRhs))
            case Float_- => genFround(js.BinaryOp(JSBinaryOp.-, newLhs, newRhs))
            case Float_* =>
              genFround(lhs match {
                case FloatLiteral(-1.0f) => js.UnaryOp(JSUnaryOp.-, newRhs)
                case _                   => js.BinaryOp(JSBinaryOp.*, newLhs, newRhs)
              })
            case Float_/ => genFround(js.BinaryOp(JSBinaryOp./, newLhs, newRhs))
            case Float_% => genFround(js.BinaryOp(JSBinaryOp.%, newLhs, newRhs))

            case Double_+ => js.BinaryOp(JSBinaryOp.+, newLhs, newRhs)
            case Double_- => js.BinaryOp(JSBinaryOp.-, newLhs, newRhs)
            case Double_* =>
              lhs match {
                case DoubleLiteral(-1.0) => js.UnaryOp(JSUnaryOp.-, newRhs)
                case _                   => js.BinaryOp(JSBinaryOp.*, newLhs, newRhs)
              }
            case Double_/ => js.BinaryOp(JSBinaryOp./, newLhs, newRhs)
            case Double_% => js.BinaryOp(JSBinaryOp.%, newLhs, newRhs)

            case Double_<  => js.BinaryOp(JSBinaryOp.<, newLhs, newRhs)
            case Double_<= => js.BinaryOp(JSBinaryOp.<=, newLhs, newRhs)
            case Double_>  => js.BinaryOp(JSBinaryOp.>, newLhs, newRhs)
            case Double_>= => js.BinaryOp(JSBinaryOp.>=, newLhs, newRhs)

            case Boolean_| => !(!js.BinaryOp(JSBinaryOp.|, newLhs, newRhs))
            case Boolean_& => !(!js.BinaryOp(JSBinaryOp.&, newLhs, newRhs))

            case String_charAt =>
              semantics.stringIndexOutOfBounds match {
                case CheckedBehavior.Compliant | CheckedBehavior.Fatal =>
                  genCallHelper(VarField.charAt, newLhs, newRhs)
                case CheckedBehavior.Unchecked =>
                  js.Apply(genIdentBracketSelect(newLhs, "charCodeAt"), List(newRhs))
              }

            case Class_isInstance =>
              js.Apply(extractClassData(lhs, newLhs) DOT cpn.isInstance, newRhs :: Nil)
            case Class_isAssignableFrom =>
              js.Apply(extractClassData(lhs, newLhs) DOT cpn.isAssignableFrom,
                  extractClassData(rhs, newRhs) :: Nil)
            case Class_cast =>
              if (semantics.asInstanceOfs == CheckedBehavior.Unchecked)
                js.Block(newLhs, newRhs)
              else
                js.Apply(extractClassData(lhs, newLhs) DOT cpn.cast, newRhs :: Nil)
            case Class_newArray =>
              js.Apply(extractClassData(lhs, newLhs) DOT cpn.newArray, newRhs :: Nil)
          }

        case NewArray(typeRef, length) =>
          js.New(genArrayConstrOf(typeRef), transformExprNoChar(length) :: Nil)

        case ArrayValue(typeRef, elems) =>
          val preserveChar = typeRef match {
            case ArrayTypeRef(CharRef, 1) => true
            case _                        => false
          }
          extractWithGlobals(
              genArrayValue(typeRef, elems.map(transformExpr(_, preserveChar))))

        case ArrayLength(array) =>
          val newArray = transformExprNoChar(checkNotNull(array))
          genIdentBracketSelect(
              genSyntheticPropSelect(newArray, SyntheticProperty.u),
              "length")

        case ArraySelect(array, index) =>
          val newArray = transformExprNoChar(checkNotNull(array))
          val newIndex = transformExprNoChar(index)
          semantics.arrayIndexOutOfBounds match {
            case CheckedBehavior.Compliant | CheckedBehavior.Fatal =>
              genSyntheticPropApply(newArray, SyntheticProperty.get, newIndex)
            case CheckedBehavior.Unchecked =>
              js.BracketSelect(genSyntheticPropSelect(newArray, SyntheticProperty.u), newIndex)
          }

        case tree: RecordSelect =>
          js.VarRef(makeRecordFieldIdentForVarRef(tree))

        case IsInstanceOf(expr, testType) =>
          genIsInstanceOf(transformExprNoChar(expr), testType)

        case AsInstanceOf(expr, tpe) =>
          extractWithGlobals(genAsInstanceOf(transformExprNoChar(expr), tpe))

        case GetClass(expr) =>
          genCallHelper(VarField.objectGetClass, transformExprNoChar(expr))

        case Clone(expr) =>
          val newExpr = transformExprNoChar(checkNotNull(expr))
          expr.tpe match {
            /* If the argument is known to be an array, directly call its
             * `clone__O` method.
             * This happens all the time when calling `clone()` on an array,
             * since the optimizer will inline `java.lang.Object.clone()` in
             * those cases, leaving a `Clone()` node an array.
             */
            case _: ArrayType =>
              genApply(newExpr, cloneMethodName, Nil)

            /* Otherwise, if it might be an array, use the full dispatcher.
             * In theory, only the `CloneableClass` case is required, since
             * `Clone` only accepts values of type `Cloneable`. However, since
             * the inliner does not always refine the type of receivers, we
             * also account for other supertypes of array types. There is a
             * similar issue for CharSequenceClass in `Apply` nodes.
             *
             * TODO Is the above comment still relevant now that the optimizer
             * is type-preserving?
             *
             * In practice, this only happens in the (non-inlined) definition
             * of `java.lang.Object.clone()` itself, since everywhere else it
             * is inlined in contexts where the receiver has a more precise
             * type.
             */
            case ClassType(CloneableClass, _) | ClassType(SerializableClass, _) |
                ClassType(ObjectClass, _) | AnyType | AnyNotNullType =>
              genCallHelper(VarField.objectOrArrayClone, newExpr)

            // Otherwise, it is known not to be an array.
            case _ =>
              genCallHelper(VarField.objectClone, newExpr)
          }

        case IdentityHashCode(expr) =>
          genCallHelper(VarField.systemIdentityHashCode, transformExprNoChar(expr))

        case WrapAsThrowable(expr) =>
          val newExpr = transformExprNoChar(expr).asInstanceOf[js.VarRef]
          js.If(
              genIsInstanceOfClass(newExpr, ThrowableClass),
              newExpr,
              genScalaClassNew(JavaScriptExceptionClass, AnyArgConstructorName, newExpr))

        case UnwrapFromThrowable(expr) =>
          val newExpr = transformExprNoChar(expr).asInstanceOf[js.VarRef]
          js.If(
              genIsInstanceOfClass(newExpr, JavaScriptExceptionClass),
              genSelect(newExpr, FieldIdent(exceptionFieldName)),
              genCheckNotNull(newExpr))

        // Transients

        case Transient(Cast(expr, tpe)) =>
          val newExpr = transformExpr(expr, preserveChar = true)
          if (tpe == CharType && expr.tpe != CharType)
            newExpr DOT cpn.c
          else
            newExpr

        case Transient(ZeroOf(runtimeClass)) =>
          js.DotSelect(
              genGetDataOf(transformExprNoChar(checkNotNull(runtimeClass))),
              js.Ident(cpn.zero))

        case Transient(NativeArrayWrapper(elemClass, nativeArray)) =>
          val newNativeArray = transformExprNoChar(nativeArray)
          elemClass match {
            case ClassOf(elemTypeRef) =>
              val arrayTypeRef = ArrayTypeRef.of(elemTypeRef)
              extractWithGlobals(
                  genNativeArrayWrapper(arrayTypeRef, newNativeArray))
            case _ =>
              val elemClassData =
                genGetDataOf(transformExprNoChar(checkNotNull(elemClass)))
              val arrayClassData = js.Apply(
                  js.DotSelect(elemClassData, js.Ident(cpn.getArrayOf)), Nil)
              js.Apply(arrayClassData DOT cpn.wrapArray, newNativeArray :: Nil)
          }

        case Transient(ObjectClassName(obj)) =>
          genCallHelper(VarField.objectClassName, transformExprNoChar(obj))

        case Transient(ArrayToTypedArray(expr, primRef)) =>
          val value = transformExprNoChar(checkNotNull(expr))
          val valueUnderlying = genSyntheticPropSelect(value, SyntheticProperty.u)

          if (es2015) {
            js.Apply(genIdentBracketSelect(valueUnderlying, "slice"), Nil)
          } else {
            val typedArrayClass = extractWithGlobals(typedArrayRef(primRef).get)
            js.New(typedArrayClass, valueUnderlying :: Nil)
          }

        case Transient(TypedArrayToArray(expr, primRef)) =>
          val value = transformExprNoChar(expr)

          val arrayValue = if (es2015) {
            js.Apply(genIdentBracketSelect(value, "slice"), Nil)
          } else {
            /* Array.prototype.slice.call(value)
             *
             * This works because:
             * - If the `this` value of `slice` is not a proper `Array`, the
             *   result will be created through `ArrayCreate` without explicit
             *   prototype, which creates a new proper `Array`.
             * - To know what elements to copy, `slice` does not check that its
             *   `this` value is a proper `Array`. Instead, it simply assumes
             *   that it is an "Array-like", and reads the `"length"` property
             *   as well as indexed properties. Both of those work on a typed
             *   array.
             *
             * Reference:
             * http://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.slice
             * (also follow the link for `ArraySpeciesCreate`)
             */
            js.Apply(genIdentBracketSelect(
                genIdentBracketSelect(genGlobalVarRef("Array").prototype, "slice"), "call"),
                value :: Nil)
          }
          js.New(genArrayConstrOf(ArrayTypeRef(primRef, 1)), arrayValue :: Nil)

        // JavaScript expressions

        case JSNew(constr, args) =>
          js.New(transformExprNoChar(constr), args.map(transformJSArg))

        case Transient(JSNewVararg(constr, argsArray)) =>
          assert(!es2015, s"generated a JSNewVargs with ES 2015+ at ${tree.pos}")
          genCallHelper(VarField.newJSObjectWithVarargs,
              transformExprNoChar(constr), transformExprNoChar(argsArray))

        case JSPrivateSelect(qualifier, field) =>
          genJSPrivateSelect(transformExprNoChar(qualifier), field)

        case JSSelect(qualifier, item) =>
          genBracketSelect(transformExprNoChar(qualifier),
              transformExprNoChar(item))

        case JSFunctionApply(fun, args) =>
          js.Apply.makeProtected(transformExprNoChar(fun), args.map(transformJSArg))

        case JSMethodApply(receiver, method, args) =>
          js.Apply(genBracketSelect(transformExprNoChar(receiver),
              transformExprNoChar(method)), args.map(transformJSArg))

        case JSSuperSelect(superClass, qualifier, item) =>
          genCallHelper(VarField.superGet, transformExprNoChar(superClass),
              transformExprNoChar(qualifier), transformExprNoChar(item))

        case JSImportCall(arg) =>
          js.ImportCall(transformExprNoChar(arg))

        case JSNewTarget() =>
          js.NewTarget()

        case JSImportMeta() =>
          js.ImportMeta()

        case LoadJSConstructor(className) =>
          extractWithGlobals(genJSClassConstructor(className))

        case LoadJSModule(className) =>
          globalKnowledge.getJSNativeLoadSpec(className) match {
            case None =>
              // this is a non-native JS module class
              genLoadModule(className)

            case Some(spec) =>
              extractWithGlobals(genLoadJSFromSpec(spec))
          }

        case JSUnaryOp(op, lhs) =>
          val transformedLhs = transformExprNoChar(lhs)
          val protectedLhs = if (op == JSUnaryOp.typeof && lhs.isInstanceOf[JSGlobalRef]) {
            /* #3822 We protect the argument so that it throws a ReferenceError
             * if the global variable is not defined at all, as specified.
             */
            js.Block(js.IntLiteral(0), transformedLhs)
          } else {
            transformedLhs
          }
          js.UnaryOp(op, protectedLhs)

        case JSBinaryOp(op, lhs, rhs) =>
          js.BinaryOp(op, transformExprNoChar(lhs), transformExprNoChar(rhs))

        case JSArrayConstr(items) =>
          js.ArrayConstr(items.map(transformJSArg))

        case JSObjectConstr(fields) =>
          js.ObjectConstr(fields.map { field =>
            val key = field._1
            implicit val pos = key.pos
            val newKey = key match {
              case StringLiteral(s) => js.StringLiteral(s)
              case _                => js.ComputedName(transformExprNoChar(key))
            }
            (newKey, transformExprNoChar(field._2))
          })

        case JSGlobalRef(name) =>
          js.VarRef(transformGlobalVarIdent(name))

        case JSTypeOfGlobalRef(globalRef) =>
          js.UnaryOp(JSUnaryOp.typeof, transformExprNoChar(globalRef))

        case JSLinkingInfo() =>
          globalVar(VarField.linkingInfo, CoreVar)

        // Literals

        case Undefined()            => js.Undefined()
        case Null()                 => js.Null()
        case BooleanLiteral(value)  => js.BooleanLiteral(value)
        case CharLiteral(value)     => js.IntLiteral(value.toInt)
        case ByteLiteral(value)     => js.IntLiteral(value.toInt)
        case ShortLiteral(value)    => js.IntLiteral(value.toInt)
        case IntLiteral(value)      => js.IntLiteral(value)
        case FloatLiteral(value)    => js.DoubleLiteral(value.toDouble)
        case DoubleLiteral(value)   => js.DoubleLiteral(value)
        case StringLiteral(value)   => js.StringLiteral(value)

        case LongLiteral(0L) =>
          genLongZero()
        case LongLiteral(value) =>
          if (useBigIntForLongs) {
            js.BigIntLiteral(value)
          } else {
            val (lo, hi) = LongImpl.extractParts(value)
            genScalaClassNew(
                LongImpl.RuntimeLongClass, LongImpl.initFromParts,
                js.IntLiteral(lo), js.IntLiteral(hi))
          }

        case ClassOf(typeRef) =>
          genClassOf(typeRef)

        // Atomic expressions

        case VarRef(name) =>
          env.varKind(name) match {
            case VarKind.Mutable | VarKind.Immutable =>
              js.VarRef(transformLocalVarIdent(name))

            case VarKind.ThisAlias =>
              js.This()

            case VarKind.ExplicitThisAlias =>
              fileLevelVar(VarField.thiz)

            case VarKind.ClassCapture =>
              fileLevelVar(VarField.cc, genName(name.name))
          }

        case Transient(JSVarRef(name, _)) =>
          js.VarRef(name)

        case This() =>
          if (env.hasExplicitThis)
            fileLevelVar(VarField.thiz)
          else
            js.This()

        case tree: Closure =>
          transformClosure(tree)

        case CreateJSClass(className, captureValues) =>
          val transformedArgs = if (captureValues.forall(_.tpe != CharType)) {
            // Fast path
            captureValues.map(transformExpr(_, preserveChar = true))
          } else {
            val expectedTypes =
              globalKnowledge.getJSClassCaptureTypes(className).get
            for ((value, expectedType) <- captureValues.zip(expectedTypes))
              yield transformExpr(value, expectedType)
          }
          js.Apply(globalVar(VarField.a, className), transformedArgs)

        // Invalid trees

        case _ =>
          throw new IllegalArgumentException(
              "Invalid tree in JSDesugar.transformExpr() of class " +
              tree.getClass)
      }

      if (preserveChar || tree.tpe != CharType)
        baseResult
      else
        genCallHelper(VarField.bC, baseResult)
    }

    private def transformApplyDynamicImport(tree: ApplyDynamicImport)(
        implicit env: Env): js.Tree = {
      implicit val pos = tree.pos

      val ApplyDynamicImport(flags, className, method, args) = tree
      // Protect non-elidable args by an IIFE to avoid bad loop captures (see #4385).
      val targs = transformTypedArgs(method.name, args)

      val capturesBuilder = List.newBuilder[(js.ParamDef, js.Tree)]

      val newArgs = for {
        (arg, targ) <- args.zip(targs)
      } yield {
        var newArg: js.Tree = targ

        prepareCapture(arg, forceName = None, arrow = true) { () =>
          val v = newSyntheticVar()
          capturesBuilder += js.ParamDef(v) -> targ
          newArg = js.VarRef(v)
        }

        newArg
      }

      val innerCall = extractWithGlobals {
        withDynamicGlobalVar(VarField.s, (className, method.name)) { v =>
          js.Apply(v, newArgs)
        }
      }

      val captures = capturesBuilder.result()

      if (captures.isEmpty)
        innerCall
      else
        genIIFE(captures, js.Return(innerCall))
    }

    private def transformClosure(tree: Closure)(implicit env: Env): js.Tree = {
      val Closure(arrow, captureParams, params, restParam, body, captureValues) = tree

      implicit val pos = tree.pos

      val capturesBuilder = List.newBuilder[(js.ParamDef, js.Tree)]

      val envVarsForCaptures = (for {
        (param, value) <- captureParams.zip(captureValues)
      } yield {
        assert(!param.mutable, f"Found mutable capture at ${param.pos}")

        val captureName = param.name.name

        val varKind = prepareCapture(value, Some(captureName), arrow) { () =>
          capturesBuilder += transformParamDef(param) -> transformExpr(value, param.ptpe)
        }

        captureName -> varKind
      }).toMap

      val innerFunction = {
        val bodyEnv = Env.empty(AnyType)
          .withParams(params ++ restParam)
          .withVars(envVarsForCaptures)

        desugarToFunctionInternal(arrow, params, restParam, body, isStat = false, bodyEnv)
      }

      val captures = capturesBuilder.result()

      if (captures.isEmpty) {
        innerFunction
      } else {
        genIIFE(captures, js.Return(innerFunction))
      }
    }

    private def prepareCapture(value: Tree, forceName: Option[LocalName], arrow: Boolean)(
        explicitCapture: () => Unit)(implicit env: Env): VarKind = {
      def permitImplicitJSThisCapture =
        esFeatures.useECMAScript2015Semantics && arrow

      value match {
        case VarRef(name) =>
          /* forceName is needed when capturing for Closure trees:
           *
           * If the name we want to capture implicitly isn't the same as the
           * capture param name, we cannot capture implicitly: we'd have to
           * ensure that inside the closure body there is no other var
           * named like the one we want to capture.
           * However, that would need a full rename pass.
           *
           * Note that with the optimizer enabled, this is unlikely to
           * ever happen, because the optimizer tries to give the same
           * names in this case.
           *
           * For other usages (notably ApplyDynamicImport), we generate the
           * body, so it's easy to ensure no collision.
           */
          def permitImplicitNameCapture = forceName.forall(_ == name.name)

          env.varKind(name) match {
            case VarKind.Immutable if !env.inLoopForVarCapture && permitImplicitNameCapture =>
              VarKind.Immutable

            case VarKind.ClassCapture if permitImplicitNameCapture =>
              VarKind.ClassCapture

            /* Generated trees for (Explicit)ThisAlias do not depend on the name
             * of the VarRef. Therefore, we can still implicitly capture them,
             * even if the capture param name and the capture value var name are
             * not the same.
             */

            case VarKind.ThisAlias if permitImplicitJSThisCapture =>
              VarKind.ThisAlias

            case VarKind.ExplicitThisAlias =>
              VarKind.ExplicitThisAlias

            case _ =>
              explicitCapture()
              VarKind.Immutable
          }

        case This() if env.hasExplicitThis =>
          VarKind.ExplicitThisAlias

        case This() if permitImplicitJSThisCapture =>
          VarKind.ThisAlias

        case _ =>
          explicitCapture()
          VarKind.Immutable
      }
    }

    def isMaybeHijackedClass(tpe: Type): Boolean = tpe match {
      case ClassType(className, _) =>
        HijackedClasses.contains(className) ||
        className != ObjectClass && globalKnowledge.isAncestorOfHijackedClass(className)

      case AnyType | AnyNotNullType | UndefType | BooleanType | CharType | ByteType |
          ShortType | IntType | LongType | FloatType | DoubleType | StringType =>
        true
      case _ =>
        false
    }

    def typeToBoxedHijackedClass(tpe: Type): ClassName = (tpe: @unchecked) match {
      case ClassType(className, _) => className
      case UndefType               => BoxedUnitClass
      case BooleanType             => BoxedBooleanClass
      case CharType                => BoxedCharacterClass
      case ByteType                => BoxedByteClass
      case ShortType               => BoxedShortClass
      case IntType                 => BoxedIntegerClass
      case LongType                => BoxedLongClass
      case FloatType               => BoxedFloatClass
      case DoubleType              => BoxedDoubleClass
      case StringType              => BoxedStringClass
    }

    /* Ideally, we should dynamically figure out this set. We should test
     * whether a particular method is defined in the receiver's hijacked
     * class: if not, it must be inherited. However, this would require
     * additional global knowledge for no practical reason.
     */
    val hijackedMethodsInheritedFromObject: Set[MethodName] = Set(
        getClassMethodName,
        cloneMethodName,
        MethodName("finalize", Nil, VoidRef),
        MethodName("notify", Nil, VoidRef),
        MethodName("notifyAll", Nil, VoidRef)
    )

    private def checkNotNull(tree: Tree)(implicit pos: Position): Tree = {
      if (semantics.nullPointers == CheckedBehavior.Unchecked || !tree.tpe.isNullable)
        tree
      else
        UnaryOp(UnaryOp.CheckNotNull, tree)
    }

    private def transformParamDef(paramDef: ParamDef): js.ParamDef =
      js.ParamDef(transformLocalVarIdent(paramDef.name, paramDef.originalName))(paramDef.pos)

    private def transformLabelIdent(ident: LabelIdent): js.Ident =
      js.Ident(genName(ident.name))(ident.pos)

    private def transformLocalVarIdent(ident: LocalIdent): js.Ident =
      js.Ident(transformLocalName(ident.name))(ident.pos)

    private def transformLocalVarIdent(ident: LocalIdent,
        originalName: OriginalName): js.Ident = {
      val jsName = transformLocalName(ident.name)
      js.Ident(jsName, genOriginalName(ident.name, originalName, jsName))(
          ident.pos)
    }

    private def transformGlobalVarIdent(name: String)(
        implicit pos: Position): js.Ident = {
      referenceGlobalName(name)
      js.Ident(name)
    }

    private def genGlobalVarRef(name: String)(
        implicit pos: Position): js.VarRef = {
      js.VarRef(transformGlobalVarIdent(name))
    }

    /* In FunctionEmitter, we must always keep all global var names, not only
     * dangerous ones. This helper makes it less annoying.
     */
    private def genJSClassConstructor(className: ClassName)(
        implicit pos: Position): WithGlobals[js.Tree] = {
      sjsGen.genJSClassConstructor(className)
    }

    private def genApplyStaticLike(field: VarField, className: ClassName,
        method: MethodIdent, args: List[js.Tree])(
        implicit pos: Position): js.Tree = {
      js.Apply(globalVar(field, (className, method.name)), args)
    }

    private def genGetDataOf(jlClassValue: js.Tree)(implicit pos: Position): js.Tree =
      genSyntheticPropSelect(jlClassValue, SyntheticProperty.data)

    private def genCallPolyfillableBuiltin(
        builtin: PolyfillableBuiltin, args: js.Tree*)(
        implicit pos: Position): js.Tree = {
      extractWithGlobals(sjsGen.genCallPolyfillableBuiltin(builtin, args: _*))
    }

    private def genFround(arg: js.Tree)(implicit pos: Position): js.Tree =
      genCallPolyfillableBuiltin(FroundBuiltin, arg)

    private def wrapBigInt32(tree: js.Tree)(implicit pos: Position): js.Tree =
      wrapBigIntN(32, tree)

    private def wrapBigInt64(tree: js.Tree)(implicit pos: Position): js.Tree =
      wrapBigIntN(64, tree)

    private def wrapBigIntN(n: Int, tree: js.Tree)(
        implicit pos: Position): js.Tree = {
      js.Apply(genIdentBracketSelect(genGlobalVarRef("BigInt"), "asIntN"),
          List(js.IntLiteral(n), tree))
    }

    private def wrapBigIntU64(tree: js.Tree)(implicit pos: Position): js.Tree =
      wrapBigIntUN(64, tree)

    private def wrapBigIntUN(n: Int, tree: js.Tree)(
        implicit pos: Position): js.Tree = {
      js.Apply(genIdentBracketSelect(genGlobalVarRef("BigInt"), "asUintN"),
          List(js.IntLiteral(n), tree))
    }
  }
}

private object FunctionEmitter {
  private val UTF8Period: UTF8String = UTF8String(".")

  private val thisOriginalName: OriginalName = OriginalName("this")

  private object PrimArray {
    def unapply(tpe: ArrayType): Option[PrimRef] = tpe.arrayTypeRef match {
      case ArrayTypeRef(primRef: PrimRef, 1) => Some(primRef)
      case _                                 => None
    }
  }

  private object RefArray {
    def unapply(tpe: ArrayType): Boolean = tpe.arrayTypeRef match {
      case ArrayTypeRef(_, n) if n > 1  => true
      case ArrayTypeRef(_: ClassRef, _) => true
      case _                            => false
    }

    def is(tpe: Type): Boolean = tpe match {
      case RefArray() => true
      case _          => false
    }
  }

  private final case class JSVarRef(ident: js.Ident, mutable: Boolean)(val tpe: Type)
      extends Transient.Value {

    def traverse(traverser: Traverser): Unit = ()

    def transform(transformer: Transformer, isStat: Boolean)(
        implicit pos: Position): Tree = {
      Transient(this)
    }

    def printIR(out: org.scalajs.ir.Printers.IRTreePrinter): Unit =
      out.print(ident.name)
  }

  private final case class JSNewVararg(ctor: Tree, argArray: Tree)
      extends Transient.Value {
    val tpe: Type = AnyType

    def traverse(traverser: Traverser): Unit = {
      traverser.traverse(ctor)
      traverser.traverse(argArray)
    }

    def transform(transformer: Transformer, isStat: Boolean)(
        implicit pos: Position): Tree = {
      Transient(JSNewVararg(transformer.transformExpr(ctor),
          transformer.transformExpr(argArray)))
    }

    def printIR(out: IRTreePrinter): Unit = {
      out.print("new (")
      out.print(ctor)
      out.print(")(...")
      out.print(argArray)
      out.print(')')
    }
  }

  /** A left hand side that can be pushed into a right hand side tree. */
  sealed abstract class Lhs {
    def hasNothingType: Boolean = false
  }

  object Lhs {
    final case class Assign(lhs: Tree) extends Lhs
    final case class VarDef(name: js.Ident, tpe: Type, mutable: Boolean) extends Lhs

    case object ReturnFromFunction extends Lhs {
      override def hasNothingType: Boolean = true
    }

    final case class Return(label: LabelIdent) extends Lhs {
      override def hasNothingType: Boolean = true
    }

    case object Throw extends Lhs {
      override def hasNothingType: Boolean = true
    }

    /** Discard the value of rhs (but retain side effects). */
    case object Discard extends Lhs
  }

  sealed abstract class VarKind

  object VarKind {
    case object Mutable extends VarKind
    case object Immutable extends VarKind
    case object ThisAlias extends VarKind
    case object ExplicitThisAlias extends VarKind
    case object ClassCapture extends VarKind
  }

  // Environment

  final class Env private (
      val hasExplicitThis: Boolean,
      val expectedReturnType: Type,
      val enclosingClassName: Option[ClassName],
      vars: Map[LocalName, VarKind],
      labeledExprLHSes: Map[LabelName, Lhs],
      labelsTurnedIntoContinue: Set[LabelName],
      defaultBreakTargets: Set[LabelName],
      defaultContinueTargets: Set[LabelName],
      val inLoopForVarCapture: Boolean
  ) {
    def varKind(ident: LocalIdent): VarKind = {
      // If we do not know the var, it must be a JS class capture.
      vars.getOrElse(ident.name, VarKind.ClassCapture)
    }

    def isLocalMutable(ident: LocalIdent): Boolean =
      VarKind.Mutable == varKind(ident)

    def lhsForLabeledExpr(label: LabelIdent): Lhs = labeledExprLHSes(label.name)

    def isLabelTurnedIntoContinue(label: LabelName): Boolean =
      labelsTurnedIntoContinue.contains(label)

    def isDefaultBreakTarget(label: LabelName): Boolean =
      defaultBreakTargets.contains(label)

    def isDefaultContinueTarget(label: LabelName): Boolean =
      defaultContinueTargets.contains(label)

    def withEnclosingClassName(enclosingClassName: Option[ClassName]): Env =
      copy(enclosingClassName = enclosingClassName)

    def withExplicitThis(): Env =
      copy(hasExplicitThis = true)

    def withVars(newVars: Map[LocalName, VarKind]): Env =
      copy(vars = vars ++ newVars)

    def withParams(params: List[ParamDef]): Env = {
      params.foldLeft(this) {
        case (env, ParamDef(name, _, _, mutable)) =>
          env.withDef(name, mutable)
      }
    }

    def withDef(ident: LocalIdent, mutable: Boolean): Env = {
      val kind =
        if (mutable) VarKind.Mutable
        else VarKind.Immutable
      copy(vars = vars + (ident.name -> kind))
    }

    def withLabeledExprLHS(label: LabelIdent, lhs: Lhs): Env =
      copy(labeledExprLHSes = labeledExprLHSes + (label.name -> lhs))

    def withTurnLabelIntoContinue(label: LabelName): Env =
      copy(labelsTurnedIntoContinue = labelsTurnedIntoContinue + label)

    def withDefaultBreakTargets(targets: Set[LabelName]): Env =
      copy(defaultBreakTargets = targets)

    def withDefaultContinueTargets(targets: Set[LabelName]): Env =
      copy(defaultContinueTargets = targets)

    def withInLoopForVarCapture(inLoopForVarCapture: Boolean): Env =
      copy(inLoopForVarCapture = inLoopForVarCapture)

    private def copy(
        hasExplicitThis: Boolean = this.hasExplicitThis,
        expectedReturnType: Type = this.expectedReturnType,
        enclosingClassName: Option[ClassName] = this.enclosingClassName,
        vars: Map[LocalName, VarKind] = this.vars,
        labeledExprLHSes: Map[LabelName, Lhs] = this.labeledExprLHSes,
        labelsTurnedIntoContinue: Set[LabelName] = this.labelsTurnedIntoContinue,
        defaultBreakTargets: Set[LabelName] = this.defaultBreakTargets,
        defaultContinueTargets: Set[LabelName] = this.defaultContinueTargets,
        inLoopForVarCapture: Boolean = this.inLoopForVarCapture): Env = {
      new Env(hasExplicitThis, expectedReturnType, enclosingClassName, vars,
          labeledExprLHSes, labelsTurnedIntoContinue, defaultBreakTargets,
          defaultContinueTargets, inLoopForVarCapture)
    }
  }

  object Env {
    def empty(expectedReturnType: Type): Env = {
      new Env(false, expectedReturnType, None, Map.empty, Map.empty, Set.empty,
          Set.empty, Set.empty, false)
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy