Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
deusaquilus opened this issue Apr 22, 2021 · 9 comments
Labels
area:metaprogramming:quotes Issues related to quotes and splices itype:bug

Comments

@deusaquilus
Copy link
Contributor

deusaquilus commented Apr 22, 2021

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 deusaquilus changed the title Referring to self in implicit inline does not work Referring to self in implicit inline break Expr summoning mechanism Apr 22, 2021
@deusaquilus deusaquilus changed the title Referring to self in implicit inline break Expr summoning mechanism Referring to self in implicit inline breaks Expr summoning mechanism Apr 22, 2021
@deusaquilus
Copy link
Contributor Author

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

@deusaquilus deusaquilus changed the title Referring to self in implicit inline breaks Expr summoning mechanism Referring to outer class in implicit inline breaks Expr summoning mechanism Apr 22, 2021
@nicolasstucki
Copy link
Contributor

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

@deusaquilus
Copy link
Contributor Author

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor Author

deusaquilus commented Apr 22, 2021

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
Copy link
Contributor Author

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

@deusaquilus
Copy link
Contributor Author

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
Copy link
Contributor Author

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.

@nicolasstucki nicolasstucki added area:metaprogramming:quotes Issues related to quotes and splices and removed area:metaprogramming labels Jun 2, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:metaprogramming:quotes Issues related to quotes and splices itype:bug
Projects
None yet
Development

No branches or pull requests

2 participants