scala / scala3

The Scala 3 compiler, also known as Dotty.
https://dotty.epfl.ch
Apache License 2.0
5.88k stars 1.06k forks source link

Referring to outer class in implicit inline breaks Expr summoning mechanism #12179

Open deusaquilus opened 3 years ago

deusaquilus commented 3 years ago

Compiler version

RC3

Minimized code

Create a simple class that does something which we want an implicit instance of e.g. an Encoder.

class Encoder[T]:
  def apply(element: T): List[String] = List(element.toString)

class EncoderContext { self =>
  def encode[Cls](cls: Cls) = List(cls.toString)
  implicit inline def anyClsEncoder[Cls]: Encoder[Cls] =
    MappedEncoderMaker[Cls](self)
}

Then create the macro MappedEncoderMaker in which we will access this EncoderContext. I decided to also add a print statement to see if the macro is being invoked.

object MappedEncoderMaker:
  inline def apply[T](inline ctx: EncoderContext): Encoder[T] = ${ applyImpl[T]('ctx) }
  def applyImpl[T: Type](ctx: Expr[EncoderContext])(using Quotes): Expr[Encoder[T]] =
    import quotes.reflect._
    println(s"===== Creating Instance for: ${Printer.TypeReprShortCode.show(TypeRepr.of[T])}")
    '{ new Encoder[T] { def encode(m: T) = $ctx.encode[T](m) } }

Creating a summoning macro that will attempt to summon the encoder created by our macro:

object SummonAndEncode {
  inline def apply[Cls <: AnyVal](cls: Cls): Unit = ${ applyImpl[Cls]('cls) }
  def applyImpl[Cls <: AnyVal: Type](cls: Expr[Cls])(using Quotes): Expr[Unit] = {
    import quotes.reflect._
    Expr.summon[Encoder[Cls]] match
      case Some(value) => println(s"ENCODER FOUND")
      case None => println("ENCODER NOT FOUND")
    '{ () }
  }
}

Finally, create a class that will import the context and attempt to summon the Encoder.

case class Wrap(value: String) extends AnyVal

def main(args: Array[String]): Unit = {
  val ctx = new EncoderContext()
  import ctx._
  val w = new Wrap("stuff")
  SummonAndEncode[Wrap](w)
}

Output

The result is that the summoning macro will not be able to find the Encoder. However, right before, I can see that the MappedEncoderMaker is actually working as intended.

[info] compiling 1 Scala source to....
===== Creating Instance for: Wrap
ENCODER NOT FOUND

Expectation

Then encoder for the Wrap object should be successfully summoned.

Note that if you remove the self from the MappedEncoderMaker[Cls](self) call i.e:

class EncoderContext { self =>
  def encode[Cls](cls: Cls) = List(cls.toString)
  implicit inline def anyClsEncoder[Cls]: Encoder[Cls] =
    MappedEncoderMaker[Cls](???) // <- Remove the 'self' reference here.
}

Then the encoder will be summoned:

===== Creating Instance for: Wrap
[info] compiling 1 Scala source to...
ENCODER FOUND

Repo

https://github.com/deusaquilus/anyval_encoder_issue

deusaquilus commented 3 years ago

Btw, I cannot create AnyVal encoders in Quill unless this is solved :anguished:

nicolasstucki commented 3 years ago

@deusaquilus a side question: can't you use opaque type Wrap = String?

deusaquilus commented 3 years ago

I can make an Mapped-Encoder for opaque types but whatever macros I use for that will probably have a similar issue as this one.

nicolasstucki commented 3 years ago

Minimized to

import scala.quoted.*

class Encoder

class EncoderContext:
  implicit inline def anyClsEncoder: Encoder =
    ${ EncoderContext.applyImpl('this) }

object EncoderContext:
  def applyImpl(ctx: Expr[EncoderContext])(using Quotes): Expr[Encoder] =
    '{ $ctx; ??? }

object SummonAndEncode:
  inline def apply(): Unit =
    ${ applyImpl }

  private def applyImpl(using Quotes): Expr[Unit] =
    Expr.summon[Encoder] match
      case Some(value) => println("ENCODER FOUND: " + value.show)
      case None => quotes.reflect.report.error("ENCODER NOT FOUND: " + Type.show[Encoder])
    '{ () }
object Test:
  def main(args: Array[String]): Unit = {
    val ctx = new EncoderContext()
    import ctx._
    summon[Encoder] // ok
    SummonAndEncode() // breaks
  }
nicolasstucki commented 3 years ago

Possible workaround

class EncoderContext { self =>
  def encode[Cls](cls: Cls) = List(cls.toString)
  implicit inline def anyClsEncoder[Cls]: Encoder[Cls] =
-   MappedEncoderMaker[Cls](self)
+   new Encoder[Cls] { def encode(m: Cls) = self.encode[Cls](m) }
}
deusaquilus commented 3 years ago

Unfortunately, that workaround doesn't work for me. In this toy-example, I don't actually need the MappedEncoderMaker to encode Cls. In reality, however, Cls is actually a AnyVal class that MappedEncoderMaker finds a constructor for and then creates. If you are interested, here is a full example of what I need to do (also separated out in it's own repo).

https://github.com/deusaquilus/anyval_encoder_issue/tree/full_example

In reality, MappedEncoderMaker looks like this:

object MappedEncoderMaker:
  inline def apply[Encoder[_], Mapped <: AnyVal](inline ctx: AnyValEncoderContext[Encoder, Mapped]): Encoder[Mapped] = ${ applyImpl[Encoder, Mapped]('ctx) }
  def applyImpl[Encoder[_]: Type, Mapped <: AnyVal: Type](ctx: Expr[AnyValEncoderContext[Encoder, Mapped]])(using qctx: Quotes): Expr[Encoder[Mapped]] =
    import qctx.reflect._
    val tpe = TypeRepr.of[Mapped]
    val firstParam = tpe.typeSymbol.primaryConstructor.paramSymss(0)(0)
    val firstParamField = tpe.typeSymbol.memberField(firstParam.name)
    val firstParamType = tpe.memberType(firstParamField)
    // Try to summon an encoder from the first param type
    firstParamType.asType match
      case '[tt] =>
        Expr.summon[Encoder[tt]] match
          case Some(enc) => 
            val mappedEncoding = '{ MappedEncoding((v:Mapped) => ${ Select('v.asTerm, firstParamField).asExprOf[tt] }) }
            val out = '{ $ctx.makeMappedEncoder[tt]($mappedEncoding, $enc) }
            println(s"========== RETURNING Encoder ${tpe.show} => ${firstParamType.show} Consisting of: ${out.show} =========")
            out
          case None => 
            report.throwError(s"Cannot find a regular encoder for the AnyVal type ${tpe.show} or a mapped-encoder for it's base type: ${firstParamType.show}")

... and it is invoked in the EncoderContext like this:

  implicit inline def anyValEncoder[Cls <: AnyVal]: Encoder[Cls] =
    MappedEncoderMaker[Encoder, Cls](
      new AnyValEncoderContext[Encoder, Cls] {
        override def makeMappedEncoder[Base](mapped: MappedEncoding[Cls, Base], encoder: Encoder[Base]): Encoder[Cls] =
          self.mappedEncoder(mapped, encoder)
      }
    )

So I need it to be able to construct the AnyVal instance.

One additional note: The anyValEncoder needs self.mappedEncoder since in reality (i.e. in the full ProtoQuill), EncoderContext is actually a trait whose mappedEncoder method will be implemented later by a child context that inherits from it.

deusaquilus commented 3 years ago

(Some additions in edits in the comment above just now)

deusaquilus commented 3 years ago

Not sure if it helps but if you make def encode[Cls] then it works. Unfortunately not useful for me as a workaround because I need this method to be virtual.

deusaquilus commented 3 years ago

I think I found a working workaround. If you make it return a function that takes the EncoderContext and pass the encoder context afterward it works. I.e. if you do this:

object MappedEncoderMaker:
  inline def apply[T]: EncoderContext => Encoder[T] = ${ applyImpl[T] }
  def applyImpl[T: Type](using Quotes): Expr[EncoderContext => Encoder[T]] =
    import quotes.reflect._
    println(s"===== Creating Instance for: ${Printer.TypeReprShortCode.show(TypeRepr.of[T])}")
    '{ (ctx: EncoderContext) => new Encoder[T] { def encode(m: T) = ctx.encode[T](m) } }

... and then call it like this:

class EncoderContext { self =>
  def encode[Cls](cls: Cls) = List(cls.toString)
  implicit inline def anyClsEncoder[Cls]: Encoder[Cls] =
    MappedEncoderMaker[Cls].apply(self)
}

Hope this info helps.