|
| 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 | + |
0 commit comments