Skip to content

Commit b5ecaa0

Browse files
authored
Description of capture set constraint handling (#18966)
... and a proposal how to better handle invariant mapped capture set variables
2 parents c50f2ff + 4e84ac4 commit b5ecaa0

File tree

4 files changed

+134
-20
lines changed

4 files changed

+134
-20
lines changed

compiler/src/dotty/tools/dotc/cc/CaptureSet.scala

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -701,24 +701,17 @@ object CaptureSet:
701701
if mapped.isConst then CompareResult.OK
702702
else if mapped.asVar.recordDepsState() then { addAsDependentTo(mapped); CompareResult.OK }
703703
else CompareResult.Fail(this :: Nil)
704-
else if !isFixpoint then
705-
// We did not yet observe the !isFixpoint condition in our tests, but it's a
706-
// theoretical possibility. In that case, it would be inconsistent to both
707-
// add `elem` to the set and back-propagate it. But if `{elem} <:< tm(elem)`
708-
// and the variance of the set is positive, we can soundly add `tm(ref)` to
709-
// the set while back-propagating `ref` as before. Otherwise there's nothing
710-
// obvious left to do except fail (which is always sound).
711-
if variance > 0
712-
&& elem.singletonCaptureSet.subCaptures(mapped, frozen = true).isOK then
713-
// widen to fixpoint. mapped is known to be a fixpoint since tm is idempotent.
714-
// The widening is sound, but loses completeness.
715-
addMapped
716-
else
717-
failNoFixpoint
718704
else if accountsFor(elem) then
719705
CompareResult.OK
720-
else
706+
else if variance > 0 then
707+
// we can soundly add nothing to source and `x` to this set
708+
addNewElem(elem)
709+
else if isFixpoint then
710+
// We can soundly add `x` to both this set and source since `f(x) = x`
721711
addNewElem(elem).andAlso(propagate)
712+
else
713+
// we are out of options; fail (which is always sound).
714+
failNoFixpoint
722715
end tryInclude
723716

724717
override def computeApprox(origin: CaptureSet)(using Context): CaptureSet =
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
Handling invariant capture set variables
2+
========================================
3+
4+
This note gives a high-level overview of constraints and constraint solving in Scala's capture checker. It then describes a proposal to improve a current problem area: the handling of invariantly mapped capture set variables.
5+
6+
A capture checking constraint consists of subcapturing constraints between
7+
capture sets. Capture sets can be variable or constant. A capture set variable is either defined by itself,
8+
or is the result of a mapping of a type map.
9+
Depending on what part of an original type created a map result, the set `v` is classified as invariant, covariant, or contravariant.
10+
11+
We can describe the syntax of capture set constraints like this:
12+
```
13+
Capture set a, b, c = ac // constant, aliases bv, cc
14+
| av // variable, aliases bv, cv
15+
16+
Typemap tm = B // bijective on capabilities
17+
| F // arbitrary
18+
19+
Constraint C = a <: b // simple constraint
20+
| a = B(b) // bimap constraint
21+
| a >: F(b) // covariant map constraint
22+
| a <: F(b) // contravariant map constraint
23+
| a = F(b) // invariant map constraint
24+
```
25+
Variables appearing in the left hand side of a map constraint are called
26+
map results. In a constraint set, each map result variable appears on the left hand side of only one map constraint, the one which defines it.
27+
We distinguish two kinds of type maps, bijective maps `B` and general maps `F`. General maps are capture-monotonic: if `A <: B`
28+
then the capture set of `F(A)` subcaptures the capture set of `F(B)`. All considered type maps map `cap` to `cap`.
29+
30+
Examples of bijective maps are substitutions of parameters of one method type
31+
for another, or mapping local method parameter symbols to method parameters.
32+
Examples of monotonic maps are substitutions of argument types for parameters,
33+
avoidance, or as-seen-from.
34+
35+
Bijective map constraints define variables exactly. General map constraints come in three different forms. In the form `a >: F(b)`, `a` is bounded from
36+
below by the map's result. Such constraints are created when a part
37+
of a type appearing in covariant position is mapped. Consequently, variables
38+
bound that way are called _covariant_. _Contravariant_ variables are defined
39+
by constraints of the `a <: F(b)`. Such constraints are created when a part
40+
of a type appearing in contravariant position is mapped. Finally, _invariant_ variables are defined by constraints of the `a = F(b)`, which are created when a part of a type appearing in invariant position is mapped.
41+
42+
A solution to a subcapturing constraint set consists of an assignment of capture set constants to capture set variables, so that the resulting constraint set is true. The capture checker uses an incremental propagation-based
43+
solver where capture set variables record which references they are known to contain. When new elements
44+
are added to a set or a new relationship between capture sets is added, elements are propagated until
45+
a new partial solution is found, or a contradiction is detected. A contradiction arises when an attempt
46+
is made to add an element to a constant set that is not accounted for by the set.
47+
48+
The propagation actions depend on the kind of constraint and whether a new capture set `c` is propagated
49+
to the left part `a` or the right part `b` of the constraint. In the cases where the constraint
50+
involves a general mapping `F`, the mapping `F(r)` of a capability `r` is in general a type, not
51+
a capability or capture set. In this case we use `F(r).CS` for the capture set of the type
52+
`F(r)`. Likewise, if `c` is a capture set, then `F(c)` might be a capture set `c'` if `F` maps all
53+
capabilities in `c` to capabilities. But in general it will be a set of types, and `F(c).CS`
54+
denotes the set of capture sets of these types. Furthermore, we use `c.super` for the capture set
55+
of the definition of the reference `c` (as it shows up in rule (sc-var)).
56+
57+
The propagation rules are summarized as follows:
58+
59+
```
60+
Constraint new relation propagation remark
61+
---------------------------------------------------------------
62+
a <: b c <: a c <: b
63+
c <: b none
64+
65+
a = B(b) c <: a B^-1(c) <: b
66+
c <: b B(c) <: a
67+
68+
a >: F(b) c <: a none
69+
c <: b F(c).CS <: a
70+
71+
a <: F(b) c <: a c <: b if F(c) = c (1)
72+
c.super <: a otherwise
73+
c <: b none
74+
75+
a = F(b) c <: a c <: b if F(c) = c (1)
76+
c.super <: a otherwise
77+
c <: b c' <: a if F(c) = c'
78+
F(c).CS? <: a otherwise (2)
79+
```
80+
Remarks:
81+
82+
(1) If `F(c) = c`, we solve by adding `c` also to `b`. Otherwise
83+
we try again with `c.super <: a` instead of `c <: a`. At some point
84+
this will succeed since we assume that for all maps `F`, `F(cap) = cap`.
85+
This is clearly sound, but loses completeness since there might be
86+
other, smaller solutions for the constraint. But with `F` not being
87+
bijective, we'd have to try all possible capture sets to find
88+
a suitable set that we could add to `b` so that `c` subcaptures the mapped set.
89+
90+
(2) is the hard case. If the target set `a` appears invariantly, and `F(x)` is a type, we can use neither the lower bound set `{}` nor the upper bound set `F(x).CS`. Previously, we addressed this problem by making sure case (2) could not arise using the _range_ mechanism, which is also employed for regular type inference. To explain, let's say we try to map some type `t` where `F` is not defined on `t` but we know lower and upper approximations `tl` and `tu`. If `t` appears in
91+
co- or contravariant positions, we can return `tu` or `tl`, respectively. But if `t`
92+
appears in invariant position, we return `Range(tl, tu)`. This is not a type, but a type interval
93+
which will be mapped itself, and will be resolved to one of its bounds at the first
94+
point further out where the variance is positive or negative. There's also the case
95+
where a Range appears in argument position of a parameterized class type, where we can
96+
map it to the wildcard type `? >: tl <: tu`. We used the same mechanism for capture
97+
set inference, by converting invariant occurrences of mapped type arguments `t^v` to wildcard types `? >: t^v1 <: t^v2` proactively, splitting the variable `v` to `v1` and `v2`, so that all capture set constraints occurred at co- or contravariant positions.
98+
99+
Unfortunately this splitting has bad consequences for type inference, since a wildcard
100+
is usually not compatible with a single expected type. So we would like to find another
101+
technique for solving this issue.
102+
103+
The idea is to use a technique similar to ranges directly on the capture sets. This means
104+
that we now have a new class of elements of capture sets standing for references that might or might not be in the set. We use `{x?, ...}` as notation for a reference `x` that might or might not be an element of the enclosing capture set. Like `Range`, the notation is purely internal, we
105+
never write it in a source program.
106+
107+
We now describe how to handle the missing case (2) above. Assume `F(x)` is not a tracked reference, its image `a` is invariant and its capture set `cfr` is `{x_1,...,x_n}` where `n > 0`. In this case we add to `a` the _maybe references_ `x_1?, ..., x_n?`. Generalized to sets, `c <: b` where
108+
`F(c) != c` leads to `F(c).CS?` to be added to `a`. Here, `CS?` is the set consisting of all references `x` in `cs` converted to maybe references `x?`.
109+
110+
These references behave as follows:
111+
112+
- For unions, `x` subsumes `x?`, so adding both `x` and `x?` to a set means
113+
only `x` needs to be addded.
114+
115+
- For further propagation of elements up the chain in e.g. `a <: b`, a maybe reference `x?` behaves like `x`. That is, since the reference could be in the set we have to propagate it upwards to be sound.
116+
117+
- For propagations from lower sets, a maybe reference is treated as if it was missing. For instance, if we have `a <: b`, `b` contains `x?` and we propagate `x` from `a` to `b`, we need to add `x` as usual to `b`, which might imply back-proagation in the case `b` is an image of a type map `F`.
118+
119+
- For mappings, maybe references are handled depending on the variance of the
120+
image set. Assume we add `x?` to `b`. If `a >: F(b)` this amounts
121+
to adding `F(x).CS` to `a`. If `a` is contravariant, this amounts to doing nothing. If `a` is invariant, we add `F(x).CS?` to `a`. If
122+
`CS` is a capture set variable, it's "maybe" status has to be recorded and all elements this set acquires in the future also have to be converted to maybe references.
123+
124+
125+

tests/neg-custom-args/captures/usingLogFile.check

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,3 @@
1010
44 | val later = usingFile("out", f => (y: Int) => xs.foreach(x => f.write(x + y))) // error
1111
| ^^^^^^^^^
1212
| local reference f leaks into outer capture set of type parameter T of method usingFile
13-
-- Error: tests/neg-custom-args/captures/usingLogFile.scala:52:16 ------------------------------------------------------
14-
52 | val later = usingFile("logfile", // error !!! but should be ok, since we can widen `l` to `file` instead of to `cap`
15-
| ^^^^^^^^^
16-
| local reference l leaks into outer capture set of type parameter T of method usingFile

tests/neg-custom-args/captures/usingLogFile.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,6 @@ object Test3:
4949
op(logger)
5050

5151
def test =
52-
val later = usingFile("logfile", // error !!! but should be ok, since we can widen `l` to `file` instead of to `cap`
52+
val later = usingFile("logfile", // now ok
5353
usingLogger(_, l => () => l.log("test")))
5454
later()

0 commit comments

Comments
 (0)