alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

Scala example source code file (Inliners.scala)

This example Scala source code file (Inliners.scala) is included in my "Source Code Warehouse" project. The intent of this project is to help you more easily find Scala source code examples by using tags.

All credit for the original source code belongs to scala-lang.org; I'm just trying to make examples easier to find. (For my Scala work, see my Scala examples and tutorials.)

Scala tags/keywords

boolean, call_method, collection, compiler, dontinlinehere, inlinesafetyinfo, int, list, local, nsc, private, reflection, string, symbol, utilities

The Inliners.scala Scala example source code

/* NSC -- new Scala compiler
 * Copyright 2005-2013 LAMP/EPFL
 * @author  Iulian Dragos
 */


package scala.tools.nsc
package backend.opt

import scala.collection.mutable
import scala.tools.nsc.symtab._
import scala.reflect.internal.util.NoSourceFile

/**
 * Inliner balances two competing goals:
 *   (a) aggressive inlining of:
 *       (a.1) the apply methods of anonymous closures, so that their anon-classes can be eliminated;
 *       (a.2) higher-order-methods defined in an external library, e.g. `Range.foreach()` among many others.
 *   (b) circumventing the barrier to inter-library inlining that private accesses in the callee impose.
 *
 * Summing up the discussion in SI-5442 and SI-5891,
 * the current implementation achieves to a large degree both goals above, and
 * overcomes a problem exhibited by previous versions:
 *
 *   (1) Problem: Attempting to access a private member `p` at runtime resulting in an `IllegalAccessError`,
 *                where `p` is defined in a library L, and is accessed from a library C (for Client),
 *                where C was compiled against L', an optimized version of L where the inliner made `p` public at the bytecode level.
 *                The only such members are fields, either synthetic or isParamAccessor, and thus having a dollar sign in their name
 *                (the accesibility of methods and constructors isn't touched by the inliner).
 *
 * Thus we add one more goal to our list:
 *   (c) Compile C (either optimized or not) against any of L or L',
 *       so that it runs with either L or L' (in particular, compile against L' and run with L).
 *
 * The chosen strategy is described in some detail in the comments for `accessRequirements()` and `potentiallyPublicized()`.
 * Documentation at http://lamp.epfl.ch/~magarcia/ScalaCompilerCornerReloaded/2011Q4/Inliner.pdf
 *
 *  @author Iulian Dragos
 */
abstract class Inliners extends SubComponent {
  import global._
  import icodes._
  import icodes.opcodes._
  import definitions.{
    NullClass, NothingClass, ObjectClass,
    PredefModule, RuntimePackage, ScalaInlineClass, ScalaNoInlineClass,
    isFunctionType, isByNameParamType
  }

  val phaseName = "inliner"

  override val enabled: Boolean = settings.inline

  /** Debug - for timing the inliner. */
  /****
  private def timed[T](s: String, body: => T): T = {
    val t1 = System.currentTimeMillis()
    val res = body
    val t2 = System.currentTimeMillis()
    val ms = (t2 - t1).toInt
    if (ms >= MAX_INLINE_MILLIS)
      println("%s: %d milliseconds".format(s, ms))

    res
  }
  ****/

  /** Look up implementation of method 'sym in 'clazz'.
   */
  def lookupImplFor(sym: Symbol, clazz: Symbol): Symbol = {
    // TODO: verify that clazz.superClass is equivalent here to clazz.tpe.parents(0).typeSymbol (.tpe vs .info)
    def needsLookup = (
         (clazz != NoSymbol)
      && (clazz != sym.owner)
      && !sym.isEffectivelyFinalOrNotOverridden
      && clazz.isEffectivelyFinalOrNotOverridden
    )
    def lookup(clazz: Symbol): Symbol = {
      // println("\t\tlooking up " + meth + " in " + clazz.fullName + " meth.owner = " + meth.owner)
      assert(clazz != NoSymbol, "Walked up past Object.superClass looking for " + sym +
                                ", most likely this reveals the TFA at fault (receiver and callee don't match).")
      if (sym.owner == clazz || isBottomType(clazz)) sym
      else sym.overridingSymbol(clazz) orElse (
        if (sym.owner.isTrait) sym
        else lookup(clazz.superClass)
      )
    }
    if (needsLookup) {
      val concreteMethod = lookup(clazz)
      debuglog("\tlooked up method: " + concreteMethod.fullName)

      concreteMethod
    }
    else sym
  }

  /* A warning threshold */
  private final val MAX_INLINE_MILLIS = 2000

  /** The maximum size in basic blocks of methods considered for inlining. */
  final val MAX_INLINE_SIZE = 16

  /** Maximum loop iterations. */
  final val MAX_INLINE_RETRY = 15

  /** Small method size (in blocks) */
  val SMALL_METHOD_SIZE = 1

  /** Create a new phase */
  override def newPhase(p: Phase) = new InliningPhase(p)

  /** The Inlining phase.
   */
  class InliningPhase(prev: Phase) extends ICodePhase(prev) {
    def name = phaseName
    val inliner = new Inliner

    object iclassOrdering extends Ordering[IClass] {
      def compare(a: IClass, b: IClass) = {
        val sourceNamesComparison = (a.cunit.toString() compare b.cunit.toString())
        if(sourceNamesComparison != 0) sourceNamesComparison
        else {
          val namesComparison = (a.toString() compare b.toString())
          if(namesComparison != 0) namesComparison
          else {
            a.symbol.id compare b.symbol.id
          }
        }
      }
    }
    val queue = new mutable.PriorityQueue[IClass]()(iclassOrdering)

    override def apply(c: IClass) { queue += c }

