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

scala.scalanative.nscplugin.NirGenExpr.scala Maven / Gradle / Ivy

There is a newer version: 0.5.5
Show newest version
package scala.scalanative
package nscplugin

import scala.language.implicitConversions
import scala.annotation.{tailrec, switch}

import dotty.tools.dotc.ast
import ast.tpd._
import dotty.tools.backend.ScalaPrimitivesOps._
import dotty.tools.dotc.core
import core.Contexts._
import core.Symbols._
import core.Names._
import core.Types._
import core.Constants._
import core.StdNames._
import core.Flags._
import core.Denotations._
import core.SymDenotations._
import core.TypeErasure.ErasedValueType
import core._
import dotty.tools.FatalError
import dotty.tools.dotc.report
import dotty.tools.dotc.transform
import dotty.tools.dotc.util.Spans.*
import transform.{ValueClasses, Erasure}
import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions
import scala.scalanative.nscplugin.CompilerCompat.SymUtilsCompat.*

import scala.scalanative.nir.Defn.Define.DebugInfo
import scala.scalanative.util.ScopedVar.scoped
import scala.scalanative.util.unsupported
import scala.scalanative.util.StringUtils
import dotty.tools.dotc.ast.desugar
import dotty.tools.dotc.util.Property
import scala.scalanative.nscplugin.NirDefinitions.NonErasedType
import scala.scalanative.util.unreachable

trait NirGenExpr(using Context) {
  self: NirCodeGen =>
  import positionsConversions.fromSpan

  sealed case class ValTree(value: nir.Val)(
      span: Span = NoSpan
  ) extends Tree { this.span = span }

  def ValTree(from: Tree)(value: nir.Val) =
    new ValTree(value = value)(span = from.span)

  sealed case class ContTree(f: ExprBuffer => nir.Val)(
      span: Span = NoSpan
  ) extends Tree { this.span = span }

  def ContTree(from: Tree)(build: ExprBuffer => nir.Val) =
    new ContTree(f = build)(span = from.span)

  def fallbackSourcePosition: nir.SourcePosition = curMethodSym.get.span

  class ExprBuffer(using fresh: nir.Fresh) extends FixupBuffer {
    buf =>

    def genExpr(tree: Tree): nir.Val = {
      tree match {
        case EmptyTree      => nir.Val.Unit
        case ValTree(value) => value
        case ContTree(f)    => f(this)
        case tree: Apply =>
          val updatedTree = lazyValsAdapter.transformApply(tree)
          genApply(updatedTree)
        case tree: Assign         => genAssign(tree)
        case tree: Block          => genBlock(tree)
        case tree: Closure        => genClosure(tree)
        case tree: Labeled        => genLabelDef(tree)
        case tree: Ident          => genIdent(tree)
        case tree: If             => genIf(tree)
        case tree: JavaSeqLiteral => genJavaSeqLiteral(tree)
        case tree: Literal        => genLiteral(tree)
        case tree: Match          => genMatch(tree)
        case tree: Return         => genReturn(tree)
        case tree: Select         => genSelect(tree)
        case tree: This           => genThis(tree)
        case tree: Try            => genTry(tree)
        case tree: Typed          => genTyped(tree)
        case tree: TypeApply      => genTypeApply(tree)
        case tree: ValDef         => genValDef(tree)
        case tree: WhileDo        => genWhileDo(tree)
        case _ =>
          throw FatalError(
            s"""Unexpected tree in genExpr: `${tree}`
             raw=$tree
             pos=${tree.span}
             """
          )
      }
    }

    object SafeZoneInstance extends Property.Key[nir.Val]

    def genApply(app: Apply): nir.Val = {
      given nir.SourcePosition = app.span.orElse(fallbackSourcePosition)
      val Apply(fun, args) = app

      val sym = fun.symbol
      def isStatic = sym.owner.isStaticOwner
      def qualifier0 = qualifierOf(fun)
      def qualifier = qualifier0.withSpan(qualifier0.span.orElse(fun.span))
      def arg = args.head

      inline def fail(msg: String)(using Context) = {
        report.error(msg, app.srcPos)
        nir.Val.Null
      }
      fun match {
        case _ if sym == defnNir.UnsafePackage_extern =>
          fail(s"extern can be used only from non-inlined extern methods")

        case _: TypeApply => genApplyTypeApply(app)
        case Select(Super(_, _), _) =>
          genApplyMethod(
            sym,
            statically = true,
            curMethodThis.get.get,
            args
          )
        case Select(New(_), nme.CONSTRUCTOR) =>
          genApplyNew(app)
        case _ if sym == defn.newArrayMethod =>
          val Seq(
            Literal(componentType: Constant),
            arrayType,
            SeqLiteral(dimensions, _)
          ) = args: @unchecked
          if (dimensions.size == 1)
            val length = genExpr(dimensions.head)
            buf.arrayalloc(
              genType(componentType.typeValue),
              length,
              unwind,
              zone = app.getAttachment(SafeZoneInstance)
            )
          else genApplyMethod(sym, statically = isStatic, qualifier, args)
        case _ =>
          if (nirPrimitives.isPrimitive(fun)) genApplyPrimitive(app)
          else if (Erasure.Boxing.isBox(sym)) genApplyBox(arg.tpe, arg)
          else if (Erasure.Boxing.isUnbox(sym)) genApplyUnbox(app.tpe, arg)
          else genApplyMethod(sym, statically = false, qualifier, args)
      }
    }

    def genAssign(tree: Assign): nir.Val = {
      val Assign(lhsp, rhsp) = tree
      given nir.SourcePosition = tree.span

      lhsp match {
        case DesugaredSelect(qualp, _) =>
          def rhs = genExpr(rhsp)
          val sym = lhsp.symbol
          val name = genFieldName(sym)
          if (sym.isExtern) {
            // Ignore intrinsic call to extern in class constructor
            // It would always present and would lead to undefined behaviour otherwise
            val shouldIgnoreAssign =
              curMethodSym.get.isClassConstructor &&
                rhsp.symbol == defnNir.UnsafePackage_extern
            if (shouldIgnoreAssign) nir.Val.Unit
            else {
              val externTy = genExternType(lhsp.tpe)
              genStoreExtern(externTy, sym, rhs)
            }
          } else {
            val qual =
              if (sym.isStaticMember) genModule(qualp.symbol)
              else genExpr(qualp)
            val ty = genType(lhsp.tpe)
            buf.fieldstore(ty, qual, name, rhs, unwind)
          }

        case id: Ident =>
          val rhs = genExpr(rhsp)
          val slot = curMethodEnv.resolve(id.symbol)
          buf.varstore(slot, rhs, unwind)
      }
    }

    def genBlock(block: Block): nir.Val = {
      val Block(stats, last) = block

      def isCaseLabelDef(tree: Tree) =
        tree.isInstanceOf[Labeled] && tree.symbol.isAllOf(SyntheticCase)

      def translateMatch(last: Labeled) = {
        val (prologue, cases) = stats.span(!isCaseLabelDef(_))
        val labels = cases.asInstanceOf[List[Labeled]]
        genMatch(prologue, labels :+ last)
      }

      withFreshBlockScope(block.span) { parentScope =>
        last match {
          case label: Labeled if isCaseLabelDef(label) =>
            translateMatch(label)

          case Apply(
                TypeApply(Select(label: Labeled, nme.asInstanceOf_), _),
                _
              ) if isCaseLabelDef(label) =>
            translateMatch(label)

          case _ =>
            stats.foreach(genExpr)
            genExpr(last)
        }
      }

    }

    // Scala Native does not have any special treatment for closures.
    // We reimplement the anonymous class generation which was originally used on
    // Scala 2.11 and earlier.
    //
    // Each anonymous function gets a generated class for it,
    // similarly to closure encoding on Scala 2.11 and earlier:
    //
    //   class AnonFunX extends java.lang.Object with FunctionalInterface {
    //     var capture1: T1
    //     ...
    //     var captureN: TN
    //     def (this, v1: T1, ..., vN: TN): Unit = {
    //       java.lang.Object.(this)
    //       this.capture1 = v1
    //       ...
    //       this.captureN = vN
    //     }
    //     def samMethod(param1: Tp1, ..., paramK: TpK): Tret = {
    //       EnclsoingClass.this.staticMethod(param1, ..., paramK, this.capture1, ..., this.captureN)
    //     }
    //   }
    //
    // Bridges might require multiple samMethod variants to be created.
    def genClosure(tree: Closure): nir.Val = {
      given nir.SourcePosition = tree.span
      val Closure(env, fun, functionalInterface) = tree
      val treeTpe = tree.tpe.typeSymbol
      val funSym = fun.symbol
      val funInterfaceSym = functionalInterface.tpe.typeSymbol

      val anonClassName = {
        val nir.Global.Top(className) = genTypeName(curClassSym)
        val suffix = "$$Lambda$" + curClassFresh.get.apply().id
        nir.Global.Top(className + suffix)
      }

      val isStaticCall = funSym.isStaticInNIR
      val allCaptureValues =
        if (isStaticCall) env
        else qualifierOf(fun) :: env
      val captureSyms = allCaptureValues.map(_.symbol)
      val captureTypesAndNames = {
        for
          (tree, idx) <- allCaptureValues.zipWithIndex
          tpe = tree match {
            case This(iden) => genType(fun.symbol.owner)
            case _          => genType(tree.tpe)
          }
          name = anonClassName.member(nir.Sig.Field(s"capture$idx"))
        yield (tpe, name)
      }
      val (captureTypes, captureNames) = captureTypesAndNames.unzip

      def genAnonymousClass: nir.Defn = {
        val traits =
          if (functionalInterface.isEmpty) genTypeName(treeTpe) :: Nil
          else {
            val parents = funInterfaceSym.info.parents
            genTypeName(funInterfaceSym) +: parents.collect {
              case tpe if tpe.typeSymbol.isTraitOrInterface =>
                genTypeName(tpe.typeSymbol)
            }
          }

        nir.Defn.Class(
          attrs = nir.Attrs.None,
          name = anonClassName,
          parent = Some(nir.Rt.Object.name),
          traits = traits
        )
      }

      def genCaptureFields: List[nir.Defn] = {
        for (tpe, name) <- captureTypesAndNames
        yield nir.Defn.Var(
          attrs = nir.Attrs.None,
          name = name,
          ty = tpe,
          rhs = nir.Val.Zero(tpe)
        )
      }

      val ctorName = anonClassName.member(nir.Sig.Ctor(captureTypes))
      val ctorTy = nir.Type.Function(
        nir.Type.Ref(anonClassName) +: captureTypes,
        nir.Type.Unit
      )

      def genAnonymousClassCtor: nir.Defn = {
        val body = {
          scoped(
            curScopeId := nir.ScopeId.TopLevel
          ) {
            val fresh = nir.Fresh()
            val buf = new nir.InstructionBuilder()(fresh)

            val superTy = nir.Type.Function(Seq(nir.Rt.Object), nir.Type.Unit)
            val superName = nir.Rt.Object.name.member(nir.Sig.Ctor(Seq.empty))
            val superCtor = nir.Val.Global(superName, nir.Type.Ptr)

            val self = nir.Val.Local(fresh(), nir.Type.Ref(anonClassName))
            val captureFormals = captureTypes.map(nir.Val.Local(fresh(), _))
            buf.label(fresh(), self +: captureFormals)
            buf.call(superTy, superCtor, Seq(self), nir.Next.None)
            captureNames.zip(captureFormals).foreach { (name, capture) =>
              buf.fieldstore(capture.ty, self, name, capture, nir.Next.None)
            }
            buf.ret(nir.Val.Unit)

            buf.toSeq
          }
        }

        new nir.Defn.Define(nir.Attrs.None, ctorName, ctorTy, body)
      }

      def resolveAnonClassMethods: List[Symbol] = {
        val samMethods =
          if (funInterfaceSym.exists) funInterfaceSym.info.possibleSamMethods
          else treeTpe.info.possibleSamMethods

        samMethods
          .map(_.symbol)
          .toList
      }

      def genAnonClassMethod(sym: Symbol): nir.Defn = {
        val nir.Global.Member(_, funSig) = genName(sym): @unchecked
        val nir.Sig.Method(_, sigTypes :+ retType, _) =
          funSig.unmangled: @unchecked

        val selfType = nir.Type.Ref(anonClassName)
        val methodName = anonClassName.member(funSig)
        val paramTypes = selfType +: sigTypes
        val paramSyms = funSym.paramSymss.flatten

        def genBody = {
          given fresh: nir.Fresh = nir.Fresh()
          val freshScopes = initFreshScope(EmptyTree)
          given buf: ExprBuffer = new ExprBuffer()
          scoped(
            curFresh := fresh,
            curFreshScope := freshScopes,
            curScopeId := nir.ScopeId.of(freshScopes.last),
            curExprBuffer := buf,
            curMethodEnv := MethodEnv(fresh),
            curMethodLabels := MethodLabelsEnv(fresh),
            curMethodInfo := CollectMethodInfo(),
            curUnwindHandler := None
          ) {
            val self = nir.Val.Local(fresh(), selfType)
            val params = sigTypes.map(nir.Val.Local(fresh(), _))

            buf.label(fresh(), self +: params)
            // At this point, the type parameter symbols are all Objects.
            // We need to transform them, so that their type conforms to
            // what the apply method expects:
            // - values that can be unboxed, are unboxed
            // - otherwise, the value is cast to the appropriate type
            val paramVals =
              for (param, sym) <- params.zip(paramSyms)
              yield ensureUnboxed(param, sym.info.finalResultType)

            val captureVals =
              for (sym, (tpe, name)) <- captureSyms.zip(captureTypesAndNames)
              yield buf.fieldload(tpe, self, name, unwind)

            val allVals =
              (captureVals ++ paramVals).toList.map(ValTree(_)(sym.span))
            val res = if (isStaticCall) {
              scoped(curMethodThis := None) {
                buf.genApplyStaticMethod(
                  funSym,
                  qualifierOf(fun).symbol,
                  allVals
                )
              }
            } else {
              val thisVal :: argVals = allVals: @unchecked
              scoped(curMethodThis := Some(thisVal.value)) {
                buf.genApplyMethod(
                  funSym,
                  statically = false,
                  thisVal,
                  argVals
                )
              }
            }

            val retValue =
              if (retType == res.ty) res
              else
                // Get the result type of the lambda after erasure, when entering posterasure.
                // This allows to recover the correct type in case value classes are involved.
                // In that case, the type will be an ErasedValueType.
                buf.ensureBoxed(res, funSym.info.finalResultType)
            buf.ret(retValue)
            buf.toSeq
          }
        }

        new nir.Defn.Define(
          nir.Attrs.None,
          methodName,
          nir.Type.Function(paramTypes, retType),
          genBody
        )
      }

      def genAnonymousClassMethods: List[nir.Defn] =
        resolveAnonClassMethods
          .map(genAnonClassMethod)

      generatedDefns ++= {
        genAnonymousClass ::
          genAnonymousClassCtor ::
          genCaptureFields :::
          genAnonymousClassMethods
      }

      def allocateClosure() = {
        val alloc = buf.classalloc(anonClassName, unwind)
        val captures = allCaptureValues.map(genExpr)
        buf.call(
          ctorTy,
          nir.Val.Global(ctorName, nir.Type.Ptr),
          alloc +: captures,
          unwind
        )
        alloc
      }
      allocateClosure()
    }

    def genIdent(tree: Ident): nir.Val =
      tree match {
        case DesugaredSelect(_, _) =>
          genSelect(DesugaredSelect.desugared.withSpan(tree.span))
        case _ =>
          val sym = tree.symbol
          given nir.SourcePosition = tree.span
          val value =
            if sym.is(Module) then genModule(sym)
            else if (curMethodInfo.mutableVars.contains(sym))
              buf.varload(curMethodEnv.resolve(sym), unwind)
            else curMethodEnv.resolve(sym)
          if nir.Type.isNothing(value.ty) then
            // Short circuit the generated code for phantom value
            // scala.runtime.Nothing$ extends Throwable so it's safe to throw
            buf.raise(value, unwind)
            buf.unreachable(unwind)
          value
      }

    def genIf(tree: If): nir.Val = {
      given nir.SourcePosition = tree.span
      val If(cond, thenp, elsep) = tree
      def isUnitType(tpe: Type) =
        tpe =:= defn.UnitType || defn.isBoxedUnitClass(tpe.sym)
      val retty =
        if (isUnitType(thenp.tpe) || isUnitType(elsep.tpe)) nir.Type.Unit
        else genType(tree.tpe)
      genIf(retty, cond, thenp, elsep)
    }

    def genIf(
        retty: nir.Type,
        condp: Tree,
        thenp: Tree,
        elsep: Tree,
        ensureLinktime: Boolean = false
    )(using enclosingPos: nir.SourcePosition): nir.Val = {
      val thenn, elsen, mergen = fresh()
      val mergev = nir.Val.Local(fresh(), retty)

      locally {
        given nir.SourcePosition = condp.span.orElse(enclosingPos)
        getLinktimeCondition(condp) match {
          case Some(cond) =>
            curMethodEnv.get.isUsingLinktimeResolvedValue = true
            buf.branchLinktime(cond, nir.Next(thenn), nir.Next(elsen))
          case None =>
            if ensureLinktime then
              report.error(
                "Cannot resolve given condition in linktime, it might be depending on runtime value",
                condp.srcPos
              )
            val cond = genExpr(condp)
            buf.branch(cond, nir.Next(thenn), nir.Next(elsen))
        }
      }

      locally {
        given nir.SourcePosition = thenp.span.orElse(enclosingPos)
        buf.label(thenn)
        val thenv = genExpr(thenp)
        buf.jumpExcludeUnitValue(retty)(mergen, thenv)
      }
      locally {
        given nir.SourcePosition = elsep.span.orElse(enclosingPos)
        buf.label(elsen)
        val elsev = genExpr(elsep)
        buf.jumpExcludeUnitValue(retty)(mergen, elsev)
      }
      buf.labelExcludeUnitValue(mergen, mergev)
    }

    def genJavaSeqLiteral(tree: JavaSeqLiteral): nir.Val = {
      val JavaArrayType(elemTpe) = tree.tpe: @unchecked

      val elems = tree.elems
      val elemty = genType(elemTpe)
      val values = genSimpleArgs(elems)
      given nir.SourcePosition = tree.span

      if (values.forall(_.isCanonical) && values.exists(v => !v.isZero))
        buf.arrayalloc(elemty, nir.Val.ArrayValue(elemty, values), unwind)
      else
        val alloc = buf.arrayalloc(elemty, nir.Val.Int(values.length), unwind)
        for (v, i) <- values.zipWithIndex if !v.isZero do
          given nir.SourcePosition = elems(i).span
          buf.arraystore(elemty, alloc, nir.Val.Int(i), v, unwind)
        alloc
    }

    def genLabelDef(label: Labeled): nir.Val = {
      given nir.SourcePosition = label.span
      val Labeled(bind, body) = label
      assert(bind.body == EmptyTree, "non-empty Labeled bind body")

      val (labelEntry, labelExit) = curMethodLabels.enterLabel(label)
      val labelExitParam = nir.Val.Local(fresh(), genType(bind.tpe))
      curMethodLabels.enterExitType(labelExit, labelExitParam.ty)

      buf.jump(nir.Next(labelEntry))

      buf.label(labelEntry, Nil)
      buf.jumpExcludeUnitValue(labelExitParam.ty)(
        labelExit,
        genExpr(label.expr)
      )

      buf.labelExcludeUnitValue(labelExit, labelExitParam)
    }

    def genLiteral(lit: Literal): nir.Val = {
      val value = lit.const

      value.tag match {
        case ClazzTag => genTypeValue(value.typeValue)
        case _        => genLiteralValue(lit)
      }
    }

    private def genLiteralValue(lit: Literal): nir.Val = {
      val value = lit.const
      value.tag match {
        case UnitTag => nir.Val.Unit
        case NullTag => nir.Val.Null
        case BooleanTag =>
          if (value.booleanValue) nir.Val.True else nir.Val.False
        case ByteTag   => nir.Val.Byte(value.intValue.toByte)
        case ShortTag  => nir.Val.Short(value.intValue.toShort)
        case CharTag   => nir.Val.Char(value.intValue.toChar)
        case IntTag    => nir.Val.Int(value.intValue)
        case LongTag   => nir.Val.Long(value.longValue)
        case FloatTag  => nir.Val.Float(value.floatValue)
        case DoubleTag => nir.Val.Double(value.doubleValue)
        case StringTag => nir.Val.String(value.stringValue)
      }
    }

    def genMatch(m: Match): nir.Val = {
      given nir.SourcePosition = m.span
      val Match(scrutp, allcaseps) = m
      case class Case(
          id: nir.Local,
          value: nir.Val,
          tree: Tree,
          position: nir.SourcePosition
      )

      // Extract switch cases and assign unique names to them.
      val caseps: Seq[Case] = allcaseps.flatMap {
        case CaseDef(Ident(nme.WILDCARD), _, _) =>
          Seq.empty
        case cd @ CaseDef(pat, guard, body) =>
          assert(guard.isEmpty, "CaseDef guard was not empty")
          val vals: Seq[nir.Val] = pat match {
            case lit: Literal =>
              List(genLiteralValue(lit))
            case Alternative(alts) =>
              alts.map {
                case lit: Literal => genLiteralValue(lit)
              }
            case _ =>
              Nil
          }
          vals.map(Case(fresh(), _, body, cd.span: nir.SourcePosition))
      }

      // Extract default case.
      val defaultp: Tree = allcaseps.collectFirst {
        case c @ CaseDef(Ident(nme.WILDCARD), _, body) => body
      }.get

      val retty = genType(m.tpe)
      val scrut = genExpr(scrutp)

      // Generate code for the switch and its cases.
      def genSwitch(): nir.Val = {
        // Generate some more fresh names and types.
        val casenexts = caseps.map {
          case Case(n, v, _, _) => nir.Next.Case(v, n)
        }
        val defaultnext = nir.Next(fresh())
        val merge = fresh()
        val mergev = nir.Val.Local(fresh(), retty)

        val defaultCasePos: nir.SourcePosition = defaultp.span

        // Generate code for the switch and its cases.
        val scrut = genExpr(scrutp)
        buf.switch(scrut, defaultnext, casenexts)
        buf.label(defaultnext.id)(using defaultCasePos)
        buf.jumpExcludeUnitValue(retty)(merge, genExpr(defaultp))(using
          defaultCasePos
        )
        caseps.foreach {
          case Case(n, _, expr, pos) =>
            given nir.SourcePosition = pos
            buf.label(n)
            val caseres = genExpr(expr)
            buf.jumpExcludeUnitValue(retty)(merge, caseres)
        }
        buf.labelExcludeUnitValue(merge, mergev)
      }

      def genIfsChain(): nir.Val = {
        /* Default label needs to be generated before any others and then added to
         * current MethodEnv. It's label might be referenced in any of them in
         * case of match with guards, eg.:
         *
         * "Hello, World!" match {
         *  case "Hello" if cond1 => "foo"
         *  case "World" if cond2 => "bar"
         *  case _ if cond3 => "bar-baz"
         *  case _ => "baz-bar"
         * }
         *
         * might be translated to something like:
         *
         * val x1 = "Hello, World!"
         * if(x1 == "Hello"){ if(cond1) "foo" else default4() }
         * else if (x1 == "World"){ if(cond2) "bar" else default4() }
         * else default4()
         *
         * def default4() = if(cond3) "bar-baz" else "baz-bar
         *
         * We need to make sure to only generate LabelDef at this stage.
         * Generating other ASTs and mutating state might lead to unexpected
         * runtime errors.
         */
        val optDefaultLabel = defaultp match {
          case label: Labeled => Some(genLabelDef(label))
          case _              => None
        }

        def loop(cases: List[Case]): nir.Val = {
          cases match {
            case Case(_, caze, body, p) :: elsep =>
              given nir.SourcePosition = p

              val cond =
                buf.genClassEquality(
                  leftp = ValTree(scrutp)(scrut),
                  rightp = ValTree(body)(caze),
                  ref = false,
                  negated = false
                )
              buf.genIf(
                retty = retty,
                condp = ValTree(body)(cond),
                thenp = ContTree(body)(_.genExpr(body)),
                elsep = ContTree(body)(_ => loop(elsep))
              )

            case Nil => optDefaultLabel.getOrElse(genExpr(defaultp))
          }
        }
        loop(caseps.toList)
      }

      /* Since 2.13 we need to enforce that only Int switch cases reach backend
       * For all other cases we're generating If-else chain */
      val isIntMatch = scrut.ty == nir.Type.Int &&
        caseps.forall(_._2.ty == nir.Type.Int)

      if (isIntMatch) genSwitch()
      else genIfsChain()
    }

    private def genMatch(prologue: List[Tree], lds: List[Labeled]): nir.Val = {
      // Generate prologue expressions.
      prologue.foreach(genExpr(_))

      // Enter symbols for all labels and jump to the first one.
      lds.foreach(curMethodLabels.enterLabel)
      val firstLd = lds.head
      given nir.SourcePosition = firstLd.span
      buf.jump(nir.Next(curMethodLabels.resolveEntry(firstLd)))

      // Generate code for all labels and return value of the last one.
      lds.map(genLabelDef(_)).last
    }

    def genModule(sym: Symbol)(using nir.SourcePosition): nir.Val = {
      val moduleSym = if (sym.isTerm) sym.moduleClass match {
        case NoSymbol  => sym.info.typeSymbol
        case moduleCls => moduleCls
      }
      else sym
      val name = genModuleName(moduleSym)
      buf.module(name, unwind)
    }

    def genReturn(tree: Return): nir.Val = {
      val Return(exprp, from) = tree
      given nir.SourcePosition = tree.span
      val rhs = genExpr(exprp)
      val fromSym = from.symbol
      val label = Option.when(fromSym.is(Label)) {
        curMethodLabels.resolveExit(fromSym)
      }
      genReturn(rhs, label)
    }

    def genReturn(value: nir.Val, from: Option[nir.Local] = None)(using
        pos: nir.SourcePosition
    ): nir.Val = {
      val retv =
        if (curMethodIsExtern.get)
          val nir.Type.Function(_, retty) = genExternMethodSig(curMethodSym)
          toExtern(retty, value)
        else value

      from match {
        case Some(label) =>
          val retty = curMethodLabels.resolveExitType(label)
          buf.jumpExcludeUnitValue(retty)(label, retv)
        case _ if retv.ty == nir.Type.Unit => buf.ret(nir.Val.Unit)
        case _                             => buf.ret(retv)
      }
      nir.Val.Unit
    }

    def genSelect(tree: Select): nir.Val = {
      given nir.SourcePosition = tree.span
      val Select(qualp, selp) = tree

      val sym = tree.symbol
      val owner = if sym != NoSymbol then sym.owner else NoSymbol

      if (sym.is(Module)) genModule(sym)
      else if (sym.isStaticInNIR && !sym.isExtern)
        genStaticMember(sym, qualp.symbol)
      else if (sym.is(Method))
        genApplyMethod(sym, statically = false, qualp, Seq.empty)
      else if (owner.isStruct) {
        val index = owner.info.decls.filter(_.isField).toList.indexOf(sym)
        val qual = genExpr(qualp)
        buf.extract(qual, Seq(index), unwind)
      } else {
        val ty = genType(tree.tpe)
        val qual =
          if (sym.isStaticMember) genModule(owner)
          else genExpr(qualp)
        val name = genFieldName(tree.symbol)
        if (sym.isExtern) {
          val externTy = genExternType(tree.tpe)
          genLoadExtern(ty, externTy, tree.symbol)
        } else {
          buf.fieldload(ty, qual, name, unwind)
        }
      }
    }

    def genThis(tree: This): nir.Val = {
      given nir.SourcePosition = tree.span
      val sym = tree.symbol
      def currentThis = curMethodThis.get
      def currentClass = curClassSym.get
      def currentMethod = curMethodSym.get

      def canUseCurrentThis = currentThis.nonEmpty &&
        (sym == currentClass || currentMethod.owner == currentClass)
      val canLoadAsModule =
        sym != currentClass &&
          sym.is(ModuleClass, butNot = Package) ||
          sym.isPackageObject

      if (canLoadAsModule) genModule(sym)
      else if (canUseCurrentThis) currentThis.get
      else
        report.error(
          s"Cannot resolve `this` instance for ${tree}",
          tree.sourcePos
        )
        nir.Val.Zero(genType(currentClass))
    }

    def genTry(tree: Try): nir.Val = tree match {
      case Try(expr, catches, finalizer)
          if catches.isEmpty && finalizer.isEmpty =>
        genExpr(expr)
      case Try(expr, catches, finalizer) =>
        val retty = genType(tree.tpe)
        genTry(retty, expr, catches, finalizer)
    }

    private def genTry(
        retty: nir.Type,
        expr: Tree,
        catches: List[Tree],
        finallyp: Tree
    ): nir.Val = {
      given nir.SourcePosition = expr.span
      val handler, normaln, mergen = fresh()
      val excv = nir.Val.Local(fresh(), nir.Rt.Object)
      val mergev = nir.Val.Local(fresh(), retty)

      // Nested code gen to separate out try/catch-related instructions.
      val nested = ExprBuffer()
      scoped(
        curUnwindHandler := Some(handler)
      ) {
        withFreshBlockScope(summon[nir.SourcePosition]) { _ =>
          nested.label(normaln)
          val res = nested.genExpr(expr)
          nested.jumpExcludeUnitValue(retty)(mergen, res)
        }
      }
      withFreshBlockScope(summon[nir.SourcePosition]) { _ =>
        nested.label(handler, Seq(excv))
        val res = nested.genTryCatch(retty, excv, mergen, catches)
        nested.jumpExcludeUnitValue(retty)(mergen, res)
      }

      // Append finally to the try/catch instructions and merge them back.
      val insts =
        if (finallyp.isEmpty) nested.toSeq
        else genTryFinally(finallyp, nested.toSeq)

      // Append try/catch instructions to the outher instruction buffer.
      buf.jump(nir.Next(normaln))
      buf ++= insts
      buf.labelExcludeUnitValue(mergen, mergev)
    }

    private def genTryCatch(
        retty: nir.Type,
        exc: nir.Val,
        mergen: nir.Local,
        catches: List[Tree]
    )(using exprPos: nir.SourcePosition): nir.Val = {
      val cases = catches.map {
        case cd @ CaseDef(pat, _, body) =>
          val (excty, symopt) = pat match {
            case Typed(Ident(nme.WILDCARD), tpt) =>
              genType(tpt.tpe) -> None
            case Ident(nme.WILDCARD) =>
              genType(defn.ThrowableClass.info.resultType) -> None
            case Bind(_, _) =>
              genType(pat.tpe) -> Some(pat.symbol)
          }
          val f = ContTree(body) { (buf: ExprBuffer) =>
            withFreshBlockScope(body.span) { _ =>
              symopt.foreach { sym =>
                val cast = buf.as(excty, exc, unwind)(cd.span, getScopeId)
                curMethodLocalNames.get.update(cast.id, genLocalName(sym))
                curMethodEnv.enter(sym, cast)
              }
              val res = genExpr(body)
              buf.jumpExcludeUnitValue(retty)(mergen, res)
            }
            nir.Val.Unit
          }
          (excty, f, exprPos)
      }

      def wrap(
          cases: Seq[(nir.Type, ContTree, nir.SourcePosition)]
      ): nir.Val =
        cases match {
          case Seq() =>
            buf.raise(exc, unwind)
            nir.Val.Unit
          case (excty, f, pos) +: rest =>
            val cond = buf.is(excty, exc, unwind)(pos, getScopeId)
            genIf(
              retty,
              ValTree(f)(cond),
              f,
              ContTree(f)(_ => wrap(rest))
            )(using pos)
        }

      wrap(cases)
    }

    private def genTryFinally(
        finallyp: Tree,
        insts: Seq[nir.Inst]
    ): Seq[nir.Inst] = {
      val labels =
        insts.collect {
          case nir.Inst.Label(n, _) => n
        }.toSet
      def internal(cf: nir.Inst.Cf) = cf match {
        case inst @ nir.Inst.Jump(n) =>
          labels.contains(n.id)
        case inst @ nir.Inst.If(_, n1, n2) =>
          labels.contains(n1.id) && labels.contains(n2.id)
        case inst @ nir.Inst.LinktimeIf(_, n, n2) =>
          labels.contains(n.id) && labels.contains(n2.id)
        case inst @ nir.Inst.Switch(_, n, ns) =>
          labels.contains(n.id) && ns.forall(n => labels.contains(n.id))
        case inst @ nir.Inst.Throw(_, n) =>
          (n ne nir.Next.None) && labels.contains(n.id)
        case _ =>
          false
      }

      val finalies = new ExprBuffer
      val transformed = insts.map {
        case cf: nir.Inst.Cf if internal(cf) =>
          // We don't touch control-flow within try/catch block.
          cf
        case cf: nir.Inst.Cf =>
          // All control-flow edges that jump outside the try/catch block
          // must first go through finally block if it's present. We generate
          // a new copy of the finally handler for every edge.
          val finallyn = fresh()
          withFreshBlockScope(cf.pos) { _ =>
            finalies.label(finallyn)(cf.pos)
            finalies.genExpr(finallyp)
          }
          finalies += cf
          // The original jump outside goes through finally block first.
          nir.Inst.Jump(nir.Next(finallyn))(cf.pos)
        case inst =>
          inst
      }
      transformed ++ finalies.toSeq
    }

    def genTyped(tree: Typed): nir.Val = tree match {
      case Typed(Super(_, _), _) => curMethodThis.get.get
      case Typed(expr, _)        => genExpr(expr)
    }

    def genTypeApply(tree: TypeApply): nir.Val = {
      given nir.SourcePosition = tree.span
      val TypeApply(fun @ Select(receiverp, _), targs) = tree: @unchecked

      val funSym = fun.symbol
      val fromty = genType(receiverp.tpe)
      val toty = genType(targs.head.tpe)
      def boxty = genBoxType(targs.head.tpe)
      val value = genExpr(receiverp)
      lazy val boxed = boxValue(receiverp.tpe, value)(using receiverp.span)

      if (funSym == defn.Any_isInstanceOf) buf.is(boxty, boxed, unwind)
      else if (funSym == defn.Any_asInstanceOf)
        (fromty, toty) match {
          case (_: nir.Type.PrimitiveKind, _: nir.Type.PrimitiveKind) =>
            genCoercion(value, fromty, toty)
          case _ if boxed.ty =?= boxty => boxed
          case (_, nir.Type.Nothing) =>
            val isNullL, notNullL = fresh()
            val isNull =
              buf.comp(nir.Comp.Ieq, boxed.ty, boxed, nir.Val.Null, unwind)
            buf.branch(isNull, nir.Next(isNullL), nir.Next(notNullL))
            buf.label(isNullL)
            buf.raise(nir.Val.Null, unwind)
            buf.label(notNullL)
            buf.as(nir.Rt.RuntimeNothing, boxed, unwind)
            buf.unreachable(unwind)
            buf.label(fresh())
            nir.Val.Zero(nir.Type.Nothing)
          case _ =>
            val cast = buf.as(boxty, boxed, unwind)
            unboxValue(tree.tpe, partial = true, cast)
        }
      else {
        report.error("Unkown case genTypeApply: " + funSym, tree.sourcePos)
        nir.Val.Null
      }
    }

    def genValDef(vd: ValDef): nir.Val = {
      given nir.SourcePosition = vd.span
      val localNames = curMethodLocalNames.get
      val isMutable = curMethodInfo.mutableVars.contains(vd.symbol)
      def name = genLocalName(vd.symbol)
      val rhs = genExpr(vd.rhs) match {
        case v @ nir.Val.Local(id, _) =>
          if !(localNames.contains(id) || isMutable)
          then localNames.update(id, name)
          vd.rhs match {
            // When rhs is a block patch the scopeId of it's result to match the current scopeId
            // This allows us to reflect that ValDef is accessible in this scope
            case _: Block | Typed(_: Block, _) | Try(_: Block, _, _) |
                Try(Typed(_: Block, _), _, _) =>
              buf.updateLetInst(id)(i => i.copy()(i.pos, curScopeId.get))
            case _ => ()
          }
          v
        case nir.Val.Unit =>
          nir.Val.Unit
        case v =>
          buf.let(fresh.namedId(name), nir.Op.Copy(v), unwind)
      }

      if (vd.symbol.isExtern)
        checkExplicitReturnTypeAnnotation(vd, "extern field")
      if (isMutable)
        val slot = curMethodEnv.resolve(vd.symbol)
        buf.varstore(slot, rhs, unwind)
      else
        curMethodEnv.enter(vd.symbol, rhs)
        nir.Val.Unit
    }

    def genWhileDo(wd: WhileDo): nir.Val = {
      val WhileDo(cond, body) = wd
      val condLabel, bodyLabel, exitLabel = fresh()

      locally {
        given nir.SourcePosition = wd.span
        buf.jump(nir.Next(condLabel))
      }

      locally {
        given nir.SourcePosition = cond.span.orElse(wd.span)
        buf.label(condLabel)
        val genCond =
          if (cond == EmptyTree) nir.Val.Bool(true)
          else genExpr(cond)
        buf.branch(genCond, nir.Next(bodyLabel), nir.Next(exitLabel))
      }

      locally {
        given nir.SourcePosition = body.span
        buf.label(bodyLabel)
        val _ = genExpr(body)
        buf.jump(condLabel, Nil)
      }

      locally {
        given nir.SourcePosition = wd.span.endPos
        buf.label(exitLabel, Seq.empty)
        if (cond == EmptyTree) nir.Val.Zero(genType(defn.NothingClass))
        else nir.Val.Unit
      }
    }

    private def genApplyBox(st: SimpleType, argp: Tree)(using
        nir.SourcePosition
    ): nir.Val = {
      val value = genExpr(argp)
      buf.box(genBoxType(st), value, unwind)
    }

    private def genApplyUnbox(st: SimpleType, argp: Tree)(using
        nir.SourcePosition
    ): nir.Val = {
      val value = genExpr(argp)
      value.ty match {
        case _: nir.Type.I | _: nir.Type.F =>
          // No need for unboxing, fixing some slack generated by the general
          // purpose Scala compiler.
          value
        case _ =>
          buf.unbox(genBoxType(st), value, unwind)
      }
    }

    private def genApplyPrimitive(app: Apply): nir.Val = {
      import NirPrimitives._
      import dotty.tools.backend.ScalaPrimitivesOps._
      given nir.SourcePosition = app.span
      val Apply(fun @ DesugaredSelect(receiver, _), args) = app: @unchecked

      val sym = app.symbol
      val code = nirPrimitives.getPrimitive(app, receiver.tpe)
      def arg = args.head

      (code: @switch) match {
        case THROW                  => genThrow(app, args)
        case CONCAT                 => genStringConcat(app)
        case HASH                   => genHashCode(arg)
        case BOXED_UNIT             => nir.Val.Unit
        case SYNCHRONIZED           => genSynchronized(receiver, arg)
        case CFUNCPTR_APPLY         => genCFuncPtrApply(app)
        case CFUNCPTR_FROM_FUNCTION => genCFuncFromScalaFunction(app)
        case STACKALLOC             => genStackalloc(app)
        case SAFEZONE_ALLOC         => genSafeZoneAlloc(app)
        case CQUOTE                 => genCQuoteOp(app)
        case CLASS_FIELD_RAWPTR     => genClassFieldRawPtr(app)
        case SIZE_OF                => genSizeOf(app)
        case ALIGNMENT_OF           => genAlignmentOf(app)
        case REFLECT_SELECTABLE_SELECTDYN =>
          genReflectiveCall(app, isSelectDynamic = true)
        case REFLECT_SELECTABLE_APPLYDYN =>
          genReflectiveCall(app, isSelectDynamic = false)
        case USES_LINKTIME_INTRINSIC => genLinktimeIntrinsicApply(app)
        case _ =>
          if (isArithmeticOp(code) || isLogicalOp(code) || isComparisonOp(code))
            genSimpleOp(app, receiver :: args, code)
          else if (isArrayOp(code) || code == ARRAY_CLONE) genArrayOp(app, code)
          else if (isCoercion(code)) genCoercion(app, receiver, code)
          else if (NirPrimitives.isRawPtrOp(code)) genRawPtrOp(app, code)
          else if (NirPrimitives.isRawPtrCastOp(code)) genRawPtrCastOp(app)
          else if (NirPrimitives.isRawSizeCastOp(code))
            genRawSizeCastOp(app, code)
          else if (NirPrimitives.isUnsignedOp(code)) genUnsignedOp(app, code)
          else {
            report.error(
              s"Unknown primitive operation: ${sym.fullName}(${fun.symbol.showName})",
              app.sourcePos
            )
            nir.Val.Null
          }
      }
    }

    private def genLinktimeIntrinsicApply(app: Apply): nir.Val = {
      import defnNir.*
      given nir.SourcePosition = app.span
      val Apply(fun, args) = app

      val sym = fun.symbol
      def isStatic = sym.owner.isStaticOwner
      def qualifier0 = qualifierOf(fun)
      def qualifier = qualifier0.withSpan(qualifier0.span.orElse(fun.span))

      sym match {
        case _
            if JavaUtilServiceLoaderLoad.contains(sym) ||
              JavaUtilServiceLoaderLoadInstalled == sym =>
          args.head match {
            case Literal(c: Constant) => () // ok
            case _ =>
              report.error(
                s"Limitation of ScalaNative runtime: first argument of ${sym.show} needs to be literal constant of class type, use `classOf[T]` instead.",
                app.srcPos
              )
          }
        case _ =>
          report.error(
            s"Unhandled intrinsic function call for ${sym.show}",
            app.srcPos
          )
      }

      curMethodEnv.get.isUsingIntrinsics = true
      genApplyMethod(sym, statically = isStatic, qualifier, args)
    }

    private def genApplyTypeApply(app: Apply): nir.Val = {
      val Apply(
        tApply @ TypeApply(fun @ DesugaredSelect(receiverp, _), targs),
        argsp
      ) = app: @unchecked
      given nir.SourcePosition = app.span

      val funSym = fun.symbol
      genExpr(receiverp)
      if (funSym == defn.Object_synchronized)
        assert(argsp.size == 1, "synchronized with wrong number of args")
        genSynchronized(receiverp, argsp.head)
      else genTypeApply(tApply)
    }

    private def genApplyNew(app: Apply): nir.Val = {
      val Apply(fun @ Select(New(tpt), nme.CONSTRUCTOR), args) = app: @unchecked
      given nir.SourcePosition = app.span

      fromType(tpt.tpe) match {
        case st if st.sym.isStruct =>
          genApplyNewStruct(st, args)

        case SimpleType(cls, Seq()) =>
          val ctor = fun.symbol
          assert(
            ctor.isClassConstructor,
            "'new' call to non-constructor: " + ctor.name
          )

          genApplyNew(
            cls,
            ctor,
            args,
            zone = app.getAttachment(SafeZoneInstance)
          )

        case SimpleType(sym, targs) =>
          unsupported(s"unexpected new: $sym with targs $targs")
      }
    }

    private def genApplyNewStruct(st: SimpleType, argsp: Seq[Tree]): nir.Val = {
      val ty = genType(st)
      val args = genSimpleArgs(argsp)
      var res: nir.Val = nir.Val.Zero(ty)

      for ((arg, argp), idx) <- args.zip(argsp).zipWithIndex
      do
        given nir.SourcePosition = argp.span
        res = buf.insert(res, arg, Seq(idx), unwind)
      res
    }

    private def genApplyNew(
        clssym: Symbol,
        ctorsym: Symbol,
        args: List[Tree],
        zone: Option[nir.Val]
    )(using
        nir.SourcePosition
    ): nir.Val = {
      val alloc = buf.classalloc(genTypeName(clssym), unwind, zone)
      genApplyMethod(ctorsym, statically = true, alloc, args)
      alloc
    }

    def genApplyModuleMethod(
        module: Symbol,
        method: Symbol,
        args: Seq[Tree]
    )(using
        nir.SourcePosition
    ): nir.Val = {
      val self = genModule(module)
      genApplyMethod(method, statically = true, self, args)
    }

    def genApplyMethod(
        sym: Symbol,
        statically: Boolean,
        selfp: Tree,
        argsp: Seq[Tree]
    )(using nir.SourcePosition): nir.Val = {
      if (sym.isExtern && sym.is(Accessor)) genApplyExternAccessor(sym, argsp)
      else if (sym.isStaticInNIR && !sym.isExtern)
        genApplyStaticMethod(sym, selfp.symbol, argsp)
      else
        val self = genExpr(selfp)
        genApplyMethod(sym, statically, self, argsp)
    }

    private def genApplyMethod(
        sym: Symbol,
        statically: Boolean,
        self: nir.Val,
        argsp: Seq[Tree]
    )(using nir.SourcePosition): nir.Val = {
      assert(!sym.isStaticMethod, sym)
      val owner = sym.owner.asClass
      val name = genMethodName(sym)
      val isExtern = sym.isExtern

      val origSig = genMethodSig(sym)
      val sig =
        if isExtern then genExternMethodSig(sym)
        else origSig
      val args = genMethodArgs(sym, argsp)

      val isStaticCall = statically || owner.isStruct || isExtern
      val method =
        if (isStaticCall) nir.Val.Global(name, nir.Type.Ptr)
        else
          val nir.Global.Member(_, sig) = name: @unchecked
          buf.method(self, sig, unwind)
      val values =
        if isExtern then args
        else self +: args

      val res = buf.call(sig, method, values, unwind)

      if !isExtern then res
      else {
        val nir.Type.Function(_, retty) = origSig
        fromExtern(retty, res)
      }
    }

    def genApplyStaticMethod(
        sym: Symbol,
        receiver: Symbol,
        argsp: Seq[Tree]
    )(using nir.SourcePosition): nir.Val = {
      require(!sym.isExtern, sym)

      val sig = genMethodSig(sym, statically = true)
      val args = genMethodArgs(sym, argsp)
      val methodName = genStaticMemberName(sym, receiver)
      val method = nir.Val.Global(methodName, nir.Type.Ptr)
      buf.call(sig, method, args, unwind)
    }

    private def genApplyExternAccessor(sym: Symbol, argsp: Seq[Tree])(using
        nir.SourcePosition
    ): nir.Val = {
      argsp match {
        case Seq() =>
          val ty = genMethodSig(sym).ret
          val externTy = genExternMethodSig(sym).ret
          genLoadExtern(ty, externTy, sym)
        case Seq(valuep) =>
          val externTy = genExternType(valuep.tpe)
          genStoreExtern(externTy, sym, genExpr(valuep))
      }
    }

    // Utils
    private def boxValue(st: SimpleType, value: nir.Val)(using
        nir.SourcePosition
    ): nir.Val = {
      if (st.sym.isUnsignedType)
        genApplyModuleMethod(
          defnNir.RuntimeBoxesModule,
          defnNir.BoxUnsignedMethod(st.sym),
          Seq(ValTree(value)())
        )
      else if (genPrimCode(st) == 'O') value
      else genApplyBox(st, ValTree(value)())
    }

    private def unboxValue(st: SimpleType, partial: Boolean, value: nir.Val)(
        using nir.SourcePosition
    ): nir.Val = {
      if (st.sym.isUnsignedType) {
        // Results of asInstanceOfs are partially unboxed, meaning
        // that non-standard value types remain to be boxed.
        if (partial) value
        else
          genApplyModuleMethod(
            defnNir.RuntimeBoxesModule,
            defnNir.UnboxUnsignedMethod(st.sym),
            Seq(ValTree(value)())
          )
      } else if (genPrimCode(st) == 'O') value
      else genApplyUnbox(st, ValTree(value)())
    }

    private def genSimpleOp(
        app: Apply,
        args: List[Tree],
        code: Int
    ): nir.Val = {
      given nir.SourcePosition = app.span
      val retty = genType(app.tpe)

      args match {
        case List(right)       => genUnaryOp(code, right, retty)
        case List(left, right) => genBinaryOp(code, left, right, retty)
        case _ =>
          report.error(
            s"Too many arguments for primitive function: $app",
            app.sourcePos
          )
          nir.Val.Zero(retty)
      }
    }

    private def negateBool(value: nir.Val)(using nir.SourcePosition): nir.Val =
      buf.bin(nir.Bin.Xor, nir.Type.Bool, nir.Val.True, value, unwind)

    private def genUnaryOp(code: Int, rightp: Tree, opty: nir.Type)(using
        nir.SourcePosition
    ): nir.Val = {
      val right = genExpr(rightp)
      val coerced = genCoercion(right, right.ty, opty)
      val tpe = coerced.ty

      def numOfType(num: Int, ty: nir.Type): nir.Val = ty match {
        case nir.Type.Byte                  => nir.Val.Byte(num.toByte)
        case nir.Type.Short | nir.Type.Char => nir.Val.Short(num.toShort)
        case nir.Type.Int                   => nir.Val.Int(num)
        case nir.Type.Long                  => nir.Val.Long(num.toLong)
        case nir.Type.Float                 => nir.Val.Float(num.toFloat)
        case nir.Type.Double                => nir.Val.Double(num.toDouble)
        case nir.Type.Size                  => nir.Val.Size(num.toLong)
        case _ => unsupported(s"num = $num, ty = ${ty.show}")
      }

      (opty, code) match {
        case (_: nir.Type.I | _: nir.Type.F, POS) => coerced
        case (_: nir.Type.I, NOT) =>
          buf.bin(nir.Bin.Xor, tpe, numOfType(-1, tpe), coerced, unwind)
        case (_: nir.Type.F, NEG) =>
          buf.bin(nir.Bin.Fmul, tpe, numOfType(-1, tpe), coerced, unwind)
        case (_: nir.Type.I, NEG) =>
          buf.bin(nir.Bin.Isub, tpe, numOfType(0, tpe), coerced, unwind)
        case (nir.Type.Bool, ZNOT) => negateBool(coerced)
        case _ =>
          report.error(s"Unknown unary operation code: $code", rightp.sourcePos)
          nir.Val.Null
      }
    }

    private def genBinaryOp(
        code: Int,
        left: Tree,
        right: Tree,
        retty: nir.Type
    )(using nir.SourcePosition): nir.Val = {
      val lty = genType(left.tpe)
      val rty = genType(right.tpe)
      val opty = {
        if (isShiftOp(code))
          if (lty == nir.Type.Long) nir.Type.Long
          else nir.Type.Int
        else
          binaryOperationType(lty, rty)
      }
      def genOp(op: (nir.Type, nir.Val, nir.Val) => nir.Op): nir.Val = {
        val leftcoerced = genCoercion(genExpr(left), lty, opty)(using left.span)
        val rightcoerced =
          genCoercion(genExpr(right), rty, opty)(using right.span)
        buf.let(op(opty, leftcoerced, rightcoerced), unwind)(using
          left.span,
          getScopeId
        )
      }

      val binres = opty match {
        case _: nir.Type.F =>
          code match {
            case ADD => genOp(nir.Op.Bin(nir.Bin.Fadd, _, _, _))
            case SUB => genOp(nir.Op.Bin(nir.Bin.Fsub, _, _, _))
            case MUL => genOp(nir.Op.Bin(nir.Bin.Fmul, _, _, _))
            case DIV => genOp(nir.Op.Bin(nir.Bin.Fdiv, _, _, _))
            case MOD => genOp(nir.Op.Bin(nir.Bin.Frem, _, _, _))

            case EQ => genOp(nir.Op.Comp(nir.Comp.Feq, _, _, _))
            case NE => genOp(nir.Op.Comp(nir.Comp.Fne, _, _, _))
            case LT => genOp(nir.Op.Comp(nir.Comp.Flt, _, _, _))
            case LE => genOp(nir.Op.Comp(nir.Comp.Fle, _, _, _))
            case GT => genOp(nir.Op.Comp(nir.Comp.Fgt, _, _, _))
            case GE => genOp(nir.Op.Comp(nir.Comp.Fge, _, _, _))

            case _ =>
              report.error(
                s"Unknown floating point type binary operation code: $code",
                right.sourcePos
              )
              nir.Val.Zero(retty)
          }
        case nir.Type.Bool | _: nir.Type.I =>
          code match {
            case ADD => genOp(nir.Op.Bin(nir.Bin.Iadd, _, _, _))
            case SUB => genOp(nir.Op.Bin(nir.Bin.Isub, _, _, _))
            case MUL => genOp(nir.Op.Bin(nir.Bin.Imul, _, _, _))
            case DIV => genOp(nir.Op.Bin(nir.Bin.Sdiv, _, _, _))
            case MOD => genOp(nir.Op.Bin(nir.Bin.Srem, _, _, _))

            case OR  => genOp(nir.Op.Bin(nir.Bin.Or, _, _, _))
            case XOR => genOp(nir.Op.Bin(nir.Bin.Xor, _, _, _))
            case AND => genOp(nir.Op.Bin(nir.Bin.And, _, _, _))
            case LSL => genOp(nir.Op.Bin(nir.Bin.Shl, _, _, _))
            case LSR => genOp(nir.Op.Bin(nir.Bin.Lshr, _, _, _))
            case ASR => genOp(nir.Op.Bin(nir.Bin.Ashr, _, _, _))

            case EQ => genOp(nir.Op.Comp(nir.Comp.Ieq, _, _, _))
            case NE => genOp(nir.Op.Comp(nir.Comp.Ine, _, _, _))
            case LT => genOp(nir.Op.Comp(nir.Comp.Slt, _, _, _))
            case LE => genOp(nir.Op.Comp(nir.Comp.Sle, _, _, _))
            case GT => genOp(nir.Op.Comp(nir.Comp.Sgt, _, _, _))
            case GE => genOp(nir.Op.Comp(nir.Comp.Sge, _, _, _))

            case ZOR  => genIf(retty, left, Literal(Constant(true)), right)
            case ZAND => genIf(retty, left, right, Literal(Constant(false)))
            case _ =>
              report.error(
                s"Unknown integer type binary operation code: $code",
                right.sourcePos
              )
              nir.Val.Zero(retty)
          }
        case _: nir.Type.RefKind =>
          def genEquals(ref: Boolean, negated: Boolean) = (left, right) match {
            // If null is present on either side, we must always
            // generate reference equality, regardless of where it
            // was called with == or eq. This shortcut is not optional.
            case (Literal(Constant(null)), _) | (_, Literal(Constant(null))) =>
              genClassEquality(left, right, ref = true, negated = negated)
            case _ =>
              genClassEquality(left, right, ref = ref, negated = negated)
          }

          code match {
            case EQ => genEquals(ref = false, negated = false)
            case NE => genEquals(ref = false, negated = true)
            case ID => genEquals(ref = true, negated = false)
            case NI => genEquals(ref = true, negated = true)
            case _ =>
              report.error(
                s"Unknown reference type binary operation code: $code",
                right.sourcePos
              )
              nir.Val.Zero(retty)
          }
        case nir.Type.Ptr =>
          code match {
            case EQ | ID => genOp(nir.Op.Comp(nir.Comp.Ieq, _, _, _))
            case NE | NI => genOp(nir.Op.Comp(nir.Comp.Ine, _, _, _))
          }
        case ty =>
          report.error(
            s"Unknown binary operation type: $ty",
            right.sourcePos
          )
          nir.Val.Zero(retty)
      }

      genCoercion(binres, binres.ty, retty)(using right.span)
    }

    private def binaryOperationType(lty: nir.Type, rty: nir.Type) =
      (lty, rty) match {
        // Bug compatibility with scala/bug/issues/11253
        case (nir.Type.Long, nir.Type.Float) =>
          nir.Type.Double

        case (nir.Type.Ptr, _: nir.Type.RefKind) =>
          lty

        case (_: nir.Type.RefKind, nir.Type.Ptr) =>
          rty

        case (nir.Type.Bool, nir.Type.Bool) =>
          nir.Type.Bool

        case (lhs: nir.Type.FixedSizeI, rhs: nir.Type.FixedSizeI) =>
          if (lhs.width < 32 && rhs.width < 32) {
            nir.Type.Int
          } else if (lhs.width >= rhs.width) {
            lhs
          } else {
            rhs
          }

        case (_: nir.Type.FixedSizeI, _: nir.Type.F) =>
          rty

        case (_: nir.Type.F, _: nir.Type.FixedSizeI) =>
          lty

        case (lhs: nir.Type.F, rhs: nir.Type.F) =>
          if (lhs.width >= rhs.width) lhs else rhs

        case (_: nir.Type.RefKind, _: nir.Type.RefKind) =>
          nir.Rt.Object

        case (ty1, ty2) if ty1 == ty2 =>
          ty1

        case (nir.Type.Nothing, ty) =>
          ty

        case (ty, nir.Type.Nothing) =>
          ty

        case _ =>
          report.error(s"can't perform binary operation between $lty and $rty")
          nir.Type.Nothing
      }

