Skip to content

Exclusive capabilities #22218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
22cb87b
Elide capabilities implied by Capability subtypes when printing
odersky Dec 6, 2024
216973e
Add Mutable classes and ReadOnly capabilities
odersky Jan 10, 2025
234d0dc
Drop special handling of functions with pure arguments in Existential…
odersky Dec 15, 2024
a239bfa
Implement fresh capabilities
odersky Jan 11, 2025
d211bf9
Separation checking for applications
odersky Jan 11, 2025
fd327e3
Separation checking for blocks
odersky Jan 12, 2025
b0be75b
Address review comments
odersky Jan 19, 2025
c377f49
Use deep capturesets for separation checking.
odersky Jan 20, 2025
1cdb21b
Cache derived reach, readOnly, and maybe capabilities
odersky Jan 20, 2025
937b8af
Avoid forming intersections of capture sets on refined type lookup
odersky Jan 21, 2025
438ea60
Check separation of different parts of a declared type.
odersky Jan 24, 2025
c1f2542
Check that hidden parameters are annotated @consume
odersky Jan 25, 2025
9ca1776
Check that only @consume parameters flow to @consume parameters
odersky Jan 26, 2025
564c847
Check that SharedCapabilities don't capture `cap`.
odersky Jan 26, 2025
94fcabc
Turn separation checking on by default
odersky Jan 26, 2025
e229807
Make sure fresh results of methods only hide local refs
odersky Jan 27, 2025
84b5bff
Make sure parameters are not used again after they are consumed
odersky Jan 28, 2025
c1ad0c1
Check accesses to non-local this in hidden sets
odersky Jan 28, 2025
65a1301
Check that @consumed prefix capabilities are not re-used
odersky Jan 29, 2025
9f40649
Allow SharableCapablity anywhere on a path
odersky Jan 30, 2025
b0628d1
Polishings
odersky Jan 31, 2025
813c585
Polish and document separation checker.
odersky Feb 1, 2025
a41c10f
Address review comments
odersky Feb 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Polishings
  • Loading branch information
odersky committed Jan 31, 2025
commit b0628d1e7d08b70bc49f6001db41b3d72f2b8338
22 changes: 18 additions & 4 deletions compiler/src/dotty/tools/dotc/cc/CaptureOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,9 @@ extension (tp: Type)
case tp @ ReachCapability(_) =>
tp.singletonCaptureSet
case ReadOnlyCapability(ref) =>
ref.deepCaptureSet(includeTypevars)
val refDcs = ref.deepCaptureSet(includeTypevars)
if refDcs.isConst then CaptureSet(refDcs.elems.map(_.readOnly))
else refDcs // this case should not happen for correct programs
case tp: SingletonCaptureRef if tp.isTrackableRef =>
tp.reach.singletonCaptureSet
case _ =>
Expand Down Expand Up @@ -279,17 +281,19 @@ extension (tp: Type)
case _ =>
tp

/** The first element of this path type */
/** The first element of this path type. Note that class parameter references
* are of the form this.C but their pathroot is still this.C, not this.
*/
final def pathRoot(using Context): Type = tp.dealias match
case tp1: TermRef if tp1.symbol.maybeOwner.isClass => tp1.prefix.pathRoot
case tp1: TypeRef if !tp1.symbol.is(Param) => tp1.prefix.pathRoot
case tp1 => tp1

/** The first element of a path type, but stop at references extending
* SharableCapability
* SharedCapability.
*/
final def pathRootOrShared(using Context): Type =
if tp.derivesFrom(defn.Caps_SharedCapability) then tp
if tp.derivesFromSharedCapability then tp
else tp.dealias match
case tp1: TermRef if tp1.symbol.maybeOwner.isClass => tp1.prefix.pathRoot
case tp1: TypeRef if !tp1.symbol.is(Param) => tp1.prefix.pathRoot
Expand Down Expand Up @@ -427,6 +431,7 @@ extension (tp: Type)

def derivesFromCapability(using Context): Boolean = derivesFromCapTrait(defn.Caps_Capability)
def derivesFromMutable(using Context): Boolean = derivesFromCapTrait(defn.Caps_Mutable)
def derivesFromSharedCapability(using Context): Boolean = derivesFromCapTrait(defn.Caps_SharedCapability)

/** Drop @retains annotations everywhere */
def dropAllRetains(using Context): Type = // TODO we should drop retains from inferred types before unpickling
Expand Down Expand Up @@ -466,6 +471,11 @@ extension (tp: Type)
* is the union of all capture sets that appear in covariant position in the
* type of `x`. If `x` and `y` are different variables then `{x*}` and `{y*}`
* are unrelated.
*
* Reach capabilities cannot wrap read-only capabilities or maybe capabilities.
* We have
* (x.rd).reach = x*.rd
* (x.rd)? = (x*)?
*/
def reach(using Context): CaptureRef = tp match
case tp @ AnnotatedType(tp1: CaptureRef, annot)
Expand All @@ -483,6 +493,10 @@ extension (tp: Type)
/** If `x` is a capture ref, its read-only capability `x.rd`, represented internally
* as `x @readOnlyCapability`. We have {x.rd} <: {x}. If `x` is a reach capability `y*`,
* then its read-only version is `x.rd*`.
*
* Read-only capabilities cannot wrap maybe capabilities
* but they can wrap reach capabilities. We have
* (x?).readOnly = (x.rd)?
*/
def readOnly(using Context): CaptureRef = tp match
case tp @ AnnotatedType(tp1: CaptureRef, annot)
Expand Down
14 changes: 9 additions & 5 deletions compiler/src/dotty/tools/dotc/cc/CaptureRef.scala
Original file line number Diff line number Diff line change
Expand Up @@ -100,19 +100,25 @@ trait CaptureRef extends TypeProxy, ValueType:
/** Is this reference the generic root capability `cap` or a Fresh.Cap instance? */
final def isCapOrFresh(using Context): Boolean = isCap || isFresh

/** Is this reference one the generic root capabilities `cap` or `cap.rd` ? */
/** Is this reference one of the generic root capabilities `cap` or `cap.rd` ? */
final def isRootCapability(using Context): Boolean = this match
case ReadOnlyCapability(tp1) => tp1.isCapOrFresh
case _ => isCapOrFresh

/** Is this reference capability that does not derive from another capability ? */
/** Is this reference a capability that does not derive from another capability?
* Includes read-only versions of maximal capabilities.
*/
final def isMaxCapability(using Context): Boolean = this match
case tp: TermRef => tp.isCap || tp.info.derivesFrom(defn.Caps_Exists)
case tp: TermParamRef => tp.underlying.derivesFrom(defn.Caps_Exists)
case Fresh.Cap(_) => true
case ReadOnlyCapability(tp1) => tp1.isMaxCapability
case _ => false

/** An exclusive capability is a capability that derives
* indirectly from a maximal capability without goinh through
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* indirectly from a maximal capability without goinh through
* indirectly from a maximal capability without going through

* a read-only capability first.
*/
final def isExclusive(using Context): Boolean =
!isReadOnly && (isMaxCapability || captureSetOfInfo.isExclusive)

Expand Down Expand Up @@ -159,8 +165,6 @@ trait CaptureRef extends TypeProxy, ValueType:
* X: CapSet^c1...CapSet^c2, (CapSet^c1) subsumes y ==> X subsumes y
* Y: CapSet^c1...CapSet^c2, x subsumes (CapSet^c2) ==> x subsumes Y
* Contains[X, y] ==> X subsumes y
*
* TODO: Move to CaptureSet
*/
final def subsumes(y: CaptureRef)(using ctx: Context, vs: VarState = VarState.Separate): Boolean =

Expand Down Expand Up @@ -239,7 +243,7 @@ trait CaptureRef extends TypeProxy, ValueType:
end subsumes

/** This is a maximal capabaility that subsumes `y` in given context and VarState.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** This is a maximal capabaility that subsumes `y` in given context and VarState.
/** This is a maximal capability that subsumes `y` in given context and VarState.

* @param canAddHidden If true we allow maximal capabilties to subsume all other capabilities.
* @param canAddHidden If true we allow maximal capabilities to subsume all other capabilities.
* We add those capabilities to the hidden set if this is Fresh.Cap
* If false we only accept `y` elements that are already in the
* hidden set of this Fresh.Cap. The idea is that in a VarState that
Expand Down
4 changes: 3 additions & 1 deletion compiler/src/dotty/tools/dotc/cc/CaptureSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,8 @@ object CaptureSet:
*/
def solve()(using Context): Unit =
if !isConst then
val approx = upperApprox(empty).map(Fresh.FromCap(NoSymbol).inverse)
val approx = upperApprox(empty)
.map(Fresh.FromCap(NoSymbol).inverse) // Fresh.Cap --> cap
.showing(i"solve $this = $result", capt)
//println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}")
val newElems = approx.elems -- elems
Expand Down Expand Up @@ -1139,6 +1140,7 @@ object CaptureSet:

/** A template for maps on capabilities where f(c) <: c and f(f(c)) = c */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this not be an IdempotentCaptRefMap as well then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's always one or the other. BiTypeMaps are better for type inference.

private abstract class NarrowingCapabilityMap(using Context) extends BiTypeMap:

def mapRef(ref: CaptureRef): CaptureRef

def apply(t: Type) = t match
Expand Down
60 changes: 38 additions & 22 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -519,8 +519,7 @@ class CheckCaptures extends Recheck, SymTransformer:
def includeCallCaptures(sym: Symbol, resType: Type, tree: Tree)(using Context): Unit = resType match
case _: MethodOrPoly => // wait until method is fully applied
case _ =>
if sym.exists then
if curEnv.isOpen then markFree(capturedVars(sym), tree)
if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), tree)

/** Under the sealed policy, disallow the root capability in type arguments.
* Type arguments come either from a TypeApply node or from an AppliedType
Expand Down Expand Up @@ -556,16 +555,21 @@ class CheckCaptures extends Recheck, SymTransformer:
if param.isUseParam then markFree(arg.nuType.deepCaptureSet, errTree)
end disallowCapInTypeArgs

/** Rechecking idents involves:
* - adding call captures for idents referring to methods
* - marking as free the identifier with any selections or .rd
* modifiers implied by the expected type
*/
override def recheckIdent(tree: Ident, pt: Type)(using Context): Type =
val sym = tree.symbol
if sym.is(Method) then
// If ident refers to a parameterless method, charge its cv to the environment
includeCallCaptures(sym, sym.info, tree)
else if !sym.isStatic then
// Otherwise charge its symbol, but add all selections implied by the e
// expected type `pt`.
// Example: If we have `x` and the expected type says we select that with `.a.b`,
// we charge `x.a.b` instead of `x`.
// Otherwise charge its symbol, but add all selections and also any `.rd`
// modifier implied by the expected type `pt`.
// Example: If we have `x` and the expected type says we select that with `.a.b`
// where `b` is a read-only method, we charge `x.a.b.rd` instead of `x`.
def addSelects(ref: TermRef, pt: Type): CaptureRef = pt match
case pt: PathSelectionProto if ref.isTracked =>
if pt.sym.isReadOnlyMethod then
Expand All @@ -582,7 +586,8 @@ class CheckCaptures extends Recheck, SymTransformer:
super.recheckIdent(tree, pt)

/** The expected type for the qualifier of a selection. If the selection
* could be part of a capabaility path, we return a PathSelectionProto.
* could be part of a capability path or is a a read-only method, we return
* a PathSelectionProto.
*/
override def selectionProto(tree: Select, pt: Type)(using Context): Type =
val sym = tree.symbol
Expand Down Expand Up @@ -616,6 +621,9 @@ class CheckCaptures extends Recheck, SymTransformer:
}
case _ => denot

// Don't allow update methods to be called unless the qualifier captures
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Don't allow update methods to be called unless the qualifier captures
// Don't allow update methods to be called unless the qualifier

// contain an exclusive referenece. TODO This should probabkly rolled into
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// contain an exclusive referenece. TODO This should probabkly rolled into
// contains an exclusive referenec. TODO This should probably be rolled into

// qualifier logic once we have it.
if tree.symbol.isUpdateMethod && !qualType.captureSet.isExclusive then
report.error(
em"""cannot call update ${tree.symbol} from $qualType,
Expand Down Expand Up @@ -651,8 +659,8 @@ class CheckCaptures extends Recheck, SymTransformer:
selType
}//.showing(i"recheck sel $tree, $qualType = $result")

/** Hook for massaging a function before it is applied. Copies all @use annotations
* on method parameter symbols to the corresponding paramInfo types.
/** Hook for massaging a function before it is applied. Copies all @use and @consume
* annotations on method parameter symbols to the corresponding paramInfo types.
*/
override def prepareFunction(funtpe: MethodType, meth: Symbol)(using Context): MethodType =
val paramInfosWithUses =
Expand Down Expand Up @@ -682,7 +690,8 @@ class CheckCaptures extends Recheck, SymTransformer:
includeCallCaptures(meth, res, tree)
res

/** Recheck argument, and, if formal parameter carries a `@use`,
/** Recheck argument against a "freshened" version of `formal` where toplevel `cap`
* occurrences are replaced by `Fresh.Cap`. Also, if formal parameter carries a `@use`,
* charge the deep capture set of the actual argument to the environment.
*/
protected override def recheckArg(arg: Tree, formal: Type)(using Context): Type =
Expand Down Expand Up @@ -773,16 +782,21 @@ class CheckCaptures extends Recheck, SymTransformer:

/** First half of result pair:
* Refine the type of a constructor call `new C(t_1, ..., t_n)`
* to C{val x_1: T_1, ..., x_m: T_m} where x_1, ..., x_m are the tracked
* parameters of C and T_1, ..., T_m are the types of the corresponding arguments.
* to C{val x_1: @refineOverride T_1, ..., x_m: @refineOverride T_m}
* where x_1, ..., x_m are the tracked parameters of C and
* T_1, ..., T_m are the types of the corresponding arguments. The @refineOveride
* annotations avoid problematic intersections of capture sets when those
* parameters are selected.
*
* Second half: union of initial capture set and all capture sets of arguments
* to tracked parameters.
* to tracked parameters. The initial capture set `initCs` is augmented with
* - Fresh.Cap if `core` extends Mutable
* - Fresh.Cap.rd if `core` extends Capability
*/
def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) =
var refined: Type = core
var allCaptures: CaptureSet =
if core.derivesFromMutable then CaptureSet.fresh()
if core.derivesFromMutable then initCs ++ CaptureSet.fresh()
else if core.derivesFromCapability then initCs ++ Fresh.Cap().readOnly.singletonCaptureSet
else initCs
for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do
Expand Down Expand Up @@ -1488,7 +1502,7 @@ class CheckCaptures extends Recheck, SymTransformer:
/** If actual is a capturing type T^C extending Mutable, and expected is an
* unboxed non-singleton value type not extending mutable, narrow the capture
* set `C` to `ro(C)`.
* The unboxed condition ensures that the expected is not a type variable
* The unboxed condition ensures that the expected type is not a type variable
* that's upper bounded by a read-only type. In this case it would not be sound
* to narrow to the read-only set, since that set can be propagated
* by the type variable instantiation.
Expand All @@ -1514,9 +1528,9 @@ class CheckCaptures extends Recheck, SymTransformer:
actual
else
val improvedVAR = improveCaptures(actual.widen.dealiasKeepAnnots, actual)
val improvedRO = improveReadOnly(improvedVAR, expected)
val improved = improveReadOnly(improvedVAR, expected)
val adapted = adaptBoxed(
improvedRO.withReachCaptures(actual), expected, tree,
improved.withReachCaptures(actual), expected, tree,
covariant = true, alwaysConst = false, boxErrors)
if adapted eq improvedVAR // no .rd improvement, no box-adaptation
then actual // might as well use actual instead of improved widened
Expand Down Expand Up @@ -1563,17 +1577,19 @@ class CheckCaptures extends Recheck, SymTransformer:

/** Check that overrides don't change the @use or @consume status of their parameters */
override def additionalChecks(member: Symbol, other: Symbol)(using Context): Unit =
def fail(msg: String) =
report.error(
OverrideError(msg, self, member, other, self.memberInfo(member), self.memberInfo(other)),
if member.owner == clazz then member.srcPos else clazz.srcPos)
for
(params1, params2) <- member.rawParamss.lazyZip(other.rawParamss)
(param1, param2) <- params1.lazyZip(params2)
do
def checkAnnot(cls: ClassSymbol) =
if param1.hasAnnotation(cls) != param2.hasAnnotation(cls) then
fail(i"has a parameter ${param1.name} with different @${cls.name} status than the corresponding parameter in the overridden definition")
report.error(
OverrideError(
i"has a parameter ${param1.name} with different @${cls.name} status than the corresponding parameter in the overridden definition",
self, member, other, self.memberInfo(member), self.memberInfo(other)
),
if member.owner == clazz then member.srcPos else clazz.srcPos)

checkAnnot(defn.UseAnnot)
checkAnnot(defn.ConsumeAnnot)
end OverridingPairsCheckerCC
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/cc/Existential.scala
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ object Existential:
tp1.derivedAnnotatedType(toCap(parent), ann)
case _ => tp

/** Map existentials at the top-level and in all nested result types to `cap`
/** Map existentials at the top-level and in all nested result types to `Fresh.Cap`
*/
def toCapDeeply(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match
case Existential(boundVar, unpacked) =>
Expand Down
15 changes: 12 additions & 3 deletions compiler/src/dotty/tools/dotc/cc/Fresh.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@ import util.SimpleIdentitySet.empty
import CaptureSet.{Refs, emptySet, NarrowingCapabilityMap}
import dotty.tools.dotc.util.SimpleIdentitySet

/** Handling fresh in CC:

*/
/** A module for handling Fresh types. Fresh.Cap instances are top type that keep
* track of what they hide when capabilities get widened by subsumption to fresh.
* The module implements operations to convert between regular caps.cap and
* Fresh.Cap instances. Fresh.Cap is encoded as `caps.cap @freshCapability(...)` where
* `freshCapability(...)` is a special kind of annotation of type `Fresh.Annot`
* that contains a hidden set.
*/
object Fresh:

/** The annotation of a Fresh.Cap instance */
case class Annot(hidden: CaptureSet.HiddenSet) extends Annotation:
override def symbol(using Context) = defn.FreshCapabilityAnnot
override def tree(using Context) = New(symbol.typeRef, Nil)
Expand All @@ -32,13 +37,17 @@ object Fresh:
case _ => false
end Annot

/** The initial elements (either 0 or 1) of a hidden set created for given `owner`.
* If owner `x` is a trackable this is `x*` if reach` is true, or `x` otherwise.
*/
private def ownerToHidden(owner: Symbol, reach: Boolean)(using Context): Refs =
val ref = owner.termRef
if reach then
if ref.isTrackableRef then SimpleIdentitySet(ref.reach) else emptySet
else
if ref.isTracked then SimpleIdentitySet(ref) else emptySet

/** An extractor for "fresh" capabilities */
object Cap:

def apply(initialHidden: Refs = emptySet)(using Context): CaptureRef =
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/cc/SepCheck.scala
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
def currentOwner = role.dclSym.orElse(ctx.owner)
for hiddenRef <- prune(refsToCheck, tpe, role) do
val proot = hiddenRef.pathRootOrShared
if !proot.widen.derivesFrom(defn.Caps_SharedCapability) then
if !proot.widen.derivesFromSharedCapability then
proot match
case ref: TermRef =>
val refSym = ref.symbol
Expand Down Expand Up @@ -448,7 +448,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
role match
case _: TypeRole.Argument | _: TypeRole.Qualifier =>
for ref <- refsToCheck do
if !ref.pathRootOrShared.derivesFrom(defn.Caps_SharedCapability) then
if !ref.pathRootOrShared.derivesFromSharedCapability then
consumed.put(ref, pos)
case _ =>
end checkConsumedRefs
Expand Down
10 changes: 9 additions & 1 deletion compiler/src/dotty/tools/dotc/cc/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -345,10 +345,18 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
parent
case _ => tp

/** Check that types extending SharedCapability don't have a `cap` in their capture set.
* TODO This is not enough.
* We need to also track that we cannot get exclusive capabilities in paths
* where some prefix derives from SharedCapability. Also, can we just
* exclude `cap`, or do we have to extend this to all exclusive capabilties?
* The problem is that we know what is exclusive in general only after capture
* checking, not before.
*/
def checkSharedOK(tp: Type): tp.type =
tp match
case CapturingType(parent, refs)
if refs.isUniversal && parent.derivesFrom(defn.Caps_SharedCapability) =>
if refs.isUniversal && parent.derivesFromSharedCapability =>
fail(em"$tp extends SharedCapability, so it cannot capture `cap`")
case _ =>
tp
Expand Down
2 changes: 0 additions & 2 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1115,8 +1115,6 @@ class Definitions {
@tu lazy val SilentAnnots: Set[Symbol] =
Set(InlineParamAnnot, ErasedParamAnnot, RefineOverrideAnnot)

@tu lazy val ccParamOnlyAnnotations: Set[Symbol] = Set(UseAnnot, ConsumeAnnot)

// A list of annotations that are commonly used to indicate that a field/method argument or return
// type is not null. These annotations are used by the nullification logic in JavaNullInterop to
// improve the precision of type nullification.
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Checking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ object Checking {
if sym.isWrappedToplevelDef && !sym.isType && sym.flags.is(Infix, butNot = Extension) then
fail(ModifierNotAllowedForDefinition(Flags.Infix, s"A top-level ${sym.showKind} cannot be infix."))
if sym.isUpdateMethod && !sym.owner.derivesFrom(defn.Caps_Mutable) then
fail(em"Update methods can only be used as members of classes deriving from the `Mutable` trait")
fail(em"Update methods can only be used as members of classes extending the `Mutable` trait")
checkApplicable(Erased,
!sym.is(Lazy, butNot = Given)
&& !sym.isMutableVarOrAccessor
Expand Down
Loading
Loading