Skip to content

Commit 05f7026

Browse files
committed
Add caps.immutable wrapper
1 parent e62efd0 commit 05f7026

File tree

8 files changed

+156
-9
lines changed

8 files changed

+156
-9
lines changed

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

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -790,22 +790,44 @@ class CheckCaptures extends Recheck, SymTransformer:
790790
selType
791791
}//.showing(i"recheck sel $tree, $qualType = $result")
792792

793-
/** Recheck applications, with special handling of unsafeAssumePure.
793+
/** Recheck `caps.unsafe.unsafeAssumePure(...)` */
794+
def applyAssumePure(tree: Apply, pt: Type)(using Context): Type =
795+
val arg :: Nil = tree.args: @unchecked
796+
val argType0 = recheck(arg, pt.stripCapturing.capturing(FreshCap(Origin.UnsafeAssumePure)))
797+
val argType =
798+
if argType0.captureSet.isAlwaysEmpty then argType0
799+
else argType0.widen.stripCapturing
800+
capt.println(i"rechecking unsafeAssumePure of $arg with $pt: $argType")
801+
super.recheckFinish(argType, tree, pt)
802+
803+
/** Recheck `caps.immutable(...)` */
804+
def applyImmutable(tree: Apply)(using Context): Type =
805+
val arg :: Nil = tree.args: @unchecked
806+
def imm = new TypeMap:
807+
def apply(t: Type) = t match
808+
case t @ CapturingType(parent, _)
809+
if parent.derivesFromMutable && variance > 0 =>
810+
t.derivedCapturingType(apply(parent), CaptureSet.emptyOfMutable)
811+
case _ =>
812+
mapOver(t)
813+
val opProto = // () ?-> <?>
814+
defn.FunctionType(0, isContextual = true).appliedTo(WildcardType)
815+
recheck(arg, opProto).stripCapturing match
816+
case defn.ContextFunctionType(Nil, resType) => imm(resType)
817+
818+
/** Recheck applications, with special handling of unsafeAssumePure,
819+
* unsafeDiscardUses, and immutable.
794820
* More work is done in `recheckApplication`, `recheckArg` and `instantiate` below.
795821
*/
796822
override def recheckApply(tree: Apply, pt: Type)(using Context): Type =
797823
val meth = tree.fun.symbol
798824
if meth == defn.Caps_unsafeAssumePure then
799-
val arg :: Nil = tree.args: @unchecked
800-
val argType0 = recheck(arg, pt.stripCapturing.capturing(FreshCap(Origin.UnsafeAssumePure)))
801-
val argType =
802-
if argType0.captureSet.isAlwaysEmpty then argType0
803-
else argType0.widen.stripCapturing
804-
capt.println(i"rechecking unsafeAssumePure of $arg with $pt: $argType")
805-
super.recheckFinish(argType, tree, pt)
825+
applyAssumePure(tree, pt)
806826
else if meth == defn.Caps_unsafeDiscardUses then
807827
val arg :: Nil = tree.args: @unchecked
808828
withDiscardedUses(recheck(arg, pt))
829+
else if meth == defn.Caps_immutable then
830+
applyImmutable(tree)
809831
else
810832
val res = super.recheckApply(tree, pt)
811833
includeCallCaptures(meth, res, tree)

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,7 @@ class Definitions {
10311031
@tu lazy val Caps_ContainsTrait: TypeSymbol = CapsModule.requiredType("Contains")
10321032
@tu lazy val Caps_ContainsModule: Symbol = requiredModule("scala.caps.Contains")
10331033
@tu lazy val Caps_containsImpl: TermSymbol = Caps_ContainsModule.requiredMethod("containsImpl")
1034+
@tu lazy val Caps_immutable: TermSymbol = CapsModule.requiredMethod("immutable")
10341035

10351036
@tu lazy val PureClass: ClassSymbol = requiredClass("scala.caps.Pure")
10361037

@@ -2098,7 +2099,7 @@ class Definitions {
20982099
RequiresCapabilityAnnot,
20992100
captureRoot, Caps_CapSet, Caps_ContainsTrait, Caps_ContainsModule, Caps_ContainsModule.moduleClass,
21002101
ConsumeAnnot, UseAnnot, ReserveAnnot,
2101-
CapsUnsafeModule, CapsUnsafeModule.moduleClass,
2102+
CapsUnsafeModule, CapsUnsafeModule.moduleClass, Caps_immutable,
21022103
CapsInternalModule, CapsInternalModule.moduleClass,
21032104
RetainsAnnot, RetainsCapAnnot, RetainsByNameAnnot)
21042105

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,3 +339,34 @@ The subcapturing theory for sets is then as before, with the following additiona
339339
- `{x, ...}.RD = {x.rd, ...}.RD`
340340
- `{x.rd, ...} <: {x, ...}`
341341

342+
## The `immutable` Wrapper
343+
344+
We often want to create a mutable data structure like an array, initialize by assigning to its elements and then return the array as an immutable type that does not
345+
capture any capabilities. This can be achieved using the `immutable` wrapper.
346+
347+
As an example, consider a class `Arr` which is modelled after `Array` and its immutable counterpart `IArr`:
348+
349+
```scala
350+
class Arr[T: reflect.ClassTag](len: Int) extends Mutable:
351+
private val arr: Array[T] = new Array[T](len)
352+
def get(i: Int): T = arr(i)
353+
update def update(i: Int, x: T): Unit = arr(i) = x
354+
type IArr[T] = Arr[T]^{}
355+
```
356+
357+
The `immutable` wrapper allows us to go from an `Arr` to an `IArr`, safely:
358+
```scala
359+
import caps.immutable
360+
361+
val f: IArr[String] = immutable:
362+
val a = Arr[String](2)
363+
a(0) = "hello"
364+
a(1) = "world"
365+
a
366+
```
367+
The `immutable` method is defined in `caps` like this:
368+
```scala
369+
def immutable[T](op: -> T): T = op
370+
```
371+
It takes a pure by-name parameter and returns its result. But the actual return type after capture checking is special. Instead of just `T` as in the declaration above suggests, it's `T` where every covariant occurrence of a `Mutable` type gets its capture set mapped to `{}`.
372+

library/src/scala/caps/package.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ final class reserve extends annotation.StaticAnnotation
123123
* environment.
124124
*/
125125
@experimental
126+
@deprecated(since = "3.8.0")
126127
final class use extends annotation.StaticAnnotation
127128

128129
/** A trait that used to allow expressing existential types. Replaced by
@@ -132,6 +133,12 @@ final class use extends annotation.StaticAnnotation
132133
@deprecated
133134
sealed trait Exists extends Capability
134135

136+
/** A wrapper that strips all covariant capture sets from Mutable types in the
137+
* result of pure operation `op`, turning them into immutable types.
138+
*/
139+
@experimental
140+
def immutable[T](op: -> T): T = op
141+
135142
@experimental
136143
object internal:
137144

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/immutable.scala:13:20 ------------------------------------
2+
13 | val b = immutable(a) // error
3+
| ^
4+
| Found: () ?->{a} Arr[String]^{a}
5+
| Required: () ?-> <?>
6+
|
7+
| Note that capability a is not included in capture set {}.
8+
|
9+
| longer explanation available when compiling with `-explain`
10+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/immutable.scala:22:4 -------------------------------------
11+
21 | immutable:
12+
22 | mkExclusive() // error
13+
| ^
14+
| Capability cap outlives its scope: it leaks into outer capture set 's1 of method test3.
15+
| The leakage occurred when trying to match the following types:
16+
|
17+
| Found: EX^{cap}
18+
| Required: EX^'s1
19+
|
20+
| where: cap is a root capability associated with the result type of (): EX^
21+
|
22+
| longer explanation available when compiling with `-explain`
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import caps.*
2+
3+
class Arr[T: reflect.ClassTag](len: Int) extends Mutable:
4+
private val arr: Array[T] = new Array[T](len)
5+
def get(i: Int): T = arr(i)
6+
update def update(i: Int, x: T): Unit = arr(i) = x
7+
8+
9+
def test2 =
10+
val a = Arr[String](2)
11+
a(0) = "1"
12+
a(1) = "2"
13+
val b = immutable(a) // error
14+
b
15+
16+
class EX
17+
18+
def mkExclusive(): EX^ = ???
19+
20+
def test3 =
21+
immutable:
22+
mkExclusive() // error
23+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import caps.*
2+
3+
class Node(val payload: Int) extends Mutable:
4+
var next: Node = ???
5+
6+
7+
type INode = Node^{}
8+
9+
def cycle(x: Int, y: Int): (INode, INode) =
10+
immutable:
11+
val a = Node(x)
12+
val b = Node(y)
13+
a.next = b
14+
b.next = a
15+
(a, b)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import language.experimental.captureChecking
2+
import caps.*
3+
4+
class Arr[T: reflect.ClassTag](len: Int) extends Mutable:
5+
private val arr: Array[T] = new Array[T](len)
6+
def get(i: Int): T = arr(i)
7+
update def update(i: Int, x: T): Unit = arr(i) = x
8+
9+
10+
def test2 =
11+
val a = immutable:
12+
val a = Arr[String](2)
13+
a(0) = "1"
14+
a(1) = "2"
15+
a
16+
val _: Arr[String]^{} = a
17+
18+
val a2 = immutable:
19+
val a = Arr[String](2)
20+
val b = Arr[String](2)
21+
a(0) = "1"
22+
a(1) = "2"
23+
b(0) = "1"
24+
b(1) = "2"
25+
(a, b)
26+
val _: (Arr[String]^{}, Arr[String]^{}) = a2

0 commit comments

Comments
 (0)