Skip to content

Commit 7a7a18f

Browse files
committed
Make Arrays mutable types under separation checking
1 parent 18a6b91 commit 7a7a18f

23 files changed

+345
-94
lines changed

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

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -394,11 +394,18 @@ extension (tp: Type)
394394
case _ =>
395395
false
396396

397-
def derivesFromCapability(using Context): Boolean = derivesFromCapTrait(defn.Caps_Capability)
398-
def derivesFromStateful(using Context): Boolean = derivesFromCapTrait(defn.Caps_Stateful)
399-
def derivesFromShared(using Context): Boolean = derivesFromCapTrait(defn.Caps_SharedCapability)
400-
def derivesFromUnscoped(using Context): Boolean = derivesFromCapTrait(defn.Caps_Unscoped)
401-
def derivesFromMutable(using Context): Boolean = derivesFromCapTrait(defn.Caps_Mutable)
397+
def derivesFromCapability(using Context): Boolean =
398+
derivesFromCapTrait(defn.Caps_Capability) || isArrayUnderStrictMut
399+
def derivesFromStateful(using Context): Boolean =
400+
derivesFromCapTrait(defn.Caps_Stateful) || isArrayUnderStrictMut
401+
def derivesFromShared(using Context): Boolean =
402+
derivesFromCapTrait(defn.Caps_SharedCapability)
403+
def derivesFromUnscoped(using Context): Boolean =
404+
derivesFromCapTrait(defn.Caps_Unscoped) || isArrayUnderStrictMut
405+
def derivesFromMutable(using Context): Boolean =
406+
derivesFromCapTrait(defn.Caps_Mutable) || isArrayUnderStrictMut
407+
408+
def isArrayUnderStrictMut(using Context): Boolean = tp.classSymbol.isArrayUnderStrictMut
402409

403410
/** Drop @retains annotations everywhere */
404411
def dropAllRetains(using Context): Type = // TODO we should drop retains from inferred types before unpickling
@@ -488,7 +495,8 @@ extension (tp: Type)
488495
tp
489496

490497
def inheritedClassifier(using Context): ClassSymbol =
491-
tp.classSymbols.map(_.classifier).foldLeft(defn.AnyClass)(leastClassifier)
498+
if tp.isArrayUnderStrictMut then defn.Caps_Unscoped
499+
else tp.classSymbols.map(_.classifier).foldLeft(defn.AnyClass)(leastClassifier)
492500

493501
extension (tp: MethodType)
494502
/** A method marks an existential scope unless it is the prefix of a curried method */
@@ -553,10 +561,13 @@ extension (sym: Symbol)
553561
* - or it is a value class
554562
* - or it is an exception
555563
* - or it is one of Nothing, Null, or String
564+
* Arrays are not pure under strict mutability even though their self type is declared pure
565+
* in Arrays.scala.
556566
*/
557567
def isPureClass(using Context): Boolean = sym match
558568
case cls: ClassSymbol =>
559-
cls.pureBaseClass.isDefined || defn.pureSimpleClasses.contains(cls)
569+
(cls.pureBaseClass.isDefined || defn.pureSimpleClasses.contains(cls))
570+
&& !cls.isArrayUnderStrictMut
560571
case _ =>
561572
false
562573

@@ -588,8 +599,8 @@ extension (sym: Symbol)
588599
&& !defn.isPolymorphicAfterErasure(sym)
589600
&& !defn.isTypeTestOrCast(sym)
590601

591-
/** It's a parameter accessor that is not annotated @constructorOnly or @uncheckedCaptures
592-
* and that is not a consume accessor.
602+
/** It's a parameter accessor for a parameter that that is not annotated
603+
* @constructorOnly or @uncheckedCaptures and that is not a consume parameter.
593604
*/
594605
def isRefiningParamAccessor(using Context): Boolean =
595606
sym.is(ParamAccessor)
@@ -600,6 +611,17 @@ extension (sym: Symbol)
600611
&& !param.hasAnnotation(defn.ConsumeAnnot)
601612
}
602613

614+
/** It's a parameter accessor that is tracked for capture checking. Excluded are
615+
* accessors for parameters annotated with constructorOnly or @uncheckedCaptures.
616+
*/
617+
def isTrackedParamAccessor(using Context): Boolean =
618+
sym.is(ParamAccessor)
619+
&& {
620+
val param = sym.owner.primaryConstructor.paramNamed(sym.name)
621+
!param.hasAnnotation(defn.ConstructorOnlyAnnot)
622+
&& !param.hasAnnotation(defn.UntrackedCapturesAnnot)
623+
}
624+
603625
def hasTrackedParts(using Context): Boolean =
604626
!CaptureSet.ofTypeDeeply(sym.info).isAlwaysEmpty
605627

@@ -642,6 +664,9 @@ extension (sym: Symbol)
642664
else if sym.name.is(TryOwnerName) then i"an enclosing try expression"
643665
else sym.show
644666

667+
def isArrayUnderStrictMut(using Context): Boolean =
668+
sym == defn.ArrayClass && ccConfig.strictMutability
669+
645670
extension (tp: AnnotatedType)
646671
/** Is this a boxed capturing type? */
647672
def isBoxed(using Context): Boolean = tp.annot match

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

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -666,13 +666,33 @@ object CaptureSet:
666666
then i" under-approximating the result of mapping $ref to $mapped"
667667
else ""
668668

669-
private def capImpliedByCapability(parent: Type)(using Context): Capability =
670-
if parent.derivesFromStateful then GlobalCap.readOnly else GlobalCap
669+
private def capImpliedByCapability(parent: Type, sym: Symbol, variance: Int)(using Context): Capability =
670+
// Since standard library classes are not compiled with separation checking,
671+
// they treat Array as a Pure class. That means, no effort is made to distinguish
672+
// between exclusive and read-only arrays. To compensate in code compiled under
673+
// strict mutability, we treat contravariant arrays in signatures of stdlib
674+
// members as read-only (so all arrays may be passed to them), and co- and
675+
// invariant arrays as exclusive.
676+
// TODO This scheme should also apply whenever code under strict mutability interfaces
677+
// with code compiled without. To do that we will need to store in the Tasty format
678+
// a flag whether code was compiled with separation checking on. This will have
679+
// to wait until 3.10.
680+
def isArrayFromScalaPackage =
681+
parent.classSymbol == defn.ArrayClass
682+
&& ccConfig.strictMutability
683+
&& variance >= 0
684+
&& sym.isContainedIn(defn.ScalaPackageClass)
685+
if parent.derivesFromStateful && !isArrayFromScalaPackage
686+
then GlobalCap.readOnly
687+
else GlobalCap
671688

672689
/* The same as {cap} but generated implicitly for references of Capability subtypes.
690+
* @param parent the type to which the capture set will be attached
691+
* @param sym the symbol carrying that type
692+
* @param variance the variance in which `parent` appears in the type of `sym`
673693
*/
674-
class CSImpliedByCapability(parent: Type)(using @constructorOnly ctx: Context)
675-
extends Const(SimpleIdentitySet(capImpliedByCapability(parent)))
694+
class CSImpliedByCapability(parent: Type, sym: Symbol, variance: Int)(using @constructorOnly ctx: Context)
695+
extends Const(SimpleIdentitySet(capImpliedByCapability(parent, sym, variance)))
676696

677697
/** A special capture set that gets added to the types of symbols that were not
678698
* themselves capture checked, in order to admit arbitrary corresponding capture

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

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,9 +1410,7 @@ class CheckCaptures extends Recheck, SymTransformer:
14101410

14111411
// (3) Capture set of self type includes capture sets of parameters
14121412
for param <- cls.paramGetters do
1413-
if !param.hasAnnotation(defn.ConstructorOnlyAnnot)
1414-
&& !param.hasAnnotation(defn.UntrackedCapturesAnnot)
1415-
then
1413+
if param.isTrackedParamAccessor then
14161414
withCapAsRoot: // OK? We need this here since self types use `cap` instead of `fresh`
14171415
checkSubset(param.termRef.captureSet, thisSet, param.srcPos)
14181416

@@ -2148,26 +2146,6 @@ class CheckCaptures extends Recheck, SymTransformer:
21482146
checker.traverse(tree.nuType)
21492147
end checkTypeParam
21502148

2151-
/** Under the unsealed policy: Arrays are like vars, check that their element types
2152-
* do not contains `cap` (in fact it would work also to check on array creation
2153-
* like we do under sealed).
2154-
*/
2155-
def checkArraysAreSealedIn(tp: Type, pos: SrcPos)(using Context): Unit =
2156-
val check = new TypeTraverser:
2157-
def traverse(t: Type): Unit =
2158-
t match
2159-
case AppliedType(tycon, arg :: Nil) if tycon.typeSymbol == defn.ArrayClass =>
2160-
if !(pos.span.isSynthetic && ctx.reporter.errorsReported)
2161-
&& !arg.typeSymbol.name.is(WildcardParamName)
2162-
then
2163-
disallowBadRootsIn(arg, NoSymbol, "Array", "have element type", "", pos)
2164-
traverseChildren(t)
2165-
case defn.RefinedFunctionOf(rinfo: MethodType) =>
2166-
traverse(rinfo)
2167-
case _ =>
2168-
traverseChildren(t)
2169-
check.traverse(tp)
2170-
21712149
/** Check that no uses refer to reach capabilities of parameters of enclosing
21722150
* methods or classes.
21732151
*/

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import config.Printers.capt
1010
import config.Feature
1111
import ast.tpd.Tree
1212
import typer.ProtoTypes.LhsProto
13+
import StdNames.nme
1314

1415
/** Handling mutability and read-only access
1516
*/
@@ -59,6 +60,7 @@ object Mutability:
5960
&& !sym.field.hasAnnotation(defn.UntrackedCapturesAnnot)
6061
else true
6162
)
63+
|| ccConfig.strictMutability && sym.name == nme.update && sym == defn.Array_update
6264

6365
/** A read-only member is a lazy val or a method that is not an update method. */
6466
def isReadOnlyMember(using Context): Boolean =

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,14 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
165165
if !pastRecheck && Feature.ccEnabledSomewhere then
166166
val sym = symd.symbol
167167
def mappedInfo =
168-
if toBeUpdated.contains(sym)
169-
then symd.info // don't transform symbols that will anyway be updated
170-
else transformExplicitType(symd.info, sym)
168+
if toBeUpdated.contains(sym) then
169+
symd.info // don't transform symbols that will anyway be updated
170+
else if sym.isArrayUnderStrictMut then
171+
val cinfo: ClassInfo = sym.info.asInstanceOf
172+
cinfo.derivedClassInfo(
173+
declaredParents = cinfo.declaredParents :+ defn.Caps_Mutable.typeRef)
174+
else
175+
transformExplicitType(symd.info, sym)
171176
if Synthetics.needsTransform(symd) then
172177
Synthetics.transform(symd, mappedInfo)
173178
else if isPreCC(sym) then
@@ -425,7 +430,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
425430
then
426431
normalizeCaptures(mapOver(t)) match
427432
case t1 @ CapturingType(_, _) => t1
428-
case t1 => CapturingType(t1, CaptureSet.CSImpliedByCapability(t1), boxed = false)
433+
case t1 => CapturingType(t1, CaptureSet.CSImpliedByCapability(t1, sym, variance), boxed = false)
429434
else normalizeCaptures(mapFollowingAliases(t))
430435

431436
def innerApply(t: Type) =

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,12 @@ object ccConfig:
6161
def newScheme(using ctx: Context): Boolean =
6262
Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.7`)
6363

64+
/** Allow @use annotations */
6465
def allowUse(using Context): Boolean =
6566
Feature.sourceVersion.stable.isAtMost(SourceVersion.`3.7`)
6667

67-
68+
/** Treat arrays as mutable types. Enabled under separation checking */
69+
def strictMutability(using Context): Boolean =
70+
Feature.enabled(Feature.separationChecking)
6871

6972
end ccConfig

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1886,7 +1886,7 @@ object SymDenotations {
18861886
myBaseTypeCache.nn
18871887
}
18881888

1889-
private def invalidateBaseDataCache() = {
1889+
def invalidateBaseDataCache() = {
18901890
baseDataCache.invalidate()
18911891
baseDataCache = BaseData.None
18921892
}

library/src/scala/caps/package.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ end internal
195195
* result of pure operation `op`, turning them into immutable types.
196196
*/
197197
@experimental
198-
def freeze(@internal.consume x: Mutable): x.type = x
198+
def freeze(@internal.consume x: Mutable | Array[?]): x.type = x
199199

200200
@experimental
201201
object unsafe:

tests/neg-custom-args/captures/existential-mapping.check

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
-- Error: tests/neg-custom-args/captures/existential-mapping.scala:46:10 -----------------------------------------------
22
46 | val z2: (x: A^) => Array[C^] = ??? // error
33
| ^^^^^^^^^^^^^^^^^^^^
4-
| Array[C^] captures the root capability `cap` in invariant position.
4+
| Array[C^]^{cap.rd} captures the root capability `cap` in invariant position.
55
| This capability cannot be converted to an existential in the result type of a function.
66
|
7-
| where: ^ refers to the universal root capability
7+
| where: ^ and cap refer to the universal root capability
88
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:9:25 ---------------------------
99
9 | val _: (x: C^) -> C = x1 // error
1010
| ^^

tests/neg-custom-args/captures/freeze-double-flip.check

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
15 | (x: Ref^) => (op: Ref^ => IRef) => op(x) // error
33
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
44
| Found: Ref^ -> (Ref^ => IRef) -> IRef
5-
| Required: scala.caps.Mutable
5+
| Required: scala.caps.Mutable | Array[?]
66
|
77
| longer explanation available when compiling with `-explain`

0 commit comments

Comments
 (0)