Skip to content

Commit 7ba98d3

Browse files
committed
Special rule for {this} in capture sets of class members
Consider the lazylists.scala test in pos-custom-args/captures: ```scala class CC type Cap = {*} CC trait LazyList[+A]: this: ({*} LazyList[A]) => def isEmpty: Boolean def head: A def tail: {this} LazyList[A] object LazyNil extends LazyList[Nothing]: def isEmpty: Boolean = true def head = ??? def tail = ??? extension [A](xs: {*} LazyList[A]) def map[B](f: {*} A => B): {xs, f} LazyList[B] = class Mapped extends LazyList[B]: this: ({xs, f} Mapped) => def isEmpty = false def head: B = f(xs.head) def tail: {this} LazyList[B] = xs.tail.map(f) // OK new Mapped ``` Without this commit, the second to last line is an error since the right hand side has capture set `{xs, f}` but the required capture set is `this`. To fix this, we widen the expected type of the rhs `xs.tail.map(f)` from `{this}` to `{this, f, xs}`. That is, we add the declared captures of the self type to the expected type. The soundness argument for doing this is as follows: Since `tail` does not have parameters, the only thing it could capture are references that the receiver `this` captures as well. So `xs` and `f` must come via `this`. For instance, if the receiver `xs` of `xs.tail` happens to be pure, then `xs.tail` is pure as well. On the other hand, in the neg test `lazylists1.scala` we add the following line to `Mapped`: ```scala def concat(other: {f} LazyList[A]): {this} LazyList[A] = ??? : ({xs, f} LazyList[A]) // error ``` Here, we cannot widen the expected type from `{this}` to `{this, xs, f}` since the result of concat refers to `f` independently of `this`, namely through its parameter `other`. Hence, if `ys: {f} LazyList[String]` then ``` LazyNil.concat(ys) ``` still refers to `f` even though `LazyNil` is pure. But if we would accept the definition of `concat` above, the type of `LazyNil.concat(ys)` would be `LazyList[String]`, which is unsound. The current implementation widens the expected type of class members if the class member does not have tracked parameters. We could potentially refine this to say we widen with all references in the expected type that are not subsumed by one of the parameter types.
1 parent 0feb5d7 commit 7ba98d3

File tree

8 files changed

+204
-2
lines changed

8 files changed

+204
-2
lines changed

compiler/src/dotty/tools/dotc/transform/Recheck.scala

+5-2
Original file line numberDiff line numberDiff line change
@@ -196,12 +196,15 @@ abstract class Recheck extends Phase, IdentityDenotTransformer:
196196
bindType
197197

198198
def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Unit =
199-
if !tree.rhs.isEmpty then recheck(tree.rhs, sym.info)
199+
if !tree.rhs.isEmpty then recheckRHS(tree.rhs, sym.info, sym)
200200

201201
def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Unit =
202202
val rhsCtx = linkConstructorParams(sym).withOwner(sym)
203203
if !tree.rhs.isEmpty && !sym.isInlineMethod && !sym.isEffectivelyErased then
204-
inContext(rhsCtx) { recheck(tree.rhs, recheck(tree.tpt)) }
204+
inContext(rhsCtx) { recheckRHS(tree.rhs, recheck(tree.tpt), sym) }
205+
206+
def recheckRHS(tree: Tree, pt: Type, sym: Symbol)(using Context): Type =
207+
recheck(tree, pt)
205208

206209
def recheckTypeDef(tree: TypeDef, sym: Symbol)(using Context): Type =
207210
recheck(tree.rhs)

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

+11
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,17 @@ class CheckCaptures extends Recheck:
322322
interpolateVarsIn(tree.tpt)
323323
curEnv = saved
324324

