Skip to content

Summoned Mirror of Spliced Type is neither Sum nor Product #7974

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

Closed
deusaquilus opened this issue Jan 13, 2020 · 9 comments
Closed

Summoned Mirror of Spliced Type is neither Sum nor Product #7974

deusaquilus opened this issue Jan 13, 2020 · 9 comments
Assignees

Comments

@deusaquilus
Copy link
Contributor

deusaquilus commented Jan 13, 2020

A full demonstration of this issue is available here (there is nothing else in the repo):
https://github.com/deusaquilus/mirror_test

minimized code

Let's say that I summon a mirror for an encoder via summonExpr inside a macro like so:

    val mirrorTpe = '[Mirror.Of[$t]]
    val mirrorExpr = summonExpr(given mirrorTpe) match {
      case Some(mirror) => mirror
    }

Then I proceed to use it to summon a derived type-class (e.g. JsonEncoder) like so:

    '{
      given JsonEncoder[T] = JsonEncoder.derived($mirrorExpr)
      val encoder = summon[JsonEncoder[$t]]
      encoder.encode($value)
    }

Then I try to declare a class and use the macro containing the above summonExpr:

case class PersonSimple(name:String, age:Int)
val stuff = PersonSimple("Joe", 123)
println(SummonJsonEncoderTest.encodeAndMessAroundType(stuff) )

Instead of summoning a Product-Type mirror, it summons the following mirror:

scala.deriving.Mirror{
  MirroredType = t.$splice; MirroredMonoType = t.$splice; 
    MirroredElemTypes <: Tuple
}

Since this mirror does not contain any information about whether the type T is a product or sum type, it is impossible to use. A typical encoder will fail with the following error:

[error] -- Error: /home/alexander/git/dotty/mirror_test/src/main/scala/org/deusaquilus/SummonJsonEncoderTest.scala:22:48 
[error] 22 |      given JsonEncoder[T] = JsonEncoder.derived($mirrorExpr)
[error]    |                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[error]    |            cannot reduce inline match with
[error]    |             scrutinee:  {
[error]    |              ev
[error]    |            } : (ev : 
[error]    |              scala.deriving.Mirror{
[error]    |                MirroredType = t.$splice; MirroredMonoType = t.$splice; 
[error]    |                  MirroredElemTypes <: Tuple
[error]    |              }
[error]    |            )
[error]    |             patterns :  case m @ _:deriving.Mirror.SumOf[T]
[error]    |                         case m @ _:deriving.Mirror.ProductOf[T]
[error]    | This location contains code that was inlined from JsonEncoder.scala:28

expectation

A Product mirror for the type PersonSimple should be summoned that looks like this:

  (
    scala.deriving.Mirror{
      MirroredType = PersonSimple; MirroredMonoType = PersonSimple; 
        MirroredElemTypes <: Tuple
    }
   & 
    scala.deriving.Mirror.Product{
      MirroredMonoType = PersonSimple; MirroredType = PersonSimple; 
        MirroredLabel = "PersonSimple"
    }
  ){MirroredElemTypes = (String, Int); MirroredElemLabels = ("name", "age")}

Edit:

Note that it is interesting that with summonFrom this seems to work:

inline def encodeAndMessAroundType[T](value: =>T): String = {
    summonFrom {
      case m: Mirror.ProductOf[T] => derived(m).encode(value)
    }
  }

Full example here:
https://github.com/deusaquilus/mirror_test/tree/summonFrom

What is the difference between what I am doing with summonExpr and summonFrom?

@milessabin
Copy link
Contributor

Thanks, but please try and reduce this to a short, single file, standalone example with no external dependencies of any sort.

@odersky
Copy link
Contributor

odersky commented Jan 13, 2020

summonFrom uses some bits of compiler magic. It delays implicit resolution to the use site where the enclosing inline function is expanded. Also, it allows implicit searches to fail, which could lead to some alternative being explored. I guess the first difference accounts for what you are seeing here.

@biboudis
Copy link
Contributor

biboudis commented Jan 13, 2020

Thanks, but please try and reduce this to a short, single file, standalone example with no external dependencies of any sort.

I am trying to do that as we speak 😄, but yes minimisations are important 🙈

@deusaquilus
Copy link
Contributor Author

deusaquilus commented Jan 13, 2020

@milessabin @biboudis Okay, here's as simple as I can make it. It has to be 2 files because otherwise the macro system complains that it can't define and execute a macro in the same file.

Here's the buggy code:

case class Person(name:String)

trait MyTypeclass[T] {
  def useit(elem: T): String
}

object MyTypeclass {
  inline def derived[T](implicit ev: Mirror.Of[T]): MyTypeclass[T] = new MyTypeclass[T] {
    def useit(value: T): String =
      inline ev match {
        case m: Mirror.SumOf[T] => "it's a sum, yay!"
        case m: Mirror.ProductOf[T] => "it's a product, yay!"
      }
  }
}

object Helper {
  inline def findAndEncode[T](value: =>T): String = ${ findAndEncodeImpl('value) }

  def findAndEncodeImpl[T](value: Expr[T])(given qctx: QuoteContext, t: Type[T]): Expr[String] = {
    import qctx.tasty.{_, given}

    val mirrorTpe = '[deriving.Mirror.Of[$t]]
    val mirrorExpr = summonExpr(given mirrorTpe) match {
      case Some(mirror) => mirror
    }

    '{
      given MyTypeclass[T] = MyTypeclass.derived($mirrorExpr)
      val mytc = summon[MyTypeclass[$t]]
      mytc.useit($value)
    }
  }
}

Here's how I run it:

@main def run() = {
  println(Helper.findAndEncode(Person("Joe")) )
}

Code is also in git here:
https://github.com/deusaquilus/mirror_test/tree/minimal

The following error happens:

[error] 38 |      given MyTypeclass[T] = MyTypeclass.derived($mirrorExpr)
[error]    |                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[error]    |            cannot reduce inline match with
[error]    |             scrutinee:  {
[error]    |              ev
[error]    |            } : (ev : 
[error]    |              scala.deriving.Mirror{
[error]    |                MirroredType = t.$splice; MirroredMonoType = t.$splice; 
[error]    |                  MirroredElemTypes <: Tuple
[error]    |              }
[error]    |            )
[error]    |             patterns :  case m @ _:deriving.Mirror.SumOf[T]
[error]    |                         case m @ _:deriving.Mirror.ProductOf[T]

Edit:
Add message, fixed names.

@biboudis
Copy link
Contributor

biboudis commented Jan 14, 2020

While trying to minimize it further by eliminating derivation mechanism etc, we reached to the following that works. It made sense after moving the code of the quote inside the summonExpr ... match. This solved the problem because previously there was no way to know whether mirrorExpr was statically typed as either SumOf or ProductOf. The problem was appearing at the expansion time of derived and not at the findAndEncode.

import scala.quoted._
import scala.deriving._
import scala.quoted.matching._
import scala.compiletime.{erasedValue, summonFrom}

trait TC[T] {
  def foo(elem: T): Unit
}

object TC {
  inline def derived[T](given ev: Mirror.Of[T]): TC[T] = new TC[T] {
    def foo(elem: T): Unit =
      inline ev match {
        case m: Mirror.SumOf[T] => ???
        case m: Mirror.ProductOf[T] => ???
      }
  }
}

object Helper {
  inline def f[T](value: =>T): Unit = ${ macroImpl('value) }

  def macroImpl[T](value: Expr[T])(given qctx: QuoteContext, t: Type[T]): Expr[Unit] = {
    import qctx.tasty._

    val mirrorTpe = '[Mirror.Of[$t]]
    summonExpr(given mirrorTpe) match {
      case Some('{$mirror: Mirror.SumOf[T]}) => '{
        given TC[T] = TC.derived(given $mirror)
        ()
      }
    }
  }
}

WDYT?

@biboudis
Copy link
Contributor

2nd version which doesn't mix inline and macros.

import scala.quoted._
import scala.deriving._
import scala.quoted.matching._
import scala.compiletime.{erasedValue, summonFrom}

trait TC[T] {
  def foo(elem: T): Unit
}

object TC {
  // derived is a regular method now, merely a combinator lets say for Expr's (Expr => Expr)
  def derived[T: Type](ev: Expr[Mirror.Of[T]])(given qctx: QuoteContext): Expr[TC[T]] = '{
    new TC[T] {
      def foo(elem: T): Unit = $ev
    }
  }
}

object Helper {
  inline def f[T](value: =>T): Unit = ${ macroImpl('value) }

  def macroImpl[T](value: Expr[T])(given qctx: QuoteContext, t: Type[T]): Expr[Unit] = {
    import qctx.tasty._

    val mirrorTpe = '[Mirror.Of[$t]]

    val mirrorExpr = summonExpr(given mirrorTpe) match {
      case Some(mirror) => mirror
    }

    // the precise type is done during macro expansion time
    val preciseMirrorExpr = mirrorExpr match {
      case '{$m: Mirror.SumOf[T]} => ???
      case '{$m: Mirror.ProductOf[T]} => ???
    }

    val res = TC.derived[T](preciseMirrorExpr)

    '{
      given TC[T] = $res
      ()
    }
  }
}

@deusaquilus
Copy link
Contributor Author

Wait a minute? So it’s impossible to summon an actual Mirror[T]? What if I need to get T’s fields?

@biboudis
Copy link
Contributor

Compile (compilation of macro??) or runtime (expansion of macro)?

@biboudis
Copy link
Contributor

The problem was clarified. PR #8011 adds the regression

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants