summaryrefslogtreecommitdiff
path: root/src/compiler/scala/tools/nsc/backend/jvm/opt/ByteCodeRepository.scala
blob: a5b85e54e790e5332004f62519737a36fbb86e74 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/* NSC -- new Scala compiler
 * Copyright 2005-2014 LAMP/EPFL
 * @author  Martin Odersky
 */

package scala.tools.nsc
package backend.jvm
package opt

import scala.tools.asm
import asm.tree._
import scala.collection.convert.decorateAsScala._
import scala.tools.asm.Attribute
import scala.tools.nsc.backend.jvm.BackendReporting._
import scala.tools.nsc.io.AbstractFile
import scala.tools.nsc.util.ClassFileLookup
import BytecodeUtils._
import ByteCodeRepository._
import BTypes.InternalName
import java.util.concurrent.atomic.AtomicLong

/**
 * The ByteCodeRepository provides utilities to read the bytecode of classfiles from the compilation
 * classpath. Parsed classes are cached in the `classes` map.
 *
 * @param classPath The compiler classpath where classfiles are searched and read from.
 * @param classes   Cache for parsed ClassNodes. Also stores the source of the bytecode:
 *                  [[Classfile]] if read from `classPath`, [[CompilationUnit]] if the bytecode
 *                  corresponds to a class being compiled.
 *                  The `Long` field encodes the age of the node in the map, which allows removing
 *                  old entries when the map grows too large.
 *                  For Java classes in mixed compilation, the map contains an error message: no
 *                  ClassNode is generated by the backend and also no classfile that could be parsed.
 */
class ByteCodeRepository(val classPath: ClassFileLookup[AbstractFile], val isJavaSourceDefined: InternalName => Boolean, val classes: collection.concurrent.Map[InternalName, Either[ClassNotFound, (ClassNode, Source, Long)]]) {

  private val maxCacheSize = 1500
  private val targetSize   = 500

  private val idCounter = new AtomicLong(0)

  /**
   * Prevent the code repository from growing too large. Profiling reveals that the average size
   * of a ClassNode is about 30 kb. I observed having 17k+ classes in the cache, i.e., 500 mb.
   *
   * We can only remove classes with `Source == Classfile`, those can be parsed again if requested.
   */
  private def limitCacheSize(): Unit = {
    if (classes.count(c => c._2.isRight && c._2.right.get._2 == Classfile) > maxCacheSize) {
      val removeId = idCounter.get - targetSize
      val toRemove = classes.iterator.collect({
        case (name, Right((_, Classfile, id))) if id < removeId => name
      }).toList
      toRemove foreach classes.remove
    }
  }

  def add(classNode: ClassNode, source: Source) = {
    classes(classNode.name) = Right((classNode, source, idCounter.incrementAndGet()))
  }

  /**
   * The class node and source for an internal name. If the class node is not yet available, it is
   * parsed from the classfile on the compile classpath.
   */
  def classNodeAndSource(internalName: InternalName): Either[ClassNotFound, (ClassNode, Source)] = {
    val r = classes.getOrElseUpdate(internalName, {
      limitCacheSize()
      parseClass(internalName).map((_, Classfile, idCounter.incrementAndGet()))
    })
    r.map(v => (v._1, v._2))
  }

  /**
   * The class node for an internal name. If the class node is not yet available, it is parsed from
   * the classfile on the compile classpath.
   */
  def classNode(internalName: InternalName): Either[ClassNotFound, ClassNode] = classNodeAndSource(internalName).map(_._1)

  /**
   * The field node for a field matching `name` and `descriptor`, accessed in class `classInternalName`.
   * The declaration of the field may be in one of the superclasses.
   *
   * @return The [[FieldNode]] of the requested field and the [[InternalName]] of its declaring
   *         class, or an error message if the field could not be found
   */
  def fieldNode(classInternalName: InternalName, name: String, descriptor: String): Either[FieldNotFound, (FieldNode, InternalName)] = {
    def fieldNodeImpl(parent: InternalName): Either[FieldNotFound, (FieldNode, InternalName)] = {
      def msg = s"The field node $name$descriptor could not be found in class $classInternalName or any of its superclasses."
      classNode(parent) match {
        case Left(e)  => Left(FieldNotFound(name, descriptor, classInternalName, Some(e)))
        case Right(c) =>
          c.fields.asScala.find(f => f.name == name && f.desc == descriptor) match {
            case Some(f) => Right((f, parent))
            case None    =>
              if (c.superName == null) Left(FieldNotFound(name, descriptor, classInternalName, None))
              else fieldNode(c.superName, name, descriptor)
          }
      }
    }
    fieldNodeImpl(classInternalName)
  }

