-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Macro annotation (part 1) #16392
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
Macro annotation (part 1) #16392
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,7 +32,7 @@ import dotty.tools.dotc.reporting.Message | |
import dotty.tools.repl.AbstractFileClassLoader | ||
|
||
/** Tree interpreter for metaprogramming constructs */ | ||
abstract class Interpreter(pos: SrcPos, classLoader: ClassLoader)(using Context): | ||
class Interpreter(pos: SrcPos, classLoader: ClassLoader)(using Context): | ||
import Interpreter._ | ||
import tpd._ | ||
|
||
|
@@ -68,7 +68,7 @@ abstract class Interpreter(pos: SrcPos, classLoader: ClassLoader)(using Context) | |
|
||
// TODO disallow interpreted method calls as arguments | ||
case Call(fn, args) => | ||
if (fn.symbol.isConstructor && fn.symbol.owner.owner.is(Package)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you mind explaining why was this check removed (or actually, what was it doing before and why is it not needed anymore?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check was there to make sure we only called |
||
if (fn.symbol.isConstructor) | ||
interpretNew(fn.symbol, args.flatten.map(interpretTree)) | ||
else if (fn.symbol.is(Module)) | ||
interpretModuleAccess(fn.symbol) | ||
|
@@ -185,8 +185,9 @@ abstract class Interpreter(pos: SrcPos, classLoader: ClassLoader)(using Context) | |
private def interpretModuleAccess(fn: Symbol): Object = | ||
loadModule(fn.moduleClass) | ||
|
||
private def interpretNew(fn: Symbol, args: => List[Object]): Object = { | ||
val clazz = loadClass(fn.owner.fullName.toString) | ||
private def interpretNew(fn: Symbol, args: List[Object]): Object = { | ||
val className = fn.owner.fullName.mangledString.replaceAll("\\$\\.", "\\$") | ||
val clazz = loadClass(className) | ||
val constr = clazz.getConstructor(paramsSig(fn): _*) | ||
constr.newInstance(args: _*).asInstanceOf[Object] | ||
} | ||
|
@@ -219,10 +220,6 @@ abstract class Interpreter(pos: SrcPos, classLoader: ClassLoader)(using Context) | |
private def loadClass(name: String): Class[?] = | ||
try classLoader.loadClass(name) | ||
catch { | ||
case _: ClassNotFoundException if ctx.compilationUnit.isSuspendable => | ||
if (ctx.settings.XprintSuspension.value) | ||
report.echo(i"suspension triggered by a dependency on $name", pos) | ||
ctx.compilationUnit.suspend() | ||
case MissingClassDefinedInCurrentRun(sym) if ctx.compilationUnit.isSuspendable => | ||
if (ctx.settings.XprintSuspension.value) | ||
report.echo(i"suspension triggered by a dependency on $sym", pos) | ||
|
@@ -277,13 +274,15 @@ abstract class Interpreter(pos: SrcPos, classLoader: ClassLoader)(using Context) | |
} | ||
|
||
private object MissingClassDefinedInCurrentRun { | ||
def unapply(targetException: NoClassDefFoundError)(using Context): Option[Symbol] = { | ||
val className = targetException.getMessage | ||
if (className eq null) None | ||
else { | ||
val sym = staticRef(className.toTypeName).symbol | ||
if (sym.isDefinedInCurrentRun) Some(sym) else None | ||
} | ||
def unapply(targetException: Throwable)(using Context): Option[Symbol] = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this because you may end up in a situation where the class defined by the macro annotation does not yet have a definition? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, does not have a definition in bytecode. This can happen when we compile the macro definition and use at the same time. In that case we have the definition as a symbol in the compiler but the bytecode for the macro definition has not been generated yet. |
||
targetException match | ||
case _: NoClassDefFoundError | _: ClassNotFoundException => | ||
val className = targetException.getMessage | ||
if className eq null then None | ||
else | ||
val sym = staticRef(className.toTypeName).symbol | ||
if (sym.isDefinedInCurrentRun) Some(sym) else None | ||
case _ => None | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
package dotty.tools.dotc | ||
package transform | ||
|
||
import scala.language.unsafeNulls | ||
|
||
import dotty.tools.dotc.ast.tpd | ||
import dotty.tools.dotc.ast.Trees.* | ||
import dotty.tools.dotc.config.Printers.{macroAnnot => debug} | ||
import dotty.tools.dotc.core.Annotations.* | ||
import dotty.tools.dotc.core.Contexts.* | ||
import dotty.tools.dotc.core.Decorators.* | ||
import dotty.tools.dotc.core.DenotTransformers.DenotTransformer | ||
import dotty.tools.dotc.core.Flags.* | ||
import dotty.tools.dotc.core.MacroClassLoader | ||
import dotty.tools.dotc.core.Symbols.* | ||
import dotty.tools.dotc.quoted.* | ||
import dotty.tools.dotc.util.SrcPos | ||
import scala.quoted.runtime.impl.{QuotesImpl, SpliceScope} | ||
|
||
import scala.quoted.Quotes | ||
|
||
class MacroAnnotations(thisPhase: DenotTransformer): | ||
import tpd.* | ||
import MacroAnnotations.* | ||
|
||
/** Expands every macro annotation that is on this tree. | ||
* Returns a list with transformed definition and any added definitions. | ||
*/ | ||
def expandAnnotations(tree: MemberDef)(using Context): List[DefTree] = | ||
if !hasMacroAnnotation(tree.symbol) then | ||
List(tree) | ||
else if tree.symbol.is(Module) then | ||
if tree.symbol.isClass then // error only reported on module class | ||
report.error("macro annotations are not supported on object", tree) | ||
List(tree) | ||
else if tree.symbol.isClass then | ||
report.error("macro annotations are not supported on class", tree) | ||
List(tree) | ||
else if tree.symbol.isType then | ||
report.error("macro annotations are not supported on type", tree) | ||
List(tree) | ||
else | ||
debug.println(i"Expanding macro annotations of:\n$tree") | ||
|
||
val macroInterpreter = new Interpreter(tree.srcPos, MacroClassLoader.fromContext) | ||
|
||
val allTrees = List.newBuilder[DefTree] | ||
var insertedAfter: List[List[DefTree]] = Nil | ||
|
||
// Apply all macro annotation to `tree` and collect new definitions in order | ||
val transformedTree: DefTree = tree.symbol.annotations.foldLeft(tree) { (tree, annot) => | ||
if isMacroAnnotation(annot) then | ||
debug.println(i"Expanding macro annotation: ${annot}") | ||
|
||
// Interpret call to `new myAnnot(..).transform(using <Quotes>)(<tree>)` | ||
val transformedTrees = callMacro(macroInterpreter, tree, annot) | ||
transformedTrees.span(_.symbol != tree.symbol) match | ||
case (prefixed, newTree :: suffixed) => | ||
allTrees ++= prefixed | ||
insertedAfter = suffixed :: insertedAfter | ||
prefixed.foreach(checkAndEnter(_, tree.symbol, annot)) | ||
suffixed.foreach(checkAndEnter(_, tree.symbol, annot)) | ||
newTree | ||
case (Nil, Nil) => | ||
report.error(i"Unexpected `Nil` returned by `(${annot.tree}).transform(..)` during macro expansion", annot.tree.srcPos) | ||
tree | ||
case (_, Nil) => | ||
report.error(i"Transformed tree for ${tree} was not return by `(${annot.tree}).transform(..)` during macro expansion", annot.tree.srcPos) | ||
tree | ||
else | ||
tree | ||
} | ||
|
||
allTrees += transformedTree | ||
insertedAfter.foreach(allTrees.++=) | ||
|
||
val result = allTrees.result() | ||
debug.println(result.map(_.show).mkString("expanded to:\n", "\n", "")) | ||
result | ||
|
||
/** Interpret the code `new annot(..).transform(using <Quotes(ctx)>)(<tree>)` */ | ||
private def callMacro(interpreter: Interpreter, tree: MemberDef, annot: Annotation)(using Context): List[MemberDef] = | ||
// TODO: Remove when scala.annaotaion.MacroAnnotation is no longer experimental | ||
import scala.reflect.Selectable.reflectiveSelectable | ||
type MacroAnnotation = { | ||
def transform(using Quotes)(tree: Object/*Erased type of quotes.refelct.Definition*/): List[MemberDef /*quotes.refelct.Definition known to be MemberDef in QuotesImpl*/] | ||
} | ||
|
||
// Interpret macro annotation instantiation `new myAnnot(..)` | ||
val annotInstance = interpreter.interpret[MacroAnnotation](annot.tree).get | ||
// TODO: Remove when scala.annaotaion.MacroAnnotation is no longer experimental | ||
assert(annotInstance.getClass.getClassLoader.loadClass("scala.annotation.MacroAnnotation").isInstance(annotInstance)) | ||
|
||
val quotes = QuotesImpl()(using SpliceScope.contextWithNewSpliceScope(tree.symbol.sourcePos)(using MacroExpansion.context(tree)).withOwner(tree.symbol)) | ||
annotInstance.transform(using quotes)(tree.asInstanceOf[quotes.reflect.Definition]) | ||
|
||
/** Check that this tree can be added by the macro annotation and enter it if needed */ | ||
private def checkAndEnter(newTree: Tree, annotated: Symbol, annot: Annotation)(using Context) = | ||
val sym = newTree.symbol | ||
if sym.isClass then | ||
report.error("Generating classes is not supported", annot.tree) | ||
else if sym.isType then | ||
report.error("Generating type is not supported", annot.tree) | ||
else if sym.owner != annotated.owner then | ||
report.error(i"macro annotation $annot added $sym with an inconsistent owner. Expected it to be owned by ${annotated.owner} but was owned by ${sym.owner}.", annot.tree) | ||
else | ||
sym.enteredAfter(thisPhase) | ||
|
||
object MacroAnnotations: | ||
|
||
/** Is this an annotation that implements `scala.annation.MacroAnnotation` */ | ||
def isMacroAnnotation(annot: Annotation)(using Context): Boolean = | ||
annot.tree.symbol.maybeOwner.derivesFrom(defn.MacroAnnotationClass) | ||
|
||
/** Is this symbol annotated with an annotation that implements `scala.annation.MacroAnnotation` */ | ||
def hasMacroAnnotation(sym: Symbol)(using Context): Boolean = | ||
sym.getAnnotation(defn.MacroAnnotationClass).isDefined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Background knowledge question - other than
SpliceInterpreter
in Splicer.scala, prior to this PR, was this class used anywhere?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The splice interpreter was the only use of this class. This class was factored out in a separate PR to make this PR simpler to review.