Skip to content

Commit b2ba6dc

Browse files
authored
Fix i19315: avoid calling addOuterRefs when the actual type is box-adapted (#19323)
Fixes #19315. The unsound code: ```scala import language.experimental.captureChecking trait Logger case class Boxed[T](unbox: T) // a horrible function, but it typechecks def magic(l: Logger^): Logger = class Foo: def foo: Boxed[Logger^{this}] = // for the following line to typecheck // the capture checker assumes {l} <: {this} Boxed[Logger^{this}](l) val x = new Foo val y = x.foo.unbox // y: Logger^{x} val z: Logger = y // now the capability becomes pure z ``` The `magic` function typechecks before this fix. It casts a capability to a pure value, which is clearly unsound. The crux of the problem is `Boxed[Logger^{this}](l)`, which relies on the subcapturing relation `{l} <: {this}` enabled by the trick implemented `addOuterRefs`. `addOuterRefs` augment a capture set that contains a self reference (like `{this}`) with all outer references reachable from `this`. In this case, the augmented set is `{this, l}`. The reasoning is that, any captured outer references in the class can be rewritten into a field, thus being a subcapture of `{this}`: ``` class Foo: val this_l: Logger^{l} = l def foo: Boxed[Logger^{this}] = Boxed(this.this_l) ``` But the problem with this unsound example is that, the reference to `l` is boxed, so `l` is not captured by the class. Therefore, the above rewriting mismatches with the actual situation. This PR proposes a simple fix, which simply disable `addOuterRefs` when box adaptation happens. This is of course imprecise, but all tests pass with this fix.
2 parents 16b26ad + a40fb19 commit b2ba6dc

File tree

2 files changed

+20
-1
lines changed

2 files changed

+20
-1
lines changed

compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -847,9 +847,13 @@ class CheckCaptures extends Recheck, SymTransformer:
847847
* where local capture roots are instantiated to root variables.
848848
*/
849849
override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda)(using Context): Type =
850-
val expected1 = alignDependentFunction(addOuterRefs(expected, actual), actual.stripCapturing)
850+
var expected1 = alignDependentFunction(expected, actual.stripCapturing)
851851
val actualBoxed = adaptBoxed(actual, expected1, tree.srcPos)
852852
//println(i"check conforms $actualBoxed <<< $expected1")
853+
854+
if actualBoxed eq actual then
855+
// Only `addOuterRefs` when there is no box adaptation
856+
expected1 = addOuterRefs(expected1, actual)
853857
if isCompatible(actualBoxed, expected1) then
854858
if debugSuccesses then tree match
855859
case Ident(_) =>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import language.experimental.captureChecking
2+
trait Logger
3+
case class Boxed[T](unbox: T)
4+
5+
// a horrible function, but it typechecks
6+
def magic(l: Logger^): Logger =
7+
class Foo:
8+
def foo: Boxed[Logger^{this}] =
9+
// for the following line to typecheck
10+
// the capture checker assumes {l} <: {this}
11+
Boxed[Logger^{this}](l) // error
12+
val x = new Foo
13+
val y = x.foo.unbox // y: Logger^{x}
14+
val z: Logger = y // now the capability becomes pure
15+
z

0 commit comments

Comments
 (0)