  /**
   * The method node for a method matching `name` and `descriptor`, accessed in class `ownerInternalNameOrArrayDescriptor`.
   * The declaration of the method may be in one of the parents.
   *
   * @return The [[MethodNode]] of the requested method and the [[InternalName]] of its declaring
   *         class, or an error message if the method could not be found.
   */
  def methodNode(ownerInternalNameOrArrayDescriptor: String, name: String, descriptor: String): Either[MethodNotFound, (MethodNode, InternalName)] = {
    // on failure, returns a list of class names that could not be found on the classpath
    def methodNodeImpl(ownerInternalName: InternalName): Either[List[ClassNotFound], (MethodNode, InternalName)] = {
      classNode(ownerInternalName) match {
        case Left(e)  => Left(List(e))
        case Right(c) =>
          c.methods.asScala.find(m => m.name == name && m.desc == descriptor) match {
            case Some(m) => Right((m, ownerInternalName))
            case None    => findInParents(Option(c.superName) ++: c.interfaces.asScala.toList, Nil)
          }
      }
    }

    // find the MethodNode in one of the parent classes
    def findInParents(parents: List[InternalName], failedClasses: List[ClassNotFound]): Either[List[ClassNotFound], (MethodNode, InternalName)] = parents match {
      case x :: xs => methodNodeImpl(x).left.flatMap(failed => findInParents(xs, failed ::: failedClasses))
      case Nil     => Left(failedClasses)
    }

    // In a MethodInsnNode, the `owner` field may be an array descriptor, for example when invoking `clone`. We don't have a method node to return in this case.
    if (ownerInternalNameOrArrayDescriptor.charAt(0) == '[')
      Left(MethodNotFound(name, descriptor, ownerInternalNameOrArrayDescriptor, Nil))
    else
      methodNodeImpl(ownerInternalNameOrArrayDescriptor).left.map(MethodNotFound(name, descriptor, ownerInternalNameOrArrayDescriptor, _))
  }

  private def parseClass(internalName: InternalName): Either[ClassNotFound, ClassNode] = {
    val fullName = internalName.replace('/', '.')
    classPath.findClassFile(fullName) map { classFile =>
      val classNode = new asm.tree.ClassNode()
      val classReader = new asm.ClassReader(classFile.toByteArray)

      // Passing the InlineInfoAttributePrototype makes the ClassReader invoke the specific `read`
      // method of the InlineInfoAttribute class, instead of putting the byte array into a generic
      // Attribute.
      // We don't need frames when inlining, but we want to keep the local variable table, so we
      // don't use SKIP_DEBUG.
      classReader.accept(classNode, Array[Attribute](InlineInfoAttributePrototype), asm.ClassReader.SKIP_FRAMES)
      // SKIP_FRAMES leaves line number nodes. Remove them because they are not correct after
      // inlining.
      // TODO: we need to remove them also for classes that are not parsed from classfiles, why not simplify and do it once when inlining?
      // OR: instead of skipping line numbers for inlined code, use write a SourceDebugExtension
      // attribute that contains JSR-45 data that encodes debugging info.
      //   http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.11
      //   https://jcp.org/aboutJava/communityprocess/final/jsr045/index.html
      removeLineNumberNodes(classNode)
      classNode
    } match {
      case Some(node) => Right(node)
      case None       => Left(ClassNotFound(internalName, isJavaSourceDefined(internalName)))
    }
  }
}

object ByteCodeRepository {
  /**
   * The source of a ClassNode in the ByteCodeRepository. Can be either [[CompilationUnit]] if the
   * class is being compiled or [[Classfile]] if the class was parsed from the compilation classpath.
   */
  sealed trait Source
  object CompilationUnit extends Source
  object Classfile extends Source
}