Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 6fc8c98

Browse files
committedOct 13, 2021
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. ## Changes: ### Refine rule for this widening We now widen the expected type of the right hand side of a class member as follows: Add all references of the declared type of this that are not subsumed by a capture set of a parameter type. ### Do expected type widening only in final classes Alex found a counter-example why this is required. See map5 in neg-customargs/captures/lazylists2.scala
1 parent 850d1c1 commit 6fc8c98

File tree

10 files changed

+276
-3
lines changed

10 files changed

+276
-3
lines changed
 

Diff for: ‎compiler/src/dotty/tools/dotc/cc/CaptureSet.scala

+8-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,14 @@ sealed abstract class CaptureSet extends Showable:
4949
/** Is this capture set definitely non-empty? */
5050
final def isNotEmpty: Boolean = !elems.isEmpty
5151

52-
/** Cast to variable. @pre: @isConst */
52+
/** Cast to Const. @pre: isConst */
53+
def asConst: Const = this match
54+
case c: Const => c
55+
case v: Var =>
56+
assert(v.isConst)
57+
Const(v.elems)
58+
59+
/** Cast to variable. @pre: !isConst */
5360
def asVar: Var =
5461
assert(!isConst)
5562
asInstanceOf[Var]

Diff for: ‎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)

Diff for: ‎compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala

+16
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,22 @@ 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 && !sym.owner.isExtensibleClass
329+
&& refs.elems.contains(sym.owner.thisType) =>
330+
val paramCaptures =
331+
sym.paramSymss.flatten.foldLeft(CaptureSet.empty) { (cs, p) =>
332+
val pcs = p.info.captureSet
333+
(cs ++ (if pcs.isConst then pcs else CaptureSet.universal)).asConst
334+
}
335+
val declaredCaptures = sym.owner.asClass.givenSelfType.captureSet
336+
pt.derivedCapturingType(core, refs ++ (declaredCaptures -- paramCaptures))
337+
case _ =>
338+
pt
339+
recheck(tree, pt1)
340+
325341
override def recheckClassDef(tree: TypeDef, impl: Template, cls: ClassSymbol)(using Context): Type =
326342
for param <- cls.paramGetters do
327343
if param.is(Private) && !param.info.captureSet.isAlwaysEmpty then

Diff for: ‎tests/neg-custom-args/captures/lazylists1.check

+7
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, xs} LazyList[A]
6+
7+
longer explanation available when compiling with `-explain`

Diff for: ‎tests/neg-custom-args/captures/lazylists1.scala

+27
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+
final 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+

Diff for: ‎tests/neg-custom-args/captures/lazylists2.check

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 | final 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 | final 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`
39+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:59:48 -----------------------------------
40+
59 | def tail: {this} LazyList[B] = xs.tail.map(f) // error
41+
| ^^^^^^^^^^^^^^
42+
| Found: {f} LazyList[B]
43+
| Required: {Mapped.this} LazyList[B]
44+
45+
longer explanation available when compiling with `-explain`

Diff for: ‎tests/neg-custom-args/captures/lazylists2.scala

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
final 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+
final 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+
final 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+
final 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+
53+
def map5[B](f: {*} A => B): LazyList[B] =
54+
class Mapped extends LazyList[B]:
55+
this: ({xs, f} Mapped) =>
56+
57+
def isEmpty = false
58+
def head: B = f(xs.head)
59+
def tail: {this} LazyList[B] = xs.tail.map(f) // error
60+
class Mapped2 extends Mapped:
61+
this: Mapped =>
62+
new Mapped2
63+
64+

Diff for: ‎tests/pos-custom-args/captures/lazylists-mono.scala

+27
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)

Diff for: ‎tests/pos-custom-args/captures/lazylists.scala

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
final 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 concat(other: {f} LazyList[A]): {this, f} LazyList[A] = ??? : ({xs, f} LazyList[A]) // OK
25+
if xs.isEmpty then LazyNil
26+
else new Mapped
27+
28+
def test(cap1: Cap, cap2: Cap) =
29+
def f(x: String): String = if cap1 == cap1 then "" else "a"
30+
def g(x: String): String = if cap2 == cap2 then "" else "a"
31+
32+
val xs =
33+
class Initial extends LazyList[String]:
34+
this: ({cap1} Initial) =>
35+
36+
def isEmpty = false
37+
def head = f("")
38+
def tail = LazyNil
39+
new Initial
40+
val xsc: {cap1} LazyList[String] = xs
41+
val ys = xs.map(g)
42+
val ysc: {cap1, cap2} LazyList[String] = ys

Diff for: ‎tests/pos-custom-args/captures/lazylists1.scala

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
final class LazyCons[+T](val x: T, val xs: {*} () => {*} LazyList[T]) extends LazyList[T]:
17+
this: ({*} LazyList[T]) =>
18+
19+
def isEmpty = false
20+
def head = x
21+
def tail: {this} LazyList[T] = xs()
22+
23+
extension [A](xs: {*} LazyList[A])
24+
def map[B](f: {*} A => B): {xs, f} LazyList[B] =
25+
if xs.isEmpty then LazyNil
26+
else LazyCons(f(xs.head), () => xs.tail.map(f))
27+
28+
def test(cap1: Cap, cap2: Cap) =
29+
def f(x: String): String = if cap1 == cap1 then "" else "a"
30+
def g(x: String): String = if cap2 == cap2 then "" else "a"
31+
32+
val xs = LazyCons("", () => if f("") == f("") then LazyNil else LazyNil)
33+
val xsc: {cap1} LazyList[String] = xs
34+
val ys = xs.map(g)
35+
val ysc: {cap1, cap2} LazyList[String] = ys

0 commit comments

Comments
 (0)
Please sign in to comment.