summaryrefslogtreecommitdiff
path: root/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala
blob: 9c22b09cdd2447e5d1d4c7e21cb68617b618504a (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
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
/* NSC -- new Scala compiler
 * Copyright 2005-2014 LAMP/EPFL
 * @author  Martin Odersky
 */

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

import scala.annotation.{tailrec, switch}

import scala.tools.asm.Type
import scala.tools.asm.tree.analysis.Frame
import scala.tools.asm.Opcodes._
import scala.tools.asm.tree._
import scala.collection.mutable
import scala.collection.JavaConverters._
import scala.tools.nsc.backend.jvm.BTypes.InternalName
import scala.tools.nsc.backend.jvm.analysis._
import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._

/**
 * Optimizations within a single method. Certain optimizations enable others, for example removing
 * unreachable code can render a `try` block empty and enable removeEmptyExceptionHandlers. The
 * latter in turn enables more unreachable code to be eliminated (the `catch` block), so there is
 * a cyclic dependency. Optimizations that depend on each other are therefore executed in a loop
 * until reaching a fixpoint.
 *
 * The optimizations marked UPSTREAM enable optimizations that were already executed, so they cause
 * another iteration in the fixpoint loop.
 *
 * nullness optimizations: rewrite null-checking branches to GOTO if nullness is known
 *   + enables downstream
 *     - unreachable code (null / non-null branch becomes unreachable)
 *     - box-unbox elimination (may render an escaping consumer of a box unreachable)
 *     - stale stores (aload x is replaced by aconst_null if it's known null)
 *     - simplify jumps (replaces conditional jumps by goto, so may enable goto chains)
 *
 * unreachable code / DCE (removes instructions of basic blocks to which there is no branch)
 *   + enables downstream:
 *     - stale stores (loads may be eliminated, removing consumers of a store)
 *     - empty handlers (try blocks may become empty)
 *     - simplify jumps (goto l; [dead code]; l: ..) => remove goto
 *     - stale local variable descriptors
 *     - (not box-unbox, which is implemented using prod-cons, so it doesn't consider dead code)
 *
 *   note that eliminating empty handlers and stale local variable descriptors is required for
 *   correctness, see the comment in the body of `methodOptimizations`.
 *
 * box-unbox elimination (eliminates box-unbox pairs within the same method)
 *   + enables UPSTREAM:
 *     - nullness optimizations (a box extraction operation (unknown nullness) may be rewritten to
 *       a read of a non-null local. example in doc comment of box-unbox implementation)
 *     - further box-unbox elimination (e.g. an Integer stored in a Tuple; eliminating the tuple may
 *       enable eliminating the Integer)
 *   + enables downstream:
 *     - copy propagation (new locals are introduced, may be aliases of existing)
 *     - stale stores (multi-value boxes where not all values are used)
 *     - redundant casts (`("a", "b")._1`: the generic `_1` method returns `Object`, a cast
 *       to String is added. The cast is redundant after eliminating the tuple.)
 *     - empty local variable descriptors (local variables that were holding the box may become unused)
 *
 * copy propagation (replaces LOAD n to the LOAD m for the smallest m that is an alias of n)
 *   + enables downstream:
 *     - stale stores (a stored value may not be loaded anymore)
 *     - store-load pairs (a load n may now be right after a store n)
 *   + NOTE: copy propagation is only executed once, in the first fixpoint loop iteration. none of
 *     the other optimizations enables further copy prop. we still run it as part of the loop
 *     because it requires unreachable code to be eliminated.
 *
 * stale stores (replace STORE by POP)
 *   + enables downstream:
 *     - push-pop (the new pop may be the single consumer for an instruction)
 *
 * redundant casts: eliminates casts that are statically known to succeed (uses type propagation)
 *   + enables UPSTREAM:
 *     - box-unbox elimination (a removed checkcast may be a box consumer)
 *   + enables downstream:
 *     - push-pop for closure allocation elimination (every indyLambda is followed by a checkcast, see SI-9540)
 *
 * push-pop (when a POP is the only consumer of a value, remove the POP and its producer)
 *   + enables UPSTREAM:
 *     - stale stores (if a LOAD is removed, a corresponding STORE may become stale)
 *     - box-unbox elimination (push-pop may eliminate a closure allocation, rendering a captured
 *       box non-escaping)
 *   + enables downstream:
 *     - store-load pairs (a variable may become non-live)
 *     - stale handlers (push-pop removes code)
 *     - simplify jumps (push-pop removes code)
 *
 * store-load pairs (remove `STORE x; LOAD x` if x is otherwise not used in the method)
 *   + enables downstream:
 *     - empty handlers (code is removes, a try block may become empty
 *     - simplify jumps (code is removed, a goto may become redundant for example)
 *     - stale local variable descriptors
 *
 * empty handlers (removes exception handlers whose try block is empty)
 *   + enables UPSTREAM:
 *     - unreachable code (catch block becomes unreachable)
 *     - box-unbox (a box may be escape in an operation in a dead handler)
 *   + enables downstream:
 *     - simplify jumps
 *
 * simplify jumps (various, like `GOTO l; l: ...`, see doc comments of individual optimizations)
 *   + enables UPSTREAM
 *     - unreachable code (`GOTO a; a: GOTO b; b: ...`, the first jump is changed to `GOTO b`, the second becomes unreachable)
 *     - store-load pairs (a `GOTO l; l: ...` is removed between store and load)
 *     - push-pop (`IFNULL l; l: ...` is replaced by `POP`)
 *
 *
 * The following cleanup optimizations don't enable any upstream optimizations, so they can be
 * executed once at the end, when the above optimizations reach a fixpoint.
 *
 *
 * empty local variable descriptors (removes unused variables from the local variable table)
 *   + enables downstream:
 *     - stale labels (labels that the entry points to, if not otherwise referenced)
 *
 * empty line numbers (eliminates line number nodes that describe no executable instructions)
 *   + enables downstream:
 *     - stale labels (label of the line number node, if not otherwise referenced)
 *
 * stale labels (eliminate labels that are not referenced, merge sequences of label definitions)
 *
 *
 * Note on a method's maxLocals / maxStack: the backend only uses those values for running
 * Analyzers. The values can be conservative approximations: if an optimization removes code and
 * the maximal stack size is now smaller, the larger maxStack value will still work fine for
 * running an Analyzer (just that frames allocate more space than required). The correct max
 * values written to the bytecode are re-computed during classfile serialization.
 * To keep things simpler, we don't update the max values in every optimization:
 *   - we do it in `removeUnreachableCodeImpl`, because it's quite straightforward
 *   - maxLocals is updated in `compactLocalVariables`, which runs at the end of method optimizations
 *
 *
 * Note on updating the call graph: whenever an optimization eliminates a callsite or a closure
 * instantiation, we eliminate the corresponding entry from the call graph.
 */
class LocalOpt[BT <: BTypes](val btypes: BT) {
  import LocalOptImpls._
  import btypes._
  import coreBTypes._
  import backendUtils._

  val boxUnbox = new BoxUnbox(btypes)
  import boxUnbox._

  val copyProp = new CopyProp(btypes)
  import copyProp._

  /**
   * Remove unreachable code from a method.
   *
   * This implementation only removes instructions that are unreachable for an ASM analyzer /
   * interpreter. This ensures that future analyses will not produce `null` frames. The inliner
   * depends on this property.
   *
   * @return A set containing the eliminated instructions
   */
  def minimalRemoveUnreachableCode(method: MethodNode, ownerClassName: InternalName): Boolean = {
    // In principle, for the inliner, a single removeUnreachableCodeImpl would be enough. But that
    // would potentially leave behind stale handlers (empty try block) which is not legal in the
    // classfile. So we run both removeUnreachableCodeImpl and removeEmptyExceptionHandlers.
    if (method.instructions.size == 0) return false     // fast path for abstract methods
    if (unreachableCodeEliminated(method)) return false // we know there is no unreachable code
    if (!AsmAnalyzer.sizeOKForBasicValue(method)) return false // the method is too large for running an analyzer

    // For correctness, after removing unreachable code, we have to eliminate empty exception
    // handlers, see scaladoc of def methodOptimizations. Removing an live handler may render more
    // code unreachable and therefore requires running another round.
    def removalRound(): Boolean = {
      val (insnsRemoved, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName)
      if (insnsRemoved) {
        val liveHandlerRemoved = removeEmptyExceptionHandlers(method).exists(h => liveLabels(h.start))
        if (liveHandlerRemoved) removalRound()
      }
      insnsRemoved
    }

    val changed = removalRound()
    if (changed) removeUnusedLocalVariableNodes(method)()
    unreachableCodeEliminated += method
    changed
  }

  /**
   * Remove unreachable instructions from all (non-abstract) methods and apply various other
   * cleanups to the bytecode.
   *
   * @param clazz The class whose methods are optimized
   * @return      `true` if unreachable code was eliminated in some method, `false` otherwise.
   */
  def methodOptimizations(clazz: ClassNode): Boolean = {
    !compilerSettings.optNone && clazz.methods.asScala.foldLeft(false) {
      case (changed, method) => methodOptimizations(method, clazz.name) || changed
    }
  }

  /**
   * Run method-level optimizations, see comment on class [[LocalOpt]].
   *
   * Returns `true` if the bytecode of `method` was changed.
   */
  def methodOptimizations(method: MethodNode, ownerClassName: InternalName): Boolean = {
    if (method.instructions.size == 0) return false // fast path for abstract methods

    // unreachable-code also removes unused local variable nodes and empty exception handlers.
    // This is required for correctness, for example:
    //
    //   def f = { return 0; try { 1 } catch { case _ => 2 } }
    //
    // The result after removeUnreachableCodeImpl:
    //
    //   TRYCATCHBLOCK L0 L1 L2 java/lang/Exception
    //   L4
    //     ICONST_0
    //     IRETURN
    //   L0
    //   L1
    //   L2
    //
    // If we don't eliminate the handler, the ClassWriter emits:
    //
    //   TRYCATCHBLOCK L0 L0 L0 java/lang/Exception
    //   L1
    //     ICONST_0
    //     IRETURN
    //   L0
    //
    // This triggers "ClassFormatError: Illegal exception table range in class file C". Similar
    // for local variables in dead blocks. Maybe that's a bug in the ASM framework.

    var currentTrace: String = null
    val methodPrefix = {val p = compilerSettings.YoptTrace.value; if (p == "_") "" else p }
    val doTrace = compilerSettings.YoptTrace.isSetByUser && s"$ownerClassName.${method.name}".startsWith(methodPrefix)
    def traceIfChanged(optName: String): Unit = if (doTrace) {
      val after = AsmUtils.textify(method)
      if (currentTrace != after) {
        println(s"after $optName")
        println(after)
      }
      currentTrace = after
    }

    /**
     * Runs the optimizations that depend on each other in a loop until reaching a fixpoint. See
     * comment in class [[LocalOpt]].
     *
     * Returns a pair of booleans (codeChanged, requireEliminateUnusedLocals).
     */
    def removalRound(
        requestNullness: Boolean,
        requestDCE: Boolean,
        requestBoxUnbox: Boolean,
        requestStaleStores: Boolean,
        requestPushPop: Boolean,
        requestStoreLoad: Boolean,
        firstIteration: Boolean,
        maxRecursion: Int = 10): (Boolean, Boolean) = {
      if (maxRecursion == 0) return (false, false)

      traceIfChanged("beforeMethodOpt")

      // NULLNESS OPTIMIZATIONS
      val runNullness = compilerSettings.optNullnessTracking && requestNullness
      val nullnessOptChanged = runNullness && nullnessOptimizations(method, ownerClassName)
      traceIfChanged("nullness")

      // UNREACHABLE CODE
      // Both AliasingAnalyzer (used in copyProp) and ProdConsAnalyzer (used in eliminateStaleStores,
      // boxUnboxElimination) require not having unreachable instructions (null frames).
      val runDCE = (compilerSettings.optUnreachableCode && (requestDCE || nullnessOptChanged)) ||
        compilerSettings.optBoxUnbox ||
        compilerSettings.optCopyPropagation
      val (codeRemoved, liveLabels) = if (runDCE) removeUnreachableCodeImpl(method, ownerClassName) else (false, Set.empty[LabelNode])
      traceIfChanged("dce")

      // BOX-UNBOX
      val runBoxUnbox = compilerSettings.optBoxUnbox && (requestBoxUnbox || nullnessOptChanged)
      val boxUnboxChanged = runBoxUnbox && boxUnboxElimination(method, ownerClassName)
      traceIfChanged("boxUnbox")

      // COPY PROPAGATION
      val runCopyProp = compilerSettings.optCopyPropagation && (firstIteration || boxUnboxChanged)
      val copyPropChanged = runCopyProp && copyPropagation(method, ownerClassName)
      traceIfChanged("copyProp")

      // STALE STORES
      val runStaleStores = compilerSettings.optCopyPropagation && (requestStaleStores || nullnessOptChanged || codeRemoved || boxUnboxChanged || copyPropChanged)
      val storesRemoved = runStaleStores && eliminateStaleStores(method, ownerClassName)
      traceIfChanged("staleStores")

      // REDUNDANT CASTS
      val runRedundantCasts = compilerSettings.optRedundantCasts && (firstIteration || boxUnboxChanged)
      val castRemoved = runRedundantCasts && eliminateRedundantCasts(method, ownerClassName)
      traceIfChanged("redundantCasts")

      // PUSH-POP
      val runPushPop = compilerSettings.optCopyPropagation && (requestPushPop || firstIteration || storesRemoved || castRemoved)
      val pushPopRemoved = runPushPop && eliminatePushPop(method, ownerClassName)
      traceIfChanged("pushPop")

      // STORE-LOAD PAIRS
      val runStoreLoad = compilerSettings.optCopyPropagation && (requestStoreLoad || boxUnboxChanged || copyPropChanged || pushPopRemoved)
      val storeLoadRemoved = runStoreLoad && eliminateStoreLoad(method)
      traceIfChanged("storeLoadPairs")

      // STALE HANDLERS
      val removedHandlers = if (runDCE) removeEmptyExceptionHandlers(method) else Set.empty[TryCatchBlockNode]
      val handlersRemoved = removedHandlers.nonEmpty
      val liveHandlerRemoved = removedHandlers.exists(h => liveLabels(h.start))
      traceIfChanged("staleHandlers")

      // SIMPLIFY JUMPS
      // almost all of the above optimizations enable simplifying more jumps, so we just run it in every iteration
      val runSimplifyJumps = compilerSettings.optSimplifyJumps
      val jumpsChanged = runSimplifyJumps && simplifyJumps(method)
      traceIfChanged("simplifyJumps")

      // See doc comment in the beginning of this file (optimizations marked UPSTREAM)
      val runNullnessAgain = boxUnboxChanged
      val runDCEAgain = liveHandlerRemoved || jumpsChanged
      val runBoxUnboxAgain = boxUnboxChanged || castRemoved || pushPopRemoved || liveHandlerRemoved
      val runStaleStoresAgain = pushPopRemoved
      val runPushPopAgain = jumpsChanged
      val runStoreLoadAgain = jumpsChanged
      val runAgain = runNullnessAgain || runDCEAgain || runBoxUnboxAgain || pushPopRemoved || runStaleStoresAgain || runPushPopAgain || runStoreLoadAgain

      val downstreamRequireEliminateUnusedLocals = runAgain && removalRound(
        requestNullness = runNullnessAgain,
        requestDCE = runDCEAgain,
        requestBoxUnbox = runBoxUnboxAgain,
        requestStaleStores = runStaleStoresAgain,
        requestPushPop = runPushPopAgain,
        requestStoreLoad = runStoreLoadAgain,
        firstIteration = false,
        maxRecursion = maxRecursion - 1)._2

      val requireEliminateUnusedLocals = downstreamRequireEliminateUnusedLocals ||
        nullnessOptChanged || // nullness opt may eliminate stores / loads, rendering a local unused
        codeRemoved ||        // see comment in method `methodOptimizations`
        boxUnboxChanged ||    // box-unbox renders locals (holding boxes) unused
        storesRemoved  ||
        storeLoadRemoved ||
        handlersRemoved

      val codeChanged = nullnessOptChanged || codeRemoved || boxUnboxChanged || castRemoved || copyPropChanged || storesRemoved || pushPopRemoved || storeLoadRemoved || handlersRemoved || jumpsChanged
      (codeChanged, requireEliminateUnusedLocals)
    }

    val (nullnessDceBoxesCastsCopypropPushpopOrJumpsChanged, requireEliminateUnusedLocals) = if (AsmAnalyzer.sizeOKForBasicValue(method)) {
      // we run DCE even if the method is already in the `unreachableCodeEliminated` map: the DCE
      // here is more thorough than `minimalRemoveUnreachableCode` that run before inlining.
      val r = removalRound(
        requestNullness = true,
        requestDCE = true,
        requestBoxUnbox = true,
        requestStaleStores = true,
        requestPushPop = true,
        requestStoreLoad = true,
        firstIteration = true)
      if (compilerSettings.optUnreachableCode) unreachableCodeEliminated += method
      r
    } else (false, false)

    // (*) Removing stale local variable descriptors is required for correctness, see comment in `methodOptimizations`
    val localsRemoved =
      if (compilerSettings.optCompactLocals) compactLocalVariables(method) // also removes unused
      else if (requireEliminateUnusedLocals) removeUnusedLocalVariableNodes(method)() // (*)
      else false
    traceIfChanged("localVariables")

    val lineNumbersRemoved = if (compilerSettings.optUnreachableCode) removeEmptyLineNumbers(method) else false
    traceIfChanged("lineNumbers")

    val labelsRemoved = if (compilerSettings.optUnreachableCode) removeEmptyLabelNodes(method) else false
    traceIfChanged("labels")

    // assert that local variable annotations are empty (we don't emit them) - otherwise we'd have
    // to eliminate those covering an empty range, similar to removeUnusedLocalVariableNodes.
    def nullOrEmpty[T](l: java.util.List[T]) = l == null || l.isEmpty
    assert(nullOrEmpty(method.visibleLocalVariableAnnotations), method.visibleLocalVariableAnnotations)
    assert(nullOrEmpty(method.invisibleLocalVariableAnnotations), method.invisibleLocalVariableAnnotations)

    nullnessDceBoxesCastsCopypropPushpopOrJumpsChanged || localsRemoved || lineNumbersRemoved || labelsRemoved
  }

  /**
   * Apply various optimizations based on nullness analysis information.
   *   - IFNULL / IFNONNULL are rewritten to GOTO if nullness is known
   *   - IF_ACMPEQ / IF_ACMPNE are rewritten to GOTO if the both references are known null, or if
   *     one is known null and the other known not-null
   *   - ALOAD is replaced by ACONST_NULL if the local is known to hold null
   *   - ASTORE of null is removed if the local is known to hold null
   *   - INSTANCEOF of null is replaced by `ICONST_0`
   *   - scala.runtime.BoxesRunTime.unboxToX(null) is rewritten to a zero-value load
   */
  def nullnessOptimizations(method: MethodNode, ownerClassName: InternalName): Boolean = {
    AsmAnalyzer.sizeOKForNullness(method) && {
      lazy val nullnessAnalyzer = new AsmAnalyzer(method, ownerClassName, new NullnessAnalyzer(btypes, method))

      // When running nullness optimizations the method may still have unreachable code. Analyzer
      // frames of unreachable instructions are `null`.
      def frameAt(insn: AbstractInsnNode): Option[Frame[NullnessValue]] = Option(nullnessAnalyzer.frameAt(insn))

      def nullness(insn: AbstractInsnNode, slot: Int): Option[NullnessValue] = {
        frameAt(insn).map(_.getValue(slot))
      }

      def isNull(insn: AbstractInsnNode, slot: Int) = nullness(insn, slot).contains(NullValue)

      // cannot change instructions while iterating, it gets the analysis out of synch (indexed by instructions)
      val toReplace = mutable.Map.empty[AbstractInsnNode, List[AbstractInsnNode]]

      val it = method.instructions.iterator()
      while (it.hasNext) it.next() match {
        case vi: VarInsnNode if isNull(vi, vi.`var`) =>
          if (vi.getOpcode == ALOAD)
            toReplace(vi) = List(new InsnNode(ACONST_NULL))
          else if (vi.getOpcode == ASTORE)
            for (frame <- frameAt(vi) if frame.peekStack(0) == NullValue)
              toReplace(vi) = List(getPop(1))

        case ji: JumpInsnNode =>
          val isIfNull = ji.getOpcode == IFNULL
          val isIfNonNull = ji.getOpcode == IFNONNULL
          if (isIfNull || isIfNonNull) for (frame <- frameAt(ji)) {
            val nullness = frame.peekStack(0)
            val taken = nullness == NullValue && isIfNull || nullness == NotNullValue && isIfNonNull
            val avoided = nullness == NotNullValue && isIfNull || nullness == NullValue && isIfNonNull
            if (taken || avoided) {
              val jump = if (taken) List(new JumpInsnNode(GOTO, ji.label)) else Nil
              toReplace(ji) = getPop(1) :: jump
            }
          } else {
            val isIfEq = ji.getOpcode == IF_ACMPEQ
            val isIfNe = ji.getOpcode == IF_ACMPNE
            if (isIfEq || isIfNe) for (frame <- frameAt(ji)) {
              val aNullness = frame.peekStack(1)
              val bNullness = frame.peekStack(0)
              val eq = aNullness == NullValue && bNullness == NullValue
              val ne = aNullness == NullValue && bNullness == NotNullValue || aNullness == NotNullValue && bNullness == NullValue
              val taken = isIfEq && eq || isIfNe && ne
              val avoided = isIfEq && ne || isIfNe && eq
              if (taken || avoided) {
                val jump = if (taken) List(new JumpInsnNode(GOTO, ji.label)) else Nil
                toReplace(ji) = getPop(1) :: getPop(1) :: jump
              }
            }
          }

        case ti: TypeInsnNode =>
          if (ti.getOpcode == INSTANCEOF) for (frame <- frameAt(ti) if frame.peekStack(0) == NullValue) {
            toReplace(ti) = List(getPop(1), new InsnNode(ICONST_0))
          }

        case mi: MethodInsnNode =>
          if (isScalaUnbox(mi)) for (frame <- frameAt(mi) if frame.peekStack(0) == NullValue) {
            toReplace(mi) = List(
              getPop(1),
              loadZeroForTypeSort(Type.getReturnType(mi.desc).getSort))
          }

        case _ =>
      }

      def removeFromCallGraph(insn: AbstractInsnNode): Unit = insn match {
        case mi: MethodInsnNode => callGraph.removeCallsite(mi, method)
        case _ =>
      }

      for ((oldOp, newOps) <- toReplace) {
        for (newOp <- newOps) method.instructions.insertBefore(oldOp, newOp)
        method.instructions.remove(oldOp)
        removeFromCallGraph(oldOp)
      }

      toReplace.nonEmpty
    }
  }

  /**
   * Removes unreachable basic blocks.
   *
   * @return A set containing eliminated instructions, and a set containing all live label nodes.
   */
  def removeUnreachableCodeImpl(method: MethodNode, ownerClassName: InternalName): (Boolean, Set[LabelNode]) = {
    val a = new AsmAnalyzer(method, ownerClassName)
    val frames = a.analyzer.getFrames

    var i = 0
    var liveLabels = Set.empty[LabelNode]
    var changed = false
    var maxLocals = parametersSize(method)
    var maxStack = 0
    val itr = method.instructions.iterator()
    while (itr.hasNext) {
      val insn = itr.next()
      val isLive = frames(i) != null
      if (isLive) maxStack = math.max(maxStack, frames(i).getStackSize)

      insn match {
        case l: LabelNode =>
          // label nodes are not removed: they might be referenced for example in a LocalVariableNode
          if (isLive) liveLabels += l

        case v: VarInsnNode if isLive =>
          val longSize = if (isSize2LoadOrStore(v.getOpcode)) 1 else 0
          maxLocals = math.max(maxLocals, v.`var` + longSize + 1) // + 1 because local numbers are 0-based

        case i: IincInsnNode if isLive =>
          maxLocals = math.max(maxLocals, i.`var` + 1)

        case _ =>
          if (!isLive || insn.getOpcode == NOP) {
            // Instruction iterators allow removing during iteration.
            // Removing is O(1): instructions are doubly linked list elements.
            itr.remove()
            changed = true
            insn match {
              case invocation: MethodInsnNode => callGraph.removeCallsite(invocation, method)
              case indy: InvokeDynamicInsnNode => callGraph.removeClosureInstantiation(indy, method)
              case _ =>
            }
          }
      }
      i += 1
    }
    method.maxLocals = maxLocals
    method.maxStack  = maxStack
    (changed, liveLabels)
  }

  /**
   * Eliminate `CHECKCAST` instructions that are statically known to succeed. This is safe if the
   * tested object is null: `null.asInstanceOf` always succeeds.
   *
   * The type of the tested object is determined using a NonLubbingTypeFlowAnalyzer. Note that this
   * analysis collapses LUBs of non-equal references types to Object for simplicity. Example:
   * given `B <: A <: Object`, the cast in `(if (..) new B else new A).asInstanceOf[A]` would not
   * be eliminated.
   *
   * Note: we cannot replace `INSTANCEOF` tests by only looking at the types, `null.isInstanceOf`
   * always returns false, so we'd also need nullness information.
   */
  def eliminateRedundantCasts(method: MethodNode, owner: InternalName): Boolean = {
    AsmAnalyzer.sizeOKForBasicValue(method) && {
      def isSubType(aRefDesc: String, bClass: InternalName): Boolean = aRefDesc == bClass || bClass == ObjectRef.internalName || {
        (bTypeForDescriptorOrInternalNameFromClassfile(aRefDesc) conformsTo classBTypeFromParsedClassfile(bClass)).getOrElse(false)
      }

      lazy val typeAnalyzer = new NonLubbingTypeFlowAnalyzer(method, owner)

      // cannot remove instructions while iterating, it gets the analysis out of synch (indexed by instructions)
      val toRemove = mutable.Set.empty[TypeInsnNode]

      val it = method.instructions.iterator()
      while (it.hasNext) it.next() match {
        case ti: TypeInsnNode if ti.getOpcode == CHECKCAST =>
          val frame = typeAnalyzer.frameAt(ti)
          val valueTp = frame.getValue(frame.stackTop)
          if (valueTp.isReference && isSubType(valueTp.getType.getDescriptor, ti.desc)) {
            toRemove += ti
          }

        case _ =>
      }

      toRemove foreach method.instructions.remove
      toRemove.nonEmpty
    }
  }
}

object LocalOptImpls {
  /**
   * Remove exception handlers that cover empty code blocks. A block is considered empty if it
   * consist only of labels, frames, line numbers, nops and gotos.
   *
   * There are no executable instructions that we can assume don't throw (eg ILOAD). The JVM spec
   * basically says that a VirtualMachineError may be thrown at any time:
   *   http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.3
   *
   * Note that no instructions are eliminated.
   *
   * @return the set of removed handlers
   */
  def removeEmptyExceptionHandlers(method: MethodNode): Set[TryCatchBlockNode] = {
    /** True if there exists code between start and end. */
    def containsExecutableCode(start: AbstractInsnNode, end: LabelNode): Boolean = {
      start != end && ((start.getOpcode: @switch) match {
        // FrameNode, LabelNode and LineNumberNode have opcode == -1.
        case -1 | GOTO => containsExecutableCode(start.getNext, end)
        case _ => true
      })
    }

    var removedHandlers = Set.empty[TryCatchBlockNode]
    val handlersIter = method.tryCatchBlocks.iterator()
    while (handlersIter.hasNext) {
      val handler = handlersIter.next()
      if (!containsExecutableCode(handler.start, handler.end)) {
        removedHandlers += handler
        handlersIter.remove()
      }
    }
    removedHandlers
  }

  /**
   * Remove all non-parameter entries from the local variable table which denote variables that are
   * not actually read or written.
   *
   * Note that each entry in the local variable table has a start, end and index. Two entries with
   * the same index, but distinct start / end ranges are different variables, they may have not the
   * same type or name.
   */
  def removeUnusedLocalVariableNodes(method: MethodNode)(firstLocalIndex: Int = parametersSize(method), renumber: Int => Int = identity): Boolean = {
    @tailrec def variableIsUsed(start: AbstractInsnNode, end: LabelNode, varIndex: Int): Boolean = {
      start != end && (start match {
        case v: VarInsnNode if v.`var` == varIndex => true
        case i: IincInsnNode if i.`var` == varIndex => true
        case _ => variableIsUsed(start.getNext, end, varIndex)
      })
    }

    val initialNumVars = method.localVariables.size
    val localsIter = method.localVariables.iterator()
    while (localsIter.hasNext) {
      val local = localsIter.next()
      val index = local.index
      // parameters and `this` (the lowest indices, starting at 0) are never removed or renumbered
      if (index >= firstLocalIndex) {
        if (!variableIsUsed(local.start, local.end, index)) localsIter.remove()
        else if (renumber(index) != index) local.index = renumber(index)
      }
    }
    method.localVariables.size != initialNumVars
  }

  /**
   * Compact the local variable slots used in the method's implementation. This prevents having
   * unused slots for example after eliminating unreachable code.
   *
   * This transformation reduces the size of the frame for invoking the method. For example, if the
   * method has an ISTORE instruction to the local variable 3, the maxLocals of the method is at
   * least 4, even if some local variable slots below 3 are not used by any instruction.
   *
   * This could be improved by doing proper register allocation.
   */
  def compactLocalVariables(method: MethodNode): Boolean = {
    // This array is built up to map local variable indices from old to new.
    val renumber = collection.mutable.ArrayBuffer.empty[Int]

    // Add the index of the local variable used by `varIns` to the `renumber` array.
    def addVar(varIns: AbstractInsnNode, slot: Int): Unit = {
      val index = slot
      val isWide = isSize2LoadOrStore(varIns.getOpcode)

      // Ensure the length of `renumber`. Unused variable indices are mapped to -1.
      val minLength = if (isWide) index + 2 else index + 1
      for (i <- renumber.length until minLength) renumber += -1

      renumber(index) = index
      if (isWide) renumber(index + 1) = index
    }

    // first phase: collect all used local variables. if the variable at index x is used, set
    // renumber(x) = x, otherwise renumber(x) = -1. if the variable is wide (long or double), set
    // renumber(x+1) = x.

    val firstLocalIndex = parametersSize(method)
    for (i <- 0 until firstLocalIndex) renumber += i // parameters and `this` are always used.
    method.instructions.iterator().asScala foreach {
      case VarInstruction(varIns, slot) => addVar(varIns, slot)
      case _ =>
    }

    // assign the next free slot to each used local variable.
    // for example, rewrite (0, 1, -1, 3, -1, 5) to (0, 1, -1, 2, -1, 3).

    var nextIndex = firstLocalIndex
    for (i <- firstLocalIndex until renumber.length if renumber(i) != -1) {
      renumber(i) = nextIndex
      nextIndex += 1
    }

    // Update the local variable descriptors according to the renumber table, and eliminate stale entries
    val removedLocalVariableDescriptors = removeUnusedLocalVariableNodes(method)(firstLocalIndex, renumber)

    if (nextIndex == renumber.length) removedLocalVariableDescriptors
    else {
      // update variable instructions according to the renumber table
      method.maxLocals = nextIndex
      method.instructions.iterator().asScala.foreach {
        case VarInstruction(varIns, slot) =>
          val oldIndex = slot
          if (oldIndex >= firstLocalIndex && renumber(oldIndex) != oldIndex) varIns match {
            case vi: VarInsnNode => vi.`var` = renumber(slot)
            case ii: IincInsnNode => ii.`var` = renumber(slot)
          }
        case _ =>
      }
      true
    }
  }

  /**
   * Removes LineNumberNodes that don't describe any executable instructions.
   *
   * This method expects (and asserts) that the `start` label of each LineNumberNode is the
   * lexically preceding label declaration.
   */
  def removeEmptyLineNumbers(method: MethodNode): Boolean = {
    def isEmpty(node: AbstractInsnNode): Boolean = node.getNext match {
      case null => true
      case l: LineNumberNode => true
      case n if n.getOpcode >= 0 => false
      case n => isEmpty(n)
    }

    val initialSize = method.instructions.size
    val iterator = method.instructions.iterator()
    var previousLabel: LabelNode = null
    while (iterator.hasNext) {
      iterator.next match {
        case label: LabelNode => previousLabel = label
        case line: LineNumberNode if isEmpty(line) =>
          assert(line.start == previousLabel)
          iterator.remove()
        case _ =>
      }
    }
    method.instructions.size != initialSize
  }

  /**
   * Removes unreferenced label declarations, also squashes sequences of label definitions.
   *
   *      [ops];              Label(a); Label(b);  [ops];
   *   => subs([ops], b, a);  Label(a);            subs([ops], b, a);
   */
  def removeEmptyLabelNodes(method: MethodNode): Boolean = {
    val references = labelReferences(method)

    val initialSize = method.instructions.size
    val iterator = method.instructions.iterator()
    var prev: LabelNode = null
    while (iterator.hasNext) {
      iterator.next match {
        case label: LabelNode =>
          if (!references.contains(label)) iterator.remove()
          else if (prev != null) {
            references(label).foreach(substituteLabel(_, label, prev))
            iterator.remove()
          } else prev = label

        case instruction =>
          if (instruction.getOpcode >= 0) prev = null
      }
    }
    method.instructions.size != initialSize
  }

  /**
   * Apply various simplifications to branching instructions.
   */
  def simplifyJumps(method: MethodNode): Boolean = {
    var changed = false

    val allHandlers = method.tryCatchBlocks.asScala.toSet

    // A set of all exception handlers that guard the current instruction, required for simplifyGotoReturn
    var activeHandlers = Set.empty[TryCatchBlockNode]

    val jumpInsns = mutable.LinkedHashMap.empty[JumpInsnNode, Boolean]

    for (insn <- method.instructions.iterator().asScala) insn match {
      case l: LabelNode =>
        activeHandlers ++= allHandlers.filter(_.start == l)
        activeHandlers = activeHandlers.filter(_.end != l)

      case ji: JumpInsnNode =>
        jumpInsns(ji) = activeHandlers.nonEmpty

      case _ =>
    }

    var _jumpTargets: Set[AbstractInsnNode] = null
    def jumpTargets = {
      if (_jumpTargets == null) {
        _jumpTargets = jumpInsns.keysIterator.map(_.label).toSet
      }
      _jumpTargets
    }

    def removeJumpFromMap(jump: JumpInsnNode) = {
      jumpInsns.remove(jump)
      _jumpTargets = null
    }

    def replaceJumpByPop(jump: JumpInsnNode) = {
      removeJumpAndAdjustStack(method, jump)
      removeJumpFromMap(jump)
    }

    /**
     * Removes a conditional jump if it is followed by a GOTO to the same destination.
     *
     *      CondJump l;  [nops];  GOTO l;  [...]
     *      POP*;        [nops];  GOTO l;  [...]
     *
     * Introduces 1 or 2 POP instructions, depending on the number of values consumed by the CondJump.
     */
    def simplifyThenElseSameTarget(insn: AbstractInsnNode): Boolean = insn match {
      case ConditionalJump(jump) =>
        nextExecutableInstruction(insn) match {
          case Some(Goto(elseJump)) if sameTargetExecutableInstruction(jump, elseJump) =>
            replaceJumpByPop(jump)
            true

          case _ => false
        }

      case _ => false
    }

    /**
     * Replace jumps to a sequence of GOTO instructions by a jump to the final destination.
     *
     * {{{
     *      Jump l;  [any ops];  l: GOTO m;  [any ops];  m: GOTO n;  [any ops];   n: NotGOTO; [...]
     *   => Jump n;  [rest unchanged]
     * }}}
     *
     * If there's a loop of GOTOs, the initial jump is replaced by one of the labels in the loop.
     */
    def collapseJumpChains(insn: AbstractInsnNode): Boolean = insn match {
      case JumpNonJsr(jump) =>
        val target = finalJumpTarget(jump)
        if (jump.label == target) false else {
          jump.label = target
          _jumpTargets = null
          true
        }

      case _ => false
    }

    /**
     * Eliminates unnecessary jump instructions
     *
     * {{{
     *      Jump l;  [nops];  l: [...]
     *   => POP*;    [nops];  l: [...]
     * }}}
     *
     * Introduces 0, 1 or 2 POP instructions, depending on the number of values consumed by the Jump.
     */
    def removeJumpToSuccessor(insn: AbstractInsnNode): Boolean = insn match {
      case JumpNonJsr(jump) if nextExecutableInstruction(jump, alsoKeep = Set(jump.label)) contains jump.label =>
        replaceJumpByPop(jump)
        true

      case _ => false
    }

    /**
     * If the "else" part of a conditional branch is a simple GOTO, negates the conditional branch
     * and eliminates the GOTO.
     *
     * {{{
     *      CondJump l;         [nops, no jump targets];  GOTO m;  [nops];  l: [...]
     *   => NegatedCondJump m;  [nops, no jump targets];           [nops];  l: [...]
     * }}}
     *
     * Note that no jump targets are allowed in the first [nops] section. Otherwise, there could
     * be some other jump to the GOTO, and eliminating it would change behavior.
     */
    def simplifyBranchOverGoto(insn: AbstractInsnNode, inTryBlock: Boolean): Boolean = insn match {
      case ConditionalJump(jump) =>
        // don't skip over jump targets, see doc comment
        nextExecutableInstruction(jump, alsoKeep = jumpTargets) match {
          case Some(Goto(goto)) =>
            if (nextExecutableInstruction(goto, alsoKeep = Set(jump.label)) contains jump.label) {
              val newJump = new JumpInsnNode(negateJumpOpcode(jump.getOpcode), goto.label)
              method.instructions.set(jump, newJump)
              removeJumpFromMap(jump)
              jumpInsns(newJump) = inTryBlock
              replaceJumpByPop(goto)
              true
            } else false

          case _ => false
        }
      case _ => false
    }

    /**
     * Inlines xRETURN and ATHROW
     *
     * {{{
     *      GOTO l;            [any ops];  l: xRETURN/ATHROW
     *   => xRETURN/ATHROW;    [any ops];  l: xRETURN/ATHROW
     * }}}
     *
     * inlining is only done if the GOTO instruction is not part of a try block, otherwise the
     * rewrite might change the behavior. For xRETURN, the reason is that return instructions may throw
     * an IllegalMonitorStateException, as described here:
     *   http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.return
     */
    def simplifyGotoReturn(instruction: AbstractInsnNode, inTryBlock: Boolean): Boolean = !inTryBlock && (instruction match {
      case Goto(jump) =>
        nextExecutableInstruction(jump.label) match {
          case Some(target) =>
            if (isReturn(target) || target.getOpcode == ATHROW) {
              method.instructions.set(jump, target.clone(null))
              removeJumpFromMap(jump)
              true
            } else false

          case _ => false
        }
      case _ => false
    })

    def run(): Boolean = {
      var changed = false

      // `.toList` because we're modifying the map while iterating over it
      for ((jumpInsn, inTryBlock) <- jumpInsns.toList if jumpInsns.contains(jumpInsn) && isJumpNonJsr(jumpInsn)) {
        var jumpRemoved = simplifyThenElseSameTarget(jumpInsn)

        if (!jumpRemoved) {
          changed = collapseJumpChains(jumpInsn) || changed
          jumpRemoved = removeJumpToSuccessor(jumpInsn)

          if (!jumpRemoved) {
            changed = simplifyBranchOverGoto(jumpInsn, inTryBlock) || changed
            changed = simplifyGotoReturn(jumpInsn, inTryBlock) || changed
          }
        }

        changed ||= jumpRemoved
      }

      if (changed) run()
      changed
    }

    run()
  }
}