Skip to content
Closed
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
49 changes: 45 additions & 4 deletions compiler/src/dotty/tools/dotc/cc/CaptureOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -898,17 +898,58 @@ extension (tp: AnnotatedType) {
/** A prototype that indicates selection */
class PathSelectionProto(val selector: Symbol, val pt: Type, val tree: Tree) extends typer.ProtoTypes.WildcardSelectionProto

/** Drop retains annotations in the inferred type if CC is not enabled
* or transform them into retains annotations with Nothing (i.e. empty set) as
* argument if CC is enabled (we need to do that to keep by-name status).
/** Drop retains annotations in the inferred type if CC is not enabled.
* When CC is enabled: keep only retained refs that are "local" capture-set
* type params/members — `TypeParamRef`s whose binder is in scope, or
* `TypeRef`s whose owner is an enclosing anon function/class. Other refs
* are erased to `Nothing` so Setup's `addVar` can install a widenable
* `Var` (preserving them would pin the set to a `Const` — breaks
* nicolas1). Inside `TypeBounds`, all CapSet-derived refs are preserved
* (bounds are contracts). See i25830.
*/
class CleanupRetains(using Context) extends TypeMap:
private var binders: List[LambdaType] = Nil
private var inBound: Boolean = false

private def isLocalCapSetRef(tp: Type): Boolean = tp match
case ref: TypeParamRef =>
ref.derivesFromCapSet && (inBound || binders.contains(ref.binder))
case ref: TypeRef if ref.derivesFromCapSet && ref.symbol.isType =>
val owner = ref.symbol.owner
inBound
|| owner.exists
&& (owner.isAnonymousFunction || owner.isClass)
&& ctx.owner.isContainedIn(owner)
case _ => false

private def filterLocal(tp: Type): Type = tp match
case OrType(tp1, tp2) =>
val tp1f = filterLocal(tp1)
val tp2f = filterLocal(tp2)
if tp1f.isNothingType then tp2f
else if tp2f.isNothingType then tp1f
else OrType(tp1f, tp2f, soft = false)
case _ =>
if isLocalCapSetRef(tp) then tp else defn.NothingType

def apply(tp: Type): Type = tp match
case tp @ AnnotatedType(parent, annot: RetainingAnnotation) =>
if Feature.ccEnabled then
if annot.symbol == defn.RetainsCapAnnot then tp
else AnnotatedType(this(parent), RetainingAnnotation(annot.symbol.asClass, defn.NothingType))
else
val filtered = filterLocal(annot.retainedType)
AnnotatedType(this(parent),
if filtered eq annot.retainedType then annot
else RetainingAnnotation(annot.symbol.asClass, filtered))
else this(parent)
case tp: LambdaType =>
val saved = binders
binders = tp :: binders
try mapOver(tp) finally binders = saved
case tp: TypeBounds =>
val saved = inBound
inBound = true
try mapOver(tp) finally inBound = saved
case _ => mapOver(tp)

/** A base class for extractors that match annotated types with a specific
Expand Down
24 changes: 21 additions & 3 deletions compiler/src/dotty/tools/dotc/cc/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -338,14 +338,29 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:

def innerApply(tp: Type) =
val tp1 = tp match
case AnnotatedType(parent, annot: RetainingAnnotation) =>
// Promote CapSet-derived refs preserved by CleanupRetains into a
// Const CapturingType; strip any empty Var that `apply(parent)`
// would have layered underneath. See i25830.
val kept = annot.retainedType.retainedElementsRaw.flatMap: e =>
if e.derivesFromCapSet then
try e.toCapability :: Nil
catch case _: IllegalCaptureRef => Nil
else Nil
if kept.isEmpty then apply(parent)
else
val parent1 = apply(parent) match
case CapturingType(p, refs) if refs.elems.isEmpty && !refs.isConst => p
case other => other
CapturingType(parent1, CaptureSet(kept*))
case AnnotatedType(parent, annot)
if annot.symbol.isRetains || annot.symbol == defn.InferredAnnot =>
// Drop explicit retains and @inferred annotations
// Drop non-RetainingAnnotation retains (e.g. pickle-read) and @inferred.
apply(parent)
case tp: TypeLambda =>
// Don't recurse into parameter bounds, just cleanup any stray retains annotations
// Leave parameter bounds alone — CleanupRetains already filtered them.
tp.derivedLambdaType(
paramInfos = tp.paramInfos.mapConserve(_.dropAllRetains.bounds),
paramInfos = tp.paramInfos,
resType = this(tp.resType))
case tp @ RefinedType(parent, rname, rinfo) =>
val saved = refiningNames
Expand Down Expand Up @@ -899,6 +914,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
needsVariable(parent)
&& refs.isConst // if refs is a variable, no need to add another
&& !refs.isUniversal // if refs is {caps.any}, an added variable would not change anything
&& !refs.elems.exists(_.coreType.derivesFromCapSet)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@odersky the exists instead of forall is for the case we have mixed retains of the form {C, x}.

// Const sets containing capset-param refs must stay Const: a Var's
// elements don't rewrite under SubstParamsMap. See i25830.
case AnnotatedType(parent, _) =>
needsVariable(parent)
case _ =>
Expand Down
37 changes: 11 additions & 26 deletions compiler/src/dotty/tools/dotc/inlines/Inlines.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import dotty.tools.dotc.transform.MegaPhase.MiniPhase
import parsing.Parsers.Parser
import transform.{PostTyper, Inlining, CrossVersionChecks}
import staging.StagingLevel
import cc.CleanupRetains

import collection.mutable
import reporting.{NotConstant, trace}
Expand Down Expand Up @@ -102,32 +101,18 @@ object Inlines:
*/
def inlineCall(tree: Tree)(using Context): Tree = ctx.profiler.onInlineCall(tree.symbol):

/** Strip @retains annotations from inferred types in the call tree */
val stripRetains = CleanupRetains()
val stripper = new TreeTypeMap(
treeMap = {
case tree: InferredTypeTree =>
val stripped = stripRetains(tree.tpe)
if stripped ne tree.tpe then tree.withType(stripped)
else tree
case tree => tree
}
)

val tree0 = stripper.transform(tree)

if tree0.symbol.denot.exists
&& tree0.symbol.effectiveOwner == defn.CompiletimeTestingPackage.moduleClass
if tree.symbol.denot.exists
&& tree.symbol.effectiveOwner == defn.CompiletimeTestingPackage.moduleClass
then
if (tree0.symbol == defn.CompiletimeTesting_typeChecks) return Intrinsics.typeChecks(tree0)
if (tree0.symbol == defn.CompiletimeTesting_typeCheckErrors) return Intrinsics.typeCheckErrors(tree0)
if (tree.symbol == defn.CompiletimeTesting_typeChecks) return Intrinsics.typeChecks(tree)
if (tree.symbol == defn.CompiletimeTesting_typeCheckErrors) return Intrinsics.typeCheckErrors(tree)

if ctx.isAfterTyper then
// During typer we wait with cross version checks until PostTyper, in order
// not to provoke cyclic references. See i16116 for a test case.
CrossVersionChecks.checkRef(tree0.symbol, tree0.srcPos)
CrossVersionChecks.checkRef(tree.symbol, tree.srcPos)

if tree0.symbol.isConstructor then return tree // error already reported for the inline constructor definition
if tree.symbol.isConstructor then return tree // error already reported for the inline constructor definition

/** Set the position of all trees logically contained in the expansion of
* inlined call `call` to the position of `call`. This transform is necessary
Expand Down Expand Up @@ -175,17 +160,17 @@ object Inlines:
tree
}

// assertAllPositioned(tree0) // debug
val tree1 = liftBindings(tree0, identity)
// assertAllPositioned(tree) // debug
val tree1 = liftBindings(tree, identity)
val tree2 =
if bindings.nonEmpty then
cpy.Block(tree0)(bindings.toList, inlineCall(tree1))
cpy.Block(tree)(bindings.toList, inlineCall(tree1))
else if enclosingInlineds.length < ctx.settings.XmaxInlines.value && !reachedInlinedTreesLimit then
val body =
try bodyToInline(tree0.symbol) // can typecheck the tree and thereby produce errors
try bodyToInline(tree.symbol) // can typecheck the tree and thereby produce errors
catch case _: MissingInlineInfo =>
throw CyclicReference(ctx.owner)
new InlineCall(tree0).expand(body)
new InlineCall(tree).expand(body)
else
ctx.base.stopInlining = true
val (reason, setting) =
Expand Down
39 changes: 39 additions & 0 deletions tests/pos-custom-args/captures/i25830-bounded.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import language.experimental.captureChecking
import caps.*

class File extends SharedCapability

// Bounded capture polymorphism: retains inside type-parameter bounds must
// survive PostTyper's CleanupRetains. The `inBound` flag in CleanupRetains
// preserves any CapSet-derived ref found inside a TypeBounds, regardless
// of whether its binder/owner is a named method. See i25830.

// (A) Flat bounded: `[C^, D^ <: {C}]` in a single poly lambda literal.
// `{C}` in the bound of `D` is a TypeParamRef to the lambda's own
// PolyType — the binders stack already handles it.
def testFlat() =
val f = { [C^, D^ <: {C}] => (xs: List[File^{D}]) => xs }
val a = File()
val _ : List[File^{a}] = f[{a}, {a}](List[File^{a}](a))

// (B) Outer `def` + inner poly lambda whose bound references the outer
// def's capset param. `{C}` in the bound is a ref to a *named*
// method's type param. Without the `inBound` flag, the scope check
// would erase `{C}` (named methods are normally excluded). With it,
// bound retains are preserved unconditionally.
def testCurried[C^]: (xs: List[File^{C}]) => List[File^{C}] =
val f = { [D^ <: {C}] => (xs: List[File^{D}]) => xs }
f[{C}]

def useCurried() =
val a = File()
val _ : List[File^{a}] = testCurried[{a}](List[File^{a}](a))

// (C) Bound references an outer-class capset param.
class Holder[OuterC^]:
val mk = { [D^ <: {OuterC}] => (xs: List[File^{D}]) => xs }

def useHolder() =
val a = File()
val h = new Holder[{a}]
val _ : List[File^{a}] = h.mk[{a}](List[File^{a}](a))
17 changes: 17 additions & 0 deletions tests/pos-custom-args/captures/i25830-capset-members.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import language.experimental.captureChecking
import caps.*

class File extends SharedCapability

// Capture-set type member of a class appears in the inferred tpt of a
// polymorphic lambda inside the class. `{C}` here is a TypeRef to the
// class's type member — it must be preserved through PostTyper's
// CleanupRetains via the owner-ancestry branch. See i25830.
class Box[X^](val x0: File^{X}):
type C^ = X
val mk = { [D^] => (xs: List[File^{C}]) => xs.head }

def useBox() =
val a = File(); val b = File()
val bx = new Box[{a}](a)
val _ : File^{a} = bx.mk[{b}](List[File^{a}](a))
86 changes: 86 additions & 0 deletions tests/pos-custom-args/captures/i25830-mixed-scopes.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import language.experimental.captureChecking
import caps.*

class File extends SharedCapability

// Every local poly lambda below mentions at least one *outer* capture-set
// parameter in its own signature, and varies the lambda's own binder shape —
// multiple cap-sets, interleaved with plain types, nested poly, etc.
// Shapes where the local lambda does not reference anything from the outer
// scope are covered by tests/pos-custom-args/captures/i25830.scala.

// (1) val-form of the `def g[C^] = (xs) => xs.head` TODO line from
// tests/neg-custom-args/captures/use-capset.scala. The def form still
// errors (separate @use/classifier issue); the val form works because
// the lambda binds its own C.
def useCapsetVal() =
val g = { [C^] => (xs: List[File^{C}]) => xs.head }
val io = File()
val _ : File^{io} = g[{io}](List[File^{io}](io))

// (2) Baseline: simple local `[C^]` inside a def with `[OuterC^]`, the
// lambda's signature mentions both.
def baseline[OuterC^](a: File^{OuterC}) =
val mk = { [C^] => (x: File^{C}, y: File^{OuterC}) => x }
val b = File()
val _ : File^{b} = mk[{b}](b, a)

// (3) Fully interleaved local binders `[T, C^, U, D^]` around an outer cap-set.
def interleaved_local[OuterC^](a: File^{OuterC}) =
val mk = { [T, C^, U, D^] =>
(t: T, x: File^{C}, u: U, y: File^{D}, z: File^{OuterC}) => (t, u, x)
}
val b = File(); val c = File()
val _ = mk[Int, {b}, String, {c}](1, b, "s", c, a)

// (4) Outer itself mixes plain + cap-set; local lambda does too.
def mixed_both_sides[T, OuterC^](t: T, a: File^{OuterC}) =
val mk = { [U, C^] =>
(u: U, x: File^{C}, t2: T, y: File^{OuterC}) => (u, x, t2)
}
val b = File()
val _ = mk[Boolean, {b}](true, b, t, a)

// (5) Class field: lambda's interleaved signature mentions the class's
// cap-set parameter.
class Holder[OuterC^](val outer: File^{OuterC}):
val mk = { [T, C^, U] =>
(t: T, x: File^{C}, u: U, y: File^{OuterC}) => x
}

def useHolder() =
val a = File(); val b = File()
val h = Holder[{a}](a)
val _ : File^{b} = h.mk[Int, {b}, String](1, b, "s", a)

// (6) Trait abstract member + subclass override, both mentioning the
// enclosing cap-set parameter. Exercises the explicit-tpt path.
trait Ops[OuterC^]:
val mk: [T, C^] -> (t: T, x: File^{C}, y: File^{OuterC}) -> File^{C}

class OpsImpl[X^](x0: File^{X}) extends Ops[X]:
val mk = { [T, C^] => (t: T, x: File^{C}, y: File^{X}) => x }

def useOps() =
val a = File(); val b = File()
val ops = OpsImpl[{a}](a)
val _ : File^{b} = ops.mk[Int, {b}](7, b, a)

// (7) Nested outer scopes (class + inner def) each contribute a cap-set;
// the local lambda mentions both.
class Outer[X^](val xr: File^{X}):
def inner[T, Y^](yr: File^{Y})(t0: T) =
val mk = { [U, C^] =>
(u: U, x: File^{C}, a: File^{X}, b: File^{Y}, t: T) => x
}
val c = File()
val _ : File^{c} = mk[String, {c}]("s", c, xr, yr, t0)

// (8) Nested poly lambda: the body of the outer lambda is itself a poly
// lambda, with interleaved binders at each level, and the inner level
// mentions the enclosing method's OuterC.
def nested_poly_interleaved[OuterC^](a: File^{OuterC}) =
val mk = { [T, C^] => (t: T, x: File^{C}) =>
{ [U, D^] => (u: U, y: File^{D}, z: File^{OuterC}) => (t, x) } }
val b = File(); val c = File()
val _ = mk[Int, {b}](1, b)[String, {c}]("s", c, a)
10 changes: 10 additions & 0 deletions tests/pos-custom-args/captures/i25830.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import language.experimental.captureChecking
import caps.*

class File extends SharedCapability

@main def test =
val convert = { [C^] => (xs: List[File^{C}]) => xs.map(_ => ()) }
val x = File()
val files: List[File^{x}] = List(x)
val result = convert[{x}](files)
Loading