Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/cc/CaptureSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1565,7 +1565,7 @@ object CaptureSet:
// `ref` will not seem subsumed by other capabilities in a `++`.
universal
case c: CoreCapability =>
ofType(c.underlying, followResult = true)
ofType(c.underlying, followResult = ccConfig.useSpanCapset)

/** Capture set of a type
* @param followResult If true, also include capture sets of function results.
Expand Down
121 changes: 87 additions & 34 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -407,25 +407,22 @@ class CheckCaptures extends Recheck, SymTransformer:
else i"references $cs1$cs1description are not all",
cs1, cs2, pos, provenance)

/** If `sym` is a class or method nested inside a term, a capture set variable representing
* the captured variables of the environment associated with `sym`.
/** If `sym` is a method or a non-static inner class, a capture set variable
* representing the captured variables of the environment associated with `sym`.
*/
def capturedVars(sym: Symbol)(using Context): CaptureSet =
myCapturedVars.getOrElseUpdate(sym,
if sym.ownersIterator.exists(_.isTerm)
if sym.isTerm || !sym.owner.isStaticOwner
then CaptureSet.Var(sym.owner, level = ccState.symLevel(sym))
else CaptureSet.empty)

// ---- Record Uses with MarkFree ----------------------------------------------------

/** The next environment enclosing `env` that needs to be charged
* with free references.
* @param included Whether an environment is included in the range of
* environments to charge. Once `included` is false, no
* more environments need to be charged.
*/
def nextEnvToCharge(env: Env, included: Env => Boolean)(using Context): Env =
if env.owner.isConstructor && included(env.outer) then env.outer.outer
def nextEnvToCharge(env: Env)(using Context): Env | Null =
if env.owner.isConstructor then env.outer.outer0
else env.outer

/** A description where this environment comes from */
Expand Down Expand Up @@ -458,21 +455,27 @@ class CheckCaptures extends Recheck, SymTransformer:
markFree(sym, sym.termRef, tree)

def markFree(sym: Symbol, ref: Capability, tree: Tree)(using Context): Unit =
if sym.exists && ref.isTracked then markFree(ref.singletonCaptureSet, tree)
if sym.exists then markFree(ref, tree)

def markFree(ref: Capability, tree: Tree)(using Context): Unit =
if ref.isTracked then markFree(ref.singletonCaptureSet, tree)

/** Make sure the (projected) `cs` is a subset of the capture sets of all enclosing
* environments. At each stage, only include references from `cs` that are outside
* the environment's owner
*/
def markFree(cs: CaptureSet, tree: Tree)(using Context): Unit =
def markFree(cs: CaptureSet, tree: Tree, addUseInfo: Boolean = true)(using Context): Unit =
// A captured reference with the symbol `sym` is visible from the environment
// if `sym` is not defined inside the owner of the environment.
inline def isVisibleFromEnv(sym: Symbol, env: Env) =
sym.exists && {
val effectiveOwner =
if env.owner.isConstructor then env.owner.owner
else env.owner
if env.kind == EnvKind.NestedInOwner then
!sym.isProperlyContainedIn(env.owner)
!sym.isProperlyContainedIn(effectiveOwner)
else
!sym.isContainedIn(env.owner)
!sym.isContainedIn(effectiveOwner)
}

/** Avoid locally defined capability by charging the underlying type
Expand All @@ -493,7 +496,7 @@ class CheckCaptures extends Recheck, SymTransformer:
case _ => c.core match
case c1: RootCapability => c1.singletonCaptureSet
case c1: CoreCapability =>
CaptureSet.ofType(c1.widen, followResult = true)
CaptureSet.ofType(c1.widen, followResult = ccConfig.useSpanCapset)
capt.println(i"Widen reach $c to $underlying in ${env.owner}")
underlying.disallowBadRoots(NoSymbol): () =>
report.error(em"Local capability $c${env.owner.qualString("in")} cannot have `cap` as underlying capture set", tree.srcPos)
Expand Down Expand Up @@ -535,13 +538,15 @@ class CheckCaptures extends Recheck, SymTransformer:
checkSubset(included, env.captured, tree.srcPos, provenance(env))
capt.println(i"Include call or box capture $included from $cs in ${env.owner} --> ${env.captured}")
if !isOfNestedMethod(env) then
recur(included, nextEnvToCharge(env, !_.owner.isStaticOwner), env)
val nextEnv = nextEnvToCharge(env)
if nextEnv != null && !nextEnv.owner.isStaticOwner then
recur(included, nextEnv, env)
// Under deferredReaches, don't propagate out of methods inside terms.
// The use set of these methods will be charged when that method is called.

if !cs.isAlwaysEmpty then
recur(cs, curEnv, null)
useInfos += ((tree, cs, curEnv))
if addUseInfo then useInfos += ((tree, cs, curEnv))
end markFree

/** If capability `c` refers to a parameter that is not implicitly or explicitly
Expand Down Expand Up @@ -626,25 +631,33 @@ class CheckCaptures extends Recheck, SymTransformer:
// 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 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): Capability = pt match
case pt: PathSelectionProto if ref.isTracked =>
if pt.sym.isReadOnlyMethod then
ref.readOnly
else
// if `ref` is not tracked then the selection could not give anything new
// class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters.
addSelects(ref.select(pt.sym).asInstanceOf[TermRef], pt.pt)
case _ => ref
var pathRef: Capability = addSelects(sym.termRef, pt)
if pathRef.derivesFromMutable && pt.isValueType && !pt.isMutableType then
pathRef = pathRef.readOnly
markFree(sym, pathRef, tree)
markFree(sym, pathRef(sym.termRef, pt), tree)
mapResultRoots(super.recheckIdent(tree, pt), tree.symbol)

override def recheckThis(tree: This, pt: Type)(using Context): Type =
markFree(pathRef(tree.tpe.asInstanceOf[ThisType], pt), tree)
super.recheckThis(tree, pt)

/** Add all selections and also any `.rd modifier implied by the expected
* type `pt` to `base`. 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`.
*/
private def pathRef(base: TermRef | ThisType, pt: Type)(using Context): Capability =
def addSelects(ref: TermRef | ThisType, pt: Type): Capability = pt match
case pt: PathSelectionProto if ref.isTracked =>
if pt.sym.isReadOnlyMethod then
ref.readOnly
else
// if `ref` is not tracked then the selection could not give anything new
// class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters.
addSelects(ref.select(pt.sym).asInstanceOf[TermRef], pt.pt)
case _ => ref
val ref: Capability = addSelects(base, pt)
if ref.derivesFromMutable && pt.isValueType && !pt.isMutableType
then ref.readOnly
else ref

/** The expected type for the qualifier of a selection. If the selection
* could be part of a capability path or is a a read-only method, we return
* a PathSelectionProto.
Expand Down Expand Up @@ -866,7 +879,7 @@ class CheckCaptures extends Recheck, SymTransformer:
val (refined, cs) = addParamArgRefinements(core, initCs)
refined.capturing(cs)

augmentConstructorType(resType, capturedVars(cls) ++ capturedVars(constr))
augmentConstructorType(resType, capturedVars(cls))
.showing(i"constr type $mt with $argTypes%, % in $constr = $result", capt)
end refineConstructorInstance

Expand Down Expand Up @@ -975,6 +988,8 @@ class CheckCaptures extends Recheck, SymTransformer:
* - Interpolate contravariant capture set variables in result type.
*/
override def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Type =
val savedEnv = curEnv
val runInConstructor = !sym.isOneOf(Param | ParamAccessor | Lazy | NonMember)
try
if sym.is(Module) then sym.info // Modules are checked by checking the module class
else
Expand All @@ -993,6 +1008,8 @@ class CheckCaptures extends Recheck, SymTransformer:
""
disallowBadRootsIn(
tree.tpt.nuType, NoSymbol, i"Mutable $sym", "have type", addendum, sym.srcPos)
if runInConstructor then
pushConstructorEnv()
checkInferredResult(super.recheckValDef(tree, sym), tree)
finally
if !sym.is(Param) then
Expand All @@ -1002,6 +1019,22 @@ class CheckCaptures extends Recheck, SymTransformer:
// function is compiled since we do not propagate expected types into blocks.
interpolateIfInferred(tree.tpt, sym)

def declaredCaptures = tree.tpt.nuType.captureSet
if runInConstructor && savedEnv.owner.isClass then
curEnv = savedEnv
markFree(declaredCaptures, tree, addUseInfo = false)

if sym.owner.isStaticOwner && !declaredCaptures.elems.isEmpty && sym != defn.captureRoot then
def where =
if sym.effectiveOwner.is(Package) then "top-level definition"
else i"member of static ${sym.owner}"
report.warning(
em"""$sym has a non-empty capture set but will not be added as
|a capability to computed capture sets since it is globally accessible
|as a $where. Global values cannot be capabilities.""",
tree.namePos)
end recheckValDef

/** Recheck method definitions:
* - check body in a nested environment that tracks uses, in a nested level,
* and in a nested context that knows abaout Contains parameters so that we
Expand Down Expand Up @@ -1228,6 +1261,24 @@ class CheckCaptures extends Recheck, SymTransformer:
recheckFinish(result, arg, pt)
*/

/** If environment is owned by a class, run in a new environment owned by
* its primary constructor instead.
*/
def pushConstructorEnv()(using Context): Unit =
if curEnv.owner.isClass then
val constr = curEnv.owner.primaryConstructor
if constr.exists then
val constrSet = capturedVars(constr)
if capturedVars(constr) ne CaptureSet.empty then
curEnv = Env(constr, EnvKind.Regular, constrSet, curEnv)

override def recheckStat(stat: Tree)(using Context): Unit =
val saved = curEnv
if !stat.isInstanceOf[MemberDef] then
pushConstructorEnv()
try recheck(stat)
finally curEnv = saved

/** The main recheck method does some box adapation for all nodes:
* - If expected type `pt` is boxed and the tree is a lambda or a reference,
* don't propagate free variables.
Expand Down Expand Up @@ -2021,7 +2072,9 @@ class CheckCaptures extends Recheck, SymTransformer:
if env.kind == EnvKind.Boxed then env.owner
else if isOfNestedMethod(env) then env.owner.owner
else if env.owner.isStaticOwner then NoSymbol
else boxedOwner(nextEnvToCharge(env, alwaysTrue))
else
val nextEnv = nextEnvToCharge(env)
if nextEnv == null then NoSymbol else boxedOwner(nextEnv)

def checkUseUnlessBoxed(c: Capability, croot: NamedType) =
if !boxedOwner(env).isContainedIn(croot.symbol.owner) then
Expand Down
21 changes: 1 addition & 20 deletions compiler/src/dotty/tools/dotc/cc/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
if sym.isType then stripImpliedCaptureSet(tp2)
else tp2
if freshen then
capToFresh(tp3, Origin.InDecl(sym)).tap(addOwnerAsHidden(_, sym))
capToFresh(tp3, Origin.InDecl(sym))
else tp3
end transformExplicitType

Expand All @@ -473,25 +473,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
extension (sym: Symbol) def nextInfo(using Context): Type =
atPhase(thisPhase.next)(sym.info)

private def addOwnerAsHidden(tp: Type, owner: Symbol)(using Context): Unit =
val ref = owner.termRef
def add = new TypeTraverser:
var reach = false
def traverse(t: Type): Unit = t match
case t @ CapturingType(parent, refs) =>
val saved = reach
reach |= t.isBoxed
try
traverse(parent)
for case fresh: FreshCap <- refs.elems.iterator do // TODO: what about fresh.rd elems?
if reach then fresh.hiddenSet.elems += ref.reach
else if ref.isTracked then fresh.hiddenSet.elems += ref
finally reach = saved
case _ =>
traverseChildren(t)
if ref.isTrackableRef then add.traverse(tp)
end addOwnerAsHidden

/** A traverser that adds knownTypes and updates symbol infos */
def setupTraverser(checker: CheckerAPI) = new TreeTraverserWithPreciseImportContexts:
import checker.*
Expand Down
5 changes: 5 additions & 0 deletions compiler/src/dotty/tools/dotc/cc/ccConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ object ccConfig:
*/
inline val postCheckCapturesets = false

/** If true take as the underlying capture set of a capability of function type
* the capture set along the span, including capture sets of function results.
*/
inline val useSpanCapset = false

/** If true, do level checking for FreshCap instances */
def useFreshLevels(using Context): Boolean =
Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.7`)
Expand Down
29 changes: 18 additions & 11 deletions compiler/src/dotty/tools/dotc/transform/Recheck.scala
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ abstract class Recheck extends Phase, SymTransformer:
def recheckSelection(tree: Select, qualType: Type, name: Name, pt: Type)(using Context): Type =
recheckSelection(tree, qualType, name, sharpen = identity[Denotation])

def recheckThis(tree: This, pt: Type)(using Context): Type =
tree.tpe

def recheckSuper(tree: Super, pt: Type)(using Context): Type =
tree.tpe

def recheckBind(tree: Bind, pt: Type)(using Context): Type = tree match
case Bind(name, body) =>
recheck(body, pt)
Expand Down Expand Up @@ -370,25 +376,21 @@ abstract class Recheck extends Phase, SymTransformer:
recheck(tree.rhs, lhsType.widen)
defn.UnitType

private def recheckBlock(stats: List[Tree], expr: Tree)(using Context): Type =
private def recheckBlock(stats: List[Tree], expr: Tree, pt: Type)(using Context): Type =
recheckStats(stats)
val exprType = recheck(expr)
val exprType = recheck(expr, pt)
TypeOps.avoid(exprType, localSyms(stats).filterConserve(_.isTerm))

def recheckBlock(tree: Block, pt: Type)(using Context): Type = tree match
case Block(Nil, expr: Block) => recheckBlock(expr, pt)
case Block((mdef : DefDef) :: Nil, closure: Closure) =>
recheckClosureBlock(mdef, closure.withSpan(tree.span), pt)
case Block(stats, expr) => recheckBlock(stats, expr)
// The expected type `pt` is not propagated. Doing so would allow variables in the
// expected type to contain references to local symbols of the block, so the
// local symbols could escape that way.
case Block(stats, expr) => recheckBlock(stats, expr, pt)

def recheckClosureBlock(mdef: DefDef, expr: Closure, pt: Type)(using Context): Type =
recheckBlock(mdef :: Nil, expr)
recheckBlock(mdef :: Nil, expr, pt)

def recheckInlined(tree: Inlined, pt: Type)(using Context): Type =
recheckBlock(tree.bindings, tree.expansion)(using inlineContext(tree))
recheckBlock(tree.bindings, tree.expansion, pt)(using inlineContext(tree))

def recheckIf(tree: If, pt: Type)(using Context): Type =
recheck(tree.cond, defn.BooleanType)
Expand Down Expand Up @@ -487,12 +489,15 @@ abstract class Recheck extends Phase, SymTransformer:
recheckStats(tree.stats)
NoType

def recheckStat(stat: Tree)(using Context): Unit =
recheck(stat)

def recheckStats(stats: List[Tree])(using Context): Unit =
@tailrec def traverse(stats: List[Tree])(using Context): Unit = stats match
case (imp: Import) :: rest =>
traverse(rest)(using ctx.importContext(imp, imp.symbol))
case stat :: rest =>
recheck(stat)
recheckStat(stat)
traverse(rest)
case _ =>
traverse(stats)
Expand Down Expand Up @@ -540,7 +545,9 @@ abstract class Recheck extends Phase, SymTransformer:
def recheckUnnamed(tree: Tree, pt: Type): Type = tree match
case tree: Apply => recheckApply(tree, pt)
case tree: TypeApply => recheckTypeApply(tree, pt)
case _: New | _: This | _: Super | _: Literal => tree.tpe
case tree: This => recheckThis(tree, pt)
case tree: Super => recheckSuper(tree, pt)
case _: New | _: Literal => tree.tpe
case tree: Typed => recheckTyped(tree)
case tree: Assign => recheckAssign(tree)
case tree: Block => recheckBlock(tree, pt)
Expand Down
Loading
Loading