-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
CaptureSet.scala
1185 lines (1014 loc) · 48.5 KB
/
CaptureSet.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 cc
import core.*
import Types.*, Symbols.*, Flags.*, Contexts.*, Decorators.*
import config.Printers.{capt, captDebug}
import Annotations.Annotation
import annotation.threadUnsafe
import annotation.constructorOnly
import annotation.internal.sharable
import reporting.trace
import printing.{Showable, Printer}
import printing.Texts.*
import util.{SimpleIdentitySet, Property}
import typer.ErrorReporting.Addenda
import TypeComparer.subsumesExistentially
import util.common.alwaysTrue
import scala.collection.{mutable, immutable}
import CCState.*
/** A class for capture sets. Capture sets can be constants or variables.
* Capture sets support inclusion constraints <:< where <:< is subcapturing.
*
* They also allow
* - mapping with functions from elements to capture sets
* - filtering with predicates on elements
* - intersecting wo capture sets
*
* That is, constraints can be of the forms
*
* cs1 <:< cs2
* cs1 = ∪ {f(x) | x ∈ cs2} where f is a function from capture references to capture sets.
* cs1 = ∪ {x | x ∈ cs2, p(x)} where p is a predicate on capture references
* cs1 = cs2 ∩ cs2
*
* We call the resulting constraint system "monadic set constraints".
* To support capture propagation across maps, mappings are supported only
* if the mapped function is either a bijection or if it is idempotent
* on capture references (c.f. doc comment on `map` below).
*/
sealed abstract class CaptureSet extends Showable:
import CaptureSet.*
/** The elements of this capture set. For capture variables,
* the elements known so far.
*/
def elems: Refs
/** Is this capture set constant (i.e. not an unsolved capture variable)?
* Solved capture variables count as constant.
*/
def isConst: Boolean
/** Is this capture set always empty? For unsolved capture veriables, returns
* always false.
*/
def isAlwaysEmpty: Boolean
/** An optional level limit, or undefinedLevel if none exists. All elements of the set
* must be at levels equal or smaller than the level of the set, if it is defined.
*/
def level: Level
/** An optional owner, or NoSymbol if none exists. Used for diagnstics
*/
def owner: Symbol
/** Is this capture set definitely non-empty? */
final def isNotEmpty: Boolean = !elems.isEmpty
/** Convert to Const. @pre: isConst */
def asConst: Const = this match
case c: Const => c
case v: Var =>
assert(v.isConst)
Const(v.elems)
/** Cast to variable. @pre: !isConst */
def asVar: Var =
assert(!isConst)
asInstanceOf[Var]
/** Does this capture set contain the root reference `cap` as element? */
final def isUniversal(using Context) =
elems.exists(_.isRootCapability)
final def isUnboxable(using Context) =
elems.exists(elem => elem.isRootCapability || Existential.isExistentialVar(elem))
final def keepAlways: Boolean = this.isInstanceOf[EmptyWithProvenance]
/** Try to include an element in this capture set.
* @param elem The element to be added
* @param origin The set that originated the request, or `empty` if the request came from outside.
*
* If the set already accounts for the element, return OK.
* Otherwise, try to add a new element to the set. This is OK if
* - the set is a variable, and
* - the element is not at a deeper nesting level than the set, and
* - the element can also be added (in mapped/filtered form) to all
* dependent sets.
* If the `origin` is the same as the `source` of the set variable, the
* element might be filtered or mapped according to the class of the variable.
* Otherwise, the element might have to be back-propagated to the source
* of the variable.
*
* If the element itself cannot be added to the set for some reason, and the
* element is not the root capability, try instead to include its underlying
* capture set.
*/
protected def tryInclude(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult =
if accountsFor(elem) then CompareResult.OK
else addNewElem(elem)
/** Try to include all element in `refs` to this capture set. */
protected final def tryInclude(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult =
(CompareResult.OK /: newElems): (r, elem) =>
r.andAlso(tryInclude(elem, origin))
/** Add an element to this capture set, assuming it is not already accounted for,
* and omitting any mapping or filtering.
*
* If the element itself cannot be added to the set for some reason, and the
* element is not the root capability, try instead to include its underlying
* capture set.
*/
protected final def addNewElem(elem: CaptureRef)(using Context, VarState): CompareResult =
if elem.isMaxCapability || summon[VarState] == FrozenState then
addThisElem(elem)
else
addThisElem(elem).orElse:
val underlying = elem.captureSetOfInfo
tryInclude(underlying.elems, this).andAlso:
underlying.addDependent(this)
CompareResult.OK
/** Add new elements one by one using `addNewElem`, abort on first failure */
protected final def addNewElems(newElems: Refs)(using Context, VarState): CompareResult =
(CompareResult.OK /: newElems): (r, elem) =>
r.andAlso(addNewElem(elem))
/** Add a specific element, assuming it is not already accounted for,
* and omitting any mapping or filtering, without possibility to backtrack
* to the underlying capture set.
*/
protected def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult
/** If this is a variable, add `cs` as a dependent set */
protected def addDependent(cs: CaptureSet)(using Context, VarState): CompareResult
/** If `cs` is a variable, add this capture set as one of its dependent sets */
protected def addAsDependentTo(cs: CaptureSet)(using Context): this.type =
cs.addDependent(this)(using ctx, UnrecordedState)
this
/** {x} <:< this where <:< is subcapturing, but treating all variables
* as frozen.
*/
def accountsFor(x: CaptureRef)(using Context): Boolean =
if comparer.isInstanceOf[ExplainingTypeComparer] then // !!! DEBUG
reporting.trace.force(i"$this accountsFor $x, ${x.captureSetOfInfo}?", show = true):
elems.exists(_.subsumes(x))
|| !x.isMaxCapability && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK
else
reporting.trace(i"$this accountsFor $x, ${x.captureSetOfInfo}?", show = true):
elems.exists(_.subsumes(x))
|| !x.isMaxCapability && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK
/** A more optimistic version of accountsFor, which does not take variable supersets
* of the `x` reference into account. A set might account for `x` if it accounts
* for `x` in a state where we assume all supersets of `x` have just the elements
* known at this point. On the other hand if x's capture set has no known elements,
* a set `cs` might account for `x` only if it subsumes `x` or it contains the
* root capability `cap`.
*/
def mightAccountFor(x: CaptureRef)(using Context): Boolean =
reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true) {
elems.exists(_.subsumes(x))
|| !x.isMaxCapability
&& {
val elems = x.captureSetOfInfo.elems
!elems.isEmpty && elems.forall(mightAccountFor)
}
}
/** A more optimistic version of subCaptures used to choose one of two typing rules
* for selections and applications. `cs1 mightSubcapture cs2` if `cs2` might account for
* every element currently known to be in `cs1`.
*/
def mightSubcapture(that: CaptureSet)(using Context): Boolean =
elems.forall(that.mightAccountFor)
/** The subcapturing test.
* @param frozen if true, no new variables or dependent sets are allowed to
* be added when making this test. An attempt to add either
* will result in failure.
*/
final def subCaptures(that: CaptureSet, frozen: Boolean)(using Context): CompareResult =
subCaptures(that)(using ctx, if frozen then FrozenState else VarState())
/** The subcapturing test, using a given VarState */
private def subCaptures(that: CaptureSet)(using Context, VarState): CompareResult =
val result = that.tryInclude(elems, this)
if result.isOK then
addDependent(that)
else
ccState.levelError = ccState.levelError.orElse(result.levelError)
varState.rollBack()
result
//.showing(i"subcaptures $this <:< $that = ${result.show}", capt)
/** Two capture sets are considered =:= equal if they mutually subcapture each other
* in a frozen state.
*/
def =:= (that: CaptureSet)(using Context): Boolean =
this.subCaptures(that, frozen = true).isOK
&& that.subCaptures(this, frozen = true).isOK
/** The smallest capture set (via <:<) that is a superset of both
* `this` and `that`
*/
def ++ (that: CaptureSet)(using Context): CaptureSet =
if this.subCaptures(that, frozen = true).isOK then
if that.isAlwaysEmpty && this.keepAlways then this else that
else if that.subCaptures(this, frozen = true).isOK then this
else if this.isConst && that.isConst then Const(this.elems ++ that.elems)
else Union(this, that)
/** The smallest superset (via <:<) of this capture set that also contains `ref`.
*/
def + (ref: CaptureRef)(using Context): CaptureSet =
this ++ ref.singletonCaptureSet
/** The largest capture set (via <:<) that is a subset of both `this` and `that`
*/
def **(that: CaptureSet)(using Context): CaptureSet =
if this.subCaptures(that, frozen = true).isOK then this
else if that.subCaptures(this, frozen = true).isOK then that
else if this.isConst && that.isConst then Const(elemIntersection(this, that))
else Intersection(this, that)
/** The largest subset (via <:<) of this capture set that does not account for
* any of the elements in the constant capture set `that`
*/
def -- (that: CaptureSet.Const)(using Context): CaptureSet =
if this.isConst then
val elems1 = elems.filter(!that.accountsFor(_))
if elems1.size == elems.size then this else Const(elems1)
else
if that.isAlwaysEmpty then this else Diff(asVar, that)
/** The largest subset (via <:<) of this capture set that does not account for `ref` */
def - (ref: CaptureRef)(using Context): CaptureSet =
this -- ref.singletonCaptureSet
/** The largest subset (via <:<) of this capture set that only contains elements
* for which `p` is true.
*/
def filter(p: Context ?=> CaptureRef => Boolean)(using Context): CaptureSet =
if this.isConst then
val elems1 = elems.filter(p)
if elems1 == elems then this
else Const(elems.filter(p))
else Filtered(asVar, p)
/** Capture set obtained by applying `tm` to all elements of the current capture set
* and joining the results. If the current capture set is a variable, the same
* transformation is applied to all future additions of new elements.
*
* Note: We have a problem how we handle the situation where we have a mapped set
*
* cs2 = tm(cs1)
*
* and then the propagation solver adds a new element `x` to `cs2`. What do we
* know in this case about `cs1`? We can answer this question in a sound way only
* if `tm` is a bijection on capture references or it is idempotent on capture references.
* (see definition in IdempotentCapRefMap).
* If `tm` is a bijection we know that `tm^-1(x)` must be in `cs1`. If `tm` is idempotent
* one possible solution is that `x` is in `cs1`, which is what we assume in this case.
* That strategy is sound but not complete.
*
* If `tm` is some other map, we don't know how to handle this case. For now,
* we simply refuse to handle other maps. If they do need to be handled,
* `OtherMapped` provides some approximation to a solution, but it is neither
* sound nor complete.
*/
def map(tm: TypeMap)(using Context): CaptureSet = tm match
case tm: BiTypeMap =>
val mappedElems = elems.map(tm.forward)
if isConst then
if mappedElems == elems then this
else Const(mappedElems)
else BiMapped(asVar, tm, mappedElems)
case tm: IdentityCaptRefMap =>
this
case _ =>
val mapped = mapRefs(elems, tm, tm.variance)
if isConst then
if mapped.isConst && mapped.elems == elems && !mapped.keepAlways then this
else mapped
else Mapped(asVar, tm, tm.variance, mapped)
/** A mapping resulting from substituting parameters of a BindingType to a list of types */
def substParams(tl: BindingType, to: List[Type])(using Context) =
map(Substituters.SubstParamsMap(tl, to))
def maybe(using Context): CaptureSet = map(MaybeMap())
/** Invoke handler if this set has (or later aquires) the root capability `cap` */
def disallowRootCapability(handler: () => Context ?=> Unit)(using Context): this.type =
if isUnboxable then handler()
this
/** Invoke handler on the elements to ensure wellformedness of the capture set.
* The handler might add additional elements to the capture set.
*/
def ensureWellformed(handler: CaptureRef => Context ?=> Unit)(using Context): this.type =
elems.foreach(handler(_))
this
/** An upper approximation of this capture set, i.e. a constant set that is
* subcaptured by this set. If the current set is a variable
* it is the intersection of all upper approximations of known supersets
* of the variable.
* The upper approximation is meaningful only if it is constant. If not,
* `upperApprox` can return an arbitrary capture set variable.
* `upperApprox` is used in `solve`.
*/
protected def upperApprox(origin: CaptureSet)(using Context): CaptureSet
/** Assuming set this set dependds on was just solved to be constant, propagate this info
* to this set. This might result in the set being solved to be constant
* itself.
*/
protected def propagateSolved()(using Context): Unit = ()
/** This capture set with a description that tells where it comes from */
def withDescription(description: String): CaptureSet
/** The provided description (set via `withDescription`) for this capture set or else "" */
def description: String
/** More info enabled by -Y flags */
def optionalInfo(using Context): String = ""
/** A regular @retains or @retainsByName annotation with the elements of this set as arguments. */
def toRegularAnnotation(cls: Symbol)(using Context): Annotation =
Annotation(CaptureAnnotation(this, boxed = false)(cls).tree)
override def toText(printer: Printer): Text =
printer.toTextCaptureSet(this) ~~ description
object CaptureSet:
type Refs = SimpleIdentitySet[CaptureRef]
type Vars = SimpleIdentitySet[Var]
type Deps = SimpleIdentitySet[CaptureSet]
@sharable private var varId = 0
/** If set to `true`, capture stack traces that tell us where sets are created */
private final val debugSets = false
private val emptySet = SimpleIdentitySet.empty
/** The empty capture set `{}` */
val empty: CaptureSet.Const = Const(emptySet)
/** The universal capture set `{cap}` */
def universal(using Context): CaptureSet =
defn.captureRoot.termRef.singletonCaptureSet
/** Used as a recursion brake */
@sharable private[dotc] val Pending = Const(SimpleIdentitySet.empty)
def apply(elems: CaptureRef*)(using Context): CaptureSet.Const =
if elems.isEmpty then empty
else Const(SimpleIdentitySet(elems.map(_.normalizedRef.ensuring(_.isTrackableRef))*))
def apply(elems: Refs)(using Context): CaptureSet.Const =
if elems.isEmpty then empty else Const(elems)
/** The subclass of constant capture sets with given elements `elems` */
class Const private[CaptureSet] (val elems: Refs, val description: String = "") extends CaptureSet:
def isConst = true
def isAlwaysEmpty = elems.isEmpty
def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult =
CompareResult.Fail(this :: Nil)
def addDependent(cs: CaptureSet)(using Context, VarState) = CompareResult.OK
def upperApprox(origin: CaptureSet)(using Context): CaptureSet = this
def withDescription(description: String): Const = Const(elems, description)
def level = undefinedLevel
def owner = NoSymbol
override def toString = elems.toString
end Const
case class EmptyWithProvenance(ref: CaptureRef, mapped: Type) extends Const(SimpleIdentitySet.empty):
override def optionalInfo(using Context): String =
if ctx.settings.YccDebug.value
then i" under-approximating the result of mapping $ref to $mapped"
else ""
/** A special capture set that gets added to the types of symbols that were not
* themselves capture checked, in order to admit arbitrary corresponding capture
* sets in subcapturing comparisons. Similar to platform types for explicit
* nulls, this provides more lenient checking against compilation units that
* were not yet compiled with capture checking on.
*/
object Fluid extends Const(emptySet):
override def isAlwaysEmpty = false
override def addThisElem(elem: CaptureRef)(using Context, VarState) = CompareResult.OK
override def accountsFor(x: CaptureRef)(using Context): Boolean = true
override def mightAccountFor(x: CaptureRef)(using Context): Boolean = true
override def toString = "<fluid>"
end Fluid
/** The subclass of captureset variables with given initial elements */
class Var(override val owner: Symbol = NoSymbol, initialElems: Refs = emptySet, val level: Level = undefinedLevel, underBox: Boolean = false)(using @constructorOnly ictx: Context) extends CaptureSet:
/** A unique identification number for diagnostics */
val id =
varId += 1
varId
//assert(id != 40)
/** A variable is solved if it is aproximated to a from-then-on constant set. */
private var isSolved: Boolean = false
/** The elements currently known to be in the set */
var elems: Refs = initialElems
/** The sets currently known to be dependent sets (i.e. new additions to this set
* are propagated to these dependent sets.)
*/
var deps: Deps = emptySet
def isConst = isSolved
def isAlwaysEmpty = isSolved && elems.isEmpty
def isMaybeSet = false // overridden in BiMapped
/** A handler to be invoked if the root reference `cap` is added to this set */
var rootAddedHandler: () => Context ?=> Unit = () => ()
private[CaptureSet] var noUniversal = false
/** A handler to be invoked when new elems are added to this set */
var newElemAddedHandler: CaptureRef => Context ?=> Unit = _ => ()
var description: String = ""
/** Record current elements in given VarState provided it does not yet
* contain an entry for this variable.
*/
private def recordElemsState()(using VarState): Boolean =
varState.getElems(this) match
case None => varState.putElems(this, elems)
case _ => true
/** Record current dependent sets in given VarState provided it does not yet
* contain an entry for this variable.
*/
private[CaptureSet] def recordDepsState()(using VarState): Boolean =
varState.getDeps(this) match
case None => varState.putDeps(this, deps)
case _ => true
/** Reset elements to what was recorded in `state` */
def resetElems()(using state: VarState): Unit =
elems = state.elems(this)
/** Reset dependent sets to what was recorded in `state` */
def resetDeps()(using state: VarState): Unit =
deps = state.deps(this)
final def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult =
if isConst // Fail if variable is solved,
|| !recordElemsState() // or given VarState is frozen,
|| Existential.isBadExistential(elem) // or `elem` is an out-of-scope existential,
then
CompareResult.Fail(this :: Nil)
else if !levelOK(elem) then
CompareResult.LevelError(this, elem) // or `elem` is not visible at the level of the set.
else
//if id == 34 then assert(!elem.isUniversalRootCapability)
assert(elem.isTrackableRef, elem)
elems += elem
if elem.isRootCapability then
rootAddedHandler()
newElemAddedHandler(elem)
val normElem = if isMaybeSet then elem else elem.stripMaybe
// assert(id != 5 || elems.size != 3, this)
val res = (CompareResult.OK /: deps): (r, dep) =>
r.andAlso(dep.tryInclude(normElem, this))
res.orElse:
elems -= elem
res.addToTrace(this)
private def levelOK(elem: CaptureRef)(using Context): Boolean =
if elem.isRootCapability || Existential.isExistentialVar(elem) then
!noUniversal
else elem match
case elem: TermRef if level.isDefined =>
elem.symbol.ccLevel <= level
case elem: ThisType if level.isDefined =>
elem.cls.ccLevel.nextInner <= level
case ReachCapability(elem1) =>
levelOK(elem1)
case MaybeCapability(elem1) =>
levelOK(elem1)
case _ =>
true
def addDependent(cs: CaptureSet)(using Context, VarState): CompareResult =
if (cs eq this) || cs.isUniversal || isConst then
CompareResult.OK
else if recordDepsState() then
deps += cs
CompareResult.OK
else
CompareResult.Fail(this :: Nil)
override def disallowRootCapability(handler: () => Context ?=> Unit)(using Context): this.type =
noUniversal = true
rootAddedHandler = handler
super.disallowRootCapability(handler)
override def ensureWellformed(handler: CaptureRef => (Context) ?=> Unit)(using Context): this.type =
newElemAddedHandler = handler
super.ensureWellformed(handler)
private var computingApprox = false
/** Roughly: the intersection of all constant known supersets of this set.
* The aim is to find an as-good-as-possible constant set that is a superset
* of this set. The universal set {cap} is a sound fallback.
*/
final def upperApprox(origin: CaptureSet)(using Context): CaptureSet =
if isConst then
this
else if elems.exists(_.isRootCapability) || computingApprox then
universal
else
computingApprox = true
try
val approx = computeApprox(origin).ensuring(_.isConst)
if approx.elems.exists(Existential.isExistentialVar(_)) then
ccState.approxWarnings +=
em"""Capture set variable $this gets upper-approximated
|to existential variable from $approx, using {cap} instead."""
universal
else approx
finally computingApprox = false
/** The intersection of all upper approximations of dependent sets */
protected def computeApprox(origin: CaptureSet)(using Context): CaptureSet =
(universal /: deps) { (acc, sup) => acc ** sup.upperApprox(this) }
/** Widen the variable's elements to its upper approximation and
* mark it as constant from now on. This is used for contra-variant type variables
* in the results of defs and vals.
*/
def solve()(using Context): Unit =
if !isConst then
val approx = upperApprox(empty)
.showing(i"solve $this = $result", capt)
//println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}")
val newElems = approx.elems -- elems
if tryInclude(newElems, empty)(using ctx, VarState()).isOK then
markSolved()
/** Mark set as solved and propagate this info to all dependent sets */
def markSolved()(using Context): Unit =
isSolved = true
deps.foreach(_.propagateSolved())
def withDescription(description: String): this.type =
this.description = this.description.join(" and ", description)
this
/** Adds variables to the ShownVars context property if that exists, which
* establishes a record of all variables printed in an error message.
* Returns variable `ids` under -Ycc-debug, and owner/nesting level info
* under -Yprint-level.
*/
override def optionalInfo(using Context): String =
for vars <- ctx.property(ShownVars) do vars += this
val debugInfo =
if !ctx.settings.YccDebug.value then ""
else if isConst then ids ++ "(solved)"
else ids
val limitInfo =
if ctx.settings.YprintLevel.value && level.isDefined
then i"<at level ${level.toString}>"
else ""
debugInfo ++ limitInfo
/** Used for diagnostics and debugging: A string that traces the creation
* history of a variable by following source links. Each variable on the
* path is characterized by the variable's id and the first letter of the
* variable's class name. The path ends in a plain variable with letter `V` that
* is not derived from some other variable.
*/
protected def ids(using Context): String =
val trail = this.match
case dv: DerivedVar => dv.source.ids
case _ => ""
val descr = getClass.getSimpleName.nn.take(1)
s"$id$descr$trail"
override def toString = s"Var$id$elems"
end Var
/** Variables that represent refinements of class parameters can have the universal
* capture set, since they represent only what is the result of the constructor.
* Test case: Without that tweak, logger.scala would not compile.
*/
class RefiningVar(owner: Symbol)(using Context) extends Var(owner):
override def disallowRootCapability(handler: () => Context ?=> Unit)(using Context) = this
/** A variable that is derived from some other variable via a map or filter. */
abstract class DerivedVar(owner: Symbol, initialElems: Refs)(using @constructorOnly ctx: Context)
extends Var(owner, initialElems):
// For debugging: A trace where a set was created. Note that logically it would make more
// sense to place this variable in Mapped, but that runs afoul of the initializatuon checker.
val stack = if debugSets && this.isInstanceOf[Mapped] then (new Throwable).getStackTrace().nn.take(20) else null
/** The variable from which this variable is derived */
def source: Var
addAsDependentTo(source)
override def propagateSolved()(using Context) =
if source.isConst && !isConst then markSolved()
end DerivedVar
/** A variable that changes when `source` changes, where all additional new elements are mapped
* using ∪ { tm(x) | x <- source.elems }.
* @param source the original set that is mapped
* @param tm the type map, which is assumed to be idempotent on capture refs
* (except if ccUnsoundMaps is enabled)
* @param variance the assumed variance with which types with capturesets of size >= 2 are approximated
* (i.e. co: full capture set, contra: empty set, nonvariant is not allowed.)
* @param initial The initial mappings of source's elements at the point the Mapped set is created.
*/
class Mapped private[CaptureSet]
(val source: Var, tm: TypeMap, variance: Int, initial: CaptureSet)(using @constructorOnly ctx: Context)
extends DerivedVar(source.owner, initial.elems):
addAsDependentTo(initial) // initial mappings could change by propagation
private def mapIsIdempotent = tm.isInstanceOf[IdempotentCaptRefMap]
assert(ccConfig.allowUnsoundMaps || mapIsIdempotent, tm.getClass)
private def whereCreated(using Context): String =
if stack == null then ""
else i"""
|Stack trace of variable creation:"
|${stack.mkString("\n")}"""
override def tryInclude(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult =
def propagate: CompareResult =
if (origin ne source) && (origin ne initial) && mapIsIdempotent then
// `tm` is idempotent, propagate back elems from image set.
// This is sound, since we know that for `r in newElems: tm(r) = r`, hence
// `r` is _one_ possible solution in `source` that would make an `r` appear in this set.
// It's not necessarily the only possible solution, so the scheme is incomplete.
source.tryInclude(elem, this)
else if ccConfig.allowUnsoundMaps && !mapIsIdempotent
&& variance <= 0 && !origin.isConst && (origin ne initial) && (origin ne source)
then
// The map is neither a BiTypeMap nor an idempotent type map.
// In that case there's no much we can do.
// The scheme then does not propagate added elements back to source and rejects adding
// elements from variable sources in contra- and non-variant positions. In essence,
// we approximate types resulting from such maps by returning a possible super type
// from the actual type. But this is neither sound nor complete.
report.warning(em"trying to add $elem from unrecognized source $origin of mapped set $this$whereCreated")
CompareResult.Fail(this :: Nil)
else
CompareResult.OK
def propagateIf(cond: Boolean): CompareResult =
if cond then propagate else CompareResult.OK
val mapped = extrapolateCaptureRef(elem, tm, variance)
def isFixpoint =
mapped.isConst && mapped.elems.size == 1 && mapped.elems.contains(elem)
def failNoFixpoint =
val reason =
if variance <= 0 then i"the set's variance is $variance"
else i"$elem gets mapped to $mapped, which is not a supercapture."
report.warning(em"""trying to add $elem from unrecognized source $origin of mapped set $this$whereCreated
|The reference cannot be added since $reason""")
CompareResult.Fail(this :: Nil)
if origin eq source then // elements have to be mapped
val added = mapped.elems.filter(!accountsFor(_))
addNewElems(added)
.andAlso:
if mapped.isConst then CompareResult.OK
else if mapped.asVar.recordDepsState() then { addAsDependentTo(mapped); CompareResult.OK }
else CompareResult.Fail(this :: Nil)
.andAlso:
propagateIf(!added.isEmpty)
else if accountsFor(elem) then
CompareResult.OK
else if variance > 0 then
// we can soundly add nothing to source and `x` to this set
addNewElem(elem)
else if isFixpoint then
// We can soundly add `x` to both this set and source since `f(x) = x`
addNewElem(elem).andAlso(propagate)
else
// we are out of options; fail (which is always sound).
failNoFixpoint
end tryInclude
override def computeApprox(origin: CaptureSet)(using Context): CaptureSet =
if source eq origin then
// it's a mapping of origin, so not a superset of `origin`,
// therefore don't contribute to the intersection.
universal
else
source.upperApprox(this).map(tm)
override def propagateSolved()(using Context) =
if initial.isConst then super.propagateSolved()
override def toString = s"Mapped$id($source, elems = $elems)"
end Mapped
/** A mapping where the type map is required to be a bijection.
* Parameters as in Mapped.
*/
final class BiMapped private[CaptureSet]
(val source: Var, bimap: BiTypeMap, initialElems: Refs)(using @constructorOnly ctx: Context)
extends DerivedVar(source.owner, initialElems):
override def tryInclude(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult =
if origin eq source then
val mappedElem = bimap.forward(elem)
if accountsFor(mappedElem) then CompareResult.OK
else addNewElem(mappedElem)
else if accountsFor(elem) then
CompareResult.OK
else
source.tryInclude(bimap.backward(elem), this)
.showing(i"propagating new elem $elem backward from $this to $source = $result", captDebug)
.andAlso(addNewElem(elem))
/** For a BiTypeMap, supertypes of the mapped type also constrain
* the source via the inverse type mapping and vice versa. That is, if
* B = f(A) and B <: C, then A <: f^-1(C), so C should flow into
* the upper approximation of A.
* Conversely if A <: C2, then we also know that B <: f(C2).
* These situations are modeled by the two branches of the conditional below.
*/
override def computeApprox(origin: CaptureSet)(using Context): CaptureSet =
val supApprox = super.computeApprox(this)
if source eq origin then supApprox.map(bimap.inverse)
else source.upperApprox(this).map(bimap) ** supApprox
override def isMaybeSet: Boolean = bimap.isInstanceOf[MaybeMap]
override def toString = s"BiMapped$id($source, elems = $elems)"
end BiMapped
/** A variable with elements given at any time as { x <- source.elems | p(x) } */
class Filtered private[CaptureSet]
(val source: Var, p: Context ?=> CaptureRef => Boolean)(using @constructorOnly ctx: Context)
extends DerivedVar(source.owner, source.elems.filter(p)):
override def tryInclude(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult =
if accountsFor(elem) then
CompareResult.OK
else if origin eq source then
if p(elem) then addNewElem(elem)
else CompareResult.OK
else
// Filtered elements have to be back-propagated to source.
// Elements that don't satisfy `p` are not allowed.
if p(elem) then source.tryInclude(elem, this)
else CompareResult.Fail(this :: Nil)
override def computeApprox(origin: CaptureSet)(using Context): CaptureSet =
if source eq origin then
// it's a filter of origin, so not a superset of `origin`,
// therefore don't contribute to the intersection.
universal
else
source.upperApprox(this).filter(p)
override def toString = s"${getClass.getSimpleName}$id($source, elems = $elems)"
end Filtered
/** A variable with elements given at any time as { x <- source.elems | !other.accountsFor(x) } */
class Diff(source: Var, other: Const)(using Context)
extends Filtered(source, !other.accountsFor(_))
class Union(cs1: CaptureSet, cs2: CaptureSet)(using Context)
extends Var(initialElems = cs1.elems ++ cs2.elems):
addAsDependentTo(cs1)
addAsDependentTo(cs2)
override def tryInclude(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult =
if accountsFor(elem) then CompareResult.OK
else
val res = super.tryInclude(elem, origin)
// If this is the union of a constant and a variable,
// propagate `elem` to the variable part to avoid slack
// between the operands and the union.
if res.isOK && (origin ne cs1) && (origin ne cs2) then
if cs1.isConst then cs2.tryInclude(elem, origin)
else if cs2.isConst then cs1.tryInclude(elem, origin)
else res
else res
override def propagateSolved()(using Context) =
if cs1.isConst && cs2.isConst && !isConst then markSolved()
end Union
class Intersection(cs1: CaptureSet, cs2: CaptureSet)(using Context)
extends Var(initialElems = elemIntersection(cs1, cs2)):
addAsDependentTo(cs1)
addAsDependentTo(cs2)
deps += cs1
deps += cs2
override def tryInclude(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult =
val present =
if origin eq cs1 then cs2.accountsFor(elem)
else if origin eq cs2 then cs1.accountsFor(elem)
else true
if present && !accountsFor(elem) then addNewElem(elem)
else CompareResult.OK
override def computeApprox(origin: CaptureSet)(using Context): CaptureSet =
if (origin eq cs1) || (origin eq cs2) then
// it's a combination of origin with some other set, so not a superset of `origin`,
// therefore don't contribute to the intersection.
universal
else
CaptureSet(elemIntersection(cs1.upperApprox(this), cs2.upperApprox(this)))
override def propagateSolved()(using Context) =
if cs1.isConst && cs2.isConst && !isConst then markSolved()
end Intersection
def elemIntersection(cs1: CaptureSet, cs2: CaptureSet)(using Context): Refs =
cs1.elems.filter(cs2.mightAccountFor) ++ cs2.elems.filter(cs1.mightAccountFor)
/** Extrapolate tm(r) according to `variance`. Let r1 be the result of tm(r).
* - If r1 is a tracked CaptureRef, return {r1}
* - If r1 has an empty capture set, return {}
* - Otherwise,
* - if the variance is covariant, return r1's capture set
* - if the variance is contravariant, return {}
* - Otherwise assertion failure
*/
def extrapolateCaptureRef(r: CaptureRef, tm: TypeMap, variance: Int)(using Context): CaptureSet =
val r1 = tm(r)
val upper = r1.captureSet
def isExact =
upper.isAlwaysEmpty
|| upper.isConst && upper.elems.size == 1 && upper.elems.contains(r1)
|| r.derivesFrom(defn.Caps_CapSet)
if variance > 0 || isExact then upper
else if variance < 0 then CaptureSet.EmptyWithProvenance(r, r1)
else upper.maybe
/** Apply `f` to each element in `xs`, and join result sets with `++` */
def mapRefs(xs: Refs, f: CaptureRef => CaptureSet)(using Context): CaptureSet =
((empty: CaptureSet) /: xs)((cs, x) => cs ++ f(x))
/** Apply extrapolated `tm` to each element in `xs`, and join result sets with `++` */
def mapRefs(xs: Refs, tm: TypeMap, variance: Int)(using Context): CaptureSet =
mapRefs(xs, extrapolateCaptureRef(_, tm, variance))
/** Return true iff
* - arg1 is a TypeBounds >: CL T <: CH T of two capturing types with equal parents.
* - arg2 is a capturing type CA U
* - CH <: CA <: CL
* In other words, we can unify CL, CH and CA.
*/
def subCapturesRange(arg1: TypeBounds, arg2: Type)(using Context): Boolean = arg1 match
case TypeBounds(CapturingType(lo, loRefs), CapturingType(hi, hiRefs)) if lo =:= hi =>
given VarState = VarState()
val cs2 = arg2.captureSet
hiRefs.subCaptures(cs2).isOK && cs2.subCaptures(loRefs).isOK
case _ =>
false
/** A TypeMap with the property that every capture reference in the image
* of the map is mapped to itself. I.e. for all capture references r1, r2,
* if M(r1) == r2 then M(r2) == r2.
*/
trait IdempotentCaptRefMap extends TypeMap
/** A TypeMap that is the identity on capture references */
trait IdentityCaptRefMap extends TypeMap
enum CompareResult extends Showable:
case OK
case Fail(trace: List[CaptureSet])
case LevelError(cs: CaptureSet, elem: CaptureRef)
override def toText(printer: Printer): Text =
inContext(printer.printerContext):
this match
case OK => Str("OK")
case Fail(trace) =>
if ctx.settings.YccDebug.value then printer.toText(trace, ", ")
else blocking.show
case LevelError(cs: CaptureSet, elem: CaptureRef) =>
Str(i"($elem at wrong level for $cs at level ${cs.level.toString})")
/** The result is OK */
def isOK: Boolean = this == OK
/** If not isOK, the blocking capture set */
def blocking: CaptureSet = (this: @unchecked) match
case Fail(cs) => cs.last
case LevelError(cs, _) => cs
/** Optionally, this result if it is a level error */
def levelError: Option[LevelError] = this match
case result: LevelError => Some(result)
case _ => None
inline def andAlso(op: Context ?=> CompareResult)(using Context): CompareResult =
if isOK then op else this
inline def orElse(op: Context ?=> CompareResult)(using Context): CompareResult =
if isOK then this
else
val alt = op
if alt.isOK then alt
else this
inline def addToTrace(cs: CaptureSet): CompareResult = this match
case Fail(trace) => Fail(cs :: trace)
case _ => this
end CompareResult
/** A VarState serves as a snapshot mechanism that can undo
* additions of elements or super sets if an operation fails
*/
class VarState:
/** A map from captureset variables to their elements at the time of the snapshot. */
private val elemsMap: util.EqHashMap[Var, Refs] = new util.EqHashMap
/** A map from captureset variables to their dependent sets at the time of the snapshot. */
private val depsMap: util.EqHashMap[Var, Deps] = new util.EqHashMap
/** The recorded elements of `v` (it's required that a recording was made) */
def elems(v: Var): Refs = elemsMap(v)
/** Optionally the recorded elements of `v`, None if nothing was recorded for `v` */
def getElems(v: Var): Option[Refs] = elemsMap.get(v)
/** Record elements, return whether this was allowed.
* By default, recording is allowed but the special state FrozenState
* overrides this.
*/
def putElems(v: Var, elems: Refs): Boolean = { elemsMap(v) = elems; true }
/** The recorded dependent sets of `v` (it's required that a recording was made) */
def deps(v: Var): Deps = depsMap(v)
/** Optionally the recorded dependent sets of `v`, None if nothing was recorded for `v` */
def getDeps(v: Var): Option[Deps] = depsMap.get(v)
/** Record dependent sets, return whether this was allowed.
* By default, recording is allowed but the special state FrozenState
* overrides this.
*/
def putDeps(v: Var, deps: Deps): Boolean = { depsMap(v) = deps; true }
/** Roll back global state to what was recorded in this VarState */
def rollBack(): Unit =
elemsMap.keysIterator.foreach(_.resetElems()(using this))
depsMap.keysIterator.foreach(_.resetDeps()(using this))
end VarState
/** A special state that does not allow to record elements or dependent sets.
* In effect this means that no new elements or dependent sets can be added
* in this state (since the previous state cannot be recorded in a snapshot)
*/
@sharable
object FrozenState extends VarState: