VirtusLab / besom

Besom - a Pulumi SDK for Scala. Also, incidentally, a broom made of twigs tied round a stick. Brooms and besoms are used for protection, to ward off evil spirits, and cleansing of ritual spaces.
https://virtuslab.github.io/besom/
Apache License 2.0
114 stars 7 forks source link

Recurrent args hang in compile time or runtime #407

Closed pawelprazak closed 4 months ago

pawelprazak commented 4 months ago

This test replicated a problem detected in JsonSchemaPropsArgs (k8s)

class RecurrentArgsTest extends munit.FunSuite with ValueAssertions:
  case class Recurrent (
    properties: Option[Recurrent]
  )
  object Recurrent:
    given encoder(using besom.types.Context): besom.types.Encoder[Recurrent] =
      besom.internal.Encoder.derived[Recurrent]

  test("encode recurrent type") {
    given Context = DummyContext().unsafeRunSync()
    val e         = summon[Encoder[Recurrent]]

    val (_, encoded) = e.encode(Recurrent(Some(Recurrent(Some(Recurrent(Some(Recurrent(None)))))))).unsafeRunSync()

    val expected =
      Map("value" -> Map("value" -> Map("value" -> Map("value" -> Map("value" -> Map().asValue).asValue).asValue).asValue).asValue).asValue
    assertEqualsValue(encoded, expected)
  }
end RecurrentArgsTest

This results in a stack overflow:

besom.internal.RecurrentArgsTest:
==> X besom.internal.RecurrentArgsTest.encode recurrent type  0.033s java.lang.StackOverflowError: null
    at scala.collection.immutable.List.prependedAll(List.scala:148)
    at scala.collection.immutable.List$.from(List.scala:684)
    at scala.collection.immutable.List$.from(List.scala:681)
    at scala.collection.IterableFactory.apply(Factory.scala:103)
    at scala.collection.IterableFactory.apply$(Factory.scala:103)
    at scala.collection.immutable.List$.apply(List.scala:681)
    at besom.internal.RecurrentArgsTest.labels$lzyINIT17$1(EncoderTest.scala:464)
    at besom.internal.RecurrentArgsTest.besom$internal$RecurrentArgsTest$Recurrent$$$_$labels$17(EncoderTest.scala:477)
    at besom.internal.RecurrentArgsTest$Recurrent$.nameEncoderPairs$lzyINIT17$1(EncoderTest.scala:464)
    at besom.internal.RecurrentArgsTest$Recurrent$.nameEncoderPairs$17(EncoderTest.scala:477)
    at besom.internal.RecurrentArgsTest$Recurrent$.encoder(EncoderTest.scala:464)
    at besom.internal.RecurrentArgsTest$Recurrent$.instances$lzyINIT17$1(EncoderTest.scala:464)
    at besom.internal.RecurrentArgsTest$Recurrent$.instances$17(EncoderTest.scala:477)
    at besom.internal.RecurrentArgsTest$Recurrent$.nameEncoderPairs$lzyINIT17$1(EncoderTest.scala:464)
    at besom.internal.RecurrentArgsTest$Recurrent$.nameEncoderPairs$17(EncoderTest.scala:477)
    at besom.internal.RecurrentArgsTest$Recurrent$.encoder(EncoderTest.scala:464)
    at besom.internal.RecurrentArgsTest$Recurrent$.instances$lzyINIT17$1(EncoderTest.scala:464)
    at besom.internal.RecurrentArgsTest$Recurrent$.instances$17(EncoderTest.scala:477)
    at besom.internal.RecurrentArgsTest$Recurrent$.nameEncoderPairs$lzyINIT17$1(EncoderTest.scala:464)
    at besom.internal.RecurrentArgsTest$Recurrent$.nameEncoderPairs$17(EncoderTest.scala:477)
    at besom.internal.RecurrentArgsTest$Recurrent$.encoder(EncoderTest.scala:464)
    at besom.internal.RecurrentArgsTest$Recurrent$.instances$lzyINIT17$1(EncoderTest.scala:464)
    at besom.internal.RecurrentArgsTest$Recurrent$.instances$17(EncoderTest.scala:477)
    at besom.internal.RecurrentArgsTest$Recurrent$.nameEncoderPairs$lzyINIT17$1(EncoderTest.scala:464)
    at besom.internal.RecurrentArgsTest$Recurrent$.nameEncoderPairs$17(EncoderTest.scala:477)
    at besom.internal.RecurrentArgsTest$Recurrent$.encoder(EncoderTest.scala:464)
    at besom.internal.RecurrentArgsTest$Recurrent$.instances$lzyINIT17$1(EncoderTest.scala:464)
    at besom.internal.RecurrentArgsTest$Recurrent$.instances$17(EncoderTest.scala:477)
    at besom.internal.RecurrentArgsTest$Recurrent$.nameEncoderPairs$lzyINIT17$1(EncoderTest.scala:464)
    ...

Two interesting variants:

  1. adding union causes the test to pass

    case class Recurrent(value: Recurrent | Option[Recurrent])
  2. using derives instead of derived results in a compile time hang.

lbialy commented 4 months ago

fixed by these changes in codecs.scala

 // here: change to by-name parameter
  def encoderSum[A](mirror: Mirror.SumOf[A], nameEncoderPairs: => List[(String, Encoder[?])]): Encoder[A] = ...

  // here: change to by-name parameter
  def encoderProduct[A](nameEncoderPairs: => List[(String, Encoder[?])]): Encoder[A] = ...