From f94b27caf962cfa066b0b91fef6cf4649afd642a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Thu, 16 Apr 2026 18:40:50 +0200 Subject: [PATCH 1/6] Fix type inference for capture-polymorphic lambdas Fix #25830 **Problem.** `val convert = { [C^] => (xs: List[File^{C}]) => xs.map(_ => ()) }` had its capture-set type parameter `C` silently erased during capture checking, so `convert[{x}](files)` failed with `List[File^{}]` instead of `List[File^{x}]`. **Root cause.** Three places dropped or rewrote the `{C}` reference in the val's inferred type: 1. `CleanupRetains` in [CaptureOps.scala](compiler/src/dotty/tools/dotc/cc/CaptureOps.scala) rewrote every inferred `@retains[T]` to `@retains[Nothing]`. 2. `Setup.mapInferred` in [Setup.scala](compiler/src/dotty/tools/dotc/cc/Setup.scala) stripped `@retains` entirely and then `addVar` attached a fresh empty variable. 3. `needsVariable` said a const capture set containing a type-param reference still needed a fresh variable, severing the link to `C` across type-arg substitution. **Fix.** - `CleanupRetains` now tracks enclosing `LambdaType` binders and preserves `@retains[TypeParamRef]` whose binder is in scope (safe for the pickler). Other retains are still normalized to `@retains[Nothing]`, so unrelated tests like `cap-paramlists5.scala` and `nicolas1.scala` keep compiling. - `Setup.mapInferred.innerApply` has a new case that preserves a retains whose elements derive from `CapSet`, and the `TypeLambda` case keeps CapSet-derived retains on parameter bounds so `[C^]`'s `CapSet^{any}` upper bound survives. - `Setup.needsVariable` refuses to wrap a const set whose elements reference a capture-set type parameter. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 25 +++++++++++++- compiler/src/dotty/tools/dotc/cc/Setup.scala | 33 +++++++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index f6ad067168a1..48c3f84c408c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -900,15 +900,38 @@ class PathSelectionProto(val selector: Symbol, val pt: Type, val tree: Tree) ext /** 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). + * argument if CC is enabled (we need to do that to keep by-name status). + * Retains are preserved as-is when they reference a named capture-set type parameter + * symbol (e.g. `T^{C}` where `C: CapSet^`): those references are load-bearing for + * capture-polymorphic lambdas and cannot be recovered after being rewritten to + * `Nothing`. Retains whose refs are anonymous TypeParamRefs — e.g. the bound of an + * un-named polymorphic lambda type parameter — are still cleaned up, since keeping + * them would leave orphan parameter references in the annotation tree when pickled. + * See i25830. */ class CleanupRetains(using Context) extends TypeMap: + + // LambdaType binders we are currently inside of. Any TypeParamRef bound by one + // of these is still valid when this type is serialized (its binder will be on + // the pickler stack). TypeParamRefs bound by an outer binder would be orphan. + private var inScope: List[LambdaType] = Nil + + private def isPreservableCapSetRef(tp: Type): Boolean = tp match + case tp: TypeParamRef => tp.derivesFromCapSet && inScope.contains(tp.binder) + case _ => false + 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 if annot.retainedType.retainedElementsRaw.exists(isPreservableCapSetRef) then + tp.derivedAnnotatedType(this(parent), annot) else AnnotatedType(this(parent), RetainingAnnotation(annot.symbol.asClass, defn.NothingType)) else this(parent) + case tp: LambdaType => + val saved = inScope + inScope = tp :: inScope + try mapOver(tp) finally inScope = 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..1d87a5572fbd 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -338,14 +338,39 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def innerApply(tp: Type) = val tp1 = tp match + case AnnotatedType(parent, annot: RetainingAnnotation) + if annot.retainedType.retainedElementsRaw.exists(_.derivesFromCapSet) => + // Preserve retains annotations that reference capture-set type parameters + // (e.g. `File^{C}` where `C: CapSet^`). Process the parent but strip the + // fresh empty variable that `addVar` would attach, then re-attach the + // original retains set so the reference to `C` survives later type-arg + // substitution. Without this, `{C}` would be silently erased to `{}`. + // See i25830. + val parent1 = apply(parent) match + case CapturingType(p, refs) if refs.elems.isEmpty && !refs.isConst => p + case other => other + // `toCaptureSet` can throw when an element isn't a well-formed capability + // — mirrors `decomposeCapturingType` in CapturingType.scala. Fall back to + // the plain parent in that case. + try CapturingType(parent1, annot.toCaptureSet) + catch case _: IllegalCaptureRef => parent1 case AnnotatedType(parent, annot) if annot.symbol.isRetains || annot.symbol == defn.InferredAnnot => // Drop explicit retains and @inferred annotations apply(parent) case tp: TypeLambda => - // Don't recurse into parameter bounds, just cleanup any stray retains annotations + // Don't recurse into parameter bounds, just cleanup any stray retains + // annotations. Retains on CapSet-derived parts are meaningful (they encode + // the `CapSet^{any}` upper bound of a capture-set type parameter `[C^]`) + // and must be preserved so that `{C}` is a valid reference in the body. + // See i25830. + def cleanBound(bnd: Type): Type = bnd match + case bnd: TypeBounds => + bnd.derivedTypeBounds(cleanBound(bnd.lo), cleanBound(bnd.hi)) + case _ => + if bnd.derivesFromCapSet then bnd else bnd.dropAllRetains tp.derivedLambdaType( - paramInfos = tp.paramInfos.mapConserve(_.dropAllRetains.bounds), + paramInfos = tp.paramInfos.mapConserve(cleanBound(_).bounds), resType = this(tp.resType)) case tp @ RefinedType(parent, rname, rinfo) => val saved = refiningNames @@ -899,6 +924,10 @@ 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) + // Don't wrap a const set that references a capture-set type parameter in a + // fresh variable: the resulting Var's elements would no longer track the + // original ParamRef across type-argument substitution. See i25830. case AnnotatedType(parent, _) => needsVariable(parent) case _ => From ea79100a7367f308fdd303ada1c7d60568a406da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Thu, 16 Apr 2026 20:38:42 +0200 Subject: [PATCH 2/6] Cleanup --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 48c3f84c408c..0b8c89e92083 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -911,27 +911,30 @@ class PathSelectionProto(val selector: Symbol, val pt: Type, val tree: Tree) ext */ class CleanupRetains(using Context) extends TypeMap: - // LambdaType binders we are currently inside of. Any TypeParamRef bound by one - // of these is still valid when this type is serialized (its binder will be on - // the pickler stack). TypeParamRefs bound by an outer binder would be orphan. - private var inScope: List[LambdaType] = Nil - - private def isPreservableCapSetRef(tp: Type): Boolean = tp match - case tp: TypeParamRef => tp.derivesFromCapSet && inScope.contains(tp.binder) + // Enclosing LambdaType binders; a TypeParamRef bound by one of these is + // self-contained in this type and safe to pickle. Refs to outer binders + // would be orphan, so we still erase their retains to `Nothing`. + private var binders: List[LambdaType] = Nil + + // A retained element is preserved iff it is a TypeParamRef, it derives + // from CapSet, and its binder is enclosed by this traversal. + private def isLocalCapSetParam(tp: Type): Boolean = tp match + case ref: TypeParamRef => + ref.derivesFromCapSet && binders.contains(ref.binder) case _ => false 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 if annot.retainedType.retainedElementsRaw.exists(isPreservableCapSetRef) then + else if annot.retainedType.retainedElementsRaw.exists(isLocalCapSetParam) then tp.derivedAnnotatedType(this(parent), annot) else AnnotatedType(this(parent), RetainingAnnotation(annot.symbol.asClass, defn.NothingType)) else this(parent) case tp: LambdaType => - val saved = inScope - inScope = tp :: inScope - try mapOver(tp) finally inScope = saved + val saved = binders + binders = tp :: binders + try mapOver(tp) finally binders = saved case _ => mapOver(tp) /** A base class for extractors that match annotated types with a specific From 5283f495d9d451bb0bf932dfc8cc2cbf21d713ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Fri, 17 Apr 2026 13:27:17 +0200 Subject: [PATCH 3/6] Keep {C} in inferred tpt of curried poly lambdas --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 0b8c89e92083..a26b09f11972 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -916,11 +916,21 @@ class CleanupRetains(using Context) extends TypeMap: // would be orphan, so we still erase their retains to `Nothing`. private var binders: List[LambdaType] = Nil - // A retained element is preserved iff it is a TypeParamRef, it derives - // from CapSet, and its binder is enclosed by this traversal. + // A retained element is preserved when it references a capture-set type + // parameter that is still in scope for the type we are cleaning — either: + // - A TypeParamRef whose binder lambda is inside the current traversal; or + // - A TypeRef whose symbol is a type parameter of an enclosing anonymous + // function (e.g. the outer `[C^]` of a curried poly lambda value, when + // processing the tpt of the nested inner closure). + // Refs to type params of named enclosing methods are deliberately NOT + // preserved here — they match the pre-existing CleanupRetains behavior + // (otherwise existing neg/pos tests change semantics). private def isLocalCapSetParam(tp: Type): Boolean = tp match case ref: TypeParamRef => ref.derivesFromCapSet && binders.contains(ref.binder) + case ref: TypeRef if ref.derivesFromCapSet => + val sym = ref.symbol + sym.isType && sym.owner.isAnonymousFunction case _ => false def apply(tp: Type): Type = tp match From 16da1c20ec077ad4116d1796c5d7b1b6c9c187bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Fri, 17 Apr 2026 14:13:46 +0200 Subject: [PATCH 4/6] Also preserve outer-capset refs in poly lambda tpt --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 19 +++--- .../captures/i25830-mixed-scopes.scala | 65 +++++++++++++++++++ tests/pos-custom-args/captures/i25830.scala | 10 +++ 3 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 tests/pos-custom-args/captures/i25830-mixed-scopes.scala create mode 100644 tests/pos-custom-args/captures/i25830.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index a26b09f11972..e96a1def9ce7 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -917,20 +917,23 @@ class CleanupRetains(using Context) extends TypeMap: private var binders: List[LambdaType] = Nil // A retained element is preserved when it references a capture-set type - // parameter that is still in scope for the type we are cleaning — either: + // parameter that is meaningful for the tpe we are cleaning: // - A TypeParamRef whose binder lambda is inside the current traversal; or - // - A TypeRef whose symbol is a type parameter of an enclosing anonymous - // function (e.g. the outer `[C^]` of a curried poly lambda value, when - // processing the tpt of the nested inner closure). - // Refs to type params of named enclosing methods are deliberately NOT - // preserved here — they match the pre-existing CleanupRetains behavior - // (otherwise existing neg/pos tests change semantics). + // - A TypeRef to a cap-set type parameter symbol, when either: + // (i) we are currently inside a LambdaType in this tpe (so the tpe + // itself is a polymorphic structure — preserve is load-bearing); + // (ii) its owner is an anonymous function (the curried poly lambda + // value case: inner closure tpt has no own LambdaType but the + // outer $anonfun's C is the referenced symbol). + // Refs not fitting either condition (e.g. a free ref to a named method's + // type param in an inferred TypeApply arg — see nicolas1.scala, + // cap-paramlists5.scala) stay erased to `Nothing`. private def isLocalCapSetParam(tp: Type): Boolean = tp match case ref: TypeParamRef => ref.derivesFromCapSet && binders.contains(ref.binder) case ref: TypeRef if ref.derivesFromCapSet => val sym = ref.symbol - sym.isType && sym.owner.isAnonymousFunction + sym.isType && (binders.nonEmpty || sym.owner.isAnonymousFunction) case _ => false def apply(tp: Type): Type = tp match 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..462e15e9c83f --- /dev/null +++ b/tests/pos-custom-args/captures/i25830-mixed-scopes.scala @@ -0,0 +1,65 @@ +import language.experimental.captureChecking +import caps.* + +class File extends SharedCapability + +// (1) val-form of use-capset.scala's TODO line — the def-form still errors +// due to the `@use`/classifier issue, but this val-form works. +def useCapsetVal() = + val g = { [C^] => (xs: List[File^{C}]) => xs.head } + val io = File() + val _ : File^{io} = g[{io}](List[File^{io}](io)) + +// (2) Curried poly lambda inside a def whose own type params are cap-set. +// Local lambda doesn't reference the outer cap-set. +def insideDef[OuterC^](a: File^{OuterC}) = + val g = { [C^] => (x: File^{C}) => x } + val b = File() + val _ : File^{b} = g[{b}](b) + +// (3) Curried poly lambda inside a def whose type params are cap-set. +def insideDefCurried[OuterC^](a: File^{OuterC}) = + val g = { [C^] => (x: File^{C}) => (y: File^{C}) => x } + val b = File() + val _ : File^{b} = g[{b}](b)(b) + +// (4) Local lambda's type references the outer cap-set param +// (non-curried, so no closure-capture classifier issue). +def outerReferenced[OuterC^](a: File^{OuterC}) = + val mk = { [C^] => (x: File^{C}, y: File^{OuterC}) => x } + val b = File() + val _ : File^{b} = mk[{b}](b, a) + +// (5) Def with an inferred return type that is itself a poly lambda +// referencing the outer cap-set. +def mkLambdaInferred[OuterC^](a: File^{OuterC}) = + [C^] => (x: File^{C}, y: File^{OuterC}) => x + +def useMk() = + val a = File(); val b = File() + val mk = mkLambdaInferred[{a}](a) + val _ : File^{b} = mk[{b}](b, a) + +// (6) Inside a class with a cap-set type parameter. +class Holder[OuterC^](val outer: File^{OuterC}): + val convert = { [C^] => (xs: List[File^{C}]) => xs.map(_ => ()) } + def curried = { [C^] => (a: File^{C}) => (b: File^{C}) => a } + +def useHolder() = + val b = File() + val h = Holder[{b}](b) + val _ = h.convert[{b}](List[File^{b}](b)) + val _ : File^{b} = h.curried[{b}](b)(b) + +// (7) Inside a trait with a cap-set type parameter. +trait Ops[OuterC^]: + val mk = { [C^] => (xs: List[File^{C}]) => xs } + +class OpsImpl[X^] extends Ops[X]: + val specific = { [C^] => (a: File^{C}) => a } + +def useOps() = + val b = File() + val ops = OpsImpl[{b}]() + val _ = ops.mk[{b}](List[File^{b}](b)) + val _ : File^{b} = ops.specific[{b}](b) 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) From 025913ae457a1a2f192f2419ac432d8c9de39e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Fri, 17 Apr 2026 14:46:36 +0200 Subject: [PATCH 5/6] Update test file --- .../captures/i25830-mixed-scopes.scala | 103 +++++++++++------- 1 file changed, 62 insertions(+), 41 deletions(-) diff --git a/tests/pos-custom-args/captures/i25830-mixed-scopes.scala b/tests/pos-custom-args/captures/i25830-mixed-scopes.scala index 462e15e9c83f..ebf07ec8af7c 100644 --- a/tests/pos-custom-args/captures/i25830-mixed-scopes.scala +++ b/tests/pos-custom-args/captures/i25830-mixed-scopes.scala @@ -3,63 +3,84 @@ import caps.* class File extends SharedCapability -// (1) val-form of use-capset.scala's TODO line — the def-form still errors -// due to the `@use`/classifier issue, but this val-form works. +// 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) Curried poly lambda inside a def whose own type params are cap-set. -// Local lambda doesn't reference the outer cap-set. -def insideDef[OuterC^](a: File^{OuterC}) = - val g = { [C^] => (x: File^{C}) => x } - val b = File() - val _ : File^{b} = g[{b}](b) - -// (3) Curried poly lambda inside a def whose type params are cap-set. -def insideDefCurried[OuterC^](a: File^{OuterC}) = - val g = { [C^] => (x: File^{C}) => (y: File^{C}) => x } - val b = File() - val _ : File^{b} = g[{b}](b)(b) - -// (4) Local lambda's type references the outer cap-set param -// (non-curried, so no closure-capture classifier issue). -def outerReferenced[OuterC^](a: File^{OuterC}) = +// (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) -// (5) Def with an inferred return type that is itself a poly lambda -// referencing the outer cap-set. -def mkLambdaInferred[OuterC^](a: File^{OuterC}) = - [C^] => (x: File^{C}, y: File^{OuterC}) => x +// (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) -def useMk() = - val a = File(); val b = File() - val mk = mkLambdaInferred[{a}](a) - val _ : File^{b} = mk[{b}](b, 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) -// (6) Inside a class with a cap-set type parameter. +// (5) Class field: lambda's interleaved signature mentions the class's +// cap-set parameter. class Holder[OuterC^](val outer: File^{OuterC}): - val convert = { [C^] => (xs: List[File^{C}]) => xs.map(_ => ()) } - def curried = { [C^] => (a: File^{C}) => (b: File^{C}) => a } + val mk = { [T, C^, U] => + (t: T, x: File^{C}, u: U, y: File^{OuterC}) => x + } def useHolder() = - val b = File() - val h = Holder[{b}](b) - val _ = h.convert[{b}](List[File^{b}](b)) - val _ : File^{b} = h.curried[{b}](b)(b) + val a = File(); val b = File() + val h = Holder[{a}](a) + val _ : File^{b} = h.mk[Int, {b}, String](1, b, "s", a) -// (7) Inside a trait with a cap-set type parameter. +// (6) Trait abstract member + subclass override, both mentioning the +// enclosing cap-set parameter. Exercises the explicit-tpt path. trait Ops[OuterC^]: - val mk = { [C^] => (xs: List[File^{C}]) => xs } + val mk: [T, C^] -> (t: T, x: File^{C}, y: File^{OuterC}) -> File^{C} -class OpsImpl[X^] extends Ops[X]: - val specific = { [C^] => (a: File^{C}) => a } +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 b = File() - val ops = OpsImpl[{b}]() - val _ = ops.mk[{b}](List[File^{b}](b)) - val _ : File^{b} = ops.specific[{b}](b) + 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) From f5a38e9bea68a964c771d62eeaaf805ed3823c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Fri, 17 Apr 2026 15:12:39 +0200 Subject: [PATCH 6/6] Comments --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index e96a1def9ce7..40a7dd77d9e5 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -911,26 +911,21 @@ class PathSelectionProto(val selector: Symbol, val pt: Type, val tree: Tree) ext */ class CleanupRetains(using Context) extends TypeMap: - // Enclosing LambdaType binders; a TypeParamRef bound by one of these is - // self-contained in this type and safe to pickle. Refs to outer binders - // would be orphan, so we still erase their retains to `Nothing`. + // LambdaTypes entered during the traversal of the *current* tpe only — + // outer binders from the surrounding source are not here. private var binders: List[LambdaType] = Nil - // A retained element is preserved when it references a capture-set type - // parameter that is meaningful for the tpe we are cleaning: - // - A TypeParamRef whose binder lambda is inside the current traversal; or - // - A TypeRef to a cap-set type parameter symbol, when either: - // (i) we are currently inside a LambdaType in this tpe (so the tpe - // itself is a polymorphic structure — preserve is load-bearing); - // (ii) its owner is an anonymous function (the curried poly lambda - // value case: inner closure tpt has no own LambdaType but the - // outer $anonfun's C is the referenced symbol). - // Refs not fitting either condition (e.g. a free ref to a named method's - // type param in an inferred TypeApply arg — see nicolas1.scala, - // cap-paramlists5.scala) stay erased to `Nothing`. private def isLocalCapSetParam(tp: Type): Boolean = tp match + // Proper scope check: the ref's binder must sit inside the current tpe. case ref: TypeParamRef => ref.derivesFromCapSet && binders.contains(ref.binder) + // A TypeRef has no .binder, so two heuristics stand in: + // - owner is an anonymous function: the ref points at an enclosing + // $anonfun's C (curried poly-lambda value, inner closure tpt). + // - binders.nonEmpty: the tpe itself has a LambdaType structure, so + // preserving an outer cap-set ref inside it is load-bearing. + // Refs to a named method's type param sitting in a non-poly inferred + // tpe stay erased — see nicolas1.scala, cap-paramlists5.scala. case ref: TypeRef if ref.derivesFromCapSet => val sym = ref.symbol sym.isType && (binders.nonEmpty || sym.owner.isAnonymousFunction)