Skip to content

Commit 9f0adf6

Browse files
committed
Improvements to read-only handling
- Allow to charge read-only set on unboxing (previously it was always the exclusive set that was charged). - Treat also methods outside Mutable types as read-only members. Previously we charged read-only when calling a read-only method of a mutable type, but not when calling an inherited method such as `eq`. - Use the same criterion for read-only everywhere. - Distinguish between read- and write-accesses to mutable fields.
1 parent 78507dd commit 9f0adf6

File tree

7 files changed

+114
-49
lines changed

7 files changed

+114
-49
lines changed

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import CaptureSet.VarState
1515
import Capabilities.*
1616
import StdNames.nme
1717
import config.Feature
18-
import dotty.tools.dotc.core.NameKinds.TryOwnerName
18+
import NameKinds.TryOwnerName
19+
import typer.ProtoTypes.WildcardSelectionProto
1920

2021
/** Attachment key for capturing type trees */
2122
private val Captures: Key[CaptureSet] = Key()
@@ -639,6 +640,10 @@ extension (tp: AnnotatedType)
639640
case ann: CaptureAnnotation => ann.boxed
640641
case _ => false
641642

643+
/** A prototype that indicates selection */
644+
class PathSelectionProto(val select: Select, val pt: Type) extends typer.ProtoTypes.WildcardSelectionProto:
645+
def selector(using Context): Symbol = select.symbol
646+
642647
/** Drop retains annotations in the inferred type if CC is not enabled
643648
* or transform them into RetainingTypes if CC is enabled.
644649
*/

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

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,6 @@ object CheckCaptures:
103103
override def toString = "SubstParamsMap"
104104
end SubstParamsMap
105105

106-
/** A prototype that indicates selection with an immutable value */
107-
class PathSelectionProto(val select: Select, val pt: Type)(using Context) extends WildcardSelectionProto
108-
109106
/** Check that a @retains annotation only mentions references that can be tracked.
110107
* This check is performed at Typer.
111108
*/
@@ -598,7 +595,10 @@ class CheckCaptures extends Recheck, SymTransformer:
598595
if !isOfNestedMethod(env) then
599596
val nextEnv = nextEnvToCharge(env)
600597
if nextEnv != null && !nextEnv.owner.isStaticOwner then
601-
if env.owner.isReadOnlyMethodOrLazyVal && nextEnv.owner != env.owner then
598+
if nextEnv.owner != env.owner
599+
&& env.owner.isReadOnlyMember
600+
&& env.owner.owner.derivesFrom(defn.Caps_Mutable)
601+
then
602602
checkReadOnlyMethod(included, env.owner)
603603
recur(included, nextEnv, env)
604604
// Under deferredReaches, don't propagate out of methods inside terms.
@@ -705,29 +705,23 @@ class CheckCaptures extends Recheck, SymTransformer:
705705
* where `b` is a read-only method, we charge `x.a.b.rd` for tree `x.a.b`
706706
* instead of just charging `x`.
707707
*/
708-
private def markPathFree(ref: TermRef | ThisType, pt: Type, tree: Tree)(using Context): Unit =
709-
pt match
710-
case pt: PathSelectionProto if ref.isTracked =>
711-
// if `ref` is not tracked then the selection could not give anything new
712-
// class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters.
713-
if pt.select.symbol.isReadOnlyMethodOrLazyVal then
714-
markFree(ref.readOnly, tree)
715-
else
716-
val sel = ref.select(pt.select.symbol).asInstanceOf[TermRef]
717-
markPathFree(sel, pt.pt, pt.select)
718-
case _ =>
719-
markFree(ref.adjustReadOnly(pt), tree)
708+
private def markPathFree(ref: TermRef | ThisType, pt: Type, tree: Tree)(using Context): Unit = pt match
709+
case pt: PathSelectionProto
710+
if ref.isTracked && !pt.selector.isOneOf(MethodOrLazyOrMutable) =>
711+
// if `ref` is not tracked then the selection could not give anything new
712+
// class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters.
713+
val sel = ref.select(pt.selector).asInstanceOf[TermRef]
714+
markPathFree(sel, pt.pt, pt.select)
715+
case _ =>
716+
markFree(ref.adjustReadOnly(pt), tree)
720717

721718
/** The expected type for the qualifier of a selection. If the selection
722719
* could be part of a capability path or is a a read-only method, we return
723720
* a PathSelectionProto.
724721
*/
725722
override def selectionProto(tree: Select, pt: Type)(using Context): Type =
726-
val sym = tree.symbol
727-
if !sym.isOneOf(MethodOrLazyOrMutable) && !sym.isStatic
728-
|| sym.isReadOnlyMethodOrLazyVal
729-
then PathSelectionProto(tree, pt)
730-
else super.selectionProto(tree, pt)
723+
if tree.symbol.isStatic then super.selectionProto(tree, pt)
724+
else PathSelectionProto(tree, pt)
731725

732726
/** A specialized implementation of the selection rule.
733727
*
@@ -1131,21 +1125,30 @@ class CheckCaptures extends Recheck, SymTransformer:
11311125
try
11321126
if sym.is(Module) then sym.info // Modules are checked by checking the module class
11331127
else
1134-
if sym.is(Mutable) && !sym.hasAnnotation(defn.UncheckedCapturesAnnot) then
1135-
val addendum = setup.capturedBy.get(sym) match
1136-
case Some(encl) =>
1137-
val enclStr =
1138-
if encl.isAnonymousFunction then
1139-
val location = setup.anonFunCallee.get(encl) match
1140-
case Some(meth) if meth.exists => i" argument in a call to $meth"
1141-
case _ => ""
1142-
s"an anonymous function$location"
1143-
else encl.show
1144-
i"\n\nNote that $sym does not count as local since it is captured by $enclStr"
1145-
case _ =>
1146-
""
1147-
disallowBadRootsIn(
1148-
tree.tpt.nuType, NoSymbol, i"Mutable $sym", "have type", addendum, sym.srcPos)
1128+
if sym.is(Mutable) then
1129+
if !sym.hasAnnotation(defn.UncheckedCapturesAnnot) then
1130+
val addendum = setup.capturedBy.get(sym) match
1131+
case Some(encl) =>
1132+
val enclStr =
1133+
if encl.isAnonymousFunction then
1134+
val location = setup.anonFunCallee.get(encl) match
1135+
case Some(meth) if meth.exists => i" argument in a call to $meth"
1136+
case _ => ""
1137+
s"an anonymous function$location"
1138+
else encl.show
1139+
i"\n\nNote that $sym does not count as local since it is captured by $enclStr"
1140+
case _ =>
1141+
""
1142+
disallowBadRootsIn(
1143+
tree.tpt.nuType, NoSymbol, i"Mutable $sym", "have type", addendum, sym.srcPos)
1144+
if sepChecksEnabled && false
1145+
&& sym.owner.isClass
1146+
&& !sym.owner.derivesFrom(defn.Caps_Mutable)
1147+
&& !sym.hasAnnotation(defn.UntrackedCapturesAnnot) then
1148+
report.error(
1149+
em"""Mutable $sym is defined in a class that does not extend `Mutable`.
1150+
|The variable needs to be annotated with `untrackedCaptures` to allow this.""",
1151+
tree.namePos)
11491152

11501153
// Lazy vals need their own environment to track captures from their RHS,
11511154
// similar to how methods work
@@ -1793,7 +1796,10 @@ class CheckCaptures extends Recheck, SymTransformer:
17931796

17941797
if needsAdaptation && !insertBox then // we are unboxing
17951798
val criticalSet = // the set with which we unbox
1796-
if covariant then captures // covariant: we box with captures of actual type plus captures leaked by inner adapation
1799+
if covariant then
1800+
if expected.expectsReadOnly && actual.derivesFromMutable
1801+
then captures.readOnly
1802+
else captures
17971803
else expected.captureSet // contravarant: we unbox with captures of epected type
17981804
//debugShowEnvs()
17991805
markFree(criticalSet, tree)

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

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Capabilities.*
88
import util.SrcPos
99
import config.Printers.capt
1010
import ast.tpd.Tree
11+
import typer.ProtoTypes.LhsProto
1112

1213
/** Handling mutability and read-only access
1314
*/
@@ -58,12 +59,9 @@ object Mutability:
5859
else true
5960
)
6061

61-
/** A read-only method is a real method (not an accessor) in a type extending
62-
* Mutable that is not an update method. Included are also lazy vals in such types.
63-
*/
64-
def isReadOnlyMethodOrLazyVal(using Context): Boolean =
65-
sym.isOneOf(MethodOrLazy, butNot = Mutable | Accessor)
66-
&& sym.owner.derivesFrom(defn.Caps_Mutable)
62+
/** A read-only member is a lazy val or a method that is not an update method. */
63+
def isReadOnlyMember(using Context): Boolean =
64+
sym.isOneOf(MethodOrLazy) && !sym.isUpdateMethod
6765

6866
private def inExclusivePartOf(cls: Symbol)(using Context): Exclusivity =
6967
import Exclusivity.*
@@ -104,19 +102,25 @@ object Mutability:
104102
case _ =>
105103
tp.exclusivity
106104

105+
def expectsReadOnly(using Context): Boolean = tp match
106+
case tp: PathSelectionProto =>
107+
tp.selector.isReadOnlyMember || tp.selector.isMutableVar && tp.pt != LhsProto
108+
case _ => tp.isValueType && !tp.isMutableType
109+
107110
extension (cs: CaptureSet)
108111
private def exclusivity(tp: Type)(using Context): Exclusivity =
109112
if cs.isExclusive then Exclusivity.OK else Exclusivity.ReadOnly(tp)
110113

111114
extension (ref: TermRef | ThisType)
112115
/** Map `ref` to `ref.readOnly` if its type extends Mutble, and one of the
113-
* following is true: it appears in a non-exclusive context, or the expected
114-
* type is a value type that is not a mutable type.
116+
* following is true:
117+
* - it appears in a non-exclusive context,
118+
* - the expected type is a value type that is not a mutable type,
119+
* - the expected type is a read-only selection
115120
*/
116121
def adjustReadOnly(pt: Type)(using Context): Capability =
117122
if ref.derivesFromMutable
118-
&& (pt.isValueType && !pt.isMutableType
119-
|| ref.exclusivityInContext != Exclusivity.OK)
123+
&& (pt.expectsReadOnly || ref.exclusivityInContext != Exclusivity.OK)
120124
then ref.readOnly
121125
else ref
122126

@@ -148,7 +152,6 @@ object Mutability:
148152
&& expected.isValueType
149153
&& (!expected.derivesFromMutable || expected.captureSet.isAlwaysReadOnly)
150154
&& !expected.isSingleton
151-
&& actual.isBoxedCapturing == expected.isBoxedCapturing
152155
then refs.readOnly
153156
else refs
154157
actual.derivedCapturingType(parent1, refs1)

compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
333333
case tp: LazyRef if !printDebug =>
334334
try toText(tp.ref)
335335
catch case ex: Throwable => "..."
336+
case sel: cc.PathSelectionProto =>
337+
"?.{ " ~ toText(sel.select.symbol) ~ "}"
336338
case AnySelectionProto =>
337339
"a type that can be selected or applied"
338340
case tp: SelectionProto =>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/var-access.scala:9:21 ------------------------------------
2+
9 | val _: () -> Int = f // error
3+
| ^
4+
| Found: (f : () ->{a.rd} Int)
5+
| Required: () -> Int
6+
|
7+
| Note that capability a.rd 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/var-access.scala:12:22 -----------------------------------
11+
12 | val _: () -> Unit = g // error
12+
| ^
13+
| Found: (g : () ->{a} Unit)
14+
| Required: () -> Unit
15+
|
16+
| Note that capability a is not included in capture set {}.
17+
|
18+
| longer explanation available when compiling with `-explain`
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import caps.*
2+
class A extends Mutable:
3+
var x: Int = 0
4+
5+
def test =
6+
val a = A()
7+
val f = () => a.x
8+
val _: () ->{a.rd} Int = f
9+
val _: () -> Int = f // error
10+
val g = () => a.x += 1
11+
val _: () ->{a} Unit = g
12+
val _: () -> Unit = g // error
13+
14+
15+
16+
17+
18+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import caps.{cap, Mutable}
2+
import caps.unsafe.untrackedCaptures
3+
4+
class Test extends Mutable:
5+
var ctxStack: Array[FreshCtx^] = new Array(10)
6+
7+
class FreshCtx(level: Int) extends Mutable:
8+
this: FreshCtx^ =>
9+
def detached: Boolean =
10+
val c: FreshCtx^{cap.rd} = ctxStack(level)
11+
(c eq this)
12+
def detached2 =
13+
ctxStack(level) eq this

0 commit comments

Comments
 (0)