Skip to content

Commit be10bc6

Browse files
authored
Enable returning classes from MacroAnnotations (part 3) (scala#16534)
Enable the addition of classes from a `MacroAnnotation`: * Can add new `class`/`object` definitions next to the annotated definition Special cases: * An annotated top-level `def`, `val`, `var`, `lazy val` can return a `class`/`object` definition that is owned by the package or package object. Related PRs: * Follows scala#16454
2 parents 80e8365 + 6c6dc77 commit be10bc6

File tree

32 files changed

+653
-31
lines changed

32 files changed

+653
-31
lines changed

compiler/src/dotty/tools/dotc/core/Symbols.scala

+26
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,32 @@ object Symbols {
630630
owner.thisType, modcls, parents, decls, TermRef(owner.thisType, module)),
631631
privateWithin, coord, assocFile)
632632

633+
/** Same as `newCompleteModuleSymbol` except that `parents` can be a list of arbitrary
634+
* types which get normalized into type refs and parameter bindings.
635+
*/
636+
def newNormalizedModuleSymbol(
637+
owner: Symbol,
638+
name: TermName,
639+
modFlags: FlagSet,
640+
clsFlags: FlagSet,
641+
parentTypes: List[Type],
642+
decls: Scope,
643+
privateWithin: Symbol = NoSymbol,
644+
coord: Coord = NoCoord,
645+
assocFile: AbstractFile | Null = null)(using Context): TermSymbol = {
646+
def completer(module: Symbol) = new LazyType {
647+
def complete(denot: SymDenotation)(using Context): Unit = {
648+
val cls = denot.asClass.classSymbol
649+
val decls = newScope
650+
denot.info = ClassInfo(owner.thisType, cls, parentTypes.map(_.dealias), decls, TermRef(owner.thisType, module))
651+
}
652+
}
653+
newModuleSymbol(
654+
owner, name, modFlags, clsFlags,
655+
(module, modcls) => completer(module),
656+
privateWithin, coord, assocFile)
657+
}
658+
633659
/** Create a package symbol with associated package class
634660
* from its non-info fields and a lazy type for loading the package's members.
635661
*/

compiler/src/dotty/tools/dotc/transform/Inlining.scala

+29-4
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ import dotty.tools.dotc.inlines.Inlines
1414
import dotty.tools.dotc.ast.TreeMapWithImplicits
1515
import dotty.tools.dotc.core.DenotTransformers.IdentityDenotTransformer
1616

17+
import scala.collection.mutable.ListBuffer
1718

