Skip to content

Commit

Permalink
Special rule for {this} in capture sets of class members
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
odersky committed Oct 2, 2021
1 parent 0feb5d7 commit 7ba98d3
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 2 deletions.
7 changes: 5 additions & 2 deletions compiler/src/dotty/tools/dotc/transform/Recheck.scala
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,15 @@ abstract class Recheck extends Phase, IdentityDenotTransformer:
bindType

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

def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Unit =
val rhsCtx = linkConstructorParams(sym).withOwner(sym)
if !tree.rhs.isEmpty && !sym.isInlineMethod && !sym.isEffectivelyErased then
inContext(rhsCtx) { recheck(tree.rhs, recheck(tree.tpt)) }
inContext(rhsCtx) { recheckRHS(tree.rhs, recheck(tree.tpt), sym) }

def recheckRHS(tree: Tree, pt: Type, sym: Symbol)(using Context): Type =
recheck(tree, pt)

def recheckTypeDef(tree: TypeDef, sym: Symbol)(using Context): Type =
recheck(tree.rhs)
Expand Down
11 changes: 11 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,17 @@ class CheckCaptures extends Recheck:
interpolateVarsIn(tree.tpt)
curEnv = saved

override def recheckRHS(tree: Tree, pt: Type, sym: Symbol)(using Context): Type =
val pt1 = pt match
case CapturingType(core, refs, _)
if sym.owner.isClass
&& refs.elems.contains(sym.owner.thisType)
&& sym.paramSymss.forall(_.forall(p => p.isType || p.info.captureSet.isAlwaysEmpty)) =>
pt.derivedCapturingType(core, refs ++ sym.owner.asClass.givenSelfType.captureSet)
case _ =>
pt
recheck(tree, pt1)

override def recheckClassDef(tree: TypeDef, impl: Template, cls: ClassSymbol)(using Context): Type =
for param <- cls.paramGetters do
if param.is(Private) && !param.info.captureSet.isAlwaysEmpty then
Expand Down
7 changes: 7 additions & 0 deletions tests/neg-custom-args/captures/lazylists1.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists1.scala:25:63 -----------------------------------
25 | def concat(other: {f} LazyList[A]): {this} LazyList[A] = ??? : ({xs, f} LazyList[A]) // error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
| Found: {xs, f} LazyList[A]
| Required: {Mapped.this} LazyList[A]

longer explanation available when compiling with `-explain`
27 changes: 27 additions & 0 deletions tests/neg-custom-args/captures/lazylists1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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
def drop(n: Int): {this} LazyList[B] = ??? : ({xs, f} LazyList[B]) // OK
def concat(other: {f} LazyList[A]): {this} LazyList[A] = ??? : ({xs, f} LazyList[A]) // error
new Mapped

38 changes: 38 additions & 0 deletions tests/neg-custom-args/captures/lazylists2.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
-- [E163] Declaration Error: tests/neg-custom-args/captures/lazylists2.scala:50:10 -------------------------------------
50 | def tail: {xs, f} LazyList[B] = xs.tail.map(f) // error
| ^
| error overriding method tail in trait LazyList of type => {Mapped.this} LazyList[B];
| method tail of type => {xs, f} LazyList[B] has incompatible type

longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:18:4 ------------------------------------
18 | class Mapped extends LazyList[B]: // error
| ^
| Found: {f, xs} LazyList[B]
| Required: {f} LazyList[B]
19 | this: ({xs, f} Mapped) =>
20 | def isEmpty = false
21 | def head: B = f(xs.head)
22 | def tail: {this} LazyList[B] = xs.tail.map(f)
23 | new Mapped

longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:27:4 ------------------------------------
27 | class Mapped extends LazyList[B]: // error
| ^
| Found: {f, xs} LazyList[B]
| Required: {xs} LazyList[B]
28 | this: ({xs, f} Mapped) =>
29 | def isEmpty = false
30 | def head: B = f(xs.head)
31 | def tail: {this} LazyList[B] = xs.tail.map(f)
32 | new Mapped

longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:41:48 -----------------------------------
41 | def tail: {this} LazyList[B] = xs.tail.map(f) // error
| ^^^^^^^^^^^^^^
| Found: {f} LazyList[B]
| Required: {xs} LazyList[B]

longer explanation available when compiling with `-explain`
52 changes: 52 additions & 0 deletions tests/neg-custom-args/captures/lazylists2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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): {f} LazyList[B] =
class Mapped extends LazyList[B]: // error
this: ({xs, f} Mapped) =>

def isEmpty = false
def head: B = f(xs.head)
def tail: {this} LazyList[B] = xs.tail.map(f)
new Mapped

def map2[B](f: {*} A => B): {xs} LazyList[B] =
class Mapped extends LazyList[B]: // error
this: ({xs, f} Mapped) =>

def isEmpty = false
def head: B = f(xs.head)
def tail: {this} LazyList[B] = xs.tail.map(f)
new Mapped

def map3[B](f: {*} A => B): {xs} LazyList[B] =
class Mapped extends LazyList[B]:
this: ({xs} Mapped) =>

def isEmpty = false
def head: B = f(xs.head)
def tail: {this} LazyList[B] = xs.tail.map(f) // error
new Mapped

def map4[B](f: {*} A => B): {xs} LazyList[B] =
class Mapped extends LazyList[B]:
this: ({xs, f} Mapped) =>

def isEmpty = false
def head: B = f(xs.head)
def tail: {xs, f} LazyList[B] = xs.tail.map(f) // error
new Mapped

27 changes: 27 additions & 0 deletions tests/pos-custom-args/captures/lazylists-mono.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class CC
type Cap = {*} CC

//-------------------------------------------------

def test(E: Cap) =

trait LazyList[+A]:
protected def contents: {E} () => (A, {E} LazyList[A])
def isEmpty: Boolean
def head: A = contents()._1
def tail: {E} LazyList[A] = contents()._2

class LazyCons[+A](override val contents: {E} () => (A, {E} LazyList[A]))
extends LazyList[A]:
def isEmpty: Boolean = false

object LazyNil extends LazyList[Nothing]:
def contents: {E} () => (Nothing, LazyList[Nothing]) = ???
def isEmpty: Boolean = true

extension [A](xs: {E} LazyList[A])
def map[B](f: {E} A => B): {E} LazyList[B] =
if xs.isEmpty then LazyNil
else
val cons = () => (f(xs.head), xs.tail.map(f))
LazyCons(cons)
37 changes: 37 additions & 0 deletions tests/pos-custom-args/captures/lazylists.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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

def test(cap1: Cap, cap2: Cap) =
def f(x: String): String = if cap1 == cap1 then "" else "a"
def g(x: String): String = if cap2 == cap2 then "" else "a"

val xs =
class Initial extends LazyList[String]:
this: ({cap1} Initial) =>

def isEmpty = false
def head = f("")
def tail = LazyNil
new Initial

0 comments on commit 7ba98d3

Please sign in to comment.