diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index f6ad067168a1..41fe7c170643 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -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 diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index ad96444917b1..f1ab21275639 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -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 @@ -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) + // 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 _ => diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 859648a47c6a..bf77a3a2d566 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -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} @@ -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 @@ -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) = diff --git a/tests/pos-custom-args/captures/i25830-bounded.scala b/tests/pos-custom-args/captures/i25830-bounded.scala new file mode 100644 index 000000000000..455fd68d005d --- /dev/null +++ b/tests/pos-custom-args/captures/i25830-bounded.scala @@ -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)) diff --git a/tests/pos-custom-args/captures/i25830-capset-members.scala b/tests/pos-custom-args/captures/i25830-capset-members.scala new file mode 100644 index 000000000000..4b2bda5ade3b --- /dev/null +++ b/tests/pos-custom-args/captures/i25830-capset-members.scala @@ -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)) diff --git a/tests/pos-custom-args/captures/i25830-mixed-scopes.scala b/tests/pos-custom-args/captures/i25830-mixed-scopes.scala new file mode 100644 index 000000000000..ebf07ec8af7c --- /dev/null +++ b/tests/pos-custom-args/captures/i25830-mixed-scopes.scala @@ -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) diff --git a/tests/pos-custom-args/captures/i25830.scala b/tests/pos-custom-args/captures/i25830.scala new file mode 100644 index 000000000000..73f39a04ef80 --- /dev/null +++ b/tests/pos-custom-args/captures/i25830.scala @@ -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)