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

org.scalajs.linker.frontend.optimizer.IncOptimizer.scala Maven / Gradle / Ivy

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

package org.scalajs.linker.frontend.optimizer

import scala.annotation.{switch, tailrec}

import scala.collection.mutable

import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean

import org.scalajs.ir._
import org.scalajs.ir.Names._
import org.scalajs.ir.Trees._
import org.scalajs.ir.Types._
import org.scalajs.ir.Position.NoPosition

import org.scalajs.logging._

import org.scalajs.linker._
import org.scalajs.linker.backend.emitter.LongImpl
import org.scalajs.linker.frontend.LinkingUnit
import org.scalajs.linker.interface.{CheckedBehavior, ModuleKind}
import org.scalajs.linker.standard._
import org.scalajs.linker.CollectionsCompat._

import OptimizerCore.InlineableFieldBodies.FieldBody

/** Incremental optimizer.
 *
 *  An incremental optimizer optimizes a [[LinkingUnit]]
 *  in an incremental way.
 *
 *  It maintains state between runs to do a minimal amount of work on every
 *  run, based on detecting what parts of the program must be re-optimized,
 *  and keeping optimized results from previous runs for the rest.
 *
 *  A general note about use of ConcurrentHashMap[T, Unit] as concurrent sets:
 *  It would seem better to use ConcurrentHashMap.newKeySet() which is
 *  specifically designed for this purpose. However, the views alone use up 4 MB
 *  of shallow size on the test suite at the time of writing. Therefore, we give
 *  up on the convenience API and use the underlying ConcurrentHashMap directly.
 *
 *  A similar argument applies to usages of keySet(): It appears that the
 *  implementation holds on to the key set once it is created, resulting in
 *  unnecessary memory usage.
 *
 *  @param semantics Required Scala.js Semantics
 *  @param esLevel ECMAScript level
 */
final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: AbsCollOps) {

  import IncOptimizer._

  val symbolRequirements: SymbolRequirement = {
    import config.coreSpec._

    val factory = SymbolRequirement.factory("optimizer")
    import factory._

    def cond(p: Boolean)(v: => SymbolRequirement): SymbolRequirement =
      if (p) v else none()

    multiple(
      cond(!targetIsWebAssembly && !esFeatures.allowBigIntsForLongs) {
        // Required by the intrinsics manipulating Longs
        callMethods(LongImpl.RuntimeLongClass, LongImpl.AllIntrinsicMethods.toList)
      },
      cond(targetIsWebAssembly) {
        // Required by the intrinsic CharacterCodePointToString
        instantiateClass(IllegalArgumentExceptionClass, NoArgConstructorName)
      }
    )
  }

  /** Are we in batch mode? I.e., are we running from scratch?
   *  Various parts of the algorithm can be skipped entirely when running in
   *  batch mode.
   */
  private var batchMode: Boolean = false

  private var objectClass: Class = _
  private val classes = new ConcurrentHashMap[ClassName, Class]
  private val interfaces = new ConcurrentHashMap[ClassName, InterfaceType]
  private val topLevelExports = new JSTopLevelMethodContainer

  private var methodsToProcess = collOps.emptyAddable[Processable]

  @inline
  private def getInterface(className: ClassName): InterfaceType =
    interfaces.get(className)

  @inline
  private def classOrElse[T >: Class](className: ClassName, default: => T): T = {
    val clazz = classes.get(className)
    if (clazz != null) clazz
    else default
  }

  /** Update the incremental analyzer with a new run. */
  def update(unit: LinkingUnit, logger: Logger): List[(ClassDef, Version)] = {
    batchMode = objectClass == null
    logger.debug(s"Optimizer: Batch mode: $batchMode")

    logger.time("Optimizer: Incremental part") {
      /* UPDATE PASS */
      updateAndTagEverything(unit)
    }

    logger.time("Optimizer: Elidable constructors") {
      /** ELIDABLE CTORS PASS */
      updateElidableConstructors()
    }

    logger.time("Optimizer: Optimizer part") {
      /* PROCESS PASS */
      processAllTaggedMethods(logger)
    }

    val groupedTopLevelExports = unit.topLevelExports.groupBy(_.owningClass)

    for {
      linkedClass <- unit.classDefs
    } yield {
      val topLevelExports = groupedTopLevelExports.getOrElse(linkedClass.className, Nil)
      optimizedClass(linkedClass, topLevelExports)
    }
  }

  private def optimizedClass(linkedClass: LinkedClass,
      tles: List[LinkedTopLevelExport]): (ClassDef, Version) = {
    val className = linkedClass.className
    val interface = getInterface(className)

    val publicContainer = classOrElse(className, {
      /* For interfaces, we need to look at default methods.
       * For other kinds of classes, the public namespace is necessarily
       * empty.
       */
      val container = interface.staticLike(MemberNamespace.Public)
      assert(
          linkedClass.kind == ClassKind.Interface || container.methods.isEmpty,
          linkedClass.className -> linkedClass.kind)
      container
    })

    val newMethods = for (m <- linkedClass.methods) yield {
      val namespace = m.flags.namespace
      val container =
        if (namespace == MemberNamespace.Public) publicContainer
        else interface.staticLike(namespace)
      container.methods(m.methodName).optimizedDef
    }

    val newTopLevelExports = tles.map { tle =>
      tle.tree match {
        case method: TopLevelMethodExportDef =>
          topLevelExports.optimizedMethod(method.moduleID, method.topLevelExportName)

        case tree =>
          tree
      }
    }

    val classDef = ClassDef(
      linkedClass.name,
      OriginalName.NoOriginalName,
      linkedClass.kind,
      linkedClass.jsClassCaptures,
      linkedClass.superClass,
      linkedClass.interfaces,
      linkedClass.jsSuperClass,
      linkedClass.jsNativeLoadSpec,
      linkedClass.fields,
      newMethods,
      interface.optimizedJSConstructorDef(),
      interface.optimizedExportedMembers(),
      linkedClass.jsNativeMembers,
      newTopLevelExports
    )(linkedClass.optimizerHints)(linkedClass.pos)

    (classDef, linkedClass.version)
  }

  /** Incremental part: update state and detect what needs to be re-optimized.
   *  UPDATE PASS ONLY. (This IS the update pass).
    */
  private def updateAndTagEverything(unit: LinkingUnit): Unit = {
    updateAndTagClasses(unit.classDefs)
    topLevelExports.updateWith(unit.topLevelExports)
  }

  private def updateAndTagClasses(linkedClasses: List[LinkedClass]): Unit = {
    val neededInterfaces = new ConcurrentHashMap[ClassName, LinkedClass]
    val neededClasses = new ConcurrentHashMap[ClassName, LinkedClass]
    for (linkedClass <- linkedClasses) {
      neededInterfaces.put(linkedClass.className, linkedClass)

      if (linkedClass.hasInstances &&
          (linkedClass.kind.isClass || linkedClass.kind == ClassKind.HijackedClass)) {
        neededClasses.put(linkedClass.className, linkedClass)
      }
    }

    /* Remove deleted interfaces, and update existing ones.
     * We don't even have to notify callers in case of additions or removals
     * because callers have got to be invalidated by themselves.
     * Only changed methods need to trigger notifications.
     *
     * Non-batch mode only.
     */
    assert(!batchMode || interfaces.isEmpty())
    if (!batchMode) {
      interfaces.forEach(collOps.parThreshold, { (className, interface) =>
        val linkedClass = neededInterfaces.remove(className)
        if (linkedClass == null) {
          interface.delete()
          interfaces.remove(className)
        } else {
          interface.updateWith(linkedClass)
        }
      })
    }

    /* Add new interfaces.
     * Easy, we don't have to notify anyone.
     */
    neededInterfaces.forEachValue(collOps.parThreshold, { linkedClass =>
      val interface = new InterfaceType(linkedClass)
      interfaces.put(interface.className, interface)
    })

    if (!batchMode) {
      /* Class removals:
       * * If a class is deleted or moved, delete its entire subtree (because
       *   all its descendants must also be deleted or moved).
       * * If an existing class was instantiated but is no more, notify callers
       *   of its methods.
       *
       * Non-batch mode only.
       */
      val objectClassStillExists =
        objectClass.walkClassesForDeletions(className => Option(neededClasses.get(className)))
      assert(objectClassStillExists, "Uh oh, java.lang.Object was deleted!")

      /* Class changes:
       * * Delete removed methods, update existing ones, add new ones
       * * Update the list of ancestors
       * * Class newly instantiated
       *
       * Non-batch mode only.
       */
      objectClass.walkForChanges(neededClasses.remove(_), Set.empty)
    }

    /* Class additions:
     * * Add new classes (including those that have moved from elsewhere).
     * In batch mode, we avoid doing notifications.
     */

    // Group children by (immediate) parent
    val newChildrenByParent = new ConcurrentHashMap[ClassName, collOps.Addable[LinkedClass]]

    neededClasses.forEachValue(collOps.parThreshold, { linkedClass =>
      linkedClass.superClass.fold[Unit] {
        assert(batchMode, "Trying to add java.lang.Object in incremental mode")
        objectClass = new Class(None, linkedClass)
        classes.put(linkedClass.className, objectClass)
      } { superClassName =>
        val addable = newChildrenByParent
          .computeIfAbsent(superClassName.name, _ => collOps.emptyAddable)

        collOps.add(addable, linkedClass)
      }
    })

    val getNewChildren = { (name: ClassName) =>
      val acc = newChildrenByParent.get(name)
      if (acc == null) collOps.emptyParIterable[LinkedClass]
      else collOps.finishAdd(acc)
    }

    // Walk the tree to add children
    if (batchMode) {
      objectClass.walkForAdditions(getNewChildren)
    } else {
      newChildrenByParent.forEachKey(1, { parentName =>
        val parent = classes.get(parentName)
        if (parent != null)
          parent.walkForAdditions(getNewChildren)
      })
    }

  }

  /** Elidable constructors: compute the fix point of hasElidableConstructors.
   *
   *  ELIDABLE CTORS PASS ONLY. (This IS the elidable ctors pass).
   */
  private def updateElidableConstructors(): Unit = {
    import ElidableConstructorsInfo._

    val toProcessStack = mutable.ArrayBuffer.empty[Class]

    /* Invariants for this algo:
     * - when a class is in the stack, its elidableConstructorsInfo was set
     *   to AcyclicElidable,
     * - when a class has `DependentOn` as its info, its
     *   `elidableConstructorsRemainingDependenciesCount` is the number of
     *   classes in its `dependencies` that have not yet been *processed* as
     *   AcyclicElidable.
     *
     * During this algorithm, the info can transition from DependentOn to
     *
     * - NotElidable, if its getter dependencies were not satisfied.
     * - AcyclicElidable.
     *
     * Other transitions are not possible.
     */

    def isGetter(classAndMethodName: (ClassName, MethodName)): Boolean = {
      val (className, methodName) = classAndMethodName
      classes.get(className).lookupMethod(methodName).exists { m =>
        m.originalDef.body match {
          case Some(Select(This(), _)) => true
          case _                          => false
        }
      }
    }

    /* Initialization:
     * - Prune classes with unsatisfied getter dependencies
     * - Build reverse dependencies
     * - Initialize `elidableConstructorsRemainingDependenciesCount` for `DependentOn` classes
     * - Initialize the stack with dependency-free classes
     */
    classes.forEachValue(Long.MaxValue, { cls =>
      cls.elidableConstructorsInfo match {
        case DependentOn(deps, getterDeps) =>
          if (!getterDeps.forall(isGetter(_))) {
            cls.elidableConstructorsInfo = NotElidable
          } else {
            if (deps.isEmpty) {
              cls.elidableConstructorsInfo = AcyclicElidable
              toProcessStack += cls
            } else {
              cls.elidableConstructorsRemainingDependenciesCount = deps.size
              deps.foreach(dep => classes.get(dep).elidableConstructorsDependents += cls)
            }
          }
        case AcyclicElidable =>
          toProcessStack += cls
        case NotElidable =>
          ()
      }
    })

    /* Propagate AcyclicElidable
     * When a class `cls` is on the stack, it is known to be AcyclicElidable.
     * Go to all its dependents and decrement their count of remaining
     * dependencies. If the count reaches 0, then all the dependencies of the
     * class are known to be AcyclicElidable, and so the new class is known to
     * be AcyclicElidable.
     */
    while (toProcessStack.nonEmpty) {
      val cls = toProcessStack.remove(toProcessStack.size - 1)

      for (dependent <- cls.elidableConstructorsDependents) {
        dependent.elidableConstructorsInfo match {
          case DependentOn(_, _) =>
            dependent.elidableConstructorsRemainingDependenciesCount -= 1
            if (dependent.elidableConstructorsRemainingDependenciesCount == 0) {
              dependent.elidableConstructorsInfo = AcyclicElidable
              toProcessStack += dependent
            }
          case NotElidable =>
            ()
          case AcyclicElidable =>
            throw new AssertionError(
              s"Unexpected dependent link from class ${cls.className.nameString} " +
              s"to ${dependent.className.nameString} which is AcyclicElidable"
            )
        }
      }
    }

    // Set the final value of hasElidableConstructors
    classes.forEachValue(Long.MaxValue, _.setHasElidableConstructors())
  }

  /** Optimizer part: process all methods that need reoptimizing.
   *  PROCESS PASS ONLY. (This IS the process pass).
   */
  private def processAllTaggedMethods(logger: Logger): Unit = {
    val methods = collOps.finishAdd(methodsToProcess)
    methodsToProcess = collOps.emptyAddable

    val count = collOps.count(methods)(!_.deleted)
    logger.debug(s"Optimizer: Optimizing $count methods.")
    collOps.foreach(methods)(_.process())
  }

  /** Base class for [[IncOptimizer.Class]] and
   *  [[IncOptimizer.StaticLikeNamespace]].
   */
  private abstract class MethodContainer(linkedClass: LinkedClass,
      val myInterface: InterfaceType, val namespace: MemberNamespace) {

    val className: ClassName = linkedClass.className

    def untrackedThisType: Type =
      if (namespace.isStatic) NoType
      else myInterface.untrackedInstanceThisType

    val methods = mutable.Map.empty[MethodName, MethodImpl]

    updateWith(linkedClass)

    /** UPDATE PASS ONLY. Global concurrency safe but not on same instance */
    def updateWith(linkedClass: LinkedClass):
        (Set[MethodName], Set[MethodName], Set[MethodName]) = {

      val addedMethods = Set.newBuilder[MethodName]
      val changedMethods = Set.newBuilder[MethodName]
      val deletedMethods = Set.newBuilder[MethodName]

      val applicableNamespaceOrdinal = this match {
        case _: StaticLikeNamespace
            if namespace == MemberNamespace.Public &&
                (linkedClass.kind.isClass || linkedClass.kind == ClassKind.HijackedClass) =>
          /* The public non-static namespace for a class is always empty,
           * because its would-be content must be handled by the `Class`
           * instead.
           */
          -1 // different from all `ordinal` values of legit namespaces
        case _ =>
          namespace.ordinal
      }
      val linkedMethodDefs = linkedClass.methods.withFilter {
        _.flags.namespace.ordinal == applicableNamespaceOrdinal
      }

      val newMethodNames = linkedMethodDefs.map(_.methodName).toSet
      val methodSetChanged = methods.keySet != newMethodNames
      if (methodSetChanged) {
        // Remove deleted methods
        methods.filterInPlace { (methodName, method) =>
          if (newMethodNames.contains(methodName)) {
            true
          } else {
            deletedMethods += methodName
            method.delete()
            false
          }
        }
      }

      for (linkedMethodDef <- linkedMethodDefs) {
        val methodName = linkedMethodDef.methodName

        methods.get(methodName).fold {
          addedMethods += methodName
          val method = new MethodImpl(this, methodName)
          method.updateWith(linkedMethodDef)
          methods(methodName) = method
          method
        } { method =>
          if (method.updateWith(linkedMethodDef))
            changedMethods += methodName
          method
        }
      }

      (addedMethods.result(), changedMethods.result(), deletedMethods.result())
    }

    def lookupMethod(methodName: MethodName): Option[MethodImpl]

    override def toString(): String =
      namespace.prefixString + className.nameString
  }

  /** Class in the class hierarchy (not an interface).
   *  A class may be a module class.
   *  A class knows its superclass and the interfaces it implements. It also
   *  maintains a list of its direct subclasses, so that the instances of
   *  [[Class]] form a tree of the class hierarchy.
   */
  private final class Class(val superClass: Option[Class], linkedClass: LinkedClass)
      extends MethodContainer(linkedClass, getInterface(linkedClass.className), MemberNamespace.Public)
      with Unregisterable {

    if (className == ObjectClass) {
      assert(superClass.isEmpty)
      assert(objectClass == null)
    } else {
      assert(superClass.isDefined)
    }

    /** Parent chain from this to Object. */
    val parentChain: List[Class] =
      this :: superClass.fold[List[Class]](Nil)(_.parentChain)

    /** Reverse parent chain from Object to this. */
    val reverseParentChain: List[Class] =
      parentChain.reverse

    var interfaces: Set[InterfaceType] = linkedClass.ancestors.map(getInterface).toSet
    var subclasses: collOps.ParIterable[Class] = collOps.emptyParIterable
    var isInstantiated: Boolean = linkedClass.hasInstances

    // Temporary information used to eventually derive `hasElidableConstructors`
    var elidableConstructorsInfo: ElidableConstructorsInfo =
      computeElidableConstructorsInfo(linkedClass)
    val elidableConstructorsDependents: mutable.ArrayBuffer[Class] = mutable.ArrayBuffer.empty
    var elidableConstructorsRemainingDependenciesCount: Int = 0

    /** True if *all* constructors of this class are recursively elidable. */
    private var hasElidableConstructors: Boolean =
      elidableConstructorsInfo != ElidableConstructorsInfo.NotElidable // initial educated guess
    private val hasElidableConstructorsAskers = new ConcurrentHashMap[Processable, Unit]

    var fields: List[AnyFieldDef] = linkedClass.fields
    var fieldsRead: Set[FieldName] = linkedClass.fieldsRead
    var tryNewInlineable: Option[OptimizerCore.InlineableClassStructure] = None

    /** The "bodies" of fields that can "inlined", *provided that* the enclosing
     *  module class has elidable constructors.
     */
    private var inlineableFieldBodies: OptimizerCore.InlineableFieldBodies =
      computeInlineableFieldBodies(linkedClass)
    private val inlineableFieldBodiesAskers = new ConcurrentHashMap[Processable, Unit]

    setupAfterCreation(linkedClass)

    override def toString(): String =
      className.nameString

    /** Walk the class hierarchy tree for deletions.
     *  This includes "deleting" classes that were previously instantiated but
     *  are no more.
     *  UPDATE PASS ONLY. Not concurrency safe on same instance.
     */
    def walkClassesForDeletions(
        getLinkedClassIfNeeded: ClassName => Option[LinkedClass]): Boolean = {
      def sameSuperClass(linkedClass: LinkedClass): Boolean =
        superClass.map(_.className) == linkedClass.superClass.map(_.name)

      getLinkedClassIfNeeded(className) match {
        case Some(linkedClass) if sameSuperClass(linkedClass) =>
          // Class still exists. Recurse.
          subclasses = collOps.filter(subclasses)(
              _.walkClassesForDeletions(getLinkedClassIfNeeded))
          if (isInstantiated && !linkedClass.hasInstances)
            notInstantiatedAnymore()
          true
        case _ =>
          // Class does not exist or has been moved. Delete the entire subtree.
          deleteSubtree()
          false
      }
    }

    /** Delete this class and all its subclasses. UPDATE PASS ONLY. */
    def deleteSubtree(): Unit = {
      delete()
      collOps.foreach(subclasses)(_.deleteSubtree())
    }

    /** UPDATE PASS ONLY. */
    private def delete(): Unit = {
      if (isInstantiated)
        notInstantiatedAnymore()
      for (method <- methods.values)
        method.delete()
      classes.remove(className)
      /* Note: no need to tag methods that call *statically* one of the methods
       * of the deleted classes, since they've got to be invalidated by
       * themselves.
       */
    }

    /** UPDATE PASS ONLY. */
    def notInstantiatedAnymore(): Unit = {
      assert(isInstantiated)
      isInstantiated = false
      for (intf <- interfaces) {
        intf.removeInstantiatedSubclass(this)
        for (methodName <- allMethods().keys)
          intf.tagDynamicCallersOf(methodName)
      }
    }

    /** UPDATE PASS ONLY. */
    def walkForChanges(getLinkedClass: ClassName => LinkedClass,
        parentMethodAttributeChanges: Set[MethodName]): Unit = {

      val linkedClass = getLinkedClass(className)

      val (addedMethods, changedMethods, deletedMethods) =
        updateWith(linkedClass)

      fields = linkedClass.fields
      fieldsRead = linkedClass.fieldsRead

      val oldInterfaces = interfaces
      val newInterfaces = linkedClass.ancestors.map(getInterface).toSet
      interfaces = newInterfaces

      val methodAttributeChanges =
        (parentMethodAttributeChanges -- methods.keys ++
            addedMethods ++ changedMethods ++ deletedMethods)

      // Tag callers with dynamic calls
      val wasInstantiated = isInstantiated
      isInstantiated = linkedClass.hasInstances
      assert(!(wasInstantiated && !isInstantiated),
          "(wasInstantiated && !isInstantiated) should have been handled "+
          "during deletion phase")

      if (isInstantiated) {
        if (wasInstantiated) {
          val existingInterfaces = oldInterfaces.intersect(newInterfaces)
          for {
            intf <- existingInterfaces
            methodName <- methodAttributeChanges
          } {
            intf.tagDynamicCallersOf(methodName)
          }
          if (newInterfaces.size != oldInterfaces.size ||
              newInterfaces.size != existingInterfaces.size) {
            val allMethodNames = allMethods().keys
            for {
              intf <- oldInterfaces ++ newInterfaces -- existingInterfaces
              methodName <- allMethodNames
            } {
              intf.tagDynamicCallersOf(methodName)
            }
          }
        } else {
          val allMethodNames = allMethods().keys
          for (intf <- interfaces) {
            intf.addInstantiatedSubclass(this)
            for (methodName <- allMethodNames)
              intf.tagDynamicCallersOf(methodName)
          }
        }
      }

      // Tag callers with static calls
      for (methodName <- methodAttributeChanges)
        myInterface.tagStaticCallersOf(namespace, methodName)

      // Elidable constructors
      elidableConstructorsInfo = computeElidableConstructorsInfo(linkedClass)

      // Inlineable field bodies
      val newInlineableFieldBodies = computeInlineableFieldBodies(linkedClass)
      if (inlineableFieldBodies != newInlineableFieldBodies) {
        inlineableFieldBodies = newInlineableFieldBodies
        inlineableFieldBodiesAskers.forEachKey(Long.MaxValue, _.tag())
        inlineableFieldBodiesAskers.clear()
      }

      // Inlineable class
      if (updateTryNewInlineable(linkedClass)) {
        for (method <- methods.values; if method.methodName.isConstructor)
          myInterface.tagStaticCallersOf(namespace, method.methodName)
      }

      // Recurse in subclasses
      collOps.foreach(subclasses) { cls =>
        cls.walkForChanges(getLinkedClass, methodAttributeChanges)
      }
    }

    /** ELIDABLE CTORS PASS ONLY. */
    def setHasElidableConstructors(): Unit = {
      import ElidableConstructorsInfo._

      val newHasElidableConstructors = elidableConstructorsInfo == AcyclicElidable

      if (hasElidableConstructors != newHasElidableConstructors) {
        hasElidableConstructors = newHasElidableConstructors
        hasElidableConstructorsAskers.forEachKey(Long.MaxValue, _.tag())
        hasElidableConstructorsAskers.clear()
      }

      // Release memory that we won't use anymore
      if (!newHasElidableConstructors)
        elidableConstructorsInfo = NotElidable // get rid of DependentOn
      elidableConstructorsDependents.clear() // also resets state for next run
    }

    /** UPDATE PASS ONLY. */
    def walkForAdditions(
        getNewChildren: ClassName => collOps.ParIterable[LinkedClass]): Unit = {

      val subclassAcc = collOps.prepAdd(subclasses)

      collOps.foreach(getNewChildren(className)) { linkedClass =>
        val cls = new Class(Some(this), linkedClass)
        collOps.add(subclassAcc, cls)
        classes.put(linkedClass.className, cls)
        cls.walkForAdditions(getNewChildren)
      }

      subclasses = collOps.finishAdd(subclassAcc)
    }

    def askHasElidableConstructors(asker: Processable): Boolean = {
      hasElidableConstructorsAskers.put(asker, ())
      asker.registerTo(this)
      hasElidableConstructors
    }

    def askInlineableFieldBodies(asker: Processable): OptimizerCore.InlineableFieldBodies = {
      inlineableFieldBodiesAskers.put(asker, ())

      if (inlineableFieldBodies.isEmpty) {
        // Avoid asking for `hasInlineableConstructors`; we always get here for non-ModuleClass'es
        asker.registerTo(this)
        inlineableFieldBodies
      } else {
        // No need for asker.registerTo(this) in this branch; it is done anyway in askHasElidableConstructors
        if (askHasElidableConstructors(asker))
          inlineableFieldBodies
        else
          OptimizerCore.InlineableFieldBodies.Empty
      }
    }

    /** UPDATE PASS ONLY. */
    private def computeElidableConstructorsInfo(linkedClass: LinkedClass): ElidableConstructorsInfo = {
      import ElidableConstructorsInfo._

      if (isAdHocElidableConstructors(className)) {
        AcyclicElidable
      } else {
        // It's OK to look at the superClass like this because it will always be updated before myself
        var result = superClass.fold[ElidableConstructorsInfo](AcyclicElidable)(_.elidableConstructorsInfo)

        if (result == NotElidable) {
          // fast path
          result
        } else {
          val ctorIterator = myInterface.staticLike(MemberNamespace.Constructor).methods.valuesIterator
          while (result != NotElidable && ctorIterator.hasNext) {
            result = result.mergeWith(computeCtorElidableInfo(ctorIterator.next()))
          }
          result
        }
      }
    }

    /** UPDATE PASS ONLY. */
    private def computeInlineableFieldBodies(
        linkedClass: LinkedClass): OptimizerCore.InlineableFieldBodies = {
      import OptimizerCore.InlineableFieldBodies

      if (linkedClass.kind != ClassKind.ModuleClass) {
        InlineableFieldBodies.Empty
      } else {
        myInterface.staticLike(MemberNamespace.Constructor).methods.get(NoArgConstructorName) match {
          case None =>
            InlineableFieldBodies.Empty

          case Some(ctor) =>
            val initFieldBodies = for {
              fieldDef <- computeAllInstanceFieldDefs()
              if !fieldDef.flags.isMutable
            } yield {
              // the zero value is always a Literal because the ftpe is not a RecordType
              val zeroValue = zeroOf(fieldDef.ftpe)(NoPosition).asInstanceOf[Literal]
              fieldDef.name.name -> FieldBody.Literal(zeroValue)
            }

            if (initFieldBodies.isEmpty) {
              // fast path
              InlineableFieldBodies.Empty
            } else {
              val finalFieldBodies = interpretConstructor(ctor, initFieldBodies.toMap, Nil)
              new InlineableFieldBodies(finalFieldBodies)
            }
        }
      }
    }

    /** UPDATE PASS ONLY. */
    def updateTryNewInlineable(linkedClass: LinkedClass): Boolean = {
      val oldTryNewInlineable = tryNewInlineable

      tryNewInlineable = if (!linkedClass.optimizerHints.inline) {
        None
      } else {
        val allFields = computeAllInstanceFieldDefs()
        Some(new OptimizerCore.InlineableClassStructure(allFields))
      }

      tryNewInlineable != oldTryNewInlineable
    }

    /** UPDATE PASS ONLY, used by `computeInlineableFieldBodies` and `updateTryNewInlineable`. */
    private def computeAllInstanceFieldDefs(): List[FieldDef] = {
      for {
        parent <- reverseParentChain
        anyField <- parent.fields
        if !anyField.flags.namespace.isStatic
        // non-JS class may only contain FieldDefs (no JSFieldDef)
        field = anyField.asInstanceOf[FieldDef]
        if parent.fieldsRead.contains(field.name.name)
      } yield {
        field
      }
    }

    /** UPDATE PASS ONLY. */
    private[this] def setupAfterCreation(linkedClass: LinkedClass): Unit = {
      if (batchMode) {
        if (isInstantiated) {
          /* Only add the class to all its ancestor interfaces */
          for (intf <- interfaces)
            intf.addInstantiatedSubclass(this)
        }
      } else {
        val allMethodNames = allMethods().keys

        if (isInstantiated) {
          /* Add the class to all its ancestor interfaces + notify all callers
           * of any of the methods.
           * TODO: be more selective on methods that are notified: it is not
           * necessary to modify callers of methods defined in a parent class
           * that already existed in the previous run.
           */
          for (intf <- interfaces) {
            intf.addInstantiatedSubclass(this)
            for (methodName <- allMethodNames)
              intf.tagDynamicCallersOf(methodName)
          }
        }

        /* Tag static callers because the class could have been *moved*,
         * not just added.
         */
        for (methodName <- allMethodNames)
          myInterface.tagStaticCallersOf(namespace, methodName)
      }

      updateTryNewInlineable(linkedClass)
    }

    /** UPDATE PASS ONLY. */
    private def computeCtorElidableInfo(impl: MethodImpl): ElidableConstructorsInfo = {
      /* Dependencies on other classes to have acyclic elidable constructors
       * It is possible for the enclosing class name to be added to this set,
       * if the constructor depends on its own class. In that case, the
       * analysis will naturally treat it as a cycle and will conclude that the
       * class does not have elidable constructors.
       */
      val dependenciesBuilder = Set.newBuilder[ClassName]

      // Dependencies on certain methods to be getters
      val getterDependenciesBuilder = Set.newBuilder[(ClassName, MethodName)]

      def isTriviallySideEffectFree(tree: Tree): Boolean = tree match {
        case _:VarRef | _:Literal | _:This | _:Skip =>
          true

        case Closure(_, _, _, _, _, captureValues) =>
          captureValues.forall(isTriviallySideEffectFree(_))

        case UnaryOp(UnaryOp.CheckNotNull, expr) =>
          config.coreSpec.semantics.nullPointers == CheckedBehavior.Unchecked &&
          isTriviallySideEffectFree(expr)
        case GetClass(expr) => // Before 1.17, we used GetClass as CheckNotNull
          config.coreSpec.semantics.nullPointers == CheckedBehavior.Unchecked &&
          isTriviallySideEffectFree(expr)

        case New(className, _, args) =>
          dependenciesBuilder += className
          args.forall(isTriviallySideEffectFree(_))

        case LoadModule(className) =>
          dependenciesBuilder += className
          true

        case Select(LoadModule(className), _) =>
          /* If the given module can be loaded without cycles, it is guaranteed
           * to be non-null, and therefore the Select is side-effect-free.
           */
          dependenciesBuilder += className
          true

        case Select(This(), _) =>
          true

        case Apply(_, LoadModule(className), MethodIdent(methodName), Nil)
            if !methodName.isReflectiveProxy =>
          // For a getter-like call, we need the method to actually be a getter.
          dependenciesBuilder += className
          getterDependenciesBuilder += ((className, methodName))
          true

        case Apply(_, This(), MethodIdent(methodName), Nil)
            if !methodName.isReflectiveProxy =>
          getterDependenciesBuilder += ((className, methodName))
          true

        case _ =>
          false
      }

      def isElidableStat(tree: Tree): Boolean = tree match {
        case Block(stats)                   => stats.forall(isElidableStat)
        case Assign(Select(This(), _), rhs) => isTriviallySideEffectFree(rhs)

        // Mixin constructor -- test whether its body is entirely empty
        case ApplyStatically(flags, This(), className, methodName, Nil)
            if !flags.isPrivate && !classes.containsKey(className) =>
          // Since className is not in classes, it must be a default method call.
          val container =
            getInterface(className).staticLike(MemberNamespace.Public)
          container.methods.get(methodName.name) exists { methodDef =>
            methodDef.originalDef.body exists {
              case Skip() => true
              case _      => false
            }
          }

        /* Delegation to another constructor (super or in the same class)
         *
         * - for super constructor calls, we have already checked before getting
         *   here that the super class has elidable constructors, so by
         *   construction they are elidable and we do not need to test them
         * - for other constructors in the same class, we will collectively
         *   treat them as all-elidable or non-elidable; therefore, we do not
         *   need to check them either at this point.
         *
         * We only need to check the arguments to the constructor, not their
         * bodies.
         */
        case ApplyStatically(flags, This(), _, _, args) if flags.isConstructor =>
          args.forall(isTriviallySideEffectFree)

        case StoreModule() => true
        case _             => isTriviallySideEffectFree(tree)
      }

      impl.originalDef.body.fold {
        throw new AssertionError("Constructor cannot be abstract")
      } { body =>
        if (isElidableStat(body)) {
          val dependencies = dependenciesBuilder.result()
          val getterDependencies = getterDependenciesBuilder.result()
          if (dependencies.isEmpty && getterDependencies.isEmpty)
            ElidableConstructorsInfo.AcyclicElidable
          else
            ElidableConstructorsInfo.DependentOn(dependencies, getterDependencies)
        } else {
          ElidableConstructorsInfo.NotElidable
        }
      }
    }

    /** UPDATE PASS ONLY. */
    private def interpretConstructor(impl: MethodImpl,
        fieldBodies: Map[FieldName, FieldBody],
        paramBodies: List[Option[FieldBody]]): Map[FieldName, FieldBody] = {

      /* This method performs a kind of abstract intepretation of the given
       * given constructor `impl`. It *assumes* that the enclosing class ends
       * up having elidable constructors. If that is not the case, the
       * computation must not crash but its result can be arbitrary.
       */

      type FieldBodyMap = Map[FieldName, FieldBody]
      type ParamBodyMap = Map[LocalName, FieldBody]

      def interpretSelfGetter(methodName: MethodName,
          fieldBodies: FieldBodyMap): Option[FieldBody] = {
        methods.get(methodName).flatMap { impl =>
          impl.originalDef.body match {
            case Some(Select(This(), FieldIdent(fieldName))) =>
              fieldBodies.get(fieldName)

            case _ =>
              /* If we get here, it means the class won't have elidable
               * constructors, and therefore we can return arbitrary values
               * from the whole method, so we don't care.
               */
              None
          }
        }
      }

      def interpretExpr(tree: Tree, fieldBodies: FieldBodyMap,
          paramBodies: ParamBodyMap): Option[FieldBody] = {
        tree match {
          case lit: Literal =>
            Some(FieldBody.Literal(lit))

          case VarRef(LocalIdent(valName)) =>
            paramBodies.get(valName)

          case LoadModule(moduleClassName) =>
            Some(FieldBody.LoadModule(moduleClassName, tree.pos))

          case Select(qual @ LoadModule(moduleClassName), FieldIdent(fieldName)) =>
            val moduleBody = FieldBody.LoadModule(moduleClassName, qual.pos)
            Some(FieldBody.ModuleSelect(moduleBody, fieldName, tree.tpe, tree.pos))

          case Select(This(), FieldIdent(fieldName)) =>
            fieldBodies.get(fieldName)

          case Apply(_, receiver @ LoadModule(moduleClassName), MethodIdent(methodName), Nil)
              if !methodName.isReflectiveProxy =>
            val moduleBody = FieldBody.LoadModule(moduleClassName, receiver.pos)
            Some(FieldBody.ModuleGetter(moduleBody, methodName, tree.tpe, tree.pos))

          case Apply(_, This(), MethodIdent(methodName), Nil)
              if !methodName.isReflectiveProxy =>
            interpretSelfGetter(methodName, fieldBodies)

          case _ =>
            None
        }
      }

      def interpretBody(body: Tree, fieldBodies: FieldBodyMap,
          paramBodies: ParamBodyMap): FieldBodyMap = {
        body match {
          case Block(stats) =>
            stats.foldLeft(fieldBodies) { (prev, stat) =>
              interpretBody(stat, prev, paramBodies)
            }

          case Skip() =>
            fieldBodies

          case Assign(Select(This(), FieldIdent(fieldName)), rhs) =>
            if (!fieldBodies.contains(fieldName)) {
              // It is a mutable field or it is dce'ed as never read, don't track it
              fieldBodies
            } else {
              interpretExpr(rhs, fieldBodies, paramBodies) match {
                case Some(newFieldBody) =>
                  fieldBodies.updated(fieldName, newFieldBody)
                case None =>
                  // Unknown value; don't track it anymore
                  fieldBodies - fieldName
              }
            }

          // Mixin constructor -- assume it is empty
          case ApplyStatically(flags, This(), className, methodName, Nil)
              if !flags.isPrivate && !classes.containsKey(className) =>
            fieldBodies

          // Delegation to another constructor
          case ApplyStatically(flags, This(), className, MethodIdent(methodName), args) if flags.isConstructor =>
            val argBodies = args.map(interpretExpr(_, fieldBodies, paramBodies))
            val ctorImpl = getInterface(className)
              .staticLike(MemberNamespace.Constructor)
              .methods(methodName)
            interpretConstructor(ctorImpl, fieldBodies, argBodies)

          case _ =>
            /* Other statements cannot affect the eventual fieldBodies
             * (assuming the class ends up having elidable constructors, as always).
             */
            fieldBodies
        }
      }

      impl.originalDef.body.fold {
        throw new AssertionError(s"Constructor $impl cannot be abstract")
      } { body =>
        val paramBodiesMap: ParamBodyMap =
          impl.originalDef.args.zip(paramBodies).collect {
            case (paramDef, Some(paramBody)) => paramDef.name.name -> paramBody
          }.toMap

        interpretBody(body, fieldBodies, paramBodiesMap)
      }
    }

    /** All the methods of this class, including inherited ones.
     *  It has () so we remember this is an expensive operation.
     *  UPDATE PASS ONLY.
     */
    def allMethods(): scala.collection.Map[MethodName, MethodImpl] = {
      val result = mutable.Map.empty[MethodName, MethodImpl]
      for (parent <- reverseParentChain)
        result ++= parent.methods
      result
    }

    /** BOTH PASSES. */
    @tailrec
    final def lookupMethod(methodName: MethodName): Option[MethodImpl] = {
      methods.get(methodName) match {
        case Some(impl) => Some(impl)
        case none =>
          superClass match {
            case Some(p) => p.lookupMethod(methodName)
            case none    => None
          }
      }
    }

    def unregisterDependee(dependee: Processable): Unit = {
      hasElidableConstructorsAskers.remove(dependee)
      inlineableFieldBodiesAskers.remove(dependee)
    }
  }

  /** Namespace for static members of a class. */
  private final class StaticLikeNamespace(linkedClass: LinkedClass,
      myInterface: InterfaceType, namespace: MemberNamespace)
      extends MethodContainer(linkedClass, myInterface, namespace) {

    /** BOTH PASSES. */
    final def lookupMethod(methodName: MethodName): Option[MethodImpl] =
      methods.get(methodName)
  }

  private sealed abstract class JSMethodContainer {
    def untrackedJSClassCaptures: List[ParamDef]
    def untrackedThisType(namespace: MemberNamespace): Type
  }

  private final class JSClassMethodContainer(linkedClass: LinkedClass,
      val myInterface: InterfaceType) extends JSMethodContainer {

    val className: ClassName = linkedClass.className

    override def toString(): String = className.nameString

    private[this] val exportedMembers = mutable.ArrayBuffer.empty[JSMethodImpl]
    private[this] var jsConstructorDef: Option[JSCtorImpl] = None
    private[this] var _jsClassCaptures: List[ParamDef] = Nil

    updateWith(linkedClass)

    /** JS class captures
     *
     *  A similar argument applies here than for
     *  [[InterfaceType#untrackedThisType]]: The captures are merely a
     *  convenience for the optimizer's environment: Any real change of usage
     *  also necessarily changes the body of the method.
     */
    def untrackedJSClassCaptures: List[ParamDef] = _jsClassCaptures

    def untrackedThisType(namespace: MemberNamespace): Type =
      if (namespace.isStatic) NoType
      else myInterface.untrackedInstanceThisType

    def updateWith(linkedClass: LinkedClass): Unit = {
      _jsClassCaptures = linkedClass.jsClassCaptures.getOrElse(Nil)
      updateExportedMembers(linkedClass.exportedMembers)
      updateJSConstructorDef(linkedClass.jsConstructorDef)
    }

    private def updateExportedMembers(
        newExportedMembers: List[JSMethodPropDef]): Unit = {
      val newLen = newExportedMembers.length
      val oldLen = exportedMembers.length

      if (newLen > oldLen) {
        exportedMembers.sizeHint(newLen)
        for (i <- oldLen until newLen)
          exportedMembers += new JSMethodImpl(this, i)
      } else if (newLen < oldLen) {
        for (i <- newLen until oldLen)
          exportedMembers(i).delete()
        exportedMembers.dropRightInPlace(oldLen - newLen)
      }

      for {
        (method, methodIdx) <- newExportedMembers.zipWithIndex
      } {
        exportedMembers(methodIdx).updateWith(method)
      }
    }

    private def updateJSConstructorDef(
        newJSConstructorDef: Option[JSConstructorDef]): Unit = {

      newJSConstructorDef.fold {
        jsConstructorDef.foreach(_.delete())
        jsConstructorDef = None
      } { newJSConstructorDef =>
        if (jsConstructorDef.isEmpty) {
          jsConstructorDef = Some(new JSCtorImpl(this))
        }

        jsConstructorDef.get.updateWith(newJSConstructorDef)
      }
    }

    def optimizedExportedMembers(): List[JSMethodPropDef] =
      exportedMembers.map(_.optimizedDef).toList

    def optimizedJSConstructorDef(): Option[JSConstructorDef] =
      jsConstructorDef.map(_.optimizedDef)
  }

  private final class JSTopLevelMethodContainer extends JSMethodContainer {

    private[this] var methods = Map.empty[(String, String), (JSMethodImpl, Position)]

    val untrackedJSClassCaptures: List[ParamDef] = Nil
    def untrackedThisType(namespace: MemberNamespace): Type = NoType

    override def toString(): String = ""

    def updateWith(topLevelExports: List[LinkedTopLevelExport]): Unit = {
      val newMethods = topLevelExports.map(_.tree).collect {
        case m: TopLevelMethodExportDef =>
          val key = (m.moduleID, m.topLevelExportName)
          val impl = methods.get(key).fold(new JSMethodImpl(this, key))(_._1)
          impl.updateWith(m.methodDef)
          key -> (impl, m.pos)
      }.toMap

      methods
        .withFilter(e => !newMethods.contains(e._1))
        .foreach(_._2._1.delete())

      methods = newMethods
    }

    def optimizedMethod(moduleID: String, name: String): TopLevelMethodExportDef = {
      val (impl, pos) = methods((moduleID, name))
      val newMethod = impl.optimizedDef.asInstanceOf[JSMethodDef]
      TopLevelMethodExportDef(moduleID, newMethod)(pos)
    }
  }

  /** Thing from which a [[MethodImpl]] can unregister itself from. */
  private trait Unregisterable {
    /** UPDATE PASS ONLY. */
    def unregisterDependee(dependee: Processable): Unit
  }

  /** Type of a class or interface.
   *
   *  There exists exactly one instance of this per LinkedClass.
   *
   *  Fully concurrency safe unless otherwise noted.
   */
  private final class InterfaceType(linkedClass: LinkedClass) extends Unregisterable {

    val className: ClassName = linkedClass.className

    private type MethodCallers =
      ConcurrentHashMap[MethodName, ConcurrentHashMap[Processable, Unit]]

    private val ancestorsAskers = new ConcurrentHashMap[Processable, Unit]
    private val dynamicCallers: MethodCallers = new ConcurrentHashMap

    // ArrayBuffer to avoid need for ClassTag[collOps.Map[_, _]]
    private val staticCallers =
      mutable.ArrayBuffer.fill[MethodCallers](MemberNamespace.Count)(new ConcurrentHashMap)

    private val jsNativeImportsAskers = new ConcurrentHashMap[Processable, Unit]
    private val fieldsReadAskers = new ConcurrentHashMap[Processable, Unit]

    private var _ancestors: List[ClassName] = linkedClass.ancestors

    private val _instantiatedSubclasses = new ConcurrentHashMap[Class, Unit]

    private val staticLikes: Array[StaticLikeNamespace] = {
      Array.tabulate(MemberNamespace.Count) { ord =>
        new StaticLikeNamespace(linkedClass, this, MemberNamespace.fromOrdinal(ord))
      }
    }

    private val jsMethodContainer = new JSClassMethodContainer(linkedClass, this)

    /* For now, we track all JS native imports together (the class itself and native members).
     *
     * This is more to avoid unnecessary tracking than due to an intrinsic reason.
     */

    private type JSNativeImports =
      (Option[JSNativeLoadSpec.Import], Map[MethodName, JSNativeLoadSpec.Import])

    private var jsNativeImports: JSNativeImports =
      computeJSNativeImports(linkedClass)

    /* Similar comment than for JS native imports:
     * We track read state of all fields together to avoid too much tracking.
     */

    private var fieldsRead: Set[FieldName] = linkedClass.fieldsRead
    private var staticFieldsRead: Set[FieldName] = linkedClass.staticFieldsRead

    /** The type of instances of this interface.
     *
     *  Offered via untracked accessor since its only usage is in the
     *  environment of the Optimizer.
     *
     *  However, this is merely a convenience: If the this type changes
     *  and a method body relies on it, the method body itself must change,
     *  because the type of the This() tree must change.
     *
     *  Therefore, any tracking would be unnecessarily duplicate.
     */
    def untrackedInstanceThisType: Type = _instanceThisType

    private var _instanceThisType = computeInstanceThisType(linkedClass)

    override def toString(): String =
      s"intf ${className.nameString}"

    /** PROCESS PASS ONLY. */
    def askDynamicCallTargets(methodName: MethodName,
        asker: Processable): List[MethodImpl] = {
      dynamicCallers
        .computeIfAbsent(methodName, _ => new ConcurrentHashMap())
        .put(asker, ())
      asker.registerTo(this)

      val res = mutable.Set.empty[MethodImpl]
      _instantiatedSubclasses.forEachKey(Long.MaxValue, _.lookupMethod(methodName).foreach(res += _))
      res.toList
    }

    /** PROCESS PASS ONLY. */
    def askStaticCallTarget(namespace: MemberNamespace, methodName: MethodName,
        asker: Processable): MethodImpl = {
      staticCallers(namespace.ordinal)
        .computeIfAbsent(methodName, _ => new ConcurrentHashMap())
        .put(asker, ())
      asker.registerTo(this)

      def inStaticsLike = staticLike(namespace)

      val container =
        if (namespace != MemberNamespace.Public) inStaticsLike
        else classOrElse(className, inStaticsLike)

      // Method must exist, otherwise it's a bug / invalid IR.
      container.lookupMethod(methodName).getOrElse {
        throw new AssertionError(s"could not find method $className.$methodName")
      }
    }

    /** UPDATE PASS ONLY. */
    def addInstantiatedSubclass(x: Class): Unit =
      _instantiatedSubclasses.put(x, ())

    /** UPDATE PASS ONLY. */
    def removeInstantiatedSubclass(x: Class): Unit =
      _instantiatedSubclasses.remove(x)

    /** PROCESS PASS ONLY. */
    def askAncestors(asker: Processable): List[ClassName] = {
      ancestorsAskers.put(asker, ())
      asker.registerTo(this)
      _ancestors
    }

    /** PROCESS PASS ONLY. Concurrency safe except with [[updateWith]]. */
    def askJSNativeImport(asker: Processable): Option[JSNativeLoadSpec.Import] = {
      jsNativeImportsAskers.put(asker, ())
      asker.registerTo(this)
      jsNativeImports._1
    }

    /** PROCESS PASS ONLY. Concurrency safe except with [[updateWith]]. */
    def askJSNativeImport(methodName: MethodName,
        asker: Processable): Option[JSNativeLoadSpec.Import] = {
      jsNativeImportsAskers.put(asker, ())
      asker.registerTo(this)
      jsNativeImports._2.get(methodName)
    }

    def askFieldRead(name: FieldName, asker: Processable): Boolean = {
      fieldsReadAskers.put(asker, ())
      asker.registerTo(this)
      fieldsRead.contains(name)
    }

    def askStaticFieldRead(name: FieldName, asker: Processable): Boolean = {
      fieldsReadAskers.put(asker, ())
      asker.registerTo(this)
      staticFieldsRead.contains(name)
    }

    @inline
    def staticLike(namespace: MemberNamespace): StaticLikeNamespace =
      staticLikes(namespace.ordinal)

    def optimizedExportedMembers(): List[JSMethodPropDef] =
      jsMethodContainer.optimizedExportedMembers()

    def optimizedJSConstructorDef(): Option[JSConstructorDef] =
      jsMethodContainer.optimizedJSConstructorDef()

    /** UPDATE PASS ONLY. Not concurrency safe. */
    def updateWith(linkedClass: LinkedClass): Unit = {
      // Update ancestors
      if (linkedClass.ancestors != _ancestors) {
        _ancestors = linkedClass.ancestors
        ancestorsAskers.forEachKey(Long.MaxValue, _.tag())
        ancestorsAskers.clear()
      }

      // Update jsNativeImports
      val newJSNativeImports = computeJSNativeImports(linkedClass)
      if (jsNativeImports != newJSNativeImports) {
        jsNativeImports = newJSNativeImports
        jsNativeImportsAskers.forEachKey(Long.MaxValue, _.tag())
        jsNativeImportsAskers.clear()
      }

      // Update fields read.
      if (fieldsRead != linkedClass.fieldsRead ||
          staticFieldsRead != linkedClass.staticFieldsRead) {
        fieldsRead = linkedClass.fieldsRead
        staticFieldsRead = linkedClass.staticFieldsRead
        fieldsReadAskers.forEachKey(Long.MaxValue, _.tag())
        fieldsReadAskers.clear()
      }

      // Update static likes
      for (staticLike <- staticLikes) {
        val (_, changed, _) = staticLike.updateWith(linkedClass)
        for (method <- changed) {
          this.tagStaticCallersOf(staticLike.namespace, method)
        }
      }

      _instanceThisType = computeInstanceThisType(linkedClass)

      jsMethodContainer.updateWith(linkedClass)
    }

    /** UPDATE PASS ONLY. */
    def delete(): Unit = {
      // Mark all static like methods as deleted.
      staticLikes.foreach(_.methods.values.foreach(_.delete()))
    }

    /** Tag the dynamic-callers of an instance method.
     *  UPDATE PASS ONLY.
     */
    def tagDynamicCallersOf(methodName: MethodName): Unit = {
      val callers = dynamicCallers.remove(methodName)
      if (callers != null)
        callers.forEachKey(Long.MaxValue, _.tag())
    }

    /** Tag the static-callers of an instance method.
     *  UPDATE PASS ONLY.
     */
    def tagStaticCallersOf(namespace: MemberNamespace,
        methodName: MethodName): Unit = {
      val callers = staticCallers(namespace.ordinal).remove(methodName)
      if (callers != null)
        callers.forEachKey(Long.MaxValue, _.tag())
    }

    /** UPDATE PASS ONLY. */
    def unregisterDependee(dependee: Processable): Unit = {
      ancestorsAskers.remove(dependee)
      dynamicCallers.forEachValue(Long.MaxValue, _.remove(dependee))
      staticCallers.foreach(_.forEachValue(Long.MaxValue, _.remove(dependee)))
      jsNativeImportsAskers.remove(dependee)
    }

    private def computeJSNativeImports(linkedClass: LinkedClass): JSNativeImports = {
      def maybeImport(spec: JSNativeLoadSpec): Option[JSNativeLoadSpec.Import] = spec match {
        case i: JSNativeLoadSpec.Import =>
          Some(i)

        case JSNativeLoadSpec.ImportWithGlobalFallback(i, _) =>
          if (config.coreSpec.moduleKind != ModuleKind.NoModule) Some(i)
          else None

        case _: JSNativeLoadSpec.Global =>
          None
      }

      val clazz = linkedClass.jsNativeLoadSpec.flatMap(maybeImport(_))
      val nativeMembers = for {
        member <- linkedClass.jsNativeMembers
        jsImport <- maybeImport(member.jsNativeLoadSpec)
      } yield {
        member.name.name -> jsImport
      }

      (clazz, nativeMembers.toMap)
    }

    private def computeInstanceThisType(linkedClass: LinkedClass): Type = {
      if (linkedClass.kind.isJSType) AnyType
      else if (linkedClass.kind == ClassKind.HijackedClass) BoxedClassToPrimType(className)
      else ClassType(className, nullable = false)
    }
  }

  /** A thing that can be tagged for reprocessing and then reprocessed. */
  private abstract class Processable {
    type Def >: scala.Null <: VersionedMemberDef

    private[this] val registeredTo = new ConcurrentHashMap[Unregisterable, Unit]
    private[this] val tagged = new AtomicBoolean(false)
    private[this] var _deleted: Boolean = false

    private[this] var lastInVersion: Version = Version.Unversioned
    private[this] var lastOutVersion: Int = 0

    private[this] var _originalDef: Def = _
    private[this] var _optimizedDef: Def = _

    protected def doProcess(newVersion: Version): Def

    final def deleted: Boolean = _deleted

    final def originalDef: Def = _originalDef
    final def optimizedDef: Def = _optimizedDef

    /** PROCESS PASS ONLY. */
    final def process(): Unit = {
      if (!_deleted) {
        lastOutVersion += 1
        _optimizedDef = doProcess(Version.fromInt(lastOutVersion))
        tagged.set(false)
      }
    }

    /** Returns true if the method changed */
    protected def updateDef(methodDef: Def): Boolean = {
      assert(!deleted, "updateDef() called on a deleted method")

      if (lastInVersion.sameVersion(methodDef.version)) {
        false
      } else {
        lastInVersion = methodDef.version
        _originalDef = methodDef
        _optimizedDef = null
        tag()
        true
      }
    }

    private def unregisterFromEverywhere(): Unit = {
      registeredTo.forEachKey(Long.MaxValue, _.unregisterDependee(this))
      registeredTo.clear()
    }

    /** UPDATE PASS ONLY. Not concurrency safe on same instance. */
    final def delete(): Unit = {
      assert(!_deleted, "delete() called twice")
      _deleted = true
      if (protectTag())
        unregisterFromEverywhere()
    }

    /** Concurrency safe with itself and [[delete]] on the same instance
     *
     *  [[tag]] can be called concurrently with [[delete]] when methods in
     *  traits/classes are updated.
     *
     *  UPDATE PASS ONLY.
     */
    final def tag(): Unit = {
      if (protectTag()) {
        collOps.add(methodsToProcess, this)
        unregisterFromEverywhere()
      }
    }

    /** PROCESS PASS ONLY. */
    final def registerTo(unregisterable: Unregisterable): Unit =
      registeredTo.put(unregisterable, ())

    /** Tag this method and return true iff it wasn't tagged before.
     *  UPDATE PASS ONLY.
     */
    private def protectTag(): Boolean = !tagged.getAndSet(true)
  }

  /** A method implementation.
   *  It must be concrete, and belong either to a [[IncOptimizer.Class]] or a
   *  [[IncOptimizer.StaticsNamespace]].
   *
   *  A single instance is **not** concurrency safe (unless otherwise noted in
   *  a method comment). However, the global state modifications are
   *  concurrency safe.
   */
  private final class MethodImpl(owner: MethodContainer,
      val methodName: MethodName)
      extends Processable with OptimizerCore.AbstractMethodID with Unregisterable {

    type Def = MethodDef

    private val bodyAskers = new ConcurrentHashMap[Processable, Unit]

    var attributes: OptimizerCore.MethodAttributes = _

    def enclosingClassName: ClassName = owner.className

    override def toString(): String =
      s"$owner.${methodName.nameString}"

    /** PROCESS PASS ONLY. */
    def askBody(asker: Processable): MethodDef = {
      bodyAskers.put(asker, ())
      asker.registerTo(this)
      originalDef
    }

    /** UPDATE PASS ONLY. */
    def tagBodyAskers(): Unit = {
      bodyAskers.forEachKey(Long.MaxValue, _.tag())
      bodyAskers.clear()
    }

    /** UPDATE PASS ONLY. */
    def unregisterDependee(dependee: Processable): Unit =
      bodyAskers.remove(dependee)

    /** Returns true if the method's attributes changed.
     *  Attributes are whether it is inlineable, and whether it is a trait
     *  impl forwarder. Basically this is what is declared in
     *  `OptimizerCore.AbstractMethodID`.
     *  In the process, tags all the body askers if the body changes.
     *  UPDATE PASS ONLY. Not concurrency safe on same instance.
     */
    def updateWith(methodDef: MethodDef): Boolean = {
      val changed = updateDef(methodDef)
      if (changed) {
        tagBodyAskers()

        val oldAttributes = attributes
        attributes = OptimizerCore.MethodAttributes.compute(enclosingClassName, methodDef)
        attributes != oldAttributes
      } else {
        false
      }
    }

    /** PROCESS PASS ONLY. */
    protected def doProcess(newVersion: Version): MethodDef = {
      val MethodDef(static, name, originalName, params, resultType, optBody) =
        originalDef
      val body = optBody.getOrElse {
        throw new AssertionError("Methods to optimize must be concrete")
      }

      val (newParams, newBody) = new Optimizer(this, Some(this), this.toString()).optimize(
          owner.untrackedThisType, params, jsClassCaptures = Nil,
          resultType, body, isNoArgCtor = name.name == NoArgConstructorName)

      MethodDef(static, name, originalName,
          newParams, resultType, Some(newBody))(
          originalDef.optimizerHints, newVersion)(originalDef.pos)
    }
  }

  private final class JSMethodImpl(owner: JSMethodContainer, id: Any) extends Processable {

    type Def = JSMethodPropDef

    override def toString(): String =
      s"$owner[$id]"

    def updateWith(linkedMethod: JSMethodPropDef): Unit =
      updateDef(linkedMethod)

    protected def doProcess(newVersion: Version): JSMethodPropDef = {
      originalDef match {
        case originalDef @ JSMethodDef(flags, name, params, restParam, body) =>
          val thisType = owner.untrackedThisType(flags.namespace)

          val (newParamsAndRest, newBody) = new Optimizer(this, None, this.toString()).optimize(
            thisType, params ++ restParam.toList, owner.untrackedJSClassCaptures,
            AnyType, body, isNoArgCtor = false)

          val (newParams, newRestParam) =
            if (restParam.isDefined) (newParamsAndRest.init, Some(newParamsAndRest.last))
            else (newParamsAndRest, None)

          JSMethodDef(flags, name, newParams, newRestParam, newBody)(
              originalDef.optimizerHints, newVersion)(originalDef.pos)

        case originalDef @ JSPropertyDef(flags, name, getterBody, setterArgAndBody) =>
          val thisType = owner.untrackedThisType(flags.namespace)
          val jsClassCaptures = owner.untrackedJSClassCaptures

          val newGetterBody = getterBody.map { body =>
            val (_, newBody) = new Optimizer(this, None, "get " + this.toString()).optimize(
                thisType, Nil, jsClassCaptures, AnyType, body, isNoArgCtor = false)
            newBody
          }

          val newSetterArgAndBody = setterArgAndBody.map { case (param, body) =>
            val (List(newParam), newBody) = new Optimizer(this, None, "set " + this.toString()).optimize(
                thisType, List(param), jsClassCaptures, AnyType, body,
                isNoArgCtor = false)
            (newParam, newBody)
          }

          JSPropertyDef(flags, name, newGetterBody, newSetterArgAndBody)(newVersion)(originalDef.pos)
      }
    }
  }

  private final class JSCtorImpl(owner: JSMethodContainer) extends Processable {

    type Def = JSConstructorDef

    override def toString(): String =
      s"$owner ctor"

    def updateWith(linkedMethod: JSConstructorDef): Unit =
      updateDef(linkedMethod)

    protected def doProcess(newVersion: Version): JSConstructorDef = {
      val JSConstructorDef(flags, params, restParam, body) = originalDef

      val thisType = owner.untrackedThisType(flags.namespace)

      val (newParamsAndRest, newRawBody) = new Optimizer(this, None, this.toString()).optimize(
          thisType, params ++ restParam.toList, owner.untrackedJSClassCaptures, AnyType,
          Block(body.allStats)(body.pos), isNoArgCtor = false)

      val (newParams, newRestParam) =
        if (restParam.isDefined) (newParamsAndRest.init, Some(newParamsAndRest.last))
        else (newParamsAndRest, None)

      val bodyStats = newRawBody match {
        case Block(stats) => stats
        case stat         => List(stat)
      }

      val (beforeSuper, superCall :: afterSuper) =
        bodyStats.span(!_.isInstanceOf[JSSuperConstructorCall])

      val newBody = JSConstructorBody(beforeSuper,
          superCall.asInstanceOf[JSSuperConstructorCall], afterSuper)(body.pos)

      JSConstructorDef(flags, newParams, newRestParam, newBody)(
          originalDef.optimizerHints, newVersion)(originalDef.pos)
    }
  }

  /** Concrete optimizer bound to types we use.
   *
   *  All methods are PROCESS PASS ONLY
   */
  private final class Optimizer(
      asker: Processable, protected val myself: Option[MethodImpl], debugID: String)
      extends OptimizerCore(config, debugID) {
    import OptimizerCore.ImportTarget

    type MethodID = MethodImpl

    protected def getMethodBody(method: MethodID): MethodDef =
      method.askBody(asker)

    /** Look up the targets of a dynamic call to an instance method. */
    protected def dynamicCall(intfName: ClassName,
        methodName: MethodName): List[MethodID] = {
      getInterface(intfName).askDynamicCallTargets(methodName, asker)
    }

    /** Look up the target of a static call to an instance method. */
    protected def staticCall(className: ClassName, namespace: MemberNamespace,
        methodName: MethodName): MethodID = {
      getInterface(className).askStaticCallTarget(namespace, methodName, asker)
    }

    protected def getAncestorsOf(intfName: ClassName): List[ClassName] =
      getInterface(intfName).askAncestors(asker)

    protected def hasElidableConstructors(className: ClassName): Boolean =
      classes.get(className).askHasElidableConstructors(asker)

    protected def inlineableFieldBodies(className: ClassName): OptimizerCore.InlineableFieldBodies = {
      val clazz = classes.get(className)
      if (clazz == null) OptimizerCore.InlineableFieldBodies.Empty
      else clazz.askInlineableFieldBodies(asker)
    }

    protected def tryNewInlineableClass(
        className: ClassName): Option[OptimizerCore.InlineableClassStructure] = {
      classes.get(className).tryNewInlineable
    }

    protected def getJSNativeImportOf(
        target: ImportTarget): Option[JSNativeLoadSpec.Import] = {
      target match {
        case ImportTarget.Class(className) =>
          getInterface(className).askJSNativeImport(asker)
        case ImportTarget.Member(className, methodName) =>
          getInterface(className).askJSNativeImport(methodName, asker)
      }
    }

    protected def isFieldRead(fieldName: FieldName): Boolean =
      getInterface(fieldName.className).askFieldRead(fieldName, asker)

    protected def isStaticFieldRead(fieldName: FieldName): Boolean =
      getInterface(fieldName.className).askStaticFieldRead(fieldName, asker)
  }

}

object IncOptimizer {
  def apply(config: CommonPhaseConfig): IncOptimizer =
    new IncOptimizer(config, SeqCollOps)

  private val isAdHocElidableConstructors: Set[ClassName] =
    Set(ClassName("scala.Predef$"), ClassName("scala.package$"))

  sealed abstract class ElidableConstructorsInfo {
    import ElidableConstructorsInfo._

    final def mergeWith(that: ElidableConstructorsInfo): ElidableConstructorsInfo = (this, that) match {
      case (DependentOn(deps1, getterDeps1), DependentOn(deps2, getterDeps2)) =>
        DependentOn(deps1 ++ deps2, getterDeps1 ++ getterDeps2)
      case (AcyclicElidable, _) =>
        that
      case (_, AcyclicElidable) =>
        this
      case _ =>
        NotElidable
    }
  }

  object ElidableConstructorsInfo {
    case object NotElidable extends ElidableConstructorsInfo

    case object AcyclicElidable extends ElidableConstructorsInfo

    final case class DependentOn(
      dependencies: Set[ClassName],
      getterDependencies: Set[(ClassName, MethodName)]
    ) extends ElidableConstructorsInfo
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy