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

Inline summon generated by macro doesn't consider expanded macro scope #12359

Open
japgolly opened this issue May 7, 2021 · 15 comments
Open
Assignees
Labels
area:metaprogramming:reflection Issues related to the quotes reflection API

Comments

@japgolly
Copy link
Contributor

japgolly commented May 7, 2021

Compiler version

3.0.0-RC3

Attempt 1: summonInline

Minimized code

a:scala:

import scala.quoted.*

case class F[A]()

object F:
  implicit def option[A: F]: F[Option[A]] = F()

  inline def test[A]: F[Option[A]] =
    ${ _test[A] }

  def _test[A](using Quotes, Type[A]): Expr[F[Option[A]]] =
    import quotes.reflect.*

    // Dynamically created `implicit val`
    val faSym  = Symbol.newVal(Symbol.spliceOwner, "fa", TypeRepr.of[F[A]], Flags.Implicit, Symbol.noSymbol)
    val faBody = '{ F[Int]() }.asTerm
    val faDef  = ValDef(faSym, Some(faBody))

    val summonInline = '{ scala.compiletime.summonInline[F[Option[A]]] }.asTerm
    val result = Block(List(faDef), summonInline).asExprOf[F[Option[A]]]

    println(s"\n${result.show}\n")
    result

b:scala:

object Test:

  val ko = F.test[Int]

  // Copy-paste of the .show output of the generated Expr
  val ok =
    implicit val fa: F[scala.Int] = F.apply[scala.Int]()
    scala.compiletime.package$package.summonInline[F[scala.Option[scala.Int]]]

Output

{
  implicit val fa: F[scala.Int] = F.apply[scala.Int]()
  scala.compiletime.package$package.summonInline[F[scala.Option[scala.Int]]]
}

[error] -- Error: b.scala:3:17 ----------------
[error] 3 |  val ko = F.test[Int]
[error]   |           ^^^^^^^^^^^
[error]   |           cannot reduce summonFrom with
[error]   |            patterns :  case given t @ _:F[Option[Int]]
[error]   | This location contains code that was inlined from package.scala:140
[error]   | This location contains code that was inlined from a.scala:19
[error]   | This location contains code that was inlined from a.scala:19
[error] one error found

Attempt 2: inline call to Implicits.search

Minimized code

a:scala:

import scala.quoted.*

object Util:
  def summonLater[A: Type](using Quotes): Expr[A] =
    '{ inlineSummon[A] }

  inline def inlineSummon[A]: A =
    ${ _inlineSummon[A] }

  def _inlineSummon[A: Type](using Quotes): Expr[A] =
    summonOrError[A]

  def summonOrError[A](using Type[A])(using Quotes): Expr[A] =
    import quotes.reflect.*
    Implicits.search(TypeRepr.of[A]) match
      case iss: ImplicitSearchSuccess => iss.tree.asExpr.asInstanceOf[Expr[A]]
      case isf: ImplicitSearchFailure => report.throwError(isf.explanation)

case class F[A]()

object F:
  implicit def option[A: F]: F[Option[A]] = F()

  inline def test[A]: F[Option[A]] =
    ${ _test[A] }

  def _test[A](using Quotes, Type[A]): Expr[F[Option[A]]] =
    import quotes.reflect.*

    // Dynamically created `implicit val`
    val faSym  = Symbol.newVal(Symbol.spliceOwner, "fa", TypeRepr.of[F[A]], Flags.Implicit, Symbol.noSymbol)
    val faBody = '{ F[Int]() }.asTerm
    val faDef  = ValDef(faSym, Some(faBody))

    val summonInline = Util.summonLater[F[Option[A]]].asTerm
    val result = Block(List(faDef), summonInline).asExprOf[F[Option[A]]]

    println(s"\n${result.show}\n")
    result

b:scala:

object Test:

  val ko = F.test[Int]

  // Copy-paste of the .show output of the generated Expr
  val ok =
    implicit val fa: F[scala.Int] = F.apply[scala.Int]()
    Util.inlineSummon[F[scala.Option[scala.Int]]]

Output

{
  implicit val fa: F[scala.Int] = F.apply[scala.Int]()
  Util.inlineSummon[F[scala.Option[scala.Int]]]
}

[error] -- Error: b.scala:3:17 ----------------
[error] 3 |  val ko = F.test[Int]
[error]   |           ^^^^^^^^^^^
[error]   |           no implicit values were found that match type F[Int]
[error]   | This location contains code that was inlined from b.scala:3
[error]   | This location contains code that was inlined from a.scala:5
[error]   | This location contains code that was inlined from a.scala:5
[error] one error found

Expectation

Neither macro compiles, however in both cases if I copy-paste the output of .show, the code compiles ok.

Implicit search should consider sibling implicits in local scope, just like normal code.

@som-snytt
Copy link
Contributor

In attempt 1, if it is inline def summonInline:

exception occurred while compiling a.scala
java.lang.AssertionError: assertion failed: unresolved symbols: type A (line 12) #9326 when pickling a.scala while compiling a.scala
Exception in thread "main" java.lang.AssertionError: assertion failed: unresolved symbols: type A (line 12) #9326 when pickling a.scala
        at scala.runtime.Scala3RunTime$.assertFailed(Scala3RunTime.scala:8)
        at dotty.tools.dotc.core.tasty.TreePickler.pickle(TreePickler.scala:788)
        at dotty.tools.dotc.quoted.PickledQuotes$.pickle(PickledQuotes.scala:168)

If it is transparent inline def summonInline then it just works.

Then I quit while I was ahead.

@japgolly
Copy link
Contributor Author

japgolly commented May 7, 2021

@som-snytt If I had your consent, I would kiss you!! ❤️ Thank you so much!

import scala.quoted.*

case class F[A](i: Int)

object F:
  implicit def option[A](implicit f: F[A]): F[Option[A]] = F(f.i * 10)

  inline def test[A]: F[Option[A]] =
    ${ _test[A] }

  def _test[A](using Quotes, Type[A]): Expr[F[Option[A]]] =
    import quotes.reflect.*
    transparent inline def summonInline = '{ scala.compiletime.summonInline[F[Option[A]]] }
    val result: Expr[F[Option[A]]] = '{
      implicit val fa: F[A] = F(3)
      $summonInline
    }
    // println(s"\n${result.show}\n")
    result
@main def run =
  println("Expecting to see F(30)")
  println(F.test[Int])

which does indeed print:

Expecting to see F(30)
F(30)

@japgolly
Copy link
Contributor Author

japgolly commented May 7, 2021

Sorry @som-snytt but I need to take that potential kiss back. It doesn't actually work. It only appeared to work because it was statically able to derive an instance before macro-expansion time and only because this is a static example for minification purposes. I'll clarify with a new snippet....

@japgolly
Copy link
Contributor Author

japgolly commented May 7, 2021

I've updated the examples in the main issue description. I minimised a little too much, in reality I'm working with dynamically-created implicits in scope.

@som-snytt
Copy link
Contributor

Sorry to break your heart like that 💔 but I just started reading the docs.

I'm pretty sure this was the tag line for the movie "Highlander 4: The Inlining":

The summoning is delayed until the call has been fully inlined.

@nicolasstucki
Copy link
Contributor

The correct way to summon this value is using Expr.summon. But still there is some bug.

- val summonInline = '{ scala.compiletime.summonInline[F[Option[A]]] }.asTerm
+ val summonInline = Expr.summon[F[Option[A]]].get.asTerm

@nicolasstucki
Copy link
Contributor

Oh, there is no implicit F[scala.Int] at the expansion site. The following fixes the issue.

object Test:
+  implicit val fa: F[scala.Int] = F.apply[scala.Int]()
  val ko = F.test[Int]

@nicolasstucki
Copy link
Contributor

The here is if the following summonInline can see the given definition that got inlined

inline def f() =
  given Int = 3
  compiletime.summonInline[Int]

def test() = 
  f()
  // given given_Int: Int = 3
  // compiletime.summonInline[Int](using ???)

The real question is if this would count as a re-elaboration of the code or not.

We designed the inline summoning assuming that we would find the implicit at call site. But this shows that the call site might also contain inlined given definitions.

@odersky, @sjrd WDYT?

@nicolasstucki
Copy link
Contributor

nicolasstucki commented May 10, 2021

Note that even if we can see the previously given definition we would not be able to see this one as f is inlined after g is expanded.

inline def f(inline thunk: Unit) =
  given Int = 3
  thunk

def g = summonInline[Int]

def test() = f { g }

@nicolasstucki
Copy link
Contributor

