milessabin / shapeless

Generic programming for Scala

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`Default.AsRecord` causes `StackOverflow` in a path-dependent type's companion object

justinp opened this issue · comments

I'm not sure if this is expected behavior or not, but it caused a bug deep within my code base that wasn't intuitive to me. The following code throws a StackOverflowError.

import shapeless._

class DefaultsTest {
  final case class A(a: Int = 9)

  object A {
    val d = Default.AsRecord[A].apply()
  }
}

object DefaultsTest {
  def main(args: Array[String]): Unit = {
    val c = new DefaultsTest
    println(c.A.d)
  }
}

It works the way I expect if:

  • d is declared as a def instead of a val
  • A is not path-dependent
  • A.a does not have a default value
  • d is declared outside of A's companion object

I'm using shapeless 2.3.3 on Scala 2.12.12.

@justinp What does the stacktrace look like?

Exception in thread "main" java.lang.StackOverflowError
	at org.scalawag.bateman.jsonapi.generic.DefaultsTest.A$lzycompute$1(DefaultsTest.scala:6)
	at org.scalawag.bateman.jsonapi.generic.DefaultsTest.A(DefaultsTest.scala:6)
	at org.scalawag.bateman.jsonapi.generic.DefaultsTest$A$.<init>(DefaultsTest.scala:7)
	at org.scalawag.bateman.jsonapi.generic.DefaultsTest.A$lzycompute$1(DefaultsTest.scala:6)
	at org.scalawag.bateman.jsonapi.generic.DefaultsTest.A(DefaultsTest.scala:6)
	at org.scalawag.bateman.jsonapi.generic.DefaultsTest$A$.<init>(DefaultsTest.scala:7)
	at org.scalawag.bateman.jsonapi.generic.DefaultsTest.A$lzycompute$1(DefaultsTest.scala:6)
	at org.scalawag.bateman.jsonapi.generic.DefaultsTest.A(DefaultsTest.scala:6)
	at org.scalawag.bateman.jsonapi.generic.DefaultsTest$A$.<init>(DefaultsTest.scala:7)
	at org.scalawag.bateman.jsonapi.generic.DefaultsTest.A$lzycompute$1(DefaultsTest.scala:6)
	at org.scalawag.bateman.jsonapi.generic.DefaultsTest.A(DefaultsTest.scala:6)

...

The AsRecord.apply() is trying to reinitialize object A.

Minimisation with Default[A] instead of Default.AsRecord[A]

The difference between:

  object A {
    val x = 42
    val d = x
  }

and:

  object A {
    val x = 42
    val d = A.x
  }

Note that in general we can't avoid a reference to A:

  object A {
    val x = 42
    object B {
      val x = "shadow"
      val d = A.x
    }
  }