    override def run() {
      knownLacksInline.clear()
      knownHasInline.clear()
      try {
        super.run()
        for(c <- queue) { inliner analyzeClass c }
      } finally {
        inliner.clearCaches()
        knownLacksInline.clear()
        knownHasInline.clear()
      }
    }
  }

  def isBottomType(sym: Symbol) = sym == NullClass || sym == NothingClass

  /** Is the given class a closure? */
  def isClosureClass(cls: Symbol): Boolean =
    cls.isFinal && cls.isSynthetic && !cls.isModuleClass && cls.isAnonymousFunction

  /*
      TODO now that Inliner runs faster we could consider additional "monadic methods" (in the limit, all those taking a closure as last arg)
      Any "monadic method" occurring in a given caller C that is not `isMonadicMethod()` will prevent CloseElim from eliminating
      any anonymous-closure-class any whose instances are given as argument to C invocations.
   */
  def isMonadicMethod(sym: Symbol) = {
    nme.unspecializedName(sym.name) match {
      case nme.foreach | nme.filter | nme.withFilter | nme.map | nme.flatMap => true
      case _                                                                 => false
    }
  }

  val knownLacksInline = mutable.Set.empty[Symbol] // cache to avoid multiple inliner.hasInline() calls.
  val knownHasInline   = mutable.Set.empty[Symbol] // as above. Motivated by the need to warn on "inliner failures".

  def hasInline(sym: Symbol)    = {
    if     (knownLacksInline(sym)) false
    else if(knownHasInline(sym))   true
    else {
      val b = (sym hasAnnotation ScalaInlineClass)
      if(b) { knownHasInline   += sym }
      else  { knownLacksInline += sym }

      b
    }
  }

  def hasNoInline(sym: Symbol)  = sym hasAnnotation ScalaNoInlineClass

  /**
   * Simple inliner.
   */
  class Inliner {
    object NonPublicRefs extends Enumeration {
      val Private, Protected, Public = Value

      /** Cache whether a method calls private members. */
      val usesNonPublics = mutable.Map.empty[IMethod, Value]
    }
    import NonPublicRefs._

    /** The current iclass */
    private var currentIClazz: IClass = _
    private def warn(pos: Position, msg: String) = currentIClazz.cunit.inlinerWarning(pos, msg)

    private def ownedName(sym: Symbol): String = exitingUncurry {
      val count = (
        if (!sym.isMethod) 1
        else if (sym.owner.isAnonymousFunction) 3
        else 2
      )
      (sym.ownerChain take count filterNot (_.isPackageClass)).reverseMap(_.nameString).mkString(".")
    }
    private def inlineLog(what: String, main: => String, comment: => String) {
      def cstr = comment match {
        case ""   => ""
        case str  => " // " + str
      }
      val width = if (currentIClazz eq null) 40 else currentIClazz.symbol.enclosingPackage.fullName.length + 25
      val fmt = "%8s  %-" + width + "s" + cstr
      log(fmt.format(what, main))
    }
    private def inlineLog(what: String, main: Symbol, comment: => String) {
      inlineLog(what, ownedName(main), comment)
    }

    val recentTFAs = mutable.Map.empty[Symbol, Tuple2[Boolean, analysis.MethodTFA]]

    private def getRecentTFA(incm: IMethod, forceable: Boolean): (Boolean, analysis.MethodTFA) = {

        def containsRETURN(blocks: List[BasicBlock]) = blocks exists { bb => bb.lastInstruction.isInstanceOf[RETURN] }

      val opt = recentTFAs.get(incm.symbol)
      if(opt.isDefined) {
        // FYI val cachedBBs = opt.get._2.in.keySet
        // FYI assert(incm.blocks.toSet == cachedBBs)
        // incm.code.touched plays no role here
        return opt.get
      }

      val hasRETURN = containsRETURN(incm.code.blocksList) || (incm.exh exists { eh => containsRETURN(eh.blocks) })
      var a: analysis.MethodTFA = null
      if(hasRETURN) { a = new analysis.MethodTFA(incm); a.run() }

      if(forceable) { recentTFAs.put(incm.symbol, (hasRETURN, a)) }

      (hasRETURN, a)
    }

    def clearCaches() {
      // methods
      NonPublicRefs.usesNonPublics.clear()
      recentTFAs.clear()
      tfa.knownUnsafe.clear()
      tfa.knownSafe.clear()
      tfa.knownNever.clear()
      // basic blocks
      tfa.preCandidates.clear()
      tfa.relevantBBs.clear()
      // callsites
      tfa.remainingCALLs.clear()
      tfa.isOnWatchlist.clear()
    }

    object imethodOrdering extends Ordering[IMethod] {
      def compare(a: IMethod, b: IMethod) = {
        val namesComparison = (a.toString() compare b.toString())
        if(namesComparison != 0) namesComparison
        else {
          a.symbol.id compare b.symbol.id
        }
      }
    }

    def analyzeClass(cls: IClass): Unit =
      if (settings.inline) {
        inlineLog("class", s"${cls.symbol.decodedName}", s"analyzing ${cls.methods.size} methods in $cls")

        this.currentIClazz = cls
        val ms = cls.methods sorted imethodOrdering
        ms foreach { im =>
          if (hasInline(im.symbol)) {
            inlineLog("skip", im.symbol, "no inlining into @inline methods")
          }
          else if(im.hasCode && !im.symbol.isBridge) {
            analyzeMethod(im)
          }
        }
      }

    val tfa   = new analysis.MTFAGrowable()
    tfa.stat  = global.settings.Ystatistics.value
    val staleOut      = new mutable.ListBuffer[BasicBlock]
    val splicedBlocks = mutable.Set.empty[BasicBlock]
    val staleIn       = mutable.Set.empty[BasicBlock]

    /**
     * A transformation local to the body of the IMethod received as argument.
     * An linining decision consists in replacing a callsite with the body of the callee.
     * Please notice that, because `analyzeMethod()` itself may modify a method body,
     * the particular callee bodies that end up being inlined depend on the particular order in which methods are visited
     * (no topological sorting over the call-graph is attempted).
     *
     * Making an inlining decision requires type-flow information for both caller and callee.
     * Regarding the caller, such information is needed only for basic blocks containing inlining candidates
     * (and their transitive predecessors). This observation leads to using a custom type-flow analysis (MTFAGrowable)
     * that can be re-inited, i.e. that reuses lattice elements (type-flow information computed in a previous iteration)
     * as starting point for faster convergence in a new iteration.
     *
     * The mechanics of inlining are iterative for a given invocation of `analyzeMethod(m)`,
     * and are affected by inlinings from previous iterations
     * (ie, "heuristic" rules are based on statistics tracked for that purpose):
     *
     *   (1) before the iterations proper start, so-called preinlining is performed.
     *       Those callsites whose (receiver, concreteMethod) are both known statically
     *       can be analyzed for inlining before computing a type-flow. Details in `preInline()`
     *
     *   (2) the first iteration computes type-flow information for basic blocks containing inlining candidates
     *       (and their transitive predecessors), so called `relevantBBs` basic blocks.
     *       The ensuing analysis of each candidate (performed by `analyzeInc()`)
     *       may result in a CFG isomorphic to that of the callee being inserted in place of the callsite
     *       (i.e. a CALL_METHOD instruction is replaced with a single-entry single-exit CFG,
     *        a substitution we call "successful inlining").
     *
     *   (3) following iterations have `relevantBBs` updated to focus on the inlined basic blocks and their successors only.
     *       Details in `MTFAGrowable.reinit()`
     * */
    def analyzeMethod(m: IMethod): Unit = {
      // m.normalize
      if (settings.debug)
        inlineLog("caller", ownedName(m.symbol), "in " + m.symbol.owner.fullName)

      val sizeBeforeInlining  = m.code.blockCount
      val instrBeforeInlining = m.code.instructionCount
      var retry = false
      var count = 0

      // fresh name counter
      val fresh = mutable.HashMap.empty[String, Int] withDefaultValue 0
      // how many times have we already inlined this method here?
      val inlinedMethodCount = mutable.HashMap.empty[Symbol, Int] withDefaultValue 0
      val caller = new IMethodInfo(m)
      def analyzeMessage = s"Analyzing ${caller.length} blocks of $m for inlining sites."

      def preInline(isFirstRound: Boolean): Int = {
        val inputBlocks = caller.m.linearizedBlocks()
        val callsites: Function1[BasicBlock, List[opcodes.CALL_METHOD]] = {
          if(isFirstRound) tfa.conclusives else tfa.knownBeforehand
        }
        inlineWithoutTFA(inputBlocks, callsites)
      }

      /*
       *  Inline straightforward callsites (those that can be inlined without a TFA).
       *
       *  To perform inlining, all we need to know is listed as formal params in `analyzeInc()`:
       *    - callsite and block containing it
       *    - actual (ie runtime) class of the receiver
       *    - actual (ie runtime) method being invoked
       *    - stack length just before the callsite (to check whether enough arguments have been pushed).
       *  The assert below lists the conditions under which "no TFA is needed"
       *  (the statically known receiver and method are both final, thus, at runtime they can't be any others than those).
       *
       */
      def inlineWithoutTFA(inputBlocks: Traversable[BasicBlock], callsites: Function1[BasicBlock, List[opcodes.CALL_METHOD]]): Int = {
        var inlineCount = 0
        import scala.util.control.Breaks._
        for(x <- inputBlocks; easyCake = callsites(x); if easyCake.nonEmpty) {
          breakable {
            for(ocm <- easyCake) {
              assert(ocm.method.isEffectivelyFinalOrNotOverridden && ocm.method.owner.isEffectivelyFinalOrNotOverridden)
              if(analyzeInc(ocm, x, ocm.method.owner, -1, ocm.method)) {
                inlineCount += 1
                break()
              }
            }
          }
        }

        inlineCount
      }

      /*
       *  Decides whether it's feasible and desirable to inline the body of the method given by `concreteMethod`
       *  at the program point given by `i` (a callsite). The boolean result indicates whether inlining was performed.
       *
       */
      def analyzeInc(i: CALL_METHOD, bb: BasicBlock, receiver: Symbol, stackLength: Int, concreteMethod: Symbol): Boolean = {
        assert(bb.toList contains i, "Candidate callsite does not belong to BasicBlock.")
        val shouldWarn = hasInline(i.method)

        def warnNoInline(reason: String): Boolean = {
          def msg = "Could not inline required method %s because %s.".format(i.method.unexpandedName.decode, reason)
          if (settings.debug)
            inlineLog("fail", i.method.fullName, reason)
          if (shouldWarn)
            warn(i.pos, msg)

          false
        }

        var isAvailable = icodes available concreteMethod.enclClass

        if (!isAvailable && shouldLoadImplFor(concreteMethod, receiver)) {
          // Until r22824 this line was:
          //   icodes.icode(concreteMethod.enclClass, true)
          //
          // Changing it to
          //   icodes.load(concreteMethod.enclClass)
          // was the proximate cause for SI-3882:
          //   error: Illegal index: 0 overlaps List((variable par1,LONG))
          //   error: Illegal index: 0 overlaps List((variable par1,LONG))
          isAvailable = icodes.load(concreteMethod.enclClass)
        }

        def isCandidate = (
             isClosureClass(receiver)
          || concreteMethod.isEffectivelyFinalOrNotOverridden
          || receiver.isEffectivelyFinalOrNotOverridden
        )

        def isApply     = concreteMethod.name == nme.apply

        def isCountable = !(
             isClosureClass(receiver)
          || isApply
          || isMonadicMethod(concreteMethod)
          || receiver.enclosingPackage == definitions.RuntimePackage
        )   // only count non-closures

        debuglog("Treating " + i
              + "\n\treceiver: " + receiver
              + "\n\ticodes.available: " + isAvailable
              + "\n\tconcreteMethod.isEffectivelyFinalOrNotOverridden: " + concreteMethod.isEffectivelyFinalOrNotOverridden)

        if (!isCandidate) warnNoInline("it can be overridden")
        else if (!isAvailable) warnNoInline("bytecode unavailable")
        else lookupIMethod(concreteMethod, receiver) filter (callee => callee.hasCode || warnNoInline("callee has no code")) exists { callee =>
          val inc   = new IMethodInfo(callee)
          val pair  = new CallerCalleeInfo(caller, inc, fresh, inlinedMethodCount)

          if (inc.hasHandlers && (stackLength == -1)) {
            // no inlining is done, yet don't warn about it, stackLength == -1 indicates we're trying to inlineWithoutTFA.
            // Shortly, a TFA will be computed and an error message reported if indeed inlining not possible.
            false
          }
          else {
            val isSafe = pair isStampedForInlining stackLength match {
              case DontInlineHere(msg)                       => warnNoInline(msg)
              case NeverSafeToInline                         => false
              case InlineableAtThisCaller                    => true
              case FeasibleInline(required, toPublicize)     =>
                for (f <- toPublicize) {
                  inlineLog("access", f, "making public")
                  f setFlag Flags.notPRIVATE
                  f setFlag Flags.notPROTECTED
                }
                // only add to `knownSafe` after all `toPublicize` fields actually made public.
                if (required == NonPublicRefs.Public)
                  tfa.knownSafe += inc.sym

                true
            }
            isSafe && {
               retry   = true
               if (isCountable) count += 1
               pair.doInline(bb, i)
               if (!pair.isInlineForced || inc.isMonadic) caller.inlinedCalls += 1
               inlinedMethodCount(inc.sym) += 1

               // Remove the caller from the cache (this inlining might have changed its calls-private relation).
               usesNonPublics -= m
               recentTFAs     -= m.symbol
               true
            }
          }
        }
      }

      /* Pre-inlining consists in invoking the usual inlining subroutine with (receiver class, concrete method) pairs as input
       * where both method and receiver are final, which implies that the receiver computed via TFA will always match `concreteMethod.owner`.
       *
       * As with any invocation of `analyzeInc()` the inlining outcome is based on heuristics which favor inlining an isMonadicMethod before other methods.
       * That's why preInline() is invoked twice: any inlinings downplayed by the heuristics during the first round get an opportunity to rank higher during the second.
       *
       * As a whole, both `preInline()` invocations amount to priming the inlining process,
       * so that the first TFA that is run afterwards is able to gain more information as compared to a cold-start.
       */
      /*val totalPreInlines = */ { // Val name commented out to emphasize it is never used
        val firstRound = preInline(isFirstRound = true)
        if(firstRound == 0) 0 else (firstRound + preInline(isFirstRound = false))
      }
      staleOut.clear()
      splicedBlocks.clear()
      staleIn.clear()

      do {
        retry = false
        debuglog(analyzeMessage)

        /* it's important not to inline in unreachable basic blocks. linearizedBlocks() returns only reachable ones. */
        tfa.callerLin = caller.m.linearizedBlocks()
           /* TODO Do we really want to inline inside exception handlers?
           *  Seems counterproductive (the larger the method the less likely it will be JITed).
            * The alternative would be `linearizer.linearizeAt(caller.m, caller.m.startBlock)`.
            * And, we would cut down on TFA iterations, too.
            * See also comment on the same topic in TypeFlowAnalysis. */

        tfa.reinit(m, staleOut.toList, splicedBlocks, staleIn)
        tfa.run

        staleOut.clear()
        splicedBlocks.clear()
        staleIn.clear()

        import scala.util.control.Breaks._
        for(bb <- tfa.callerLin; if tfa.preCandidates(bb)) {
          val cms = bb.toList collect { case cm : CALL_METHOD => cm }
          breakable {
            for (cm <- cms; if tfa.remainingCALLs.isDefinedAt(cm)) {
              val analysis.CallsiteInfo(_, receiver, stackLength, concreteMethod) = tfa.remainingCALLs(cm)
              if (analyzeInc(cm, bb, receiver, stackLength, concreteMethod)) {
                break()
              }
            }
          }
        }

        /* As part of inlining, some instructions are moved to a new block.
         *     In detail: the instructions moved to a new block originally appeared after a (by now inlined) callsite.
         *     Their new home is an `afterBlock` created by `doInline()` to that effect.
         *     Each block in staleIn is one such `afterBlock`.
         *
         * Some of those instructions may be CALL_METHOD possibly tracked in `remainingCALLs`
         * (with an entry still noting the old containing block). However, that causes no problem:
         *
         *   (1) such callsites won't be analyzed for inlining by `analyzeInc()` (*in this iteration*)
         *       because of the `break` that abandons the original basic block where it was contained.
         *
         *   (2) Additionally, its new containing block won't be visited either (*in this iteration*)
         *       because the new blocks don't show up in the linearization computed before inlinings started:
         *       `for(bb <- tfa.callerLin; if tfa.preCandidates(bb)) {`
         *
         * For a next iteration, the new home of any instructions that have moved
         * will be tracked properly in `remainingCALLs` after `MTFAGrowable.reinit()` puts on radar their new homes.
         *
         */
        if(retry) {
          for(afterBlock <- staleIn) {
            val justCALLsAfter = afterBlock.toList collect { case c : opcodes.CALL_METHOD => c }
            for(ia <- justCALLsAfter) { tfa.remainingCALLs.remove(ia) }
          }
        }

        /*
        if(splicedBlocks.nonEmpty) { // TODO explore (saves time but leads to slightly different inlining decisions)
          // opportunistically perform straightforward inlinings before the next typeflow round
          val savedRetry = retry
          val savedStaleOut = staleOut.toSet; staleOut.clear()
          val savedStaleIn  = staleIn.toSet ; staleIn.clear()
          val howmany = inlineWithoutTFA(splicedBlocks, tfa.knownBeforehand)
          splicedBlocks ++= staleIn
          staleOut.clear(); staleOut ++= savedStaleOut;
          staleIn.clear();  staleIn  ++= savedStaleIn;
          retry = savedRetry
        }
        */

        if (tfa.stat)
          log(m.symbol.fullName + " iterations: " + tfa.iterations + " (size: " + caller.length + ")")
      }
      while (retry && count < MAX_INLINE_RETRY)

      for(inlFail <- tfa.warnIfInlineFails) {
        warn(inlFail.pos, "At the end of the day, could not inline @inline-marked method " + inlFail.method.unexpandedName.decode)
      }

      m.normalize()
      if (sizeBeforeInlining > 0) {
        val instrAfterInlining = m.code.instructionCount
        val inlinings = caller.inlinedCalls
        if (inlinings > 0) {
          val s1      = s"instructions $instrBeforeInlining -> $instrAfterInlining"
          val s2      = if (sizeBeforeInlining == m.code.blockCount) "" else s", blocks $sizeBeforeInlining -> ${m.code.blockCount}"
          val callees = inlinedMethodCount.toList map { case (k, v) => k.fullNameString + ( if (v == 1) "" else "/" + v ) }

          inlineLog("inlined", m.symbol.fullName, callees.sorted.mkString(inlinings + " inlined: ", ", ", ""))
          inlineLog("<<tldr>>", m.symbol.fullName, s"${m.symbol.nameString}: $s1$s2")
        }
      }
    }