    private def genClassEquality(
        leftp: Tree,
        rightp: Tree,
        ref: Boolean,
        negated: Boolean
    )(using nir.SourcePosition): nir.Val = {

      if (ref) {
        // referencial equality
        val left = genExpr(leftp)
        val right = genExpr(rightp)
        val comp = if (negated) nir.Comp.Ine else nir.Comp.Ieq
        buf.comp(comp, nir.Rt.Object, left, right, unwind)
      } else genClassUniversalEquality(leftp, rightp, negated)
    }

    private def genClassUniversalEquality(l: Tree, r: Tree, negated: Boolean)(
        using nir.SourcePosition
    ): nir.Val = {

      /* True if the equality comparison is between values that require the use of the rich equality
       * comparator (scala.runtime.BoxesRunTime.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 = {
        // Exclude custom trees introduced by Scala Natvie from checks
        def isScalaTree(tree: Tree) = tree match {
          case _: ValTree  => false
          case _: ContTree => false
          case _           => true
        }
        val usesOnlyScalaTrees = isScalaTree(l) && isScalaTree(r)
        def areSameFinals = l.tpe.typeSymbol.is(Final) &&
          r.tpe.typeSymbol.is(Final) &&
          (l.tpe =:= r.tpe)
        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))
        }
        usesOnlyScalaTrees && !areSameFinals &&
          isMaybeBoxed(l.tpe.typeSymbol) &&
          isMaybeBoxed(r.tpe.typeSymbol)
      }
      def isNull(t: Tree) = t match {
        case Literal(Constant(null))   => true
        case ValTree(nir.Val.Null)     => true
        case ValTree(nir.Val.Zero(ty)) => nir.Type.isPtrType(ty)
        case _                         => false
      }
      def isLiteral(t: Tree) = t match {
        case Literal(_)      => true
        case ValTree(nirVal) => nirVal.isLiteral
        case _               => false
      }
      def isNonNullExpr(t: Tree): Boolean =
        isLiteral(t) || ((t.symbol ne null) && t.symbol.is(Module))

      def comparator = if (negated) nir.Comp.Ine else nir.Comp.Ieq
      def maybeNegate(v: nir.Val): nir.Val =
        if negated then negateBool(v) else v

      if (mustUseAnyComparator) maybeNegate {
        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
        }
        genApplyStaticMethod(equalsMethod, defn.BoxesRunTimeModule, Seq(l, r))
      }
      else if (isNull(l)) {
        // null == expr -> expr eq null
        buf.comp(comparator, nir.Rt.Object, genExpr(r), nir.Val.Null, unwind)
      } else if (isNull(r)) {
        // expr == null -> expr eq null
        buf.comp(comparator, nir.Rt.Object, genExpr(l), nir.Val.Null, unwind)
      } else if (isNonNullExpr(l)) maybeNegate {
        // SI-7852 Avoid null check if L is statically non-null.
        genApplyMethod(
          sym = defn.Any_equals,
          statically = false,
          selfp = l,
          argsp = Seq(r)
        )

      }
      else
        maybeNegate {
          // l == r -> if (l eq null) r eq null else l.equals(r)
          val thenn, elsen, mergen = fresh()
          val mergev = nir.Val.Local(fresh(), nir.Type.Bool)
          val left = genExpr(l)
          val isnull =
            buf.comp(nir.Comp.Ieq, nir.Rt.Object, left, nir.Val.Null, unwind)
          buf.branch(isnull, nir.Next(thenn), nir.Next(elsen))
          locally {
            buf.label(thenn)
            val right = genExpr(r)
            val thenv =
              buf.comp(nir.Comp.Ieq, nir.Rt.Object, right, nir.Val.Null, unwind)
            buf.jump(mergen, Seq(thenv))
          }
          locally {
            buf.label(elsen)
            val elsev = genApplyMethod(
              defn.Any_equals,
              statically = false,
              left,
              Seq(r)
            )
            buf.jump(mergen, Seq(elsev))
          }
          buf.label(mergen, Seq(mergev))
          mergev
        }
    }

    def genMethodArgs(sym: Symbol, argsp: Seq[Tree]): Seq[nir.Val] =
      if sym.isExtern
      then genExternMethodArgs(sym, argsp)
      else genSimpleArgs(argsp)

    private def genSimpleArgs(argsp: Seq[Tree]): Seq[nir.Val] =
      argsp.map(genExpr)

    private def genExternMethodArgs(
        sym: Symbol,
        argsp: Seq[Tree]
    ): Seq[nir.Val] = {
      val res = Seq.newBuilder[nir.Val]
      val nir.Type.Function(argTypes, _) = genExternMethodSig(sym)
      val paramTypes = sym.paramInfo.paramInfoss.flatten
      assert(
        argTypes.size == argsp.size && argTypes.size == paramTypes.size,
        "Different number of arguments passed to method signature and apply method"
      )

      def genArg(
          argp: Tree,
          paramTpe: Types.Type,
          isVarArg: Boolean = false
      ): nir.Val = {
        given nir.SourcePosition = argp.span
        given ExprBuffer = buf
        val externType = genExternType(paramTpe.finalResultType)
        val rawValue = genExpr(argp)
        val maybeUnboxed =
          if (isVarArg) ensureUnboxed(rawValue, paramTpe.finalResultType)
          else rawValue
        val value = (maybeUnboxed, nir.Type.box.get(externType)) match {
          case (value @ nir.Val.Null, Some(unboxedType)) =>
            externType match {
              case nir.Type.Ptr | _: nir.Type.RefKind =>
                value
              case _ =>
                report.warning(
                  s"Passing null as argument of type ${paramTpe.show} to the extern method is unsafe. " +
                    s"The argument would be unboxed to primitive value of type $externType.",
                  argp.srcPos
                )
                nir.Val.Zero(unboxedType)
            }
          case (value, _) => value
        }
        toExtern(externType, value)
      }

      for ((argp, sigType), paramTpe) <- argsp zip argTypes zip paramTypes
      do
        sigType match {
          case nir.Type.Vararg =>
            argp match {
              case Apply(_, List(seqLiteral: JavaSeqLiteral)) =>
                for tree <- seqLiteral.elems
                do
                  given nir.SourcePosition = tree.span
                  val tpe = tree
                    .getAttachment(NirDefinitions.NonErasedType)
                    .getOrElse(tree.tpe)
                  val arg = genArg(tree, tpe, isVarArg = true)
                  def isUnsigned = nir.Type.isUnsignedType(genType(tpe))
                  // Decimal varargs needs to be promoted to at least Int, and Float needs to be promoted to Double
                  val promotedArg = arg.ty match {
                    case nir.Type.Float =>
                      this.genCastOp(nir.Type.Float, nir.Type.Double, arg)
                    case i: nir.Type.FixedSizeI
                        if i.width < nir.Type.Int.width =>
                      val conv =
                        if (isUnsigned) nir.Conv.Zext
                        else nir.Conv.Sext
                      buf.conv(conv, nir.Type.Int, arg, unwind)

                    case _ => arg
                  }
                  res += promotedArg
              case _ =>
                report.error(
                  "Unable to extract vararg arguments, varargs to extern methods must be passed directly to the applied function",
                  argp.srcPos
                )
            }
          case _ => res += genArg(argp, paramTpe)
        }
      res.result()
    }

    private def genArrayOp(app: Apply, code: Int): nir.Val = {
      import NirPrimitives._
      import dotty.tools.backend.ScalaPrimitivesOps._

      val Apply(Select(arrayp, _), argsp) = app: @unchecked
      val nir.Type.Array(elemty, _) = genType(arrayp.tpe): @unchecked
      given nir.SourcePosition = app.span

      def elemcode = genArrayCode(arrayp.tpe)
      val array = genExpr(arrayp)

      if (code == ARRAY_CLONE)
        val method = defnNir.RuntimeArray_clone(elemcode)
        genApplyMethod(method, statically = true, array, argsp)
      else if (isArrayGet(code))
        val idx = genExpr(argsp(0))
        buf.arrayload(elemty, array, idx, unwind)
      else if (isArraySet(code))
        val idx = genExpr(argsp(0))
        val value = genExpr(argsp(1))
        buf.arraystore(elemty, array, idx, value, unwind)
      else buf.arraylength(array, unwind)
    }

    private def genHashCode(argp: Tree)(using nir.SourcePosition): nir.Val =
      genApplyStaticMethod(
        defn.staticsMethod(nme.anyHash),
        defn.ScalaStaticsModule,
        Seq(argp)
      )

    /*
     * 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 (nirPrimitives.isPrimitive(fun) &&
            nirPrimitives.getPrimitive(tree, larg.tpe) == CONCAT)
          liftStringConcat(larg) ::: rarg
        else
          tree :: Nil
      case _ =>
        tree :: Nil
    }

    /* Issue a call to `StringBuilder#append` for the right element type     */
    private final def genStringBuilderAppend(
        stringBuilder: nir.Val.Local,
        tree: Tree
    ): Unit = {
      given nir.SourcePosition = tree.span
      val tpe = tree.tpe
      val argType =
        if (tpe <:< defn.StringType) nir.Rt.String
        else if (tpe <:< defnNir.jlStringBufferType)
          genType(defnNir.jlStringBufferRef)
        else if (tpe <:< defnNir.jlCharSequenceType)
          genType(defnNir.jlCharSequenceRef)
        // Don't match for `Array(Char)`, even though StringBuilder has such an overload:
        // `"a" + Array('b')` should NOT be "ab", but "a[C@...".
        else if (tpe <:< defn.ObjectType) nir.Rt.Object
        else genType(tpe)

      val value = genExpr(tree)
      val (adaptedValue, targetType) = argType match {
        // jlStringBuilder does not have overloads for byte and short, but we can just use the int version
        case nir.Type.Byte | nir.Type.Short =>
          genCoercion(value, value.ty, nir.Type.Int) -> nir.Type.Int
        case nirType => value -> nirType
      }
      val (appendFunction, appendSig) = jlStringBuilderAppendForSymbol(
        targetType
      )
      buf.call(
        appendSig,
        appendFunction,
        Seq(stringBuilder, adaptedValue),
        unwind
      )
    }

    private lazy val jlStringBuilderRef =
      nir.Type.Ref(genTypeName(defnNir.jlStringBuilderRef))
    private lazy val jlStringBuilderCtor =
      jlStringBuilderRef.name.member(nir.Sig.Ctor(Seq(nir.Type.Int)))
    private lazy val jlStringBuilderCtorSig = nir.Type.Function(
      Seq(jlStringBuilderRef, nir.Type.Int),
      nir.Type.Unit
    )
    private lazy val jlStringBuilderToString =
      jlStringBuilderRef.name.member(
        nir.Sig.Method("toString", Seq(nir.Rt.String))
      )
    private lazy val jlStringBuilderToStringSig = nir.Type.Function(
      Seq(jlStringBuilderRef),
      nir.Rt.String
    )

    private def genStringConcat(tree: Apply): nir.Val = {
      given nir.SourcePosition = tree.span
      liftStringConcat(tree) match {
        // Optimization for expressions of the form "" + x
        case List(Literal(Constant("")), arg) =>
          genApplyStaticMethod(
            defn.String_valueOf_Object,
            defn.StringClass,
            Seq(arg)
          )

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

          // new StringBuidler(approxBuilderSize)
          val stringBuilder =
            buf.classalloc(jlStringBuilderRef.name, unwind, None)
          buf.call(
            jlStringBuilderCtorSig,
            nir.Val.Global(jlStringBuilderCtor, nir.Type.Ptr),
            Seq(stringBuilder, nir.Val.Int(approxBuilderSize)),
            unwind
          )
          // concat substrings
          concatArguments.foreach(genStringBuilderAppend(stringBuilder, _))
          // stringBuilder.toString
          buf.call(
            jlStringBuilderToStringSig,
            nir.Val.Global(jlStringBuilderToString, nir.Type.Ptr),
            Seq(stringBuilder),
            unwind
          )
      }
    }

    private def genStaticMember(sym: Symbol, receiver: Symbol)(using
        nir.SourcePosition
    ): nir.Val = {
      /* Actually, there is no static member in Scala Native. If we come here, that
       * is because we found the symbol in a Java-emitted .class in the
       * classpath. But the corresponding implementation in Scala Native will
       * actually be a val in the companion module.
       */

      if (sym == defn.BoxedUnit_UNIT) nir.Val.Unit
      else if (sym == defn.BoxedUnit_TYPE) nir.Val.Unit
      else genApplyStaticMethod(sym, receiver, Seq.empty)
    }

    private def genSynchronized(receiverp: Tree, bodyp: Tree)(using
        nir.SourcePosition
    ): nir.Val = {
      genSynchronized(receiverp)(_.genExpr(bodyp))
    }

    def genSynchronized(
        receiverp: Tree
    )(bodyGen: ExprBuffer => nir.Val)(using nir.SourcePosition): nir.Val = {
      // Here we wrap the synchronized call into the try-finally block
      // to ensure that monitor would be released even in case of the exception
      // or in case of non-local returns
      val nested = new ExprBuffer()
      val normaln = fresh()
      val handler = fresh()
      val mergen = fresh()

      // scalanative.runtime.`package`.enterMonitor(receiver)
      genApplyStaticMethod(
        defnNir.RuntimePackage_enterMonitor,
        defnNir.RuntimePackageClass,
        List(receiverp)
      )

      // synchronized block
      val retValue = scoped(curUnwindHandler := Some(handler)) {
        nested.label(normaln)
        bodyGen(nested)
      }
      val retty = retValue.ty
      val mergev = nir.Val.Local(fresh(), retty)
      nested.jumpExcludeUnitValue(retty)(mergen, retValue)

      // dummy exception handler,
      // monitorExit call would be added to it in genTryFinally transformer
      locally {
        val excv = nir.Val.Local(fresh(), nir.Rt.Object)
        nested.label(handler, Seq(excv))
        nested.raise(excv, unwind)
        nested.jumpExcludeUnitValue(retty)(mergen, nir.Val.Zero(retty))
      }

      // Append try/catch instructions to the outher instruction buffer.
      buf.jump(nir.Next(normaln))
      buf ++= genTryFinally(
        // scalanative.runtime.`package`.exitMonitor(receiver)
        ContTree(receiverp)(
          _.genApplyStaticMethod(
            defnNir.RuntimePackage_exitMonitor,
            defnNir.RuntimePackageClass,
            List(receiverp)
          )
        ),
        nested.toSeq
      )
      buf.labelExcludeUnitValue(mergen, mergev)
    }

    private def genThrow(tree: Tree, args: List[Tree]): nir.Val = {
      given nir.SourcePosition = tree.span
      val exception = args.head
      val res = genExpr(exception)
      buf.raise(res, unwind)
      nir.Val.Unit
    }

    def genCastOp(from: nir.Type, to: nir.Type, value: nir.Val)(using
        nir.SourcePosition
    ): nir.Val =
      castConv(from, to)
        .orElse(castConv(value.ty, to))
        .fold(value)(buf.conv(_, to, value, unwind))

    private def genCoercion(app: Apply, receiver: Tree, code: Int): nir.Val = {
      given nir.SourcePosition = app.span
      val rec = genExpr(receiver)
      val (fromty, toty) = coercionTypes(code)

      genCoercion(rec, fromty, toty)
    }

    private def genCoercion(value: nir.Val, fromty: nir.Type, toty: nir.Type)(
        using nir.SourcePosition
    ): nir.Val = {
      if (fromty == toty) value
      else if (fromty == nir.Type.Nothing || toty == nir.Type.Nothing) value
      else {
        val conv = (fromty, toty) match {
          case (nir.Type.Ptr, _: nir.Type.RefKind) => nir.Conv.Bitcast
          case (_: nir.Type.RefKind, nir.Type.Ptr) => nir.Conv.Bitcast
          case (l: nir.Type.FixedSizeI, r: nir.Type.FixedSizeI) =>
            if (l.width < r.width)
              if (l.signed) nir.Conv.Sext
              else nir.Conv.Zext
            else if (l.width > r.width) nir.Conv.Trunc
            else nir.Conv.Bitcast
          case (i: nir.Type.I, _: nir.Type.F) if i.signed => nir.Conv.Sitofp
          case (_: nir.Type.I, _: nir.Type.F)             => nir.Conv.Uitofp
          case (_: nir.Type.F, i: nir.Type.FixedSizeI) if i.signed =>
            if (i.width < 32) {
              val ivalue = genCoercion(value, fromty, nir.Type.Int)
              return genCoercion(ivalue, nir.Type.Int, toty)
            }
            nir.Conv.Fptosi
          case (_: nir.Type.F, i: nir.Type.FixedSizeI) if !i.signed =>
            if (i.width < 32) {
              val ivalue = genCoercion(value, fromty, nir.Type.Int)
              return genCoercion(ivalue, nir.Type.Int, toty)
            }
            nir.Conv.Fptoui
          case (nir.Type.Double, nir.Type.Float) => nir.Conv.Fptrunc
          case (nir.Type.Float, nir.Type.Double) => nir.Conv.Fpext
          case _ =>
            report.error(
              s"Unsupported coercion types: from $fromty to $toty"
            )
            nir.Conv.Bitcast
        }
        buf.conv(conv, toty, value, unwind)
      }
    }

    private def coercionTypes(code: Int): (nir.Type, nir.Type) = {
      code match {
        case B2B => (nir.Type.Byte, nir.Type.Byte)
        case B2S => (nir.Type.Byte, nir.Type.Short)
        case B2C => (nir.Type.Byte, nir.Type.Char)
        case B2I => (nir.Type.Byte, nir.Type.Int)
        case B2L => (nir.Type.Byte, nir.Type.Long)
        case B2F => (nir.Type.Byte, nir.Type.Float)
        case B2D => (nir.Type.Byte, nir.Type.Double)

        case S2B => (nir.Type.Short, nir.Type.Byte)
        case S2S => (nir.Type.Short, nir.Type.Short)
        case S2C => (nir.Type.Short, nir.Type.Char)
        case S2I => (nir.Type.Short, nir.Type.Int)
        case S2L => (nir.Type.Short, nir.Type.Long)
        case S2F => (nir.Type.Short, nir.Type.Float)
        case S2D => (nir.Type.Short, nir.Type.Double)

        case C2B => (nir.Type.Char, nir.Type.Byte)
        case C2S => (nir.Type.Char, nir.Type.Short)
        case C2C => (nir.Type.Char, nir.Type.Char)
        case C2I => (nir.Type.Char, nir.Type.Int)
        case C2L => (nir.Type.Char, nir.Type.Long)
        case C2F => (nir.Type.Char, nir.Type.Float)
        case C2D => (nir.Type.Char, nir.Type.Double)

        case I2B => (nir.Type.Int, nir.Type.Byte)
        case I2S => (nir.Type.Int, nir.Type.Short)
        case I2C => (nir.Type.Int, nir.Type.Char)
        case I2I => (nir.Type.Int, nir.Type.Int)
        case I2L => (nir.Type.Int, nir.Type.Long)
        case I2F => (nir.Type.Int, nir.Type.Float)
        case I2D => (nir.Type.Int, nir.Type.Double)

        case L2B => (nir.Type.Long, nir.Type.Byte)
        case L2S => (nir.Type.Long, nir.Type.Short)
        case L2C => (nir.Type.Long, nir.Type.Char)
        case L2I => (nir.Type.Long, nir.Type.Int)
        case L2L => (nir.Type.Long, nir.Type.Long)
        case L2F => (nir.Type.Long, nir.Type.Float)
        case L2D => (nir.Type.Long, nir.Type.Double)

        case F2B => (nir.Type.Float, nir.Type.Byte)
        case F2S => (nir.Type.Float, nir.Type.Short)
        case F2C => (nir.Type.Float, nir.Type.Char)
        case F2I => (nir.Type.Float, nir.Type.Int)
        case F2L => (nir.Type.Float, nir.Type.Long)
        case F2F => (nir.Type.Float, nir.Type.Float)
        case F2D => (nir.Type.Float, nir.Type.Double)

        case D2B => (nir.Type.Double, nir.Type.Byte)
        case D2S => (nir.Type.Double, nir.Type.Short)
        case D2C => (nir.Type.Double, nir.Type.Char)
        case D2I => (nir.Type.Double, nir.Type.Int)
        case D2L => (nir.Type.Double, nir.Type.Long)
        case D2F => (nir.Type.Double, nir.Type.Float)
        case D2D => (nir.Type.Double, nir.Type.Double)
      }
    }

    private def castConv(fromty: nir.Type, toty: nir.Type): Option[nir.Conv] =
      (fromty, toty) match {
        case (_: nir.Type.I, nir.Type.Ptr)       => Some(nir.Conv.Inttoptr)
        case (nir.Type.Ptr, _: nir.Type.I)       => Some(nir.Conv.Ptrtoint)
        case (_: nir.Type.RefKind, nir.Type.Ptr) => Some(nir.Conv.Bitcast)
        case (nir.Type.Ptr, _: nir.Type.RefKind) => Some(nir.Conv.Bitcast)
        case (_: nir.Type.RefKind, _: nir.Type.RefKind) =>
          Some(nir.Conv.Bitcast)
        case (_: nir.Type.RefKind, _: nir.Type.I) => Some(nir.Conv.Ptrtoint)
        case (_: nir.Type.I, _: nir.Type.RefKind) => Some(nir.Conv.Inttoptr)
        case (l: nir.Type.FixedSizeI, r: nir.Type.F) if l.width == r.width =>
          Some(nir.Conv.Bitcast)
        case (l: nir.Type.F, r: nir.Type.FixedSizeI) if l.width == r.width =>
          Some(nir.Conv.Bitcast)
        case _ if fromty == toty               => None
        case (nir.Type.Float, nir.Type.Double) => Some(nir.Conv.Fpext)
        case (nir.Type.Double, nir.Type.Float) => Some(nir.Conv.Fptrunc)
        case _ => unsupported(s"cast from $fromty to $toty")
      }

    /** Boxes a value of the given type before `elimErasedValueType`.
     *
     *  This should be used when sending values to a LLVM context, which is
     *  erased/boxed at the NIR level, although it is not erased at the
     *  dotty/JVM level.
     *
     *  @param value
     *    Value to be boxed if needed.
     *  @param tpeEnteringElimErasedValueType
     *    The type of `value` as it was entering the `elimErasedValueType`
     *    phase.
     */
    private def ensureBoxed(
        value: nir.Val,
        tpeEnteringPosterasure: core.Types.Type
    )(using buf: ExprBuffer, pos: nir.SourcePosition): nir.Val = {
      tpeEnteringPosterasure match {
        case tpe if tpe.isPrimitiveValueType =>
          buf.boxValue(tpe, value)

        case ErasedValueType(valueClass, _) =>
          val boxedClass = valueClass.typeSymbol.asClass
          val ctorName = genMethodName(boxedClass.primaryConstructor)
          val ctorSig = genMethodSig(boxedClass.primaryConstructor)

          val alloc = buf.classalloc(genTypeName(boxedClass), unwind)
          val ctor = buf.method(
            alloc,
            ctorName.asInstanceOf[nir.Global.Member].sig,
            unwind
          )
          buf.call(ctorSig, ctor, Seq(alloc, value), unwind)

          alloc

        case _ =>
          value
      }
    }

    /** Unboxes a value typed as Any to the given type before
     *  `elimErasedValueType`.
     *
     *  This should be used when receiving values from a LLVM context, which is
     *  erased/boxed at the NIR level, although it is not erased at the
     *  dotty/JVM level.
     *
     *  @param value
     *    Tree to be extracted.
     *  @param tpeEnteringElimErasedValueType
     *    The type of `value` as it was entering the `elimErasedValueType`
     *    phase.
     */
    private def ensureUnboxed(
        value: nir.Val,
        tpeEnteringPosterasure: core.Types.Type
    )(using
        buf: ExprBuffer,
        pos: nir.SourcePosition
    ): nir.Val = {
      tpeEnteringPosterasure match {
        case tpe if tpe.isPrimitiveValueType =>
          val targetTpe = genType(tpeEnteringPosterasure)
          if (targetTpe == value.ty) value
          else buf.unbox(genBoxType(tpe), value, nir.Next.None)

        case ErasedValueType(valueClass, _) =>
          val boxedClass = valueClass.typeSymbol.asClass
          val unboxMethod = ValueClasses.valueClassUnbox(boxedClass)
          val castedValue =
            buf.genCastOp(value.ty, genRefType(valueClass), value)
          buf.genApplyMethod(
            sym = unboxMethod,
            statically = false,
            self = castedValue,
            argsp = Nil
          )

        case tpe =>
          val unboxed = buf.unboxValue(tpe, partial = true, value)
          if (unboxed == value) // no need to or cannot unbox, we should cast
            buf.genCastOp(
              genRefType(tpeEnteringPosterasure),
              genRefType(tpe),
              value
            )
          else unboxed
      }
    }

    // Native specifc features
    private def genRawPtrOp(app: Apply, code: Int): nir.Val = {
      if (NirPrimitives.isRawPtrLoadOp(code)) genRawPtrLoadOp(app, code)
      else if (NirPrimitives.isRawPtrStoreOp(code)) genRawPtrStoreOp(app, code)
      else if (code == NirPrimitives.ELEM_RAW_PTR) genRawPtrElemOp(app)
      else {
        report.error(s"Unknown pointer operation #$code : $app", app.sourcePos)
        nir.Val.Null
      }
    }

    private def genRawPtrLoadOp(app: Apply, code: Int): nir.Val = {
      import NirPrimitives._
      given nir.SourcePosition = app.span

      val Apply(_, Seq(ptrp)) = app
      val ptr = genExpr(ptrp)
      val ty = code match {
        case LOAD_BOOL     => nir.Type.Bool
        case LOAD_CHAR     => nir.Type.Char
        case LOAD_BYTE     => nir.Type.Byte
        case LOAD_SHORT    => nir.Type.Short
        case LOAD_INT      => nir.Type.Int
        case LOAD_LONG     => nir.Type.Long
        case LOAD_FLOAT    => nir.Type.Float
        case LOAD_DOUBLE   => nir.Type.Double
        case LOAD_RAW_PTR  => nir.Type.Ptr
        case LOAD_RAW_SIZE => nir.Type.Size
        case LOAD_OBJECT   => nir.Rt.Object
      }
      val memoryOrder =
        Option.when(ptrp.symbol.isVolatile)(
          nir.MemoryOrder.Acquire
        )
      buf.load(ty, ptr, unwind, memoryOrder)
    }

    private def genRawPtrStoreOp(app: Apply, code: Int): nir.Val = {
      import NirPrimitives._
      given nir.SourcePosition = app.span
      val Apply(_, Seq(ptrp, valuep)) = app

      val ptr = genExpr(ptrp)
      val value = genExpr(valuep)
      val ty = code match {
        case STORE_BOOL     => nir.Type.Bool
        case STORE_CHAR     => nir.Type.Char
        case STORE_BYTE     => nir.Type.Byte
        case STORE_SHORT    => nir.Type.Short
        case STORE_INT      => nir.Type.Int
        case STORE_LONG     => nir.Type.Long
        case STORE_FLOAT    => nir.Type.Float
        case STORE_DOUBLE   => nir.Type.Double
        case STORE_RAW_PTR  => nir.Type.Ptr
        case STORE_RAW_SIZE => nir.Type.Size
        case STORE_OBJECT   => nir.Rt.Object
      }
      val memoryOrder = Option.when(ptrp.symbol.isVolatile)(
        nir.MemoryOrder.Release
      )
      buf.store(ty, ptr, value, unwind, memoryOrder)
    }

    private def genRawPtrElemOp(app: Apply): nir.Val = {
      given nir.SourcePosition = app.span
      val Apply(_, Seq(ptrp, offsetp)) = app

      val ptr = genExpr(ptrp)
      val offset = genExpr(offsetp)
      buf.elem(nir.Type.Byte, ptr, Seq(offset), unwind)
    }

    private def genRawPtrCastOp(app: Apply): nir.Val = {
      given nir.SourcePosition = app.span
      val Apply(_, Seq(argp)) = app

      val fromty = genType(argp.tpe)
      val toty = genType(app.tpe)
      val value = genExpr(argp)

      genCastOp(fromty, toty, value)
    }

    def genRawSizeCastOp(app: Apply, code: Int): nir.Val = {
      import NirPrimitives._
      given pos: nir.SourcePosition = app.span
      val Apply(_, Seq(argp)) = app
      val rec = genExpr(argp)
      val (toty, conv) = code match {
        case CAST_RAWSIZE_TO_INT  => nir.Type.Int -> nir.Conv.SSizeCast
        case CAST_RAWSIZE_TO_LONG => nir.Type.Long -> nir.Conv.SSizeCast
        case CAST_RAWSIZE_TO_LONG_UNSIGNED =>
          nir.Type.Long -> nir.Conv.ZSizeCast
        case CAST_INT_TO_RAWSIZE          => nir.Type.Size -> nir.Conv.SSizeCast
        case CAST_INT_TO_RAWSIZE_UNSIGNED => nir.Type.Size -> nir.Conv.ZSizeCast
        case CAST_LONG_TO_RAWSIZE         => nir.Type.Size -> nir.Conv.SSizeCast
      }

      buf.conv(conv, toty, rec, unwind)
    }

    private def genUnsignedOp(app: Tree, code: Int): nir.Val = {
      given nir.SourcePosition = app.span
      import NirPrimitives._
      def castToUnsigned = code == UNSIGNED_OF
      def castUnsignedInteger = code >= BYTE_TO_UINT && code <= INT_TO_ULONG
      def castUnsignedToFloat = code >= UINT_TO_FLOAT && code <= ULONG_TO_DOUBLE
      app match {
        case Apply(_, Seq(argp)) if castToUnsigned =>
          val ty = genType(app.tpe.resultType)
          val arg = genExpr(argp)

          buf.box(ty, arg, unwind)

        case Apply(_, Seq(argp)) if castUnsignedInteger =>
          val ty = genType(app.tpe)
          val arg = genExpr(argp)

          buf.conv(nir.Conv.Zext, ty, arg, unwind)

        case Apply(_, Seq(argp)) if castUnsignedToFloat =>
          val ty = genType(app.tpe)
          val arg = genExpr(argp)

          buf.conv(nir.Conv.Uitofp, ty, arg, unwind)

        case Apply(_, Seq(leftp, rightp)) =>
          val bin = code match {
            case DIV_UINT | DIV_ULONG => nir.Bin.Udiv
            case REM_UINT | REM_ULONG => nir.Bin.Urem
          }
          val ty = genType(leftp.tpe)
          val left = genExpr(leftp)
          val right = genExpr(rightp)

          buf.bin(bin, ty, left, right, unwind)
      }
    }

    private def getLinktimeCondition(
        condp: Tree
    ): Option[nir.LinktimeCondition] = {
      import nir.LinktimeCondition._
      def genComparsion(name: Name, value: nir.Val): nir.Comp = {
        def intOrFloatComparison(onInt: nir.Comp, onFloat: nir.Comp) =
          value.ty match {
            case _: nir.Type.F =>
              onFloat
            case _ =>
              onInt
          }

        import nir.Comp._
        name match {
          case nme.EQ => intOrFloatComparison(Ieq, Feq)
          case nme.NE => intOrFloatComparison(Ine, Fne)
          case nme.GT => intOrFloatComparison(Sgt, Fgt)
          case nme.GE => intOrFloatComparison(Sge, Fge)
          case nme.LT => intOrFloatComparison(Slt, Flt)
          case nme.LE => intOrFloatComparison(Sle, Fle)
          case nme =>
            report.error(s"Unsupported condition '$nme'", condp.sourcePos)
            nir.Comp.Ine
        }
      }

      condp match {
        // if(bool) (...)
        case Apply(LinktimeProperty(name, _, position), Nil) =>
          Some {
            SimpleCondition(
              propertyName = name,
              comparison = nir.Comp.Ieq,
              value = nir.Val.True
            )(using position)
          }

        // if(!bool) (...)
        case Apply(
              Select(
                Apply(LinktimeProperty(name, _, position), Nil),
                nme.UNARY_!
              ),
              Nil
            ) =>
          Some {
            SimpleCondition(
              propertyName = name,
              comparison = nir.Comp.Ieq,
              value = nir.Val.False
            )(using position)
          }

        // if(property  x) (...)
        case Apply(
              Select(LinktimeProperty(name, _, position), comp),
              List(arg @ Literal(Constant(_)))
            ) =>
          Some {
            val argValue = genLiteralValue(arg)
            SimpleCondition(
              propertyName = name,
              comparison = genComparsion(comp, argValue),
              value = argValue
            )(using position)
          }

        // special case for Scala 3 - it wraps != into !(_ == _)
        // if(property == x) (...)
        case Apply(
              Select(
                Apply(
                  Select(LinktimeProperty(name, _, position), nme.EQ),
                  List(arg @ Literal(Constant(_)))
                ),
                nme.UNARY_!
              ),
              Nil
            ) =>
          Some {
            val argValue = genLiteralValue(arg)
            SimpleCondition(
              propertyName = name,
              comparison = genComparsion(nme.NE, argValue),
              value = argValue
            )(using position)
          }

        // if(cond1 {&&,||} cond2) (...)
        case Apply(Select(cond1, op), List(cond2)) =>
          (getLinktimeCondition(cond1), getLinktimeCondition(cond2)) match {
            case (Some(c1), Some(c2)) =>
              val bin = op match {
                case nme.ZAND => nir.Bin.And
                case nme.ZOR  => nir.Bin.Or
              }
              given nir.SourcePosition = condp.span
              Some(ComplexCondition(bin, c1, c2))
            case (None, None) => None
            case _ =>
              report.error(
                "Mixing link-time and runtime conditions is not allowed",
                condp.sourcePos
              )
              None
          }

        case _ => None
      }
    }

    private lazy val optimizedFunctions = {
      // Included functions should be pure, and should not not narrow the result type
      Set[Symbol](
        defnNir.Intrinsics_castIntToRawSize,
        defnNir.Intrinsics_castIntToRawSizeUnsigned,
        defnNir.Intrinsics_castLongToRawSize,
        defnNir.Intrinsics_castRawSizeToInt,
        defnNir.Intrinsics_castRawSizeToLong,
        defnNir.Intrinsics_castRawSizeToLongUnsigned,
        defnNir.Size_fromByte,
        defnNir.Size_fromShort,
        defnNir.Size_fromInt,
        defnNir.USize_fromUByte,
        defnNir.USize_fromUShort,
        defnNir.USize_fromUInt,
        defnNir.RuntimePackage_fromRawSize,
        defnNir.RuntimePackage_fromRawUSize
      ) ++ defnNir.Intrinsics_unsignedOfAlts ++ defnNir.RuntimePackage_toRawSizeAlts
    }

    private def getUnboxedSize(sizep: Tree)(using nir.SourcePosition): nir.Val =
      sizep match {
        // Optimize call, strip numeric conversions
        case Literal(Constant(size: Int)) => nir.Val.Size(size)
        case Block(Nil, expr)             => getUnboxedSize(expr)
        case Apply(fun, List(arg))
            if optimizedFunctions.contains(fun.symbol) ||
              arg.symbol.exists && optimizedFunctions.contains(arg.symbol) =>
          getUnboxedSize(arg)
        case Typed(expr, _) => getUnboxedSize(expr)
        case _              =>
          // actual unboxing
          val size = genExpr(sizep)
          val sizeTy = nir.Type.normalize(size.ty)
          val unboxed =
            if nir.Type.unbox.contains(sizeTy) then
              buf.unbox(sizeTy, size, unwind)
            else if nir.Type.box.contains(sizeTy) then size
            else {
              report.error(
                s"Invalid usage of Intrinsic.stackalloc, argument is not an integer type: ${sizeTy}",
                sizep.srcPos
              )
              nir.Val.Size(0)
            }

          if (unboxed.ty == nir.Type.Size) unboxed
          else buf.conv(nir.Conv.SSizeCast, nir.Type.Size, unboxed, unwind)
      }

    def genStackalloc(app: Apply): nir.Val = {
      given nir.SourcePosition = app.span
      val Apply(_, args) = app
      val tpe = app
        .getAttachment(NonErasedType)
        .map(genType(_, deconstructValueTypes = true))
        .getOrElse {
          report.error(
            "Not found type attachment for stackalloc operation, report it as a bug.",
            app.srcPos
          )
          nir.Type.Nothing
        }

      val size = args match {
        case Seq()         => nir.Val.Size(1)
        case Seq(sizep)    => getUnboxedSize(sizep)
        case Seq(_, sizep) => getUnboxedSize(sizep)
        case _             => scalanative.util.unreachable
      }
      buf.stackalloc(tpe, size, unwind)
    }

    def genSafeZoneAlloc(app: Apply): nir.Val = {
      val Apply(_, List(sz, tree)) = app
      // For new expression with a specified safe zone, e.g. `new {sz} T(...)`,
      // it's translated to `allocate(sz, new T(...))` in TyperPhase.
      tree match {
        case Apply(Select(New(_), nme.CONSTRUCTOR), _)          =>
        case Apply(fun, _) if fun.symbol == defn.newArrayMethod =>
        case _ =>
          report.error(
            s"Unexpected tree in scala.scalanative.runtime.SafeZoneAllocator.allocate: `${tree}`",
            tree.srcPos
          )
      }
      // Put the zone into the attachment of `new T(...)`.
      if tree.hasAttachment(SafeZoneInstance) then
        report.warning(
          s"Safe zone handle is already attached to ${tree}, which is unexpected.",
          tree.srcPos
        )
      tree.putAttachment(SafeZoneInstance, genExpr(sz))
      genExpr(tree)
    }

    def genCQuoteOp(app: Apply): nir.Val = {
      app match {
        // case q"""
        //   scala.scalanative.unsafe.`package`.CQuote(
        //     new StringContext(scala.this.Predef.wrapRefArray(
        //       Array[String]{${str: String}}.$asInstanceOf[Array[Object]]()
        //     ))
        //   ).c()
        case Apply(
              Select(
                Apply(
                  _, // Ident(CQuote),
                  List(
                    Apply(
                      _,
                      List(Apply(_, List(javaSeqLiteral: JavaSeqLiteral)))
                    )
                  )
                ),
                _
              ),
              _
            ) =>
          given nir.SourcePosition = app.span
          val List(Literal(Constant(str: String))) =
            javaSeqLiteral.elems: @unchecked
          val bytes = nir.Val.ByteString(StringUtils.processEscapes(str))
          val const = nir.Val.Const(bytes)
          buf.box(nir.Rt.BoxedPtr, const, unwind)

        case _ =>
          report.error("Failed to interpret CQuote", app.sourcePos)
          nir.Val.Null
      }
    }

    def genClassFieldRawPtr(app: Apply): nir.Val = {
      given nir.SourcePosition = app.span
      val Apply(_, List(target, fieldName: Literal)) = app: @unchecked
      val fieldNameId = fieldName.const.stringValue
      val classInfo = target.tpe.finalResultType
      val classInfoSym = classInfo.typeSymbol.asClass
      def matchesName(f: SingleDenotation) =
        f.name.mangledString == fieldNameId
      def isImmutableField(f: SymDenotation) = {
        // If `val` was defined in trait it would be internally mutable, but with stable accessors
        !f.is(Mutable) || classInfoSym.parentSyms.exists(s =>
          s.asClass.info.decls.exists { f =>
            matchesName(f) && f.asSymDenotation
              .isAllOf(Method | Accessor, butNot = Mutable)
          }
        )
      }

      val allFields =
        classInfoSym.info.fields ++ classInfoSym.info.parents.flatMap(_.fields)
      allFields
        .collectFirst {
          case f if matchesName(f) =>
            // Don't allow to get pointer to immutable field, as it might allow for mutation
            if (isImmutableField(f.asSymDenotation)) {
              val owner = f.asSymDenotation.owner
              report.error(
                s"Resolving pointer of immutable field ${fieldNameId} in ${owner.show} is not allowed"
              )
            }
            buf.field(genExpr(target), genFieldName(f.symbol), unwind)
        }
        .getOrElse {
          report.error(
            s"${classInfoSym.show} does not contain field ${fieldNameId}",
            app.sourcePos
          )
          nir.Val.Int(-1)
        }
    }

    def genSizeOf(app: Apply): nir.Val =
      genLayoutValueOf("sizeOf", buf.sizeOf(_, unwind))(app)
    def genAlignmentOf(app: Apply): nir.Val =
      genLayoutValueOf("alignmentOf", buf.alignmentOf(_, unwind))(app)

    private def genLayoutValueOf(
        opType: => String,
        toVal: nir.SourcePosition ?=> nir.Type => nir.Val
    )(app: Apply): nir.Val = {
      given nir.SourcePosition = app.span
      def fail(msg: => String) =
        report.error(msg, app.srcPos)
        nir.Val.Zero(nir.Type.Size)

      app.getAttachment(NirDefinitions.NonErasedType) match
        case None =>
          app.args match {
            case Seq(Literal(cls: Constant)) =>
              val nirTpe = genType(cls.typeValue, deconstructValueTypes = false)
              toVal(nirTpe)
            case _ =>
              fail(
                s"Method $opType(Class[_]) requires single class literal argument, if you used $opType[T] report it as a bug"
              )
          }
        case Some(tpe) if tpe.typeSymbol.isTraitOrInterface =>
          fail(
            s"Type ${tpe.show} is a trait or interface, its $opType cannot be calculated"
          )
        case Some(tpe) =>
          try {
            val nirTpe = genType(tpe, deconstructValueTypes = true)
            toVal(nirTpe)
          } catch {
            case ex: Throwable =>
              fail(
                s"Failed to generate exact NIR type of ${tpe.show} - ${ex.getMessage}"
              )
          }
    }

    def genLoadExtern(ty: nir.Type, externTy: nir.Type, sym: Symbol)(using
        nir.SourcePosition
    ): nir.Val = {
      assert(sym.isExtern, "loadExtern was not extern")

      val name = nir.Val.Global(genName(sym), nir.Type.Ptr)
      val memoryOrder = Option.when(sym.isVolatile)(
        nir.MemoryOrder.Acquire
      )
      fromExtern(
        ty,
        buf.load(externTy, name, unwind, memoryOrder)
      )
    }

    def genStoreExtern(externTy: nir.Type, sym: Symbol, value: nir.Val)(using
        nir.SourcePosition
    ): nir.Val = {
      assert(sym.isExtern, "storeExtern was not extern")
      val name = nir.Val.Global(genName(sym), nir.Type.Ptr)
      val externValue = toExtern(externTy, value)
      val memoryOrder = Option.when(sym.isVolatile)(
        nir.MemoryOrder.Release
      )

      buf.store(externTy, name, externValue, unwind, memoryOrder)
    }

    def toExtern(expectedTy: nir.Type, value: nir.Val)(using
        nir.SourcePosition
    ): nir.Val =
      (expectedTy, value.ty) match {
        case (nir.Type.Unit, _) => nir.Val.Unit
        case (_, refty: nir.Type.Ref)
            if nir.Type.boxClasses.contains(refty.name)
              && nir.Type.unbox(nir.Type.Ref(refty.name)) == expectedTy =>
          buf.unbox(nir.Type.Ref(refty.name), value, unwind)
        case _ =>
          value
      }

    def fromExtern(expectedTy: nir.Type, value: nir.Val)(using
        nir.SourcePosition
    ): nir.Val =
      (expectedTy, value.ty) match {
        case (refty: nir.Type.Ref, ty)
            if nir.Type.boxClasses.contains(refty.name)
              && nir.Type.unbox(nir.Type.Ref(refty.name)) == ty =>
          buf.box(nir.Type.Ref(refty.name), value, unwind)
        case _ =>
          value
      }

    /** Generates direct call to function ptr with optional unboxing arguments
     *  and boxing result Apply.args can contain different number of arguments
     *  depending on usage, however they are passed in constant order:
     *    - 0..N args
     *    - return type evidence
     */
    private def genCFuncPtrApply(app: Apply): nir.Val = {
      given nir.SourcePosition = app.span
      val Apply(appRec @ Select(receiverp, _), aargs) = app: @unchecked

      val attachment = app
        .getAttachment(NirDefinitions.NonErasedTypes)
        .orElse(appRec.getAttachment(NirDefinitions.NonErasedTypes))

      val paramTypes = attachment match {
        case None =>
          report.error(
            s"Failed to generated exact NIR types for $app, something is wrong with scala-native internls.",
            app.srcPos
          )
          return nir.Val.Null
        case Some(paramTys) => paramTys
      }

      val self = genExpr(receiverp)
      val retType = genType(paramTypes.last)
      val unboxedRetType = nir.Type.unbox.getOrElse(retType, retType)

      val args = aargs
        .zip(paramTypes)
        .map {
          case (Apply(Select(_, nme.box), List(value)), _) =>
            genExpr(value)
          case (arg, ty) =>
            given nir.SourcePosition = arg.span
            val tpe = genType(ty)
            val obj = genExpr(arg)

            /* buf.unboxValue does not handle Ref( Ptr | CArray | ... ) unboxing
             * That's why we're doing it directly */
            if (nir.Type.unbox.isDefinedAt(tpe)) buf.unbox(tpe, obj, unwind)
            else buf.unboxValue(fromType(ty), partial = false, obj)
        }
      val argTypes = args.map(_.ty)
      val funcSig = nir.Type.Function(argTypes, unboxedRetType)

      val selfName = genTypeName(defnNir.CFuncPtrClass)
      val getRawPtrName = selfName
        .member(nir.Sig.Field("rawptr", nir.Sig.Scope.Private(selfName)))

      val target = buf.fieldload(nir.Type.Ptr, self, getRawPtrName, unwind)
      val result = buf.call(funcSig, target, args, unwind)
      if (retType != unboxedRetType) buf.box(retType, result, unwind)
      else boxValue(paramTypes.last, result)
    }

    private final val ExternForwarderSig =
      nir.Sig.Generated("$extern$forwarder")

    private def genCFuncFromScalaFunction(app: Apply): nir.Val = {
      given pos: nir.SourcePosition = app.span
      val paramTypes = app.getAttachment(NirDefinitions.NonErasedTypes) match
        case None =>
          report.error(
            s"Failed to generate exact NIR types for $app, something is wrong with scala-native internals.",
            app.srcPos
          )
          Nil
        case Some(paramTys) =>
          paramTys.map(fromType)

      val fn :: _ = app.args: @unchecked

      @tailrec
      def resolveFunction(tree: Tree): nir.Val = tree match {
        case Typed(expr, _) => resolveFunction(expr)
        case Block(_, expr) => resolveFunction(expr)
        case fn @ Closure(env, target, _) =>
          if env.nonEmpty then
            report.error(
              s"Closing over local state of ${env.map(_.symbol.show).mkString(", ")} in function transformed to CFuncPtr results in undefined behaviour.",
              fn.srcPos
            )

          val fnRef = genClosure(fn)
          val nir.Type.Ref(className, _, _) = fnRef.ty: @unchecked

          generatedDefns += genFuncExternForwarder(
            className,
            target.symbol,
            fn,
            paramTypes
          )
          fnRef

        case ref: RefTree =>
          report.error(
            s"Function passed to ${app.symbol.show} needs to be inlined",
            tree.sourcePos
          )
          nir.Val.Null

        case _ =>
          report.error(
            "Failed to resolve function ref for extern forwarder",
            tree.sourcePos
          )
          nir.Val.Null
      }

      val fnRef = resolveFunction(fn)
      val className = genTypeName(app.tpe.sym)

      val ctorTy = nir.Type.Function(
        Seq(nir.Type.Ref(className), nir.Type.Ptr),
        nir.Type.Unit
      )
      val ctorName = className.member(nir.Sig.Ctor(Seq(nir.Type.Ptr)))
      val rawptr = buf.method(fnRef, ExternForwarderSig, unwind)

      val alloc = buf.classalloc(className, unwind)
      buf.call(
        ctorTy,
        nir.Val.Global(ctorName, nir.Type.Ptr),
        Seq(alloc, rawptr),
        unwind
      )
      alloc
    }

    private def genFuncExternForwarder(
        funcName: nir.Global,
        funSym: Symbol,
        funTree: Closure,
        evidences: List[SimpleType]
    )(using nir.SourcePosition): nir.Defn = {
      val attrs = nir.Attrs(isExtern = true)

      // In case if passed function is adapted closure it's param types
      // would be erased, in such case we would recover original types
      // using evidence types (materialized unsafe.Tags)
      val isAdapted = funSym.name.mangledString.contains("$adapted$")
      val sig = genMethodSig(funSym)
      val externSig = genExternMethodSig(funSym)

      val nir.Type.Function(origtys, _) =
        if (!isAdapted) sig
        else {
          val params :+ retty = evidences
            .map(genType(_))
            .map(t => nir.Type.box.getOrElse(t, t)): @unchecked
          nir.Type.Function(params, retty)
        }

      val forwarderSig @ nir.Type.Function(paramtys, retty) =
        if (!isAdapted) externSig
        else {
          val params :+ retty = evidences
            .map(genExternType)
            .map(t => nir.Type.unbox.getOrElse(t, t)): @unchecked
          nir.Type.Function(params, retty)
        }

      val forwarderName = funcName.member(ExternForwarderSig)
      val forwarderBody = scoped(
        curUnwindHandler := None,
        curScopeId := nir.ScopeId.TopLevel
      ) {
        val fresh = nir.Fresh()
        val buf = ExprBuffer(using fresh)

        val params = paramtys.map(ty => nir.Val.Local(fresh(), ty))
        buf.label(fresh(), params)
        val origTypes =
          if (funSym.isStaticInNIR || isAdapted) origtys else origtys.tail
        val boxedParams = origTypes.zip(params).map(buf.fromExtern(_, _))
        val argsp = boxedParams.map(ValTree(funTree)(_))

        // Check number of arguments that would be be used in a call to the function,
        // it should be equal to the quantity of implicit evidences (without return type evidence)
        // and arguments passed via closure env.
        if (argsp.size != evidences.length - 1 + funTree.env.size) {
          report.error(
            "Failed to create scalanative.unsafe.CFuncPtr from scala.Function, report this issue to Scala Native team.",
            funTree.srcPos
          )
        }

        val res =
          if (funSym.isStaticInNIR)
            buf.genApplyStaticMethod(funSym, NoSymbol, argsp)
          else
            val owner =
              if funSym.owner.companionModule.exists then
                buf.genModule(funSym.owner)
              else
                // Safe becouse usage of This is guarded in NativeInterop
                nir.Val.Null
            val selfp = ValTree(funTree)(owner)
            buf.genApplyMethod(funSym, statically = true, selfp, argsp)

        val unboxedRes = buf.toExtern(retty, res)
        buf.ret(unboxedRes)

        buf.toSeq
      }
      new nir.Defn.Define(attrs, forwarderName, forwarderSig, forwarderBody)
    }

    private object WrapArray {
      lazy val isWrapArray: Set[Symbol] = {
        val names = defn
          .ScalaValueClasses()
          .map(sym => nme.wrapXArray(sym.name))
          .concat(Set(nme.wrapRefArray, nme.genericWrapArray))
        val symsInPredef = names.map(defn.ScalaPredefModule.requiredMethod(_))
        val symsInScalaRunTime =
          names.map(defn.ScalaRuntimeModule.requiredMethod(_))
        (symsInPredef ++ symsInScalaRunTime).toSet
      }

      def unapply(tree: Apply): Option[Tree] = tree match {
        case Apply(wrapArray_?, List(wrapped))
            if isWrapArray(wrapArray_?.symbol) =>
          Some(wrapped)
        case _ =>
          None
      }
    }

    private def genReflectiveCall(
        tree: Apply,
        isSelectDynamic: Boolean
    ): nir.Val = {
      given nir.SourcePosition = tree.span
      val Apply(fun @ Select(receiver, _), args) = tree: @unchecked

      val selectedValue = genApplyMethod(
        defnNir.ReflectSelectable_selectedValue,
        statically = false,
        genExpr(receiver),
        Seq.empty
      )

      // Extract the method name as a String
      val methodNameStr = args.head match {
        case Literal(Constants.Constant(name: String)) => name
        case _ =>
          report.error(
            "The method name given to Selectable.selectDynamic or Selectable.applyDynamic " +
              "must be a literal string. " +
              "Other uses are not supported in Scala Native.",
            args.head.sourcePos
          )
          "erroneous"
      }

      val (formalParamTypeRefs, actualArgs) =
        if (isSelectDynamic) (Nil, Nil)
        else
          args.tail match {
            // Extract the param type refs and actual args from the 2nd and 3rd argument to applyDynamic
            case WrapArray(classOfsArray: JavaSeqLiteral) ::
                WrapArray(actualArgsAnyArray: JavaSeqLiteral) :: Nil =>
              // Extract nir.Type's from the classOf[_] trees
              val formalParamTypes = classOfsArray.elems.map {
                // classOf[tp] -> tp
                case Literal(const) if const.tag == Constants.ClazzTag =>
                  genType(const.typeValue)

                // Anything else is invalid
                case otherTree =>
                  report.error(
                    "The java.lang.Class[_] arguments passed to Selectable.applyDynamic must be " +
                      "literal classOf[T] expressions (typically compiler-generated). " +
                      "Other uses are not supported in Scala Native.",
                    otherTree.sourcePos
                  )
                  nir.Rt.Object
              }

              // Gen the actual args, downcasting them to the formal param types
              val actualArgs =
                actualArgsAnyArray.elems
                  .zip(formalParamTypes)
                  .map { (actualArgAny, formalParamType) =>
                    val genActualArgAny = genExpr(actualArgAny)
                    (genActualArgAny.ty, formalParamType) match {
                      case (ty: nir.Type.Ref, formal: nir.Type.Ref) =>
                        if ty.name == formal.name then genActualArgAny
                        else
                          buf.as(
                            formalParamType,
                            genActualArgAny,
                            unwind
                          )

                      case (ty: nir.Type.Ref, formal: nir.Type.PrimitiveKind) =>
                        assert(nir.Type.Ref(ty.name) == nir.Type.box(formal))
                        genActualArgAny

                      case _ => scalanative.util.unreachable
                    }
                  }

              (formalParamTypes, actualArgs)

            case _ =>
              report.error(
                "Passing the varargs of Selectable.applyDynamic with `: _*` " +
                  "is not supported in Scala Native.",
                tree.sourcePos
              )
              (Nil, Nil)
          }

      val dynMethod = buf.dynmethod(
        selectedValue,
        nir.Sig.Proxy(methodNameStr, formalParamTypeRefs),
        unwind
      )
      // Proxies operate only on boxed types, however formal param types and name of the method
      // might contain primitive types. With current imlementation of proxies we workaround it
      // by always using boxed types in function calls
      val boxedFormalParamTypeRefs = formalParamTypeRefs.map {
        case ty: nir.Type.PrimitiveKind =>
          nir.Type.box(ty)
        case ty =>
          ty
      }
      buf.call(
        nir.Type.Function(
          selectedValue.ty :: boxedFormalParamTypeRefs,
          nir.Rt.Object
        ),
        dynMethod,
        selectedValue :: actualArgs,
        unwind
      )
    }

    private def labelExcludeUnitValue(label: nir.Local, value: nir.Val.Local)(
        using nir.SourcePosition
    ): nir.Val =
      value.ty match
        case nir.Type.Unit =>
          buf.label(label); nir.Val.Unit
        case _ =>
          buf.label(label, Seq(value)); value

    private def jumpExcludeUnitValue(
        mergeType: nir.Type
    )(label: nir.Local, value: nir.Val)(using
        nir.SourcePosition
    ): Unit =
      mergeType match
        case nir.Type.Unit =>
          buf.jump(label, Nil)
        case _ =>
          buf.jump(label, Seq(value))

  }

  sealed class FixupBuffer(using fresh: nir.Fresh)
      extends nir.InstructionBuilder {
    private var labeled = false

    override def +=(inst: nir.Inst): Unit = {
      given nir.SourcePosition = inst.pos
      inst match {
        case inst: nir.Inst.Label =>
          if (labeled) {
            unreachable(unwind)
          }
          labeled = true
        case _ =>
          if (!labeled) {
            label(fresh())
          }
          labeled = !inst.isInstanceOf[nir.Inst.Cf]
      }
      super.+=(inst)
      inst match {
        case nir.Inst.Let(_, op, _) if op.resty == nir.Type.Nothing =>
          unreachable(unwind)
          label(fresh())
        case _ => ()
      }
    }

    override def ++=(insts: Seq[nir.Inst]): Unit =
      insts.foreach { inst => this += inst }

    override def ++=(other: nir.InstructionBuilder): Unit =
      this ++= other.toSeq
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy