Skip to content

Fix handling of @use and @consume in class parameters #23342

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 4 commits into from
Jun 11, 2025
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
15 changes: 9 additions & 6 deletions compiler/src/dotty/tools/dotc/cc/Capability.scala
Original file line number Diff line number Diff line change
Expand Up @@ -375,15 +375,18 @@ object Capabilities:
case tp1: FreshCap => tp1.ccOwner
case _ => NoSymbol

final def isParamPath(using Context): Boolean = this match
final def paramPathRoot(using Context): Type = core match
case tp1: NamedType =>
tp1.prefix match
case _: ThisType | NoPrefix =>
tp1.symbol.is(Param) || tp1.symbol.is(ParamAccessor)
case prefix: CoreCapability => prefix.isParamPath
case _ => false
case _: ParamRef => true
case _ => false
if tp1.symbol.is(Param) || tp1.symbol.is(ParamAccessor) then tp1
else NoType
case prefix: CoreCapability => prefix.paramPathRoot
case _ => NoType
case tp1: ParamRef => tp1
case _ => NoType

final def isParamPath(using Context): Boolean = paramPathRoot.exists

final def ccOwner(using Context): Symbol = this match
case self: ThisType => self.cls
Expand Down
13 changes: 4 additions & 9 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,6 @@ object CheckCaptures:

def isOutermost = outer0 == null

/** If an environment is open it tracks free references */
def isOpen(using Context) = !captured.isAlwaysEmpty && kind != EnvKind.Boxed

def outersIterator: Iterator[Env] = new:
private var cur = Env.this
def hasNext = !cur.isOutermost
Expand Down Expand Up @@ -466,7 +463,7 @@ class CheckCaptures extends Recheck, SymTransformer:
def checkUseDeclared(c: Capability, env: Env, lastEnv: Env | Null) =
if lastEnv != null && env.nestedClosure.exists && env.nestedClosure == lastEnv.owner then
assert(ccConfig.deferredReaches) // access is from a nested closure under deferredReaches, so it's OK
else c.pathRoot match
else c.paramPathRoot match
case ref: NamedType if !ref.symbol.isUseParam =>
val what = if ref.isType then "Capture set parameter" else "Local reach capability"
report.error(
Expand Down Expand Up @@ -528,7 +525,7 @@ class CheckCaptures extends Recheck, SymTransformer:
case _ =>

def recur(cs: CaptureSet, env: Env, lastEnv: Env | Null): Unit =
if env.isOpen && !env.owner.isStaticOwner && !cs.isAlwaysEmpty then
if env.kind != EnvKind.Boxed && !env.owner.isStaticOwner && !cs.isAlwaysEmpty then
// Only captured references that are visible from the environment
// should be included.
val included = cs.filter: c =>
Expand Down Expand Up @@ -556,7 +553,7 @@ class CheckCaptures extends Recheck, SymTransformer:
def isRetained(ref: Capability): Boolean = ref.pathRoot match
case root: ThisType => ctx.owner.isContainedIn(root.cls)
case _ => true
if sym.exists && curEnv.isOpen then
if sym.exists && curEnv.kind != EnvKind.Boxed then
markFree(capturedVars(sym).filter(isRetained), tree)

/** If `tp` (possibly after widening singletons) is an ExprType
Expand Down Expand Up @@ -1106,7 +1103,6 @@ class CheckCaptures extends Recheck, SymTransformer:
try
// Setup environment to reflect the new owner.
val envForOwner: Map[Symbol, Env] = curEnv.outersIterator
.takeWhile(e => !capturedVars(e.owner).isAlwaysEmpty) // no refs can leak beyond this point
.map(e => (e.owner, e))
.toMap
def restoreEnvFor(sym: Symbol): Env =
Expand Down Expand Up @@ -1142,8 +1138,7 @@ class CheckCaptures extends Recheck, SymTransformer:
checkSubset(capturedVars(parent.tpe.classSymbol), localSet, parent.srcPos,
i"\nof the references allowed to be captured by $cls")
val saved = curEnv
if localSet ne CaptureSet.empty then
curEnv = Env(cls, EnvKind.Regular, localSet, curEnv)
curEnv = Env(cls, EnvKind.Regular, localSet, curEnv)
try
val thisSet = cls.classInfo.selfType.captureSet.withDescription(i"of the self type of $cls")
checkSubset(localSet, thisSet, tree.srcPos) // (2)
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1107,7 +1107,7 @@ class Definitions {
@tu lazy val NonBeanMetaAnnots: Set[Symbol] =
Set(FieldMetaAnnot, GetterMetaAnnot, ParamMetaAnnot, SetterMetaAnnot, CompanionClassMetaAnnot, CompanionMethodMetaAnnot)
@tu lazy val NonBeanParamAccessorAnnots: Set[Symbol] =
Set(PublicInBinaryAnnot)
Set(PublicInBinaryAnnot, UseAnnot, ConsumeAnnot)
@tu lazy val MetaAnnots: Set[Symbol] =
NonBeanMetaAnnots + BeanGetterMetaAnnot + BeanSetterMetaAnnot

Expand Down
15 changes: 10 additions & 5 deletions compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -252,11 +252,16 @@ object SymDenotations {
final def filterAnnotations(p: Annotation => Boolean)(using Context): Unit =
annotations = annotations.filterConserve(p)

def annotationsCarrying(meta: Set[Symbol], orNoneOf: Set[Symbol] = Set.empty)(using Context): List[Annotation] =
annotations.filterConserve(_.hasOneOfMetaAnnotation(meta, orNoneOf = orNoneOf))

def keepAnnotationsCarrying(phase: DenotTransformer, meta: Set[Symbol], orNoneOf: Set[Symbol] = Set.empty)(using Context): Unit =
updateAnnotationsAfter(phase, annotationsCarrying(meta, orNoneOf = orNoneOf))
def annotationsCarrying(meta: Set[Symbol],
orNoneOf: Set[Symbol] = Set.empty,
andAlso: Set[Symbol] = Set.empty)(using Context): List[Annotation] =
annotations.filterConserve: annot =>
annot.hasOneOfMetaAnnotation(meta, orNoneOf = orNoneOf)
|| andAlso.contains(annot.symbol)

def keepAnnotationsCarrying(phase: DenotTransformer, meta: Set[Symbol],
orNoneOf: Set[Symbol] = Set.empty, andAlso: Set[Symbol] = Set.empty)(using Context): Unit =
updateAnnotationsAfter(phase, annotationsCarrying(meta, orNoneOf, andAlso))

def updateAnnotationsAfter(phase: DenotTransformer, annots: List[Annotation])(using Context): Unit =
if annots ne annotations then
Expand Down
6 changes: 2 additions & 4 deletions compiler/src/dotty/tools/dotc/transform/PostTyper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,8 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
sym.keepAnnotationsCarrying(thisPhase, Set(defn.ParamMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots)
unusing.foreach(sym.addAnnotation)
else if sym.is(ParamAccessor) then
// @publicInBinary is not a meta-annotation and therefore not kept by `keepAnnotationsCarrying`
val publicInBinaryAnnotOpt = sym.getAnnotation(defn.PublicInBinaryAnnot)
sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot))
for publicInBinaryAnnot <- publicInBinaryAnnotOpt do sym.addAnnotation(publicInBinaryAnnot)
sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot),
andAlso = defn.NonBeanParamAccessorAnnots)
else
sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots)
if sym.isScala2Macro && !ctx.settings.XignoreScala2Macros.value &&
Expand Down
10 changes: 10 additions & 0 deletions tests/neg-custom-args/captures/i23303.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Error: tests/neg-custom-args/captures/i23303.scala:6:36 -------------------------------------------------------------
6 | def execute: Unit = ops.foreach(f => f()) // error
| ^^^^^^^^
| Local reach capability Runner.this.ops* leaks into capture scope of class Runner.
| To allow this, the value ops should be declared with a @use annotation
-- Error: tests/neg-custom-args/captures/i23303.scala:9:22 -------------------------------------------------------------
9 | () => ops.foreach(f => f()) // error
| ^^^^^^^^
| Local reach capability ops* leaks into capture scope of method Runner2.
| To allow this, the parameter ops should be declared with a @use annotation
17 changes: 17 additions & 0 deletions tests/neg-custom-args/captures/i23303.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import language.experimental.captureChecking
import caps.use

class Test:
class Runner(ops: List[() => Unit]):
def execute: Unit = ops.foreach(f => f()) // error

def Runner2(ops: List[() => Unit]) =
() => ops.foreach(f => f()) // error


class Test2:
class Runner(@use ops: List[() => Unit]):
def execute: Unit = ops.foreach(f => f()) //ok

private def Runner2(@use ops: List[() => Unit]) =
() => ops.foreach(f => f()) // ok
Loading