    private def isHigherOrderMethod(sym: Symbol) = (
         sym.isMethod
      && enteringExplicitOuter(sym.info.paramTypes exists isFunctionType) // was "at erasurePhase.prev"
    )

    /** Should method 'sym' being called in 'receiver' be loaded from disk? */
    def shouldLoadImplFor(sym: Symbol, receiver: Symbol): Boolean = {
      def alwaysLoad    = (receiver.enclosingPackage == RuntimePackage) || (receiver == PredefModule.moduleClass)
      def loadCondition = sym.isEffectivelyFinalOrNotOverridden && isMonadicMethod(sym) && isHigherOrderMethod(sym)

      val res = hasInline(sym) || alwaysLoad || loadCondition
      debuglog("shouldLoadImplFor: " + receiver + "." + sym + ": " + res)
      res
    }

    class IMethodInfo(val m: IMethod) {
      override def toString = m.toString

      val sym           = m.symbol
      def owner         = sym.owner
      def paramTypes    = sym.info.paramTypes
      def minimumStack  = paramTypes.length + 1

      def isBridge      = sym.isBridge
      val isInClosure   = isClosureClass(owner)
      val isHigherOrder = isHigherOrderMethod(sym)
      def isMonadic     = isMonadicMethod(sym)

      def handlers      = m.exh
      def blocks        = m.blocks
      def locals        = m.locals
      def length        = blocks.length
      def openBlocks    = blocks filterNot (_.closed)
      def instructions  = m.code.instructions

      def isSmall         = (length <= SMALL_METHOD_SIZE) && blocks(0).length < 10
      def isLarge         = length > MAX_INLINE_SIZE
      def isRecursive     = m.recursive
      def hasHandlers     = handlers.nonEmpty || m.bytecodeHasEHs

      def isSynchronized         = sym.hasFlag(Flags.SYNCHRONIZED)
      def hasNonFinalizerHandler = handlers exists {
        case _: Finalizer => true
        case _            => false
      }

      // the number of inlined calls in 'm', used by 'isScoreOK'
      var inlinedCalls = 0

      def addLocals(ls: List[Local])  = m.locals ++= ls
      def addLocal(l: Local)          = addLocals(List(l))
      def addHandlers(exhs: List[ExceptionHandler]) = m.exh = exhs ::: m.exh

      /**
       * This method inspects the callee's instructions, finding out the most restrictive accessibility implied by them.
       *
       * Rather than giving up upon encountering an access to a private field `p`, it provisorily admits `p` as "can-be-made-public", provided:
       *   - `p` is being compiled as part of this compilation run, and
       *   - `p` is synthetic or param-accessor.
       *
       * This method is side-effect free, in particular it lets the invoker decide
       * whether the accessibility of the `toBecomePublic` fields should be changed or not.
       */
      def accessRequirements: AccessReq = {

        var toBecomePublic: List[Symbol] = Nil

        def check(sym: Symbol, cond: Boolean) =
          if (cond) Private
          else if (sym.isProtected) Protected
          else Public

        def canMakePublic(f: Symbol): Boolean =
          (m.sourceFile ne NoSourceFile) &&
          (f.isSynthetic || f.isParamAccessor) &&
          { toBecomePublic = f :: toBecomePublic; true }

        /* A safety check to consider as private, for the purposes of inlining, a public field that:
         *   (1) is defined in an external library, and
         *   (2) can be presumed synthetic (due to a dollar sign in its name).
         * Such field was made public by `doMakePublic()` and we don't want to rely on that,
         * because under other compilation conditions (ie no -optimize) that won't be the case anymore.
         *
         * This allows aggressive intra-library inlining (making public if needed)
         * that does not break inter-library scenarios (see comment for `Inliners`).
         *
         * TODO handle more robustly the case of a trait var changed at the source-level from public to private[this]
         *      (eg by having ICodeReader use unpickler, see SI-5442).

         DISABLED

        def potentiallyPublicized(f: Symbol): Boolean = {
          (m.sourceFile eq NoSourceFile) && f.name.containsChar('$')
        }
        */


        def isPrivateForInlining(sym: Symbol): Boolean = {
          if (sym.isJavaDefined) {
            def check(sym: Symbol) = !(sym.isPublic || sym.isProtected)
            check(sym) || check(sym.owner) // SI-7582 Must check the enclosing class *and* the symbol for Java.
          }
          else sym.isPrivate // Scala never emits package-private bytecode
        }

        def checkField(f: Symbol)   = check(f, isPrivateForInlining(f) && !canMakePublic(f))
        def checkSuper(n: Symbol)   = check(n, isPrivateForInlining(n) || !n.isClassConstructor)
        def checkMethod(n: Symbol)  = check(n, isPrivateForInlining(n))

        def getAccess(i: Instruction) = i match {
          case CALL_METHOD(n, SuperCall(_)) => checkSuper(n)
          case CALL_METHOD(n, _)            => checkMethod(n)
          case LOAD_FIELD(f, _)             => checkField(f)
          case STORE_FIELD(f, _)            => checkField(f)
          case _                            => Public
        }

        var seen = Public
        val iter = instructions.iterator
        while((seen ne Private) && iter.hasNext) {
          val i = iter.next()
          getAccess(i) match {
            case Private    =>
              inlineLog("access", s"instruction $i requires private access", "pos=" + i.pos)
              toBecomePublic = Nil
              seen = Private
            case Protected  => seen = Protected
            case _          => ()
          }
        }

        AccessReq(seen, toBecomePublic)
      }

    }

    /**
     * Classifies a pair (caller, callee) into one of four categories:
     *
     *   (a) inlining should be performed, classified in turn into:
     *       (a.1) `InlineableAtThisCaller`: unconditionally at this caller
     *       (a.2) `FeasibleInline`: it only remains for certain access requirements to be met (see `IMethodInfo.accessRequirements()`)
     *
     *   (b) inlining shouldn't be performed, classified in turn into:
     *       (b.1) `DontInlineHere`: indicates that this particular occurrence of the callee at the caller shouldn't be inlined.
     *                - Nothing is said about the outcome for other callers, or for other occurrences of the callee for the same caller.
     *                - In particular inlining might be possible, but heuristics gave a low score for it.
     *       (b.2) `NeverSafeToInline`: the callee can't be inlined anywhere, irrespective of caller.
     *
     * The classification above is computed by `isStampedForInlining()` based on which `analyzeInc()` goes on to:
     *   - either log the reason for failure --- case (b) ---,
     *   - or perform inlining --- case (a) ---.
     */
    sealed abstract class InlineSafetyInfo
    case object NeverSafeToInline           extends InlineSafetyInfo
    case object InlineableAtThisCaller      extends InlineSafetyInfo
    case class  DontInlineHere(msg: String) extends InlineSafetyInfo
    case class  FeasibleInline(accessNeeded: NonPublicRefs.Value, toBecomePublic: List[Symbol]) extends InlineSafetyInfo

    case class AccessReq(
      accessNeeded:   NonPublicRefs.Value,
      toBecomePublic: List[Symbol]
    )

    final class CallerCalleeInfo(val caller: IMethodInfo, val inc: IMethodInfo, fresh: mutable.Map[String, Int], inlinedMethodCount: scala.collection.Map[Symbol, Int]) {

      assert(!caller.isBridge && inc.m.hasCode,
             "A guard in Inliner.analyzeClass() should have prevented from getting here.")

      def isLargeSum  = caller.length + inc.length - 1 > SMALL_METHOD_SIZE

      private def freshName(s: String): TermName = {
        fresh(s) += 1
        newTermName(s + fresh(s))
      }

      private def isKnownToInlineSafely: Boolean = { tfa.knownSafe(inc.sym) }

      val isInlineForced    = hasInline(inc.sym)
      val isInlineForbidden = hasNoInline(inc.sym)
      assert(!(isInlineForced && isInlineForbidden), "method ("+inc.m+") marked both @inline and @noinline.")

