Skip to content

Commit d8e4b60

Browse files
committed
Revised scheme: Make freeze a method with consume parameter
1 parent 0a60e5c commit d8e4b60

File tree

10 files changed

+80
-94
lines changed

10 files changed

+80
-94
lines changed

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

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -800,21 +800,6 @@ class CheckCaptures extends Recheck, SymTransformer:
800800
capt.println(i"rechecking unsafeAssumePure of $arg with $pt: $argType")
801801
super.recheckFinish(argType, tree, pt)
802802

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-
818803
/** Recheck applications, with special handling of unsafeAssumePure,
819804
* unsafeDiscardUses, and freeze.
820805
* More work is done in `recheckApplication`, `recheckArg` and `instantiate` below.
@@ -827,7 +812,7 @@ class CheckCaptures extends Recheck, SymTransformer:
827812
val arg :: Nil = tree.args: @unchecked
828813
withDiscardedUses(recheck(arg, pt))
829814
else if meth == defn.Caps_freeze then
830-
applyFreeze(tree)
815+
freeze(super.recheckApply(tree, pt), tree.srcPos)
831816
else
832817
val res = super.recheckApply(tree, pt)
833818
includeCallCaptures(meth, res, tree)

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Symbols.*, Types.*, Flags.*, Contexts.*, Names.*, Decorators.*
77
import Capabilities.*
88
import util.SrcPos
99
import config.Printers.capt
10+
import config.Feature
1011
import ast.tpd.Tree
1112
import typer.ProtoTypes.LhsProto
1213

@@ -249,4 +250,22 @@ object Mutability:
249250
case improved =>
250251
improved
251252

253+
/** Apply `caps.freeze(...)`. Strip all capture sets of covariant Mutable
254+
* types, turning them into `CaptureSet.emptyOfMutable`. Only Mutable types
255+
* that contribute to the overall capture set are considered, since that is the
256+
* set analyzed by consume/use checking. That means that double-flip covariant
257+
* and boxed capture sets are not dropped.
258+
*/
259+
def freeze(tp: Type, pos: SrcPos)(using Context): Type = tp.widen match
260+
case tpw @ CapturingType(parent, refs)
261+
if parent.derivesFromMutable && !tpw.isBoxed =>
262+
if !Feature.enabled(Feature.separationChecking) then
263+
report.warning(
264+
em"""freeze is safe only if separation checking is enabled.
265+
|You can enable separation checking with the language import
266+
|
267+
| import language.experimental.separationChecking""",
268+
pos)
269+
tpw.derivedCapturingType(parent, CaptureSet.emptyOfStateful)
270+
case _ => tp
252271
end Mutability

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

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

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-

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,36 @@ val c = b += 3
235235
```
236236
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.
237237

238+
## The `freeze` Wrapper
239+
240+
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
241+
capture any capabilities. This can be achieved using the `freeze` wrapper.
242+
243+
As an example, consider a class `Arr` which is modelled after `Array` and its immutable counterpart `IArr`:
244+
245+
```scala
246+
class Arr[T: reflect.ClassTag](len: Int) extends Mutable:
247+
private val arr: Array[T] = new Array[T](len)
248+
def get(i: Int): T = arr(i)
249+
update def update(i: Int, x: T): Unit = arr(i) = x
250+
type IArr[T] = Arr[T]^{}
251+
```
252+
253+
The `freeze` wrapper allows us to go from an `Arr` to an `IArr`, safely:
254+
```scala
255+
import caps.freeze
256+
257+
val f: IArr[String] =
258+
val a = Arr[String](2)
259+
a(0) = "hello"
260+
a(1) = "world"
261+
freeze(a)
262+
```
263+
The `freeze` method is defined in `caps` like this:
264+
```scala
265+
def freeze[T](consume x: Mutable^): x.type = x
266+
```
267+
It consumes a value of `Mutable` type with arbitrary capture set. The actual signature of
268+
`consume` declares that `x.type` is returned, but the actual return type after capture checking is special. Instead of `x.type` it is the underlying `Mutable` type with its top-level capture set
269+
mapped to `{}`. Applications of `freeze` are safe only if separation checking is enabled.
270+

library/src/scala/caps/package.scala

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,6 @@ final class use extends annotation.StaticAnnotation
151151
@deprecated
152152
sealed trait Exists extends Capability
153153

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-
160154
@experimental
161155
object internal:
162156

@@ -197,6 +191,12 @@ object internal:
197191

198192
end internal
199193

194+
/** A wrapper that strips all covariant capture sets from Mutable types in the
195+
* result of pure operation `op`, turning them into immutable types.
196+
*/
197+
@experimental
198+
def freeze[T](@internal.consume x: Mutable^): x.type = x
199+
200200
@experimental
201201
object unsafe:
202202
/** Two usages:
Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,7 @@
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^
1+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/freeze.scala:11:17 ---------------------------------------
2+
11 | val b = freeze((a, a)) // error
3+
| ^^^^^^
4+
| Found: (Arr[String], Arr[String])
5+
| Required: scala.caps.Mutable^
216
|
227
| longer explanation available when compiling with `-explain`

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

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,5 @@ class Arr[T: reflect.ClassTag](len: Int) extends Mutable:
88

99
def test2 =
1010
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
11+
val b = freeze((a, a)) // error
2312

tests/neg-custom-args/captures/sep-consume.check

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
-- Error: tests/neg-custom-args/captures/sep-consume.scala:30:49 -------------------------------------------------------
2+
30 |def test4(consume p: Pair[Ref^, Ref^]): Ref^ = p.fst // error
3+
| ^^^^^
4+
| Local reach capability p.fst* leaks into capture scope of method test4.
5+
| You could try to abstract the capabilities referred to by p.fst* in a capset variable.
16
-- Error: tests/neg-custom-args/captures/sep-consume.scala:19:2 --------------------------------------------------------
27
19 | x.put(42) // error
38
| ^

tests/neg-custom-args/captures/sep-consume.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ def test3(consume x: Ref^): Unit =
2727
foo()
2828
foo()
2929

30+
def test4(consume p: Pair[Ref^, Ref^]): Ref^ = p.fst // error
31+
3032
/*
3133
def test4(consume @use p: Pair[Ref^, Ref^]): Unit =
3234
val x: Ref^{p.fst*} = p.fst

tests/pos-custom-args/captures/freeze.scala

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@ def test2 =
1515
a
1616
val _: Arr[String]^{} = a
1717

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
18+
def test3 =
19+
val a = Arr[String](2)
20+
a(0) = "1"
21+
a(1) = "2"
22+
val b = freeze(a)
23+
val _: Arr[String]^{} = b
24+
b
25+

0 commit comments

Comments
 (0)