summaryrefslogtreecommitdiff
path: root/src/compiler/scala/tools/nsc/backend/jvm/BCodeSyncAndTry.scala
blob: b94208c1a5a4ee4cb54a8bd7b330274cb54c5294 (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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
/* NSC -- new Scala compiler
 * Copyright 2005-2012 LAMP/EPFL
 * @author  Martin Odersky
 */


package scala
package tools.nsc
package backend
package jvm

import scala.collection.immutable
import scala.tools.asm

/*
 *
 *  @author  Miguel Garcia, http://lamp.epfl.ch/~magarcia/ScalaCompilerCornerReloaded/
 *  @version 1.0
 *
 */
abstract class BCodeSyncAndTry extends BCodeBodyBuilder {
  import global._
  import bTypes._
  import coreBTypes._

  /*
   * Functionality to lower `synchronized` and `try` expressions.
   */
  abstract class SyncAndTryBuilder(cunit: CompilationUnit) extends PlainBodyBuilder(cunit) {

    def genSynchronized(tree: Apply, expectedType: BType): BType = {
      val Apply(fun, args) = tree
      val monitor = locals.makeLocal(ObjectReference, "monitor")
      val monCleanup = new asm.Label

      // if the synchronized block returns a result, store it in a local variable.
      // Just leaving it on the stack is not valid in MSIL (stack is cleaned when leaving try-blocks).
      val hasResult = (expectedType != UNIT)
      val monitorResult: Symbol = if (hasResult) locals.makeLocal(tpeTK(args.head), "monitorResult") else null;

      /* ------ (1) pushing and entering the monitor, also keeping a reference to it in a local var. ------ */
      genLoadQualifier(fun)
      bc dup ObjectReference
      locals.store(monitor)
      emit(asm.Opcodes.MONITORENTER)

      /* ------ (2) Synchronized block.
       *            Reached by fall-through from (1).
       *            Protected by:
       *            (2.a) the EH-version of the monitor-exit, and
       *            (2.b) whatever protects the whole synchronized expression.
       * ------
       */
      val startProtected = currProgramPoint()
      registerCleanup(monCleanup)
      genLoad(args.head, expectedType /* toTypeKind(tree.tpe.resultType) */)
      unregisterCleanup(monCleanup)
      if (hasResult) { locals.store(monitorResult) }
      nopIfNeeded(startProtected)
      val endProtected = currProgramPoint()

      /* ------ (3) monitor-exit after normal, non-early-return, termination of (2).
       *            Reached by fall-through from (2).
       *            Protected by whatever protects the whole synchronized expression.
       * ------
       */
      locals.load(monitor)
      emit(asm.Opcodes.MONITOREXIT)
      if (hasResult) { locals.load(monitorResult) }
      val postHandler = new asm.Label
      bc goTo postHandler

      /* ------ (4) exception-handler version of monitor-exit code.
       *            Reached upon abrupt termination of (2).
       *            Protected by whatever protects the whole synchronized expression.
       * ------
       */
      protect(startProtected, endProtected, currProgramPoint(), ThrowableReference)
      locals.load(monitor)
      emit(asm.Opcodes.MONITOREXIT)
      emit(asm.Opcodes.ATHROW)

      /* ------ (5) cleanup version of monitor-exit code.
       *            Reached upon early-return from (2).
       *            Protected by whatever protects the whole synchronized expression.
       * ------
       */
      if (shouldEmitCleanup) {
        markProgramPoint(monCleanup)
        locals.load(monitor)
        emit(asm.Opcodes.MONITOREXIT)
        pendingCleanups()
      }

      /* ------ (6) normal exit of the synchronized expression.
       *            Reached after normal, non-early-return, termination of (3).
       *            Protected by whatever protects the whole synchronized expression.
       * ------
       */
      mnode visitLabel postHandler

      lineNumber(tree)

      expectedType
    }

    /*
     *  Detects whether no instructions have been emitted since label `lbl` and if so emits a NOP.
     *  Useful to avoid emitting an empty try-block being protected by exception handlers,
     *  which results in "java.lang.ClassFormatError: Illegal exception table range". See SI-6102.
     */
    def nopIfNeeded(lbl: asm.Label) {
      val noInstructionEmitted = isAtProgramPoint(lbl)
      if (noInstructionEmitted) { emit(asm.Opcodes.NOP) }
    }

    /*
     *  Emitting try-catch is easy, emitting try-catch-finally not quite so.
     *  A finally-block (which always has type Unit, thus leaving the operand stack unchanged)
     *  affects control-transfer from protected regions, as follows:
     *
     *    (a) `return` statement:
     *
     *        First, the value to return (if any) is evaluated.
     *        Afterwards, all enclosing finally-blocks are run, from innermost to outermost.
     *        Only then is the return value (if any) returned.
     *
     *        Some terminology:
     *          (a.1) Executing a return statement that is protected
     *                by one or more finally-blocks is called "early return"
     *          (a.2) the chain of code sections (a code section for each enclosing finally-block)
     *                to run upon early returns is called "cleanup chain"
     *
     *        As an additional spin, consider a return statement in a finally-block.
     *        In this case, the value to return depends on how control arrived at that statement:
     *        in case it arrived via a previous return, the previous return enjoys priority:
     *        the value to return is given by that statement.
     *
     *    (b) A finally-block protects both the try-clause and the catch-clauses.
     *
     *           Sidenote:
     *             A try-clause may contain an empty block. On CLR, a finally-block has special semantics
     *             regarding Abort interruptions; but on the JVM it's safe to elide an exception-handler
     *             that protects an "empty" range ("empty" as in "containing NOPs only",
     *             see `asm.optimiz.DanglingExcHandlers` and SI-6720).
     *
     *        This means a finally-block indicates instructions that can be reached:
     *          (b.1) Upon normal (non-early-returning) completion of the try-clause or a catch-clause
     *                In this case, the next-program-point is that following the try-catch-finally expression.
     *          (b.2) Upon early-return initiated in the try-clause or a catch-clause
     *                In this case, the next-program-point is the enclosing cleanup section (if any), otherwise return.
     *          (b.3) Upon abrupt termination (due to unhandled exception) of the try-clause or a catch-clause
     *                In this case, the unhandled exception must be re-thrown after running the finally-block.
     *
     *    (c) finally-blocks are implicit to `synchronized` (a finally-block is added to just release the lock)
     *        that's why `genSynchronized()` too emits cleanup-sections.
     *
     *  A number of code patterns can be emitted to realize the intended semantics.
     *
     *  A popular alternative (GenICode, javac) consists in duplicating the cleanup-chain at each early-return position.
     *  The principle at work being that once control is transferred to a cleanup-section,
     *  control will always stay within the cleanup-chain.
     *  That is, barring an exception being thrown in a cleanup-section, in which case the enclosing try-block
     *  (reached via abrupt termination) takes over.
     *
     *  The observations above hint at another code layout, less verbose, for the cleanup-chain.
     *
     *  The code layout that GenBCode emits takes into account that once a cleanup section has been reached,
     *  jumping to the next cleanup-section (and so on, until the outermost one) realizes the correct semantics.
     *
     *  There is still code duplication in that two cleanup-chains are needed (but this is unavoidable, anyway):
     *  one for normal control flow and another chain consisting of exception handlers.
     *  The in-line comments below refer to them as
     *    - "early-return-cleanups" and
     *    - "exception-handler-version-of-finally-block" respectively.
     *
     */
    def genLoadTry(tree: Try): BType = {

      val Try(block, catches, finalizer) = tree
      val kind = tpeTK(tree)

      val caseHandlers: List[EHClause] =
        for (CaseDef(pat, _, caseBody) <- catches) yield {
          pat match {
            case Typed(Ident(nme.WILDCARD), tpt)  => NamelessEH(tpeTK(tpt).asClassBType, caseBody)
            case Ident(nme.WILDCARD)              => NamelessEH(ThrowableReference,  caseBody)
            case Bind(_, _)                       => BoundEH   (pat.symbol, caseBody)
          }
        }

      // ------ (0) locals used later ------

      /*
       * `postHandlers` is a program point denoting:
       *     (a) the finally-clause conceptually reached via fall-through from try-catch-finally
       *         (in case a finally-block is present); or
       *     (b) the program point right after the try-catch
       *         (in case there's no finally-block).
       * The name choice emphasizes that the code section lies "after all exception handlers",
       * where "all exception handlers" includes those derived from catch-clauses as well as from finally-blocks.
       */
      val postHandlers = new asm.Label

      val hasFinally   = (finalizer != EmptyTree)

      /*
       * used in the finally-clause reached via fall-through from try-catch, if any.
       */
      val guardResult  = hasFinally && (kind != UNIT) && mayCleanStack(finalizer)

      /*
       * please notice `tmp` has type tree.tpe, while `earlyReturnVar` has the method return type.
       * Because those two types can be different, dedicated vars are needed.
       */
      val tmp          = if (guardResult) locals.makeLocal(tpeTK(tree), "tmp") else null;

      /*
       * upon early return from the try-body or one of its EHs (but not the EH-version of the finally-clause)
       * AND hasFinally, a cleanup is needed.
       */
      val finCleanup   = if (hasFinally) new asm.Label else null

      /* ------ (1) try-block, protected by:
       *                       (1.a) the EHs due to case-clauses,   emitted in (2),
       *                       (1.b) the EH  due to finally-clause, emitted in (3.A)
       *                       (1.c) whatever protects the whole try-catch-finally expression.
       * ------
       */

      val startTryBody = currProgramPoint()
      registerCleanup(finCleanup)
      genLoad(block, kind)
      unregisterCleanup(finCleanup)
      nopIfNeeded(startTryBody)
      val endTryBody = currProgramPoint()
      bc goTo postHandlers

      /* ------ (2) One EH for each case-clause (this does not include the EH-version of the finally-clause)
       *            An EH in (2) is reached upon abrupt termination of (1).
       *            An EH in (2) is protected by:
       *                         (2.a) the EH-version of the finally-clause, if any.
       *                         (2.b) whatever protects the whole try-catch-finally expression.
       * ------
       */

      for (ch <- caseHandlers) {

        // (2.a) emit case clause proper
        val startHandler = currProgramPoint()
        var endHandler: asm.Label = null
        var excType: ClassBType = null
        registerCleanup(finCleanup)
        ch match {
          case NamelessEH(typeToDrop, caseBody) =>
            bc drop typeToDrop
            genLoad(caseBody, kind) // adapts caseBody to `kind`, thus it can be stored, if `guardResult`, in `tmp`.
            nopIfNeeded(startHandler)
            endHandler = currProgramPoint()
            excType = typeToDrop

          case BoundEH   (patSymbol,  caseBody) =>
            // test/files/run/contrib674.scala , a local-var already exists for patSymbol.
            // rather than creating on first-access, we do it right away to emit debug-info for the created local var.
            val Local(patTK, _, patIdx, _) = locals.getOrMakeLocal(patSymbol)
            bc.store(patIdx, patTK)
            genLoad(caseBody, kind)
            nopIfNeeded(startHandler)
            endHandler = currProgramPoint()
            emitLocalVarScope(patSymbol, startHandler, endHandler)
            excType = patTK.asClassBType
        }
        unregisterCleanup(finCleanup)
        // (2.b)  mark the try-body as protected by this case clause.
        protect(startTryBody, endTryBody, startHandler, excType)
        // (2.c) emit jump to the program point where the finally-clause-for-normal-exit starts, or in effect `after` if no finally-clause was given.
        bc goTo postHandlers

      }

      /* ------ (3.A) The exception-handler-version of the finally-clause.
       *              Reached upon abrupt termination of (1) or one of the EHs in (2).
       *              Protected only by whatever protects the whole try-catch-finally expression.
       * ------
       */

      // a note on terminology: this is not "postHandlers", despite appearances.
      // "postHandlers" as in the source-code view. And from that perspective, both (3.A) and (3.B) are invisible implementation artifacts.
      if (hasFinally) {
        nopIfNeeded(startTryBody)
        val finalHandler = currProgramPoint() // version of the finally-clause reached via unhandled exception.
        protect(startTryBody, finalHandler, finalHandler, null)
        val Local(eTK, _, eIdx, _) = locals(locals.makeLocal(ThrowableReference, "exc"))
        bc.store(eIdx, eTK)
        emitFinalizer(finalizer, null, isDuplicate = true)
        bc.load(eIdx, eTK)
        emit(asm.Opcodes.ATHROW)
      }

      /* ------ (3.B) Cleanup-version of the finally-clause.
       *              Reached upon early RETURN from (1) or upon early RETURN from one of the EHs in (2)
       *              (and only from there, ie reached only upon early RETURN from
       *               program regions bracketed by registerCleanup/unregisterCleanup).
       *              Protected only by whatever protects the whole try-catch-finally expression.
       *
       *              Given that control arrives to a cleanup section only upon early RETURN,
       *              the value to return (if any) is always available. Therefore, a further RETURN
       *              found in a cleanup section is always ignored (a warning is displayed, @see `genReturn()`).
       *              In order for `genReturn()` to know whether the return statement is enclosed in a cleanup section,
       *              the variable `insideCleanupBlock` is used.
       * ------
       */

      // this is not "postHandlers" either.
      // `shouldEmitCleanup` can be set, and at the same time this try expression may lack a finally-clause.
      // In other words, all combinations of (hasFinally, shouldEmitCleanup) are valid.
      if (hasFinally && shouldEmitCleanup) {
        val savedInsideCleanup = insideCleanupBlock
        insideCleanupBlock = true
        markProgramPoint(finCleanup)
        // regarding return value, the protocol is: in place of a `return-stmt`, a sequence of `adapt, store, jump` are inserted.
        emitFinalizer(finalizer, null, isDuplicate = true)
        pendingCleanups()
        insideCleanupBlock = savedInsideCleanup
      }

      /* ------ (4) finally-clause-for-normal-nonEarlyReturn-exit
       *            Reached upon normal, non-early-return termination of (1) or of an EH in (2).
       *            Protected only by whatever protects the whole try-catch-finally expression.
       * TODO explain what happens upon RETURN contained in (4)
       * ------
       */

      markProgramPoint(postHandlers)
      if (hasFinally) {
        emitFinalizer(finalizer, tmp, isDuplicate = false) // the only invocation of emitFinalizer with `isDuplicate == false`
      }

      kind
    } // end of genLoadTry()

    /* if no more pending cleanups, all that remains to do is return. Otherwise jump to the next (outer) pending cleanup. */
    private def pendingCleanups() {
      cleanups match {
        case Nil =>
          if (earlyReturnVar != null) {
            locals.load(earlyReturnVar)
            bc.emitRETURN(locals(earlyReturnVar).tk)
          } else {
            bc emitRETURN UNIT
          }
          shouldEmitCleanup = false

        case nextCleanup :: _ =>
          bc goTo nextCleanup
      }
    }

    def protect(start: asm.Label, end: asm.Label, handler: asm.Label, excType: ClassBType) {
      val excInternalName: String =
        if (excType == null) null
        else excType.internalName
      assert(start != end, "protecting a range of zero instructions leads to illegal class format. Solution: add a NOP to that range.")
      mnode.visitTryCatchBlock(start, end, handler, excInternalName)
    }

    /* `tmp` (if non-null) is the symbol of the local-var used to preserve the result of the try-body, see `guardResult` */
    def emitFinalizer(finalizer: Tree, tmp: Symbol, isDuplicate: Boolean) {
      var saved: immutable.Map[ /* LabelDef */ Symbol, asm.Label ] = null
      if (isDuplicate) {
        saved = jumpDest
        for(ldef <- labelDefsAtOrUnder(finalizer)) {
          jumpDest -= ldef.symbol
        }
      }
      // when duplicating, the above guarantees new asm.Labels are used for LabelDefs contained in the finalizer (their vars are reused, that's ok)
      if (tmp != null) { locals.store(tmp) }
      genLoad(finalizer, UNIT)
      if (tmp != null) { locals.load(tmp)  }
      if (isDuplicate) {
        jumpDest = saved
      }
    }

    /* Does this tree have a try-catch block? */
    def mayCleanStack(tree: Tree): Boolean = tree exists { t => t.isInstanceOf[Try] }

    trait EHClause
    case class NamelessEH(typeToDrop: ClassBType,  caseBody: Tree) extends EHClause
    case class BoundEH    (patSymbol: Symbol, caseBody: Tree) extends EHClause

  }

}