scala / scala3

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

`Expr.summon` does not resolve opaque types #21199

Open joan38 opened 1 month ago

joan38 commented 1 month ago

Compiler version

Issue reproduced in 3.4.2 and 3.5.0-RC4.

Minimized code

Please check the min repro repo https://github.com/joan38/macro-summon-bug

inline def summonValuesInPackage(inline packageName: String): Seq[?] = ${ summonValuesInPackageImpl('packageName) }

private def summonValuesInPackageImpl(packageName: Expr[String])(using Quotes): Expr[Seq[?]] =
  import quotes.reflect.*

  def findAllTypesInPackage(pkg: Symbol): Seq[TypeRepr] =
    pkg.declarations.flatMap:
      case sym if sym.isPackageDef || (sym.isValDef && sym.flags.is(Flags.Module)) =>
        findAllTypesInPackage(sym) // Recurse
      case sym if sym.isTypeDef => Seq(sym.typeRef.dealiasKeepOpaques) // Include
      case _                    => Seq.empty                           // Skip

  // Get the package where we will look for types
  val typesPackage = Symbol.requiredPackage(packageName.valueOrAbort)

  // Find all types in the package and summon them
  val values = findAllTypesInPackage(typesPackage).flatMap: typeRepr =>
    typeRepr.asType match
      case '[t] => Expr.summon[t] // This does not seem to work when `t` is an opaque type

  Expr.ofSeq(values)

Output

Gives only MyCaseClass instances (not sure why 3 times but that's a different issue):

ArraySeq(MyCaseClass(), MyCaseClass(), MyCaseClass())

Expectation

Expr.summon[t] should resolve opaque type too:

ArraySeq(MyCaseClass(), MyCaseClass(), MyCaseClass(), true, true, true)

Thanks

joan38 commented 1 month ago

Sounds like it's the same with summonInline instead of Expr.summon

joan38 commented 1 month ago

Also another thing I experimented with to make it even simplier:

import scala.quoted.*
import types.Hello

inline def summonHello: Hello = ${ summonHelloImpl }

private def summonHelloImpl(using Quotes): Expr[Hello] =
  import quotes.reflect.*

  Symbol.requiredModule("types").typeMember("Hello").typeRef.asType match
    case '[t] => Expr.summon[t].getOrElse(report.errorAndAbort(s"Cannot summon a value of type ${TypeRepr.of[Hello]}")).asExprOf[Hello]

and:

object types:
  opaque type Hello = String
  def Hello(s: String): Hello = s
import types.*

@main def run =
  given Hello = Hello("dummy")
  println(summonHello)

Which fails with:

% ./scala -Xcheck-macros .
Compiling project (Scala 3.4.2, JVM (17))
[error] ./run.scala:8:11
[error] Cannot summon a value of type TypeRef(TermRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class <root>)),module class <empty>)),types),Hello)
[error]   println(summonHello)
[error]           ^^^^^^^^^^^
Error compiling project (Scala 3.4.2, JVM (17))
bishabosha commented 1 month ago

seems pretty clearly broken, thanks for the minimisation

joan38 commented 1 month ago

Thanks. If I change the line:

case '[t] => Expr.summon[t].getOrElse(report.errorAndAbort(s"Cannot summon a value of type ${TypeRepr.of[Hello]}")).asExprOf[Hello]

by

case '[t] => Expr.summon[t].getOrElse(report.errorAndAbort(s"Cannot summon a value of type ${TypeRepr.of[t]}")).asExprOf[Hello]

then it prints:

Cannot summon a value of type TypeRef(ThisType(TypeRef(NoPrefix,module class lang)),class String)

So that means it looks for a String instead of Hello?

joan38 commented 1 month ago

Here another conclusion:

import scala.quoted.*
import types.Hello

inline def summonHello: Hello = ${ summonHelloImpl }

private def summonHelloImpl(using Quotes): Expr[Hello] =
  import quotes.reflect.*

  val tpe = Symbol.requiredPackage("types").typeMember("Hello").typeRef
  val value = Implicits.search(tpe) match
    case iss: ImplicitSearchSuccess => Some(iss.tree.asExprOf[Hello])
    case isf: ImplicitSearchFailure => None

  value.getOrElse(report.errorAndAbort(s"Cannot summon a value of type ${tpe.typeSymbol.fullName} - ${tpe}"))

and:

package types:
  opaque type Hello = String
  object Hello:
    def apply(s: String): Hello = s
    given Hello = Hello("dummy") // Some instance in the companion object
import types.*

@main def run =
  // import Hello.given // Uncomment this line and it works
  println(summonHello)

Fails to resolve the implicit but with an import of the givens from the companion object, it works. So that means it's not able to resolve givens from companion objects of opaque types.

joan38 commented 1 month ago

Also:

  val tpe = Symbol.requiredPackage("types").typeMember("MyCaseClass").typeRef
  println(tpe.typeSymbol.companionModule.exists.toString)

Would return true if there is a case class MyCaseClass declared in the package types. But false for an opaque type. I wonder why?

joan38 commented 2 weeks ago

BTW I'm happy to look into this issue but not sure where to start from. I'm actually surprised it does not work because it sounds like Expr.summon plugs into the compiler's code