325+
override def recheckRHS(tree: Tree, pt: Type, sym: Symbol)(using Context): Type =
326+
val pt1 = pt match
327+
case CapturingType(core, refs, _)
328+
if sym.owner.isClass
329+
&& refs.elems.contains(sym.owner.thisType)
330+
&& sym.paramSymss.forall(_.forall(p => p.isType || p.info.captureSet.isAlwaysEmpty)) =>
331+
pt.derivedCapturingType(core, refs ++ sym.owner.asClass.givenSelfType.captureSet)
332+
case _ =>
333+
pt
334+
recheck(tree, pt1)
335+
325336
override def recheckClassDef(tree: TypeDef, impl: Template, cls: ClassSymbol)(using Context): Type =
326337
for param <- cls.paramGetters do
327338
if param.is(Private) && !param.info.captureSet.isAlwaysEmpty then
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists1.scala:25:63 -----------------------------------
2+
25 | def concat(other: {f} LazyList[A]): {this} LazyList[A] = ??? : ({xs, f} LazyList[A]) // error
3+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
4+
| Found: {xs, f} LazyList[A]
5+
| Required: {Mapped.this} LazyList[A]
6+
7+
longer explanation available when compiling with `-explain`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
class CC
2+
type Cap = {*} CC
3+
4+
trait LazyList[+A]:
5+
this: ({*} LazyList[A]) =>
6+
7+
def isEmpty: Boolean
8+
def head: A
9+
def tail: {this} LazyList[A]
10+
11+
object LazyNil extends LazyList[Nothing]:
12+
def isEmpty: Boolean = true
13+
def head = ???
14+
def tail = ???
15+
16+
extension [A](xs: {*} LazyList[A])
17+
def map[B](f: {*} A => B): {xs, f} LazyList[B] =
18+
class Mapped extends LazyList[B]:
19+
this: ({xs, f} Mapped) =>
20+
21+
def isEmpty = false
22+
def head: B = f(xs.head)
23+
def tail: {this} LazyList[B] = xs.tail.map(f) // OK
24+
def drop(n: Int): {this} LazyList[B] = ??? : ({xs, f} LazyList[B]) // OK
25+
def concat(other: {f} LazyList[A]): {this} LazyList[A] = ??? : ({xs, f} LazyList[A]) // error
26+
new Mapped
27+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
-- [E163] Declaration Error: tests/neg-custom-args/captures/lazylists2.scala:50:10 -------------------------------------
2+
50 | def tail: {xs, f} LazyList[B] = xs.tail.map(f) // error
3+
| ^
4+
| error overriding method tail in trait LazyList of type => {Mapped.this} LazyList[B];
5+
| method tail of type => {xs, f} LazyList[B] has incompatible type
6+
7+
longer explanation available when compiling with `-explain`
8+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:18:4 ------------------------------------
9+
18 | class Mapped extends LazyList[B]: // error
10+
| ^
11+
| Found: {f, xs} LazyList[B]
12+
| Required: {f} LazyList[B]
13+
19 | this: ({xs, f} Mapped) =>
14+
20 | def isEmpty = false
15+
21 | def head: B = f(xs.head)
16+
22 | def tail: {this} LazyList[B] = xs.tail.map(f)
17+
23 | new Mapped
18+
19+
longer explanation available when compiling with `-explain`
20+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:27:4 ------------------------------------
21+
27 | class Mapped extends LazyList[B]: // error
22+
| ^
23+
| Found: {f, xs} LazyList[B]
24+
| Required: {xs} LazyList[B]
25+
28 | this: ({xs, f} Mapped) =>
26+
29 | def isEmpty = false
27+
30 | def head: B = f(xs.head)
28+
31 | def tail: {this} LazyList[B] = xs.tail.map(f)
29+
32 | new Mapped
30+
31+
longer explanation available when compiling with `-explain`
32+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:41:48 -----------------------------------
33+
41 | def tail: {this} LazyList[B] = xs.tail.map(f) // error
34+
| ^^^^^^^^^^^^^^
35+
| Found: {f} LazyList[B]
36+
| Required: {xs} LazyList[B]
37+
38+
longer explanation available when compiling with `-explain`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
class CC
2+
type Cap = {*} CC
3+
4+
trait LazyList[+A]:
5+
this: ({*} LazyList[A]) =>
6+
7+
def isEmpty: Boolean
8+
def head: A
9+
def tail: {this} LazyList[A]
10+
11+
object LazyNil extends LazyList[Nothing]:
12+
def isEmpty: Boolean = true
13+
def head = ???
14+
def tail = ???
15+
16+
extension [A](xs: {*} LazyList[A])
17+
def map[B](f: {*} A => B): {f} LazyList[B] =
18+
class Mapped extends LazyList[B]: // error
19+
this: ({xs, f} Mapped) =>
20+
21+
def isEmpty = false
22+
def head: B = f(xs.head)
23+
def tail: {this} LazyList[B] = xs.tail.map(f)
24+
new Mapped
25+
26+
def map2[B](f: {*} A => B): {xs} LazyList[B] =
27+
class Mapped extends LazyList[B]: // error
28+
this: ({xs, f} Mapped) =>
29+
30+
def isEmpty = false
31+
def head: B = f(xs.head)
32+
def tail: {this} LazyList[B] = xs.tail.map(f)
33+
new Mapped
34+
35+
def map3[B](f: {*} A => B): {xs} LazyList[B] =
36+
class Mapped extends LazyList[B]:
37+
this: ({xs} Mapped) =>
38+
39+
def isEmpty = false
40+
def head: B = f(xs.head)
41+
def tail: {this} LazyList[B] = xs.tail.map(f) // error
42+
new Mapped
43+
44+
def map4[B](f: {*} A => B): {xs} LazyList[B] =
45+
class Mapped extends LazyList[B]:
46+
this: ({xs, f} Mapped) =>
47+
48+
def isEmpty = false
49+
def head: B = f(xs.head)
50+
def tail: {xs, f} LazyList[B] = xs.tail.map(f) // error
51+
new Mapped
52+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
class CC
2+
type Cap = {*} CC
3+
4+
//-------------------------------------------------
5+
6+
def test(E: Cap) =
7+
8+
trait LazyList[+A]:
9+
protected def contents: {E} () => (A, {E} LazyList[A])
10+
def isEmpty: Boolean
11+
def head: A = contents()._1
12+
def tail: {E} LazyList[A] = contents()._2
13+
14+
class LazyCons[+A](override val contents: {E} () => (A, {E} LazyList[A]))
15+
extends LazyList[A]:
16+
def isEmpty: Boolean = false
17+
18+
object LazyNil extends LazyList[Nothing]:
19+
def contents: {E} () => (Nothing, LazyList[Nothing]) = ???
20+
def isEmpty: Boolean = true
21+
22+
extension [A](xs: {E} LazyList[A])
23+
def map[B](f: {E} A => B): {E} LazyList[B] =
24+
if xs.isEmpty then LazyNil
25+
else
26+
val cons = () => (f(xs.head), xs.tail.map(f))
27+
LazyCons(cons)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
class CC
2+
type Cap = {*} CC
3+
4+
trait LazyList[+A]:
5+
this: ({*} LazyList[A]) =>
6+
7+
def isEmpty: Boolean
8+
def head: A
9+
def tail: {this} LazyList[A]
10+
11+
object LazyNil extends LazyList[Nothing]:
12+
def isEmpty: Boolean = true
13+
def head = ???
14+
def tail = ???
15+
16+
extension [A](xs: {*} LazyList[A])
17+
def map[B](f: {*} A => B): {xs, f} LazyList[B] =
18+
class Mapped extends LazyList[B]:
19+
this: ({xs, f} Mapped) =>
20+
21+
def isEmpty = false
22+
def head: B = f(xs.head)
23+
def tail: {this} LazyList[B] = xs.tail.map(f) // OK
24+
new Mapped
25+
26+
def test(cap1: Cap, cap2: Cap) =
27+
def f(x: String): String = if cap1 == cap1 then "" else "a"
28+
def g(x: String): String = if cap2 == cap2 then "" else "a"
29+
30+
val xs =
31+
class Initial extends LazyList[String]:
32+
this: ({cap1} Initial) =>
33+
34+
def isEmpty = false
35+
def head = f("")
36+
def tail = LazyNil
37+
new Initial

0 commit comments

Comments
 (0)