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

Macro-based automatic derivation results differ based on how you construct a typeclass instance #16835

Open
arainko opened this issue Feb 5, 2023 · 5 comments
Assignees
Labels
area:metaprogramming:reflection Issues related to the quotes reflection API itype:bug

Comments

@arainko
Copy link
Contributor

arainko commented Feb 5, 2023

Compiler version

3.2.1, 3.2.2, 3.3.0-RC2, 3.3.1-RC1-bin-20230204-a356581-NIGHTLY

Minimized code

import scala.quoted.*
import scala.deriving.Mirror

// derivation code is a slightly modified version of: https://github.com/lampepfl/dotty-macro-examples/blob/main/macroTypeClassDerivation/src/macro.scala
object Derivation {

  // Typeclass instance gets constructed as part of a macro
  inline given deriveFullyConstrucedByMacro[A](using Mirror.ProductOf[A]): Show[A] = Derivation.deriveShow[A]

  // Typeclass instance is built inside as part of a method, only the 'show' impl is filled in by a macro
  inline given derivePartiallyConstructedByMacro[A](using Mirror.ProductOf[A]): Show[A] =
    new {
      def show(value: A): String = Derivation.show(value)
    }

  inline def show[T](value: T): String = ${ showValue('value) }

  inline def deriveShow[T]: Show[T] = ${ deriveCaseClassShow[T] }

  private def deriveCaseClassShow[T](using quotes: Quotes, tpe: Type[T]): Expr[Show[T]] = {
    import quotes.reflect.*
    // Getting the case fields of the case class
    val fields: List[Symbol] = TypeTree.of[T].symbol.caseFields

    '{
      new Show[T] {
        override def show(t: T): String =
          ${ showValue('t) }
      }
    }
  }

  def showValue[T: Type](value: Expr[T])(using Quotes): Expr[String] = {
    import quotes.reflect.*

    val fields: List[Symbol] = TypeTree.of[T].symbol.caseFields

    val vTerm: Term = value.asTerm
    val valuesExprs: List[Expr[String]] = fields.map(showField(vTerm, _))
    val exprOfList: Expr[List[String]] = Expr.ofList(valuesExprs)
    '{ "{ " + $exprOfList.mkString(", ") + " }" }
  }

  /** Create a quoted String representation of a given field of the case class */
  private def showField(using Quotes)(caseClassTerm: quotes.reflect.Term, field: quotes.reflect.Symbol): Expr[String] = {
    import quotes.reflect.*

    val fieldValDef: ValDef = field.tree.asInstanceOf[ValDef]
    val fieldTpe: TypeRepr = fieldValDef.tpt.tpe
    val fieldName: String = fieldValDef.name

    val tcl: Term = lookupShowFor(fieldTpe) // Show[$fieldTpe]
    val fieldValue: Term = Select(caseClassTerm, field) // v.field
    val strRepr: Expr[String] = applyShow(tcl, fieldValue).asExprOf[String]
    '{ ${ Expr(fieldName) } + ": " + $strRepr } // summon[Show[$fieldTpe]].show(v.field)
  }

  /** Look up the Show[$t] typeclass for a given type t */
  private def lookupShowFor(using Quotes)(t: quotes.reflect.TypeRepr): quotes.reflect.Term = {
    import quotes.reflect.*
    t.asType match {
      case '[tpe] =>
        Implicits.search(TypeRepr.of[Show[tpe]]) match {
          case res: ImplicitSearchSuccess   => res.tree
          case failure: DivergingImplicit   => report.errorAndAbort(s"Diverving: ${failure.explanation}")
          case failure: NoMatchingImplicits => report.errorAndAbort(s"NoMatching: ${failure.explanation}")
          case failure: AmbiguousImplicits  => report.errorAndAbort(s"Ambiguous: ${failure.explanation}")
          case failure: ImplicitSearchFailure =>
            report.errorAndAbort(s"catch all: ${failure.explanation}")
        }
    }
  }

  /** Composes the tree: $tcl.show($arg) */
  private def applyShow(using Quotes)(tcl: quotes.reflect.Term, arg: quotes.reflect.Term): quotes.reflect.Term = {
    import quotes.reflect.*
    Apply(Select.unique(tcl, "show"), arg :: Nil)
  }
}
import scala.deriving.*

trait Show[A] {
  def show(value: A): String
}

object Show {
  given identity: Show[String] = a => a

  given int: Show[Int] = _.toString()

  given list[A](using A: Show[A]): Show[List[A]] = _.map(A.show).toString()
}

object usage {
  final case class Person(name: String, age: Int, otherNames: List[String], p2: Person2)

  final case class Person2(name: String, age: Int, otherNames: List[String])

  locally {
    import Derivation.deriveFullyConstrucedByMacro
    // works for case classes without other nested case classes inside
    summon[Show[Person2]]

    // also derives instances with nested case classes
    summon[Show[Person]]
  }

  locally {
    import Derivation.derivePartiallyConstructedByMacro

    // works for case classes without other nested case classes inside
    summon[Show[Person2]]

    // fails for case classes with other nested case classes inside,
    // note how that error is not a `NonMatching', `Diverging` or `Ambiguous` implicit search error but something else
    /*
    catch all: given instance deriveWithConstructionOutsideMacro in object Derivation does not match type io.github.arainko.ducktape.issue_repros.Show[Person2]
    */
    summon[Show[Person]]
  }
}

Output

catch all: given instance derivePartiallyConstructedByMacro in object Derivation does not match type io.github.arainko.ducktape.issue_repros.Show[
  io.github.arainko.ducktape.issue_repros.usage.Person2
]

Expectation

These two versions of automatic derivation should be equivalent or a less cryptic error message

Workarounds

If we alter the definition of Derivation.lookupShowFor to include a splice of summonInline[Show[tpe]] instead of aborting, the derivation will succeed in all of the cases:

/** Look up the Show[$t] typeclass for a given type t */
  private def lookupShowFor(using Quotes)(t: quotes.reflect.TypeRepr): quotes.reflect.Term = {
    import quotes.reflect.*
    t.asType match {
      case '[tpe] =>
        Implicits.search(TypeRepr.of[Show[tpe]]) match {
          case res: ImplicitSearchSuccess   => res.tree
          case failure: DivergingImplicit   => report.errorAndAbort(s"Diverving: ${failure.explanation}")
          case failure: NoMatchingImplicits => report.errorAndAbort(s"NoMatching: ${failure.explanation}")
          case failure: AmbiguousImplicits  => report.errorAndAbort(s"Ambiguous: ${failure.explanation}")
          case failure: ImplicitSearchFailure =>
            '{ compiletime.summonInline[Show[tpe]] }.asTerm
        }
    }
  }

But that severely limits what you can do with an AST like that moving forward (you can't really inspect the trees that
get summoned, from my testing only after the whole tree is spliced summonInlines get replaced with actual trees that can be inspected) plus I'd kind of expect that Implicits.search will behave just like summonInline

@arainko arainko added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Feb 5, 2023
@mbovel mbovel added area:metaprogramming:reflection Issues related to the quotes reflection API and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels Feb 6, 2023
@nicolasstucki
Copy link
Contributor

From a quick look into this issue I found that Implicits.search finds and constructs the implicit tree for Show[Person2]. But for some reason it sets the type to SearchFailureType with the message given instance derivePartiallyConstructedByMacro in object Derivation does not match type Show[usage.Person2].

The tree of the implicit search is

{
  final class $anon() extends Object(), Show[usage.Person2] {
    def show(valuea: usage.Person2): String = ...
  }
  new $anon():Show[usage.Person2]
}:Show[usage.Person2]

I would have expected this to succeed the implicit search. Or at least not fail with that error.

@nicolasstucki nicolasstucki self-assigned this Feb 6, 2023
@nicolasstucki
Copy link
Contributor

nicolasstucki commented Feb 6, 2023

The nested implicit search is reporting an error

While expanding a macro, a reference to parameter `value` was used outside the scope where it was defined

This error becomes an impictit search failure. There are two distinct problems:

nicolasstucki added a commit to dotty-staging/dotty that referenced this issue Feb 6, 2023
Consider that `val macro` expansion in the context can come from
an outer macro that is being expanded (i.e. this is a nested macro).
Nested macro expansion can occur when a macro summons an implicit macro.

Fixes partially scala#16835
nicolasstucki added a commit to dotty-staging/dotty that referenced this issue Feb 6, 2023
Consider that `val macro` expansion in the context can come from
an outer macro that is being expanded (i.e. this is a nested macro).
Nested macro expansion can occur when a macro summons an implicit macro.

Fixes partially scala#16835
@nicolasstucki
Copy link
Contributor

Minimization

import scala.quoted.*

class Bar

inline def foo: Unit = ${ fooExpr }

def fooExpr(using Quotes): Expr[Unit] =
  import quotes.reflect.*
  Implicits.search(TypeRepr.of[Bar]) match
    case res: ImplicitSearchSuccess   => '{}
    case failure: ImplicitSearchFailure =>
      report.errorAndAbort(s"ImplicitSearchFailure: ${failure.explanation}")


inline given bar: Bar = ${ barExpr }

def barExpr(using Quotes): Expr[Bar] =
  import quotes.reflect.*
  report.error(s"my error")
  '{ new Bar }
def test: Unit = foo
1 |def test: Unit = foo
  |                 ^^^
  |                 ImplicitSearchFailure: given instance bar does not match type Bar

@arainko
Copy link
Contributor Author

arainko commented Feb 6, 2023

Thank you very much for a very quick response on this, it's been driving me crazy for some time now but I gaslighted myself into thinking this is correct behavior 😄

nicolasstucki added a commit to dotty-staging/dotty that referenced this issue Feb 7, 2023
nicolasstucki added a commit to dotty-staging/dotty that referenced this issue Feb 7, 2023
Consider that `val macro` expansion in the context can come from
an outer macro that is being expanded (i.e. this is a nested macro).
Nested macro expansion can occur when a macro summons an implicit macro.

Fixes partially scala#16835
@nicolasstucki
Copy link
Contributor

@arainko this one was a tricky one. It also confused me a lot. The combination of the two errors just made it hard to understand where things went wrong.

odersky added a commit that referenced this issue Feb 12, 2023
nicolasstucki added a commit to dotty-staging/dotty that referenced this issue Feb 14, 2023
Consider that `val macro` expansion in the context can come from
an outer macro that is being expanded (i.e. this is a nested macro).
Nested macro expansion can occur when a macro summons an implicit macro.

Fixes partially scala#16835
Kordyjan added a commit that referenced this issue Feb 16, 2023
Consider that `val macro` expansion in the context can come from an
outer macro that is being expanded (i.e. this is a nested macro). Nested
macro expansion can occur when a macro summons an implicit macro.

Fixes partially #16835
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:metaprogramming:reflection Issues related to the quotes reflection API itype:bug
Projects
None yet
Development

No branches or pull requests

3 participants