There are no quasiquotes (q"..."
, tq"..."
, pq"..."
, cq"..."
, fq"..."
) in Scala 3. Feel yourself like in early days of macros in Scala 2.10 :)
Scala 3 quotations '{...}
(and splicing ${...}
) must typecheck not only at compile time of the main code (i.e. runtime of macros, the time when macros expand) but also earlier at compile time of macros themselves. This is similar to reify {...}
(and .splice
) in Scala 2.
new Foo {...}
is actually an instance of an anonymous class extending Foo
. So see your previous question Macro class name expansion in Scala 3, Method Override with Scala 3 Macros
Everything depends on whether Foo
and params
are known statically. If so then everything is easy:
import scala.quoted.*
trait Foo
inline def doSmth[A](x: Int): Unit = ${doSmthImpl[A]('x)}
def doSmthImpl[A](x: Expr[Int])(using Quotes, Type[A]): Expr[Unit]= {
// import quotes.reflect.*
'{
new Foo {
type Bar = (Double, Boolean)
}
// doing smth with this instance
}
}
or
val params = Type.of[(Double, Boolean)]
'{
new Foo {
type Bar = $params
}
}
or
'{
new Foo {
type Bar = params.Underlying
}
}
In comments @Jasper-M advises how to handle the case when we have Foo
statically but params
not statically:
type X
given Type[X] = paramsTypeTree.tpe.asType.asInstanceOf[Type[X]]
'{
new Foo {
type Bar = X
}
}
or
paramsTypeTree.tpe.asType match {
case '[x] =>
'{
new Foo {
type Bar = x
}
}
}
Now suppose that Foo
is not known statically. Since there are no quasiquotes the only different way of constructing trees in Scala 3 is to go deeper to Tasty reflection level and build a tree manually. So you can print a tree of statically typechecked code and try to reconstruct it manually. The code
println('{
new Foo {
type Bar = (Double, Boolean)
}
}.asTerm.underlyingArgument.show
prints
{
final class $anon() extends App.Foo {
type Bar = scala.Tuple2[scala.Double, scala.Boolean]
}
(new $anon(): App.Foo)
}
And
println('{
new Foo {
type Bar = (Double, Boolean)
}
}.asTerm.underlyingArgument.show(using Printer.TreeStructure))
prints
Block(
List(ClassDef(
"$anon",
DefDef("<init>", List(TermParamClause(Nil)), Inferred(), None),
List(
Apply(Select(New(Inferred()), "<init>"), Nil),
TypeIdent("Foo")
),
None,
List(TypeDef(
"Bar",
Applied(
Inferred(),
List(TypeIdent("Double"), TypeIdent("Boolean")) // this should be params
)
))
)),
Typed(
Apply(Select(New(TypeIdent("$anon")), "<init>"), Nil),
Inferred()
)
)
One more complication here is that Scala 3 macros accept typed trees and must return typed trees. So we must handle symbols as well.
Actually, in reflection API I can see Symbol.newMethod
, Symbol.newClass
, Symbol.newVal
, Symbol.newBind
but no Symbol.newType
. (It turns out a method for new type member is not exposed to the reflection API, so we have to use internal dotty.tools.dotc.core.Symbols.newSymbol
.)
I can imagine something like
val fooTypeTree = TypeTree.ref(Symbol.classSymbol("mypackage.App.Foo"))
val parents = List(TypeTree.of[AnyRef], fooTypeTree)
def decls(cls: Symbol): List[Symbol] = {
given dotty.tools.dotc.core.Contexts.Context =
quotes.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx
import dotty.tools.dotc.core.Decorators.toTypeName
List(dotty.tools.dotc.core.Symbols.newSymbol(
cls.asInstanceOf[dotty.tools.dotc.core.Symbols.Symbol],
"Bar".toTypeName,
Flags.EmptyFlags/*Override*/.asInstanceOf[dotty.tools.dotc.core.Flags.FlagSet],
TypeRepr.of[(Double, Boolean)]/*params*/.asInstanceOf[dotty.tools.dotc.core.Types.Type]
).asInstanceOf[Symbol])
}
val cls = Symbol.newClass(Symbol.spliceOwner, "FooImpl", parents = parents.map(_.tpe), decls, selfType = None)
val typeSym = cls.declaredType("Bar").head
val typeDef = TypeDef(typeSym)
val clsDef = ClassDef(cls, parents, body = List(typeDef))
val newCls = Typed(Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil), fooTypeTree)
Block(List(clsDef, newCls), '{()}.asTerm).asExprOf[Unit]
//{
// class FooImpl extends java.lang.Object with mypackage.App.Foo {
// type Bar = scala.Tuple2[scala.Double, scala.Boolean]
// }
//
// (new FooImpl(): mypackage.App.Foo)
// ()
//}
package mypackage
object App {
trait Foo
}
Scala 3 macros are def macros, all generated definitions will be seen only inside the block that a macro expands into.
Maybe if it's enough to generate code at pre-compile time you can consider to use Scalameta. There are quasiquotes there :) q"..."
, t"..."
, p"..."
, param"..."
, tparam"..."
, init"..."
, self"..."
, template"..."
, mod"..."
, enumerator"..."
, import"..."
, importer"..."
, importee"..."
, source"..."
.
params
from you might be able to use match types. – Lashondra