Skip to content

[compiler crash | "Recursion limit exceeded"][regression] caused by self-types + with in generic method signature + quasi-covariant wildcards (e.g. (that: PThis with HasThisType[PThis])) #5877

Closed
@ibaklan

Description

@ibaklan

When testing HasThisType trait defined in following from (PThis parameter itself is invariant, but recursive constraint is deliberately covariant)

    trait HasThisType[PThis <: HasThisType[_ <: PThis]] {
      this: PThis =>
      type This = PThis

    }

During verification whether "Self-type" constraint is visible or not in particular contexts (especially from outside of that trait) some problems with HasThisType[_] and generic methods signatures was revealed.

Part1
In particular, snippet (complete code here)

      def testSelf[PThis <: HasThisType[_ <: PThis]](that: PThis with HasThisType[PThis]): Unit = {
        // some asserts here
      }
      val that: HasThisType[_] = ???
      // this line of code makes Dotty compiler infinite recursion (stopped only by overflow)
      testSelf(that)

makes Dotty compiler crash
assertion failure for Main.HasThisType[_ <: LazyRef(Main.HasThisType[_]#PThis)] <:< Main.HasThisType ...
After rephrasing generic method signature to some alternative equivalents

// rewrite#1
def testSelf[PThis <: HasThisType[_ <: PThis], That <: PThis with HasThisType[PThis]](that: That): Unit = ???

// rewrite#2
type SelfForHasThisType[PThis <: HasThisType[_ <: PThis]] = PThis with HasThisType[PThis]
def testSelf[PThis <: HasThisType[_ <: PThis]](that: SelfForHasThisType[PThis]): Unit = ???

compiler crash could be reduced to regular compilation error.
While for "rewrite#2" compiler provides some error message of quite reasonable size (couple of lines)
for "rewrite#1" it shows "significantly" multiline error message like this:

42 |      testSelf(that)
   |      ^
   |Recursion limit exceeded.
   |Maybe there is an illegal cyclic reference?
   |If that's not the case, you could also try to increase the stacksize using the -Xss JVM option.
   |A recurring operation is (inner to outer):
   |
   |  subtype Main.HasThisType[_ <: LazyRef(Main.HasThisType[_ <: LazyRef(PThis)]#PThis)] <:< Nothing

  ... - 20 more similar lines here

   |  subtype Main.HasThisType[_ <: LazyRef(PThis)] <:< Main.HasThisType[PThis]

Also, essentially, that problem with tricky signature reproduces only with argument of wildcard (HasThisType[_]) type.
For normal (not wildcard) types matching that signature everything works well
(complete code here)

      def testSelf[PThis <: HasThisType[_ <: PThis]](that: PThis with HasThisType[PThis]): Unit = {
        // some asserts here
      }
      // this lines works as expected
      val that: Foo = Foo()
      testSelf(that)

Other snippet shows that problem with aforementioned signature could also happens not only when signature doesn't match passed argument, but in potentially positive case (but when some wildcard-ed type need to be passed/inferred through 'PThis')
Considering following example (complete code here)

      def testSelf[PThis <: HasThisType[_ <: PThis]](that: PThis with HasThisType[PThis]): Unit = {
        // some asserts here
      }
      val that3: Bar = Bar()
      // this line of code makes Dotty compiler runtime crash - comment it to make it compilable again
      testSelf(that3)

// ...

    // `HasThisType` instantiation/sub-classing
    trait FooLike[PThis <: FooLike[_ <: PThis]] extends HasThisType[PThis] {
      this: PThis =>
    }
    case class Foo(payload: Any = "dummy") extends FooLike[Foo]
    case class Bar(dummy: Any = "payload") extends FooLike[FooLike[_]]

One may expect no error there, while in fact it end up with similar compile runtime crash.
However in this case one may resolve problem "manually" by explicitly hinting PThis parameter (instead of expecting it's correct inference).
So that following fixed (explicit) snippet (complete code here)

        val that3: Bar = Bar()
        // provide `PThis` generic method parameter explicitly
        testSelf[PThis=FooLike[_ <: FooLike[_]]](that3)

will become compilable under Dotty compiler


Eventually, in case of invariant PThis parameter definition, which may looks like

    trait HasThisType[PThis <: HasThisType[PThis]] {
      this: PThis =>
      type This = PThis

    }

Runtime crash is not reproducible, however unreasonably long compilation error messages still appears.
Demonstrating snippet

      def testSelf[PThis <: HasThisType[PThis]](that: PThis with HasThisType[PThis]): Unit = {
        // some asserts here
      }
      val that: HasThisType[_] = ???
      // this line of code makes Dotty compiler uncontrolled recursion which produces unreasonably long error message
      testSelf(that)

Also rewriting signature using intermediate aliasing type as

type SelfForHasThisType[PThis <: HasThisType[PThis]] = PThis with HasThisType[PThis]
def testSelf[PThis <: HasThisType[PThis]](that: SelfForHasThisType[PThis]): Unit = ???

allows to see error message of more reasonable size, and may look like

41 |      testSelf(that)
   |               ^^^^
   |Found:    Main.HasThisType[_](that)
   |Required: SelfForHasThisType[PThis]
   |
   |where:    PThis is a type variable with constraint <: Main.HasThisType[LazyRef(PThis)]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions