scala / scala3

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

Typing error when inline used together with summons and implicits in imported class #16156

Open deusaquilus opened 1 year ago

deusaquilus commented 1 year ago

Compiler version

Error happens in 3.2.0 as well as latest nightly 3.2.2-RC1-bin-20221006-a6a3385-NIGHTLY.

Minimized code

Let's create a context object that has an encoder instance:

import quotes.reflect._

trait MyEncoder[T]:
  def encode: String
class Context:
  inline def summonMyEncoder[T]: String =
    ${ SummonEncoder.impl[T] }
  implicit val encoderInstance: MyEncoder[String] =
    new MyEncoder[String] { def encode = "blah" }
end Context

Then let's make the macro SummonEncoder.impl summon that instance:

import scala.quoted._

object SummonEncoder:
  def impl[T: Type](using Quotes) =
    import quotes.reflect._
    Expr.summon[MyEncoder[T]] match
      case Some(enc) => '{ $enc.encode }
      case None      => report.throwError("can't do it")

Then let's create a 'Repo' object that internally uses the context to summon an encoder for a particular type, ...and then use that summoning function.

class Repo[T]:
  val ctx = new Context
  inline def summonEncoder = { import ctx._ // change to: import ctx.{given, _} for the given example
    summonMyEncoder[T]
  }

object Use:
  val repo = new Repo[String]
  val v = repo.summonEncoder // error happens here!

Output

Long exception while typing Repo.this... error happens:

exception while typing Repo.this of class class dotty.tools.dotc.ast.Trees$This # -1
exception while typing Repo.this.ctx of class class dotty.tools.dotc.ast.Trees$Select # -1
exception while typing Repo.this.ctx.encoderInstance of class class dotty.tools.dotc.ast.Trees$Select # -1
exception while typing Repo.this.ctx.encoderInstance of class class dotty.tools.dotc.ast.Trees$Inlined # -1
exception while typing Repo.this.ctx.encoderInstance.encode of class class dotty.tools.dotc.ast.Trees$Select # -1
exception while typing Repo.this.ctx.encoderInstance.encode of class class dotty.tools.dotc.ast.Trees$Inlined # -1
exception while typing Repo.this.ctx.encoderInstance.encode:String of class class dotty.tools.dotc.ast.Trees$Typed # -1
exception while typing Repo.this.ctx.encoderInstance.encode:String of class class dotty.tools.dotc.ast.Trees$Inlined # -1
exception while typing {
  Repo.this.ctx.encoderInstance.encode:String
} of class class dotty.tools.dotc.ast.Trees$Block # -1
exception while typing {
  Repo.this.ctx.encoderInstance.encode:String
}:String of class class dotty.tools.dotc.ast.Trees$Typed # -1
exception while typing {
  val Repo_this: (org.deusaquilus.Use.repo : (): org.deusaquilus.Repo) = 
    org.deusaquilus.Use.repo
  {
    Repo.this.ctx.encoderInstance.encode:String
  }:String
} of class class dotty.tools.dotc.ast.Trees$Inlined # -1
exception while typing def v: String = 
  {
    val Repo_this: (org.deusaquilus.Use.repo : (): org.deusaquilus.Repo) = 
      org.deusaquilus.Use.repo
    {
      Repo.this.ctx.encoderInstance.encode:String
    }:String
  } of class class dotty.tools.dotc.ast.Trees$DefDef # -1
exception while typing @SourceFile("src/main/scala/org/deusaquilus/Use.scala") final module class Use()
   extends
 Object() {
  private def writeReplace(): AnyRef = 
    new scala.runtime.ModuleSerializationProxy(classOf[org.deusaquilus.Use.type]
      )
  def repo: org.deusaquilus.Repo[String] = new org.deusaquilus.Repo[String]()
  def v: String = 
    {
      val Repo_this: (org.deusaquilus.Use.repo : (): org.deusaquilus.Repo) = 
        org.deusaquilus.Use.repo
      {
        Repo.this.ctx.encoderInstance.encode:String
      }:String
    }
} of class class dotty.tools.dotc.ast.Trees$TypeDef # -1
exception while typing package org.deusaquilus {
  @SourceFile("src/main/scala/org/deusaquilus/Use.scala") class Repo[T]()
     extends
   Object() {
    private type T
    def ctx: org.deusaquilus.Context = new org.deusaquilus.Context()
    private inline def summonEncoder: String = 
      scala.compiletime.package$package.erasedValue[String]
  }
  final lazy module val Use: org.deusaquilus.Use = new org.deusaquilus.Use()
  @SourceFile("src/main/scala/org/deusaquilus/Use.scala") final module class Use
    ()
   extends Object() {
    private def writeReplace(): AnyRef = 
      new scala.runtime.ModuleSerializationProxy(
        classOf[org.deusaquilus.Use.type]
      )
    def repo: org.deusaquilus.Repo[String] = new org.deusaquilus.Repo[String]()
    def v: String = 
      {
        val Repo_this: (org.deusaquilus.Use.repo : (): org.deusaquilus.Repo) = 
          org.deusaquilus.Use.repo
        {
          Repo.this.ctx.encoderInstance.encode:String
        }:String
      }
  }
} of class class dotty.tools.dotc.ast.Trees$PackageDef # -1
[info] exception occurred while compiling /home/alexi/git/encoder-typing-reproduction/src/main/scala/org/deusaquilus/Use.scala
java.lang.AssertionError: assertion failed: asTerm called on not-a-Term val <none> while compiling /home/alexi/git/encoder-typing-reproduction/src/main/scala/org/deusaquilus/Use.scala
[error] ## Exception when compiling 3 sources to /home/alexi/git/encoder-typing-reproduction/target/scala-3.2.0/classes
[error] java.lang.AssertionError: assertion failed: asTerm called on not-a-Term val <none>
[error] scala.runtime.Scala3RunTime$.assertFailed(Scala3RunTime.scala:8)
[error] dotty.tools.dotc.core.Symbols$Symbol.asTerm(Symbols.scala:169)

Expectation

It should work and summon the encoder property.

Github Example:

You can find an example of the codebase here: https://github.com/deusaquilus/encoder-typing-reproduction The exact same problem happens if you change the encoder to a given and change the import to import ctx.{given, _}.

Why this is Important

I would really like to use Scala 3 inline to be able to create DAO repository patterns. That would look something like this:

  // Some arbitrary class
  case class Person(id: Int, name: String, age: Int)

  // Outline of a generic DAO repository
  class Repo[T <: { def id: Int }](val ctx: PostgresJdbcContext[Literal]) {

    inline def getById(inline id: Int): Option[T] = { import ctx._
      run(query[T].filter(t => t.id == lift(id))).headOption
    }
    inline def insert(inline t: T): Int = { import ctx._
      run(query[T].insertValue(lift(t)).returning(_.id))
    }
    inline def searchByField(inline predicate: T => Boolean) = { import ctx._
      run(query[T].filter(p => predicate(p)))
    }
  }

  // Specialize the repo for a particular class
  class PeopleRepo(val ctx: PostgresJdbcContext[Literal]) extends Repo[Person](myContext)
  // Declare and use it:
  val peopleRepo = new PeopleRepo("testPostgresDB")
  val joe = Person(123, "Joe", 123)
  val joeId = peopleRepo.insert(joe)
  val joeNew = peopleRepo.getById(joeId)
  val allJoes = peopleRepo.searchByField(p => p.name == "Joe")

Only I can't do this because the above error will happen in my encoders and decoders.

deusaquilus commented 1 year ago

Also, is it necessary to do import ctx._ for every method or should this also work?

class Repo[T]:
  val ctx = new Context
  import ctx._ // move to here
  inline def summonEncoder = {
    summonMyEncoder[T]
  }

object Use:
  val repo = new Repo[String]
  val v = repo.summonEncoder

// [error] -- Error: /home/git/encoder-typing-reproduction/src/main/scala/org/deusaquilus/Use.scala:12:15 
// [error] 12 |  val v = repo.summonEncoder
// [error]    |          ^^^^^^^^^^^^^^^^^^
// [error]    |          can't do it

When I do this, no encoder is found and the report.throwError("can't do it") happens.

deusaquilus commented 1 year ago

One other thing, when I move the context variable to be passed in via a parameter it also throws an error.

class Repo[T]:
  inline def summonEncoder(ctx: Context) = { // Pass it in as a parameter
    import ctx._                             // ... then import it!
    summonMyEncoder[T]
  }

object Use:
  val repo = new Repo[String]
  val v = repo.summonEncoder(new Context)

Then the following error happens:

java.lang.IllegalArgumentException: Could not find proxy for ctx: org.deusaquilus.Context in [parameter ctx, method summonEncoder, class Repo, package org.deusaquilus, package org, package <root>], encl = package org.deusaquilus, owners = package org.deusaquilus, package org, package <root>; enclosures = package org.deusaquilus, package org, package <root> while compiling /home/alexi/git/encoder-typing-reproduction/src/main/scala/org/deusaquilus/Use.scala
[error] ## Exception when compiling 3 sources to /home/git/encoder-typing-reproduction/target/scala-3.2.0/classes
[error] java.lang.IllegalArgumentException: Could not find proxy for ctx: org.deusaquilus.Context in [parameter ctx, method summonEncoder, class Repo, package org.deusaquilus, package org, package <root>], encl = package org.deusaquilus, owners = package org.deusaquilus, package org, package <root>; enclosures = package org.deusaquilus, package org, package <root>
[error] dotty.tools.dotc.transform.LambdaLift$Lifter.searchIn$1(LambdaLift.scala:135)
[error] dotty.tools.dotc.transform.LambdaLift$Lifter.proxy(LambdaLift.scala:148)
[error] dotty.tools.dotc.transform.LambdaLift$Lifter.proxyRef(LambdaLift.scala:166)
[error] dotty.tools.dotc.transform.LambdaLift$Lifter.addFreeArgs$$anonfun$1(LambdaLift.scala:172)
odersky commented 1 year ago

When inlining the repo.summonEncoder(new Context) context we first expand to

{
  import Repo_this.ctx.*
  Repo_this.ctx.summonMyEncoder[String]
}

But when we expand summonMyEncoder[String] we get this:

  {
    import Repo_this.ctx.*
    Repo.this.ctx.encoderInstance.encode
  }

It looks to me that the implicit search is made in the context of class Repo, so we get a prefix Repo.this. It should have been the proxy Repo_this instead.

odersky commented 1 year ago

I noted the same problem happens when the inline methods are transparent.