Skip to content

Commit d131a1c

Browse files
authored
Add freeze wrapper (2) (#24518)
Revised version of #24423. Based on #24495.
2 parents ab8bf5f + 7cb39e6 commit d131a1c

File tree

21 files changed

+284
-9
lines changed

21 files changed

+284
-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: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -790,22 +790,29 @@ 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 applications, with special handling of unsafeAssumePure,
804+
* unsafeDiscardUses, and freeze.
794805
* More work is done in `recheckApplication`, `recheckArg` and `instantiate` below.
795806
*/
796807
override def recheckApply(tree: Apply, pt: Type)(using Context): Type =
797808
val meth = tree.fun.symbol
798809
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)
810+
applyAssumePure(tree, pt)
806811
else if meth == defn.Caps_unsafeDiscardUses then
807812
val arg :: Nil = tree.args: @unchecked
808813
withDiscardedUses(recheck(arg, pt))
814+
else if meth == defn.Caps_freeze then
815+
freeze(super.recheckApply(tree, pt), tree.srcPos)
809816
else
810817
val res = super.recheckApply(tree, pt)
811818
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

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,3 +444,4 @@ 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+

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(consume x: Mutable): x.type = x
266+
```
267+
It consumes a value of `Mutable` type with arbitrary capture set (since any capture set conforms to the implied `{cap.rd}`). 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: 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
@@ -190,6 +191,12 @@ object internal:
190191

191192
end internal
192193

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(@internal.consume x: Mutable): x.type = x
199+
193200
@experimental
194201
object unsafe:
195202
/** Two usages:

project/MiMaFilters.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ object MiMaFilters {
3232
ProblemFilters.exclude[MissingClassProblem]("scala.caps.Classifier"),
3333
ProblemFilters.exclude[MissingClassProblem]("scala.caps.SharedCapability"),
3434
ProblemFilters.exclude[MissingClassProblem]("scala.caps.Control"),
35+
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.caps.package#package.freeze"),
3536
),
3637

3738
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-- Error: tests/neg-custom-args/captures/boxed-consume.scala:10:52 -----------------------------------------------------
2+
10 | def h(consume x: (Object^, Object^)): Object^ = x._1 // error: local reach leak
3+
| ^^^^
4+
| Local reach capability x._1* leaks into capture scope of method h.
5+
| You could try to abstract the capabilities referred to by x._1* in a capset variable.
6+
-- Error: tests/neg-custom-args/captures/boxed-consume.scala:16:10 -----------------------------------------------------
7+
16 | println(d) // error
8+
| ^
9+
| Separation failure: Illegal access to (d : Object^), which was passed as a consume parameter to method g
10+
| on line 15 and therefore is no longer available.
11+
|
12+
| where: ^ refers to a fresh root capability in the type of value d
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import caps.*
2+
3+
def Test =
4+
val c: Object^ = "a"
5+
val d: Object^ = "a"
6+
val e1, e2: Object^ = "a"
7+
val falseDeps: Object^ = "a"
8+
def f[T](consume x: T): Unit = ()
9+
def g(consume x: Object^): Unit = ()
10+
def h(consume x: (Object^, Object^)): Object^ = x._1 // error: local reach leak
11+
val cc = (c, c)
12+
f(cc) // no consume, it's boxed
13+
f(c) // no consume, it's boxed
14+
println(c) // ok
15+
g(d) // consume
16+
println(d) // error
17+
h((e1, e2)) // no consume, still boxed
18+
println(e1) // ok
19+
20+
21+
22+

0 commit comments

Comments
 (0)