Skip to content

Commit b9f2ef0

Browse files
authoredFeb 15, 2024
Ignore orphan parameters inside a retains annotation during Ycheck (#19684)
Fixes #19661. ## Cause of the issue As reported in #19661, the following code triggers an assertation failure during Ycheck: ```scala import language.experimental.captureChecking trait MySet[A]: def collect[B](pf: PartialFunction[A, B]^): MySet[B]^{this, pf} class Test: def f(xs: MySet[Int]) = xs collect { case x => x } def g(xs: MySet[Int]): MySet[Int] = f(xs) ``` The failure happens when checking the tree `f(xs)`, whose type is `MySet[Int]^{this, PartialFunction[Int, Int]}`. The `checkNoOrphans` function is invoked on `this`, whose type turns out to be an orphan parameter reference (`xs`). We first inspect the tree outputed by `typer`: ```scala class Test() extends Object() { def f(xs: MySet[Int]): MySet[Int]^{this, PartialFunction[Int, Int]} = xs.collect[Int]( { def $anonfun(x$1: Int): Int = x$1 match { case x @ _ => x:Int } closure($anonfun:PartialFunction[Int, Int]) } ) def g(xs: MySet[Int]): MySet[Int] = this.f(xs) } ``` The problem roots in the signature of the method `f`: in the capture set of its result type, the `this` reference is dangling. How come? It turns out that the `asSeenFrom` map is not working correctly for the typing of `xs.collect`: ``` (xs.collect : [B](pf: PartialFunction[Int, B]^): MySet[B]^{this, pf}) ``` Instead of replacing `this` with `xs`, `asSeenFrom` keeps `this` untouched. This is what happened: - When mapping `asSeenFrom` on the method type, the `asSeenFrom` map recurses and applies on the annotated type. - When mapping the annotation (`@retains(this, pf)`), the `asSeenFrom` map derives a `TreeTypeMap` from itself and uses it to map the `tree` of the annotation. - During that, the type of `this` is properly mapped to `xs.type` but the tree `this` is never changed (since the `TreeTypeMap` is an identity on the structure of trees). To solve this issue, there are (at least) two possibilities: - Refactor the `TypeMap` machineries on annotations to enable it to properly handle these cases. But it is hard: when mapping the capture annotation, we are at a pre-CC phase, so tools for manipulating capture sets are not available. And it is unnecessary: even if we compute these references properly, it gets discarded during CC. - During Ycheck, ignore orphan parameter references inside a normal `@retains` annotation (as opposed to an optimised `CaptureAnnotation`). This feels like a dangerous fix but these `@retains` annotations, even if they are ill-formed, is already treated as being unreliable in CC and get rechecked. Also, CC turns these concrete annotations into optimised `CaptureAnnotation`s, which are not ignored by Ycheck. ## Fix So this PR implements the second option: - Ignore orphan parameter errors inside a normal `@retains` annotation during Ycheck. - The check for `CaptureAnnotation`s will not be bypassed.
2 parents fc593df + 0f81c2f commit b9f2ef0

File tree

2 files changed

+27
-1
lines changed

2 files changed

+27
-1
lines changed
 

‎compiler/src/dotty/tools/dotc/transform/TreeChecker.scala

+19-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import collection.mutable
2323
import ProtoTypes.*
2424
import staging.StagingLevel
2525
import inlines.Inlines.inInlineMethod
26+
import cc.{isRetainsLike, CaptureAnnotation}
2627

2728
import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions
2829

@@ -162,17 +163,34 @@ object TreeChecker {
162163
*/
163164
def checkNoOrphans(tp0: Type, tree: untpd.Tree = untpd.EmptyTree)(using Context): Type = new TypeMap() {
164165
val definedBinders = new java.util.IdentityHashMap[Type, Any]
166+
private var inRetainingAnnot = false
167+
168+
def insideRetainingAnnot[T](op: => T): T =
169+
val saved = inRetainingAnnot
170+
inRetainingAnnot = true
171+
try op finally inRetainingAnnot = saved
172+
165173
def apply(tp: Type): Type = {
166174
tp match {
167175
case tp: BindingType =>
168176
definedBinders.put(tp, tp)
169177
mapOver(tp)
170178
definedBinders.remove(tp)
171179
case tp: ParamRef =>
172-
assert(definedBinders.get(tp.binder) != null, s"orphan param: ${tp.show}, hash of binder = ${System.identityHashCode(tp.binder)}, tree = ${tree.show}, type = $tp0")
180+
val isValidRef =
181+
definedBinders.get(tp.binder) != null
182+
|| inRetainingAnnot
183+
// Inside a normal @retains annotation, the captured references could be ill-formed. See issue #19661.
184+
// But this is ok since capture checking does not rely on them.
185+
assert(isValidRef, s"orphan param: ${tp.show}, hash of binder = ${System.identityHashCode(tp.binder)}, tree = ${tree.show}, type = $tp0")
173186
case tp: TypeVar =>
174187
assert(tp.isInstantiated, s"Uninstantiated type variable: ${tp.show}, tree = ${tree.show}")
175188
apply(tp.underlying)
189+
case tp @ AnnotatedType(underlying, annot) if annot.symbol.isRetainsLike && !annot.isInstanceOf[CaptureAnnotation] =>
190+
val underlying1 = this(underlying)
191+
val annot1 = insideRetainingAnnot:
192+
annot.mapWith(this)
193+
derivedAnnotatedType(tp, underlying1, annot1)
176194
case _ =>
177195
mapOver(tp)
178196
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import language.experimental.captureChecking
2+
3+
trait MySet[A]:
4+
def collect[B](pf: PartialFunction[A, B]^): MySet[B]^{this, pf}
5+
6+
class Test:
7+
def f(xs: MySet[Int]) = xs collect { case x => x }
8+
def g(xs: MySet[Int]): MySet[Int] = f(xs)

0 commit comments

Comments
 (0)
Please sign in to comment.