summaryrefslogtreecommitdiff
path: root/src/compiler/scala/tools/nsc/interactive/ScratchPadMaker.scala
blob: 8b4c2ce4ebcd9728cb46d097a9f05694909b9296 (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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package scala.tools.nsc
package interactive

import scala.reflect.internal.util.{SourceFile, BatchSourceFile, RangePosition}
import scala.collection.mutable.ArrayBuffer
import scala.reflect.internal.Chars.{isLineBreakChar, isWhitespace}
import ast.parser.Tokens._

@deprecated("SI-6458: Instrumentation logic will be moved out of the compiler.","2.10.0")
trait ScratchPadMaker { self: Global =>

  import definitions._

  private case class Patch(offset: Int, text: String)

  private class Patcher(contents: Array[Char], lex: LexicalStructure, endOffset: Int) extends Traverser {
    var objectName: String = ""

    private val patches = new ArrayBuffer[Patch]
    private val toPrint = new ArrayBuffer[String]
    private var skipped = 0
    private var resNum: Int = -1

    private def nextRes(): String = {
      resNum += 1
      "res$"+resNum
    }

    private def nameType(name: String, tpe: Type): String = {
      // if name ends in symbol character, add a space to separate it from the following ':'
      val pad = if (Character.isLetter(name.last) || Character.isDigit(name.last)) "" else " "
      name+pad+": "+tpe
    }

    private def nameType(sym: Symbol): String = nameType(sym.name.decoded, sym.tpe)

    private def literal(str: String) = "\"\"\""+str+"\"\"\""

    private val prologue = ";import scala.runtime.WorksheetSupport._; def main(args: Array[String])=$execute{"

    private val epilogue = "}"

    private def applyPendingPatches(offset: Int) = {
      if (skipped == 0) patches += Patch(offset, prologue)
      for (msg <- toPrint) patches += Patch(offset, ";System.out.println("+msg+")")
      toPrint.clear()
    }

    /** The position where to insert an instrumentation statement in front of giuven statement.
     *  This is at the latest `stat.pos.start`. But in order not to mess with column numbers
     *  in position we try to insert it at the end of the previous token instead.
     *  Furthermore, `(' tokens have to be skipped because they do not show up
     *  in statement range positions.
     */
    private def instrumentPos(start: Int): Int = {
      val (prevToken, prevStart, prevEnd) = lex.locate(start - 1)
      if (prevStart >= start) start
      else if (prevToken == LPAREN) instrumentPos(prevStart)
      else prevEnd
    }

    private def addSkip(stat: Tree): Unit = {
      val ipos = instrumentPos(stat.pos.start)
      if (stat.pos.start > skipped) applyPendingPatches(ipos)
      if (stat.pos.start >= endOffset)
        patches += Patch(ipos, ";$stop()")
      var end = stat.pos.end
      if (end > skipped) {
        while (end < contents.length && !isLineBreakChar(contents(end))) end += 1
        patches += Patch(ipos, ";$skip("+(end-skipped)+"); ")
        skipped = end
      }
    }

    private def addSandbox(expr: Tree) = {}
//      patches += (Patch(expr.pos.start, "sandbox("), Patch(expr.pos.end, ")"))

    private def resultString(prefix: String, expr: String) =
      literal(prefix + " = ") + " + $show(" + expr + ")"

    private def traverseStat(stat: Tree) =
      if (stat.pos.isInstanceOf[RangePosition]) {
        stat match {
          case ValDef(_, _, _, rhs) =>
            addSkip(stat)
            if (stat.symbol.isLazy)
              toPrint += literal(nameType(stat.symbol) + " = <lazy>")
            else if (!stat.symbol.isSynthetic) {
              addSandbox(rhs)
              toPrint += resultString(nameType(stat.symbol), stat.symbol.name.toString)
            }
          case DefDef(_, _, _, _, _, _) =>
            addSkip(stat)
            toPrint += literal(nameType(stat.symbol))
          case Annotated(_, arg) =>
            traverse(arg)
          case DocDef(_, defn) =>
            traverse(defn)
          case _ =>
            if (stat.isTerm) {
              addSkip(stat)
              if (stat.tpe.typeSymbol == UnitClass) {
                addSandbox(stat)
              } else {
                val resName = nextRes()
                val dispResName = resName filter ('$' != _)
                val offset = instrumentPos(stat.pos.start)
                patches += Patch(offset, "val " + resName + " = ")
                addSandbox(stat)
                toPrint += resultString(nameType(dispResName, stat.tpe), resName)
              }
            }
        }
      }

    override def traverse(tree: Tree): Unit = tree match {
      case PackageDef(_, _) =>
        super.traverse(tree)
      case ModuleDef(_, name, Template(_, _, body)) =>
        val topLevel = objectName.isEmpty
        if (topLevel) {
          objectName = tree.symbol.fullName
          body foreach traverseStat
          if (skipped != 0) { // don't issue prologue and epilogue if there are no instrumented statements
            applyPendingPatches(skipped)
            patches += Patch(skipped, epilogue)
          }
        }
      case _ =>
    }

    /** The patched text.
     *  @require  traverse is run first
     */
    def result: Array[Char] = {
      val reslen = contents.length + (patches map (_.text.length)).sum
      val res = Array.ofDim[Char](reslen)
      var lastOffset = 0
      var from = 0
      var to = 0
      for (Patch(offset, text) <- patches) {
        val delta = offset - lastOffset
        assert(delta >= 0)
        Array.copy(contents, from, res, to, delta)
        from += delta
        to += delta
        lastOffset = offset
        text.copyToArray(res, to)
        to += text.length
      }
      assert(contents.length - from == reslen - to)
      Array.copy(contents, from, res, to, contents.length - from)
      res
    }
  }

  class LexicalStructure(source: SourceFile) {
    val token = new ArrayBuffer[Int]
    val startOffset = new ArrayBuffer[Int]
    val endOffset = new ArrayBuffer[Int]
    private val scanner = new syntaxAnalyzer.UnitScanner(new CompilationUnit(source))
    scanner.init()
    while (scanner.token != EOF) {
      startOffset += scanner.offset
      token += scanner.token
      scanner.nextToken()
      endOffset += scanner.lastOffset
    }

    /** @return token that starts before or at offset, its startOffset, its endOffset
     */
    def locate(offset: Int): (Int, Int, Int) = {
      var lo = 0
      var hi = token.length - 1
      while (lo < hi) {
        val mid = (lo + hi + 1) / 2
        if (startOffset(mid) <= offset) lo = mid
        else hi = mid - 1
      }
      (token(lo), startOffset(lo), endOffset(lo))
    }
  }

  /** Compute an instrumented version of a sourcefile.
   *  @param source  The given sourcefile.
   *  @param line    The line up to which results should be printed, -1 = whole document.
   *  @return        A pair consisting of
   *                  - the fully qualified name of the first top-level object definition in the file.
   *                    or "" if there are no object definitions.
   *                  - the text of the instrumented program which, when run,
   *                    prints its output and all defined values in a comment column.
   */
  protected def instrument(source: SourceFile, line: Int): (String, Array[Char]) = {
    val tree = typedTree(source, true)
    val endOffset = if (line < 0) source.length else source.lineToOffset(line + 1)
    val patcher = new Patcher(source.content, new LexicalStructure(source), endOffset)
    patcher.traverse(tree)
    (patcher.objectName, patcher.result)
  }
}