-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
Copy pathPatternMatcher.scala
1068 lines (975 loc) · 41.7 KB
/
PatternMatcher.scala
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
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package dotty.tools
package dotc
package transform
import core.*
import MegaPhase.*
import Symbols.*, Contexts.*, Types.*, StdNames.*, NameOps.*
import patmat.SpaceEngine
import util.Spans.*
import typer.Applications.*
import Annotations.*
import Flags.*, Constants.*
import Decorators.*
import NameKinds.{PatMatStdBinderName, PatMatAltsName, PatMatResultName}
import config.Printers.patmatch
import reporting.*
import ast.*
import util.Property.*
import scala.annotation.tailrec
import scala.collection.mutable
/** The pattern matching transform.
* After this phase, the only Match nodes remaining in the code are simple switches
* where every pattern is an integer or string constant
*/
class PatternMatcher extends MiniPhase {
import ast.tpd.*
import PatternMatcher.*
override def phaseName: String = PatternMatcher.name
override def description: String = PatternMatcher.description
override def runsAfter: Set[String] = Set(ElimRepeated.name)
private val InInlinedCode = new util.Property.Key[Boolean]
private def inInlinedCode(using Context) = ctx.property(InInlinedCode).getOrElse(false)
override def prepareForInlined(tree: Inlined)(using Context): Context =
if inInlinedCode then ctx
else ctx.fresh.setProperty(InInlinedCode, true)
override def transformMatch(tree: Match)(using Context): Tree =
if (tree.isInstanceOf[InlineMatch]) tree
else {
// Widen termrefs with underlying `=> T` types. Otherwise ElimByName will produce
// inconsistent types. See i7743.scala.
// Question: Does this need to be done more systematically, not just for pattern matches?
val matchType = tree.tpe.widenSingleton match
case ExprType(rt) => rt
case rt => tree.tpe
val translated = new Translator(matchType, this).translateMatch(tree)
if !inInlinedCode then
// check exhaustivity and unreachability
SpaceEngine.checkMatch(tree)
translated.ensureConforms(matchType)
}
}
object PatternMatcher {
import ast.tpd.*
val name: String = "patternMatcher"
val description: String = "compile pattern matches"
inline val selfCheck = false // debug option, if on we check that no case gets generated twice
/** Minimal number of cases to emit a switch */
inline val MinSwitchCases = 4
val TrustedTypeTestKey: Key[Unit] = new StickyKey[Unit]
/** Was symbol generated by pattern matcher? */
def isPatmatGenerated(sym: Symbol)(using Context): Boolean =
sym.is(Synthetic) && sym.name.is(PatMatStdBinderName)
/** The pattern matching translator.
* Its general structure is a pipeline:
*
* Match tree ---matchPlan---> Plan ---optimize---> Plan ---emit---> Tree
*
* The pipeline consists of three steps:
*
* - build a plan, using methods `matchPlan`, `caseDefPlan`, `patternPlan`.
* - optimize the plan, using methods listed in `optimization`,
* - emit the translated tree, using methods `emit`, `collectSwitchCases`,
* `emitSwitchCases`, and `emitCondition`.
*
* A plan represents the underlying decision graph. It consists of tests,
* let bindings, labeled blocks, return from said labeled blocks and code blocks.
* It's represented by its own data type. Plans are optimized by merging common
* tests and eliminating dead code.
*/
class Translator(resultType: Type, thisPhase: MiniPhase)(using Context) {
// ------- Bindings for variables and labels ---------------------
private val resultLabel =
newSymbol(ctx.owner, PatMatResultName.fresh(), Synthetic | Label, resultType)
/** A map from variable symbols to their defining trees
* and from labels to their defining plans
*/
private val initializer = MutableSymbolMap[Tree]()
private def newVar(rhs: Tree, flags: FlagSet, tpe: Type): TermSymbol =
newSymbol(ctx.owner, PatMatStdBinderName.fresh(), SyntheticCase | flags,
sanitize(tpe), coord = rhs.span)
// TODO: Drop Case once we use everywhere else `isPatmatGenerated`.
/** The plan `let x = rhs in body(x)` where `x` is a fresh variable */
private def letAbstract(rhs: Tree, tpe: Type = NoType)(body: Symbol => Plan): Plan = {
val declTpe = if tpe.exists then tpe else rhs.tpe
val vble = newVar(rhs, EmptyFlags, declTpe)
initializer(vble) = rhs
LetPlan(vble, body(vble))
}
/** The plan `l: { expr(l) }` where `l` is a fresh label */
private def altsLabeledAbstract(expr: (=> Plan) => Plan): Plan = {
val label = newSymbol(ctx.owner, PatMatAltsName.fresh(), Synthetic | Label,
defn.UnitType)
LabeledPlan(label, expr(ReturnPlan(label)))
}
/** Test whether a type refers to a pattern-generated variable */
private val refersToInternal = new TypeAccumulator[Boolean] {
def apply(x: Boolean, tp: Type) =
x || {
tp match {
case tp: TermRef => isPatmatGenerated(tp.symbol)
case _ => false
}
} || foldOver(x, tp)
}
/** Widen type as far as necessary so that it does not refer to a pattern-
* generated variable.
*/
private def sanitize(tp: Type): Type = tp.widenIfUnstable match {
case tp: TermRef if refersToInternal(false, tp) => sanitize(tp.underlying)
case tp => tp
}
// ------- Plan and test types ------------------------
/** Counter to display plans nicely, for debugging */
private var nxId = 0
/** The different kinds of plans */
sealed abstract class Plan { val id: Int = nxId; nxId += 1 }
case class TestPlan(test: Test, var scrutinee: Tree, span: Span,
var onSuccess: Plan) extends Plan {
override def equals(that: Any): Boolean = that match {
case that: TestPlan => this.scrutinee === that.scrutinee && this.test == that.test
case _ => false
}
override def hashCode: Int = scrutinee.hash * 41 + test.hashCode
}
case class LetPlan(sym: TermSymbol, var body: Plan) extends Plan
case class LabeledPlan(sym: TermSymbol, var expr: Plan) extends Plan
case class ReturnPlan(var label: TermSymbol) extends Plan
case class SeqPlan(var head: Plan, var tail: Plan) extends Plan
case class ResultPlan(var tree: Tree) extends Plan
object TestPlan {
def apply(test: Test, sym: Symbol, span: Span, ons: Plan): TestPlan =
TestPlan(test, ref(sym), span, ons)
}
/** The different kinds of tests */
sealed abstract class Test
case class TypeTest(tpt: Tree, trusted: Boolean) extends Test { // scrutinee.isInstanceOf[tpt]
override def equals(that: Any): Boolean = that match {
case that: TypeTest => this.tpt.tpe =:= that.tpt.tpe
case _ => false
}
override def hashCode: Int = tpt.tpe.hash
}
case class EqualTest(tree: Tree) extends Test { // scrutinee == tree
override def equals(that: Any): Boolean = that match {
case that: EqualTest => this.tree === that.tree
case _ => false
}
override def hashCode: Int = tree.hash
}
case class LengthTest(len: Int, exact: Boolean) extends Test // scrutinee (== | >=) len
case object NonEmptyTest extends Test // !scrutinee.isEmpty
case object NonNullTest extends Test // scrutinee ne null
case object GuardTest extends Test // scrutinee
// ------- Generating plans from trees ------------------------
/** A set of variabes that are known to be not null */
private val nonNull = mutable.Set[Symbol]()
/** A conservative approximation of which patterns do not discern anything.
* They are discarded during the translation.
*/
private object WildcardPattern {
def unapply(pat: Tree): Boolean = pat match {
case Typed(_, tpt) if tpt.tpe.isRepeatedParam => true
case Bind(nme.WILDCARD, WildcardPattern()) => true // don't skip when binding an interesting symbol!
case t if isWildcardArg(t) => true
case x: Ident => x.name.isVarPattern && !isBackquoted(x)
case Alternative(ps) => ps.forall(unapply)
case EmptyTree => true
case _ => false
}
}
private object VarArgPattern {
def unapply(pat: Tree): Option[Tree] = swapBind(pat) match {
case Typed(pat1, tpt) if tpt.tpe.isRepeatedParam => Some(pat1)
case _ => None
}
}
/** Rewrite (repeatedly) `x @ (p: T)` to `(x @ p): T`
* This brings out the type tests to where they can be analyzed.
*/
private def swapBind(tree: Tree): Tree = tree match {
case Bind(name, pat0) =>
swapBind(pat0) match {
case Typed(pat, tpt) => Typed(cpy.Bind(tree)(name, pat), tpt)
case _ => tree
}
case _ => tree
}
/** Plan for matching `scrutinee` symbol against `tree` pattern */
private def patternPlan(scrutinee: Symbol, tree: Tree, onSuccess: Plan): Plan = {
extension (tree: Tree) def avoidPatBoundType(): Type =
tree.tpe.widen match
case tref: TypeRef if tref.symbol.isPatternBound =>
defn.AnyType
case _ =>
tree.tpe
/** Plan for matching `components` against argument patterns `args` */
def matchArgsPlan(components: List[Tree], args: List[Tree], onSuccess: Plan): Plan = {
/* For a case with arguments that have some test on them such as
* ```
* case Foo(1, 2) => someCode
* ```
* all arguments values are extracted before the checks are performed. This shape is expected by `emit`
* to avoid generating deep trees.
* ```
* val x1: Foo = ...
* val x2: Int = x1._1
* val x3: Int = x1._2
* if (x2 == 1) {
* if (x3 == 2) someCode
* else ()
* } else ()
* ```
*/
def matchArgsComponentsPlan(components: List[Tree], syms: List[Symbol]): Plan =
components match {
case component :: components1 => letAbstract(component, component.avoidPatBoundType())(sym => matchArgsComponentsPlan(components1, sym :: syms))
case Nil => matchArgsPatternPlan(args, syms.reverse)
}
def matchArgsPatternPlan(args: List[Tree], syms: List[Symbol]): Plan =
args match {
case arg :: args1 =>
val sym :: syms1 = syms: @unchecked
patternPlan(sym, arg, matchArgsPatternPlan(args1, syms1))
case Nil =>
assert(syms.isEmpty)
onSuccess
}
matchArgsComponentsPlan(components, Nil)
}
/** Plan for matching the sequence in `seqSym` against sequence elements `args`.
* If `exact` is true, the sequence is not permitted to have any elements following `args`.
*/
def matchElemsPlan(seqSym: Symbol, args: List[Tree], exact: Boolean, onSuccess: Plan) = {
val selectors = args.indices.toList.map(idx =>
ref(seqSym).select(defn.Seq_apply.matchingMember(seqSym.info)).appliedTo(Literal(Constant(idx))))
TestPlan(LengthTest(args.length, exact), seqSym, seqSym.span,
matchArgsPlan(selectors, args, onSuccess))
}
/** Plan for matching the sequence in `getResult` against sequence elements
* and a possible last varargs argument `args`.
*/
def unapplySeqPlan(getResult: Symbol, args: List[Tree]): Plan = args.lastOption match {
case Some(VarArgPattern(arg)) =>
val matchRemaining =
if (args.length == 1) {
val toSeq = ref(getResult)
.select(defn.Seq_toSeq.matchingMember(getResult.info))
letAbstract(toSeq) { toSeqResult =>
patternPlan(toSeqResult, arg, onSuccess)
}
}
else {
val dropped = ref(getResult)
.select(defn.Seq_drop.matchingMember(getResult.info))
.appliedTo(Literal(Constant(args.length - 1)))
letAbstract(dropped) { droppedResult =>
patternPlan(droppedResult, arg, onSuccess)
}
}
matchElemsPlan(getResult, args.init, exact = false, matchRemaining)
case _ =>
matchElemsPlan(getResult, args, exact = true, onSuccess)
}
/** Plan for matching the sequence in `getResult`
*
* `getResult` is a product, where the last element is a sequence of elements.
*/
def unapplyProductSeqPlan(getResult: Symbol, args: List[Tree], arity: Int): Plan = {
assert(arity <= args.size + 1)
val selectors = productSelectors(getResult.info).map(ref(getResult).select(_))
val matchSeq =
letAbstract(selectors.last) { seqResult =>
unapplySeqPlan(seqResult, args.drop(arity - 1))
}
matchArgsPlan(selectors.take(arity - 1), args.take(arity - 1), matchSeq)
}
/** Plan for matching the result of an unapply against argument patterns `args` */
def unapplyPlan(unapp: Tree, args: List[Tree]): Plan = {
def caseClass = unapp.symbol.owner.linkedClass
lazy val caseAccessors = caseClass.caseAccessors.filter(_.is(Method))
def isSyntheticScala2Unapply(sym: Symbol) =
sym.isAllOf(SyntheticCase) && sym.owner.is(Scala2x)
def tupleApp(i: Int, receiver: Tree) = // manually inlining the call to NonEmptyTuple#apply, because it's an inline method
ref(defn.RuntimeTuplesModule)
.select(defn.RuntimeTuples_apply)
.appliedTo(receiver, Literal(Constant(i)))
if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length)
def tupleSel(sym: Symbol) = ref(scrutinee).select(sym)
val isGenericTuple = defn.isTupleClass(caseClass) &&
!defn.isTupleNType(tree.tpe match { case tp: OrType => tp.join case tp => tp }) // widen even hard unions, to see if it's a union of tuples
val components = if isGenericTuple then caseAccessors.indices.toList.map(tupleApp(_, ref(scrutinee))) else caseAccessors.map(tupleSel)
matchArgsPlan(components, args, onSuccess)
else if (unapp.tpe <:< (defn.BooleanType))
TestPlan(GuardTest, unapp, unapp.span, onSuccess)
else
letAbstract(unapp) { unappResult =>
val isUnapplySeq = unapp.symbol.name == nme.unapplySeq
if (isProductMatch(unapp.tpe.widen, args.length) && !isUnapplySeq) {
val selectors = productSelectors(unapp.tpe).take(args.length)
.map(ref(unappResult).select(_))
matchArgsPlan(selectors, args, onSuccess)
}
else if (isUnapplySeq && isProductSeqMatch(unapp.tpe.widen, args.length, unapp.srcPos)) {
val arity = productArity(unapp.tpe.widen, unapp.srcPos)
unapplyProductSeqPlan(unappResult, args, arity)
}
else if (isUnapplySeq && unapplySeqTypeElemTp(unapp.tpe.widen.finalResultType).exists) {
unapplySeqPlan(unappResult, args)
}
else if unappResult.info <:< defn.NonEmptyTupleTypeRef then
val components = (0 until foldApplyTupleType(unappResult.denot.info).length).toList.map(tupleApp(_, ref(unappResult)))
matchArgsPlan(components, args, onSuccess)
else {
assert(isGetMatch(unapp.tpe))
val argsPlan = {
val get = ref(unappResult).select(nme.get, _.info.isParameterless)
val arity = productArity(get.tpe, unapp.srcPos)
if (isUnapplySeq)
letAbstract(get) { getResult =>
if (arity > 0) unapplyProductSeqPlan(getResult, args, arity)
else unapplySeqPlan(getResult, args)
}
else
letAbstract(get) { getResult =>
val selectors =
if (args.tail.isEmpty) ref(getResult) :: Nil
else productSelectors(get.tpe).map(ref(getResult).select(_))
matchArgsPlan(selectors, args, onSuccess)
}
}
TestPlan(NonEmptyTest, unappResult, unapp.span, argsPlan)
}
}
}
// begin patternPlan
swapBind(tree) match {
case Typed(pat, tpt) =>
val isTrusted = pat match {
case UnApply(extractor, _, _) =>
extractor.symbol.is(Synthetic)
&& extractor.symbol.owner.linkedClass.is(Case)
&& !hasExplicitTypeArgs(extractor)
case _ => false
}
TestPlan(TypeTest(tpt, isTrusted), scrutinee, tree.span,
letAbstract(ref(scrutinee).cast(tpt.tpe)) { casted =>
nonNull += casted
patternPlan(casted, pat, onSuccess)
})
case UnApply(extractor, implicits, args) =>
val unappPlan = if (scrutinee.info.isBottomType)
// Generate a throwaway but type-correct plan.
// This plan will never execute because it'll be guarded by a `NonNullTest`.
ResultPlan(tpd.Throw(tpd.nullLiteral))
else {
def applyImplicits(acc: Tree, implicits: List[Tree], mt: Type): Tree = mt match {
case mt: MethodType =>
assert(mt.isImplicitMethod)
val (args, rest) = implicits.splitAt(mt.paramNames.size)
applyImplicits(acc.appliedToTermArgs(args), rest, mt.resultType)
case _ =>
assert(implicits.isEmpty)
acc
}
val mt @ MethodType(_) = extractor.tpe.widen: @unchecked
val unapp0 = extractor.appliedTo(ref(scrutinee).ensureConforms(mt.paramInfos.head))
val unapp = applyImplicits(unapp0, implicits, mt.resultType)
unapplyPlan(unapp, args)
}
if (scrutinee.info.isNotNull || nonNull(scrutinee)) unappPlan
else TestPlan(NonNullTest, scrutinee, tree.span, unappPlan)
case Bind(name, body) =>
if (name == nme.WILDCARD) patternPlan(scrutinee, body, onSuccess)
else {
// The type of `name` may refer to val in `body`, therefore should come after `body`
val bound = tree.symbol.asTerm
initializer(bound) = ref(scrutinee)
patternPlan(scrutinee, body, LetPlan(bound, onSuccess))
}
case Alternative(alts) =>
altsLabeledAbstract { onf =>
SeqPlan(
altsLabeledAbstract { ons =>
alts.foldRight(onf) { (alt, next) =>
SeqPlan(patternPlan(scrutinee, alt, ons), next)
}
},
onSuccess
)
}
case WildcardPattern() =>
onSuccess
case SeqLiteral(pats, _) =>
matchElemsPlan(scrutinee, pats, exact = true, onSuccess)
case _ =>
TestPlan(EqualTest(tree), scrutinee, tree.span, onSuccess)
}
}
private def caseDefPlan(scrutinee: Symbol, cdef: CaseDef): Plan = {
var onSuccess: Plan = ResultPlan(cdef.body)
if (!cdef.guard.isEmpty)
onSuccess = TestPlan(GuardTest, cdef.guard, cdef.guard.span, onSuccess)
patternPlan(scrutinee, cdef.pat, onSuccess)
}
private def matchPlan(tree: Match): Plan =
letAbstract(tree.selector) { scrutinee =>
val matchError: Plan = ResultPlan(Throw(New(defn.MatchErrorClass.typeRef, ref(scrutinee) :: Nil)))
tree.cases.foldRight(matchError) { (cdef, next) =>
SeqPlan(caseDefPlan(scrutinee, cdef), next)
}
}
// ----- Optimizing plans ---------------
/** A superclass for plan transforms */
class PlanTransform extends (Plan => Plan) {
protected val treeMap: TreeMap = new TreeMap {
override def transform(tree: Tree)(using Context) = tree
}
def apply(tree: Tree): Tree = treeMap.transform(tree)
def apply(plan: TestPlan): Plan = {
plan.scrutinee = apply(plan.scrutinee)
plan.onSuccess = apply(plan.onSuccess)
plan
}
def apply(plan: LetPlan): Plan = {
plan.body = apply(plan.body)
initializer(plan.sym) = apply(initializer(plan.sym))
plan
}
def apply(plan: LabeledPlan): Plan = {
plan.expr = apply(plan.expr)
plan
}
def apply(plan: ReturnPlan): Plan = plan
def apply(plan: SeqPlan): Plan = {
plan.head = apply(plan.head)
plan.tail = apply(plan.tail)
plan
}
def apply(plan: Plan): Plan = plan match {
case plan: TestPlan => apply(plan)
case plan: LetPlan => apply(plan)
case plan: LabeledPlan => apply(plan)
case plan: ReturnPlan => apply(plan)
case plan: SeqPlan => apply(plan)
case plan: ResultPlan => plan
}
}
private class RefCounter extends PlanTransform {
val count = new mutable.HashMap[Symbol, Int].withDefaultValue(0)
}
/** Reference counts for all labels */
private def labelRefCount(plan: Plan): collection.Map[Symbol, Int] = {
object refCounter extends RefCounter {
override def apply(plan: LabeledPlan): Plan = {
apply(plan.expr)
plan
}
override def apply(plan: ReturnPlan): Plan = {
count(plan.label) += 1
plan
}
}
refCounter(plan)
refCounter.count
}
/** Reference counts for all variables */
private def varRefCount(plan: Plan): collection.Map[Symbol, Int] = {
object refCounter extends RefCounter {
override val treeMap = new TreeMap {
override def transform(tree: Tree)(using Context) = tree match {
case tree: Ident =>
if (isPatmatGenerated(tree.symbol)) count(tree.symbol) += 1
tree
case _ =>
super.transform(tree)
}
}
override def apply(plan: LetPlan): Plan = {
apply(plan.body)
if (count(plan.sym) != 0 || !isPatmatGenerated(plan.sym))
apply(initializer(plan.sym))
plan
}
override def apply(plan: SeqPlan): Plan = {
apply(plan.head)
if (canFallThrough(plan.head))
apply(plan.tail)
plan
}
}
refCounter(plan)
refCounter.count
}
/** Merge identical consecutive tests.
*
* When we have the following shape:
*
* if (testA) plan1
* if (testA) plan2
* nextPlan?
*
* transform it to
*
* if (testA) {
* plan1
* plan2
* }
* nextPlan?
*
* Similarly, when we have equivalent let bindings:
*
* let x1 = rhs1 in plan1
* let x2 = rhs2 in plan2
* nextPlan?
*
* and rhs1 and rhs2 are equivalent, transform it to
*
* let x1 = rhs1 in {
* plan1
* plan2[x1/x2]
* }
*
* where plan2[x1/x2] means substituting x1 for x2 in plan2.
*
* There are some tricks to "ignore" non-patmat-generated let bindings, i.e.,
* captures written in the source code, while identifying common subplans.
*/
def mergeTests(plan: Plan): Plan = {
class SubstituteIdent(from: TermSymbol, to: TermSymbol) extends PlanTransform {
override val treeMap = new TreeMap {
override def transform(tree: Tree)(using Context) = tree match {
case tree: Ident if tree.symbol == from => ref(to)
case _ => super.transform(tree)
}
}
}
class MergeTests extends PlanTransform {
override def apply(plan: SeqPlan): Plan = {
def tryMerge(plan1: Plan, plan2: Plan): Option[Plan] = {
def skipNonPatmatGenedLets(plan: Plan): Plan = plan match {
case LetPlan(sym, body) if !isPatmatGenerated(sym) =>
skipNonPatmatGenedLets(body)
case _ =>
plan
}
def transferNonPatmatGenedLets(originalPlan: Plan, newPlan: Plan): Plan = originalPlan match {
case originalPlan: LetPlan if !isPatmatGenerated(originalPlan.sym) =>
originalPlan.body = transferNonPatmatGenedLets(originalPlan.body, newPlan)
originalPlan
case _ =>
newPlan
}
(skipNonPatmatGenedLets(plan1), skipNonPatmatGenedLets(plan2)) match {
case (testPlan1: TestPlan, testPlan2: TestPlan) if testPlan1 == testPlan2 =>
/* Because testPlan2 is the same as testPlan1, it cannot possibly refer to
* the symbols defined by any of the skipped lets.
*/
testPlan1.onSuccess = SeqPlan(testPlan1.onSuccess,
transferNonPatmatGenedLets(plan2, testPlan2.onSuccess))
Some(plan1) // that's the original plan1, on purpose
case (letPlan1: LetPlan, letPlan2: LetPlan) if initializer(letPlan1.sym) === initializer(letPlan2.sym) =>
// By construction, letPlan1.sym and letPlan2.sym are patmat-generated
val newPlan2Body = new SubstituteIdent(letPlan2.sym, letPlan1.sym)(letPlan2.body)
letPlan1.body = SeqPlan(letPlan1.body,
transferNonPatmatGenedLets(plan2, newPlan2Body))
Some(plan1) // that's the original plan1, on purpose
case _ =>
None
}
}
plan.head = apply(plan.head)
plan.tail = apply(plan.tail)
plan.tail match {
case SeqPlan(tailHead, tailTail) =>
tryMerge(plan.head, tailHead) match {
case Some(merged) => SeqPlan(apply(merged), tailTail)
case none => plan
}
case tail =>
tryMerge(plan.head, tail) match {
case Some(merged) => apply(merged)
case none => plan
}
}
}
}
new MergeTests()(plan)
}
/** Inline let-bound trees that are referenced only once and eliminate dead code.
*
* - Drop all variables that are not referenced anymore after inlining.
* - Drop the `tail` of `SeqPlan`s whose `head` cannot fall through.
*/
private def inlineVars(plan: Plan): Plan = {
val refCount = varRefCount(plan)
val LetPlan(topSym, _) = plan: @unchecked
def toDrop(sym: Symbol) =
val rhs = initializer.lookup(sym)
if rhs != null then
isPatmatGenerated(sym) && refCount(sym) <= 1 && sym != topSym && isPureExpr(rhs)
else
false
object Inliner extends PlanTransform {
override val treeMap = new TreeMap {
override def transform(tree: Tree)(using Context) = tree match {
case tree: Ident =>
val sym = tree.symbol
if (toDrop(sym)) transform(initializer(sym))
else tree
case _ =>
super.transform(tree)
}
}
override def apply(plan: LetPlan): Plan =
if (toDrop(plan.sym)) apply(plan.body)
else {
initializer(plan.sym) = apply(initializer(plan.sym))
plan.body = apply(plan.body)
plan
}
override def apply(plan: SeqPlan): Plan = {
val newHead = apply(plan.head)
if (!canFallThrough(newHead))
// If the head cannot fall through, the tail is dead code
newHead
else {
plan.head = newHead
plan.tail = apply(plan.tail)
plan
}
}
}
Inliner(plan)
}
// ----- Generating trees from plans ---------------
/** The condition a test plan rewrites to */
private def emitCondition(plan: TestPlan): Tree =
val scrutinee = plan.scrutinee
(plan.test: @unchecked) match
case NonEmptyTest =>
constToLiteral(
scrutinee
.select(nme.isEmpty, _.info.isParameterless)
.select(nme.UNARY_!, _.info.isParameterless))
case NonNullTest =>
scrutinee.testNotNull
case GuardTest =>
scrutinee
case EqualTest(tree) =>
tree.equal(scrutinee)
case LengthTest(len, exact) =>
val lengthCompareSym = defn.Seq_lengthCompare.matchingMember(scrutinee.tpe)
if (lengthCompareSym.exists)
scrutinee
.select(defn.Seq_lengthCompare.matchingMember(scrutinee.tpe))
.appliedTo(Literal(Constant(len)))
.select(if (exact) defn.Int_== else defn.Int_>=)
.appliedTo(Literal(Constant(0)))
else // try length
scrutinee
.select(defn.Seq_length.matchingMember(scrutinee.tpe))
.select(if (exact) defn.Int_== else defn.Int_>=)
.appliedTo(Literal(Constant(len)))
case TypeTest(tpt, trusted) =>
val expectedTp = tpt.tpe
def typeTest(scrut: Tree, expected: Type): Tree =
val ttest = scrut.select(defn.Any_typeTest).appliedToType(expected)
if trusted then ttest.pushAttachment(TrustedTypeTestKey, ())
ttest
/** An outer test is needed in a situation like `case x: y.Inner => ...
* or like case x: O#Inner if the owner of Inner is not a subclass of O.
* Outer tests are added here instead of in TypeTestsCasts since they
* might cause outer accessors to be added to inner classes (via ensureOuterAccessors)
* and therefore have to run before ExplicitOuter.
*/
def addOuterTest(tree: Tree, expected: Type): Tree = expected.dealias match
case tref @ TypeRef(pre, _) =>
tref.symbol match
case expectedCls: ClassSymbol if ExplicitOuter.needsOuterIfReferenced(expectedCls) =>
def selectOuter =
ExplicitOuter.ensureOuterAccessors(expectedCls)
scrutinee.ensureConforms(expected).outerSelect(1, expectedCls.owner.typeRef)
if pre.isSingleton then
val expectedOuter = singleton(pre)
tree.and(selectOuter.select(defn.Object_eq).appliedTo(expectedOuter))
else if !expectedCls.isStatic
&& expectedCls.owner.isType
&& !expectedCls.owner.derivesFrom(pre.classSymbol)
then
val testPre =
if expected.hasAnnotation(defn.UncheckedAnnot) then
AnnotatedType(pre, Annotation(defn.UncheckedAnnot, tree.span))
else pre
tree.and(typeTest(selectOuter, testPre))
else tree
case _ => tree
case AppliedType(tycon, _) =>
addOuterTest(tree, tycon)
case _ =>
tree
expectedTp.dealias match
case expectedTp: SingletonType =>
scrutinee.isInstance(expectedTp) // will be translated to an equality test
case _ =>
addOuterTest(typeTest(scrutinee, expectedTp), expectedTp)
end emitCondition
@tailrec
private def canFallThrough(plan: Plan): Boolean = plan match {
case _:ReturnPlan | _:ResultPlan => false
case _:TestPlan | _:LabeledPlan => true
case LetPlan(_, body) => canFallThrough(body)
case SeqPlan(_, tail) => canFallThrough(tail)
}
/** Collect longest list of plans that represent possible cases of
* a switch, including a last default case, by starting with this
* plan and following onSuccess plans.
*/
private def collectSwitchCases(scrutinee: Tree, plan: SeqPlan): List[(List[Tree], Plan)] = {
def isSwitchableType(tpe: Type): Boolean =
(tpe isRef defn.IntClass) ||
(tpe isRef defn.ByteClass) ||
(tpe isRef defn.ShortClass) ||
(tpe isRef defn.CharClass) ||
(tpe isRef defn.StringClass)
val seen = mutable.Set[Any]()
def isNewSwitchableConst(tree: Tree) = tree match {
case Literal(const)
if (const.isIntRange || const.tag == Constants.StringTag) && !seen.contains(const.value) =>
seen += const.value
true
case _ =>
false
}
// An extractor to recover the shape of plans that can become alternatives
object AlternativesPlan {
def unapply(plan: LabeledPlan): Option[(List[Tree], Plan)] =
plan.expr match {
case SeqPlan(LabeledPlan(innerLabel, innerPlan), ons) if !canFallThrough(ons) =>
val outerLabel = plan.sym
val alts = List.newBuilder[Tree]
def rec(innerPlan: Plan): Boolean = innerPlan match {
case SeqPlan(TestPlan(EqualTest(tree), scrut, _, ReturnPlan(`innerLabel`)), tail)
if scrut === scrutinee && isNewSwitchableConst(tree) =>
alts += tree
rec(tail)
case ReturnPlan(`outerLabel`) =>
true
case _ =>
false
}
if (rec(innerPlan))
Some((alts.result(), ons))
else
None
case _ =>
None
}
}
def recur(plan: Plan): List[(List[Tree], Plan)] = plan match {
case SeqPlan(testPlan @ TestPlan(EqualTest(tree), scrut, _, ons), tail)
if scrut === scrutinee && !canFallThrough(ons) && isNewSwitchableConst(tree) =>
(tree :: Nil, ons) :: recur(tail)
case SeqPlan(AlternativesPlan(alts, ons), tail) =>
(alts, ons) :: recur(tail)
case _ =>
(Nil, plan) :: Nil
}
if (isSwitchableType(scrutinee.tpe.widen)) recur(plan)
else Nil
}
private def hasEnoughSwitchCases(cases: List[(List[Tree], Plan)], required: Int): Boolean =
// 1 because of the default case
required <= 1 || {
cases match {
case (alts, _) :: cases1 => hasEnoughSwitchCases(cases1, required - alts.size)
case _ => false
}
}
/** Emit a switch-match */
private def emitSwitchMatch(scrutinee: Tree, cases: List[(List[Tree], Plan)]): Match = {
/* Make sure to adapt the scrutinee to Int or String, as well as all the
* alternatives, so that only Matches on pritimive Ints or Strings survive
* this phase.
*/
val (primScrutinee, scrutineeTpe) =
if (scrutinee.tpe.widen.isRef(defn.IntClass)) (scrutinee, defn.IntType)
else if (scrutinee.tpe.widen.isRef(defn.StringClass)) (scrutinee, defn.StringType)
else (scrutinee.select(nme.toInt), defn.IntType)
def primLiteral(lit: Tree): Tree =
val Literal(constant) = lit: @unchecked
if (constant.tag == Constants.IntTag) lit
else if (constant.tag == Constants.StringTag) lit
else cpy.Literal(lit)(Constant(constant.intValue))
val caseDefs = cases.map { (alts, ons) =>
val pat = alts match {
case alt :: Nil => primLiteral(alt)
case Nil => Underscore(scrutineeTpe) // default case
case _ => Alternative(alts.map(primLiteral))
}
CaseDef(pat, EmptyTree, emit(ons))
}
Match(primScrutinee, caseDefs)
}
/** If selfCheck is `true`, used to check whether a tree gets generated twice */
private val emitted = mutable.Set[Int]()
/** Translate plan to tree */
private def emit(plan: Plan): Tree = {
if (selfCheck) {
assert(plan.isInstanceOf[ReturnPlan] || !emitted.contains(plan.id), plan.id)
emitted += plan.id
}
plan match {
case plan: TestPlan =>
/** Merge nested `if`s that have the same `else` branch into a single `if`.
* This optimization targets calls to label defs for case failure jumps to next case.
*
* Plan for
* ```
* val x1: Int = ...
* val x2: Int = ...
* if (x1 == y1) {
* if (x2 == y2) someCode
* else ()
* } else ()
* ```
* is emitted as
* ```
* val x1: Int = ...
* val x2: Int = ...
* if (x1 == y1 && x2 == y2) someCode
* else ()
* ```
*/
def emitWithMashedConditions(plans: List[TestPlan]): Tree = {
val plan = plans.head
plan.onSuccess match {
case plan2: TestPlan =>
emitWithMashedConditions(plan2 :: plans)
case _ =>
def emitCondWithPos(plan: TestPlan) = emitCondition(plan).withSpan(plan.span)
val conditions =
plans.foldRight[Tree](EmptyTree) { (otherPlan, acc) =>
if (acc.isEmpty) emitCondWithPos(otherPlan)
else acc.select(nme.ZAND).appliedTo(emitCondWithPos(otherPlan))
}
If(conditions, emit(plan.onSuccess), unitLiteral)
}
}
emitWithMashedConditions(plan :: Nil)
case LetPlan(sym, body) =>
val valDef = ValDef(sym, initializer(sym).ensureConforms(sym.info), inferred = true).withSpan(sym.span)
seq(valDef :: Nil, emit(body))
case LabeledPlan(label, expr) =>
Labeled(label, emit(expr))
case ReturnPlan(label) =>
Return(unitLiteral, ref(label))
case plan: SeqPlan =>
def default = seq(emit(plan.head) :: Nil, emit(plan.tail))
def maybeEmitSwitch(scrutinee: Tree): Tree = {
val switchCases = collectSwitchCases(scrutinee, plan)
if (hasEnoughSwitchCases(switchCases, MinSwitchCases)) // at least 3 cases + default
emitSwitchMatch(scrutinee, switchCases)
else
default
}
plan.head match {
case testPlan: TestPlan =>
maybeEmitSwitch(testPlan.scrutinee)
case LabeledPlan(_, SeqPlan(LabeledPlan(_, SeqPlan(testPlan: TestPlan, _)), _)) =>
maybeEmitSwitch(testPlan.scrutinee)
case _ =>
default
}
case ResultPlan(tree) =>
if (tree.symbol == defn.throwMethod) tree // For example MatchError
else Return(tree, ref(resultLabel))
}
}
/** Pretty-print plan; used for debugging */
def show(plan: Plan): String = {
val lrefCount = labelRefCount(plan)
val vrefCount = varRefCount(plan)
val sb = new StringBuilder
val seen = mutable.Set[Int]()
def showTest(test: Test) = test match {
case EqualTest(tree) => i"EqualTest($tree)"
case TypeTest(tpt, trusted) => i"TypeTest($tpt, trusted=$trusted)"
case _ => test.toString
}
def showPlan(plan: Plan): Unit =
if (!seen.contains(plan.id)) {
seen += plan.id
sb append s"\n${plan.id}: "
plan match {
case TestPlan(test, scrutinee, _, ons) =>
sb.append(i"$scrutinee ? ${showTest(test)}(${ons.id})")
showPlan(ons)
case LetPlan(sym, body) =>
sb.append(i"Let($sym = ${initializer(sym)}}, ${body.id})")
sb.append(s", refcount = ${vrefCount(sym)}")
showPlan(body)