`tq` equivalent in Scala 3 macros
Asked Answered
E

1

1

With Scala2 I could implement a macro and generate types using tq quasiquote syntax, for eg:

q"""        
new Foo {
  type Bar = ${tq"(..$params)"}
}
"""

I am able to do two things with this syntax -

  1. Able to define a type Bar based on the params.
  2. Able to spread the params as a tuple.

How can I achieve this with Scala 3?

Esma answered 26/9, 2022 at 10:24 Comment(1)
Depending on where you get params from you might be able to use match types.Lashondra
M
5

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"...".

Menu answered 26/9, 2022 at 12:6 Comment(5)
You might be able to hack something together with type X; given Type[X] = arbitraryTypeTree.tpe.asType.asInstanceOf; '{ new Foo { type Bar = X } }Lashondra
Ah, you can get the same effect as my previous comment with arbitraryTypeTree.tpe.asType match { case '[x] => ... }. Makes me wonder why you have to go through these hoops instead of being able to just splice a Type instance directly.Lashondra
@Lashondra Great. Thanks. Yeah, I was talking about the case when we don't have Foo statically.Menu
Yeah if you don't have Foo you indeed have to create the whole tree manually.Lashondra
@Lashondra Yeah and my understanding is that currently there is no api for creating/overriding type member. Only class, method or val.Menu

© 2022 - 2024 — McMap. All rights reserved.