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

org.scalajs.linker.backend.wasmemitter.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.wasmemitter

import scala.annotation.{switch, tailrec}

import scala.collection.mutable

import org.scalajs.ir.{ClassKind, OriginalName, Position, UTF8String}
import org.scalajs.ir.Names._
import org.scalajs.ir.OriginalName.NoOriginalName
import org.scalajs.ir.Trees._
import org.scalajs.ir.Types._

import org.scalajs.linker.interface.CheckedBehavior
import org.scalajs.linker.backend.emitter.Transients

import org.scalajs.linker.backend.webassembly._
import org.scalajs.linker.backend.webassembly.{Instructions => wa}
import org.scalajs.linker.backend.webassembly.{Identitities => wanme}
import org.scalajs.linker.backend.webassembly.{Types => watpe}
import org.scalajs.linker.backend.webassembly.Types.{FunctionType => Sig}

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

import EmbeddedConstants._
import SWasmGen._
import VarGen._
import TypeTransformer._

object FunctionEmitter {

  /** Whether to use the legacy `try` instruction to implement `TryCatch`.
   *
   *  Support for catching JS exceptions was only added to `try_table` in V8 12.5 from April 2024.
   *  While waiting for Node.js to catch up with V8, we use `try` to implement our `TryCatch`.
   *
   *  We use this "fixed configuration option" to keep the code that implements `TryCatch` using
   *  `try_table` in the codebase, as code that is actually compiled, so that refactorings apply to
   *  it as well. It also makes it easier to manually experiment with the new `try_table` encoding,
   *  which is available in Chrome since v125.
   *
   *  Note that we use `try_table` regardless to implement `TryFinally`. Its `catch_all_ref` handler
   *  is perfectly happy to catch and rethrow JavaScript exception in Node.js 22. Duplicating that
   *  implementation for `try` would be a nightmare, given how complex it is already.
   */
  private final val UseLegacyExceptionsForTryCatch = true

  private val dotUTF8String = UTF8String(".")

  def emitFunction(
      functionID: wanme.FunctionID,
      originalName: OriginalName,
      enclosingClassName: Option[ClassName],
      captureParamDefs: Option[List[ParamDef]],
      receiverType: Option[watpe.Type],
      paramDefs: List[ParamDef],
      restParam: Option[ParamDef],
      body: Tree,
      resultType: Type
  )(implicit ctx: WasmContext, pos: Position): Unit = {
    val emitter = prepareEmitter(
      functionID,
      originalName,
      enclosingClassName,
      captureParamDefs,
      preSuperVarDefs = None,
      hasNewTarget = false,
      receiverType,
      paramDefs ::: restParam.toList,
      transformResultType(resultType)
    )
    emitter.genBody(body, resultType)
    emitter.fb.buildAndAddToModule()
  }

  def emitJSConstructorFunctions(
      preSuperStatsFunctionID: wanme.FunctionID,
      superArgsFunctionID: wanme.FunctionID,
      postSuperStatsFunctionID: wanme.FunctionID,
      enclosingClassName: ClassName,
      jsClassCaptures: List[ParamDef],
      ctor: JSConstructorDef
  )(implicit ctx: WasmContext): Unit = {
    implicit val pos = ctor.pos

    val allCtorParams = ctor.args ::: ctor.restParam.toList
    val ctorBody = ctor.body

    // Compute the pre-super environment
    val preSuperDecls = ctorBody.beforeSuper.collect { case varDef: VarDef =>
      varDef
    }

    // Build the `preSuperStats` function
    locally {
      val preSuperEnvStructTypeID = ctx.getClosureDataStructType(preSuperDecls.map(_.vtpe))
      val preSuperEnvType = watpe.RefType(preSuperEnvStructTypeID)

      val emitter = prepareEmitter(
        preSuperStatsFunctionID,
        OriginalName(UTF8String("preSuperStats.") ++ enclosingClassName.encoded),
        Some(enclosingClassName),
        Some(jsClassCaptures),
        preSuperVarDefs = None,
        hasNewTarget = true,
        receiverType = None,
        allCtorParams,
        List(preSuperEnvType)
      )

      emitter.returnWithNPEScope() {
        emitter.genBlockStats(ctorBody.beforeSuper) {
          // Build and return the preSuperEnv struct
          for (varDef <- preSuperDecls) {
            val localID = (emitter.lookupLocal(varDef.name.name): @unchecked) match {
              case VarStorage.Local(localID) => localID
            }
            emitter.fb += wa.LocalGet(localID)
          }
          emitter.fb += wa.StructNew(preSuperEnvStructTypeID)
        }
      }

      emitter.fb.buildAndAddToModule()
    }

    // Build the `superArgs` function
    locally {
      val emitter = prepareEmitter(
        superArgsFunctionID,
        OriginalName(UTF8String("superArgs.") ++ enclosingClassName.encoded),
        Some(enclosingClassName),
        Some(jsClassCaptures),
        Some(preSuperDecls),
        hasNewTarget = true,
        receiverType = None,
        allCtorParams,
        List(watpe.RefType.anyref) // a js.Array
      )
      emitter.genBody(JSArrayConstr(ctorBody.superCall.args), AnyType)
      emitter.fb.buildAndAddToModule()
    }

    // Build the `postSuperStats` function
    locally {
      val emitter = prepareEmitter(
        postSuperStatsFunctionID,
        OriginalName(UTF8String("postSuperStats.") ++ enclosingClassName.encoded),
        Some(enclosingClassName),
        Some(jsClassCaptures),
        Some(preSuperDecls),
        hasNewTarget = true,
        receiverType = Some(watpe.RefType.anyref),
        allCtorParams,
        List(watpe.RefType.anyref)
      )
      emitter.genBody(Block(ctorBody.afterSuper), AnyType)
      emitter.fb.buildAndAddToModule()
    }
  }

  private def prepareEmitter(
      functionID: wanme.FunctionID,
      originalName: OriginalName,
      enclosingClassName: Option[ClassName],
      captureParamDefs: Option[List[ParamDef]],
      preSuperVarDefs: Option[List[VarDef]],
      hasNewTarget: Boolean,
      receiverType: Option[watpe.Type],
      paramDefs: List[ParamDef],
      resultTypes: List[watpe.Type]
  )(implicit ctx: WasmContext, pos: Position): FunctionEmitter = {
    val fb = new FunctionBuilder(ctx.moduleBuilder, functionID, originalName, pos)

    def addCaptureLikeParamListAndMakeEnv(
        captureParamName: String,
        captureLikes: List[(LocalName, Type)]
    ): Env = {
      val dataStructTypeID = ctx.getClosureDataStructType(captureLikes.map(_._2))
      val param = fb.addParam(captureParamName, watpe.RefType(dataStructTypeID))
      val env: List[(LocalName, VarStorage)] = for {
        ((name, _), idx) <- captureLikes.zipWithIndex
      } yield {
        val storage = VarStorage.StructField(
          param,
          dataStructTypeID,
          genFieldID.captureParam(idx)
        )
        name -> storage
      }
      env.toMap
    }

    val captureParamsEnv: Env = captureParamDefs match {
      case None =>
        Map.empty
      case Some(defs) =>
        addCaptureLikeParamListAndMakeEnv("__captureData",
            defs.map(p => p.name.name -> p.ptpe))
    }

    val preSuperEnvEnv: Env = preSuperVarDefs match {
      case None =>
        Map.empty
      case Some(defs) =>
        addCaptureLikeParamListAndMakeEnv("__preSuperEnv",
            defs.map(p => p.name.name -> p.vtpe))
    }

    val newTargetStorage = if (!hasNewTarget) {
      None
    } else {
      val newTargetParam = fb.addParam(newTargetOriginalName, watpe.RefType.anyref)
      Some(VarStorage.Local(newTargetParam))
    }

    val receiverStorage = receiverType.map { tpe =>
      val receiverParam = fb.addParam(receiverOriginalName, tpe)
      VarStorage.Local(receiverParam)
    }

    val normalParamsEnv: Env = paramDefs.map { paramDef =>
      val param = fb.addParam(
        paramDef.originalName.orElse(paramDef.name.name),
        transformParamType(paramDef.ptpe)
      )
      paramDef.name.name -> VarStorage.Local(param)
    }.toMap

    val fullEnv: Env = captureParamsEnv ++ preSuperEnvEnv ++ normalParamsEnv

    fb.setResultTypes(resultTypes)

    new FunctionEmitter(
      fb,
      enclosingClassName,
      newTargetStorage,
      receiverStorage,
      fullEnv
    )
  }

  private val ObjectRef = ClassRef(ObjectClass)
  private val BoxedStringRef = ClassRef(BoxedStringClass)
  private val toStringMethodName = MethodName("toString", Nil, BoxedStringRef)
  private val equalsMethodName = MethodName("equals", List(ObjectRef), BooleanRef)
  private val compareToMethodName = MethodName("compareTo", List(ObjectRef), IntRef)

  private val CharSequenceClass = ClassName("java.lang.CharSequence")
  private val ComparableClass = ClassName("java.lang.Comparable")
  private val JLNumberClass = ClassName("java.lang.Number")

  private val newTargetOriginalName = OriginalName("new.target")
  private val receiverOriginalName = OriginalName("this")

  private sealed abstract class VarStorage

  private object VarStorage {
    sealed abstract class NonStructStorage extends VarStorage

    final case class Local(localID: wanme.LocalID) extends NonStructStorage

    // We use Vector here because we want a decent reverseIterator
    final case class LocalRecord(fields: Vector[(SimpleFieldName, NonStructStorage)])
        extends NonStructStorage

    final case class StructField(structLocalID: wanme.LocalID,
        structTypeID: wanme.TypeID, fieldID: wanme.FieldID)
        extends VarStorage
  }

  private type Env = Map[LocalName, VarStorage]

  private final class ClosureFunctionID(debugName: OriginalName) extends wanme.FunctionID {
    override def toString(): String = s"ClosureFunctionID(${debugName.toString()})"
  }
}

private class FunctionEmitter private (
    val fb: FunctionBuilder,
    enclosingClassName: Option[ClassName],
    _newTargetStorage: Option[FunctionEmitter.VarStorage.Local],
    _receiverStorage: Option[FunctionEmitter.VarStorage.Local],
    paramsEnv: FunctionEmitter.Env
)(implicit ctx: WasmContext) {
  import FunctionEmitter._

  private val coreSpec = ctx.coreSpec
  import coreSpec.semantics

  private var currentNPELabel: Option[wanme.LabelID] = null
  private var closureIdx: Int = 0
  private var currentEnv: Env = paramsEnv

  private def newTargetStorage: VarStorage.Local =
    _newTargetStorage.getOrElse(throw new Error("Cannot access new.target in this context."))

  private def receiverStorage: VarStorage.Local =
    _receiverStorage.getOrElse(throw new Error("Cannot access to the receiver in this context."))

  /** Opens a new scope in which NPEs can be thrown by jumping to the NPE label.
   *
   *  When NPEs are unchecked this is a no-op (other than calling `body`).
   *
   *  Otherwise, this method logically generates the following code:
   *
   *  {{{
   *  block resultType $noNPELabel
   *    block $npeLabel
   *      body
   *      br $noNPELabel
   *    end
   *    call $throwNullPointerException
   *    unreachable
   *  end
   *  }}}
   *
   *  Inside the `body`, it is therefore possible to throw an NPE by jumping to
   *  the `npeLabel`. This is typically done through a `br_on_null $npeLabel`
   *  instruction.
   *
   *  If the `npeLabel` is not actually requested while generated `body`, the
   *  surrounding code is skipped.
   */
  private def withNPEScope[A](resultType: List[watpe.Type])(body: => A): A = {
    if (semantics.nullPointers == CheckedBehavior.Unchecked) {
      body
    } else {
      val savedNPELabel = currentNPELabel

      currentNPELabel = None
      val startIndex = fb.markCurrentInstructionIndex()

      val result = body

      for (npeLabel <- currentNPELabel) {
        val noNPELabel = fb.genLabel()

        // Go back and open the two blocks
        val blockType = fb.sigToBlockType(watpe.FunctionType(Nil, resultType))
        fb.insertAll(
          startIndex,
          List(
            wa.Block(blockType, Some(noNPELabel)),
            wa.Block(wa.BlockType.ValueType(), Some(npeLabel))
          )
        )

        // Add the code after the body in the normal way
        fb += wa.Br(noNPELabel)
        fb += wa.End // npeLabel
        fb += wa.Call(genFunctionID.throwNullPointerException)
        fb += wa.Unreachable
        fb += wa.End // noNPELabel
      }

      currentNPELabel = savedNPELabel
      result
    }
  }

  /** Like `withNPEScope`, but `return` for the success path.
   *
   *  This alternative can be used instead of `withNPEScope` when the `body`
   *  is what gets returned from the current function. It generates better code
   *  for that common case, namely:
   *
   *  {{{
   *  block $npeLabel
   *    body
   *    return
   *  end
   *  call $throwNullPointerException
   *  unreachable
   *  }}}
   */
  private def returnWithNPEScope[A]()(body: => A): A = {
    if (semantics.nullPointers == CheckedBehavior.Unchecked) {
      body
    } else {
      val savedNPELabel = currentNPELabel

      currentNPELabel = None
      val startIndex = fb.markCurrentInstructionIndex()

      val result = body

      for (npeLabel <- currentNPELabel) {
        // Go back and open the block
        fb.insert(startIndex, wa.Block(wa.BlockType.ValueType(), Some(npeLabel)))

        // Add the code after the body in the normal way
        fb += wa.Return
        fb += wa.End // npeLabel
        fb += wa.Call(genFunctionID.throwNullPointerException)
        fb += wa.Unreachable
      }

      currentNPELabel = savedNPELabel
      result
    }
  }

  private def getNPELabel(): wanme.LabelID = {
    assert(semantics.nullPointers != CheckedBehavior.Unchecked)
    currentNPELabel.getOrElse {
      val label = fb.genLabel()
      currentNPELabel = Some(label)
      label
    }
  }

  /** Emits a `ref.as_non_null` or an NPE check if required for the given `Tree`.
   *
   *  This method does not emit `tree`. It only uses it to determine whether
   *  a check is required.
   */
  private def genAsNonNullOrNPEFor(tree: Tree): Unit = {
    if (tree.tpe.isNullable) {
      if (tree.tpe == NullType)
        genNPE()
      else if (semantics.nullPointers != CheckedBehavior.Unchecked)
        fb += wa.BrOnNull(getNPELabel())
      else
        fb += wa.RefAsNonNull
    }
  }

  /** Emits an NPE check if required for the given `Tree`, otherwise nothing.
   *
   *  This method does not emit `tree`. It only uses it to determine whether
   *  a check is required.
   *
   *  Unlike `genAsNonNullOrNPE`, after this codegen the value on the stack is
   *  still statically typed as nullable at the Wasm level.
   */
  private def genCheckNonNullFor(tree: Tree): Unit = {
    if (tree.tpe.isNullable && semantics.nullPointers != CheckedBehavior.Unchecked)
      fb += wa.BrOnNull(getNPELabel())
  }

  /** Emits an unconditional NPE. */
  private def genNPE(): Unit = {
    if (semantics.nullPointers == CheckedBehavior.Unchecked)
      fb += wa.Unreachable
    else
      fb += wa.Br(getNPELabel())
  }

  private def withNewLocal[A](name: LocalName, originalName: OriginalName, tpe: watpe.Type)(
      body: wanme.LocalID => A
  ): A = {
    val savedEnv = currentEnv
    val local = fb.addLocal(originalName.orElse(name), tpe)
    currentEnv = currentEnv.updated(name, VarStorage.Local(local))
    try body(local)
    finally currentEnv = savedEnv
  }

  private def lookupLocal(name: LocalName): VarStorage = {
    currentEnv.getOrElse(
      name, {
        throw new AssertionError(s"Cannot find binding for '${name.nameString}'")
      }
    )
  }

  private def lookupRecordSelect(tree: RecordSelect): VarStorage.NonStructStorage = {
    val RecordSelect(record, field) = tree

    val recordStorage = record match {
      case VarRef(LocalIdent(name)) =>
        lookupLocal(name)
      case record: RecordSelect =>
        lookupRecordSelect(record)
      case _ =>
        throw new AssertionError(s"Unexpected record tree: $record")
    }

    recordStorage match {
      case VarStorage.LocalRecord(fields) =>
        fields.find(_._1 == field.name).getOrElse {
          throw new AssertionError(s"Unknown field ${field.name} of $record")
        }._2
      case other =>
        throw new AssertionError(s"Unexpected storage $other for record $record")
    }
  }

  @tailrec
  private def canLookupRecordSelect(tree: RecordSelect): Boolean = {
    tree.record match {
      case _: VarRef            => true
      case record: RecordSelect => canLookupRecordSelect(record)
      case _                    => false
    }
  }

  private def addSyntheticLocal(tpe: watpe.Type): wanme.LocalID =
    fb.addLocal(NoOriginalName, tpe)

  private def genClosureFuncOriginalName(): OriginalName = {
    if (fb.functionOriginalName.isEmpty) {
      NoOriginalName
    } else {
      val innerName = OriginalName(fb.functionOriginalName.get ++ UTF8String("__c" + closureIdx))
      closureIdx += 1
      innerName
    }
  }

  private def markPosition(pos: Position): Unit =
    fb += wa.PositionMark(pos)

  private def markPosition(tree: Tree): Unit =
    markPosition(tree.pos)

  def genBody(tree: Tree, expectedType: Type): Unit = {
    returnWithNPEScope() {
      genTree(tree, expectedType)
    }
  }

  def genTreeAuto(tree: Tree): Unit =
    genTree(tree, tree.tpe)

  def genTreeToAny(tree: Tree): Unit =
    genTree(tree, if (tree.tpe.isNullable) AnyType else AnyNotNullType)

  def genTree(tree: Tree, expectedType: Type): Unit = {
    val generatedType: Type = tree match {
      case t: Literal             => genLiteral(t, expectedType)
      case t: UnaryOp             => genUnaryOp(t)
      case t: BinaryOp            => genBinaryOp(t)
      case t: VarRef              => genVarRef(t)
      case t: LoadModule          => genLoadModule(t)
      case t: StoreModule         => genStoreModule(t)
      case t: This                => genThis(t)
      case t: ApplyStatically     => genApplyStatically(t)
      case t: Apply               => genApply(t)
      case t: ApplyStatic         => genApplyStatic(t)
      case t: ApplyDynamicImport  => genApplyDynamicImport(t)
      case t: IsInstanceOf        => genIsInstanceOf(t)
      case t: AsInstanceOf        => genAsInstanceOf(t)
      case t: GetClass            => genGetClass(t)
      case t: Block               => genBlock(t, expectedType)
      case t: Labeled             => unwinding.genLabeled(t, expectedType)
      case t: Return              => unwinding.genReturn(t)
      case t: Select              => genSelect(t)
      case t: SelectStatic        => genSelectStatic(t)
      case t: Assign              => genAssign(t)
      case t: VarDef              => genVarDef(t)
      case t: New                 => genNew(t)
      case t: If                  => genIf(t, expectedType)
      case t: While               => genWhile(t)
      case t: ForIn               => genForIn(t)
      case t: TryCatch            => genTryCatch(t, expectedType)
      case t: TryFinally          => unwinding.genTryFinally(t, expectedType)
      case t: Throw               => genThrow(t)
      case t: Match               => genMatch(t, expectedType)
      case t: Debugger            => NoType // ignore
      case t: Skip                => NoType
      case t: Clone               => genClone(t)
      case t: IdentityHashCode    => genIdentityHashCode(t)
      case t: WrapAsThrowable     => genWrapAsThrowable(t)
      case t: UnwrapFromThrowable => genUnwrapFromThrowable(t)

      // JavaScript expressions
      case t: JSNew                => genJSNew(t)
      case t: JSSelect             => genJSSelect(t)
      case t: JSFunctionApply      => genJSFunctionApply(t)
      case t: JSMethodApply        => genJSMethodApply(t)
      case t: JSImportCall         => genJSImportCall(t)
      case t: JSImportMeta         => genJSImportMeta(t)
      case t: LoadJSConstructor    => genLoadJSConstructor(t)
      case t: LoadJSModule         => genLoadJSModule(t)
      case t: SelectJSNativeMember => genSelectJSNativeMember(t)
      case t: JSDelete             => genJSDelete(t)
      case t: JSUnaryOp            => genJSUnaryOp(t)
      case t: JSBinaryOp           => genJSBinaryOp(t)
      case t: JSArrayConstr        => genJSArrayConstr(t)
      case t: JSObjectConstr       => genJSObjectConstr(t)
      case t: JSGlobalRef          => genJSGlobalRef(t)
      case t: JSTypeOfGlobalRef    => genJSTypeOfGlobalRef(t)
      case t: JSLinkingInfo        => genJSLinkingInfo(t)
      case t: Closure              => genClosure(t)

      // array
      case t: ArrayLength => genArrayLength(t)
      case t: NewArray    => genNewArray(t)
      case t: ArraySelect => genArraySelect(t)
      case t: ArrayValue  => genArrayValue(t)

      // Non-native JS classes
      case t: CreateJSClass     => genCreateJSClass(t)
      case t: JSPrivateSelect   => genJSPrivateSelect(t)
      case t: JSSuperSelect     => genJSSuperSelect(t)
      case t: JSSuperMethodCall => genJSSuperMethodCall(t)
      case t: JSNewTarget       => genJSNewTarget(t)

      // Records (only generated by the optimizer)
      case t: RecordSelect => genRecordSelect(t)
      case t: RecordValue  => genRecordValue(t)

      // Transients (only generated by the optimizer)
      case t: Transient => genTransient(t)

      case _: JSSuperConstructorCall =>
        throw new AssertionError(s"Invalid tree: $tree")
    }

    genAdapt(generatedType, expectedType)
  }

  private def genAdapt(generatedType: Type, expectedType: Type): Unit = {
    (generatedType, expectedType) match {
      case _ if generatedType == expectedType =>
        ()
      case (NothingType, _) =>
        ()
      case (_, NoType) =>
        fb += wa.Drop
      case (primType: PrimTypeWithRef, _) =>
        // box
        primType match {
          case NullType =>
            expectedType match {
              case ClassType(BoxedStringClass, true) => fb += wa.ExternConvertAny
              case _                                 => ()
            }
          case ByteType | ShortType =>
            fb += wa.RefI31
          case CharType =>
            /* `char` and `long` are opaque to JS in the Scala.js semantics.
             * We implement them with real Wasm classes following the correct
             * vtable. Upcasting wraps a primitive into the corresponding class.
             */
            genBox(watpe.Int32, SpecialNames.CharBoxClass)
          case LongType =>
            genBox(watpe.Int64, SpecialNames.LongBoxClass)
          case NoType | NothingType =>
            throw new AssertionError(s"Unexpected adaptation from $primType to $expectedType")
          case _ =>
            /* Calls a `bX` helper. Most of them are of the form
             *   bX: (x) => x
             * at the JavaScript level, but with a primType->anyref Wasm type.
             * For example, for `IntType`, `bI` has type `i32 -> anyref`. This
             * asks the JS host to turn a primitive `i32` into its generic
             * representation, which we can store in an `anyref`.
             */
            fb += wa.Call(genFunctionID.box(primType.primRef))
        }
      case (StringType | ClassType(BoxedStringClass, _), _) =>
        expectedType match {
          case ClassType(BoxedStringClass, _) => ()
          case _                              => fb += wa.AnyConvertExtern
        }
      case _ =>
        ()
    }
  }

  private def genAssign(tree: Assign): Type = {
    val Assign(lhs, rhs) = tree

    implicit val pos = tree.pos

    lhs match {
      case Select(qualifier, field) =>
        val className = field.name.className
        val classInfo = ctx.getClassInfo(className)

        // For Select, the receiver can never be a hijacked class, so we can use genTreeAuto
        genTreeAuto(qualifier)

        if (!classInfo.hasInstances) {
          /* The field may not exist in that case, and we cannot look it up.
           * However we necessarily have a `null` receiver if we reach this
           * point, so we can trap as NPE.
           */
          markPosition(tree)
          genNPE()
        } else {
          genCheckNonNullFor(qualifier)
          genTree(rhs, lhs.tpe)
          markPosition(tree)
          fb += wa.StructSet(
            genTypeID.forClass(className),
            genFieldID.forClassInstanceField(field.name)
          )
        }

      case SelectStatic(field) =>
        val fieldName = field.name
        val globalID = genGlobalID.forStaticField(fieldName)

        genTree(rhs, lhs.tpe)
        markPosition(tree)
        fb += wa.GlobalSet(globalID)

        // Update top-level export mirrors
        val classInfo = ctx.getClassInfo(fieldName.className)
        val mirrors = classInfo.staticFieldMirrors.getOrElse(fieldName, Nil)
        for (exportedName <- mirrors) {
          fb += wa.GlobalGet(globalID)
          fb += wa.Call(genFunctionID.forTopLevelExportSetter(exportedName))
        }

      case ArraySelect(array, index) =>
        genTreeAuto(array)
        array.tpe match {
          case ArrayType(arrayTypeRef, _) =>
            def isPrimArray = arrayTypeRef match {
              case ArrayTypeRef(_: PrimRef, 1) => true
              case _                           => false
            }

            genCheckNonNullFor(array)

            def genRhs(): Unit = {
              genTree(rhs, lhs.tpe)
              lhs.tpe match {
                case ClassType(BoxedStringClass, _) => fb += wa.AnyConvertExtern
                case _                              => ()
              }
            }

            if (semantics.arrayIndexOutOfBounds == CheckedBehavior.Unchecked &&
                (semantics.arrayStores == CheckedBehavior.Unchecked || isPrimArray)) {
              // Get the underlying array
              fb += wa.StructGet(
                genTypeID.forArrayClass(arrayTypeRef),
                genFieldID.objStruct.arrayUnderlying
              )
              genTree(index, IntType)
              genRhs()
              markPosition(tree)
              fb += wa.ArraySet(genTypeID.underlyingOf(arrayTypeRef))
            } else {
              genTree(index, IntType)
              genRhs()
              markPosition(tree)
              fb += wa.Call(genFunctionID.arraySetFor(arrayTypeRef))
            }
          case NothingType =>
            // unreachable
            ()
          case NullType =>
            markPosition(tree)
            genNPE()
          case _ =>
            throw new IllegalArgumentException(
                s"ArraySelect.array must be an array type, but has type ${array.tpe}")
        }

      case JSPrivateSelect(qualifier, field) =>
        genTree(qualifier, AnyType)
        fb += wa.GlobalGet(genGlobalID.forJSPrivateField(field.name))
        genTree(rhs, AnyType)
        markPosition(tree)
        fb += wa.Call(genFunctionID.jsSelectSet)

      case JSSelect(qualifier, item) =>
        genThroughCustomJSHelper(List(qualifier, item, rhs), NoType) { allJSArgs =>
          val List(jsQualifier, jsItem, jsRhs) = allJSArgs
          js.Assign(js.BracketSelect.makeOptimized(jsQualifier, jsItem), jsRhs)
        }

      case JSSuperSelect(superClass, receiver, item) =>
        genTree(superClass, AnyType)
        genTree(receiver, AnyType)
        genTree(item, AnyType)
        genTree(rhs, AnyType)
        markPosition(tree)
        fb += wa.Call(genFunctionID.jsSuperSelectSet)

      case lhs @ JSGlobalRef(name) =>
        val builder = new CustomJSHelperBuilderWithTreeSupport()
        val rhsRef = builder.addInput(rhs)
        val helperID = builder.build(NoType) {
          js.Assign(builder.genGlobalRef(name), rhsRef)
        }
        markPosition(tree)
        fb += wa.Call(helperID)

      case VarRef(LocalIdent(name)) =>
        genTree(rhs, lhs.tpe)
        markPosition(tree)
        genWriteToStorage(lookupLocal(name))

      case lhs: RecordSelect =>
        genTree(rhs, lhs.tpe)
        markPosition(tree)
        genWriteToStorage(lookupRecordSelect(lhs))
    }

    NoType
  }

  private def genWriteToStorage(storage: VarStorage): Unit = {
    storage match {
      case VarStorage.Local(local) =>
        fb += wa.LocalSet(local)

      case VarStorage.LocalRecord(fields) =>
        fields.reverseIterator.foreach(field => genWriteToStorage(field._2))

      case storage: VarStorage.StructField =>
        throw new AssertionError(s"Unexpected write to capture storage $storage")
    }
  }

  private def genApply(tree: Apply): Type = {
    val Apply(flags, receiver, method, args) = tree

    receiver.tpe match {
      case NothingType =>
        genTree(receiver, NothingType)
        // nothing else to do; this is unreachable
        NothingType

      case NullType =>
        genTree(receiver, NullType)
        genNPE()
        NothingType

      case _ if method.name.isReflectiveProxy =>
        genReflectiveCall(tree)

      case _ =>
        val receiverClassName = receiver.tpe match {
          case prim: PrimType =>
            PrimTypeToBoxedClass(prim)
          case ClassType(cls, _) =>
            cls
          case AnyType | AnyNotNullType | ArrayType(_, _) =>
            ObjectClass
          case tpe: RecordType =>
            throw new AssertionError(s"Invalid receiver type $tpe")
        }
        val receiverClassInfo = ctx.getClassInfo(receiverClassName)

        /* Hijacked classes do not receive tables at all, and `Apply`s on array
         * types are considered to be statically resolved by the `Analyzer`.
         * Therefore, if the receiver's static type is a prim type, hijacked
         * class or array type, we must use static dispatch instead.
         *
         * This never happens when we use the optimizer, since it already turns
         * any such `Apply` into an `ApplyStatically` (when it does not inline
         * it altogether).
         */
        val useStaticDispatch = {
          receiverClassInfo.kind == ClassKind.HijackedClass ||
          receiver.tpe.isInstanceOf[ArrayType]
        }
        if (useStaticDispatch) {
          genApplyStatically(ApplyStatically(
              flags, receiver, receiverClassName, method, args)(tree.tpe)(tree.pos))
        } else {
          genApplyWithDispatch(tree, receiverClassInfo)
        }
    }
  }

  private def genReflectiveCall(tree: Apply): Type = {
    val Apply(flags, receiver, MethodIdent(methodName), args) = tree

    assert(methodName.isReflectiveProxy)

    val receiverLocalForDispatch =
      addSyntheticLocal(watpe.RefType.any)

    val proxyId = ctx.getReflectiveProxyId(methodName)
    val funcTypeID = ctx.tableFunctionType(methodName)

    /* We only need to handle calls on non-hijacked classes. For hijacked
     * classes, the compiler already emits the appropriate dispatch at the IR
     * level.
     */

    // Load receiver and arguments
    genTreeToAny(receiver)
    genAsNonNullOrNPEFor(receiver)
    fb += wa.LocalTee(receiverLocalForDispatch)
    genArgs(args, methodName)

    // Looks up the method to be (reflectively) called
    markPosition(tree)
    fb += wa.LocalGet(receiverLocalForDispatch)
    fb += wa.RefCast(watpe.RefType(genTypeID.ObjectStruct)) // see above: cannot be a hijacked class
    fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable)
    fb += wa.I32Const(proxyId)
    // `searchReflectiveProxy`: [typeData, i32] -> [(ref func)]
    fb += wa.Call(genFunctionID.searchReflectiveProxy)

    fb += wa.RefCast(watpe.RefType(watpe.HeapType(funcTypeID)))
    fb += wa.CallRef(funcTypeID)

    tree.tpe
  }

  /** Generates the code for an `Apply` tree that requires dynamic dispatch.
   *
   *  In that case, there is always at least a vtable/itable-based dispatch. It may also contain
   *  primitive-based dispatch if the receiver's type is an ancestor of a hijacked class.
   *
   *  This method must not be used if the receiver's type is a primitive, a
   *  hijacked class or an array type. Hijacked classes do not have dispatch
   *  tables, so the methods that are not available in any superclass/interface
   *  cannot be called through a table dispatch. Array types share their vtable
   *  with jl.Object, but methods called directly on an array type are not
   *  registered as called on jl.Object by the Analyzer. In all these cases,
   *  we must use a statically resolved call instead.
   */
  private def genApplyWithDispatch(tree: Apply,
      receiverClassInfo: WasmContext.ClassInfo): Type = {

    val Apply(flags, receiver, MethodIdent(methodName), args) = tree

    val receiverClassName = receiverClassInfo.name

    /* Similar to transformType(t.receiver.tpe), but:
     * - it is non-null,
     * - ancestors of hijacked classes are not treated specially,
     * - array types are treated as j.l.Object.
     *
     * This is used in the code paths where we have already ruled out `null`
     * values and primitive values (that implement hijacked classes).
     */
    val refTypeForDispatch: watpe.RefType = {
      if (receiverClassInfo.isInterface)
        watpe.RefType(genTypeID.ObjectStruct)
      else
        watpe.RefType(genTypeID.forClass(receiverClassName))
    }

    // A local for a copy of the receiver that we will use to resolve dispatch
    val receiverLocalForDispatch = addSyntheticLocal(refTypeForDispatch)

    /* Gen loading of the receiver and check that it is non-null.
     * After this codegen, the non-null receiver is on the stack.
     */
    def genReceiverNotNull(): Unit = {
      genTreeAuto(receiver)
      genAsNonNullOrNPEFor(receiver)
    }

    /* Generates a resolved call to a method of a hijacked class.
     * Before this code gen, the stack must contain the receiver and the args.
     * After this code gen, the stack contains the result.
     */
    def genHijackedClassCall(hijackedClass: ClassName): Unit = {
      val funcID = genFunctionID.forMethod(MemberNamespace.Public, hijackedClass, methodName)
      fb += wa.Call(funcID)
    }

    if (!receiverClassInfo.hasInstances) {
      /* If the target class info does not have any instance, the only possible
       * value for the receiver is `null`. We can therefore immediately trap for
       * an NPE. It is important to short-cut this path because the reachability
       * analysis may have entirely dead-code eliminated the target method,
       * which means we do not know its signature and therefore cannot emit the
       * corresponding vtable/itable calls.
       */
      genTreeAuto(receiver)
      markPosition(tree)
      genNPE()
    } else if (!receiverClassInfo.isAncestorOfHijackedClass) {
      // Standard dispatch codegen
      genReceiverNotNull()
      fb += wa.LocalTee(receiverLocalForDispatch)
      genArgs(args, methodName)

      markPosition(tree)
      genTableDispatch(receiverClassInfo, methodName, receiverLocalForDispatch)
    } else {
      /* Here the receiver's type is an ancestor of a hijacked class (or `any`,
       * which is treated as `jl.Object`).
       *
       * We must emit additional dispatch for the possible primitive values.
       *
       * The overall structure of the generated code is as follows:
       *
       * block resultType $done
       *   block (ref any) $notOurObject
       *     load non-null receiver and args and store into locals
       *     reload copy of receiver
       *     br_on_cast_fail (ref any) (ref $targetRealClass) $notOurObject
       *     reload args
       *     generate standard table-based dispatch
       *     br $done
       *   end $notOurObject
       *   choose an implementation of a single hijacked class, or a JS helper
       *   reload args
       *   call the chosen implementation
       * end $done
       */

      assert(receiverClassInfo.kind != ClassKind.HijackedClass, receiverClassName)

      val resultType = transformResultType(tree.tpe)

      fb.block(resultType) { labelDone =>
        def pushArgs(argsLocals: List[wanme.LocalID]): Unit =
          argsLocals.foreach(argLocal => fb += wa.LocalGet(argLocal))

        /* First try the case where the value is one of our objects.
         * We load the receiver and arguments inside the block `notOurObject`.
         * This helps producing good code for the no-args case, in which we do
         * not need to store the receiver in a local at all.
         * For the case with the args, it does not hurt either way. We could
         * move it out, but that would make for a less consistent codegen.
         */
        val argsLocals = fb.block(watpe.RefType.any) { labelNotOurObject =>
          // Load receiver and arguments and store them in temporary variables
          genReceiverNotNull()
          val argsLocals = if (args.isEmpty) {
            /* When there are no arguments, we can leave the receiver directly on
             * the stack instead of going through a local. We will still need a
             * local for the table-based dispatch, though.
             */
            Nil
          } else {
            /* When there are arguments, we need to store them in temporary
             * variables. This is not required for correctness of the evaluation
             * order. It is only necessary so that we do not duplicate the
             * codegen of the arguments. If the arguments are complex, doing so
             * could lead to exponential blow-up of the generated code.
             */
            val receiverLocal = addSyntheticLocal(watpe.RefType.any)

            fb += wa.LocalSet(receiverLocal)
            val argsLocals: List[wanme.LocalID] =
              for ((arg, typeRef) <- args.zip(methodName.paramTypeRefs)) yield {
                val tpe = ctx.inferTypeFromTypeRef(typeRef)
                genTree(arg, tpe)
                val localID = addSyntheticLocal(transformParamType(tpe))
                fb += wa.LocalSet(localID)
                localID
              }
            fb += wa.LocalGet(receiverLocal)
            argsLocals
          }

          markPosition(tree) // main position marker for the entire hijacked class dispatch branch

          fb += wa.BrOnCastFail(labelNotOurObject, watpe.RefType.any, refTypeForDispatch)
          fb += wa.LocalTee(receiverLocalForDispatch)
          pushArgs(argsLocals)
          genTableDispatch(receiverClassInfo, methodName, receiverLocalForDispatch)
          fb += wa.Br(labelDone)

          argsLocals
        } // end block labelNotOurObject

        /* Now we have a value that is not one of our objects, so it must be
         * a JavaScript value whose representative class extends/implements the
         * receiver class. It may be a primitive instance of a hijacked class, or
         * any other value (whose representative class is therefore `jl.Object`).
         *
         * It is also *not* `char` or `long`, since those would reach
         * `genApplyNonPrim` in their boxed form, and therefore they are
         * "ourObject".
         *
         * The (ref any) is still on the stack.
         */

        if (methodName == toStringMethodName) {
          // By spec, toString() is special
          assert(argsLocals.isEmpty)
          fb += wa.Call(genFunctionID.jsValueToString)
        } else if (receiverClassName == JLNumberClass) {
          // the value must be a `number`, hence we can unbox to `double`
          genUnbox(DoubleType)
          pushArgs(argsLocals)
          genHijackedClassCall(BoxedDoubleClass)
        } else if (receiverClassName == CharSequenceClass) {
          // the value must be a `string`
          fb += wa.ExternConvertAny
          pushArgs(argsLocals)
          genHijackedClassCall(BoxedStringClass)
        } else if (methodName == compareToMethodName) {
          /* The only method of jl.Comparable. Here the value can be a boolean,
           * a number or a string. We use `jsValueType` to dispatch to Wasm-side
           * implementations because they have to perform casts on their arguments.
           */
          assert(argsLocals.size == 1)

          val receiverLocal = addSyntheticLocal(watpe.RefType.any)
          fb += wa.LocalTee(receiverLocal)

          val jsValueTypeLocal = addSyntheticLocal(watpe.Int32)
          fb += wa.Call(genFunctionID.jsValueType)
          fb += wa.LocalTee(jsValueTypeLocal)

          fb.switch(Sig(List(watpe.Int32), Nil), Sig(Nil, List(watpe.Int32))) { () =>
            // scrutinee is already on the stack
          }(
            // case JSValueTypeFalse | JSValueTypeTrue =>
            List(JSValueTypeFalse, JSValueTypeTrue) -> { () =>
              /* The jsValueTypeLocal is the boolean value, thanks to the chosen encoding.
               * This trick avoids an additional unbox.
               */
              fb += wa.LocalGet(jsValueTypeLocal)
              pushArgs(argsLocals)
              genHijackedClassCall(BoxedBooleanClass)
            },
            // case JSValueTypeString =>
            List(JSValueTypeString) -> { () =>
              fb += wa.LocalGet(receiverLocal)
              fb += wa.ExternConvertAny
              pushArgs(argsLocals)
              genHijackedClassCall(BoxedStringClass)
            }
          ) { () =>
            // case _ (JSValueTypeNumber) =>
            fb += wa.LocalGet(receiverLocal)
            genUnbox(DoubleType)
            pushArgs(argsLocals)
            genHijackedClassCall(BoxedDoubleClass)
          }
        } else {
          /* It must be a method of j.l.Object and it can be any value.
           * hashCode() and equals() are overridden in all hijacked classes.
           * We use `identityHashCode` for `hashCode` and `Object.is` for `equals`,
           * as they coincide with the respective specifications (on purpose).
           * The other methods are never overridden and can be statically
           * resolved to j.l.Object.
           */
          pushArgs(argsLocals)
          methodName match {
            case SpecialNames.hashCodeMethodName =>
              fb += wa.Call(genFunctionID.identityHashCode)
            case `equalsMethodName` =>
              fb += wa.Call(genFunctionID.is)
            case _ =>
              genHijackedClassCall(ObjectClass)
          }
        }
      } // end block labelDone
    }

    if (tree.tpe == NothingType)
      fb += wa.Unreachable

    tree.tpe
  }

  /** Generates a vtable- or itable-based dispatch.
   *
   *  Before this code gen, the stack must contain the receiver and the args of the target method.
   *  In addition, the receiver must be available in the local `receiverLocalForDispatch`. The two
   *  occurrences of the receiver must have the type for dispatch.
   *
   *  After this code gen, the stack contains the result. If the result type is `NothingType`,
   *  `genTableDispatch` leaves the stack in an arbitrary state. It is up to the caller to insert an
   *  `unreachable` instruction when appropriate.
   */
  def genTableDispatch(receiverClassInfo: WasmContext.ClassInfo,
      methodName: MethodName, receiverLocalForDispatch: wanme.LocalID): Unit = {
    // Generates an itable-based dispatch.
    def genITableDispatch(): Unit = {
      fb += wa.LocalGet(receiverLocalForDispatch)
      fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.itables)
      fb += wa.StructGet(
        genTypeID.itables,
        genFieldID.itablesStruct.itableSlot(receiverClassInfo.itableIdx)
      )
      fb += wa.RefCast(watpe.RefType(genTypeID.forITable(receiverClassInfo.name)))
      fb += wa.StructGet(
        genTypeID.forITable(receiverClassInfo.name),
        genFieldID.forMethodTableEntry(methodName)
      )
      fb += wa.CallRef(ctx.tableFunctionType(methodName))
    }

    // Generates a vtable-based dispatch.
    def genVTableDispatch(): Unit = {
      val receiverClassName = receiverClassInfo.name

      fb += wa.LocalGet(receiverLocalForDispatch)
      fb += wa.StructGet(
        genTypeID.forClass(receiverClassName),
        genFieldID.objStruct.vtable
      )
      fb += wa.StructGet(
        genTypeID.forVTable(receiverClassName),
        genFieldID.forMethodTableEntry(methodName)
      )
      fb += wa.CallRef(ctx.tableFunctionType(methodName))
    }

    if (receiverClassInfo.isInterface)
      genITableDispatch()
    else
      genVTableDispatch()
  }

  private def genApplyStatically(tree: ApplyStatically): Type = {
    val ApplyStatically(flags, receiver, className, MethodIdent(methodName), args) = tree

    receiver.tpe match {
      case NothingType =>
        genTree(receiver, NothingType)
        // nothing else to do; this is unreachable
        NothingType

      case NullType =>
        genTree(receiver, NullType)
        markPosition(tree)
        genNPE()
        NothingType

      case _ =>
        val namespace = MemberNamespace.forNonStaticCall(flags)
        val targetClassName = {
          val classInfo = ctx.getClassInfo(className)
          if (!classInfo.isInterface && namespace == MemberNamespace.Public)
            classInfo.resolvedMethodInfos(methodName).ownerClass
          else
            className
        }

        BoxedClassToPrimType.get(targetClassName) match {
          case None =>
            genTree(receiver, ClassType(targetClassName, nullable = receiver.tpe.isNullable))
            genAsNonNullOrNPEFor(receiver)

          case Some(primReceiverType) =>
            if (receiver.tpe == primReceiverType) {
              genTreeAuto(receiver)
            } else {
              genTreeToAny(receiver)
              genAsNonNullOrNPEFor(receiver)
              genUnbox(primReceiverType)
            }
        }

        genArgs(args, methodName)

        markPosition(tree)
        val funcID = genFunctionID.forMethod(namespace, targetClassName, methodName)
        fb += wa.Call(funcID)
        if (tree.tpe == NothingType)
          fb += wa.Unreachable
        tree.tpe
    }
  }

  private def genApplyStatic(tree: ApplyStatic): Type = {
    val ApplyStatic(flags, className, MethodIdent(methodName), args) = tree

    genArgs(args, methodName)
    val namespace = MemberNamespace.forStaticCall(flags)
    val funcID = genFunctionID.forMethod(namespace, className, methodName)
    markPosition(tree)
    fb += wa.Call(funcID)
    if (tree.tpe == NothingType)
      fb += wa.Unreachable
    tree.tpe
  }

  private def genApplyDynamicImport(tree: ApplyDynamicImport): Type = {
    // As long as we do not support multiple modules, this cannot happen
    throw new AssertionError(
        s"Unexpected $tree at ${tree.pos}; multiple modules are not supported yet")
  }

  private def genArgs(args: List[Tree], methodName: MethodName): Unit = {
    for ((arg, paramTypeRef) <- args.zip(methodName.paramTypeRefs)) {
      val paramType = ctx.inferTypeFromTypeRef(paramTypeRef)
      genTree(arg, paramType)
    }
  }

  private def genLiteral(tree: Literal, expectedType: Type): Type = {
    if (expectedType == NoType) {
      /* Since all literals are pure, we can always get rid of them.
       * This is mostly useful for the argument of `Return` nodes that target a
       * `Labeled` in statement position, since they must have a non-`void`
       * type in the IR but they get a `void` expected type.
       */
      expectedType
    } else if (tree.isInstanceOf[Null] && expectedType == ClassType(BoxedStringClass, true)) {
      /* Directly emit a `ref.null noextern` instead of requiring an
       * `extern.convert_from_any` in `genAdapt`.
       */
      markPosition(tree)
      fb += wa.RefNull(watpe.HeapType.NoExtern)
      expectedType
    } else {
      markPosition(tree)

      tree match {
        case BooleanLiteral(v) => fb += wa.I32Const(if (v) 1 else 0)
        case ByteLiteral(v)    => fb += wa.I32Const(v)
        case ShortLiteral(v)   => fb += wa.I32Const(v)
        case IntLiteral(v)     => fb += wa.I32Const(v)
        case CharLiteral(v)    => fb += wa.I32Const(v)
        case LongLiteral(v)    => fb += wa.I64Const(v)
        case FloatLiteral(v)   => fb += wa.F32Const(v)
        case DoubleLiteral(v)  => fb += wa.F64Const(v)

        case Undefined() =>
          fb += wa.GlobalGet(genGlobalID.undef)
        case Null() =>
          fb += wa.RefNull(watpe.HeapType.None)

        case StringLiteral(v) =>
          fb ++= ctx.stringPool.getConstantStringInstr(v)

        case ClassOf(typeRef) =>
          genLoadTypeData(fb, typeRef)
          fb += wa.Call(genFunctionID.getClassOf)
      }

      tree.tpe
    }
  }

  private def genSelect(tree: Select): Type = {
    val Select(qualifier, FieldIdent(fieldName)) = tree

    val className = fieldName.className
    val classInfo = ctx.getClassInfo(className)

    // For Select, the receiver can never be a hijacked class, so we can use genTreeAuto
    genTreeAuto(qualifier)

    markPosition(tree)

    if (!classInfo.hasInstances) {
      /* The field may not exist in that case, and we cannot look it up.
       * However we necessarily have a `null` receiver if we reach this point,
       * so we can trap as NPE.
       */
      genNPE()
    } else {
      genCheckNonNullFor(qualifier)
      fb += wa.StructGet(
        genTypeID.forClass(className),
        genFieldID.forClassInstanceField(fieldName)
      )
    }

    tree.tpe
  }

  private def genSelectStatic(tree: SelectStatic): Type = {
    val SelectStatic(FieldIdent(fieldName)) = tree

    markPosition(tree)
    fb += wa.GlobalGet(genGlobalID.forStaticField(fieldName))
    tree.tpe
  }

  private def genStoreModule(tree: StoreModule): Type = {
    val className = enclosingClassName.getOrElse {
      throw new AssertionError(s"Cannot emit $tree at ${tree.pos} without enclosing class name")
    }

    genTreeAuto(This()(ClassType(className, nullable = false))(tree.pos))

    markPosition(tree)
    fb += wa.GlobalSet(genGlobalID.forModuleInstance(className))
    NoType
  }

  private def genLoadModule(tree: LoadModule): Type = {
    val LoadModule(className) = tree

    markPosition(tree)
    fb += wa.Call(genFunctionID.loadModule(className))
    tree.tpe
  }

  private def genUnaryOp(tree: UnaryOp): Type = {
    import UnaryOp._

    val UnaryOp(op, lhs) = tree

    genTreeAuto(lhs)

    markPosition(tree)

    (op: @switch) match {
      case Boolean_! =>
        fb += wa.I32Eqz

      // Widening conversions
      case CharToInt | ByteToInt | ShortToInt =>
        /* These are no-ops because they are all represented as i32's with the
         * right mathematical value.
         */
        ()
      case IntToLong =>
        fb += wa.I64ExtendI32S
      case IntToDouble =>
        fb += wa.F64ConvertI32S
      case FloatToDouble =>
        fb += wa.F64PromoteF32

      // Narrowing conversions
      case IntToChar =>
        fb += wa.I32Const(0xFFFF)
        fb += wa.I32And
      case IntToByte =>
        fb += wa.I32Extend8S
      case IntToShort =>
        fb += wa.I32Extend16S
      case LongToInt =>
        fb += wa.I32WrapI64
      case DoubleToInt =>
        fb += wa.I32TruncSatF64S
      case DoubleToFloat =>
        fb += wa.F32DemoteF64

      // Long <-> Double (neither widening nor narrowing)
      case LongToDouble =>
        fb += wa.F64ConvertI64S
      case DoubleToLong =>
        fb += wa.I64TruncSatF64S

      // Long -> Float (neither widening nor narrowing)
      case LongToFloat =>
        fb += wa.F32ConvertI64S

      // String.length
      case String_length =>
        fb += wa.Call(genFunctionID.stringBuiltins.length)

      // Null check
      case CheckNotNull =>
        genAsNonNullOrNPEFor(lhs)

      // Class operations
      case Class_name =>
        fb += wa.StructGet(genTypeID.ClassStruct, genFieldID.classData)
        fb += wa.Call(genFunctionID.typeDataName)
      case Class_isPrimitive =>
        fb += wa.StructGet(genTypeID.ClassStruct, genFieldID.classData)
        fb += wa.StructGet(genTypeID.typeData, genFieldID.typeData.kind)
        fb += wa.I32Const(KindLastPrimitive)
        fb += wa.I32LeU
      case Class_isInterface =>
        fb += wa.StructGet(genTypeID.ClassStruct, genFieldID.classData)
        fb += wa.StructGet(genTypeID.typeData, genFieldID.typeData.kind)
        fb += wa.I32Const(KindInterface)
        fb += wa.I32Eq
      case Class_isArray =>
        fb += wa.StructGet(genTypeID.ClassStruct, genFieldID.classData)
        fb += wa.StructGet(genTypeID.typeData, genFieldID.typeData.kind)
        fb += wa.I32Const(KindArray)
        fb += wa.I32Eq
      case Class_componentType =>
        fb += wa.Call(genFunctionID.getComponentType)
      case Class_superClass =>
        fb += wa.Call(genFunctionID.getSuperClass)
    }

    tree.tpe
  }

  private def genBinaryOp(tree: BinaryOp): Type = {
    import BinaryOp._

    val BinaryOp(op, lhs, rhs) = tree

    def genLongShiftOp(shiftInstr: wa.Instr): Type = {
      genTree(lhs, LongType)
      genTree(rhs, IntType)
      markPosition(tree)
      fb += wa.I64ExtendI32S
      fb += shiftInstr
      LongType
    }

    /* Gen the given tree of type `jl.Class!` then extract its `classData`.
     * If the arg is a literal `ClassOf`, we directly generate its type data.
     */
    def genTreeClassData(arg: Tree): Unit = {
      arg match {
        case ClassOf(typeRef) =>
          markPosition(arg)
          genLoadTypeData(fb, typeRef)
        case _ =>
          genTreeAuto(arg)
          fb += wa.StructGet(genTypeID.ClassStruct, genFieldID.classData)
      }
    }

    (op: @switch) match {
      case === | !== =>
        genEq(tree)

      case String_+ =>
        genStringConcat(tree)

      case Int_/ =>
        rhs match {
          case IntLiteral(rhsValue) =>
            genDivModByConstant(tree, isDiv = true, rhsValue, wa.I32Const(_), wa.I32Sub, wa.I32DivS)
          case _ =>
            genDivMod(tree, isDiv = true, wa.I32Const(_), wa.I32Eqz, wa.I32Eq, wa.I32Sub, wa.I32DivS)
        }
      case Int_% =>
        rhs match {
          case IntLiteral(rhsValue) =>
            genDivModByConstant(tree, isDiv = false, rhsValue, wa.I32Const(_), wa.I32Sub, wa.I32RemS)
          case _ =>
            genDivMod(tree, isDiv = false, wa.I32Const(_), wa.I32Eqz, wa.I32Eq, wa.I32Sub, wa.I32RemS)
        }
      case Long_/ =>
        rhs match {
          case LongLiteral(rhsValue) =>
            genDivModByConstant(tree, isDiv = true, rhsValue, wa.I64Const(_), wa.I64Sub, wa.I64DivS)
          case _ =>
            genDivMod(tree, isDiv = true, wa.I64Const(_), wa.I64Eqz, wa.I64Eq, wa.I64Sub, wa.I64DivS)
        }
      case Long_% =>
        rhs match {
          case LongLiteral(rhsValue) =>
            genDivModByConstant(tree, isDiv = false, rhsValue, wa.I64Const(_), wa.I64Sub, wa.I64RemS)
          case _ =>
            genDivMod(tree, isDiv = false, wa.I64Const(_), wa.I64Eqz, wa.I64Eq, wa.I64Sub, wa.I64RemS)
        }

      case Long_<< =>
        genLongShiftOp(wa.I64Shl)
      case Long_>>> =>
        genLongShiftOp(wa.I64ShrU)
      case Long_>> =>
        genLongShiftOp(wa.I64ShrS)

      /* Floating point remainders are specified by
       * https://262.ecma-international.org/#sec-numeric-types-number-remainder
       * which says that it is equivalent to the C library function `fmod`.
       * For `Float`s, we promote and demote to `Double`s.
       * `fmod` seems quite hard to correctly implement, so we delegate to a
       * JavaScript Helper.
       * (The naive function `x - trunc(x / y) * y` that we can find on the
       * Web does not work.)
       */
      case Float_% =>
        genTree(lhs, FloatType)
        fb += wa.F64PromoteF32
        genTree(rhs, FloatType)
        fb += wa.F64PromoteF32
        markPosition(tree)
        fb += wa.Call(genFunctionID.fmod)
        fb += wa.F32DemoteF64
        FloatType
      case Double_% =>
        genTree(lhs, DoubleType)
        genTree(rhs, DoubleType)
        markPosition(tree)
        fb += wa.Call(genFunctionID.fmod)
        DoubleType

      case String_charAt =>
        genTree(lhs, StringType)
        genTree(rhs, IntType)
        markPosition(tree)
        if (semantics.stringIndexOutOfBounds == CheckedBehavior.Unchecked)
          fb += wa.Call(genFunctionID.stringBuiltins.charCodeAt)
        else
          fb += wa.Call(genFunctionID.checkedStringCharAt)
        CharType

      // Class operations for which genTreeAuto would not do the right thing
      case Class_isInstance =>
        genTreeClassData(lhs)
        genTree(rhs, AnyType)
        markPosition(tree)
        fb += wa.Call(genFunctionID.isInstance)
        BooleanType
      case Class_isAssignableFrom =>
        genTreeClassData(lhs)
        genTreeClassData(rhs)
        markPosition(tree)
        fb += wa.Call(genFunctionID.isAssignableFrom)
        BooleanType
      case Class_cast =>
        if (semantics.asInstanceOfs != CheckedBehavior.Unchecked) {
          genTreeAuto(lhs)
          genTree(rhs, AnyType)
          markPosition(tree)
          fb += wa.Call(genFunctionID.cast)
          AnyType
        } else {
          genTree(lhs, NoType)
          genTreeAuto(rhs)
          rhs.tpe
        }

      case _ =>
        genTreeAuto(lhs)
        genTreeAuto(rhs)
        markPosition(tree)
        fb += getElementaryBinaryOpInstr(op)
        tree.tpe
    }
  }

  private def genEq(tree: BinaryOp): Type = {
    import BinaryOp.{===, !==}

    val BinaryOp(op, lhs, rhs) = tree
    assert(op == === || op == !==)

    def maybeGenInvert(): Unit = {
      if (op == BinaryOp.!==)
        fb += wa.I32Eqz
    }

    /* Can we use `ref.eq` for the given type?
     *
     * This is the case if the Wasm encoding of the given type a subtype of
     * `eqref`, i.e., `(ref null eq)`.
     *
     * This requires that it be a ref type of the form `(ref null? heapType)`
     * and that the `heapType` be a sub-heap-type of `eq`.
     *
     * Note that all the `HeapType.Type(_)`s returned by `transformSingleType`
     * point to struct types, which are sub-heap-types of `eq`. (In general
     * this is not true, since they could also point to `func` heap types,
     * which are not sub-heap-types of `eq`.)
     *
     * Therefore, in practice, the only heap types we can observe and that are
     * *not* sub-heap-types of `eq` are `any` and `extern`.
     */
    def canUseRefEq(tpe: Type): Boolean = transformSingleType(tpe) match {
      case watpe.RefType(_, heapType) =>
        heapType != watpe.HeapType.Any && heapType != watpe.HeapType.Extern
      case _ =>
        false
    }

    def isStringType(tpe: Type): Boolean = tpe match {
      case StringType                     => true
      case ClassType(BoxedStringClass, _) => true
      case _                              => false
    }

    val lhsType = lhs.tpe
    val rhsType = rhs.tpe

    if (lhsType == NothingType) {
      genTree(lhs, NothingType)
      NothingType
    } else if (rhsType == NothingType) {
      genTree(lhs, NoType)
      genTree(rhs, NothingType)
      NothingType
    } else if (rhsType == NullType) {
      /* Note that the optimizer normalizes Literals on the right of `===`,
       * so testing for the `lhsType == NullType` is not as useful.
       */
      genTree(lhs, AnyType)
      genTree(rhs, NoType) // no-op if it is actually a Null() literal
      markPosition(tree)
      fb += wa.RefIsNull
      maybeGenInvert()
      BooleanType
    } else if (canUseRefEq(lhsType) && canUseRefEq(rhsType)) {
      /* When both types translate to Wasm types that are subtypes of `eqref`,
       * we can use `ref.eq`. Note that for all possible `eqref`s (in all of
       * Wasm, not just the subset that we use), `Object.is` coincides with
       * `ref.eq`. So this is a sound optimization over using `Object.is`.
       */
      genTree(lhs, lhsType)
      genTree(rhs, rhsType)
      markPosition(tree)
      fb += wa.RefEq
      maybeGenInvert()
      BooleanType
    } else if (isStringType(lhsType) && isStringType(rhsType)) {
      genTreeAuto(lhs)
      genTreeAuto(rhs)
      markPosition(tree)
      fb += wa.Call(genFunctionID.stringBuiltins.equals)
      maybeGenInvert()
      BooleanType
    } else {
      // Otherwise, fall back on the `Object.is` helper
      genTree(lhs, AnyType)
      genTree(rhs, AnyType)
      markPosition(tree)
      fb += wa.Call(genFunctionID.is)
      maybeGenInvert()
      BooleanType
    }
  }

  private def getElementaryBinaryOpInstr(op: BinaryOp.Code): wa.Instr = {
    import BinaryOp._

    (op: @switch) match {
      case Boolean_== => wa.I32Eq
      case Boolean_!= => wa.I32Ne
      case Boolean_|  => wa.I32Or
      case Boolean_&  => wa.I32And

      case Int_+   => wa.I32Add
      case Int_-   => wa.I32Sub
      case Int_*   => wa.I32Mul
      case Int_|   => wa.I32Or
      case Int_&   => wa.I32And
      case Int_^   => wa.I32Xor
      case Int_<<  => wa.I32Shl
      case Int_>>> => wa.I32ShrU
      case Int_>>  => wa.I32ShrS
      case Int_==  => wa.I32Eq
      case Int_!=  => wa.I32Ne
      case Int_<   => wa.I32LtS
      case Int_<=  => wa.I32LeS
      case Int_>   => wa.I32GtS
      case Int_>=  => wa.I32GeS

      case Long_+ => wa.I64Add
      case Long_- => wa.I64Sub
      case Long_* => wa.I64Mul
      case Long_| => wa.I64Or
      case Long_& => wa.I64And
      case Long_^ => wa.I64Xor

      case Long_== => wa.I64Eq
      case Long_!= => wa.I64Ne
      case Long_<  => wa.I64LtS
      case Long_<= => wa.I64LeS
      case Long_>  => wa.I64GtS
      case Long_>= => wa.I64GeS

      case Float_+ => wa.F32Add
      case Float_- => wa.F32Sub
      case Float_* => wa.F32Mul
      case Float_/ => wa.F32Div

      case Double_+ => wa.F64Add
      case Double_- => wa.F64Sub
      case Double_* => wa.F64Mul
      case Double_/ => wa.F64Div

      case Double_== => wa.F64Eq
      case Double_!= => wa.F64Ne
      case Double_<  => wa.F64Lt
      case Double_<= => wa.F64Le
      case Double_>  => wa.F64Gt
      case Double_>= => wa.F64Ge

      case Class_newArray => wa.Call(genFunctionID.newArray)
    }
  }

  private def genStringConcat(tree: BinaryOp): Type = {
    val BinaryOp(op, lhs, rhs) = tree
    assert(op == BinaryOp.String_+)

    lhs match {
      case StringLiteral("") =>
        // Common case where we don't actually need a concatenation
        genToStringForConcat(rhs)

      case _ =>
        genToStringForConcat(lhs)
        genToStringForConcat(rhs)
        markPosition(tree)
        fb += wa.Call(genFunctionID.stringBuiltins.concat)
    }

    StringType
  }

  private def genToStringForConcat(tree: Tree): Unit = {
    def genWithDispatch(isAncestorOfHijackedClass: Boolean): Unit = {
      // TODO Better codegen when non-nullable

      /* Somewhat duplicated from genApplyNonPrim, but specialized for
       * `toString`, and where the handling of `null` is different.
       *
       * We need to return the `"null"` string in two special cases:
       * - if the value itself is `null`, or
       * - if the value's `toString(): String` method returns `null`!
       */

      // A local for a copy of the receiver that we will use to resolve dispatch
      val receiverLocalForDispatch =
        addSyntheticLocal(watpe.RefType(genTypeID.ObjectStruct))

      val objectClassInfo = ctx.getClassInfo(ObjectClass)

      if (!isAncestorOfHijackedClass) {
        /* Standard dispatch codegen, with dedicated null handling.
         *
         * The overall structure of the generated code is as follows:
         *
         * block (ref extern) $done
         *   block $isNull
         *     load receiver as (ref null java.lang.Object)
         *     br_on_null $isNull
         *     generate standard table-based dispatch
         *     br_on_non_null $done
         *   end $isNull
         *   gen "null"
         * end $done
         */

        fb.block(watpe.RefType.extern) { labelDone =>
          fb.block() { labelIsNull =>
            genTreeAuto(tree)
            markPosition(tree)
            fb += wa.BrOnNull(labelIsNull)
            fb += wa.LocalTee(receiverLocalForDispatch)
            genTableDispatch(objectClassInfo, toStringMethodName, receiverLocalForDispatch)
            fb += wa.BrOnNonNull(labelDone)
          }

          fb ++= ctx.stringPool.getConstantStringInstr("null")
        }
      } else {
        /* Dispatch where the receiver can be a JS value.
         *
         * The overall structure of the generated code is as follows:
         *
         * block (ref extern) $done
         *   block anyref $notOurObject
         *     load receiver
         *     br_on_cast_fail anyref (ref $java.lang.Object) $notOurObject
         *     generate standard table-based dispatch
         *     br_on_non_null $done
         *     ref.null any
         *   end $notOurObject
         *   call the JS helper, also handles `null`
         * end $done
         */

        fb.block(watpe.RefType.extern) { labelDone =>
          // First try the case where the value is one of our objects
          fb.block(watpe.RefType.anyref) { labelNotOurObject =>
            // Load receiver
            genTreeAuto(tree)

            markPosition(tree)

            fb += wa.BrOnCastFail(
              labelNotOurObject,
              watpe.RefType.anyref,
              watpe.RefType(genTypeID.ObjectStruct)
            )
            fb += wa.LocalTee(receiverLocalForDispatch)
            genTableDispatch(objectClassInfo, toStringMethodName, receiverLocalForDispatch)
            fb += wa.BrOnNonNull(labelDone)
            fb += wa.RefNull(watpe.HeapType.Any)
          } // end block labelNotOurObject

          // Now we have a value that is not one of our objects; the anyref is still on the stack
          fb += wa.Call(genFunctionID.jsValueToStringForConcat)
        } // end block labelDone
      }
    }

    tree.tpe match {
      case primType: PrimType =>
        genTreeAuto(tree)

        markPosition(tree)

        primType match {
          case StringType =>
            () // no-op
          case BooleanType =>
            fb += wa.Call(genFunctionID.booleanToString)
          case CharType =>
            fb += wa.Call(genFunctionID.stringBuiltins.fromCharCode)
          case ByteType | ShortType | IntType =>
            fb += wa.Call(genFunctionID.intToString)
          case LongType =>
            fb += wa.Call(genFunctionID.longToString)
          case FloatType =>
            fb += wa.F64PromoteF32
            fb += wa.Call(genFunctionID.doubleToString)
          case DoubleType =>
            fb += wa.Call(genFunctionID.doubleToString)
          case NullType | UndefType =>
            fb += wa.Call(genFunctionID.jsValueToStringForConcat)
          case NothingType =>
            () // unreachable
          case NoType =>
            throw new AssertionError(
                s"Found expression of type void in String_+ at ${tree.pos}: $tree")
        }

      case ClassType(BoxedStringClass, nullable) =>
        // Common case for which we want to avoid the hijacked class dispatch
        if (nullable) {
          fb.block(watpe.RefType.extern) { notNullLabel =>
            genTreeAuto(tree)
            markPosition(tree)
            fb += wa.BrOnNonNull(notNullLabel)
            fb ++= ctx.stringPool.getConstantStringInstr("null")
          }
        } else {
          genTreeAuto(tree)
        }

      case ClassType(className, _) =>
        genWithDispatch(ctx.getClassInfo(className).isAncestorOfHijackedClass)

      case AnyType | AnyNotNullType =>
        genWithDispatch(isAncestorOfHijackedClass = true)

      case ArrayType(_, _) =>
        genWithDispatch(isAncestorOfHijackedClass = false)

      case tpe: RecordType =>
        throw new AssertionError(
            s"Invalid type $tpe for String_+ at ${tree.pos}: $tree")
    }
  }

  private def genDivModByConstant[T](tree: BinaryOp, isDiv: Boolean,
      rhsValue: T, const: T => wa.Instr, sub: wa.Instr, mainOp: wa.Instr)(
      implicit num: Numeric[T]): Type = {
    /* When we statically know the value of the rhs, we can avoid the
     * dynamic tests for division by zero and overflow. This is quite
     * common in practice.
     */

    import BinaryOp._

    val BinaryOp(op, lhs, rhs) = tree
    assert(op == Int_/ || op == Int_% || op == Long_/ || op == Long_%)

    val tpe = tree.tpe

    if (rhsValue == num.zero) {
      genTree(lhs, tpe)
      markPosition(tree)
      genThrowArithmeticException()(tree.pos)
      NothingType
    } else if (isDiv && rhsValue == num.fromInt(-1)) {
      /* MinValue / -1 overflows; it traps in Wasm but we need to wrap.
       * We rewrite as `0 - lhs` so that we do not need any test.
       */
      markPosition(tree)
      fb += const(num.zero)
      genTree(lhs, tpe)
      markPosition(tree)
      fb += sub
      tpe
    } else {
      genTree(lhs, tpe)
      markPosition(rhs)
      fb += const(rhsValue)
      markPosition(tree)
      fb += mainOp
      tpe
    }
  }

  private def genDivMod[T](tree: BinaryOp, isDiv: Boolean, const: T => wa.Instr,
      eqz: wa.Instr, eqInstr: wa.Instr, sub: wa.Instr, mainOp: wa.Instr)(
      implicit num: Numeric[T]): Type = {
    /* Here we perform the same steps as in the static case, but using
     * value tests at run-time.
     */

    import BinaryOp._

    val BinaryOp(op, lhs, rhs) = tree
    assert(op == Int_/ || op == Int_% || op == Long_/ || op == Long_%)

    val tpe = tree.tpe.asInstanceOf[PrimType]
    val wasmType = transformPrimType(tpe)

    val lhsLocal = addSyntheticLocal(wasmType)
    val rhsLocal = addSyntheticLocal(wasmType)
    genTree(lhs, tpe)
    fb += wa.LocalSet(lhsLocal)
    genTree(rhs, tpe)
    fb += wa.LocalTee(rhsLocal)

    markPosition(tree)

    fb += eqz
    fb.ifThen() {
      genThrowArithmeticException()(tree.pos)
    }
    if (isDiv) {
      // Handle the MinValue / -1 corner case
      fb += wa.LocalGet(rhsLocal)
      fb += const(num.fromInt(-1))
      fb += eqInstr
      fb.ifThenElse(wasmType) {
        // 0 - lhs
        fb += const(num.zero)
        fb += wa.LocalGet(lhsLocal)
        fb += sub
      } {
        // lhs / rhs
        fb += wa.LocalGet(lhsLocal)
        fb += wa.LocalGet(rhsLocal)
        fb += mainOp
      }
    } else {
      // lhs % rhs
      fb += wa.LocalGet(lhsLocal)
      fb += wa.LocalGet(rhsLocal)
      fb += mainOp
    }

    tpe
  }

  private def genThrowArithmeticException()(implicit pos: Position): Unit = {
    val ctorName = MethodName.constructor(List(ClassRef(BoxedStringClass)))
    genNewScalaClass(ArithmeticExceptionClass, ctorName) {
      fb ++= ctx.stringPool.getConstantStringInstr("/ by zero")
    }
    fb += wa.ExternConvertAny
    fb += wa.Throw(genTagID.exception)
  }

  private def genIsInstanceOf(tree: IsInstanceOf): Type = {
    val IsInstanceOf(expr, testType) = tree

    genTree(expr, AnyType)

    markPosition(tree)

    def genIsPrimType(testType: PrimType): Unit = testType match {
      case UndefType =>
        fb += wa.Call(genFunctionID.isUndef)
      case StringType =>
        fb += wa.ExternConvertAny
        fb += wa.Call(genFunctionID.stringBuiltins.test)
      case CharType =>
        val structTypeID = genTypeID.forClass(SpecialNames.CharBoxClass)
        fb += wa.RefTest(watpe.RefType(structTypeID))
      case LongType =>
        val structTypeID = genTypeID.forClass(SpecialNames.LongBoxClass)
        fb += wa.RefTest(watpe.RefType(structTypeID))
      case NoType | NothingType | NullType =>
        throw new AssertionError(s"Illegal isInstanceOf[$testType]")
      case testType: PrimTypeWithRef =>
        fb += wa.Call(genFunctionID.typeTest(testType.primRef))
    }

    testType match {
      case testType: PrimType =>
        genIsPrimType(testType)

      case AnyNotNullType | ClassType(ObjectClass, false) =>
        fb += wa.RefIsNull
        fb += wa.I32Eqz

      case ClassType(JLNumberClass, false) =>
        /* Special case: the only non-Object *class* that is an ancestor of a
         * hijacked class. We need to accept `number` primitives here.
         */
        val tempLocal = addSyntheticLocal(watpe.RefType.anyref)
        fb += wa.LocalTee(tempLocal)
        fb += wa.RefTest(watpe.RefType(genTypeID.forClass(JLNumberClass)))
        fb.ifThenElse(watpe.Int32) {
          fb += wa.I32Const(1)
        } {
          fb += wa.LocalGet(tempLocal)
          fb += wa.Call(genFunctionID.typeTest(DoubleRef))
        }

      case ClassType(testClassName, false) =>
        BoxedClassToPrimType.get(testClassName) match {
          case Some(primType) =>
            genIsPrimType(primType)
          case None =>
            if (ctx.getClassInfo(testClassName).isInterface)
              fb += wa.Call(genFunctionID.instanceTest(testClassName))
            else
              fb += wa.RefTest(watpe.RefType(genTypeID.forClass(testClassName)))
        }

      case ArrayType(arrayTypeRef, false) =>
        arrayTypeRef match {
          case ArrayTypeRef(ClassRef(ObjectClass) | _: PrimRef, 1) =>
            // For primitive arrays and exactly Array[Object], a wa.RefTest is enough
            val structTypeID = genTypeID.forArrayClass(arrayTypeRef)
            fb += wa.RefTest(watpe.RefType(structTypeID))

          case _ =>
            /* Non-Object reference array types need a sophisticated type test
             * based on assignability of component types.
             */
            import watpe.RefType.anyref

            fb.block(Sig(List(anyref), List(watpe.Int32))) { doneLabel =>
              fb.block(Sig(List(anyref), List(anyref))) { notARefArrayLabel =>
                // Try and cast to the generic representation first
                val refArrayStructTypeID = genTypeID.forArrayClass(arrayTypeRef)
                fb += wa.BrOnCastFail(
                  notARefArrayLabel,
                  watpe.RefType.anyref,
                  watpe.RefType(refArrayStructTypeID)
                )

                // refArrayValue := the generic representation
                val refArrayValueLocal =
                  addSyntheticLocal(watpe.RefType(refArrayStructTypeID))
                fb += wa.LocalSet(refArrayValueLocal)

                // Load typeDataOf(arrayTypeRef)
                genLoadArrayTypeData(fb, arrayTypeRef)

                // Load refArrayValue.vtable
                fb += wa.LocalGet(refArrayValueLocal)
                fb += wa.StructGet(refArrayStructTypeID, genFieldID.objStruct.vtable)

                // Call isAssignableFrom and return its result
                fb += wa.Call(genFunctionID.isAssignableFrom)
                fb += wa.Br(doneLabel)
              }

              // Here, the value is not a reference array type, so return false
              fb += wa.Drop
              fb += wa.I32Const(0)
            }
        }

      case AnyType | ClassType(_, true) | ArrayType(_, true) | _:RecordType =>
        throw new AssertionError(s"Illegal type in IsInstanceOf: $testType")
    }

    BooleanType
  }

  private def genAsInstanceOf(tree: AsInstanceOf): Type = {
    val AsInstanceOf(expr, targetTpe) = tree

    if (semantics.asInstanceOfs == CheckedBehavior.Unchecked)
      genCast(expr, targetTpe, tree.pos)
    else
      genCheckedCast(expr, targetTpe, tree.pos)
  }

  private def genCheckedCast(expr: Tree, targetTpe: Type, pos: Position): Type = {
    genTree(expr, AnyType)

    markPosition(pos)

    targetTpe match {
      case AnyType | ClassType(ObjectClass, true) =>
        // no-op
        ()

      case ArrayType(arrayTypeRef, true) =>
        arrayTypeRef match {
          case ArrayTypeRef(ClassRef(ObjectClass) | _: PrimRef, 1) =>
            // For primitive arrays and exactly Array[Object], we have a dedicated function
            fb += wa.Call(genFunctionID.asInstance(targetTpe))
          case _ =>
            // For other array types, we must use the generic function
            genLoadArrayTypeData(fb, arrayTypeRef)
            fb += wa.Call(genFunctionID.asSpecificRefArray)
        }

      case _ =>
        fb += wa.Call(genFunctionID.asInstance(targetTpe))
    }

    targetTpe
  }

  private def genCast(expr: Tree, targetTpe: Type, pos: Position): Type = {
    val sourceTpe = expr.tpe

    /* We cannot call `transformSingleType` for NothingType, so we have to
     * handle these cases separately.
     */

    if (sourceTpe == NothingType) {
      genTree(expr, NothingType)
      NothingType
    } else if (targetTpe == NothingType) {
      genTree(expr, NoType)
      fb += wa.Unreachable
      NothingType
    } else {
      /* At this point, neither sourceTpe nor targetTpe can be NothingType,
       * NoType or RecordType, so we can use `transformSingleType`.
       */

      val sourceWasmType = transformSingleType(sourceTpe)
      val targetWasmType = transformSingleType(targetTpe)

      (sourceWasmType, targetWasmType) match {
        case _ if sourceWasmType == targetWasmType =>
          /* Common case where no cast is necessary at the Wasm level.
           * Note that this is not *obviously* correct. It is only correct
           * because, under our choices of representation and type translation
           * rules, there is no pair `(sourceTpe, targetTpe)` for which the Wasm
           * types are equal but a valid cast would require a *conversion*.
           */
          genTreeAuto(expr)

        case (watpe.RefType(true, sourceHeapType), watpe.RefType(false, targetHeapType))
            if sourceHeapType == targetHeapType =>
          /* Similar but here we need to cast away nullability. This shape of
           * Cast is a common case for checkNotNull's inserted by the optimizer
           * when null pointers are unchecked.
           */
          genTreeAuto(expr)
          markPosition(pos)
          fb += wa.RefAsNonNull

        case _ =>
          genTree(expr, AnyType)

          markPosition(pos)

          targetTpe match {
            case targetTpe: PrimType =>
              // TODO Opt: We could do something better for things like double.asInstanceOf[int]
              genUnbox(targetTpe)

            case _ =>
              targetWasmType match {
                case watpe.RefType(true, watpe.HeapType.Any) =>
                  () // nothing to do
                case watpe.RefType(targetNullable, watpe.HeapType.Extern) =>
                  fb += wa.ExternConvertAny
                  if (!targetNullable)
                    fb += wa.RefAsNonNull
                case targetWasmType: watpe.RefType =>
                  fb += wa.RefCast(targetWasmType)
                case _ =>
                  throw new AssertionError(s"Unexpected type in AsInstanceOf: $targetTpe")
              }
          }
      }

      targetTpe
    }
  }

  /** Unbox the `anyref` on the stack to the target `PrimType`.
   *
   *  `targetTpe` must not be `NothingType`, `NullType` nor `NoType`.
   *
   *  The type left on the stack is non-nullable.
   */
  private def genUnbox(targetTpe: PrimType): Unit = {
    targetTpe match {
      case UndefType =>
        fb += wa.Drop
        fb += wa.GlobalGet(genGlobalID.undef)

      case StringType =>
        fb += wa.ExternConvertAny
        val sig = watpe.FunctionType(List(watpe.RefType.externref), List(watpe.RefType.extern))
        fb.block(sig) { nonNullLabel =>
          fb += wa.BrOnNonNull(nonNullLabel)
          fb += wa.GlobalGet(genGlobalID.emptyString)
        }

      case CharType | LongType =>
        // Extract the `value` field (the only field) out of the box class.

        val boxClass =
          if (targetTpe == CharType) SpecialNames.CharBoxClass
          else SpecialNames.LongBoxClass
        val fieldName = FieldName(boxClass, SpecialNames.valueFieldSimpleName)
        val resultType = transformPrimType(targetTpe)

        fb.block(Sig(List(watpe.RefType.anyref), List(resultType))) { doneLabel =>
          fb.block(Sig(List(watpe.RefType.anyref), Nil)) { isNullLabel =>
            fb += wa.BrOnNull(isNullLabel)
            val structTypeID = genTypeID.forClass(boxClass)
            fb += wa.RefCast(watpe.RefType(structTypeID))
            fb += wa.StructGet(
              structTypeID,
              genFieldID.forClassInstanceField(fieldName)
            )
            fb += wa.Br(doneLabel)
          }
          fb += genZeroOf(targetTpe)
        }

      case NothingType | NullType | NoType =>
        throw new IllegalArgumentException(s"Illegal type in genUnbox: $targetTpe")

      case targetTpe: PrimTypeWithRef =>
        fb += wa.Call(genFunctionID.unbox(targetTpe.primRef))
    }
  }

  private def genGetClass(tree: GetClass): Type = {
    /* Unlike in `genApply` or `genStringConcat`, here we make no effort to
     * optimize known-primitive receivers. In practice, such cases would be
     * useless.
     */

    val GetClass(expr) = tree

    val needHijackedClassDispatch = expr.tpe match {
      case ClassType(className, _) =>
        ctx.getClassInfo(className).isAncestorOfHijackedClass
      case ArrayType(_, _) | NothingType | NullType =>
        false
      case _ =>
        true
    }

    if (!needHijackedClassDispatch) {
      val typeDataLocal = addSyntheticLocal(watpe.RefType(genTypeID.typeData))

      genTreeAuto(expr)
      markPosition(tree)
      genCheckNonNullFor(expr)
      fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable)
      fb += wa.Call(genFunctionID.getClassOf)
    } else {
      genTreeToAny(expr)
      markPosition(tree)
      genAsNonNullOrNPEFor(expr)
      fb += wa.Call(genFunctionID.anyGetClass)
    }

    tree.tpe
  }

  private def genReadStorage(storage: VarStorage): Unit = {
    storage match {
      case VarStorage.Local(localID) =>
        fb += wa.LocalGet(localID)
      case VarStorage.LocalRecord(fields) =>
        for ((_, fieldStorage) <- fields)
          genReadStorage(fieldStorage)
      case VarStorage.StructField(structLocal, structTypeID, fieldID) =>
        fb += wa.LocalGet(structLocal)
        fb += wa.StructGet(structTypeID, fieldID)
    }
  }

  private def genVarRef(tree: VarRef): Type = {
    val VarRef(LocalIdent(name)) = tree

    markPosition(tree)
    if (tree.tpe == NothingType)
      fb += wa.Unreachable
    else
      genReadStorage(lookupLocal(name))
    tree.tpe
  }

  private def genThis(tree: This): Type = {
    markPosition(tree)
    genReadStorage(receiverStorage)
    tree.tpe
  }

  private def genVarDef(tree: VarDef): Type = {
    /* This is an isolated VarDef that is not in a Block.
     * Its scope is empty by construction, and therefore it need not be stored.
     */
    val VarDef(_, _, _, _, rhs) = tree
    genTree(rhs, NoType)
    NoType
  }

  private def genIf(tree: If, expectedType: Type): Type = {
    val If(cond, thenp, elsep) = tree

    val ty = transformResultType(expectedType)
    genTree(cond, BooleanType)

    markPosition(tree)

    elsep match {
      case Skip() =>
        assert(expectedType == NoType)
        fb.ifThen() {
          genTree(thenp, expectedType)
        }
      case _ =>
        fb.ifThenElse(ty) {
          genTree(thenp, expectedType)
        } {
          genTree(elsep, expectedType)
        }
    }

    if (expectedType == NothingType)
      fb += wa.Unreachable

    expectedType
  }

  private def genWhile(tree: While): Type = {
    val While(cond, body) = tree

    cond match {
      case BooleanLiteral(true) =>
        // infinite loop that must be typed as `nothing`, i.e., unreachable
        markPosition(tree)
        fb.loop() { label =>
          genTree(body, NoType)
          markPosition(tree)
          fb += wa.Br(label)
        }
        fb += wa.Unreachable
        NothingType

      case _ =>
        // normal loop typed as `void`
        markPosition(tree)
        fb.loop() { label =>
          genTree(cond, BooleanType)
          markPosition(tree)
          fb.ifThen() {
            genTree(body, NoType)
            markPosition(tree)
            fb += wa.Br(label)
          }
        }
        NoType
    }
  }

  private def genForIn(tree: ForIn): Type = {
    /* This is tricky. In general, the body of a ForIn can be an arbitrary
     * statement, which can refer to the enclosing scope and its locals,
     * including for mutations. Unfortunately, there is no way to implement a
     * ForIn other than actually doing a JS `for (var key in obj) { body }`
     * loop. That means we need to pass the `body` as a JS closure.
     *
     * That is problematic for our backend because we basically need to perform
     * lambda lifting: identifying captures ourselves, and turn references to
     * local variables into accessing the captured environment.
     *
     * We side-step this issue for now by exploiting the known shape of `ForIn`
     * generated by the Scala.js compiler. This is fine as long as we do not
     * support the Scala.js optimizer. We will have to revisit this code when
     * we add that support.
     */

    val ForIn(obj, LocalIdent(keyVarName), _, body) = tree

    body match {
      case JSFunctionApply(fVarRef: VarRef, List(VarRef(argIdent)))
          if fVarRef.ident.name != keyVarName && argIdent.name == keyVarName =>
        genTree(obj, AnyType)
        genTree(fVarRef, AnyType)
        markPosition(tree)
        fb += wa.Call(genFunctionID.jsForInSimple)

      case _ =>
        throw new NotImplementedError(s"Unsupported shape of ForIn node at ${tree.pos}: $tree")
    }

    NoType
  }

  private def genTryCatch(tree: TryCatch, expectedType: Type): Type = {
    val TryCatch(block, LocalIdent(errVarName), errVarOrigName, handler) = tree

    val resultType = transformResultType(expectedType)

    if (UseLegacyExceptionsForTryCatch) {
      markPosition(tree)
      fb += wa.Try(fb.sigToBlockType(Sig(Nil, resultType)))
      withNPEScope(resultType) {
        genTree(block, expectedType)
      }
      markPosition(tree)
      fb += wa.Catch(genTagID.exception)
      withNewLocal(errVarName, errVarOrigName, watpe.RefType.anyref) { exceptionLocal =>
        fb += wa.AnyConvertExtern
        fb += wa.LocalSet(exceptionLocal)
        genTree(handler, expectedType)
      }
      fb += wa.End
    } else {
      markPosition(tree)
      fb.block(resultType) { doneLabel =>
        fb.block(watpe.RefType.externref) { catchLabel =>
          /* We used to have `resultType` as result of the try_table, with the
           * `wa.BR(doneLabel)` outside of the try_table. Unfortunately it seems
           * V8 cannot handle try_table with a result type that is `(ref ...)`.
           * The current encoding with `externref` as result type (to match the
           * enclosing block) and the `br` *inside* the `try_table` works.
           */
          fb.tryTable(watpe.RefType.externref)(
            List(wa.CatchClause.Catch(genTagID.exception, catchLabel))
          ) {
            withNPEScope(resultType) {
              genTree(block, expectedType)
            }
            markPosition(tree)
            fb += wa.Br(doneLabel)
          }
        } // end block $catch
        withNewLocal(errVarName, errVarOrigName, watpe.RefType.anyref) { exceptionLocal =>
          fb += wa.AnyConvertExtern
          fb += wa.LocalSet(exceptionLocal)
          genTree(handler, expectedType)
        }
      } // end block $done
    }

    if (expectedType == NothingType)
      fb += wa.Unreachable

    expectedType
  }

  private def genThrow(tree: Throw): Type = {
    val Throw(expr) = tree

    genTree(expr, AnyType)
    markPosition(tree)
    fb += wa.ExternConvertAny
    fb += wa.Throw(genTagID.exception)

    NothingType
  }

  private def genBlock(tree: Block, expectedType: Type): Type = {
    val Block(stats) = tree

    genBlockStats(stats.init) {
      genTree(stats.last, expectedType)
    }
    expectedType
  }

  final def genBlockStats(stats: List[Tree])(inner: => Unit): Unit = {
    val savedEnv = currentEnv

    def buildStorage(origName: UTF8String, vtpe: Type): VarStorage.NonStructStorage = vtpe match {
      case RecordType(fields) =>
        val fieldStorages = fields.map { field =>
          val fieldOrigName =
            origName ++ dotUTF8String ++ field.originalName.getOrElse(field.name)
          field.name -> buildStorage(fieldOrigName, field.tpe)
        }
        VarStorage.LocalRecord(fieldStorages.toVector)
      case _ =>
        val wasmType =
          if (vtpe == NothingType) watpe.Int32
          else transformSingleType(vtpe)
        val local = fb.addLocal(OriginalName(origName), wasmType)
        VarStorage.Local(local)
    }

    for (stat <- stats) {
      stat match {
        case VarDef(LocalIdent(name), originalName, vtpe, _, rhs) =>
          genTree(rhs, vtpe)
          markPosition(stat)
          val storage = buildStorage(originalName.getOrElse(name), vtpe)
          currentEnv = currentEnv.updated(name, storage)
          genWriteToStorage(storage)

        case _ =>
          genTree(stat, NoType)
      }
    }

    inner

    currentEnv = savedEnv
  }

  private def genNew(tree: New): Type = {
    val New(className, MethodIdent(ctorName), args) = tree

    genNewScalaClass(className, ctorName) {
      genArgs(args, ctorName)
    } (tree.pos)

    tree.tpe
  }

  private def genNewScalaClass(cls: ClassName, ctor: MethodName)(
      genCtorArgs: => Unit)(implicit pos: Position): Unit = {

    /* Do not use transformType here, because we must get the struct type even
     * if the given class is an ancestor of hijacked classes (which in practice
     * is only the case for j.l.Object).
     */
    val instanceLocal = addSyntheticLocal(watpe.RefType(genTypeID.forClass(cls)))

    markPosition(pos)
    fb += wa.Call(genFunctionID.newDefault(cls))
    fb += wa.LocalTee(instanceLocal)
    genCtorArgs
    markPosition(pos)
    fb += wa.Call(genFunctionID.forMethod(MemberNamespace.Constructor, cls, ctor))
    fb += wa.LocalGet(instanceLocal)
  }

  /** Codegen to box a primitive `char`/`long` into a `CharacterBox`/`LongBox`. */
  private def genBox(primType: watpe.SimpleType, boxClassName: ClassName): Type = {
    val primLocal = addSyntheticLocal(primType)

    /* We use a direct `StructNew` instead of the logical call to `newDefault`
     * plus constructor call. We can do this because we know that this is
     * what the constructor would do anyway (so we're basically inlining it).
     */

    fb += wa.LocalSet(primLocal)
    fb += wa.GlobalGet(genGlobalID.forVTable(boxClassName))
    fb += wa.GlobalGet(genGlobalID.forITable(boxClassName))
    fb += wa.LocalGet(primLocal)
    fb += wa.StructNew(genTypeID.forClass(boxClassName))

    ClassType(boxClassName, nullable = false)
  }

  private def genIdentityHashCode(tree: IdentityHashCode): Type = {
    val IdentityHashCode(expr) = tree

    // TODO Avoid dispatch when we know a more precise type than any
    genTree(expr, AnyType)

    markPosition(tree)
    fb += wa.Call(genFunctionID.identityHashCode)

    IntType
  }

  private def genWrapAsThrowable(tree: WrapAsThrowable): Type = {
    val WrapAsThrowable(expr) = tree

    val nonNullThrowableType = watpe.RefType(genTypeID.ThrowableStruct)
    val jsExceptionType = watpe.RefType(genTypeID.JSExceptionStruct)

    fb.block(nonNullThrowableType) { doneLabel =>
      genTree(expr, AnyType)

      markPosition(tree)

      // if expr.isInstanceOf[Throwable], then br $done
      fb += wa.BrOnCast(doneLabel, watpe.RefType.anyref, nonNullThrowableType)

      // otherwise, wrap in a new JavaScriptException

      val exprLocal = addSyntheticLocal(watpe.RefType.anyref)
      val instanceLocal = addSyntheticLocal(jsExceptionType)

      fb += wa.LocalSet(exprLocal)
      fb += wa.Call(genFunctionID.newDefault(SpecialNames.JSExceptionClass))
      fb += wa.LocalTee(instanceLocal)
      fb += wa.LocalGet(exprLocal)
      fb += wa.Call(
        genFunctionID.forMethod(
          MemberNamespace.Constructor,
          SpecialNames.JSExceptionClass,
          SpecialNames.AnyArgConstructorName
        )
      )
      fb += wa.LocalGet(instanceLocal)
    }

    tree.tpe
  }

  private def genUnwrapFromThrowable(tree: UnwrapFromThrowable): Type = {
    val UnwrapFromThrowable(expr) = tree

    fb.block(watpe.RefType.anyref) { doneLabel =>
      genTreeAuto(expr)

      markPosition(tree)

      genAsNonNullOrNPEFor(expr)

      // if !expr.isInstanceOf[js.JavaScriptException], then br $done
      fb += wa.BrOnCastFail(
        doneLabel,
        watpe.RefType(genTypeID.ThrowableStruct),
        watpe.RefType(genTypeID.JSExceptionStruct)
      )

      // otherwise, unwrap the JavaScriptException by reading its field
      fb += wa.StructGet(
        genTypeID.JSExceptionStruct,
        genFieldID.forClassInstanceField(SpecialNames.exceptionFieldName)
      )
    }

    AnyType
  }

  private def genJSNew(tree: JSNew): Type = {
    val JSNew(ctor, args) = tree

    implicit val pos = tree.pos

    genThroughCustomJSHelper(ctor :: args) { allJSArgs =>
      val jsCtor :: jsArgs = allJSArgs
      js.Return(js.New(jsCtor, jsArgs))
    }
  }

  private def genJSSelect(tree: JSSelect): Type = {
    val JSSelect(qualifier, item) = tree

    implicit val pos = tree.pos

    genThroughCustomJSHelper(List(qualifier, item)) { allJSArgs =>
      val List(jsQualifier, jsItem) = allJSArgs
      js.Return(js.BracketSelect.makeOptimized(jsQualifier, jsItem))
    }
  }

  private def genJSFunctionApply(tree: JSFunctionApply): Type = {
    val JSFunctionApply(fun, args) = tree

    implicit val pos = tree.pos

    genThroughCustomJSHelper(fun :: args) { allJSArgs =>
      val jsFun :: jsArgs = allJSArgs
      js.Return(js.Apply.makeProtected(jsFun, jsArgs))
    }
  }

  private def genJSMethodApply(tree: JSMethodApply): Type = {
    val JSMethodApply(receiver, method, args) = tree

    implicit val pos = tree.pos

    genThroughCustomJSHelper(receiver :: method :: args) { allJSArgs =>
      val jsReceiver :: jsMethod :: jsArgs = allJSArgs
      js.Return(js.Apply(js.BracketSelect.makeOptimized(jsReceiver, jsMethod), jsArgs))
    }
  }

  private def genJSImportCall(tree: JSImportCall): Type = {
    val JSImportCall(arg) = tree

    genTree(arg, AnyType)
    markPosition(tree)
    fb += wa.Call(genFunctionID.jsImportCall)
    AnyType
  }

  private def genJSImportMeta(tree: JSImportMeta): Type = {
    markPosition(tree)
    fb += wa.Call(genFunctionID.jsImportMeta)
    AnyType
  }

  private def genLoadJSConstructor(tree: LoadJSConstructor): Type = {
    val LoadJSConstructor(className) = tree

    markPosition(tree)

    ctx.getClassInfo(className).jsNativeLoadSpec match {
      case Some(loadSpec) =>
        genLoadJSFromSpec(loadSpec)(tree.pos)
      case None =>
        // This is a non-native JS class
        fb += wa.Call(genFunctionID.loadJSClass(className))
    }

    AnyType
  }

  private def genLoadJSModule(tree: LoadJSModule): Type = {
    val LoadJSModule(className) = tree

    markPosition(tree)

    ctx.getClassInfo(className).jsNativeLoadSpec match {
      case Some(loadSpec) =>
        genLoadJSFromSpec(loadSpec)(tree.pos)
      case None =>
        // This is a non-native JS module
        fb += wa.Call(genFunctionID.loadModule(className))
    }

    AnyType
  }

  private def genSelectJSNativeMember(tree: SelectJSNativeMember): Type = {
    val SelectJSNativeMember(className, MethodIdent(memberName)) = tree

    val info = ctx.getClassInfo(className)
    val jsNativeLoadSpec = info.jsNativeMembers.getOrElse(memberName, {
      throw new AssertionError(
          s"Found $tree for non-existing JS native member at ${tree.pos}")
    })
    genLoadJSFromSpec(jsNativeLoadSpec)(tree.pos)
    AnyType
  }

  private def genLoadJSFromSpec(jsNativeLoadSpec: JSNativeLoadSpec)(
      implicit pos: Position): Unit = {

    val builder = new CustomJSHelperBuilder()
    val result = builder.genJSNativeLoadSpec(jsNativeLoadSpec)
    val helperID = builder.build(AnyType) {
      js.Return(result)
    }

    markPosition(pos)
    fb += wa.Call(helperID)
  }

  private def genJSDelete(tree: JSDelete): Type = {
    val JSDelete(qualifier, item) = tree

    genTree(qualifier, AnyType)
    genTree(item, AnyType)
    markPosition(tree)
    fb += wa.Call(genFunctionID.jsDelete)
    NoType
  }

  private def genJSUnaryOp(tree: JSUnaryOp): Type = {
    val JSUnaryOp(op, lhs) = tree

    implicit val pos = tree.pos

    genThroughCustomJSHelper(List(lhs), tree.tpe) { allJSArgs =>
      val List(jsLhs) = allJSArgs

      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), jsLhs)
      } else {
        jsLhs
      }

      js.Return(js.UnaryOp(op, protectedLhs))
    }

    tree.tpe
  }

  private def genJSBinaryOp(tree: JSBinaryOp): Type = {
    val JSBinaryOp(op, lhs, rhs) = tree

    op match {
      case JSBinaryOp.|| | JSBinaryOp.&& =>
        /* Here we need to implement the short-circuiting behavior, with a
         * condition based on the truthy value of the left-hand-side.
         */
        val lhsLocal = addSyntheticLocal(watpe.RefType.anyref)
        genTree(lhs, AnyType)
        markPosition(tree)
        fb += wa.LocalTee(lhsLocal)
        fb += wa.Call(genFunctionID.jsIsTruthy)
        if (op == JSBinaryOp.||) {
          fb.ifThenElse(watpe.RefType.anyref) {
            fb += wa.LocalGet(lhsLocal)
          } {
            genTree(rhs, AnyType)
            markPosition(tree)
          }
        } else {
          fb.ifThenElse(watpe.RefType.anyref) {
            genTree(rhs, AnyType)
            markPosition(tree)
          } {
            fb += wa.LocalGet(lhsLocal)
          }
        }

      case _ =>
        implicit val pos = tree.pos

        genThroughCustomJSHelper(List(lhs, rhs), tree.tpe) { allJSArgs =>
          val List(jsLhs, jsRhs) = allJSArgs
          js.Return(js.BinaryOp(op, jsLhs, jsRhs))
        }
    }

    tree.tpe
  }

  private def genJSArrayConstr(tree: JSArrayConstr): Type = {
    val JSArrayConstr(items) = tree

    implicit val pos = tree.pos

    if (items.isEmpty) {
      markPosition(tree)
      fb += wa.Call(genFunctionID.jsNewArray)
      AnyType
    } else {
      genThroughCustomJSHelper(items, AnyNotNullType) { jsItems =>
        js.Return(js.ArrayConstr(jsItems))
      }
    }
  }

  private def genJSObjectConstr(tree: JSObjectConstr): Type = {
    val JSObjectConstr(fields) = tree

    implicit val pos = tree.pos

    if (fields.isEmpty) {
      markPosition(tree)
      fb += wa.Call(genFunctionID.jsNewObject)
      AnyType
    } else {
      val flatPropValues = fields.flatMap(pv => List(pv._1, pv._2))

      genThroughCustomJSHelper(flatPropValues, AnyNotNullType) { jsFlatPropValues =>
        val jsPropValuesIter = jsFlatPropValues.grouped(2).map { pvList =>
          val List(jsPropTree, jsValue) = pvList
          val jsProp: js.PropertyName = jsPropTree match {
            case jsPropTree: js.StringLiteral => jsPropTree
            case _                            => js.ComputedName(jsPropTree)
          }
          jsProp -> jsValue
        }
        js.Return(js.ObjectConstr(jsPropValuesIter.toList))
      }
    }
  }

  private def genJSGlobalRef(tree: JSGlobalRef): Type = {
    val JSGlobalRef(name) = tree

    implicit val pos = tree.pos

    val builder = new CustomJSHelperBuilder()
    val helperID = builder.build(AnyType) {
      js.Return(builder.genGlobalRef(name))
    }

    markPosition(pos)
    fb += wa.Call(helperID)
    AnyType
  }

  private def genJSTypeOfGlobalRef(tree: JSTypeOfGlobalRef): Type = {
    val JSTypeOfGlobalRef(JSGlobalRef(name)) = tree

    implicit val pos = tree.pos

    val builder = new CustomJSHelperBuilder()
    val helperID = builder.build(AnyType) {
      js.Return(js.UnaryOp(JSUnaryOp.typeof, builder.genGlobalRef(name)))
    }

    markPosition(pos)
    fb += wa.Call(helperID)
    AnyType
  }

  private def genJSLinkingInfo(tree: JSLinkingInfo): Type = {
    markPosition(tree)
    fb += wa.GlobalGet(genGlobalID.jsLinkingInfo)
    AnyType
  }

  private def genArrayLength(tree: ArrayLength): Type = {
    val ArrayLength(array) = tree

    genTreeAuto(array)

    markPosition(tree)

    array.tpe match {
      case ArrayType(arrayTypeRef, _) =>
        // Get the underlying array
        genCheckNonNullFor(array)
        fb += wa.StructGet(
          genTypeID.forArrayClass(arrayTypeRef),
          genFieldID.objStruct.arrayUnderlying
        )
        // Get the length
        fb += wa.ArrayLen
        IntType

      case NothingType =>
        // unreachable
        NothingType
      case NullType =>
        genNPE()
        NothingType
      case _ =>
        throw new IllegalArgumentException(
            s"ArraySelect.array must be an array type, but has type ${tree.array.tpe}")
    }
  }

  private def genNewArray(tree: NewArray): Type = {
    val NewArray(arrayTypeRef, length) = tree

    markPosition(tree)

    genLoadVTableAndITableForArray(fb, arrayTypeRef)

    // Create the underlying array
    genTree(length, IntType)
    markPosition(tree)

    if (semantics.negativeArraySizes != CheckedBehavior.Unchecked) {
      length match {
        case IntLiteral(lengthValue) if lengthValue >= 0 =>
          () // always good
        case _ =>
          // if length < 0
          val lengthLocal = addSyntheticLocal(watpe.Int32)
          fb += wa.LocalTee(lengthLocal)
          fb += wa.I32Const(0)
          fb += wa.I32LtS
          fb.ifThen() {
            // then throw NegativeArraySizeException
            fb += wa.LocalGet(lengthLocal)
            fb += wa.Call(genFunctionID.throwNegativeArraySizeException)
            fb += wa.Unreachable
          }
          fb += wa.LocalGet(lengthLocal)
      }
    }

    val underlyingArrayType = genTypeID.underlyingOf(arrayTypeRef)
    fb += wa.ArrayNewDefault(underlyingArrayType)

    // Create the array object
    fb += wa.StructNew(genTypeID.forArrayClass(arrayTypeRef))

    tree.tpe
  }

  private def genArraySelect(tree: ArraySelect): Type = {
    val ArraySelect(array, index) = tree

    genTreeAuto(array)

    array.tpe match {
      case ArrayType(arrayTypeRef, _) =>
        genCheckNonNullFor(array)

        if (semantics.arrayIndexOutOfBounds == CheckedBehavior.Unchecked) {
          // Get the underlying array
          fb += wa.StructGet(
            genTypeID.forArrayClass(arrayTypeRef),
            genFieldID.objStruct.arrayUnderlying
          )

          // Load the index
          genTree(index, IntType)

          markPosition(tree)

          // Use the appropriate variant of array.get for sign extension
          val typeIdx = genTypeID.underlyingOf(arrayTypeRef)
          arrayTypeRef match {
            case ArrayTypeRef(BooleanRef | CharRef, 1) =>
              fb += wa.ArrayGetU(typeIdx)
            case ArrayTypeRef(ByteRef | ShortRef, 1) =>
              fb += wa.ArrayGetS(typeIdx)
            case _ =>
              fb += wa.ArrayGet(typeIdx)
          }
        } else {
          genTree(index, IntType)
          markPosition(tree)
          fb += wa.Call(genFunctionID.arrayGetFor(arrayTypeRef))
        }

        /* If it is a reference array type whose element type does not translate
         * to `anyref`, we must cast down the result.
         */
        arrayTypeRef match {
          case ArrayTypeRef(_: PrimRef, 1) =>
            // a primitive array always has the correct type
            ()
          case _ =>
            transformSingleType(tree.tpe) match {
              case watpe.RefType.anyref =>
                // nothing to do
                ()
              case watpe.RefType(nullable, watpe.HeapType.Extern) =>
                fb += wa.ExternConvertAny
                if (!nullable)
                  fb += wa.RefAsNonNull
              case refType: watpe.RefType =>
                fb += wa.RefCast(refType)
              case otherType =>
                throw new AssertionError(s"Unexpected result type for reference array: $otherType")
            }
        }

        tree.tpe

      case NothingType =>
        // unreachable
        NothingType
      case NullType =>
        genNPE()
        NothingType
      case _ =>
        throw new IllegalArgumentException(
            s"ArraySelect.array must be an array type, but has type ${array.tpe}")
    }
  }

  private def genArrayValue(tree: ArrayValue): Type = {
    val ArrayValue(arrayTypeRef, elems) = tree

    val expectedElemType = arrayTypeRef match {
      case ArrayTypeRef(base: PrimRef, 1) => base.tpe
      case _                              => AnyType
    }

    // Mark the position for the header of `genArrayValue`
    markPosition(tree)

    SWasmGen.genArrayValue(fb, arrayTypeRef, elems.size) {
      // Create the underlying array
      elems.foreach(genTree(_, expectedElemType))

      // Re-mark the position for the footer of `genArrayValue`
      markPosition(tree)
    }

    tree.tpe
  }

  private def genClosure(tree: Closure): Type = {
    val Closure(arrow, captureParams, params, restParam, body, captureValues) = tree

    implicit val pos = tree.pos

    val dataStructTypeID = ctx.getClosureDataStructType(captureParams.map(_.ptpe))

    // Define the function where captures are reified as a `__captureData` argument.
    val closureFuncOrigName = genClosureFuncOriginalName()
    val closureFuncID = new ClosureFunctionID(closureFuncOrigName)
    emitFunction(
      closureFuncID,
      closureFuncOrigName,
      enclosingClassName = None,
      Some(captureParams),
      receiverType = if (arrow) None else Some(watpe.RefType.anyref),
      params,
      restParam,
      body,
      resultType = AnyType
    )

    val builder = new CustomJSHelperBuilder()

    val fRef = builder.addWasmInput("f", watpe.RefType.func) {
      markPosition(tree)
      fb += ctx.refFuncWithDeclaration(closureFuncID)
    }
    val dataRef = builder.addWasmInput("d", watpe.RefType(dataStructTypeID)) {
      for ((param, value) <- captureParams.zip(captureValues))
        genTree(value, param.ptpe)
      markPosition(tree)
      fb += wa.StructNew(dataStructTypeID)
    }

    val helperID = builder.build(AnyNotNullType) {
      js.Return {
        val (argsParamDefs, restParamDef) = builder.genJSParamDefs(params, restParam)
        js.Function(arrow, argsParamDefs, restParamDef, {
          js.Return(js.Apply(
              fRef,
              dataRef ::
              (if (arrow) Nil else List(js.This())) :::
              argsParamDefs.map(_.ref) :::
              restParamDef.map(_.ref).toList
          ))
        })
      }
    }

    markPosition(tree)
    fb += wa.Call(helperID)

    AnyNotNullType
  }

  private def genClone(tree: Clone): Type = {
    val Clone(expr) = tree

    expr.tpe match {
      case NothingType =>
        genTree(expr, NothingType)
        NothingType

      case NullType =>
        genTree(expr, NullType)
        genNPE()
        NothingType

      case exprType =>
        val exprLocal = addSyntheticLocal(watpe.RefType(genTypeID.ObjectStruct))

        genTreeAuto(expr)

        markPosition(tree)

        genAsNonNullOrNPEFor(expr)
        fb += wa.LocalTee(exprLocal)

        fb += wa.LocalGet(exprLocal)
        fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable)
        fb += wa.StructGet(genTypeID.typeData, genFieldID.typeData.cloneFunction)
        // cloneFunction: (ref jl.Object) -> (ref jl.Object)
        fb += wa.CallRef(genTypeID.cloneFunctionType)

        // cast the (ref jl.Object) back down to the result type
        transformSingleType(exprType) match {
          case watpe.RefType(_, watpe.HeapType.Type(genTypeID.ObjectStruct)) =>
            // no need to cast to (ref null? jl.Object)
          case wasmType: watpe.RefType =>
            fb += wa.RefCast(wasmType.toNonNullable)
          case wasmType =>
            // Since no hijacked class extends jl.Cloneable, this case cannot happen
            throw new AssertionError(
                s"Unexpected type for Clone: $exprType (Wasm: $wasmType)")
        }

        exprType
    }
  }

  private def genMatch(tree: Match, expectedType: Type): Type = {
    val Match(selector, cases, defaultBody) = tree

    val selectorLocal = addSyntheticLocal(transformSingleType(selector.tpe))

    genTreeAuto(selector)

    markPosition(tree)

    fb += wa.LocalSet(selectorLocal)

    fb.block(transformResultType(expectedType)) { doneLabel =>
      fb.block() { defaultLabel =>
        val caseLabels = cases.map(c => c._1 -> fb.genLabel())
        for (caseLabel <- caseLabels)
          fb += wa.Block(wa.BlockType.ValueType(), Some(caseLabel._2))

        for {
          (matchableLiterals, label) <- caseLabels
          matchableLiteral <- matchableLiterals
        } {
          markPosition(matchableLiteral)
          fb += wa.LocalGet(selectorLocal)
          matchableLiteral match {
            case IntLiteral(value) =>
              fb += wa.I32Const(value)
              fb += wa.I32Eq
              fb += wa.BrIf(label)
            case StringLiteral(value) =>
              fb ++= ctx.stringPool.getConstantStringInstr(value)
              fb += wa.Call(genFunctionID.stringBuiltins.equals)
              fb += wa.BrIf(label)
            case Null() =>
              fb += wa.RefIsNull
              fb += wa.BrIf(label)
          }
        }
        fb += wa.Br(defaultLabel)

        for {
          (caseLabel, (_, caseBody)) <- caseLabels.zip(cases).reverse
        } {
          markPosition(caseBody)
          fb += wa.End
          genTree(caseBody, expectedType)
          fb += wa.Br(doneLabel)
        }
      }
      genTree(defaultBody, expectedType)
    }

    if (expectedType == NothingType)
      fb += wa.Unreachable

    expectedType
  }

  private def genCreateJSClass(tree: CreateJSClass): Type = {
    val CreateJSClass(className, captureValues) = tree

    val classInfo = ctx.getClassInfo(className)
    val jsClassCaptures = classInfo.jsClassCaptures.getOrElse {
      throw new AssertionError(
          s"Illegal CreateJSClass of top-level class ${className.nameString}")
    }

    for ((captureValue, captureParam) <- captureValues.zip(jsClassCaptures))
      genTree(captureValue, captureParam.ptpe)

    markPosition(tree)

    fb += wa.Call(genFunctionID.createJSClassOf(className))

    AnyType
  }

  private def genJSPrivateSelect(tree: JSPrivateSelect): Type = {
    val JSPrivateSelect(qualifier, FieldIdent(fieldName)) = tree

    genTree(qualifier, AnyType)

    markPosition(tree)

    fb += wa.GlobalGet(genGlobalID.forJSPrivateField(fieldName))
    fb += wa.Call(genFunctionID.jsSelect)

    AnyType
  }

  private def genJSSuperSelect(tree: JSSuperSelect): Type = {
    val JSSuperSelect(superClass, receiver, item) = tree

    genTree(superClass, AnyType)
    genTree(receiver, AnyType)
    genTree(item, AnyType)

    markPosition(tree)

    fb += wa.Call(genFunctionID.jsSuperSelect)

    AnyType
  }

  private def genJSSuperMethodCall(tree: JSSuperMethodCall): Type = {
    val JSSuperMethodCall(superClass, receiver, method, args) = tree

    implicit val pos = tree.pos

    genThroughCustomJSHelper(superClass :: receiver :: method :: args) { allJSArgs =>
      val jsSuperClass :: jsReceiver :: jsMethod :: jsArgs = allJSArgs

      // return superClass.prototype[method].call(receiver, ...args);
      js.Return(
        js.Apply(
          js.DotSelect(
            js.BracketSelect.makeOptimized(
              js.DotSelect(jsSuperClass, js.Ident("prototype")),
              jsMethod
            ),
            js.Ident("call")
          ),
          jsReceiver :: jsArgs
        )
      )
    }
  }

  private def genJSNewTarget(tree: JSNewTarget): Type = {
    markPosition(tree)

    genReadStorage(newTargetStorage)

    AnyType
  }

  private def genRecordSelect(tree: RecordSelect): Type = {
    if (canLookupRecordSelect(tree)) {
      markPosition(tree)
      genReadStorage(lookupRecordSelect(tree))
    } else {
      /* We have a record select that we cannot simplify to a direct storage,
       * because its `record` part is neither a `VarRef` nor (recursively) a
       * `RecordSelect`. For example, it could be an `If` whose two branches
       * both return a different `VarRef`/`Record` of the same record type:
       *   (if (cond) record1 else record2).recordField
       * In that case, we must evaluate the `record` in full, then discard all
       * the fields that are not the one we're selecting.
       *
       * (The JS backend avoids this situation by construction because of its
       * unnesting logic. It always creates a temporary `VarRef` of the record
       * type. In Wasm we can use multiple values on the stack instead.)
       */

      genTreeAuto(tree.record)

      markPosition(tree)

      val tempTypes = transformResultType(tree.tpe)
      val tempLocals = tempTypes.map(addSyntheticLocal(_))

      val recordType = tree.record.tpe.asInstanceOf[RecordType]
      for (recordField <- recordType.fields.reverseIterator) {
        if (recordField.name == tree.field.name) {
          // Store this one in our temp locals
          for (tempLocal <- tempLocals.reverseIterator)
            fb += wa.LocalSet(tempLocal)
        } else {
          // Discard this field
          for (_ <- transformResultType(recordField.tpe))
            fb += wa.Drop
        }
      }

      // Read back our locals
      for (tempLocal <- tempLocals)
        fb += wa.LocalGet(tempLocal)
    }

    tree.tpe
  }

  private def genRecordValue(tree: RecordValue): Type = {
    for ((elem, field) <- tree.elems.zip(tree.tpe.fields))
      genTree(elem, field.tpe)

    tree.tpe
  }

  private def genTransient(tree: Transient): Type = {
    tree.value match {
      case Transients.Cast(expr, tpe) =>
        genCast(expr, tpe, tree.pos)

      case value: Transients.SystemArrayCopy =>
        genSystemArrayCopy(tree, value)

      case Transients.ObjectClassName(obj) =>
        genTreeToAny(obj)
        markPosition(tree)
        genAsNonNullOrNPEFor(obj)
        fb += wa.Call(genFunctionID.anyGetClassName)
        StringType

      case value @ WasmTransients.WasmUnaryOp(_, lhs) =>
        genTreeAuto(lhs)
        markPosition(tree)
        fb += value.wasmInstr
        value.tpe

      case value @ WasmTransients.WasmBinaryOp(_, lhs, rhs) =>
        genTreeAuto(lhs)
        genTreeAuto(rhs)
        markPosition(tree)
        fb += value.wasmInstr
        value.tpe

      case value @ WasmTransients.WasmStringFromCodePoint(codePoint) =>
        genTree(codePoint, IntType)
        markPosition(tree)
        fb += wa.Call(genFunctionID.stringBuiltins.fromCodePoint)
        value.tpe

      case value @ WasmTransients.WasmCodePointAt(string, index) =>
        genTree(string, ClassType(BoxedStringClass, nullable = string.tpe.isNullable))
        if (semantics.stringIndexOutOfBounds == CheckedBehavior.Unchecked)
          genCheckNonNullFor(string)
        else
          genAsNonNullOrNPEFor(string)
        genTree(index, IntType)
        markPosition(tree)
        if (semantics.stringIndexOutOfBounds == CheckedBehavior.Unchecked)
          fb += wa.Call(genFunctionID.stringBuiltins.codePointAt)
        else
          fb += wa.Call(genFunctionID.checkedStringCodePointAt)
        value.tpe

      case value @ WasmTransients.WasmSubstring(string, start, optEnd) =>
        genTree(string, ClassType(BoxedStringClass, nullable = string.tpe.isNullable))
        if (semantics.stringIndexOutOfBounds == CheckedBehavior.Unchecked)
          genCheckNonNullFor(string)
        else
          genAsNonNullOrNPEFor(string)
        genTree(start, IntType)
        optEnd.foreach(genTree(_, IntType))
        markPosition(tree)
        if (semantics.stringIndexOutOfBounds == CheckedBehavior.Unchecked) {
          if (optEnd.isEmpty)
            fb += wa.I32Const(-1) // unsigned max value
          fb += wa.Call(genFunctionID.stringBuiltins.substring)
        } else {
          if (optEnd.isEmpty)
            fb += wa.Call(genFunctionID.checkedSubstringStart)
          else
            fb += wa.Call(genFunctionID.checkedSubstringStartEnd)
        }
        value.tpe

      case other =>
        throw new AssertionError(s"Unknown transient: $other")
    }
  }

  private def genSystemArrayCopy(tree: Transient,
      transientValue: Transients.SystemArrayCopy): Type = {
    val Transients.SystemArrayCopy(src, srcPos, dest, destPos, length) = transientValue

    genTreeAuto(src)
    genTree(srcPos, IntType)
    genTreeAuto(dest)
    genTree(destPos, IntType)
    genTree(length, IntType)

    markPosition(tree)

    (src.tpe, dest.tpe) match {
      case (ArrayType(srcArrayTypeRef, _), ArrayType(destArrayTypeRef, _))
          if genTypeID.forArrayClass(srcArrayTypeRef) == genTypeID.forArrayClass(destArrayTypeRef) =>
        // Generate a specialized arrayCopyT call
        fb += wa.Call(genFunctionID.specializedArrayCopy(srcArrayTypeRef))

      case _ =>
        // Generate a generic arrayCopy call
        fb += wa.Call(genFunctionID.genericArrayCopy)
    }

    NoType
  }

  // Helpers to generate code going through custom JS helpers

  /** Generates code with a custom JS helper based on Tree inputs.
   *
   *  Logically, the following steps happen:
   *
   *  1. Generates Wasm code to evaluate the `args`, in order.
   *  2. Passes the values, converted for use by JavaScript, to a custom JS
   *     helper whose body is provided by `makeJSHelperBody`.
   *     `makeJSHelperBody` receives a list of JS trees corresponding to the JS
   *     evaluated values of `args`.
   *  3. Leaves the result of `makeJSHelperBody` on the stack, typed as
   *     `resultType`.
   *
   *  Some `args` may be evaluated on the JS side when possible, notably for
   *  most literals, JS global refs and imports.
   *
   *  @see [[CustomJSHelperBuilder]]
   */
  private def genThroughCustomJSHelper(args: List[TreeOrJSSpread],
      resultType: Type = AnyType)(
      makeJSHelperBody: List[js.Tree] => js.Tree)(
      implicit pos: Position): Type = {

    val builder = new CustomJSHelperBuilderWithTreeSupport()
    val jsArgs = args.map(builder.addInput(_))
    val helperID = builder.build(resultType) {
      makeJSHelperBody(jsArgs)
    }

    markPosition(pos)
    fb += wa.Call(helperID)

    resultType
  }

  private final class CustomJSHelperBuilderWithTreeSupport()(implicit pos: Position)
      extends CustomJSHelperBuilder.WithTreeEval {
    protected def evalTreeAtCallSite(tree: Tree, expectedType: Type): Unit =
      genTree(tree, expectedType)
  }

  /*--------------------------------------------------------------------*
   * HERE BE DRAGONS --- Handling of TryFinally, Labeled and Return --- *
   *--------------------------------------------------------------------*/

  /* From this point onwards, and until the end of the file, you will find
   * the infrastructure required to handle TryFinally and Labeled/Return pairs.
   *
   * Independently, TryFinally and Labeled/Return are not very difficult to
   * handle. The dragons come when they interact, and in particular when a
   * TryFinally stands in the middle of a Labeled/Return pair.
   *
   * For example:
   *
   * val foo: int = alpha[int]: {
   *   val bar: string = try {
   *     if (somethingHappens)
   *       return@alpha 5
   *     "bar"
   *   } finally {
   *     doTheFinally()
   *   }
   *   someOtherThings(bar)
   * }
   *
   * In that situation, if we naively translate the `return@alpha` into
   * `br $alpha`, we bypass the `finally` block, which goes against the spec.
   *
   * Instead, we must stash the result 5 in a local and jump to the finally
   * block. The issue is that, at the end of `doTheFinally()`, we need to keep
   * propagating further up (instead of executing `someOtherThings()`).
   *
   * That means that there are 3 possible outcomes after the `finally` block:
   *
   * - Rethrow the exception if we caught one.
   * - Reload the stashed result and branch further to `alpha`.
   * - Otherwise keep going to do `someOtherThings()`.
   *
   * Now what if there are *several* labels for which we cross that
   * `try..finally`? Well we need to deal with all the possible labels. This
   * means that, in general, we in fact have `2 + n` possible outcomes, where
   * `n` is the number of labels for which we found a `Return` that crosses the
   * boundary.
   *
   * In order to know whether we need to rethrow, we look at a nullable
   * `exnref`. For the remaining cases, we use a separate `destinationTag`
   * local. Every label gets assigned a distinct tag > 0. Fall-through is
   * always represented by 0. Before branching to a `finally` block, we set the
   * appropriate value to the `destinationTag` value.
   *
   * Since the various labels can have different result types, and since they
   * can be different from the result of the regular flow of the `try` block,
   * we cannot use the stack for the `try_table` itself: each label has a
   * dedicated local for its result if it comes from such a crossing `return`.
   *
   * Two more complications:
   *
   * - If the `finally` block itself contains another `try..finally`, they may
   *   need a `destinationTag` concurrently. Therefore, every `try..finally`
   *   gets its own `destinationTag` local. We do not need this for another
   *   `try..finally` inside a `try` (or elsewhere in the function), so this is
   *   not an optimal allocation; we do it this way not to complicate this
   *   further.
   * - If the `try` block contains another `try..finally`, so that there are
   *   two (or more) `try..finally` in the way between a `Return` and a
   *   `Labeled`, we must forward to the next `finally` in line (and its own
   *   `destinationTag` local) so that the whole chain gets executed before
   *   reaching the `Labeled`.
   *
   * ---
   *
   * As an evil example of everything that can happen, consider:
   *
   * alpha[double]: { // allocated destinationTag = 1
   *   val foo: int = try { // declare local destinationTagOuter
   *     beta[int]: { // allocated destinationTag = 2
   *       val bar: int = try { // declare local destinationTagInner
   *         if (A) return@alpha 5
   *         if (B) return@beta 10
   *         56
   *       } finally {
   *         doTheFinally()
   *         // not shown: there is another try..finally here using a third
   *         // local destinationTagThird, since destinationTagOuter and
   *         // destinationTagInner are alive at the same time.
   *       }
   *       someOtherThings(bar)
   *     }
   *   } finally {
   *     doTheOuterFinally()
   *   }
   *   moreOtherThings(foo)
   * }
   *
   * The whole compiled code is too overwhelming to be useful, so we show the
   * important aspects piecemiel, from the bottom up.
   *
   * First, the compiled code for `return@alpha 5`:
   *
   * i32.const 5                    ; eval the argument of the return
   * local.set $alphaResult         ; store it in $alphaResult because we are cross a try..finally
   * i32.const 1                    ; the destination tag of alpha
   * local.set $destinationTagInner ; store it in the destinationTag local of the inner try..finally
   * br $innerCross                 ; branch to the cross label of the inner try..finally
   *
   * Second, we look at the shape generated for the inner try..finally:
   *
   * block $innerDone (result i32)
   *   block $innerCatch (result exnref)
   *     block $innerCross
   *       try_table (catch_all_ref $innerCatch)
   *         ; [...] body of the try
   *
   *         local.set $innerTryResult
   *       end ; try_table
   *
   *       ; set destinationTagInner := 0 to mean fall-through
   *       i32.const 0
   *       local.set $destinationTagInner
   *     end ; block $innerCross
   *
   *     ; no exception thrown
   *     ref.null exn
   *   end ; block $innerCatch
   *
   *   ; now we have the common code with the finally
   *
   *   ; [...] body of the finally
   *
   *   ; maybe re-throw
   *   block $innerExnIsNull (param exnref)
   *     br_on_null $innerExnIsNull
   *     throw_ref
   *   end
   *
   *   ; re-dispatch after the outer finally based on $destinationTagInner
   *
   *   ; first transfer our destination tag to the outer try's destination tag
   *   local.get $destinationTagInner
   *   local.set $destinationTagOuter
   *
   *   ; now use a br_table to jump to the appropriate destination
   *   ; if 0, fall-through
   *   ; if 1, go the outer try's cross label because it is still on the way to alpha
   *   ; if 2, go to beta's cross label
   *   ; default to fall-through (never used but br_table needs a default)
   *   br_table $innerDone $outerCross $betaCross $innerDone
   * end ; block $innerDone
   *
   * We omit the shape of beta and of the outer try. There are similar to the
   * shape of alpha and inner try, respectively.
   *
   * We conclude with the shape of the alpha block:
   *
   * block $alpha (result f64)
   *   block $alphaCross
   *     ; begin body of alpha
   *
   *     ; [...]              ; the try..finally
   *     local.set $foo       ; val foo =
   *     moreOtherThings(foo)
   *
   *     ; end body of alpha
   *
   *     br $alpha ; if alpha finished normally, jump over `local.get $alphaResult`
   *   end ; block $alphaCross
   *
   *   ; if we returned from alpha across a try..finally, fetch the result from the local
   *   local.get $alphaResult
   * end ; block $alpha
   */

  /** This object namespaces everything related to unwinding, so that we don't pollute too much the
   *  overall internal scope of `FunctionEmitter`.
   */
  private object unwinding {

    /** The number of enclosing `Labeled` and `TryFinally` blocks.
     *
     *  For `TryFinally`, it is only enclosing if we are in the `try` branch, not the `finally`
     *  branch.
     *
     *  Invariant:
     *  {{{
     *  currentUnwindingStackDepth == enclosingTryFinallyStack.size + enclosingLabeledBlocks.size
     *  }}}
     */
    private var currentUnwindingStackDepth: Int = 0

    private var enclosingTryFinallyStack: List[TryFinallyEntry] = Nil

    private var enclosingLabeledBlocks: Map[LabelName, LabeledEntry] = Map.empty

    private def innermostTryFinally: Option[TryFinallyEntry] =
      enclosingTryFinallyStack.headOption

    private def enterTryFinally(entry: TryFinallyEntry)(body: => Unit): Unit = {
      assert(entry.depth == currentUnwindingStackDepth)
      enclosingTryFinallyStack ::= entry
      currentUnwindingStackDepth += 1
      try {
        body
      } finally {
        currentUnwindingStackDepth -= 1
        enclosingTryFinallyStack = enclosingTryFinallyStack.tail
      }
    }

    private def enterLabeled(entry: LabeledEntry)(body: => Unit): Unit = {
      assert(entry.depth == currentUnwindingStackDepth)
      val savedLabeledBlocks = enclosingLabeledBlocks
      enclosingLabeledBlocks = enclosingLabeledBlocks.updated(entry.irLabelName, entry)
      currentUnwindingStackDepth += 1
      try {
        body
      } finally {
        currentUnwindingStackDepth -= 1
        enclosingLabeledBlocks = savedLabeledBlocks
      }
    }

    /** The last destination tag that was allocated to a LabeledEntry. */
    private var lastDestinationTag: Int = 0

    private def allocateDestinationTag(): Int = {
      lastDestinationTag += 1
      lastDestinationTag
    }

    /** Information about an enclosing `TryFinally` block. */
    private final class TryFinallyEntry(val depth: Int) {
      import TryFinallyEntry._

      private var _crossInfo: Option[CrossInfo] = None

      def isInside(labeledEntry: LabeledEntry): Boolean =
        this.depth > labeledEntry.depth

      def wasCrossed: Boolean = _crossInfo.isDefined

      def requireCrossInfo(): CrossInfo = {
        _crossInfo.getOrElse {
          val info = CrossInfo(addSyntheticLocal(watpe.Int32), fb.genLabel())
          _crossInfo = Some(info)
          info
        }
      }
    }

    private object TryFinallyEntry {
      /** Cross info for a `TryFinally` entry.
       *
       *  @param destinationTagLocal
       *    The destinationTag local variable for this `TryFinally`.
       *  @param crossLabel
       *    The cross label for this `TryFinally`.
       */
      sealed case class CrossInfo(
        val destinationTagLocal: wanme.LocalID,
        val crossLabel: wanme.LabelID
      )
    }

    /** Information about an enclosing `Labeled` block. */
    private final class LabeledEntry(val depth: Int,
        val irLabelName: LabelName, val expectedType: Type) {

      import LabeledEntry._

      /** The regular label for this `Labeled` block, used for `Return`s that
       *  do not cross a `TryFinally`.
       */
      val regularWasmLabel: wanme.LabelID = fb.genLabel()

      private var _crossInfo: Option[CrossInfo] = None

      def wasCrossUsed: Boolean = _crossInfo.isDefined

      def requireCrossInfo(): CrossInfo = {
        _crossInfo.getOrElse {
          val destinationTag = allocateDestinationTag()
          val resultTypes = transformResultType(expectedType)
          val resultLocals = resultTypes.map(addSyntheticLocal(_))
          val crossLabel = fb.genLabel()
          val info = CrossInfo(destinationTag, resultLocals, crossLabel)
          _crossInfo = Some(info)
          info
        }
      }
    }

    private object LabeledEntry {
      /** Cross info for a `LabeledEntry`.
       *
       *  @param destinationTag
       *    The destination tag allocated to this label, used by the `finally`
       *    blocks to keep propagating to the right destination. Destination
       *    tags are always `> 0`. The value `0` is reserved for fall-through.
       *  @param resultLocals
       *    The locals in which to store the result of the label if we have to
       *    cross a `try..finally`.
       *  @param crossLabel
       *    An additional Wasm label that has a `[]` result, and which will get
       *    its result from the `resultLocal` instead of expecting it on the stack.
       */
      sealed case class CrossInfo(
        destinationTag: Int,
        resultLocals: List[wanme.LocalID],
        crossLabel: wanme.LabelID
      )
    }

    def genLabeled(tree: Labeled, expectedType: Type): Type = {
      val Labeled(LabelIdent(labelName), tpe, body) = tree

      val entry = new LabeledEntry(currentUnwindingStackDepth, labelName, expectedType)

      val ty = transformResultType(expectedType)

      markPosition(tree)

      // Manual wa.Block here because we have a specific `label`
      fb += wa.Block(fb.sigToBlockType(Sig(Nil, ty)), Some(entry.regularWasmLabel))

      /* Remember the position in the instruction stream, in case we need to
       * come back and insert the wa.Block for the cross handling.
       */
      val instrsBlockBeginIndex = fb.markCurrentInstructionIndex()

      // Emit the body
      enterLabeled(entry) {
        genTree(body, expectedType)
      }

      markPosition(tree)

      // Deal with crossing behavior
      if (entry.wasCrossUsed) {
        assert(expectedType != NothingType,
            "The tryFinallyCrossLabel should not have been used for label " +
            s"${labelName.nameString} of type nothing")

        /* In this case we need to handle situations where we receive the value
         * from the label's `result` local, branching out of the label's
         * `crossLabel`.
         *
         * Instead of the standard shape
         *
         * block $labeled (result t)
         *   body
         * end
         *
         * We need to amend the shape to become
         *
         * block $labeled (result t)
         *   block $crossLabel
         *     body            ; inside the body, jumps to this label after a
         *                     ; `finally` are compiled as `br $crossLabel`
         *     br $labeled
         *   end
         *   local.get $label.resultLocals ; (0 to many)
         * end
         */

        val LabeledEntry.CrossInfo(_, resultLocals, crossLabel) =
          entry.requireCrossInfo()

        // Go back and insert the `block $crossLabel` right after `block $labeled`
        fb.insert(instrsBlockBeginIndex, wa.Block(wa.BlockType.ValueType(), Some(crossLabel)))

        // Add the `br`, `end` and `local.get` at the current position, as usual
        fb += wa.Br(entry.regularWasmLabel)
        fb += wa.End
        for (local <- resultLocals)
          fb += wa.LocalGet(local)
      }

      fb += wa.End

      if (expectedType == NothingType)
        fb += wa.Unreachable

      expectedType
    }

    def genTryFinally(tree: TryFinally, expectedType: Type): Type = {
      val TryFinally(tryBlock, finalizer) = tree

      val entry = new TryFinallyEntry(currentUnwindingStackDepth)

      val resultType = transformResultType(expectedType)
      val resultLocals = resultType.map(addSyntheticLocal(_))

      markPosition(tree)

      fb.block() { doneLabel =>
        fb.block(watpe.RefType.exnref) { catchLabel =>
          /* Remember the position in the instruction stream, in case we need
           * to come back and insert the wa.BLOCK for the cross handling.
           */
          val instrsBlockBeginIndex = fb.markCurrentInstructionIndex()

          fb.tryTable()(List(wa.CatchClause.CatchAllRef(catchLabel))) {
            // try block
            enterTryFinally(entry) {
              withNPEScope(resultType) {
                genTree(tryBlock, expectedType)
              }
            }

            markPosition(tree)

            // store the result in locals during the finally block
            for (resultLocal <- resultLocals.reverse)
              fb += wa.LocalSet(resultLocal)
          }

          /* If this try..finally was crossed by a `Return`, we need to amend
           * the shape of our try part to
           *
           * block $catch (result exnref)
           *   block $cross
           *     try_table (catch_all_ref $catch)
           *       body
           *       set_local $results ; 0 to many
           *     end
           *     i32.const 0 ; 0 always means fall-through
           *     local.set $destinationTag
           *   end
           *   ref.null exn
           * end
           */
          if (entry.wasCrossed) {
            val TryFinallyEntry.CrossInfo(destinationTagLocal, crossLabel) =
              entry.requireCrossInfo()

            // Go back and insert the `block $cross` right after `block $catch`
            fb.insert(
              instrsBlockBeginIndex,
              wa.Block(wa.BlockType.ValueType(), Some(crossLabel))
            )

            // And the other amendments normally
            fb += wa.I32Const(0)
            fb += wa.LocalSet(destinationTagLocal)
            fb += wa.End // of the inserted wa.BLOCK
          }

          // on success, push a `null_ref exn` on the stack
          fb += wa.RefNull(watpe.HeapType.Exn)
        } // end block $catch

        // finally block (during which we leave the `(ref null exn)` on the stack)
        genTree(finalizer, NoType)

        markPosition(tree)

        if (!entry.wasCrossed) {
          // If the `exnref` is non-null, rethrow it
          fb += wa.BrOnNull(doneLabel)
          fb += wa.ThrowRef
        } else {
          /* If the `exnref` is non-null, rethrow it.
           * Otherwise, stay within the `$done` block.
           */
          fb.block(Sig(List(watpe.RefType.exnref), Nil)) { exnrefIsNullLabel =>
            fb += wa.BrOnNull(exnrefIsNullLabel)
            fb += wa.ThrowRef
          }

          /* Otherwise, use a br_table to dispatch to the right destination
           * based on the value of the try..finally's destinationTagLocal,
           * which is set by `Return` or to 0 for fall-through.
           */

          // The order does not matter here because they will be "re-sorted" by emitBRTable
          val possibleTargetEntries =
            enclosingLabeledBlocks.valuesIterator.filter(_.wasCrossUsed).toList

          val nextTryFinallyEntry = innermostTryFinally // note that we're out of ourselves already
            .filter(nextTry => possibleTargetEntries.exists(nextTry.isInside(_)))

          /* Build the destination table for `br_table`. Target Labeled's that
           * are outside of the next try..finally in line go to the latter;
           * for other `Labeled`'s, we go to their cross label.
           */
          val brTableDests: List[(Int, wanme.LabelID)] = possibleTargetEntries.map { targetEntry =>
            val LabeledEntry.CrossInfo(destinationTag, _, crossLabel) =
              targetEntry.requireCrossInfo()
            val label = nextTryFinallyEntry.filter(_.isInside(targetEntry)) match {
              case None          => crossLabel
              case Some(nextTry) => nextTry.requireCrossInfo().crossLabel
            }
            destinationTag -> label
          }

          fb += wa.LocalGet(entry.requireCrossInfo().destinationTagLocal)
          for (nextTry <- nextTryFinallyEntry) {
            // Transfer the destinationTag to the next try..finally in line
            fb += wa.LocalTee(nextTry.requireCrossInfo().destinationTagLocal)
          }
          emitBRTable(brTableDests, doneLabel)
        }
      } // end block $done

      // reload the result onto the stack
      for (resultLocal <- resultLocals)
        fb += wa.LocalGet(resultLocal)

      if (expectedType == NothingType)
        fb += wa.Unreachable

      expectedType
    }

    private def emitBRTable(dests: List[(Int, wanme.LabelID)],
        defaultLabel: wanme.LabelID): Unit = {
      dests match {
        case Nil =>
          fb += wa.Drop
          fb += wa.Br(defaultLabel)

        case (singleDestValue, singleDestLabel) :: Nil =>
          /* Common case (as far as getting here in the first place is concerned):
           * All the `Return`s that cross the current `TryFinally` have the same
           * target destination (namely the enclosing `def` in the original program).
           */
          fb += wa.I32Const(singleDestValue)
          fb += wa.I32Eq
          fb += wa.BrIf(singleDestLabel)
          fb += wa.Br(defaultLabel)

        case _ :: _ =>
          // `max` is safe here because the list is non-empty
          val table = Array.fill(dests.map(_._1).max + 1)(defaultLabel)
          for (dest <- dests)
            table(dest._1) = dest._2
          fb += wa.BrTable(table.toList, defaultLabel)
      }
    }

    def genReturn(tree: Return): Type = {
      val Return(expr, LabelIdent(labelName)) = tree

      val targetEntry = enclosingLabeledBlocks(labelName)

      genTree(expr, targetEntry.expectedType)

      markPosition(tree)

      if (targetEntry.expectedType != NothingType) {
        innermostTryFinally.filter(_.isInside(targetEntry)) match {
          case None =>
            // Easy case: directly branch out of the block
            fb += wa.Br(targetEntry.regularWasmLabel)

          case Some(tryFinallyEntry) =>
            /* Here we need to branch to the innermost enclosing `finally` block,
             * while remembering the destination label and the result value.
             */
            val LabeledEntry.CrossInfo(destinationTag, resultLocals, _) =
              targetEntry.requireCrossInfo()
            val TryFinallyEntry.CrossInfo(destinationTagLocal, crossLabel) =
              tryFinallyEntry.requireCrossInfo()

            // 1. Store the result in the label's result locals.
            for (local <- resultLocals.reverse)
              fb += wa.LocalSet(local)

            // 2. Store the label's destination tag into the try..finally's destination local.
            fb += wa.I32Const(destinationTag)
            fb += wa.LocalSet(destinationTagLocal)

            // 3. Branch to the enclosing `finally` block's cross label.
            fb += wa.Br(crossLabel)
        }
      }

      NothingType
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy