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

dotty.tools.backend.jvm.BCodeBodyBuilder.scala Maven / Gradle / Ivy

There is a newer version: 3.6.4-RC1-bin-20241220-0bfa1af-NIGHTLY
Show newest version
package dotty.tools
package backend
package jvm

import scala.language.unsafeNulls

import scala.annotation.{switch, tailrec}
import scala.collection.mutable.SortedMap

import scala.tools.asm
import scala.tools.asm.{Handle, Opcodes}
import BCodeHelpers.InvokeStyle

import dotty.tools.dotc.ast.tpd
import dotty.tools.dotc.CompilationUnit
import dotty.tools.dotc.core.Constants.*
import dotty.tools.dotc.core.Flags.{Label => LabelFlag, _}
import dotty.tools.dotc.core.Types.*
import dotty.tools.dotc.core.StdNames.{nme, str}
import dotty.tools.dotc.core.Symbols.*
import dotty.tools.dotc.transform.Erasure
import dotty.tools.dotc.util.Spans.*
import dotty.tools.dotc.core.Contexts.*
import dotty.tools.dotc.core.Phases.*
import dotty.tools.dotc.core.Decorators.em
import dotty.tools.dotc.report
import dotty.tools.dotc.ast.Trees.SyntheticUnit

/*
 *
 *  @author  Miguel Garcia, http://lamp.epfl.ch/~magarcia/ScalaCompilerCornerReloaded/
 *  @version 1.0
 *
 */
trait BCodeBodyBuilder extends BCodeSkelBuilder {
  // import global.*
  // import definitions.*
  import tpd.*
  import int.{_, given}
  import DottyBackendInterface.symExtensions
  import bTypes.*
  import coreBTypes.*

  protected val primitives: DottyPrimitives

  /*
   * Functionality to build the body of ASM MethodNode, except for `synchronized` and `try` expressions.
   */
  abstract class PlainBodyBuilder(cunit: CompilationUnit) extends PlainSkelBuilder(cunit) {

    import Primitives.TestOp

    /* ---------------- helper utils for generating methods and code ---------------- */

    def emit(opc: Int): Unit = { mnode.visitInsn(opc) }

    def emitZeroOf(tk: BType): Unit = {
      tk match {
        case BOOL => bc.boolconst(false)
        case BYTE  |
             SHORT |
             CHAR  |
             INT     => bc.iconst(0)
        case LONG    => bc.lconst(0)
        case FLOAT   => bc.fconst(0)
        case DOUBLE  => bc.dconst(0)
        case UNIT    => ()
        case _ => emit(asm.Opcodes.ACONST_NULL)
      }
    }

    /*
     * Emits code that adds nothing to the operand stack.
     * Two main cases: `tree` is an assignment,
     * otherwise an `adapt()` to UNIT is performed if needed.
     */
    def genStat(tree: Tree): Unit = {
      lineNumber(tree)

      tree match {
        case Assign(lhs @ DesugaredSelect(qual, _), rhs) =>
          val savedStackSize = stack.recordSize()
          val isStatic = lhs.symbol.isStaticMember
          if (!isStatic) {
            val qualTK = genLoad(qual)
            stack.push(qualTK)
          }
          genLoad(rhs, symInfoTK(lhs.symbol))
          stack.restoreSize(savedStackSize)
          lineNumber(tree)
          // receiverClass is used in the bytecode to access the field. using sym.owner may lead to IllegalAccessError
          val receiverClass = qual.tpe.typeSymbol
          fieldStore(lhs.symbol, receiverClass)

        case Assign(lhs, rhs) =>
          val s = lhs.symbol
          val Local(tk, _, idx, _) = locals.getOrMakeLocal(s)

          rhs match {
            case Apply(Select(larg: Ident, nme.ADD), Literal(x) :: Nil)
            if larg.symbol == s && tk.isIntSizedType && x.isShortRange =>
              lineNumber(tree)
              bc.iinc(idx, x.intValue)

            case Apply(Select(larg: Ident, nme.SUB), Literal(x) :: Nil)
            if larg.symbol == s && tk.isIntSizedType && Constant(-x.intValue).isShortRange =>
              lineNumber(tree)
              bc.iinc(idx, -x.intValue)

            case _ =>
              genLoad(rhs, tk)
              lineNumber(tree)
              bc.store(idx, tk)
          }

        case _ =>
          genLoad(tree, UNIT)
      }
    }

    /* Generate code for primitive arithmetic operations. */
    def genArithmeticOp(tree: Tree, code: Int): BType = tree match{
      case Apply(fun @ DesugaredSelect(larg, _), args) =>
      var resKind = tpeTK(larg)

      assert(resKind.isNumericType || (resKind == BOOL),
             s"$resKind is not a numeric or boolean type [operation: ${fun.symbol}]")

      import ScalaPrimitivesOps.*

      args match {
        // unary operation
        case Nil =>
          genLoad(larg, resKind)
          code match {
            case POS => () // nothing
            case NEG => bc.neg(resKind)
            case NOT => bc.genPrimitiveArithmetic(Primitives.NOT, resKind)
            case _ => abort(s"Unknown unary operation: ${fun.symbol.showFullName} code: $code")
          }

        // binary operation
        case rarg :: Nil =>
          val isShift = isShiftOp(code)
          resKind = tpeTK(larg).maxType(if (isShift) INT else tpeTK(rarg))

          if (isShift || isBitwiseOp(code)) {
            assert(resKind.isIntegralType || (resKind == BOOL),
                   s"$resKind incompatible with arithmetic modulo operation.")
          }

          genLoad(larg, resKind)
          stack.push(resKind)
          genLoad(rarg, if (isShift) INT else resKind)
          stack.pop()

          (code: @switch) match {
            case ADD => bc add resKind
            case SUB => bc sub resKind
            case MUL => bc mul resKind
            case DIV => bc div resKind
            case MOD => bc rem resKind

            case OR  | XOR | AND => bc.genPrimitiveLogical(code, resKind)

            case LSL | LSR | ASR => bc.genPrimitiveShift(code, resKind)

            case _                   => abort(s"Unknown primitive: ${fun.symbol}[$code]")
          }

        case _ =>
          abort(s"Too many arguments for primitive function: $tree")
      }
      lineNumber(tree)
      resKind
    }

    /* Generate primitive array operations. */
    def genArrayOp(tree: Tree, code: Int, expectedType: BType): BType = tree match{

      case Apply(DesugaredSelect(arrayObj, _), args) =>
      import ScalaPrimitivesOps.*
      val k = tpeTK(arrayObj)
      genLoad(arrayObj, k)
      val elementType = typeOfArrayOp.getOrElse[bTypes.BType](code, abort(s"Unknown operation on arrays: $tree code: $code"))

      var generatedType = expectedType

      if (isArrayGet(code)) {
        // load argument on stack
        assert(args.length == 1, s"Too many arguments for array get operation: $tree");
        stack.push(k)
        genLoad(args.head, INT)
        stack.pop()
        generatedType = k.asArrayBType.componentType
        bc.aload(elementType)
      }
      else if (isArraySet(code)) {
        val List(a1, a2) = args
        stack.push(k)
        genLoad(a1, INT)
        stack.push(INT)
        genLoad(a2)
        stack.pop(2)
        generatedType = UNIT
        bc.astore(elementType)
      } else {
        generatedType = INT
        emit(asm.Opcodes.ARRAYLENGTH)
      }
      lineNumber(tree)

      generatedType
    }

    def genLoadIfTo(tree: If, expectedType: BType, dest: LoadDestination): BType = tree match{
      case If(condp, thenp, elsep) =>

      val success = new asm.Label
      val failure = new asm.Label

      val hasElse = !elsep.hasAttachment(SyntheticUnit)

      genCond(condp, success, failure, targetIfNoJump = success)
      markProgramPoint(success)

      if dest == LoadDestination.FallThrough then
        if hasElse then
          val thenKind      = tpeTK(thenp)
          val elseKind      = tpeTK(elsep)
          def hasUnitBranch = (thenKind == UNIT || elseKind == UNIT) && expectedType == UNIT
          val resKind       = if (hasUnitBranch) UNIT else tpeTK(tree)

          val postIf = new asm.Label
          genLoadTo(thenp, resKind, LoadDestination.Jump(postIf, stack.recordSize()))
          markProgramPoint(failure)
          genLoadTo(elsep, resKind, LoadDestination.FallThrough)
          markProgramPoint(postIf)
          resKind
        else
          genLoad(thenp, UNIT)
          markProgramPoint(failure)
          UNIT
        end if
      else
        genLoadTo(thenp, expectedType, dest)
        markProgramPoint(failure)
        if hasElse then
          genLoadTo(elsep, expectedType, dest)
        else
          lineNumber(tree.cond)
          genAdaptAndSendToDest(UNIT, expectedType, dest)
        expectedType
      end if
    }

    def genPrimitiveOp(tree: Apply, expectedType: BType): BType = (tree: @unchecked) match {
      case Apply(fun @ DesugaredSelect(receiver, _), _) =>
      val sym = tree.symbol

      val code = primitives.getPrimitive(tree, receiver.tpe)

      import ScalaPrimitivesOps.*

      if (isArithmeticOp(code))                genArithmeticOp(tree, code)
      else if (code == CONCAT) genStringConcat(tree)
      else if (code == HASH)   genScalaHash(receiver)
      else if (isArrayOp(code))                genArrayOp(tree, code, expectedType)
      else if (isLogicalOp(code) || isComparisonOp(code)) {
        val success, failure, after = new asm.Label
        genCond(tree, success, failure, targetIfNoJump = success)
        // success block
        markProgramPoint(success)
        bc boolconst true
        bc goTo after
        // failure block
        markProgramPoint(failure)
        bc boolconst false
        // after
        markProgramPoint(after)

        BOOL
      }
      else if (isCoercion(code)) {
        genLoad(receiver)
        lineNumber(tree)
        genCoercion(code)
        coercionTo(code)
      }
      else abort(
        s"Primitive operation not handled yet: ${sym.showFullName}(${fun.symbol.name}) at: ${tree.span}"
      )
    }

    def genLoad(tree: Tree): BType = {
      val generatedType = tpeTK(tree)
      genLoad(tree, generatedType)
      generatedType
    }

    /* Generate code for trees that produce values on the stack */
    def genLoad(tree: Tree, expectedType: BType): Unit =
      genLoadTo(tree, expectedType, LoadDestination.FallThrough)

    /* Generate code for trees that produce values, sent to a given `LoadDestination`. */
    def genLoadTo(tree: Tree, expectedType: BType, dest: LoadDestination): Unit =
      var generatedType = expectedType
      var generatedDest = LoadDestination.FallThrough

      lineNumber(tree)

      tree match {
        case tree@ValDef(_, _, _) =>
          val sym = tree.symbol
          /* most of the time, !locals.contains(sym), unless the current activation of genLoad() is being called
             while duplicating a finalizer that contains this ValDef. */
          val loc = locals.getOrMakeLocal(sym)
          val Local(tk, _, idx, isSynth) = loc
          if (tree.rhs == tpd.EmptyTree) { emitZeroOf(tk) }
          else { genLoad(tree.rhs, tk) }
          bc.store(idx, tk)
          val localVarStart = currProgramPoint()
          if (!isSynth) { // there are case  ValDef's emitted by patmat
            varsInScope ::= (sym -> localVarStart)
          }
          generatedType = UNIT

        case t @ If(_, _, _) =>
          generatedType = genLoadIfTo(t, expectedType, dest)
          generatedDest = dest

        case t @ Labeled(_, _) =>
          generatedType = genLabeledTo(t, expectedType, dest)
          generatedDest = dest

        case r: Return =>
          genReturn(r)
          generatedDest = LoadDestination.Return

        case t @ WhileDo(_, _) =>
          generatedDest = genWhileDo(t)
          generatedType = UNIT

        case t @ Try(_, _, _) =>
          generatedType = genLoadTry(t)

        case t: Apply if t.fun.symbol eq defn.throwMethod =>
          val thrownExpr = t.args.head
          val thrownKind = tpeTK(thrownExpr)
          genLoadTo(thrownExpr, thrownKind, LoadDestination.Throw)
          generatedDest = LoadDestination.Throw

        case New(tpt) =>
          abort(s"Unexpected New(${tpt.tpe.showSummary()}/$tpt) reached GenBCode.\n" +
                "  Call was genLoad" + ((tree, expectedType)))

        case t @ Closure(env, call, tpt) =>
          val functionalInterface: Symbol =
            if !tpt.isEmpty then tpt.tpe.classSymbol
            else t.tpe.classSymbol
          val (fun, args) = call match {
            case Apply(fun, args) => (fun, args)
            case t @ DesugaredSelect(_, _) => (t, Nil) // TODO: use Select
            case t @ Ident(_) => (t, Nil)
          }

          val savedStackSize = stack.recordSize()
          if (!fun.symbol.isStaticMember) {
            // load receiver of non-static implementation of lambda

            // darkdimius: I haven't found in spec `this` reference should go
            // but I was able to derrive it by reading
            // AbstractValidatingLambdaMetafactory.validateMetafactoryArgs

            val DesugaredSelect(prefix, _) = fun: @unchecked
            val prefixTK = genLoad(prefix)
            stack.push(prefixTK)
          }

          genLoadArguments(env, fun.symbol.info.firstParamTypes map toTypeKind)
          stack.restoreSize(savedStackSize)
          generatedType = genInvokeDynamicLambda(NoSymbol, fun.symbol, env.size, functionalInterface)

        case app @ Apply(_, _) =>
          generatedType = genApply(app, expectedType)

        case This(qual) =>
          val symIsModuleClass = tree.symbol.is(ModuleClass)
          assert(tree.symbol == claszSymbol || symIsModuleClass,
                 s"Trying to access the this of another class: tree.symbol = ${tree.symbol}, class symbol = $claszSymbol compilation unit: $cunit")
          if (symIsModuleClass && tree.symbol != claszSymbol) {
            generatedType = genLoadModule(tree)
          }
          else {
            mnode.visitVarInsn(asm.Opcodes.ALOAD, 0)
            // When compiling Array.scala, the constructor invokes `Array.this.super.`. The expectedType
            // is `[Object` (computed by typeToBType, the type of This(Array) is `Array[T]`). If we would set
            // the generatedType to `Array` below, the call to adapt at the end would fail. The situation is
            // similar for primitives (`I` vs `Int`).
            if (tree.symbol != defn.ArrayClass && !tree.symbol.isPrimitiveValueClass) {
              generatedType = classBTypeFromSymbol(claszSymbol)
            }
          }

        case DesugaredSelect(Ident(nme.EMPTY_PACKAGE), module) =>
          assert(tree.symbol.is(Module), s"Selection of non-module from empty package: $tree sym: ${tree.symbol} at: ${tree.span}")
          genLoadModule(tree)

        case DesugaredSelect(qualifier, _) =>
          val sym = tree.symbol
          generatedType = symInfoTK(sym)
          val qualSafeToElide = tpd.isIdempotentExpr(qualifier)

          def genLoadQualUnlessElidable(): Unit = { if (!qualSafeToElide) { genLoadQualifier(tree) } }

          // receiverClass is used in the bytecode to access the field. using sym.owner may lead to IllegalAccessError
          def receiverClass = qualifier.tpe.typeSymbol
          if (sym.is(Module)) {
            genLoadQualUnlessElidable()
            genLoadModule(tree)
          } else if (sym.isStaticMember) {
            genLoadQualUnlessElidable()
            fieldLoad(sym, receiverClass)
          } else {
            genLoadQualifier(tree)
            fieldLoad(sym, receiverClass)
          }

        case t @ Ident(name) =>
          val sym = tree.symbol
          val tk = symInfoTK(sym)
          generatedType = tk

          val desugared = cachedDesugarIdent(t)
          desugared match {
            case None =>
              if (!sym.is(Package)) {
                if (sym.is(Module)) genLoadModule(sym)
                else locals.load(sym)
              }
            case Some(t) =>
              genLoad(t, generatedType)
          }

        case Literal(value) =>
          if (value.tag != UnitTag) (value.tag, expectedType) match {
            case (IntTag,   LONG  ) => bc.lconst(value.longValue);       generatedType = LONG
            case (FloatTag, DOUBLE) => bc.dconst(value.doubleValue);     generatedType = DOUBLE
            case (NullTag,  _     ) => bc.emit(asm.Opcodes.ACONST_NULL); generatedType = srNullRef
            case _                  => genConstant(value);               generatedType = tpeTK(tree)
          }

        case blck @ Block(stats, expr) =>
          if(stats.isEmpty)
            genLoadTo(expr, expectedType, dest)
          else
            genBlockTo(blck, expectedType, dest)
          generatedDest = dest

        case Typed(Super(_, _), _) =>
          genLoadTo(tpd.This(claszSymbol.asClass), expectedType, dest)
          generatedDest = dest

        case Typed(expr, _) =>
          genLoadTo(expr, expectedType, dest)
          generatedDest = dest

        case Assign(_, _) =>
          generatedType = UNIT
          genStat(tree)

        case av @ ArrayValue(_, _) =>
          generatedType = genArrayValue(av)

        case mtch @ Match(_, _) =>
          generatedType = genMatchTo(mtch, expectedType, dest)
          generatedDest = dest

        case tpd.EmptyTree => if (expectedType != UNIT) { emitZeroOf(expectedType) }


        case t: TypeApply => // dotty specific
          generatedType = genTypeApply(t)

        case _ => abort(s"Unexpected tree in genLoad: $tree/${tree.getClass} at: ${tree.span}")
      }

      // emit conversion and send to the right destination
      if generatedDest == LoadDestination.FallThrough then
        genAdaptAndSendToDest(generatedType, expectedType, dest)
    end genLoadTo

    def genAdaptAndSendToDest(generatedType: BType, expectedType: BType, dest: LoadDestination): Unit =
      if generatedType != expectedType then
        adapt(generatedType, expectedType)

      dest match
        case LoadDestination.FallThrough =>
          ()
        case LoadDestination.Jump(label, targetStackSize) =>
          val stackDiff = stack.heightDiffWrt(targetStackSize)
          if stackDiff != 0 then
            if expectedType == UNIT then
              bc dropMany stackDiff
            else
              val loc = locals.makeTempLocal(expectedType)
              bc.store(loc.idx, expectedType)
              bc dropMany stackDiff
              bc.load(loc.idx, expectedType)
          end if
          bc goTo label
        case LoadDestination.Return =>
          bc emitRETURN returnType
        case LoadDestination.Throw =>
          val thrownType = expectedType
          // `throw null` is valid although scala.Null (as defined in src/libray-aux) isn't a subtype of Throwable.
          // Similarly for scala.Nothing (again, as defined in src/libray-aux).
          assert(thrownType.isNullType || thrownType.isNothingType || thrownType.asClassBType.isSubtypeOf(jlThrowableRef))
          emit(asm.Opcodes.ATHROW)
    end genAdaptAndSendToDest

    // ---------------- field load and store ----------------

    /*
     * must-single-thread
     */
    def fieldLoad( field: Symbol, hostClass: Symbol = null): Unit = fieldOp(field, isLoad = true,  hostClass)

    /*
     * must-single-thread
     */
    def fieldStore(field: Symbol, hostClass: Symbol = null): Unit = fieldOp(field, isLoad = false, hostClass)

    /*
     * must-single-thread
     */
    private def fieldOp(field: Symbol, isLoad: Boolean, specificReceiver: Symbol): Unit = {
      val useSpecificReceiver = specificReceiver != null && !field.isScalaStatic

      val owner      = internalName(if (useSpecificReceiver) specificReceiver else field.owner)
      val fieldJName = field.javaSimpleName
      val fieldDescr = symInfoTK(field).descriptor
      val isStatic   = field.isStaticMember
      val opc =
        if (isLoad) { if (isStatic) asm.Opcodes.GETSTATIC else asm.Opcodes.GETFIELD }
        else        { if (isStatic) asm.Opcodes.PUTSTATIC else asm.Opcodes.PUTFIELD }
      mnode.visitFieldInsn(opc, owner, fieldJName, fieldDescr)

    }

    // ---------------- emitting constant values ----------------

    /*
     * For ClazzTag:
     *   must-single-thread
     * Otherwise it's safe to call from multiple threads.
     */
    def genConstant(const: Constant): Unit = {
      (const.tag/*: @switch*/) match {

        case BooleanTag => bc.boolconst(const.booleanValue)

        case ByteTag    => bc.iconst(const.byteValue)
        case ShortTag   => bc.iconst(const.shortValue)
        case CharTag    => bc.iconst(const.charValue)
        case IntTag     => bc.iconst(const.intValue)

        case LongTag    => bc.lconst(const.longValue)
        case FloatTag   => bc.fconst(const.floatValue)
        case DoubleTag  => bc.dconst(const.doubleValue)

        case UnitTag    => ()

        case StringTag  =>
          assert(const.value != null, const) // TODO this invariant isn't documented in `case class Constant`
          mnode.visitLdcInsn(const.stringValue) // `stringValue` special-cases null, but not for a const with StringTag

        case NullTag    => emit(asm.Opcodes.ACONST_NULL)

        case ClazzTag   =>
          val tp = toTypeKind(const.typeValue)
          if tp.isPrimitive then
            val boxedClass = boxedClassOfPrimitive(tp.asPrimitiveBType)
            mnode.visitFieldInsn(
              asm.Opcodes.GETSTATIC,
              boxedClass.internalName,
              "TYPE", // field name
              jlClassRef.descriptor
            )
          else
            mnode.visitLdcInsn(tp.toASMType)

        case _ => abort(s"Unknown constant value: $const")
      }
    }

    private def genLabeledTo(tree: Labeled, expectedType: BType, dest: LoadDestination): BType = tree match {
      case Labeled(bind, expr) =>

      val labelSym = bind.symbol

      if dest == LoadDestination.FallThrough then
        val resKind = tpeTK(tree)
        val jumpTarget = new asm.Label
        registerJumpDest(labelSym, resKind, LoadDestination.Jump(jumpTarget, stack.recordSize()))
        genLoad(expr, resKind)
        markProgramPoint(jumpTarget)
        resKind
      else
        registerJumpDest(labelSym, expectedType, dest)
        genLoadTo(expr, expectedType, dest)
        expectedType
      end if
    }

    private def genReturn(r: Return): Unit = {
      val expr: Tree = r.expr
      val fromSym: Symbol = if (r.from.symbol.is(LabelFlag)) r.from.symbol else NoSymbol

      if (NoSymbol == fromSym) {
        // return from enclosing method
        cleanups match {
          case Nil =>
            // not an assertion: !shouldEmitCleanup (at least not yet, pendingCleanups() may still have to run, and reset `shouldEmitCleanup`.
            genLoadTo(expr, returnType, LoadDestination.Return)
          case nextCleanup :: rest =>
            genLoad(expr, returnType)
            lineNumber(r)
            val saveReturnValue = (returnType != UNIT)
            if (saveReturnValue) {
              // regarding return value, the protocol is: in place of a `return-stmt`, a sequence of `adapt, store, jump` are inserted.
              if (earlyReturnVar == null) {
                earlyReturnVar = locals.makeLocal(returnType, "earlyReturnVar", expr.tpe, expr.span)
              }
              locals.store(earlyReturnVar)
            }
            bc goTo nextCleanup
            shouldEmitCleanup = true
        }
      } else {
        // return from labeled
        assert(fromSym.is(LabelFlag), fromSym)
        assert(!fromSym.is(Method), fromSym)

        /* TODO At the moment, we disregard cleanups, because by construction we don't have return-from-labels
         * that cross cleanup boundaries. However, in theory such crossings are valid, so we should take care
         * of them.
         */
        val (exprExpectedType, exprDest) = findJumpDest(fromSym)
        genLoadTo(expr, exprExpectedType, exprDest)
      }
    } // end of genReturn()

    def genWhileDo(tree: WhileDo): LoadDestination = tree match{
      case WhileDo(cond, body) =>

      val isInfinite = cond == tpd.EmptyTree

      val loop = new asm.Label
      markProgramPoint(loop)

      if isInfinite then
        val dest = LoadDestination.Jump(loop, stack.recordSize())
        genLoadTo(body, UNIT, dest)
        dest
      else
        body match
          case Literal(value) if value.tag == UnitTag =>
            // this is the shape of do..while loops
            val exitLoop = new asm.Label
            genCond(cond, loop, exitLoop, targetIfNoJump = exitLoop)
            markProgramPoint(exitLoop)
          case _ =>
            val success = new asm.Label
            val failure = new asm.Label
            genCond(cond, success, failure, targetIfNoJump = success)
            markProgramPoint(success)
            genLoadTo(body, UNIT, LoadDestination.Jump(loop, stack.recordSize()))
            markProgramPoint(failure)
        end match
        LoadDestination.FallThrough
    }

    def genTypeApply(t: TypeApply): BType = (t: @unchecked) match {
      case TypeApply(fun@DesugaredSelect(obj, _), targs) =>

        val sym = fun.symbol
        val cast =
          if (sym == defn.Any_isInstanceOf) false
          else if (sym == defn.Any_asInstanceOf) true
          else abort(s"Unexpected type application $fun[sym: ${sym.showFullName}] in: $t")
        val l = tpeTK(obj)
        val r = tpeTK(targs.head)
        genLoadQualifier(fun)

        // TODO @lry make pattern match
        if (l.isPrimitive && r.isPrimitive)
          genConversion(l, r, cast)
        else if (l.isPrimitive) {
          bc drop l
          if (cast) {
            mnode.visitTypeInsn(asm.Opcodes.NEW, jlClassCastExceptionRef.internalName)
            bc dup ObjectRef
            emit(asm.Opcodes.ATHROW)
          } else {
            bc boolconst false
          }
        }
        else if (r.isPrimitive && cast) {
          abort(s"Erasure should have added an unboxing operation to prevent this cast. Tree: $t")
        }
        else if (r.isPrimitive) {
          bc isInstance boxedClassOfPrimitive(r.asPrimitiveBType)
        }
        else {
          assert(r.isRef, r) // ensure that it's not a method
          genCast(r.asRefBType, cast)
        }

        if (cast) r else BOOL
    } // end of genTypeApply()


    private def mkArrayConstructorCall(arr: ArrayBType, app: Apply, args: List[Tree]) = {
      val dims     = arr.dimension
      var elemKind = arr.elementType
      val argsSize = args.length
      if (argsSize > dims) {
        report.error(em"too many arguments for array constructor: found ${args.length} but array has only $dims dimension(s)", ctx.source.atSpan(app.span))
      }
      if (argsSize < dims) {
        /* In one step:
         *   elemKind = new BType(BType.ARRAY, arr.off + argsSize, arr.len - argsSize)
         * however the above does not enter a TypeName for each nested arrays in chrs.
         */
        for (i <- args.length until dims) elemKind = ArrayBType(elemKind)
      }
      genLoadArguments(args, List.fill(args.size)(INT))
      (argsSize /*: @switch*/) match {
        case 1 => bc newarray elemKind
        case _ =>
          val descr = ("[" * argsSize) + elemKind.descriptor // denotes the same as: arrayN(elemKind, argsSize).descriptor
          mnode.visitMultiANewArrayInsn(descr, argsSize)
      }
    }


    private def genApply(app: Apply, expectedType: BType): BType = {
      var generatedType = expectedType
      lineNumber(app)
      app match {
        case Apply(_, args) if app.symbol eq defn.newArrayMethod =>
          val List(elemClaz, Literal(c: Constant), ArrayValue(_, dims)) = args: @unchecked

          generatedType = toTypeKind(c.typeValue)
          mkArrayConstructorCall(generatedType.asArrayBType, app, dims)
        case Apply(t :TypeApply, _) =>
          generatedType =
            if (t.symbol ne defn.Object_synchronized) genTypeApply(t)
            else genSynchronized(app, expectedType)

        case Apply(fun @ DesugaredSelect(Super(superQual, _), _), args) =>
          // 'super' call: Note: since constructors are supposed to
          // return an instance of what they construct, we have to take
          // special care. On JVM they are 'void', and Scala forbids (syntactically)
          // to call super constructors explicitly and/or use their 'returned' value.
          // therefore, we can ignore this fact, and generate code that leaves nothing
          // on the stack (contrary to what the type in the AST says).

          // scala/bug#10290: qual can be `this.$outer()` (not just `this`), so we call genLoad (not just ALOAD_0)
          val superQualTK = genLoad(superQual)
          stack.push(superQualTK)
          genLoadArguments(args, paramTKs(app))
          stack.pop()
          generatedType = genCallMethod(fun.symbol, InvokeStyle.Super, app.span)

        // 'new' constructor call: Note: since constructors are
        // thought to return an instance of what they construct,
        // we have to 'simulate' it by DUPlicating the freshly created
        // instance (on JVM,  methods return VOID).
        case Apply(fun @ DesugaredSelect(New(tpt), nme.CONSTRUCTOR), args) =>
          val ctor = fun.symbol
          assert(ctor.isClassConstructor, s"'new' call to non-constructor: ${ctor.name}")

          generatedType = toTypeKind(tpt.tpe)
          assert(generatedType.isRef, s"Non reference type cannot be instantiated: $generatedType")

          generatedType match {
            case arr: ArrayBType =>
              mkArrayConstructorCall(arr, app, args)

            case rt: ClassBType =>
              assert(classBTypeFromSymbol(ctor.owner) == rt, s"Symbol ${ctor.owner.showFullName} is different from $rt")
              mnode.visitTypeInsn(asm.Opcodes.NEW, rt.internalName)
              bc dup generatedType
              stack.push(rt)
              stack.push(rt)
              genLoadArguments(args, paramTKs(app))
              stack.pop(2)
              genCallMethod(ctor, InvokeStyle.Special, app.span)

            case _ =>
              abort(s"Cannot instantiate $tpt of kind: $generatedType")
          }

        case Apply(fun, List(expr)) if Erasure.Boxing.isBox(fun.symbol) && fun.symbol.denot.owner != defn.UnitModuleClass =>
          val nativeKind = tpeTK(expr)
          genLoad(expr, nativeKind)
          val MethodNameAndType(mname, methodType) = asmBoxTo(nativeKind)
          bc.invokestatic(srBoxesRuntimeRef.internalName, mname, methodType.descriptor, itf = false)
          generatedType = boxResultType(fun.symbol) // was toTypeKind(fun.symbol.tpe.resultType)

        case Apply(fun, List(expr)) if Erasure.Boxing.isUnbox(fun.symbol) && fun.symbol.denot.owner != defn.UnitModuleClass =>
          genLoad(expr)
          val boxType = unboxResultType(fun.symbol) // was toTypeKind(fun.symbol.owner.linkedClassOfClass.tpe)
          generatedType = boxType
          val MethodNameAndType(mname, methodType) = asmUnboxTo(boxType)
          bc.invokestatic(srBoxesRuntimeRef.internalName, mname, methodType.descriptor, itf = false)

        case app @ Apply(fun, args) =>
          val sym = fun.symbol

          if (isPrimitive(fun)) { // primitive method call
            generatedType = genPrimitiveOp(app, expectedType)
          } else { // normal method call
            val invokeStyle =
              if (sym.isStaticMember) InvokeStyle.Static
              else if (sym.is(Private) || sym.isClassConstructor) InvokeStyle.Special
              else if (app.hasAttachment(BCodeHelpers.UseInvokeSpecial)) InvokeStyle.Special
              else InvokeStyle.Virtual

            val savedStackSize = stack.recordSize()
            if invokeStyle.hasInstance then
              stack.push(genLoadQualifier(fun))
            genLoadArguments(args, paramTKs(app))
            stack.restoreSize(savedStackSize)

            val DesugaredSelect(qual, name) = fun: @unchecked // fun is a Select, also checked in genLoadQualifier
            val isArrayClone = name == nme.clone_ && qual.tpe.widen.isInstanceOf[JavaArrayType]
            if (isArrayClone) {
              // Special-case Array.clone, introduced in 36ef60e. The goal is to generate this call
              // as "[I.clone" instead of "java/lang/Object.clone". This is consistent with javac.
              // Arrays have a public method `clone` (jls 10.7).
              //
              // The JVMS is not explicit about this, but that receiver type can be an array type
              // descriptor (instead of a class internal name):
              //   invokevirtual  #2; //Method "[I".clone:()Ljava/lang/Object
              //
              // Note that using `Object.clone()` would work as well, but only because the JVM
              // relaxes protected access specifically if the receiver is an array:
              //   http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/interpreter/linkResolver.cpp#l439
              // Example: `class C { override def clone(): Object = "hi" }`
              // Emitting `def f(c: C) = c.clone()` as `Object.clone()` gives a VerifyError.
              val target: String = tpeTK(qual).asRefBType.classOrArrayType
              val methodBType = asmMethodType(sym)
              bc.invokevirtual(target, sym.javaSimpleName, methodBType.descriptor)
              generatedType = methodBType.returnType
            } else {
              val receiverClass = if (!invokeStyle.isVirtual) null else {
                // receiverClass is used in the bytecode to as the method receiver. using sym.owner
                // may lead to IllegalAccessErrors, see 9954eaf / aladdin bug 455.
                val qualSym = qual.tpe.typeSymbol
                if (qualSym == defn.ArrayClass) {
                  // For invocations like `Array(1).hashCode` or `.wait()`, use Object as receiver
                  // in the bytecode. Using the array descriptor (like we do for clone above) seems
                  // to work as well, but it seems safer not to change this. Javac also uses Object.
                  // Note that array apply/update/length are handled by isPrimitive (above).
                  assert(sym.owner == defn.ObjectClass, s"unexpected array call: $app")
                  defn.ObjectClass
                } else qualSym
              }
              generatedType = genCallMethod(sym, invokeStyle, app.span, receiverClass)
            }
          }
      }

      generatedType
    } // end of genApply()

    private def genArrayValue(av: tpd.JavaSeqLiteral): BType = {
      val ArrayValue(tpt, elems) = av: @unchecked

      lineNumber(av)
      genArray(elems, tpt)
    }

    private def genArray(elems: List[Tree], elemType: Type): BType = {
      val elmKind       = toTypeKind(elemType)
      val generatedType = ArrayBType(elmKind)

      bc iconst   elems.length
      bc newarray elmKind

      // during the genLoad below, there is the result, its dup, and the index
      stack.push(generatedType)
      stack.push(generatedType)
      stack.push(INT)

      var i = 0
      var rest = elems
      while (!rest.isEmpty) {
        bc dup     generatedType
        bc iconst  i
        genLoad(rest.head, elmKind)
        bc astore  elmKind
        rest = rest.tail
        i = i + 1
      }

      stack.pop(3)

      generatedType
    }

    /* A Match node contains one or more case clauses, each case clause lists one or more
     * Int/String values to use as keys, and a code block. The exception is the "default" case
     * clause which doesn't list any key (there is exactly one of these per match).
     */
    private def genMatchTo(tree: Match, expectedType: BType, dest: LoadDestination): BType = tree match {
      case Match(selector, cases) =>
      lineNumber(tree)

      val (generatedType, postMatch, postMatchDest) =
        if dest == LoadDestination.FallThrough then
          val postMatch = new asm.Label
          (tpeTK(tree), postMatch, LoadDestination.Jump(postMatch, stack.recordSize()))
        else
          (expectedType, null, dest)

      // Only two possible selector types exist in `Match` trees at this point: Int and String
      if (tpeTK(selector) == INT) {

        /* On a first pass over the case clauses, we flatten the keys and their
         * targets (the latter represented with asm.Labels). That representation
         * allows JCodeMethodV to emit a lookupswitch or a tableswitch.
         *
         * On a second pass, we emit the switch blocks, one for each different target.
         */

        var flatKeys: List[Int]       = Nil
        var targets:  List[asm.Label] = Nil
        var default:  asm.Label       = null
        var switchBlocks: List[(asm.Label, Tree)] = Nil

        genLoad(selector, INT)

        // collect switch blocks and their keys, but don't emit yet any switch-block.
        for (caze @ CaseDef(pat, guard, body) <- cases) {
          assert(guard == tpd.EmptyTree, guard)
          val switchBlockPoint = new asm.Label
          switchBlocks ::= (switchBlockPoint, body)
          pat match {
            case Literal(value) =>
              flatKeys ::= value.intValue
              targets  ::= switchBlockPoint
            case Ident(nme.WILDCARD) =>
              assert(default == null, s"multiple default targets in a Match node, at ${tree.span}")
              default = switchBlockPoint
            case Alternative(alts) =>
              alts foreach {
                case Literal(value) =>
                  flatKeys ::= value.intValue
                  targets  ::= switchBlockPoint
                case _ =>
                  abort(s"Invalid alternative in alternative pattern in Match node: $tree at: ${tree.span}")
              }
            case _ =>
              abort(s"Invalid pattern in Match node: $tree at: ${tree.span}")
          }
        }

        bc.emitSWITCH(mkArrayReverse(flatKeys), mkArrayL(targets.reverse), default, MIN_SWITCH_DENSITY)

        // emit switch-blocks.
        for (sb <- switchBlocks.reverse) {
          val (caseLabel, caseBody) = sb
          markProgramPoint(caseLabel)
          genLoadTo(caseBody, generatedType, postMatchDest)
        }
      } else {

        /* Since the JVM doesn't have a way to switch on a string, we  switch
         * on the `hashCode` of the string then do an `equals` check (with a
         * possible second set of jumps if blocks can be reach from multiple
         * string alternatives).
         *
         * This mirrors the way that Java compiles `switch` on Strings.
         */

        var default:  asm.Label       = null
        var indirectBlocks: List[(asm.Label, Tree)] = Nil


        // Cases grouped by their hashCode
        val casesByHash = SortedMap.empty[Int, List[(String, Either[asm.Label, Tree])]]
        var caseFallback: Tree = null

        for (caze @ CaseDef(pat, guard, body) <- cases) {
          assert(guard == tpd.EmptyTree, guard)
          pat match {
            case Literal(value) =>
              val strValue = value.stringValue
              casesByHash.updateWith(strValue.##) { existingCasesOpt =>
                val newCase = (strValue, Right(body))
                Some(newCase :: existingCasesOpt.getOrElse(Nil))
              }
            case Ident(nme.WILDCARD) =>
              assert(default == null, s"multiple default targets in a Match node, at ${tree.span}")
              default = new asm.Label
              indirectBlocks ::= (default, body)
            case Alternative(alts) =>
              // We need an extra basic block since multiple strings can lead to this code
              val indirectCaseGroupLabel = new asm.Label
              indirectBlocks ::= (indirectCaseGroupLabel, body)
              alts foreach {
                case Literal(value) =>
                  val strValue = value.stringValue
                  casesByHash.updateWith(strValue.##) { existingCasesOpt =>
                    val newCase = (strValue, Left(indirectCaseGroupLabel))
                    Some(newCase :: existingCasesOpt.getOrElse(Nil))
                  }
                case _ =>
                  abort(s"Invalid alternative in alternative pattern in Match node: $tree at: ${tree.span}")
              }

            case _ =>
              abort(s"Invalid pattern in Match node: $tree at: ${tree.span}")
          }
        }

        // Organize the hashCode options into switch cases
        var flatKeys: List[Int]       = Nil
        var targets:  List[asm.Label] = Nil
        var hashBlocks: List[(asm.Label, List[(String, Either[asm.Label, Tree])])] = Nil
        for ((hashValue, hashCases) <- casesByHash) {
          val switchBlockPoint = new asm.Label
          hashBlocks ::= (switchBlockPoint, hashCases)
          flatKeys ::= hashValue
          targets  ::= switchBlockPoint
        }

        // Push the hashCode of the string (or `0` it is `null`) onto the stack and switch on it
        genLoadIfTo(
          If(
            tree.selector.select(defn.Any_==).appliedTo(nullLiteral),
            Literal(Constant(0)),
            tree.selector.select(defn.Any_hashCode).appliedToNone
          ),
          INT,
          LoadDestination.FallThrough
        )
        bc.emitSWITCH(mkArrayReverse(flatKeys), mkArrayL(targets.reverse), default, MIN_SWITCH_DENSITY)

        // emit blocks for each hash case
        for ((hashLabel, caseAlternatives) <- hashBlocks.reverse) {
          markProgramPoint(hashLabel)
          for ((caseString, indirectLblOrBody) <- caseAlternatives) {
            val comparison = if (caseString == null) defn.Any_== else defn.Any_equals
            val condp = Literal(Constant(caseString)).select(defn.Any_==).appliedTo(tree.selector)
            val keepGoing = new asm.Label
            indirectLblOrBody match {
              case Left(jump) =>
                genCond(condp, jump, keepGoing, targetIfNoJump = keepGoing)

              case Right(caseBody) =>
                val thisCaseMatches = new asm.Label
                genCond(condp, thisCaseMatches, keepGoing, targetIfNoJump = thisCaseMatches)
                markProgramPoint(thisCaseMatches)
                genLoadTo(caseBody, generatedType, postMatchDest)
            }
            markProgramPoint(keepGoing)
          }
          bc goTo default
        }

        // emit blocks for common patterns
        for ((caseLabel, caseBody) <- indirectBlocks.reverse) {
          markProgramPoint(caseLabel)
          genLoadTo(caseBody, generatedType, postMatchDest)
        }
      }

      if postMatch != null then
        markProgramPoint(postMatch)
      generatedType
    }

    def genBlockTo(tree: Block, expectedType: BType, dest: LoadDestination): Unit = tree match {
      case Block(stats, expr) =>

      val savedScope = varsInScope
      varsInScope = Nil
      stats foreach genStat
      genLoadTo(expr, expectedType, dest)
      emitLocalVarScopes()
      varsInScope = savedScope
    }

    /** Add entries to the `LocalVariableTable` JVM attribute for all the vars in
     *  `varsInScope`, ending at the current program point.
     */
    def emitLocalVarScopes(): Unit =
      if (emitVars) {
        val end = currProgramPoint()
        for ((sym, start) <- varsInScope.reverse) {
          emitLocalVarScope(sym, start, end)
        }
      }
    end emitLocalVarScopes

    def adapt(from: BType, to: BType): Unit = {
      if (!from.conformsTo(to)) {
        to match {
          case UNIT => bc drop from
          case _    => bc.emitT2T(from, to)
        }
      } else if (from.isNothingType) {
        /* There are two possibilities for from.isNothingType: emitting a "throw e" expressions and
         * loading a (phantom) value of type Nothing.
         *
         * The Nothing type in Scala's type system does not exist in the JVM. In bytecode, Nothing
         * is mapped to scala.runtime.Nothing$. To the JVM, a call to Predef.??? looks like it would
         * return an object of type Nothing$. We need to do something with that phantom object on
         * the stack. "Phantom" because it never exists: such methods always throw, but the JVM does
         * not know that.
         *
         * Note: The two verifiers (old: type inference, new: type checking) have different
         * requirements. Very briefly:
         *
         * Old (http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.10.2.1): at
         * each program point, no matter what branches were taken to get there
         *   - Stack is same size and has same typed values
         *   - Local and stack values need to have consistent types
         *   - In practice, the old verifier seems to ignore unreachable code and accept any
         *     instructions after an ATHROW. For example, there can be another ATHROW (without
         *     loading another throwable first).
         *
         * New (http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.10.1)
         *   - Requires consistent stack map frames. GenBCode generates stack frames if -target:jvm-1.6
         *     or higher.
         *   - In practice: the ASM library computes stack map frames for us (ClassWriter). Emitting
         *     correct frames after an ATHROW is probably complex, so ASM uses the following strategy:
         *       - Every time when generating an ATHROW, a new basic block is started.
         *       - During classfile writing, such basic blocks are found to be dead: no branches go there
         *       - Eliminating dead code would probably require complex shifts in the output byte buffer
         *       - But there's an easy solution: replace all code in the dead block with
         *         `nop; nop; ... nop; athrow`, making sure the bytecode size stays the same
         *       - The corresponding stack frame can be easily generated: on entering a dead the block,
         *         the frame requires a single Throwable on the stack.
         *       - Since there are no branches to the dead block, the frame requirements are never violated.
         *
         * To summarize the above: it does matter what we emit after an ATHROW.
         *
         * NOW: if we end up here because we emitted a load of a (phantom) value of type Nothing$,
         * there was no ATHROW emitted. So, we have to make the verifier happy and do something
         * with that value. Since Nothing$ extends Throwable, the easiest is to just emit an ATHROW.
         *
         * If we ended up here because we generated a "throw e" expression, we know the last
         * emitted instruction was an ATHROW. As explained above, it is OK to emit a second ATHROW,
         * the verifiers will be happy.
         */
        if (lastInsn.getOpcode != asm.Opcodes.ATHROW)
          emit(asm.Opcodes.ATHROW)
      } else if (from.isNullType) {
        /* After loading an expression of type `scala.runtime.Null$`, introduce POP; ACONST_NULL.
         * This is required to pass the verifier: in Scala's type system, Null conforms to any
         * reference type. In bytecode, the type Null is represented by scala.runtime.Null$, which
         * is not a subtype of all reference types. Example:
         *
         *   def nl: Null = null // in bytecode, nl has return type scala.runtime.Null$
         *   val a: String = nl  // OK for Scala but not for the JVM, scala.runtime.Null$ does not conform to String
         *
         * In order to fix the above problem, the value returned by nl is dropped and ACONST_NULL is
         * inserted instead - after all, an expression of type scala.runtime.Null$ can only be null.
         */
        if (lastInsn.getOpcode != asm.Opcodes.ACONST_NULL) {
          bc drop from
          emit(asm.Opcodes.ACONST_NULL)
        }
      }
      else (from, to) match  {
        case (BYTE, LONG) | (SHORT, LONG) | (CHAR, LONG) | (INT, LONG) => bc.emitT2T(INT, LONG)
        case _ => ()
      }
    }

    /* Emit code to Load the qualifier of `tree` on top of the stack. */
    def genLoadQualifier(tree: Tree): BType = {
      lineNumber(tree)
      tree match {
        case DesugaredSelect(qualifier, _) => genLoad(qualifier)
        case t: Ident             => // dotty specific
          cachedDesugarIdent(t) match {
            case Some(sel) => genLoadQualifier(sel)
            case None =>
              assert(t.symbol.owner == this.claszSymbol)
              UNIT
          }
        case _                    => abort(s"Unknown qualifier $tree")
      }
    }

    def genLoadArguments(args: List[Tree], btpes: List[BType]): Unit =
      @tailrec def loop(args: List[Tree], btpes: List[BType]): Unit =
        args match
          case arg :: args1 =>
            btpes match
              case btpe :: btpes1 =>
                genLoad(arg, btpe)
                stack.push(btpe)
                loop(args1, btpes1)
              case _ =>
          case _ =>

      val savedStackSize = stack.recordSize()
      loop(args, btpes)
      stack.restoreSize(savedStackSize)
    end genLoadArguments

    def genLoadModule(tree: Tree): BType = {
      val module = (
        if (!tree.symbol.is(PackageClass)) tree.symbol
        else tree.symbol.info.member(nme.PACKAGE).symbol match {
          case NoSymbol => abort(s"SI-5604: Cannot use package as value: $tree")
          case s        => abort(s"SI-5604: found package class where package object expected: $tree")
        }
      )
      lineNumber(tree)
      genLoadModule(module)
      symInfoTK(module)
    }

    def genLoadModule(module: Symbol): Unit = {
      def inStaticMethod = methSymbol != null && methSymbol.isStaticMember
      if (claszSymbol == module.moduleClass && jMethodName != "readResolve" && !inStaticMethod) {
        mnode.visitVarInsn(asm.Opcodes.ALOAD, 0)
      } else {
        val mbt = symInfoTK(module).asClassBType
        mnode.visitFieldInsn(
          asm.Opcodes.GETSTATIC,
          mbt.internalName /* + "$" */ ,
          str.MODULE_INSTANCE_FIELD,
          mbt.descriptor // for nostalgics: toTypeKind(module.tpe).descriptor
        )
      }
    }

    def genConversion(from: BType, to: BType, cast: Boolean): Unit = {
      if (cast) { bc.emitT2T(from, to) }
      else {
        bc drop from
        bc boolconst (from == to)
      }
    }

    def genCast(to: RefBType, cast: Boolean): Unit = {
      if (cast) { bc checkCast  to }
      else      { bc isInstance to }
    }

    /* Is the given symbol a primitive operation? */
    def isPrimitive(fun: Tree): Boolean = {
      primitives.isPrimitive(fun)
    }

    /* Generate coercion denoted by "code" */
    def genCoercion(code: Int): Unit = {
      import ScalaPrimitivesOps.*
      (code: @switch) match {
        case B2B | S2S | C2C | I2I | L2L | F2F | D2D => ()
        case _ =>
          val from = coercionFrom(code)
          val to   = coercionTo(code)
          bc.emitT2T(from, to)
      }
    }

    /* Generate string concatenation
     *
     * On JDK 8: create and append using `StringBuilder`
     * On JDK 9+: use `invokedynamic` with `StringConcatFactory`
     */
    def genStringConcat(tree: Tree): BType = {
      lineNumber(tree)
      liftStringConcat(tree) match {
        // Optimization for expressions of the form "" + x
        case List(Literal(Constant("")), arg) =>
          genLoad(arg, ObjectRef)
          genCallMethod(defn.String_valueOf_Object, InvokeStyle.Static)

        case concatenations =>
          val concatArguments = concatenations.view
            .filter {
              case Literal(Constant("")) => false // empty strings are no-ops in concatenation
              case _ => true
            }
            .map {
              case Apply(boxOp, value :: Nil) if Erasure.Boxing.isBox(boxOp.symbol) && boxOp.symbol.denot.owner != defn.UnitModuleClass =>
                // Eliminate boxing of primitive values. Boxing is introduced by erasure because
                // there's only a single synthetic `+` method "added" to the string class.
                value
              case other => other
            }
            .toList

          // `StringConcatFactory` only got added in JDK 9, so use `StringBuilder` for lower
          if (backendUtils.classfileVersion < asm.Opcodes.V9) {

            // Estimate capacity needed for the string builder
            val approxBuilderSize = concatArguments.view.map {
              case Literal(Constant(s: String)) => s.length
              case Literal(c @ Constant(_)) if c.isNonUnitAnyVal => String.valueOf(c).length
              case _ => 0
            }.sum
            bc.genNewStringBuilder(approxBuilderSize)

            stack.push(jlStringBuilderRef) // during the genLoad below, there is a reference to the StringBuilder on the stack
            for (elem <- concatArguments) {
              val elemType = tpeTK(elem)
              genLoad(elem, elemType)
              bc.genStringBuilderAppend(elemType)
            }
            stack.pop()

            bc.genStringBuilderEnd
          } else {

            /* `StringConcatFactory#makeConcatWithConstants` accepts max 200 argument slots. If
             * the string concatenation is longer (unlikely), we spill into multiple calls
             */
            val MaxIndySlots = 200
            val TagArg = '\u0001'    // indicates a hole (in the recipe string) for an argument
            val TagConst = '\u0002'  // indicates a hole (in the recipe string) for a constant

            val recipe = new StringBuilder()
            val argTypes = Seq.newBuilder[asm.Type]
            val constVals = Seq.newBuilder[String]
            var totalArgSlots = 0
            var countConcats = 1     // ie. 1 + how many times we spilled

            val savedStackSize = stack.recordSize()

            for (elem <- concatArguments) {
              val tpe = tpeTK(elem)
              val elemSlots = tpe.size

              // Unlikely spill case
              if (totalArgSlots + elemSlots >= MaxIndySlots) {
                stack.restoreSize(savedStackSize)
                for _ <- 0 until countConcats do
                  stack.push(StringRef)
                bc.genIndyStringConcat(recipe.toString, argTypes.result(), constVals.result())
                countConcats += 1
                totalArgSlots = 0
                recipe.setLength(0)
                argTypes.clear()
                constVals.clear()
              }

              elem match {
                case Literal(Constant(s: String)) =>
                  if (s.contains(TagArg) || s.contains(TagConst)) {
                    totalArgSlots += elemSlots
                    recipe.append(TagConst)
                    constVals += s
                  } else {
                    recipe.append(s)
                  }

                case other =>
                  totalArgSlots += elemSlots
                  recipe.append(TagArg)
                  val tpe = tpeTK(elem)
                  argTypes += tpe.toASMType
                  genLoad(elem, tpe)
                  stack.push(tpe)
              }
            }
            stack.restoreSize(savedStackSize)
            bc.genIndyStringConcat(recipe.toString, argTypes.result(), constVals.result())

            // If we spilled, generate one final concat
            if (countConcats > 1) {
              bc.genIndyStringConcat(
                TagArg.toString * countConcats,
                Seq.fill(countConcats)(StringRef.toASMType),
                Seq.empty
              )
            }
          }
      }
      StringRef
    }

    /**
     * Generate a method invocation. If `specificReceiver != null`, it is used as receiver in the
     * invocation instruction, otherwise `method.owner`. A specific receiver class is needed to
     * prevent an IllegalAccessError, (aladdin bug 455).
     */
    def genCallMethod(method: Symbol, style: InvokeStyle, pos: Span = NoSpan, specificReceiver: Symbol = null): BType = {
      val methodOwner = method.owner

      // the class used in the invocation's method descriptor in the classfile
      val receiverClass = {
        if (specificReceiver != null)
          assert(style.isVirtual || specificReceiver == methodOwner, s"specificReceiver can only be specified for virtual calls. $method - $specificReceiver")

        val useSpecificReceiver = specificReceiver != null && !defn.isBottomClass(specificReceiver) && !method.isScalaStatic
        val receiver = if (useSpecificReceiver) specificReceiver else methodOwner

        // workaround for a JVM bug: https://bugs.openjdk.java.net/browse/JDK-8154587
        // when an interface method overrides a member of Object (note that all interfaces implicitly
        // have superclass Object), the receiver needs to be the interface declaring the override (and
        // not a sub-interface that inherits it). example:
        //   trait T { override def clone(): Object = "" }
        //   trait U extends T
        //   class C extends U
        //   class D { def f(u: U) = u.clone() }
        // The invocation `u.clone()` needs `T` as a receiver:
        //   - using Object is illegal, as Object.clone is protected
        //   - using U results in a `NoSuchMethodError: U.clone. This is the JVM bug.
        // Note that a mixin forwarder is generated, so the correct method is executed in the end:
        //   class C { override def clone(): Object = super[T].clone() }
        val isTraitMethodOverridingObjectMember = {
          receiver != methodOwner && // fast path - the boolean is used to pick either of these two, if they are the same it does not matter
            style.isVirtual &&
            isEmittedInterface(receiver) &&
            defn.ObjectType.decl(method.name).symbol.exists && { // fast path - compute overrideChain on the next line only if necessary
              val syms = method.allOverriddenSymbols.toList
              !syms.isEmpty && syms.last.owner == defn.ObjectClass
            }
        }
        if (isTraitMethodOverridingObjectMember) methodOwner else receiver
      }

      receiverClass.info // ensure types the type is up to date; erasure may add lateINTERFACE to traits
      val receiverName = internalName(receiverClass)

      val jname    = method.javaSimpleName
      val bmType   = asmMethodType(method)
      val mdescr   = bmType.descriptor

      val isInterface = isEmittedInterface(receiverClass)
      import InvokeStyle.*
      if (style == Super) {
        if (isInterface && !method.is(JavaDefined)) {
          val args = new Array[BType](bmType.argumentTypes.length + 1)
          val ownerBType = toTypeKind(method.owner.info)
          bmType.argumentTypes.copyToArray(args, 1)
          val staticDesc = MethodBType(ownerBType :: bmType.argumentTypes, bmType.returnType).descriptor
          val staticName = traitSuperAccessorName(method)
          bc.invokestatic(receiverName, staticName, staticDesc, isInterface)
        } else {
          bc.invokespecial(receiverName, jname, mdescr, isInterface)
        }
      } else {
        val opc = style match {
          case Static => Opcodes.INVOKESTATIC
          case Special => Opcodes.INVOKESPECIAL
          case Virtual => if (isInterface) Opcodes.INVOKEINTERFACE else Opcodes.INVOKEVIRTUAL
        }
        bc.emitInvoke(opc, receiverName, jname, mdescr, isInterface)
      }

      bmType.returnType
    } // end of genCallMethod()

    /* Generate the scala ## method. */
    def genScalaHash(tree: Tree): BType = {
      genLoad(tree, ObjectRef)
      genCallMethod(NoSymbol, InvokeStyle.Static) // used to dispatch ## on primitives to ScalaRuntime.hash. Should be implemented by a miniphase
    }

    /*
     * Returns a list of trees that each should be concatenated, from left to right.
     * It turns a chained call like "a".+("b").+("c") into a list of arguments.
     */
    def liftStringConcat(tree: Tree): List[Tree] = tree match {
      case tree @ Apply(fun @ DesugaredSelect(larg, method), rarg) =>
        if (isPrimitive(fun) &&
            primitives.getPrimitive(tree, larg.tpe) == ScalaPrimitivesOps.CONCAT)
          liftStringConcat(larg) ::: rarg
        else
          tree :: Nil
      case _ =>
        tree :: Nil
    }

    /* Emit code to compare the two top-most stack values using the 'op' operator. */
    private def genCJUMP(success: asm.Label, failure: asm.Label, op: TestOp, tk: BType, targetIfNoJump: asm.Label, negated: Boolean = false): Unit = {
      if (targetIfNoJump == success) genCJUMP(failure, success, op.negate(), tk, targetIfNoJump, negated = !negated)
      else {
        if (tk.isIntSizedType) { // BOOL, BYTE, CHAR, SHORT, or INT
          bc.emitIF_ICMP(op, success)
        } else if (tk.isRef) { // REFERENCE(_) | ARRAY(_)
          bc.emitIF_ACMP(op, success)
        } else {
          import Primitives.*
          def useCmpG = if (negated) op == GT || op == GE else op == LT || op == LE
          (tk: @unchecked) match {
            case LONG   => emit(asm.Opcodes.LCMP)
            case FLOAT  => emit(if (useCmpG) asm.Opcodes.FCMPG else asm.Opcodes.FCMPL)
            case DOUBLE => emit(if (useCmpG) asm.Opcodes.DCMPG else asm.Opcodes.DCMPL)
          }
          bc.emitIF(op, success)
        }
        if (targetIfNoJump != failure) bc goTo failure
      }
    }

    /* Emits code to compare (and consume) stack-top and zero using the 'op' operator */
    private def genCZJUMP(success: asm.Label, failure: asm.Label, op: TestOp, tk: BType, targetIfNoJump: asm.Label, negated: Boolean = false): Unit = {
      import Primitives.*
      if (targetIfNoJump == success) genCZJUMP(failure, success, op.negate(), tk, targetIfNoJump, negated = !negated)
      else {
        if (tk.isIntSizedType) { // BOOL, BYTE, CHAR, SHORT, or INT
          bc.emitIF(op, success)
        } else if (tk.isRef) { // REFERENCE(_) | ARRAY(_)
          (op: @unchecked) match { // references are only compared with EQ and NE
            case EQ => bc emitIFNULL    success
            case NE => bc emitIFNONNULL success
          }
        } else {
          def useCmpG = if (negated) op == GT || op == GE else op == LT || op == LE
          (tk: @unchecked) match {
            case LONG   =>
              emit(asm.Opcodes.LCONST_0)
              emit(asm.Opcodes.LCMP)
            case FLOAT  =>
              emit(asm.Opcodes.FCONST_0)
              emit(if (useCmpG) asm.Opcodes.FCMPG else asm.Opcodes.FCMPL)
            case DOUBLE =>
              emit(asm.Opcodes.DCONST_0)
              emit(if (useCmpG) asm.Opcodes.DCMPG else asm.Opcodes.DCMPL)
          }
          bc.emitIF(op, success)
        }
        if (targetIfNoJump != failure) bc goTo failure
      }
    }

    def testOpForPrimitive(primitiveCode: Int) = (primitiveCode: @switch) match {
       case ScalaPrimitivesOps.ID => Primitives.EQ
       case ScalaPrimitivesOps.NI => Primitives.NE
       case ScalaPrimitivesOps.EQ => Primitives.EQ
       case ScalaPrimitivesOps.NE => Primitives.NE
       case ScalaPrimitivesOps.LT => Primitives.LT
       case ScalaPrimitivesOps.LE => Primitives.LE
       case ScalaPrimitivesOps.GT => Primitives.GT
       case ScalaPrimitivesOps.GE => Primitives.GE
     }

    /*
     * Generate code for conditional expressions.
     * The jump targets success/failure of the test are `then-target` and `else-target` resp.
     */
    private def genCond(tree: Tree, success: asm.Label, failure: asm.Label, targetIfNoJump: asm.Label): Unit = {

      def genComparisonOp(l: Tree, r: Tree, code: Int): Unit = {
        val op = testOpForPrimitive(code)
        def isNull(t: Tree): Boolean = t match {
          case Literal(Constant(null)) => true
          case _ => false
        }
        def ifOneIsNull(l: Tree, r: Tree): Tree = if (isNull(l)) r else if (isNull(r)) l else null
        val nonNullSide = if (ScalaPrimitivesOps.isReferenceEqualityOp(code)) ifOneIsNull(l, r) else null
        if (nonNullSide != null) {
          // special-case reference (in)equality test for null (null eq x, x eq null)
          genLoad(nonNullSide, ObjectRef)
          genCZJUMP(success, failure, op, ObjectRef, targetIfNoJump)
        } else {
          val tk = tpeTK(l).maxType(tpeTK(r))
          genLoad(l, tk)
          stack.push(tk)
          genLoad(r, tk)
          stack.pop()
          genCJUMP(success, failure, op, tk, targetIfNoJump)
        }
      }

      def loadAndTestBoolean() = {
        genLoad(tree, BOOL)
        genCZJUMP(success, failure, Primitives.NE, BOOL, targetIfNoJump)
      }

      lineNumber(tree)
      tree match {

        case tree @ Apply(fun, args) if primitives.isPrimitive(fun.symbol) =>
          import ScalaPrimitivesOps.{ ZNOT, ZAND, ZOR, EQ }

          // lhs and rhs of test
          lazy val DesugaredSelect(lhs, _) = fun: @unchecked
          val rhs = if (args.isEmpty) tpd.EmptyTree else args.head // args.isEmpty only for ZNOT

          def genZandOrZor(and: Boolean): Unit = {
            // reaching "keepGoing" indicates the rhs should be evaluated too (ie not short-circuited).
            val keepGoing = new asm.Label

            if (and) genCond(lhs, keepGoing, failure, targetIfNoJump = keepGoing)
            else     genCond(lhs, success,   keepGoing, targetIfNoJump = keepGoing)

            markProgramPoint(keepGoing)
            genCond(rhs, success, failure, targetIfNoJump)
          }

          primitives.getPrimitive(fun.symbol) match {
            case ZNOT   => genCond(lhs, failure, success, targetIfNoJump)
            case ZAND   => genZandOrZor(and = true)
            case ZOR    => genZandOrZor(and = false)
            case code   =>
              if (ScalaPrimitivesOps.isUniversalEqualityOp(code) && tpeTK(lhs).isClass) {
                // rewrite `==` to null tests and `equals`. not needed for arrays (`equals` is reference equality).
                if (code == EQ) genEqEqPrimitive(lhs, rhs, success, failure, targetIfNoJump)
                else            genEqEqPrimitive(lhs, rhs, failure, success, targetIfNoJump)
              } else if (ScalaPrimitivesOps.isComparisonOp(code)) {
                genComparisonOp(lhs, rhs, code)
              } else
                loadAndTestBoolean()
          }

        case Block(stats, expr) =>
          /* Push the decision further down the `expr`.
           * This is particularly effective for the shape of do..while loops.
           */
          val savedScope = varsInScope
          varsInScope = Nil
          stats foreach genStat
          genCond(expr, success, failure, targetIfNoJump)
          emitLocalVarScopes()
          varsInScope = savedScope

        case If(condp, thenp, elsep) =>
          val innerSuccess = new asm.Label
          val innerFailure = new asm.Label
          genCond(condp, innerSuccess, innerFailure, targetIfNoJump = innerSuccess)
          markProgramPoint(innerSuccess)
          genCond(thenp, success, failure, targetIfNoJump = innerFailure)
          markProgramPoint(innerFailure)
          genCond(elsep, success, failure, targetIfNoJump)

        case _ => loadAndTestBoolean()
      }

    } // end of genCond()

    /*
     * Generate the "==" code for object references. It is equivalent of
     * if (l eq null) r eq null else l.equals(r);
     *
     * @param l       left-hand-side  of the '=='
     * @param r       right-hand-side of the '=='
     */
    def genEqEqPrimitive(l: Tree, r: Tree, success: asm.Label, failure: asm.Label, targetIfNoJump: asm.Label): Unit = {

      /* True if the equality comparison is between values that require the use of the rich equality
       * comparator (scala.runtime.Comparator.equals). This is the case when either side of the
       * comparison might have a run-time type subtype of java.lang.Number or java.lang.Character.
       * When it is statically known that both sides are equal and subtypes of Number of Character,
       * not using the rich equality is possible (their own equals method will do ok.)
       */
      val mustUseAnyComparator: Boolean = {
        val areSameFinals = l.tpe.typeSymbol.is(Final) && r.tpe.typeSymbol.is(Final) && (l.tpe =:= r.tpe)
        // todo: remove
        def isMaybeBoxed(sym: Symbol): Boolean = {
          (sym == defn.ObjectClass) ||
            (sym == defn.JavaSerializableClass) ||
            (sym == defn.ComparableClass) ||
            (sym derivesFrom defn.BoxedNumberClass) ||
            (sym derivesFrom defn.BoxedCharClass) ||
            (sym derivesFrom defn.BoxedBooleanClass)
        }
        !areSameFinals && isMaybeBoxed(l.tpe.typeSymbol) && isMaybeBoxed(r.tpe.typeSymbol)
      }
      def isNull(t: Tree): Boolean = t match {
        case Literal(Constant(null)) => true
        case _ => false
      }
      def isNonNullExpr(t: Tree): Boolean = t.isInstanceOf[Literal] || ((t.symbol ne null) && t.symbol.is(Module))

      if (mustUseAnyComparator) {
        val equalsMethod: Symbol = {
          if (l.tpe <:< defn.BoxedNumberClass.info) {
            if (r.tpe <:< defn.BoxedNumberClass.info) defn.BoxesRunTimeModule.requiredMethod(nme.equalsNumNum)
            else if (r.tpe <:< defn.BoxedCharClass.info) defn.BoxesRunTimeModule.requiredMethod(nme.equalsNumChar)
            else defn.BoxesRunTimeModule.requiredMethod(nme.equalsNumObject)
          } else defn.BoxesRunTimeModule_externalEquals
        }

        genLoad(l, ObjectRef)
        stack.push(ObjectRef)
        genLoad(r, ObjectRef)
        stack.pop()
        genCallMethod(equalsMethod, InvokeStyle.Static)
        genCZJUMP(success, failure, Primitives.NE, BOOL, targetIfNoJump)
      }
      else {
        if (isNull(l)) {
          // null == expr -> expr eq null
          genLoad(r, ObjectRef)
          genCZJUMP(success, failure, Primitives.EQ, ObjectRef, targetIfNoJump)
        } else if (isNull(r)) {
          // expr == null -> expr eq null
          genLoad(l, ObjectRef)
          genCZJUMP(success, failure, Primitives.EQ, ObjectRef, targetIfNoJump)
        } else if (isNonNullExpr(l)) {
          // SI-7852 Avoid null check if L is statically non-null.
          genLoad(l, ObjectRef)
          stack.push(ObjectRef)
          genLoad(r, ObjectRef)
          stack.pop()
          genCallMethod(defn.Any_equals, InvokeStyle.Virtual)
          genCZJUMP(success, failure, Primitives.NE, BOOL, targetIfNoJump)
        } else {
          // l == r -> if (l eq null) r eq null else l.equals(r)
          val eqEqTempLocal = locals.makeLocal(ObjectRef, nme.EQEQ_LOCAL_VAR.mangledString, defn.ObjectType, r.span)
          val lNull    = new asm.Label
          val lNonNull = new asm.Label

          genLoad(l, ObjectRef)
          stack.push(ObjectRef)
          genLoad(r, ObjectRef)
          stack.pop()
          locals.store(eqEqTempLocal)
          bc dup ObjectRef
          genCZJUMP(lNull, lNonNull, Primitives.EQ, ObjectRef, targetIfNoJump = lNull)

          markProgramPoint(lNull)
          bc drop ObjectRef
          locals.load(eqEqTempLocal)
          genCZJUMP(success, failure, Primitives.EQ, ObjectRef, targetIfNoJump = lNonNull)

          markProgramPoint(lNonNull)
          locals.load(eqEqTempLocal)
          genCallMethod(defn.Any_equals, InvokeStyle.Virtual)
          genCZJUMP(success, failure, Primitives.NE, BOOL, targetIfNoJump)
        }
      }
    }


    def genSynchronized(tree: Apply, expectedType: BType): BType
    def genLoadTry(tree: Try): BType

    def genInvokeDynamicLambda(ctor: Symbol, lambdaTarget: Symbol, environmentSize: Int, functionalInterface: Symbol): BType = {
      import java.lang.invoke.LambdaMetafactory.{FLAG_BRIDGES, FLAG_SERIALIZABLE}

      report.debuglog(s"Using invokedynamic rather than `new ${ctor.owner}`")
      val generatedType = classBTypeFromSymbol(functionalInterface)
      // Lambdas should be serializable if they implement a SAM that extends Serializable or if they
      // implement a scala.Function* class.
      val isSerializable = functionalInterface.isSerializable || defn.isFunctionClass(functionalInterface)
      val isInterface = isEmittedInterface(lambdaTarget.owner)
      val invokeStyle =
        if (lambdaTarget.isStaticMember) asm.Opcodes.H_INVOKESTATIC
        else if (lambdaTarget.is(Private) || lambdaTarget.isClassConstructor) asm.Opcodes.H_INVOKESPECIAL
        else if (isInterface) asm.Opcodes.H_INVOKEINTERFACE
        else asm.Opcodes.H_INVOKEVIRTUAL

      val targetHandle =
        new asm.Handle(invokeStyle,
          classBTypeFromSymbol(lambdaTarget.owner).internalName,
          lambdaTarget.javaSimpleName,
          asmMethodType(lambdaTarget).descriptor,
          /* itf = */ isInterface)

      val (a,b) = lambdaTarget.info.firstParamTypes.splitAt(environmentSize)
      var (capturedParamsTypes, lambdaParamTypes) = (a,b)

      if (invokeStyle != asm.Opcodes.H_INVOKESTATIC) capturedParamsTypes = lambdaTarget.owner.info :: capturedParamsTypes

      // Requires https://github.com/scala/scala-java8-compat on the runtime classpath
      val returnUnit = lambdaTarget.info.resultType.typeSymbol == defn.UnitClass
      val functionalInterfaceDesc: String = generatedType.descriptor
      val desc = capturedParamsTypes.map(tpe => toTypeKind(tpe)).mkString(("("), "", ")") + functionalInterfaceDesc
      // TODO specialization
      val instantiatedMethodType = new MethodBType(lambdaParamTypes.map(p => toTypeKind(p)), toTypeKind(lambdaTarget.info.resultType)).toASMType

      val samMethod = atPhase(erasurePhase) {
        val samMethods = toDenot(functionalInterface).info.possibleSamMethods.toList
        samMethods match {
          case x :: Nil => x.symbol
          case Nil => abort(s"${functionalInterface.show} is not a functional interface. It doesn't have abstract methods")
          case xs => abort(s"${functionalInterface.show} is not a functional interface. " +
            s"It has the following abstract methods: ${xs.map(_.name).mkString(", ")}")
        }
      }

      val methodName = samMethod.javaSimpleName
      val samMethodType = asmMethodType(samMethod).toASMType
      // scala/bug#10334: make sure that a lambda object for `T => U` has a method `apply(T)U`, not only the `(Object)Object`
      // version. Using the lambda a structural type `{def apply(t: T): U}` causes a reflective lookup for this method.
      val needsGenericBridge = samMethodType != instantiatedMethodType
      val bridgeMethods = atPhase(erasurePhase){
        samMethod.allOverriddenSymbols.toList
      }
      val overriddenMethodTypes = bridgeMethods.map(b => asmMethodType(b).toASMType)

      // any methods which `samMethod` overrides need bridges made for them
      // this is done automatically during erasure for classes we generate, but LMF needs to have them explicitly mentioned
      // so we have to compute them at this relatively late point.
      val bridgeTypes = (
        if (needsGenericBridge)
          instantiatedMethodType +: overriddenMethodTypes
        else
          overriddenMethodTypes
      ).distinct.filterNot(_ == samMethodType)

      val needsBridges = bridgeTypes.nonEmpty

      def flagIf(b: Boolean, flag: Int): Int = if (b) flag else 0
      val flags = flagIf(isSerializable, FLAG_SERIALIZABLE) | flagIf(needsBridges, FLAG_BRIDGES)

      val bsmArgs0 = Seq(samMethodType, targetHandle, instantiatedMethodType)
      val bsmArgs1 = if (flags != 0) Seq(Int.box(flags)) else Seq.empty
      val bsmArgs2 = if needsBridges then bridgeTypes.length +: bridgeTypes else Seq.empty

      val bsmArgs = bsmArgs0 ++ bsmArgs1 ++ bsmArgs2

      val metafactory =
        if (flags != 0)
          jliLambdaMetaFactoryAltMetafactoryHandle // altMetafactory required to be able to pass the flags and additional arguments if needed
        else
          jliLambdaMetaFactoryMetafactoryHandle

      bc.jmethod.visitInvokeDynamicInsn(methodName, desc, metafactory, bsmArgs*)

      generatedType
    }
  }

  /** Does this symbol actually correspond to an interface that will be emitted?
   *  In the backend, this should be preferred over `isInterface` because it
   *  also returns true for the symbols of the fake companion objects we
   *  create for Java-defined classes as well as for Java annotations
   *  which we represent as classes.
   */
  private def isEmittedInterface(sym: Symbol): Boolean = sym.isInterface ||
    sym.is(JavaDefined) && (toDenot(sym).isAnnotation || sym.is(ModuleClass) && (sym.companionClass.is(PureInterface)) || sym.companionClass.is(Trait))


}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy