Open japgolly opened 3 years ago
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.
@som-snytt If I had your consent, I would kiss you!! :heart: 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)
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....
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.
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.
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
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]
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?
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 }
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.
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.
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 }
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
.
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
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.
@nicolasstucki Hi, I want to vote for this issue.
In 3.3.3 Expr.summon
still not sees givens put in a block statements from block expr.
I can use summonInline
instead, but it is quite annoying since I can't see what is actually summoned.
This is what generated (same with summonInline).
{
val given_JsonWriter_Option[String]: tethys.JsonWriter[scala.Option[scala.Predef.String]] = tethys.JsonWriter.optionalWriter[java.lang.String](tethys.JsonWriter.stringWriter)
final class $anon() extends tethys.JsonObjectWriter[Person] {
override def writeValues(value: Person, tokenWriter: tethys.writers.tokens.TokenWriter): scala.Unit = {
tethys.derivation.ConfigurationMacroUtils$package.delayedSummonInline[tethys.JsonWriter[scala.Int]].write("id", value.id, tokenWriter)
tethys.derivation.ConfigurationMacroUtils$package.delayedSummonInline[tethys.JsonWriter[java.lang.String]].write("name", value.name, tokenWriter)
tethys.derivation.ConfigurationMacroUtils$package.delayedSummonInline[tethys.JsonWriter[scala.Option[scala.Predef.String]]].write("phone", value.phone, tokenWriter)
tethys.derivation.ConfigurationMacroUtils$package.delayedSummonInline[tethys.JsonWriter[java.lang.String]].write("default", value.default, tokenWriter)
()
}
}
(new $anon(): tethys.JsonObjectWriter[Person])
}
So I can't actually see, is given_JsonWriter_Option[String]
used or it summons tethys.JsonWriter.optionalWriter[java.lang.String](tethys.JsonWriter.stringWriter)
again
And I want to get this, but Expr.summon can't find given_JsonWriter_Option[String]
:
{
val given_JsonWriter_Option[String]: tethys.JsonWriter[scala.Option[scala.Predef.String]] = tethys.JsonWriter.optionalWriter[java.lang.String](tethys.JsonWriter.stringWriter)
final class $anon() extends tethys.JsonObjectWriter[Person] {
override def writeValues(value: Person, tokenWriter: tethys.writers.tokens.TokenWriter): scala.Unit = {
tethys.JsonWriter.intWriter.write("id", value.id, tokenWriter)
tethys.JsonWriter.stringWriter.write("name", value.name, tokenWriter)
given_JsonWriter_Option[String].write("phone", value.phone, tokenWriter)
tethys.JsonWriter.stringWriter.write("default", value.default, tokenWriter)
()
}
}
(new $anon(): tethys.JsonObjectWriter[Person])
}
Hi @goshacodes, I am not actively working on Scala anymore. @jchyb took over this part of the language. Please follow up with him.
Compiler version
3.0.0-RC3
Attempt 1:
summonInline
Minimized code
a:scala
:b:scala
:Output
Attempt 2:
inline
call toImplicits.search
Minimized code
a:scala
:b:scala
:Output
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.