Skip to content

Decoupling Macros in ReifyQuotes #4795

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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class Compiler {
List(new sbt.ExtractDependencies) :: // Sends information on classes' dependencies to sbt via callbacks
List(new PostTyper) :: // Additional checks and cleanups after type checking
List(new sbt.ExtractAPI) :: // Sends a representation of the API of classes to sbt via callbacks
List(new MacrosSplitter) :: // Applies ReifyQuotes transformation to the body of the macros
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"to macro bodies"

Nil

/** Phases dealing with TASTY tree pickling and unpickling */
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,7 @@ class Definitions {
def Unpickler_unpickleType = ctx.requiredMethod("scala.runtime.quoted.Unpickler.unpickleType")

lazy val TastyTopLevelSpliceModule = ctx.requiredModule("scala.tasty.TopLevelSplice")
lazy val TastyTopLevelSplice_compilationTopLevelSplice = TastyTopLevelSpliceModule.requiredMethod("tastyContext")
lazy val TastyTopLevelSplice_tastyContext = TastyTopLevelSpliceModule.requiredMethod("tastyContext")

lazy val EqType = ctx.requiredClassRef("scala.Eq")
def EqClass(implicit ctx: Context) = EqType.symbol.asClass
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/NameKinds.scala
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ object NameKinds {
val SuperArgName = new UniqueNameKind("$superArg$")
val DocArtifactName = new UniqueNameKind("$doc")
val UniqueInlineName = new UniqueNameKind("$i")
val UniqueSpliceName = new UniqueNameKind("$splice$")

/** A kind of unique extension methods; Unlike other unique names, these can be
* unmangled.
Expand Down
188 changes: 188 additions & 0 deletions compiler/src/dotty/tools/dotc/transform/MacrosSplitter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package dotty.tools.dotc
package transform

import core._
import Decorators._
import Types._
import Contexts._
import Symbols._
import Constants._
import Flags._
import ast.Trees._
import ast.{TreeTypeMap, tpd}
import tasty.TreePickler.Hole
import SymUtils._
import NameKinds._

import scala.collection.mutable
import dotty.tools.dotc.core.StdNames._


/** Translates macro implementation.
* Checks that the phase consistency principle (PCP) holds on the body of a macro.
*
*
* Transforms an inline macro definitions we assume that we have a single ~ directly as the RHS.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(?) Should be two sentences, maybe?

* We will transform the definition from
* ```
* inline def foo[T1, ...](inline x1: X, ..., y1: Y, ....): Z = ~{ ... x1 ... '{ ... T1 ... x1 ... '(y) ... } }
* ```
* to
* ```
* inline def foo[T1, ...](inline x1: X, ..., y1: Y, ....): Z = ~foo$splice$1[T1, ...]('[T1], ..., x1, ..., '(y1), ...)
*
* def foo$splice$1[T1, ...](T1$1: Type[T1], ..., x1$1: X, ..., y1: Expr[Y], ....): Z =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the original splice is already of the form

~bar(...)

In that case, we can simply forward to bar, no? Would it not be simpler to require that all toplevel splices are of this form?

If we do have a requirement like this, what is the added benefit of the phase?

* { ... x1$1 .... '{ ... T1$1.unary_~ ... x1$1.toExpr.unary_~ ... y1$1.unary_~ ... } ... }
* ```
* Where `inline` parameters with type Boolean, Byte, Short, Int, Long, Float, Double, Char and String are
* passed as their actual runtime value. See `isStage0Value`. Other `inline` arguments such as functions are handled
* like `y1: Y`.
*
*
* At inline site we will call reflectively the static method `foo$splice$1` with the interpreted parameters.
*/
class MacrosSplitter extends ReifyQuotes {
import ast.tpd._

override def phaseName: String = "macrosSplitter"

override def run(implicit ctx: Context): Unit =
if (ctx.compilationUnit.containsQuotesOrSplices) super.run

protected override def newTransformer(implicit ctx: Context): Transformer = {
new Transformer {
override def transform(tree: tpd.Tree)(implicit ctx: Context): tpd.Tree = tree match {
case tree: PackageDef => super.transform(tree)
case tree: TypeDef => super.transform(tree)
case tree: Template => super.transform(tree)
case macroDefTree: DefDef if macroDefTree.symbol.is(Inline) =>

val reifier = new Reifier(true, null, 1, new LevelInfo, new mutable.ListBuffer[Tree])

val transformedTree = reifier.transform(macroDefTree) // Ignore output, we only need the its embedding

if (reifier.embedded.isEmpty) macroDefTree // Not a macro
else {
if (!macroDefTree.symbol.isStatic) // TODO remove restriction (issue #4803)
ctx.error("Inline macro method must be a static method.", macroDefTree.pos)
if (InlineSplice.unapply(macroDefTree.rhs).isEmpty) { // TODO allow multiple splices (issue #4801)
ctx.error(
"""Malformed inline macro.
|
|Expected the ~ to be at the top of the RHS:
| inline def foo(...): Int = ~impl(...)
|or
| inline def foo(...): Int = ~{
| val x = 1
| impl(... x ...)
| }
""".stripMargin, macroDefTree.rhs.pos)
EmptyTree
}
else {
val splicers = List.newBuilder[DefDef]
val transformer = new TreeMap() {
override def transform(tree: tpd.Tree)(implicit ctx: Context): tpd.Tree = tree match {
case Hole(idx, args) =>
val targs = args.filter(_.isType).map(_.tpe)

val Block((lambdaDef: tpd.DefDef) :: Nil, _) = reifier.embedded(idx)

val splicer = genSplicer(macroDefTree, lambdaDef, targs)
splicers += splicer

val liftedArgs = args.map { arg =>
if (arg.symbol.is(Inline) || arg.symbol == defn.TastyTopLevelSplice_tastyContext) arg
else if (arg.isType) ref(defn.QuotedType_apply).appliedToType(arg.tpe)
else ref(defn.QuotedExpr_apply).appliedToType(arg.tpe.widen).appliedTo(arg)
}
ref(splicer.symbol).appliedToTypes(targs).appliedToArgs(liftedArgs).select(nme.UNARY_~)
case _ => super.transform(tree)
}
}
val newRhs = transformer.transform(transformedTree.asInstanceOf[DefDef].rhs)
val newDef = cpy.DefDef(macroDefTree)(rhs = newRhs)
Thicket(newDef :: splicers.result())
}
}
case _ =>
tree
}
}
}

/** Generates splicer method from a lambda generated by `ReifyQuotes`
*
* Transforms `lambdaDef` (with the given `targs`)
* ```
* (args: Seq[Any]) => {
* val T1$1 = args(0).asInstanceOf[Type[T1]]
* ...
* val x1$1 = args(0).asInstanceOf[X]
* ...
* val y1$1 = args(1).asInstanceOf[Expr[Y]]
* ...
* { ... x1$1 .... '{ ... T1$1.unary_~ ... x1$1.toExpr.unary_~ ... y1$1.unary_~ ... } ... }
* }
* ```
*
* into
*
* ```
* def foo$splice$1[T1, ...](T1$1: Type[T1], ..., x1$1: X, ..., y1: Expr[Y], ....): Z =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does foo come from. It does not appear in the input.

* { ... x1$1 .... '{ ... T1$1.unary_~ ... x1$1.toExpr.unary_~ ... y1$1.unary_~ ... } ... }
* ```
*/
private def genSplicer(macroDefTree: DefDef, lambdaDef: DefDef, targs: List[Type])(implicit ctx: Context): DefDef = {
val sym = macroDefTree.symbol

val DefDef(_, _, _, _, Block(args: List[ValDef] @unchecked, body: Tree @unchecked)) = lambdaDef

val splicerRetType = {
val methodType = MethodType(args.map(_.name), args.map(_.tpt.tpe), lambdaDef.symbol.info.finalResultType)
if (targs.isEmpty) methodType
else {
val tpArgSyms = targs.map(_.typeSymbol)
val tpArgName = tpArgSyms.map(_.name.asTypeName)
val tpArgBounds = tpArgSyms.map(_.info.bounds)
PolyType(tpArgName)(_ => tpArgBounds, pt => methodType.subst(targs.map(_.typeSymbol), pt.paramRefs))
}
}

val splicerName = UniqueSpliceName.fresh(sym.name.asTermName)
val splicerSym = ctx.newSymbol(sym.owner, splicerName, sym.flags &~ Inline &~ Implicit, splicerRetType, sym.privateWithin, sym.coord).asTerm

def treeTypeMap(tparams: List[Type], params: List[Tree]) = {
val treeMap: Tree => Tree = {
val map = args.map(_.symbol).zip(params).toMap
tree => map.getOrElse(tree.symbol, tree)
}
val typeMap: Type => Type = new TypeMap() {
private val map = macroDefTree.tparams.map(_.symbol).zip(tparams).toMap
def apply(tp: Type): Type = map.getOrElse(tp.typeSymbol, mapOver(tp))
}
new TreeTypeMap(
typeMap = typeMap, treeMap = treeMap,
oldOwners = lambdaDef.symbol :: Nil, newOwners = splicerSym :: Nil,
substFrom = lambdaDef.tparams.map(_.symbol), substTo = tparams.map(_.typeSymbol)
)
}

polyDefDef(splicerSym, tparams => vparamss => treeTypeMap(tparams, vparamss.head).transform(body))
}

/** InlineSplice is used to detect cases where the expansion
* consists of a (possibly multiple & nested) block or a sole expression.
*/
object InlineSplice {
def unapply(tree: Tree)(implicit ctx: Context): Option[Select] = {
tree match {
case expansion: Select if expansion.symbol.isSplice => Some(expansion)
case Block(List(stat), Literal(Constant(()))) => unapply(stat)
case Block(Nil, expr) => unapply(expr)
case _ => None
}
}
}

}
Loading