Also, summonInline should behave the same way as Expr.summon which seems to indicate that we cannot rely on the given definitions that are inlined.

@nicolasstucki
Copy link
Contributor

It seems that the current semantics are correct and we cannot change them without breaking something.

summonInline, Expr.summon and quotes.refelct.Implicits.search see the context of the expansion site at the time of exapansion.

@nicolasstucki
Copy link
Contributor

Note that we can inject a given definition if we combine transparent and non-transparent definitions

transparent inline def f(inline thunk: Unit) =
  given Int = 3
  thunk

inline def g = compiletime.summonInline[Int]

def test() = f { g }

@nicolasstucki
Copy link
Contributor

This means that we can, with some ugly hacks get some kind of given injection using

import scala.quoted.*

case class F[A]()

object F:
  implicit def option[A: F]: F[Option[A]] = F()

  transparent inline def test[A]: F[Option[A]] =
    ${ _test[A] }

+  inline def delayedSummonInline[T] = scala.compiletime.summonInline[T]

  def _test[A](using Quotes, Type[A]): Expr[F[Option[A]]] =
    import quotes.reflect.*

    // Dynamically created `implicit val`
    val faSym  = Symbol.newVal(Symbol.spliceOwner, "fa", TypeRepr.of[F[A]], Flags.Implicit, Symbol.noSymbol)
    val faBody = '{ F[Int]() }.asTerm
    val faDef  = ValDef(faSym, Some(faBody))

-   val summonInline = '{ scala.compiletime.summonInline[F[Option[A]]] }.asTerm
+   val summonInline = '{ delayedSummonInline[F[Option[A]]] }.asTerm
    val result = Block(List(faDef), summonInline).asExprOf[F[Option[A]]]

    println(s"\n${result.show}\n")
    result

But this is a quite bad pattern as it is just forcing a new implicit search for something that we already know how to build.
If we created fa then we should just use it where we need to find fa.

@japgolly
Copy link
Contributor Author

Oh wow! Thanks so much @nicolasstucki ! You accidentally missed another small change that was required in that test needs to be transparent but yeah, I can confirm that those 3 small changes make all the difference! Seriously thanks so much!!

import scala.quoted.*

case class F[A]()

object F:
  implicit def option[A: F]: F[Option[A]] = F()

-  inline def test[A]: F[Option[A]] =
+  transparent inline def test[A]: F[Option[A]] =
    ${ _test[A] }

+  inline def delayedSummonInline[T] = scala.compiletime.summonInline[T]

  def _test[A](using Quotes, Type[A]): Expr[F[Option[A]]] =
    import quotes.reflect.*

    // Dynamically created `implicit val`
    val faSym  = Symbol.newVal(Symbol.spliceOwner, "fa", TypeRepr.of[F[A]], Flags.Implicit, Symbol.noSymbol)
    val faBody = '{ F[Int]() }.asTerm
    val faDef  = ValDef(faSym, Some(faBody))

-   val summonInline = '{ scala.compiletime.summonInline[F[Option[A]]] }.asTerm
+   val summonInline = '{ delayedSummonInline[F[Option[A]]] }.asTerm
    val result = Block(List(faDef), summonInline).asExprOf[F[Option[A]]]

    println(s"\n${result.show}\n")
    result

@japgolly
Copy link
Contributor Author

But this is a quite bad pattern as it is just forcing a new implicit search for something that we already know how to build.
If we created fa then we should just use it where we need to find fa.

If you know exactly what you need, then yes 100% this is a bad pattern, I agree with you. The problem is that there are cases where you not only do you know what what's required, but the effort to work out if something is required and then to try to recursively populate it with it's required implicits, especially when you're generating implicits that can (according to user-space rules and not the rules of the macro) have mutual/recursive implicit dependencies, trying to calculate all of that effort 100% statically is equivalent to just implementing the implicit search algorithm. Using this pattern I can just generate all of the expected implicits and if the user has done everything right on their side, then everything should work out at expansion time. If they haven't, then they'll get a nice implicit-not-found error after my macro expands which is perfect for both users, and me as the macro author.

japgolly added a commit to japgolly/microlibs-scala that referenced this issue May 10, 2021
@nicolasstucki nicolasstucki added area:metaprogramming:reflection Issues related to the quotes reflection API and removed area:metaprogramming labels Jun 2, 2022
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
Projects
None yet
Development

No branches or pull requests

4 participants