Skip to content

Enable returning classes from MacroAnnotations (part 3) #16534

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

Merged
merged 13 commits into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 23 additions & 1 deletion compiler/src/dotty/tools/dotc/transform/Inlining.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ class Inlining extends MacroTransform with IdentityDenotTransformer {
}

private class InliningTreeMap extends TreeMapWithImplicits {

/** List of top level classes added by macro annotation in a package object.
* These are added the PackageDef that owns this particular package object.
*/
private val topClasses = new collection.mutable.ListBuffer[Tree]
Copy link
Member

Choose a reason for hiding this comment

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

Unfortunately I think this isn't sufficient because package objects can be nested:

package foo {
  val x = 1
  package bar {
    val y = 2
  }
}

Instead, maybe the MemberDef case of transform should return a Thicket with the top-level classes, and we should add an extra case to transform to handle the package object module class itself, where we should also return a Thicket with the top-level classes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This use case was considered and works. I added tests for it in tests/run-macros/annot-add-global-class.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that after after post typer the tree is

package foo {
  package bar {
    val y = 2
  }
  val x = 1
}

This implies that nested classes are processed first and the buffer never overlaps and is emptied just after transforming the nested package.

Copy link
Member

Choose a reason for hiding this comment

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

This implies that nested classes are processed first and the buffer never overlaps and is emptied just after transforming the nested package.

This is subtle, so this precondition should be documented in the code (and ideally checked somewhere, in case it breaks)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I found cases where this precondition does not hold. I updated the implementation to handle such cases.


override def transform(tree: Tree)(using Context): Tree = {
tree match
case tree: MemberDef =>
Expand All @@ -74,7 +80,15 @@ class Inlining extends MacroTransform with IdentityDenotTransformer {
&& MacroAnnotations.hasMacroAnnotation(tree.symbol)
then
val trees = new MacroAnnotations(thisPhase).expandAnnotations(tree)
flatTree(trees.map(super.transform))
val trees1 = trees.map(super.transform)

// Find classes added to the top level from a package object
val (topClasses0, trees2) =
if ctx.owner.isPackageObject then trees1.partition(_.symbol.owner == ctx.owner.owner)
else (Nil, trees1)
topClasses ++= topClasses0

flatTree(trees2)
else super.transform(tree)
case _: Typed | _: Block =>
super.transform(tree)
Expand All @@ -86,6 +100,14 @@ class Inlining extends MacroTransform with IdentityDenotTransformer {
super.transform(tree)(using StagingContext.quoteContext)
case _: GenericApply if tree.symbol.isExprSplice =>
super.transform(tree)(using StagingContext.spliceContext)
case _: PackageDef =>
super.transform(tree) match
case tree1: PackageDef if !topClasses.isEmpty =>
topClasses ++= tree1.stats
val newStats = topClasses.result()
topClasses.clear()
cpy.PackageDef(tree1)(tree1.pid, newStats)
case tree1 => tree1
case _ =>
super.transform(tree)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,11 @@ class MacroAnnotations(thisPhase: DenotTransformer):
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) =
private def checkAndEnter(newTree: DefTree, annotated: Symbol, annot: Annotation)(using Context) =
val sym = newTree.symbol
if sym.isClass then
report.error(i"macro annotation returning a `class` is not yet supported. $annot tried to add $sym", annot.tree)
else if sym.isType then
if sym.isType && !sym.isClass then
report.error(i"macro annotation cannot return a `type`. $annot tried to add $sym", annot.tree)
else if sym.owner != annotated.owner then
else if sym.owner != annotated.owner && !(annotated.owner.isPackageObject && (sym.isClass || sym.is(Module)) && sym.owner == annotated.owner.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 if annotated.isClass && annotated.owner.is(Package) /*&& !sym.isClass*/ then
report.error(i"macro annotation can not add top-level ${sym.showKind}. $annot tried to add $sym.", annot.tree)
Expand Down
3 changes: 2 additions & 1 deletion library/src/scala/quoted/Quotes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3638,6 +3638,7 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
* @note As a macro can only splice code into the point at which it is expanded, all generated symbols must be
* direct or indirect children of the reflection context's owner.
*/
// TODO: add flags and privateWithin
@experimental def newClass(parent: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr]): Symbol

/** Generates a new method symbol with the given parent, name and type.
Expand Down Expand Up @@ -4217,7 +4218,7 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
// FLAGS //
///////////////

/** FlagSet of a Symbol */
/** Flags of a Symbol */
type Flags

/** Module object of `type Flags` */
Expand Down
13 changes: 13 additions & 0 deletions tests/neg-macros/annot-mod-top-method-add-top-method/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import scala.annotation.{experimental, MacroAnnotation}
import scala.quoted._
import scala.collection.mutable

@experimental
// Assumes annotation is on top level def or val
class addTopLevelMethodOutsidePackageObject extends MacroAnnotation:
def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
import quotes.reflect._
val methType = MethodType(Nil)(_ => Nil, _ => TypeRepr.of[Int])
val methSym = Symbol.newMethod(Symbol.spliceOwner.owner, Symbol.freshName("toLevelMethod"), methType, Flags.EmptyFlags, Symbol.noSymbol)
val methDef = ValDef(methSym, Some(Literal(IntConstant(1))))
List(methDef, tree)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@addTopLevelMethodOutsidePackageObject // error
def foo = 1

@addTopLevelMethodOutsidePackageObject // error
val bar = 1
12 changes: 12 additions & 0 deletions tests/neg-macros/annot-mod-top-method-add-top-val/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import scala.annotation.{experimental, MacroAnnotation}
import scala.quoted._
import scala.collection.mutable

@experimental
// Assumes annotation is on top level def or val
class addTopLevelValOutsidePackageObject extends MacroAnnotation:
def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
import quotes.reflect._
val valSym = Symbol.newVal(Symbol.spliceOwner.owner, Symbol.freshName("toLevelVal"), TypeRepr.of[Int], Flags.EmptyFlags, Symbol.noSymbol)
val valDef = ValDef(valSym, Some(Literal(IntConstant(1))))
List(valDef, tree)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@addTopLevelValOutsidePackageObject // error
def foo = 1

@addTopLevelValOutsidePackageObject // error
val bar = 1
4 changes: 4 additions & 0 deletions tests/run-macros/annot-add-global-class.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
macro generated main
executed in: Bar$macro$1
macro generated main
executed in: Bar$macro$2
28 changes: 28 additions & 0 deletions tests/run-macros/annot-add-global-class/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import scala.annotation.{experimental, MacroAnnotation}
import scala.quoted._
import scala.collection.mutable

@experimental
class addClass extends MacroAnnotation:
def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
import quotes.reflect._
tree match
case DefDef(name, List(TermParamClause(Nil)), tpt, Some(rhs)) =>
val parents = List(TypeTree.of[Object])
def decls(cls: Symbol): List[Symbol] =
List(Symbol.newMethod(cls, "run", MethodType(Nil)(_ => Nil, _ => TypeRepr.of[Unit]), Flags.EmptyFlags, Symbol.noSymbol))

val newClassName = Symbol.freshName("Bar")
val cls = Symbol.newClass(Symbol.spliceOwner.owner, newClassName, parents = parents.map(_.tpe), decls, selfType = None)
val runSym = cls.declaredMethod("run").head

val runDef = DefDef(runSym, _ => Some(rhs))
val clsDef = ClassDef(cls, parents, body = List(runDef))

val newCls = Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil)

val newDef = DefDef.copy(tree)(name, List(TermParamClause(Nil)), tpt, Some(Apply(Select(newCls, runSym), Nil)))
List(clsDef, newDef)
case _ =>
report.error("Annotation only supports `def` with one argument")
List(tree)
25 changes: 25 additions & 0 deletions tests/run-macros/annot-add-global-class/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@addClass def foo(): Unit =
println("macro generated main")
println("executed in: " + (new Throwable().getStackTrace().head.getClassName))
//> class Baz$macro$1 extends Object {
//> def run() =
//> println("macro generated main")
//> println("executed in: " + getClass.getName)
//> }
//> def foo(): Unit =
//> new Baz$macro$1.run

@addClass def bar(): Unit =
println("macro generated main")
println("executed in: " + (new Throwable().getStackTrace().head.getClassName))
//> class Baz$macro$2 extends Object {
//> def run() =
//> println("macro generated main")
//> println("executed in: " + getClass.getName)
//> }
//> def foo(): Unit =
//> new Baz$macro$2.run

@main def Test(): Unit =
foo()
bar()
4 changes: 4 additions & 0 deletions tests/run-macros/annot-add-local-class.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
macro generated main
executed in: Test_2$package$Baz$1
macro generated main
executed in: Test_2$package$Baz$2
27 changes: 27 additions & 0 deletions tests/run-macros/annot-add-local-class/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import scala.annotation.{experimental, MacroAnnotation}
import scala.quoted._
import scala.collection.mutable

@experimental
class addClass extends MacroAnnotation:
def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
import quotes.reflect._
tree match
case DefDef(name, List(TermParamClause(Nil)), tpt, Some(rhs)) =>
val parents = List(TypeTree.of[Object])
def decls(cls: Symbol): List[Symbol] =
List(Symbol.newMethod(cls, "run", MethodType(Nil)(_ => Nil, _ => TypeRepr.of[Unit]), Flags.EmptyFlags, Symbol.noSymbol))

val cls = Symbol.newClass(Symbol.spliceOwner, "Baz", parents = parents.map(_.tpe), decls, selfType = None)
val runSym = cls.declaredMethod("run").head

val runDef = DefDef(runSym, _ => Some(rhs))
val clsDef = ClassDef(cls, parents, body = List(runDef))

val newCls = Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil)

val newDef = DefDef.copy(tree)(name, List(TermParamClause(Nil)), tpt, Some(Apply(Select(newCls, runSym), Nil)))
List(clsDef, newDef)
case _ =>
report.error("Annotation only supports `def` with one argument")
List(tree)
25 changes: 25 additions & 0 deletions tests/run-macros/annot-add-local-class/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@main def Test(): Unit =
@addClass def foo(): Unit =
println("macro generated main")
println("executed in: " + (new Throwable().getStackTrace().head.getClassName))
//> class Baz extends Object {
//> def run() =
//> println("macro generated main")
//> println("executed in: " + getClass.getName)
//> }
//> def foo(): Unit =
//> new Baz().run

@addClass def bar(): Unit =
println("macro generated main")
println("executed in: " + (new Throwable().getStackTrace().head.getClassName))
//> class Baz extends Object {
//> def run() =
//> println("macro generated main")
//> println("executed in: " + getClass.getName)
//> }
//> def Baz(): Unit =
//> new Baz().run

foo()
bar()
4 changes: 4 additions & 0 deletions tests/run-macros/annot-add-local-object.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
macro generated main
executed in: Test_2$package$Baz$2
macro generated main
executed in: Test_2$package$Baz$4
32 changes: 32 additions & 0 deletions tests/run-macros/annot-add-local-object/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import scala.annotation.{experimental, MacroAnnotation}
import scala.quoted._
import scala.collection.mutable

@experimental
class addClass extends MacroAnnotation:
def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
import quotes.reflect._
tree match
case DefDef(name, List(TermParamClause(Nil)), tpt, Some(rhs)) =>
val parents = List(TypeTree.of[Object])
def decls(cls: Symbol): List[Symbol] =
List(Symbol.newMethod(cls, "run", MethodType(Nil)(_ => Nil, _ => TypeRepr.of[Unit]), Flags.Static, Symbol.noSymbol))

// FIXME: missing flags: Final | Module
// FIXME: how to set the self type?
val cls = Symbol.newClass(Symbol.spliceOwner, "Baz", parents = parents.map(_.tpe), decls, selfType = None)
val mod = Symbol.newVal(Symbol.spliceOwner, "Baz", cls.typeRef, Flags.Module | Flags.Lazy | Flags.Final, Symbol.noSymbol)
val runSym = cls.declaredMethod("run").head

val runDef = DefDef(runSym, _ => Some(rhs))

val clsDef = ClassDef(cls, parents, body = List(runDef))

val newCls = Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil)
val modVal = ValDef(mod, Some(newCls))

val newDef = DefDef.copy(tree)(name, List(TermParamClause(Nil)), tpt, Some(Apply(Select(Ref(mod), runSym), Nil)))
List(modVal, clsDef, newDef)
case _ =>
report.error("Annotation only supports `def` with one argument")
List(tree)
25 changes: 25 additions & 0 deletions tests/run-macros/annot-add-local-object/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@main def Test(): Unit =
@addClass def foo(): Unit =
println("macro generated main")
println("executed in: " + (new Throwable().getStackTrace().head.getClassName))
//> object Baz {
//> def run() =
//> println("macro generated main")
//> println("executed in: " + getClass.getName)
//> }
//> def foo(): Unit =
//> Baz.run

@addClass def bar(): Unit =
println("macro generated main")
println("executed in: " + (new Throwable().getStackTrace().head.getClassName))
//> object Baz {
//> def run() =
//> println("macro generated main")
//> println("executed in: " + getClass.getName)
//> }
//> def Baz(): Unit =
//> Baz.run

foo()
bar()
4 changes: 4 additions & 0 deletions tests/run-macros/annot-add-nested-class.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
macro generated main
executed in: Foo$Bar$macro$1
macro generated main
executed in: Foo$Bar$macro$2
28 changes: 28 additions & 0 deletions tests/run-macros/annot-add-nested-class/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import scala.annotation.{experimental, MacroAnnotation}
import scala.quoted._
import scala.collection.mutable

@experimental
class addClass extends MacroAnnotation:
def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
import quotes.reflect._
tree match
case DefDef(name, List(TermParamClause(Nil)), tpt, Some(rhs)) =>
val parents = List(TypeTree.of[Object])
def decls(cls: Symbol): List[Symbol] =
List(Symbol.newMethod(cls, "run", MethodType(Nil)(_ => Nil, _ => TypeRepr.of[Unit]), Flags.EmptyFlags, Symbol.noSymbol))

val newClassName = Symbol.freshName("Bar")
val cls = Symbol.newClass(Symbol.spliceOwner, newClassName, parents = parents.map(_.tpe), decls, selfType = None)
val runSym = cls.declaredMethod("run").head

val runDef = DefDef(runSym, _ => Some(rhs))
val clsDef = ClassDef(cls, parents, body = List(runDef))

val newCls = Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil)

val newDef = DefDef.copy(tree)(name, List(TermParamClause(Nil)), tpt, Some(Apply(Select(newCls, runSym), Nil)))
List(clsDef, newDef)
case _ =>
report.error("Annotation only supports `def` with one argument")
List(tree)
26 changes: 26 additions & 0 deletions tests/run-macros/annot-add-nested-class/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class Foo():
@addClass def foo(): Unit =
println("macro generated main")
println("executed in: " + (new Throwable().getStackTrace().head.getClassName))
//> class Baz$macro$1 extends Object {
//> def run() =
//> println("macro generated main")
//> println("executed in: " + getClass.getName)
//> }
//> def foo(): Unit =
//> new Baz$macro$1.run

@addClass def bar(): Unit =
println("macro generated main")
println("executed in: " + (new Throwable().getStackTrace().head.getClassName))
//> class Baz$macro$2 extends Object {
//> def run() =
//> println("macro generated main")
//> println("executed in: " + getClass.getName)
//> }
//> def foo(): Unit =
//> new Baz$macro$2.run

@main def Test(): Unit =
new Foo().foo()
new Foo().bar()
1 change: 1 addition & 0 deletions tests/run-macros/annot-macro-main.check
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
macro generated main
24 changes: 24 additions & 0 deletions tests/run-macros/annot-macro-main/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import scala.annotation.{experimental, MacroAnnotation}
import scala.quoted._
import scala.collection.mutable

@experimental
class mainMacro extends MacroAnnotation:
def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
import quotes.reflect._
tree match
case DefDef(name, List(TermParamClause(Nil)), _, _) =>
val parents = List(TypeTree.of[Object])
def decls(cls: Symbol): List[Symbol] =
List(Symbol.newMethod(cls, "main", MethodType(List("args"))(_ => List(TypeRepr.of[Array[String]]), _ => TypeRepr.of[Unit]), Flags.Static, Symbol.noSymbol))

val cls = Symbol.newClass(Symbol.spliceOwner.owner, name, parents = parents.map(_.tpe), decls, selfType = None)
val mainSym = cls.declaredMethod("main").head

val mainDef = DefDef(mainSym, _ => Some(Apply(Ref(tree.symbol), Nil)))
val clsDef = ClassDef(cls, parents, body = List(mainDef))

List(clsDef, tree)
case _ =>
report.error("Annotation only supports `def` without arguments")
List(tree)
1 change: 1 addition & 0 deletions tests/run-macros/annot-macro-main/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@mainMacro def Test(): Unit = println("macro generated main")