scala / scala3

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

Cannot find extension method path dependent type #10689

Open nicolasstucki opened 3 years ago

nicolasstucki commented 3 years ago

Minimized example

import scala.quoted._

def test[T: Type](expr: Expr[T])(using Quotes): Expr[T] =
  import MyAPI.given
  val term = expr.toTerm
  term.toExprOf[T]

object MyAPI:
  given (using q: Quotes): AnyRef with
    extension (expr: Expr[Any]) def toTerm: q.reflect.Term =
      q.reflect.Term.of(expr)
    extension [T: Type](term: q.reflect.Term) def toExprOf: Expr[T] =
      term.asExprOf[T]

Output

6 |  term.toExprOf[T]
  |  ^^^^^^^^^^^^^
  |  value toExprOf is not a member of MyAPI.given_AnyRef#q.reflect.Term

Expectation

Should find extension method toTerm and toExprOf.

deusaquilus commented 3 years ago

Thanks @nicolasstucki for the info. I actually tried something like this myself trying add extensions methods on things inside of quotes.reflect like Term and Constant but it never worked.

@odersky It would be immensely useful for macro authors to be able to add extensions to things inside quotes.reflect, but this means that we need to be able to add extension methods to path-dependent types.

To bypass this issue I gathered all of this functionality inside this large file called TastyMatchers.scala that assigns quotes to a local val and then imports from it. That's a really clunky solution because then when I try to do something like this:

object MatchMac:
  inline def apply(inline any: Any): Any = ${ matchMacImpl('any) }
  def matchMacImpl(any: Expr[Any])(using Quotes): Expr[Any] =
    import quotes.reflect._
    val tmc = new TastyMatchersContext
    import tmc._

    val lastPart: Term = Untype(any.asTerm) match
        case Block(parts, lastPart) => lastPart
        case other => report.throwError("Error")

    lastPart.asExpr

I get the following error:

[error] -- [E007] Type Mismatch Error: /Users/aleiof/git/dotty_test/src/main/scala/miniquill/parser/PrintMac.scala:35:36 
[error] 35 |    val lastPart: Term = Untype(any.asTerm) match
[error]    |                                ^^^^^^^^^^
[error]    |    Found:    x$2.reflect.Term
[error]    |    Required: tmc.qctx.reflect.Term

That is because the Untype function in TastyMatchers uses its local reference to Quotes (**)... and it needs to do that since if it doesn't, the following error happens.

(** Additional Details: Untype does several internal things related to ignoring Typed(...) nodes which I typically need to ignore)

[error] -- Error: /Users/aleiof/git/dotty_test/src/main/scala/miniquill/parser/TastyMatchers.scala:84:8 
[error] 84 |    def unapply(term: Term): Option[Term] = term match {
[error]    |        ^
[error]    |non-private method unapply in object Untype refers to private value qctx
[error]    |in its type signature (term: TastyMatchers.this.qctx.reflect.Term): 
[error]    |  Option[TastyMatchers.this.qctx.reflect.Term]

The only current way to bypass this issue is like this:

object MatchMac:
  inline def apply(inline any: Any): Any = ${ matchMacImpl('any) }
  def matchMacImpl(any: Expr[Any])(using Quotes): Expr[Any] =
    import quotes.reflect._
    class Operations(using val qctx: Quotes) extends TastyMatchers:
      import qctx.reflect._
      def apply =
        val lastPart: Term = Untype(any.asTerm) match
            case Block(parts, lastPart) => lastPart
            case other => report.throwError("Error")
        lastPart.asExpr

    new Operations().apply

That way, inside of the Operations class, Scala knows that the two instances of quotes are the same thing.

This is really clunky and impractical in many cases since instance of Term, Constant, and anything else coming from qctx cannot be returned from Operations back into the outer context.

It would be really, really nice to add extension methods on things inside of quotes.reflect directly.