1819
/** Inlines all calls to inline methods that are not in an inline method or a quote */
19-
class Inlining extends MacroTransform with IdentityDenotTransformer {
20-
thisPhase =>
20+
class Inlining extends MacroTransform {
2121

2222
import tpd._
2323

@@ -63,6 +63,12 @@ class Inlining extends MacroTransform with IdentityDenotTransformer {
6363
}
6464

6565
private class InliningTreeMap extends TreeMapWithImplicits {
66+
67+
/** List of top level classes added by macro annotation in a package object.
68+
* These are added to the PackageDef that owns this particular package object.
69+
*/
70+
private val newTopClasses = MutableSymbolMap[ListBuffer[Tree]]()
71+
6672
override def transform(tree: Tree)(using Context): Tree = {
6773
tree match
6874
case tree: MemberDef =>
@@ -73,8 +79,17 @@ class Inlining extends MacroTransform with IdentityDenotTransformer {
7379
&& StagingContext.level == 0
7480
&& MacroAnnotations.hasMacroAnnotation(tree.symbol)
7581
then
76-
val trees = new MacroAnnotations(thisPhase).expandAnnotations(tree)
77-
flatTree(trees.map(super.transform))
82+
val trees = (new MacroAnnotations).expandAnnotations(tree)
83+
val trees1 = trees.map(super.transform)
84+
85+
// Find classes added to the top level from a package object
86+
val (topClasses, trees2) =
87+
if ctx.owner.isPackageObject then trees1.partition(_.symbol.owner == ctx.owner.owner)
88+
else (Nil, trees1)
89+
if topClasses.nonEmpty then
90+
newTopClasses.getOrElseUpdate(ctx.owner.owner, new ListBuffer) ++= topClasses
91+
92+
flatTree(trees2)
7893
else super.transform(tree)
7994
case _: Typed | _: Block =>
8095
super.transform(tree)
@@ -86,6 +101,16 @@ class Inlining extends MacroTransform with IdentityDenotTransformer {
86101
super.transform(tree)(using StagingContext.quoteContext)
87102
case _: GenericApply if tree.symbol.isExprSplice =>
88103
super.transform(tree)(using StagingContext.spliceContext)
104+
case _: PackageDef =>
105+
super.transform(tree) match
106+
case tree1: PackageDef =>
107+
newTopClasses.get(tree.symbol.moduleClass) match
108+
case Some(topClasses) =>
109+
newTopClasses.remove(tree.symbol.moduleClass)
110+
val newStats = tree1.stats ::: topClasses.result()
111+
cpy.PackageDef(tree1)(tree1.pid, newStats)
112+
case _ => tree1
113+
case tree1 => tree1
89114
case _ =>
90115
super.transform(tree)
91116
}

compiler/src/dotty/tools/dotc/transform/MacroAnnotations.scala

+7-11
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import scala.util.control.NonFatal
2323

2424
import java.lang.reflect.InvocationTargetException
2525

26-
class MacroAnnotations(thisPhase: DenotTransformer):
26+
class MacroAnnotations:
2727
import tpd.*
2828
import MacroAnnotations.*
2929

@@ -82,8 +82,8 @@ class MacroAnnotations(thisPhase: DenotTransformer):
8282
case (prefixed, newTree :: suffixed) =>
8383
allTrees ++= prefixed
8484
insertedAfter = suffixed :: insertedAfter
85-
prefixed.foreach(checkAndEnter(_, tree.symbol, annot))
86-
suffixed.foreach(checkAndEnter(_, tree.symbol, annot))
85+
prefixed.foreach(checkMacroDef(_, tree.symbol, annot))
86+
suffixed.foreach(checkMacroDef(_, tree.symbol, annot))
8787
newTree
8888
case (Nil, Nil) =>
8989
report.error(i"Unexpected `Nil` returned by `(${annot.tree}).transform(..)` during macro expansion", annot.tree.srcPos)
@@ -118,19 +118,15 @@ class MacroAnnotations(thisPhase: DenotTransformer):
118118
val quotes = QuotesImpl()(using SpliceScope.contextWithNewSpliceScope(tree.symbol.sourcePos)(using MacroExpansion.context(tree)).withOwner(tree.symbol.owner))
119119
annotInstance.transform(using quotes)(tree.asInstanceOf[quotes.reflect.Definition])
120120

121-
/** Check that this tree can be added by the macro annotation and enter it if needed */
122-
private def checkAndEnter(newTree: Tree, annotated: Symbol, annot: Annotation)(using Context) =
121+
/** Check that this tree can be added by the macro annotation */
122+
private def checkMacroDef(newTree: DefTree, annotated: Symbol, annot: Annotation)(using Context) =
123123
val sym = newTree.symbol
124-
if sym.isClass then
125-
report.error(i"macro annotation returning a `class` is not yet supported. $annot tried to add $sym", annot.tree)
126-
else if sym.isType then
124+
if sym.isType && !sym.isClass then
127125
report.error(i"macro annotation cannot return a `type`. $annot tried to add $sym", annot.tree)
128-
else if sym.owner != annotated.owner then
126+
else if sym.owner != annotated.owner && !(annotated.owner.isPackageObject && (sym.isClass || sym.is(Module)) && sym.owner == annotated.owner.owner) then
129127
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)
130128
else if annotated.isClass && annotated.owner.is(Package) /*&& !sym.isClass*/ then
131129
report.error(i"macro annotation can not add top-level ${sym.showKind}. $annot tried to add $sym.", annot.tree)
132-
else
133-
sym.enteredAfter(thisPhase)
134130

135131
object MacroAnnotations:
136132

compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala

+29-5
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ import dotty.tools.dotc.ast.tpd
88
import dotty.tools.dotc.ast.untpd
99
import dotty.tools.dotc.core.Annotations
1010
import dotty.tools.dotc.core.Contexts._
11-
import dotty.tools.dotc.core.Types
11+
import dotty.tools.dotc.core.Decorators._
1212
import dotty.tools.dotc.core.Flags._
1313
import dotty.tools.dotc.core.NameKinds
14+
import dotty.tools.dotc.core.NameOps._
1415
import dotty.tools.dotc.core.StdNames._
15-
import dotty.tools.dotc.quoted.reflect._
16-
import dotty.tools.dotc.core.Decorators._
16+
import dotty.tools.dotc.core.Types
1717
import dotty.tools.dotc.NoCompilationUnit
18-
19-
import dotty.tools.dotc.quoted.{MacroExpansion, PickledQuotes}
18+
import dotty.tools.dotc.quoted.MacroExpansion
19+
import dotty.tools.dotc.quoted.PickledQuotes
20+
import dotty.tools.dotc.quoted.reflect._
2021

2122
import scala.quoted.runtime.{QuoteUnpickler, QuoteMatching}
2223
import scala.quoted.runtime.impl.printers._
@@ -242,6 +243,14 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
242243
def unapply(cdef: ClassDef): (String, DefDef, List[Tree /* Term | TypeTree */], Option[ValDef], List[Statement]) =
243244
val rhs = cdef.rhs.asInstanceOf[tpd.Template]
244245
(cdef.name.toString, cdef.constructor, cdef.parents, cdef.self, rhs.body)
246+
247+
def module(module: Symbol, parents: List[Tree /* Term | TypeTree */], body: List[Statement]): (ValDef, ClassDef) = {
248+
val cls = module.moduleClass
249+
val clsDef = ClassDef(cls, parents, body)
250+
val newCls = Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil)
251+
val modVal = ValDef(module, Some(newCls))
252+
(modVal, clsDef)
253+
}
245254
end ClassDef
246255

247256
given ClassDefMethods: ClassDefMethods with
@@ -2481,6 +2490,21 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
24812490
for sym <- decls(cls) do cls.enter(sym)
24822491
cls
24832492

2493+
def newModule(owner: Symbol, name: String, modFlags: Flags, clsFlags: Flags, parents: List[TypeRepr], decls: Symbol => List[Symbol], privateWithin: Symbol): Symbol =
2494+
assert(parents.nonEmpty && !parents.head.typeSymbol.is(dotc.core.Flags.Trait), "First parent must be a class")
2495+
val mod = dotc.core.Symbols.newNormalizedModuleSymbol(
2496+
owner,
2497+
name.toTermName,
2498+
modFlags | dotc.core.Flags.ModuleValCreationFlags,
2499+
clsFlags | dotc.core.Flags.ModuleClassCreationFlags,
2500+
parents,
2501+
dotc.core.Scopes.newScope,
2502+
privateWithin)
2503+
val cls = mod.moduleClass.asClass
2504+
cls.enter(dotc.core.Symbols.newConstructor(cls, dotc.core.Flags.Synthetic, Nil, Nil))
2505+
for sym <- decls(cls) do cls.enter(sym)
2506+
mod
2507+
24842508
def newMethod(owner: Symbol, name: String, tpe: TypeRepr): Symbol =
24852509
newMethod(owner, name, tpe, Flags.EmptyFlags, noSymbol)
24862510
def newMethod(owner: Symbol, name: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol =

library/src/scala/annotation/MacroAnnotation.scala

+34-10
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,41 @@ package annotation
44

55
import scala.quoted._
66

7-
/** Base trait for macro annotation that will transform a definition */
7+
/** Base trait for macro annotation implementation.
8+
* Macro annotations can transform definitions and add new definitions.
9+
*
10+
* See: `MacroAnnotation.transform`
11+
*
12+
* @syntax markdown
13+
*/
814
@experimental
915
trait MacroAnnotation extends StaticAnnotation:
1016

11-
/** Transform the `tree` definition and add other definitions
17+
/** Transform the `tree` definition and add new definitions
1218
*
1319
* This method takes as argument the annotated definition.
1420
* It returns a non-empty list containing the modified version of the annotated definition.
1521
* The new tree for the definition must use the original symbol.
1622
* New definitions can be added to the list before or after the transformed definitions, this order
17-
* will be retained.
23+
* will be retained. New definitions will not be visible from outside the macro expansion.
1824
*
19-
* All definitions in the result must have the same owner. The owner can be recovered from `tree.symbol.owner`.
25+
* #### Restrictions
26+
* - All definitions in the result must have the same owner. The owner can be recovered from `Symbol.spliceOwner`.
27+
* - Special case: an annotated top-level `def`, `val`, `var`, `lazy val` can return a `class`/`object`
28+
definition that is owned by the package or package object.
29+
* - Can not return a `type`.
30+
* - Annotated top-level `class`/`object` can not return top-level `def`, `val`, `var`, `lazy val`.
31+
* - Can not see new definition in user written code.
2032
*
21-
* The result cannot add new `class`, `object` or `type` definition. This limitation will be relaxed in the future.
33+
* #### Good practices
34+
* - Make your new definitions private if you can.
35+
* - New definitions added as class members should use a fresh name (`Symbol.freshName`) to avoid collisions.
36+
* - New top-level definitions should use a fresh name (`Symbol.freshName`) that includes the name of the annotated
37+
* member as a prefix to avoid collisions of definitions added in other files.
2238
*
23-
* IMPORTANT: When developing and testing a macro annotation, you must enable `-Xcheck-macros` and `-Ycheck:all`.
39+
* **IMPORTANT**: When developing and testing a macro annotation, you must enable `-Xcheck-macros` and `-Ycheck:all`.
2440
*
25-
* Example 1:
41+
* #### Example 1
2642
* This example shows how to modify a `def` and add a `val` next to it using a macro annotation.
2743
* ```scala
2844
* import scala.quoted.*
@@ -54,7 +70,10 @@ trait MacroAnnotation extends StaticAnnotation:
5470
* List(tree)
5571
* ```
5672
* with this macro annotation a user can write
57-
* ```scala sc:nocompile
73+
* ```scala
74+
* //{
75+
* class memoize extends scala.annotation.StaticAnnotation
76+
* //}
5877
* @memoize
5978
* def fib(n: Int): Int =
6079
* println(s"compute fib of $n")
@@ -74,7 +93,7 @@ trait MacroAnnotation extends StaticAnnotation:
7493
* )
7594
* ```
7695
*
77-
* Example 2:
96+
* #### Example 2
7897
* This example shows how to modify a `class` using a macro annotation.
7998
* It shows how to override inherited members and add new ones.
8099
* ```scala
@@ -164,7 +183,10 @@ trait MacroAnnotation extends StaticAnnotation:
164183
* }
165184
* ```
166185
* with this macro annotation a user can write
167-
* ```scala sc:nocompile
186+
* ```scala
187+
* //{
188+
* class equals extends scala.annotation.StaticAnnotation
189+
* //}
168190
* @equals class User(val name: String, val id: Int)
169191
* ```
170192
* and the macro will modify the class definition to generate the following code
@@ -184,5 +206,7 @@ trait MacroAnnotation extends StaticAnnotation:
184206
*
185207
* @param Quotes Implicit instance of Quotes used for tree reflection
186208
* @param tree Tree that will be transformed
209+
*
210+
* @syntax markdown
187211
*/
188212
def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition]

library/src/scala/quoted/Quotes.scala

+84-1
Original file line numberDiff line numberDiff line change
@@ -467,9 +467,33 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
467467
* otherwise the can be `Term` containing the `New` applied to the parameters of the extended class.
468468
* @param body List of members of the class. The members must align with the members of `cls`.
469469
*/
470+
// TODO add selfOpt: Option[ValDef]?
470471
@experimental def apply(cls: Symbol, parents: List[Tree /* Term | TypeTree */], body: List[Statement]): ClassDef
471472
def copy(original: Tree)(name: String, constr: DefDef, parents: List[Tree /* Term | TypeTree */], selfOpt: Option[ValDef], body: List[Statement]): ClassDef
472473
def unapply(cdef: ClassDef): (String, DefDef, List[Tree /* Term | TypeTree */], Option[ValDef], List[Statement])
474+
475+
476+
/** Create the ValDef and ClassDef of a module (equivalent to an `object` declaration in source code).
477+
*
478+
* Equivalent to
479+
* ```
480+
* def module(module: Symbol, parents: List[Tree], body: List[Statement]): (ValDef, ClassDef) =
481+
* val modCls = module.moduleClass
482+
* val modClassDef = ClassDef(modCls, parents, body)
483+
* val modValDef = ValDef(module, Some(Apply(Select(New(TypeIdent(modCls)), cls.primaryConstructor), Nil)))
484+
* List(modValDef, modClassDef)
485+
* ```
486+
*
487+
* @param module the module symbol (created using `Symbol.newModule`)
488+
* @param parents parents of the module class
489+
* @param body body of the module class
490+
* @return The module lazy val definition and module class definition.
491+
* These should be added one after the other (in that order) in the body of a class or statements of a block.
492+
*
493+
* @syntax markdown
494+
*/
495+
// TODO add selfOpt: Option[ValDef]?
496+
@experimental def module(module: Symbol, parents: List[Tree /* Term | TypeTree */], body: List[Statement]): (ValDef, ClassDef)
473497
}
474498

475499
/** Makes extension methods on `ClassDef` available without any imports */
@@ -3638,8 +3662,67 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
36383662
* @note As a macro can only splice code into the point at which it is expanded, all generated symbols must be
36393663
* direct or indirect children of the reflection context's owner.
36403664
*/
3665+
// TODO: add flags and privateWithin
36413666
@experimental def newClass(parent: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr]): Symbol
36423667

3668+
/** Generates a new module symbol with an associated module class symbol,
3669+
* this is equivalent to an `object` declaration in source code.
3670+
* This method returns the module symbol. The module class can be accessed calling `moduleClass` on this symbol.
3671+
*
3672+
* Example usage:
3673+
* ```scala
3674+
* //{
3675+
* given Quotes = ???
3676+
* import quotes.reflect._
3677+
* //}
3678+
* val moduleName: String = Symbol.freshName("MyModule")
3679+
* val parents = List(TypeTree.of[Object])
3680+
* def decls(cls: Symbol): List[Symbol] =
3681+
* List(Symbol.newMethod(cls, "run", MethodType(Nil)(_ => Nil, _ => TypeRepr.of[Unit]), Flags.EmptyFlags, Symbol.noSymbol))
3682+
*
3683+
* val mod = Symbol.newModule(Symbol.spliceOwner, moduleName, Flags.EmptyFlags, Flags.EmptyFlags, parents.map(_.tpe), decls, Symbol.noSymbol)
3684+
* val cls = mod.moduleClass
3685+
* val runSym = cls.declaredMethod("run").head
3686+
*
3687+
* val runDef = DefDef(runSym, _ => Some('{ println("run") }.asTerm))
3688+
* val modDef = ClassDef.module(mod, parents, body = List(runDef))
3689+
*
3690+
* val callRun = Apply(Select(Ref(mod), runSym), Nil)
3691+
*
3692+
* Block(modDef.toList, callRun)
3693+
* ```
3694+
* constructs the equivalent to
3695+
* ```scala
3696+
* //{
3697+
* given Quotes = ???
3698+
* import quotes.reflect._
3699+
* //}
3700+
* '{
3701+
* object MyModule$macro$1 extends Object:
3702+
* def run(): Unit = println("run")
3703+
* MyModule$macro$1.run()
3704+
* }
3705+
* ```
3706+
*
3707+
* @param parent The owner of the class
3708+
* @param name The name of the class
3709+
* @param modFlags extra flags with which the module symbol should be constructed
3710+
* @param clsFlags extra flags with which the module class symbol should be constructed
3711+
* @param parents The parent classes of the class. The first parent must not be a trait.
3712+
* @param decls A function that takes the symbol of the module class as input and return the symbols of its declared members
3713+
* @param privateWithin the symbol within which this new method symbol should be private. May be noSymbol.
3714+
*
3715+
* This symbol starts without an accompanying definition.
3716+
* It is the meta-programmer's responsibility to provide exactly one corresponding definition by passing
3717+
* this symbol to `ClassDef.module`.
3718+
*
3719+
* @note As a macro can only splice code into the point at which it is expanded, all generated symbols must be
3720+
* direct or indirect children of the reflection context's owner.
3721+
*
3722+
* @syntax markdown
3723+
*/
3724+
@experimental def newModule(owner: Symbol, name: String, modFlags: Flags, clsFlags: Flags, parents: List[TypeRepr], decls: Symbol => List[Symbol], privateWithin: Symbol): Symbol
3725+
36433726
/** Generates a new method symbol with the given parent, name and type.
36443727
*
36453728
* To define a member method of a class, use the `newMethod` within the `decls` function of `newClass`.
@@ -4217,7 +4300,7 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
42174300
// FLAGS //
42184301
///////////////
42194302

4220-
/** FlagSet of a Symbol */
4303+
/** Flags of a Symbol */
42214304
type Flags
42224305

42234306
/** Module object of `type Flags` */

0 commit comments

Comments
 (0)