Skip to content

Commit 66b14d3

Browse files
authored
Implement inheritance condition for Mutable types (#24253)
2 parents 8fc1798 + 97a2ef2 commit 66b14d3

File tree

4 files changed

+131
-225
lines changed

4 files changed

+131
-225
lines changed

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

Lines changed: 90 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -886,7 +886,7 @@ class CheckCaptures extends Recheck, SymTransformer:
886886
*/
887887
def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) =
888888
var refined: Type = core
889-
var allCaptures: CaptureSet = initCs ++ impliedByFields(core)
889+
var allCaptures: CaptureSet = initCs ++ captureSetImpliedByFields(cls, core)
890890
for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do
891891
val getter = cls.info.member(getterName).suchThat(_.isRefiningParamAccessor).symbol
892892
if !getter.is(Private) && getter.hasTrackedParts then
@@ -910,62 +910,61 @@ class CheckCaptures extends Recheck, SymTransformer:
910910
val (refined, cs) = addParamArgRefinements(core, initCs)
911911
refined.capturing(cs)
912912

913-
/** The additional capture set implied by the capture sets of its fields. This
914-
* is either empty or, if some fields have a terminal capability in their span
915-
* capture sets, it consists of a single fresh cap that subsumes all these terminal
916-
* capabiltities. Class parameters are not counted. If the type is a mutable type,
917-
* we add a fresh cap in any case -- this is because we can currently hide
918-
* mutability in array vals, an example is neg-customargs/captures/matrix.scala.
919-
*/
920-
def impliedByFields(core: Type): CaptureSet =
921-
var infos: List[String] = Nil
922-
def pushInfo(msg: => String) =
923-
if ctx.settings.YccVerbose.value then infos = msg :: infos
924-
925-
/** The classifiers of the fresh caps in the span capture sets of all fields
926-
* in the given class `cls`. Mutable types get at least a fresh classified
927-
* as mutable.
928-
*/
929-
def impliedClassifiers(cls: Symbol): List[ClassSymbol] = cls match
930-
case cls: ClassSymbol =>
931-
val fields = cls.info.decls.toList
932-
var fieldClassifiers =
933-
for
934-
sym <- fields if contributesFreshToClass(sym)
935-
case fresh: FreshCap <- sym.info.spanCaptureSet.elems
936-
.filter(_.isTerminalCapability)
937-
.map(_.stripReadOnly)
938-
.toList
939-
_ = pushInfo(i"Note: ${sym.showLocated} captures a $fresh")
940-
yield fresh.hiddenSet.classifier
941-
if cls.typeRef.isMutableType then
942-
fieldClassifiers = defn.Caps_Mutable :: fieldClassifiers
943-
val parentClassifiers =
944-
cls.parentSyms.map(impliedClassifiers).filter(_.nonEmpty)
945-
if fieldClassifiers.isEmpty && parentClassifiers.isEmpty
946-
then Nil
947-
else parentClassifiers.foldLeft(fieldClassifiers.distinct)(dominators)
948-
case _ => Nil
949-
950-
def fresh =
951-
FreshCap(Origin.NewInstance(core)).tap: fresh =>
952-
if ctx.settings.YccVerbose.value then
953-
pushInfo(i"Note: instance of $cls captures a $fresh that comes from a field")
954-
report.echo(infos.mkString("\n"), ctx.owner.srcPos)
955-
956-
knownFresh.getOrElseUpdate(cls, impliedClassifiers(cls)) match
957-
case Nil => CaptureSet.empty
958-
case cl :: Nil =>
959-
val result = fresh
960-
result.hiddenSet.adoptClassifier(cl)
961-
result.singletonCaptureSet
962-
case _ => fresh.singletonCaptureSet
963-
end impliedByFields
964-
965913
augmentConstructorType(resType, capturedVars(cls))
966914
.showing(i"constr type $mt with $argTypes%, % in $constr = $result", capt)
967915
end refineConstructorInstance
968916

917+
/** The additional capture set implied by the capture sets of its fields. This
918+
* is either empty or, if some fields have a terminal capability in their span
919+
* capture sets, it consists of a single fresh cap that subsumes all these terminal
920+
* capabiltities. Class parameters are not counted. If the type is a mutable type,
921+
* we add a fresh cap in any case -- this is because we can currently hide
922+
* mutability in array vals, an example is neg-customargs/captures/matrix.scala.
923+
*/
924+
def captureSetImpliedByFields(cls: ClassSymbol, core: Type)(using Context): CaptureSet =
925+
var infos: List[String] = Nil
926+
def pushInfo(msg: => String) =
927+
if ctx.settings.YccVerbose.value then infos = msg :: infos
928+
929+
/** The classifiers of the fresh caps in the span capture sets of all fields
930+
* in the given class `cls`.
931+
*/
932+
def impliedClassifiers(cls: Symbol): List[ClassSymbol] = cls match
933+
case cls: ClassSymbol =>
934+
var fieldClassifiers =
935+
for
936+
sym <- cls.info.decls.toList
937+
if contributesFreshToClass(sym)
938+
case fresh: FreshCap <- sym.info.spanCaptureSet.elems
939+
.filter(_.isTerminalCapability)
940+
.map(_.stripReadOnly)
941+
.toList
942+
_ = pushInfo(i"Note: ${sym.showLocated} captures a $fresh")
943+
yield fresh.hiddenSet.classifier
944+
if cls.typeRef.isMutableType then
945+
fieldClassifiers = defn.Caps_Mutable :: fieldClassifiers
946+
val parentClassifiers =
947+
cls.parentSyms.map(impliedClassifiers).filter(_.nonEmpty)
948+
if fieldClassifiers.isEmpty && parentClassifiers.isEmpty
949+
then Nil
950+
else parentClassifiers.foldLeft(fieldClassifiers.distinct)(dominators)
951+
case _ => Nil
952+
953+
def fresh =
954+
FreshCap(Origin.NewInstance(core)).tap: fresh =>
955+
if ctx.settings.YccVerbose.value then
956+
pushInfo(i"Note: instance of $cls captures a $fresh that comes from a field")
957+
report.echo(infos.mkString("\n"), ctx.owner.srcPos)
958+
959+
knownFresh.getOrElseUpdate(cls, impliedClassifiers(cls)) match
960+
case Nil => CaptureSet.empty
961+
case cl :: Nil =>
962+
val result = fresh
963+
result.hiddenSet.adoptClassifier(cl)
964+
result.singletonCaptureSet
965+
case _ => fresh.singletonCaptureSet
966+
end captureSetImpliedByFields
967+
969968
/** Recheck type applications:
970969
* - Map existential captures in result to `cap`
971970
* - include captures of called methods in environment
@@ -2139,7 +2138,38 @@ class CheckCaptures extends Recheck, SymTransformer:
21392138
end for
21402139
end checkEscapingUses
21412140

2142-
/** Check that arguments of TypeApplys and AppliedTypes conform to their bounds.
2141+
/** Check all parent class constructors of classes extending Mutable
2142+
* either also extend Mutable or are read-only.
2143+
*
2144+
* A parent class constructor is _read-only_ if the following conditions are met
2145+
* 1. The class does not retain any exclusive capabilities from its environment.
2146+
* 2. The constructor does not take arguments that retain exclusive capabilities.
2147+
* 3. The class does not does not have fields that retain exclusive universal capabilities.
2148+
*/
2149+
def checkMutableInheritance(cls: ClassSymbol, parents: List[Tree])(using Context): Unit =
2150+
if cls.derivesFrom(defn.Caps_Mutable) then
2151+
for parent <- parents do
2152+
if !parent.tpe.derivesFromMutable then
2153+
val pcls = parent.nuType.classSymbol
2154+
val parentIsExclusive =
2155+
if parent.isType then
2156+
capturedVars(pcls).isExclusive
2157+
|| captureSetImpliedByFields(pcls.asClass, parent.nuType).isExclusive
2158+
2159+
else parent.nuType.captureSet.isExclusive
2160+
if parentIsExclusive then
2161+
report.error(
2162+
em"""illegal inheritance: $cls which extends `Mutable` is not allowed to also extend $pcls
2163+
|since $pcls retains exclusive capabilities but does not extend `Mutable`.""",
2164+
parent.srcPos)
2165+
2166+
/** Checks to run after the rechecking pass:
2167+
* - Check that arguments of TypeApplys and AppliedTypes conform to their bounds.
2168+
* - Check that no uses refer to reach capabilities of parameters of enclosing
2169+
* methods or classes.
2170+
* - Run the separation checker under language.experimental.separationChecking
2171+
* - Check that classes extending Mutable do not extend other classes that do
2172+
* not extend Mutable yet retain exclusive capabilities
21432173
*/
21442174
def postCheck(unit: tpd.Tree)(using Context): Unit =
21452175
val checker = new TreeTraverser:
@@ -2150,6 +2180,7 @@ class CheckCaptures extends Recheck, SymTransformer:
21502180
trace(i"post check $tree"):
21512181
traverseChildren(tree)(using lctx)
21522182
check(tree)
2183+
21532184
def check(tree: Tree)(using Context) = tree match
21542185
case TypeApply(fun, args) =>
21552186
fun.nuType.widen match
@@ -2162,8 +2193,9 @@ class CheckCaptures extends Recheck, SymTransformer:
21622193
if ccConfig.postCheckCapturesets then
21632194
args.lazyZip(tl.paramNames).foreach(checkTypeParam(_, _, fun.symbol))
21642195
case _ =>
2196+
case TypeDef(_, impl: Template) =>
2197+
checkMutableInheritance(tree.symbol.asClass, impl.parents)
21652198
case _ =>
2166-
end check
21672199
end checker
21682200

21692201
checker.traverse(unit)(using ctx.withOwner(defn.RootClass))
@@ -2173,19 +2205,20 @@ class CheckCaptures extends Recheck, SymTransformer:
21732205
usedSet(tree) = tree.markedFree ++ cs
21742206
ccState.inSepCheck:
21752207
SepCheck(this).traverse(unit)
2208+
21762209
if !ctx.reporter.errorsReported then
21772210
// We dont report errors here if previous errors were reported, because other
21782211
// errors often result in bad applied types, but flagging these bad types gives
21792212
// often worse error messages than the original errors.
2180-
val checkApplied = new TreeTraverser:
2213+
val checkAppliedTypes = new TreeTraverser:
21812214
def traverse(t: Tree)(using Context) = t match
21822215
case tree: InferredTypeTree =>
21832216
case tree: New =>
21842217
case tree: TypeTree =>
21852218
withCollapsedFresh:
21862219
checkAppliedTypesIn(tree.withType(tree.nuType))
21872220
case _ => traverseChildren(t)
2188-
checkApplied.traverse(unit)
2221+
checkAppliedTypes.traverse(unit)
21892222
end postCheck
21902223

21912224
/** Perform the following kinds of checks:

docs/_docs/reference/experimental/capture-checking/mutability.md

Lines changed: 5 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,13 @@ an update method or an update class as non-private member or constructor.
7575

7676
When we create an instance of a mutable type we always add `cap` to its capture set. For instance, if class `Ref` is declared as shown previously then `new Ref(1)` has type `Ref[Int]^`.
7777

78-
**Restriction:** A non-mutable type cannot be downcast by a pattern match to a mutable type.
78+
**Restriction:** A non-mutable type cannot be downcast by a pattern match to a mutable type. (Note: This is currently not enforced)
7979

80-
**Definition:** A class is _read_only_ if the following conditions are met:
80+
**Definition:** A parent class constructor is _read-only_ if the following conditions are met:
8181

82-
1. It does not extend any exclusive capabilities from its environment.
83-
2. It does not take parameters with exclusive capabilities.
84-
3. It does not contain mutable fields, or fields that take exclusive capabilities.
82+
1. The class does not retain any exclusive capabilities from its environment.
83+
2. The constructor does not take arguments that retain exclusive capabilities.
84+
3. The class does not does not have fields that retain exclusive universal capabilities.
8585

8686
**Restriction:** If a class or trait extends `Mutable` all its parent classes or traits must either extend `Mutable` or be read-only.
8787

@@ -293,166 +293,3 @@ The subcapturing theory for sets is then as before, with the following additiona
293293
- `{x, ...}.RD = {x.rd, ...}.RD`
294294
- `{x.rd, ...} <: {x, ...}`
295295

296-
## Separation Checking
297-
298-
The idea behind separation checking is simple: We now interpret each occurrence of `cap` as a separate top capability. This includes derived syntaxes like `A^` and `B => C`. We further keep track during capture checking which capabilities are subsumed by each `cap`. If capture checking widens a capability `x` to a top capability `capᵢ`, we say `x` is _hidden_ by `capᵢ`. The rule then is that any capability hidden by a top capability `capᵢ` cannot be referenced independently or hidden in another `capⱼ` in code that can see `capᵢ`.
299-
300-
Here's an example:
301-
```scala
302-
val x: C^ = y
303-
... x ... // ok
304-
... y ... // error
305-
```
306-
This principle ensures that capabilities such as `x` that have `cap` as underlying capture set are un-aliased or "fresh". Any previously existing aliases such as `y` in the code above are inaccessible as long as `x` is also visible.
307-
308-
Separation checking applies only to exclusive capabilities and their read-only versions. Any capability extending `SharedCapability` in its type is exempted; the following definitions and rules do not apply to them.
309-
310-
**Definitions:**
311-
312-
- The _transitive capture set_ `tcs(c)` of a capability `c` with underlying capture set `C` is `c` itself, plus the transitive capture set of `C`.
313-
314-
- The _transitive capture set_ `tcs(C)` of a capture set C is the union
315-
of `tcs(c)` for all elements `c` of `C`.
316-
317-
- Two capture sets _interfere_ if one contains an exclusive capability `x` and the other also contains `x` or contains the read-only capability `x.rd`. Conversely, two capture sets are _separated_ if their transitive capture sets don't interfere.
318-
319-
Separation checks are applied in the following scenarios:
320-
321-
### Checking Applications
322-
323-
When checking a function application `f(e_1, ..., e_n)`, we instantiate each `cap` in a formal parameter of `f` to a fresh top capability and compare the argument types with these instantiated parameter types. We then check that the hidden set of each instantiated top capability for an argument `eᵢ` is separated from the capture sets of all the other arguments as well as from the capture sets of the function prefix and the function result. For instance a
324-
call to
325-
```scala
326-
multiply(a, b, a)
327-
```
328-
would be rejected since `a` appears in the hidden set of the last parameter of multiply, which has type `Matrix^` and also appears in the capture set of the
329-
first parameter.
330-
331-
We do not report a separation error between two sets if a formal parameter's capture set explicitly names a conflicting parameter. For instance, consider a method `seq` to apply two effectful function arguments in sequence. It can be declared as follows:
332-
```scala
333-
def seq(f: () => Unit; g: () ->{cap, f} Unit): Unit =
334-
f(); g()
335-
```
336-
Here, the `g` parameter explicitly mentions `f` in its potential capture set. This means that the `cap` in the same capture set would not need to hide the first argument, since it already appears explicitly in the same set. Consequently, we can pass the same function twice to `compose` without violating the separation criteria:
337-
```scala
338-
val r = Ref(1)
339-
val plusOne = r.set(r.get + 1)
340-
seq(plusOne, plusOne)
341-
```
342-
Without the explicit mention of parameter `f` in the capture set of parameter `g` of `seq` we'd get a separation error, since the transitive capture sets of both arguments contain `r` and are therefore not separated.
343-
344-
### Checking Statement Sequences
345-
346-
When a capability `x` is used at some point in a statement sequence, we check that `{x}` is separated from the hidden sets of all previous definitions.
347-
348-
Example:
349-
```scala
350-
val a: Ref^ = Ref(1)
351-
val b: Ref^ = a
352-
val x = a.get // error
353-
```
354-
Here, the last line violates the separation criterion since it uses in `a.get` the capability `a`, which is hidden by the definition of `b`.
355-
Note that this check only applies when there are explicit top capabilities in play. One could very well write
356-
```scala
357-
val a: Ref^ = Ref(1)
358-
val b: Ref^{a} = a
359-
val x = a.get // ok
360-
```
361-
One can also drop the explicit type of `b` and leave it to be inferred. That would
362-
not cause a separation error either.
363-
```scala
364-
val a: Ref^ = Ref(0
365-
val b = a
366-
val x = a.get // ok
367-
```
368-
369-
### Checking Types
370-
371-
When a type contains top capabilities we check that their hidden sets don't interfere with other parts of the same type.
372-
373-
Example:
374-
```scala
375-
val b: (Ref^, Ref^) = (a, a) // error
376-
val c: (Ref^, Ref^{a}) = (a, a) // error
377-
val d: (Ref^{a}, Ref^{a}) = (a, a) // ok
378-
```
379-
Here, the definition of `b` is in error since the hidden sets of the two `^`s in its type both contain `a`. Likewise, the definition of `c` is in error since the hidden set of the `^` in its type contains `a`, which is also part of a capture set somewhere else in the type. On the other hand, the definition of `d` is legal since there are no hidden sets to check.
380-
381-
### Checking Return Types
382-
383-
When a `cap` appears in the return type of a function it means a "fresh" top capability, different from what is known at the call site. Separation checking makes sure this is the case. For instance, the following is OK:
384-
```scala
385-
def newRef(): Ref^ = Ref(1)
386-
```
387-
And so is this:
388-
```scala
389-
def newRef(): Ref^ =
390-
val a = Ref(1)
391-
a
392-
```
393-
But the next definitions would cause a separation error:
394-
```scala
395-
val a = Ref(1)
396-
def newRef(): Ref^ = a // error
397-
```
398-
The rule is that the hidden set of a fresh cap in a return type cannot reference exclusive or read-only capabilities defined outside of the function. What about parameters? Here's another illegal version:
399-
```scala
400-
def incr(a: Ref^): Ref^ =
401-
a.set(a.get + 1)
402-
a
403-
```
404-
These needs to be rejected because otherwise we could have set up the following bad example:
405-
```scala
406-
val a = Ref(1)
407-
val b: Ref^ = incr(a)
408-
```
409-
Here, `b` aliases `a` but does not hide it. If we referred to `a` afterwards we would be surprised to see that the reference has now a value of 2.
410-
Therefore, parameters cannot appear in the hidden sets of fresh result caps either, at least not in general. An exception to this rule is described in the next section.
411-
412-
### Consume Parameters
413-
414-
Returning parameters in fresh result caps is safe if the actual argument to the parameter is not used afterwards. We can signal and enforce this pattern by adding a `consume` modifier to a parameter. With that new soft modifier, the following variant of `incr` is legal:
415-
```scala
416-
def incr(consume a: Ref^): Ref^ =
417-
a.set(a.get + 1)
418-
a
419-
```
420-
Here, we increment the value of a reference and then return the same reference while enforcing the condition that the original reference cannot be used afterwards. Then the following is legal:
421-
```scala
422-
val a1 = Ref(1)
423-
val a2 = incr(a1)
424-
val a3 = incr(a2)
425-
println(a3)
426-
```
427-
Each reference `aᵢ` is unused after it is passed to `incr`. But the following continuation of that sequence would be in error:
428-
```scala
429-
val a4 = println(a2) // error
430-
val a5 = incr(a1) // error
431-
```
432-
In both of these assignments we use a capability that was consumed in an argument
433-
of a previous application.
434-
435-
Consume parameters enforce linear access to resources. This can be very useful. As an example, consider Scala's buffers such as `ListBuffer` or `ArrayBuffer`. We can treat these buffers as if they were purely functional, if we can enforce linear access.
436-
437-
For instance, we can define a function `linearAdd` that adds elements to buffers in-place without violating referential transparency:
438-
```scala
439-
def linearAdd[T](consume buf: Buffer[T]^, elem: T): Buffer[T]^ =
440-
buf += elem
441-
```
442-
`linearAdd` returns a fresh buffer resulting from appending `elem` to `buf`. It overwrites `buf`, but that's OK since the `consume` modifier on `buf` ensures that the argument is not used after the call.
443-
444-
### Consume Methods
445-
446-
Buffers in Scala's standard library use a single-argument method `+=` instead of a two argument global function like `linearAdd`. We can enforce linearity in this case by adding the `consume` modifier to the method itself.
447-
```scala
448-
class Buffer[T] extends Mutable:
449-
consume def +=(x: T): Buffer[T]^ = this // ok
450-
```
451-
`consume` on a method implies `update`, so there's no need to label `+=` separately as an update method. Then we can write
452-
```scala
453-
val b = Buffer[Int]() += 1 += 2
454-
val c = b += 3
455-
// b cannot be used from here
456-
```
457-
This code is equivalent to functional append with `+`, and is at the same time more efficient since it re-uses the storage of the argument buffer.
458-

0 commit comments

Comments
 (0)