      /** Inline 'inc' into 'caller' at the given block and instruction.
       *  The instruction must be a CALL_METHOD.
       */
      def doInline(block: BasicBlock, instr: CALL_METHOD) {

        staleOut += block

        tfa.remainingCALLs.remove(instr) // this bookkpeeping is done here and not in MTFAGrowable.reinit due to (1st) convenience and (2nd) necessity.
        tfa.isOnWatchlist.remove(instr)  // ditto
        tfa.warnIfInlineFails.remove(instr)

        val targetPos = instr.pos

        def blockEmit(i: Instruction) = block.emit(i, targetPos)
        def newLocal(baseName: String, kind: TypeKind) =
          new Local(caller.sym.newVariable(freshName(baseName), targetPos) setInfo kind.toType, kind, false)

        val (hasRETURN, a) = getRecentTFA(inc.m, isInlineForced)

        /* The exception handlers that are active at the current block. */
        val activeHandlers = caller.handlers filter (_ covered block)

        /* Map 'original' blocks to the ones inlined in the caller. */
        val inlinedBlock = mutable.Map[BasicBlock, BasicBlock]()

        val varsInScope = mutable.HashSet[Local]() ++= block.varsInScope

        /* Side effects varsInScope when it sees SCOPE_ENTERs. */
        def instrBeforeFilter(i: Instruction): Boolean = {
          i match { case SCOPE_ENTER(l) => varsInScope += l ; case _ => () }
          i ne instr
        }
        val instrBefore = block.toList takeWhile instrBeforeFilter
        val instrAfter  = block.toList drop (instrBefore.length + 1)

        assert(!instrAfter.isEmpty, "CALL_METHOD cannot be the last instruction in block!")

        // store the '$this' into the special local
        val inlinedThis = newLocal("$inlThis", REFERENCE(ObjectClass))

        /* buffer for the returned value */
        val retVal = inc.m.returnType match {
          case UNIT  => null
          case x     => newLocal("$retVal", x)
        }

        val inlinedLocals = mutable.HashMap.empty[Local, Local]

        /* Add a new block in the current context. */
        def newBlock() = {
          val b = caller.m.code.newBlock()
          activeHandlers foreach (_ addCoveredBlock b)
          if (retVal ne null) b.varsInScope += retVal
          b.varsInScope += inlinedThis
          b.varsInScope ++= varsInScope
          b
        }

        def translateExh(e: ExceptionHandler) = {
          val handler: ExceptionHandler = e.dup
          handler.covered = handler.covered map inlinedBlock
          handler setStartBlock inlinedBlock(e.startBlock)
          handler
        }

        /* alfa-rename `l` in caller's context. */
        def dupLocal(l: Local): Local = {
          val sym = caller.sym.newVariable(freshName(l.sym.name.toString), l.sym.pos)
          // sym.setInfo(l.sym.tpe)
          val dupped = new Local(sym, l.kind, false)
          inlinedLocals(l) = dupped
          dupped
        }

        val afterBlock = newBlock()

        /* Map from nw.init instructions to their matching NEW call */
        val pending: mutable.Map[Instruction, NEW] = new mutable.HashMap

        /* Map an instruction from the callee to one suitable for the caller. */
        def map(i: Instruction): Instruction = {
          def assertLocal(l: Local) = {
            assert(caller.locals contains l, "Could not find local '" + l + "' in locals, nor in inlinedLocals: " + inlinedLocals)
            i
          }
          def isInlined(l: Local) = inlinedLocals isDefinedAt l

          val newInstr = i match {
            case THIS(clasz)                    => LOAD_LOCAL(inlinedThis)
            case STORE_THIS(_)                  => STORE_LOCAL(inlinedThis)
            case JUMP(whereto)                  => JUMP(inlinedBlock(whereto))
            case CJUMP(succ, fail, cond, kind)  => CJUMP(inlinedBlock(succ), inlinedBlock(fail), cond, kind)
            case CZJUMP(succ, fail, cond, kind) => CZJUMP(inlinedBlock(succ), inlinedBlock(fail), cond, kind)
            case SWITCH(tags, labels)           => SWITCH(tags, labels map inlinedBlock)
            case RETURN(_)                      => JUMP(afterBlock)
            case LOAD_LOCAL(l) if isInlined(l)  => LOAD_LOCAL(inlinedLocals(l))
            case STORE_LOCAL(l) if isInlined(l) => STORE_LOCAL(inlinedLocals(l))
            case LOAD_LOCAL(l)                  => assertLocal(l)
            case STORE_LOCAL(l)                 => assertLocal(l)
            case SCOPE_ENTER(l) if isInlined(l) => SCOPE_ENTER(inlinedLocals(l))
            case SCOPE_EXIT(l) if isInlined(l)  => SCOPE_EXIT(inlinedLocals(l))

            case nw @ NEW(sym) =>
              val r = NEW(sym)
              pending(nw.init) = r
              r

            case CALL_METHOD(meth, Static(true)) if meth.isClassConstructor =>
              CALL_METHOD(meth, Static(onInstance = true))

            case _ => i.clone()
          }
          // check any pending NEW's
          pending remove i foreach (_.init = newInstr.asInstanceOf[CALL_METHOD])
          newInstr
        }

        caller addLocals (inc.locals map dupLocal)
        caller addLocal inlinedThis

        if (retVal ne null)
          caller addLocal retVal

        inc.m foreachBlock { b =>
          inlinedBlock += (b -> newBlock())
          inlinedBlock(b).varsInScope ++= (b.varsInScope map inlinedLocals)
        }

        // re-emit the instructions before the call
        block.open()
        block.clear()
        block emit instrBefore

        // store the arguments into special locals
        inc.m.params.reverse foreach (p => blockEmit(STORE_LOCAL(inlinedLocals(p))))
        blockEmit(STORE_LOCAL(inlinedThis))

        // jump to the start block of the callee
        blockEmit(JUMP(inlinedBlock(inc.m.startBlock)))
        block.close()

        // duplicate the other blocks in the callee
        val calleeLin = inc.m.linearizedBlocks()
        calleeLin foreach { bb =>
          var info = if(hasRETURN) (a in bb) else null
          def emitInlined(i: Instruction) = inlinedBlock(bb).emit(i, targetPos)
          def emitDrops(toDrop: Int)      = info.stack.types drop toDrop foreach (t => emitInlined(DROP(t)))

          for (i <- bb) {
            i match {
              case RETURN(UNIT) => emitDrops(0)
              case RETURN(kind) =>
                if (info.stack.length > 1) {
                  emitInlined(STORE_LOCAL(retVal))
                  emitDrops(1)
                  emitInlined(LOAD_LOCAL(retVal))
                }
              case _            => ()
            }
            emitInlined(map(i))
            info = if(hasRETURN) a.interpret(info, i) else null
          }
          inlinedBlock(bb).close()
        }

        afterBlock emit instrAfter
        afterBlock.close()

        staleIn        += afterBlock
        splicedBlocks ++= (calleeLin map inlinedBlock)

        // add exception handlers of the callee
        caller addHandlers (inc.handlers map translateExh)
        assert(pending.isEmpty, "Pending NEW elements: " + pending)
        if (settings.debug) icodes.checkValid(caller.m)
      }

      def isStampedForInlining(stackLength: Int): InlineSafetyInfo = {

        if(tfa.blackballed(inc.sym)) { return NeverSafeToInline }

        if(!isKnownToInlineSafely) {

          if(inc.openBlocks.nonEmpty) {
            val msg = ("Encountered " + inc.openBlocks.size + " open block(s) in isSafeToInline: this indicates a bug in the optimizer!\n" +
                       "  caller = " + caller.m + ", callee = " + inc.m)
            warn(inc.sym.pos, msg)
            tfa.knownNever += inc.sym
            return DontInlineHere("Open blocks in " + inc.m)
          }

          val reasonWhyNever: String = {
            var rs: List[String] = Nil
            if(inc.isRecursive)    { rs ::= "is recursive"           }
            if(isInlineForbidden)  { rs ::= "is annotated @noinline" }
            if(inc.isSynchronized) { rs ::= "is synchronized method" }
            if(inc.m.bytecodeHasEHs) { rs ::= "bytecode contains exception handlers / finally clause" } // SI-6188
            if(inc.m.bytecodeHasInvokeDynamic) { rs ::= "bytecode contains invoke dynamic" }
            if(rs.isEmpty) null else rs.mkString("", ", and ", "")
          }

          if(reasonWhyNever != null) {
            tfa.knownNever += inc.sym
            inlineLog("never", inc.sym, reasonWhyNever)
            // next time around NeverSafeToInline is returned, thus skipping (duplicate) msg, this is intended.
            return DontInlineHere(inc.m + " " + reasonWhyNever)
          }

          if(sameSymbols) { // TODO but this also amounts to recursive, ie should lead to adding to tfa.knownNever, right?
            tfa.knownUnsafe += inc.sym
            return DontInlineHere("sameSymbols (ie caller == callee)")
          }

        }

        /*
         * From here on, two main categories of checks remain, (a) and (b) below:
         *   (a.1) either the scoring heuristics give green light; or
         *   (a.2) forced as candidate due to @inline.
         * After that, safety proper is checked:
         *   (b.1) the callee does not contain calls to private methods when called from another class
         *   (b.2) the callee is not going to be inlined into a position with non-empty stack,
         *         while having a top-level finalizer (see liftedTry problem)
         * As a result of (b), some synthetic private members can be chosen to become public.
         */

        val score    = inlinerScore
        val scoreStr = if (score > 0) "+" + score else "" + score
        val what     = if (score > 0) "ok to" else "don't"
        inlineLog(scoreStr, inc.m.symbol, s"$what inline into ${ownedName(caller.m.symbol)}")

        if (!isInlineForced && score <= 0) {
          // During inlining retry, a previous caller-callee pair that scored low may pass.
          // Thus, adding the callee to tfa.knownUnsafe isn't warranted.
          return DontInlineHere(s"inliner heuristic")
        }

        if(inc.hasHandlers && (stackLength > inc.minimumStack)) {
          return DontInlineHere("callee contains exception handlers / finally clause, and is invoked with non-empty operand stack") // SI-6157
        }

        if(isKnownToInlineSafely) { return InlineableAtThisCaller }

        if(stackLength > inc.minimumStack && inc.hasNonFinalizerHandler) {
          val msg = "method " + inc.sym + " is used on a non-empty stack with finalizer."
          debuglog(msg)
          // FYI: not reason enough to add inc.sym to tfa.knownUnsafe (because at other callsite in this caller, inlining might be ok)
          return DontInlineHere(msg)
        }

        val accReq = inc.accessRequirements
        if(!canAccess(accReq.accessNeeded)) {
          tfa.knownUnsafe += inc.sym
          val msg = "access level required by callee not matched by caller"
          inlineLog("fail", inc.sym, msg)
          return DontInlineHere(msg)
        }

        FeasibleInline(accReq.accessNeeded, accReq.toBecomePublic)

      }

      def canAccess(level: NonPublicRefs.Value) = level match {
        case Private    => caller.owner == inc.owner
        case Protected  => caller.owner.tpe <:< inc.owner.tpe
        case Public     => true
      }
      private def sameSymbols = caller.sym == inc.sym

      /** Gives green light for inlining (which may still be vetoed later). Heuristics:
       *   - it's bad to make the caller larger (> SMALL_METHOD_SIZE) if it was small
       *   - it's bad to inline large methods
       *   - it's good to inline higher order functions
       *   - it's good to inline closures functions.
       *   - it's bad (useless) to inline inside bridge methods
       */
      def inlinerScore: Int = {
        var score = 0

        // better not inline inside closures, but hope that the closure itself is repeatedly inlined
        if (caller.isInClosure)           score -= 2
        else if (caller.inlinedCalls < 1) score -= 1 // only monadic methods can trigger the first inline

        if (inc.isSmall) score += 1
        // if (inc.hasClosureParam) score += 2
        if (inc.isLarge) score -= 1
        if (caller.isSmall && isLargeSum) {
          score -= 1
          debuglog(s"inliner score decreased to $score because small caller $caller would become large")
        }

        if (inc.isMonadic)          score += 3
        else if (inc.isHigherOrder) score += 1

        if (inc.isInClosure)                 score += 2
        if (inlinedMethodCount(inc.sym) > 2) score -= 2
        score
      }
    }

    def lookupIMethod(meth: Symbol, receiver: Symbol): Option[IMethod] = {
      def tryParent(sym: Symbol) = icodes icode sym flatMap (_ lookupMethod meth)

      (receiver.info.baseClasses.iterator map tryParent find (_.isDefined)).flatten
    }
  } /* class Inliner */
} /* class Inliners */

Other Scala source code examples

Here is a short list of links related to this Scala Inliners.scala source code file:

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2024 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.