Skip to content

Commit 0a60e5c

Browse files
Linyxusodersky
authored andcommitted
Add caps.freeze wrapper
1 parent a70df7b commit 0a60e5c

File tree

10 files changed

+159
-9
lines changed

10 files changed

+159
-9
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ extension (tp: Type)
398398
def derivesFromStateful(using Context): Boolean = derivesFromCapTrait(defn.Caps_Stateful)
399399
def derivesFromShared(using Context): Boolean = derivesFromCapTrait(defn.Caps_SharedCapability)
400400
def derivesFromUnscoped(using Context): Boolean = derivesFromCapTrait(defn.Caps_Unscoped)
401+
def derivesFromMutable(using Context): Boolean = derivesFromCapTrait(defn.Caps_Mutable)
401402

402403
/** Drop @retains annotations everywhere */
403404
def dropAllRetains(using Context): Type = // TODO we should drop retains from inferred types before unpickling

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.freeze(...)` */
804+
def applyFreeze(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.emptyOfStateful)
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 freeze.
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_freeze then
830+
applyFreeze(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
@@ -1028,6 +1028,7 @@ class Definitions {
10281028
@tu lazy val Caps_ContainsTrait: TypeSymbol = CapsModule.requiredType("Contains")
10291029
@tu lazy val Caps_ContainsModule: Symbol = requiredModule("scala.caps.Contains")
10301030
@tu lazy val Caps_containsImpl: TermSymbol = Caps_ContainsModule.requiredMethod("containsImpl")
1031+
@tu lazy val Caps_freeze: TermSymbol = CapsModule.requiredMethod("freeze")
10311032

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

@@ -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_freeze,
21022103
CapsInternalModule, CapsInternalModule.moduleClass,
21032104
RetainsAnnot, RetainsCapAnnot, RetainsByNameAnnot)
21042105

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,3 +444,35 @@ The subcapturing theory for sets is then as before, with the following additiona
444444
- `C₁.reader <: C₂.reader` if `C₍ <: C₂`
445445
- `{x, ...}.reader = {x.rd, ...}.reader`
446446
- `{x.rd, ...} <: {x, ...}`
447+
448+
## The `freeze` Wrapper
449+
450+
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
451+
capture any capabilities. This can be achieved using the `freeze` wrapper.
452+
453+
As an example, consider a class `Arr` which is modelled after `Array` and its immutable counterpart `IArr`:
454+
455+
```scala
456+
class Arr[T: reflect.ClassTag](len: Int) extends Mutable:
457+
private val arr: Array[T] = new Array[T](len)
458+
def get(i: Int): T = arr(i)
459+
update def update(i: Int, x: T): Unit = arr(i) = x
460+
type IArr[T] = Arr[T]^{}
461+
```
462+
463+
The `freeze` wrapper allows us to go from an `Arr` to an `IArr`, safely:
464+
```scala
465+
import caps.freeze
466+
467+
val f: IArr[String] = freeze:
468+
val a = Arr[String](2)
469+
a(0) = "hello"
470+
a(1) = "world"
471+
a
472+
```
473+
The `freeze` method is defined in `caps` like this:
474+
```scala
475+
def freeze[T](op: -> T): T = op
476+
```
477+
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 `{}`.
478+

library/src/scala/caps/package.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ final class reserve extends annotation.StaticAnnotation
141141
* environment.
142142
*/
143143
@experimental
144+
@deprecated(since = "3.8.0")
144145
final class use extends annotation.StaticAnnotation
145146

146147
/** A trait that used to allow expressing existential types. Replaced by
@@ -150,6 +151,12 @@ final class use extends annotation.StaticAnnotation
150151
@deprecated
151152
sealed trait Exists extends Capability
152153

154+
/** A wrapper that strips all covariant capture sets from Mutable types in the
155+
* result of pure operation `op`, turning them into immutable types.
156+
*/
157+
@experimental
158+
def freeze[T](op: -> T): T = op
159+
153160
@experimental
154161
object internal:
155162

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/freeze.scala:13:17 ---------------------------------------
2+
13 | val b = freeze(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/freeze.scala:22:4 ----------------------------------------
11+
21 | freeze:
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 = freeze(a) // error
14+
b
15+
16+
class EX
17+
18+
def mkExclusive(): EX^ = ???
19+
20+
def test3 =
21+
freeze:
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+
freeze:
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 = freeze:
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 = freeze:
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

tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ val experimentalDefinitionInLibrary = Set(
4949
"scala.caps.unsafe$",
5050
"scala.caps.use",
5151
"scala.caps.reserve",
52+
"scala.caps.package$package$.freeze",
5253
"scala.caps.package$package$.Exclusive",
5354
"scala.caps.package$package$.Shared",
5455

0 commit comments

Comments
 (0)