Fix inferred capture-polymorphic function literals #25936
Fix inferred capture-polymorphic function literals #25936bracevac wants to merge 5 commits intoscala:mainfrom
Conversation
Originally added in scala#24560 to strip inferred retains from inlined call trees before they reach the pickler. The cc setup pipeline now handles retains correctly without this preemptive strip, and removing it leaves scala3-bootstrapped/testCompilation green.
Fixes scala#25830. For `val f = { [C^] => (xs: List[File^{C}]) => xs }`, the typer now produces a stable inferred type whose `{C}` retains survive PostTyper, so callers like `f[{x}](files)` work as expected. Implementation: * `cc/CaptureOps.scala`: rewrote `CleanupRetains` as a `TypeMap` that preserves a retain element only when it's (a) a capset `TypeParamRef` of an in-scope TypeLambda, (b) a capset `TypeRef` aliasing such a binder (synthetic poly-fn class type members surfaced when curried literals are visited as inner fragments), or (c) `caps.any` inside a capset bound. Anything else is erased to `Nothing` as before. * `transform/PostTyper.scala`: added `IsCapsetPolyFunLiteralTpt` sticky attachment plus an `isCapsetPolyFunLiteralRhs` helper based on `tpd.closureDef`. PostTyper attaches the marker to the inferred tpt of any val/def whose RHS is a capset poly-fn literal. CleanupRetains reads it to enable an implementation-restriction error precisely on user-written literals (not on inferred poly-fn types from method results or aliases). * `cc/Setup.scala`: accept capset `TypeRef` alongside `ParamRef` in `isRetainedParamRef` so curried literals' synthetic class type members flow through Setup correctly. Rejected shapes (with a clear restriction error): * term refs in retains (value-position or capset-bound), * `caps.any` in non-bound positions, * nested capset binders like `[C^] => x => [D^] => y` (which would otherwise crash later in cc's `SubstBindingMap`). Tests: * tests/pos-custom-args/captures/i25830.scala: plain, curried, def-bound, and anonymous-function-context literals. * tests/pos-custom-args/captures/i25830-bounded.scala: bounded capset binders, curried-bounded. * tests/pos-custom-args/captures/i25830-apply-workaround.scala: the class-with-polymorphic-apply workaround for shapes outside the supported form. * tests/neg-custom-args/captures/i25830-unsupported.scala: each rejected shape, with `// error` annotations. * Removed tests/pos-custom-args/captures/cap-paramlists{3,6,7,8}.scala: these were parser-only tests using outer-term refs in capset bounds. Equivalent parser coverage remains in cap-paramlists.scala (def-based) and cap-paramlist8-desugared.scala (explicit ascription).
Recurse `isCapsetPolyFunLiteralRhs` through closure bodies whose
synthesized `$anonfun` is non-polymorphic, so a literal nested inside a
Function1 like `(i: Int) => { [C^] => ... }` is recognized as a literal
RHS. Previously such cases silently erased their retains; now the
implementation restriction fires when retains reference non-binder refs.
Add positive (clean nested literal) and negative (nested literal with
external dep) tests.
| // Capset param of an in-scope TypeLambda. | ||
| ref.derivesFromCapSet && capSetBinders.exists(_ eq ref.binder) | ||
| case ref: TypeRef => | ||
| // Capset typedef aliasing an in-scope binder — for curried literals. |
There was a problem hiding this comment.
I believe this is dead code. paramRefs returns either a list of TermParamRefs or of TypeParamRefs. Neither can equal a TypeRef. I guess the case can simply be dropped.
| else | ||
| val elems = annot.retainedType.retainedElementsRaw | ||
| val keep = elems.nonEmpty && elems.forall(isPreserved(_, parent)) | ||
| // Suppress only the narrow case where the entire retain is a single |
There was a problem hiding this comment.
| // Suppress only the narrow case where the entire retain is a single | |
| // Suppress only the narrow case where the entire retains is a single |
| def innerApply(tp: Type) = | ||
| val tp1 = tp match | ||
| case AnnotatedType(parent, annot: RetainingAnnotation) => | ||
| val elems = annot.retainedType.retainedElementsRaw |
There was a problem hiding this comment.
The treatment here assumes that we have cleaned up the type with CleanupRetains before. But that's not true if the type arose from inlining, since we just dropped CleanupRetains there. We probably need to bring back CleanupRetains for inlined code.
| val parent1 = apply(parent) match | ||
| case CapturingType(p, refs) if refs.elems.isEmpty && !refs.isConst => p | ||
| case other => other | ||
| CapturingType(parent1, CaptureSet(caps*)) |
There was a problem hiding this comment.
This might be too simplistic. There are a lot of subtleties and corner cases whan mapping a retains to a capture set. We should use the same code as in transformExplicitType for this.
| * restriction error: it should fire only on user-written literals, not on | ||
| * inferred poly-fn types that come from method calls or aliases. | ||
| */ | ||
| val IsCapsetPolyFunLiteralTpt: util.Property.StickyKey[Unit] = util.Property.StickyKey() |
There was a problem hiding this comment.
We don't need a StickyAttachment here. It's a local property for PostTyper traversals. In fact, we don't need an attachment at all. A local set maintained by PostTyper is good enough. See last commit.
|
Superseded by #25939 |
Fixes #25830
Problem
For a value
val f = { [C^] => (xs: List[File^{C}]) => xs }, the typercorrectly infers
[C^] => List[File^{C}] => List[File^{C}], but PostTyper'sCleanupRetainserased the{C}retains because they weren't recognized asstable references. After erasure the lambda lost its capture polymorphism and
calls like
f[{x}](files)failed.Approach
CleanupRetainsnow preserves a retain element if it is one of:TypeParamRefof a TypeLambda we're inside (own binder),TypeRefaliasing such a binder (synthetic class type memberssurfaced by curried literals on inner-fragment visits),
caps.anyonly inside a capset bound (the synthesized default upperbound
<: caps.CapSet^{any}for[C^]).Anything else is erased to
Nothingas before. WhenCleanupRetainsisapplied to the inferred tpt of a user-written capture-polymorphic literal,
erasure additionally raises an implementation-restriction error.
Detection of "literal RHS"
PostTyper attaches a sticky
IsCapsetPolyFunLiteralTptmarker to theinferred tpt of a
val/defwhose RHS is a capset poly-fn literal,detected via
tpd.closureDefplus aPolyType-with-capset-binder check onthe synthesized
$anonfun.CleanupRetainsenables the restriction erroronly when this attachment is present, which avoids false positives for
inferred poly-fn types coming from method calls or aliases.
Supported shapes
The restriction is basically
[C^, D^,...]in a curried lambda.[C^, D^,...]block can refer toany, or the boundC, D,...capset variables in that block.Examples:
{ [C^] => xs => xs }{ [C^, D^ <: {C}, E^ >: {C} <: {C, D}] => (xs, ys) => (zs, ws) => () }{ [C^] => xs => ys => body }(curried via Function1)def d = { [C^] => xs => xs }val f: T = { [C^] => xs => xs }(explicit ascription is silent)files.map(file => { [C^] => xs => xs })Rejected shapes (implementation restriction)
[C^] => (xs: List[File^{C, external}])[C^, D^ <: {C, external}]caps.anyin value-position retains[C^] => xs => [D^] => ys(the cc machinery'sSubstBindingMapover Vars containing outer-binder refs would otherwisecrash later during Setup; see comment in CaptureOps)
For these the user can fall back to a class with a polymorphic apply method,
which is shown as a workaround in the new
i25830-apply-workaround.scalatest.
Files changed
cc/CaptureOps.scala— rewroteCleanupRetains(TypeMap withcapSetBinderstracking,isPreservedcheck, restriction reporting).cc/Setup.scala— accept capsetTypeRefalongsideParamRefinisRetainedParamRefso curried literals' synthetic class type membersflow through Setup correctly.
transform/PostTyper.scala— addedIsCapsetPolyFunLiteralTptstickyattachment +
isCapsetPolyFunLiteralRhshelper; sets the attachment onval/def tpts whose RHS is a literal.
tests/pos-custom-args/captures/i25830.scala(plain, curried,def-bound, anonymous-function-context)
tests/pos-custom-args/captures/i25830-bounded.scala(boundedcapset binders, curried-bounded)
tests/pos-custom-args/captures/i25830-apply-workaround.scala(theclass-with-apply workaround)
tests/neg-custom-args/captures/i25830-unsupported.scala(everyrejected shape with
// errorannotations)tests/pos-custom-args/captures/cap-paramlists{3,6,7,8}.scala—these were parser-only tests using outer-term refs in capset bounds and
no longer compile under the new restriction. The same parser coverage
exists in
cap-paramlists.scala(def-based) andcap-paramlist8-desugared.scala(explicit ascription).Known limitations
[C^] => xs => [D^] => ys) are nowrejected with a restriction error rather than crashing in cc — but
ideally the cc machinery would handle them; tracked separately.