Skip to content

Commit 0555491

Browse files
Macro annotations class modifications (part 2) (#16454)
Enable modification of classes with `MacroAnnotation`: * Can annotate `class` to transform it * Can annotate `object` to transform the companion class Supported class modifications: * Modify the implementations of `def`, `val`, `var`, `lazy val`, `class`, `object` in the class * Add new `def`, `val`, `var`, `lazy val`, `class`, `object` members to the class * Add a new override for a `def`, `val`, `var`, `lazy val` members in the class Restrictions: * An annotation on a top-level class cannot return a top-level `def`, `val`, `var`, `lazy val`. Related PRs: * Includes #16513 * Follows #16392 * Followed by #16534
2 parents 4b0fe4d + 52c9461 commit 0555491

File tree

58 files changed

+1143
-195
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1143
-195
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ object NameKinds {
300300
val UniqueInlineName: UniqueNameKind = new UniqueNameKind("$i")
301301
val InlineScrutineeName: UniqueNameKind = new UniqueNameKind("$scrutinee")
302302
val InlineBinderName: UniqueNameKind = new UniqueNameKind("$proxy")
303+
val MacroNames: UniqueNameKind = new UniqueNameKind("$macro$")
303304

304305
/** A kind of unique extension methods; Unlike other unique names, these can be
305306
* unmangled.

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,10 @@ class MacroAnnotations(thisPhase: DenotTransformer):
3333
def expandAnnotations(tree: MemberDef)(using Context): List[DefTree] =
3434
if !hasMacroAnnotation(tree.symbol) then
3535
List(tree)
36-
else if tree.symbol.is(Module) then
37-
if tree.symbol.isClass then // error only reported on module class
38-
report.error("macro annotations are not supported on object", tree)
36+
else if tree.symbol.is(Module) && !tree.symbol.isClass then
37+
// only class is transformed
3938
List(tree)
40-
else if tree.symbol.isClass then
41-
report.error("macro annotations are not supported on class", tree)
42-
List(tree)
43-
else if tree.symbol.isType then
39+
else if tree.symbol.isType && !tree.symbol.isClass then
4440
report.error("macro annotations are not supported on type", tree)
4541
List(tree)
4642
else
@@ -126,11 +122,13 @@ class MacroAnnotations(thisPhase: DenotTransformer):
126122
private def checkAndEnter(newTree: Tree, annotated: Symbol, annot: Annotation)(using Context) =
127123
val sym = newTree.symbol
128124
if sym.isClass then
129-
report.error("Generating classes is not supported", annot.tree)
125+
report.error(i"macro annotation returning a `class` is not yet supported. $annot tried to add $sym", annot.tree)
130126
else if sym.isType then
131-
report.error("Generating type is not supported", annot.tree)
127+
report.error(i"macro annotation cannot return a `type`. $annot tried to add $sym", annot.tree)
132128
else if sym.owner != annotated.owner then
133129
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)
130+
else if annotated.isClass && annotated.owner.is(Package) /*&& !sym.isClass*/ then
131+
report.error(i"macro annotation can not add top-level ${sym.showKind}. $annot tried to add $sym.", annot.tree)
134132
else
135133
sym.enteredAfter(thisPhase)
136134

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2485,17 +2485,14 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
24852485
newMethod(owner, name, tpe, Flags.EmptyFlags, noSymbol)
24862486
def newMethod(owner: Symbol, name: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol =
24872487
dotc.core.Symbols.newSymbol(owner, name.toTermName, flags | dotc.core.Flags.Method, tpe, privateWithin)
2488-
def newUniqueMethod(owner: Symbol, namePrefix: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol =
2489-
val name = NameKinds.UniqueName.fresh(namePrefix.toTermName)
2490-
dotc.core.Symbols.newSymbol(owner, name, dotc.core.Flags.PrivateMethod | flags, tpe, privateWithin)
24912488
def newVal(owner: Symbol, name: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol =
24922489
dotc.core.Symbols.newSymbol(owner, name.toTermName, flags, tpe, privateWithin)
2493-
def newUniqueVal(owner: Symbol, namePrefix: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol =
2494-
val name = NameKinds.UniqueName.fresh(namePrefix.toTermName)
2495-
dotc.core.Symbols.newSymbol(owner, name, flags, tpe, privateWithin)
24962490
def newBind(owner: Symbol, name: String, flags: Flags, tpe: TypeRepr): Symbol =
24972491
dotc.core.Symbols.newSymbol(owner, name.toTermName, flags | Case, tpe)
24982492
def noSymbol: Symbol = dotc.core.Symbols.NoSymbol
2493+
2494+
def freshName(prefix: String): String =
2495+
NameKinds.MacroNames.fresh(prefix.toTermName).toString
24992496
end Symbol
25002497

25012498
given SymbolMethods: SymbolMethods with
@@ -2519,6 +2516,8 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
25192516
def name: String = self.denot.name.toString
25202517
def fullName: String = self.denot.fullName.toString
25212518

2519+
def info: TypeRepr = self.denot.info
2520+
25222521
def pos: Option[Position] =
25232522
if self.exists then Some(self.sourcePos) else None
25242523

library/src/scala/annotation/MacroAnnotation.scala

Lines changed: 116 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ trait MacroAnnotation extends StaticAnnotation:
1818
*
1919
* All definitions in the result must have the same owner. The owner can be recovered from `tree.symbol.owner`.
2020
*
21-
* The result cannot contain `class`, `object` or `type` definition. This limitation will be relaxed in the future.
21+
* The result cannot add new `class`, `object` or `type` definition. This limitation will be relaxed in the future.
2222
*
23-
* When developing and testing a macro annotation, you must enable `-Xcheck-macros` and `-Ycheck:all`.
23+
* IMPORTANT: When developing and testing a macro annotation, you must enable `-Xcheck-macros` and `-Ycheck:all`.
2424
*
25-
* Example:
25+
* Example 1:
26+
* This example shows how to modify a `def` and add a `val` next to it using a macro annotation.
2627
* ```scala
2728
* import scala.quoted.*
2829
* import scala.collection.mutable
@@ -34,7 +35,8 @@ trait MacroAnnotation extends StaticAnnotation:
3435
* case DefDef(name, TermParamClause(param :: Nil) :: Nil, tpt, Some(rhsTree)) =>
3536
* (param.tpt.tpe.asType, tpt.tpe.asType) match
3637
* case ('[t], '[u]) =>
37-
* val cacheSymbol = Symbol.newUniqueVal(Symbol.spliceOwner, name + "Cache", TypeRepr.of[mutable.Map[t, u]], Flags.Private, Symbol.noSymbol)
38+
* val cacheName = Symbol.freshName(name + "Cache")
39+
* val cacheSymbol = Symbol.newVal(Symbol.spliceOwner, cacheName, TypeRepr.of[mutable.Map[t, u]], Flags.Private, Symbol.noSymbol)
3840
* val cacheRhs =
3941
* given Quotes = cacheSymbol.asQuotes
4042
* '{ mutable.Map.empty[t, u] }.asTerm
@@ -60,10 +62,10 @@ trait MacroAnnotation extends StaticAnnotation:
6062
* ```
6163
* and the macro will modify the definition to create
6264
* ```scala
63-
* val fibCache =
65+
* val fibCache$macro$1 =
6466
* scala.collection.mutable.Map.empty[Int, Int]
6567
* def fib(n: Int): Int =
66-
* fibCache.getOrElseUpdate(
68+
* fibCache$macro$1.getOrElseUpdate(
6769
* n,
6870
* {
6971
* println(s"compute fib of $n")
@@ -72,6 +74,114 @@ trait MacroAnnotation extends StaticAnnotation:
7274
* )
7375
* ```
7476
*
77+
* Example 2:
78+
* This example shows how to modify a `class` using a macro annotation.
79+
* It shows how to override inherited members and add new ones.
80+
* ```scala
81+
* import scala.annotation.{experimental, MacroAnnotation}
82+
* import scala.quoted.*
83+
*
84+
* @experimental
85+
* class equals extends MacroAnnotation:
86+
* def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
87+
* import quotes.reflect.*
88+
* tree match
89+
* case ClassDef(className, ctr, parents, self, body) =>
90+
* val cls = tree.symbol
91+
*
92+
* val constructorParameters = ctr.paramss.collect { case clause: TermParamClause => clause }
93+
* if constructorParameters.size != 1 || constructorParameters.head.params.isEmpty then
94+
* report.errorAndAbort("@equals class must have a single argument list with at least one argument", ctr.pos)
95+
* def checkNotOverridden(sym: Symbol): Unit =
96+
* if sym.overridingSymbol(cls).exists then
97+
* report.error(s"Cannot override ${sym.name} in a @equals class")
98+
*
99+
* val fields = body.collect {
100+
* case vdef: ValDef if vdef.symbol.flags.is(Flags.ParamAccessor) =>
101+
* Select(This(cls), vdef.symbol).asExpr
102+
* }
103+
*
104+
* val equalsSym = Symbol.requiredMethod("java.lang.Object.equals")
105+
* checkNotOverridden(equalsSym)
106+
* val equalsOverrideSym = Symbol.newMethod(cls, "equals", equalsSym.info, Flags.Override, Symbol.noSymbol)
107+
* def equalsOverrideDefBody(argss: List[List[Tree]]): Option[Term] =
108+
* given Quotes = equalsOverrideSym.asQuotes
109+
* cls.typeRef.asType match
110+
* case '[c] =>
111+
* Some(equalsExpr[c](argss.head.head.asExpr, fields).asTerm)
112+
* val equalsOverrideDef = DefDef(equalsOverrideSym, equalsOverrideDefBody)
113+
*
114+
* val hashSym = Symbol.newVal(cls, Symbol.freshName("hash"), TypeRepr.of[Int], Flags.Private | Flags.Lazy, Symbol.noSymbol)
115+
* val hashVal = ValDef(hashSym, Some(hashCodeExpr(className, fields)(using hashSym.asQuotes).asTerm))
116+
*
117+
* val hashCodeSym = Symbol.requiredMethod("java.lang.Object.hashCode")
118+
* checkNotOverridden(hashCodeSym)
119+
* val hashCodeOverrideSym = Symbol.newMethod(cls, "hashCode", hashCodeSym.info, Flags.Override, Symbol.noSymbol)
120+
* val hashCodeOverrideDef = DefDef(hashCodeOverrideSym, _ => Some(Ref(hashSym)))
121+
*
122+
* val newBody = equalsOverrideDef :: hashVal :: hashCodeOverrideDef :: body
123+
* List(ClassDef.copy(tree)(className, ctr, parents, self, newBody))
124+
* case _ =>
125+
* report.error("Annotation only supports `class`")
126+
* List(tree)
127+
*
128+
* private def equalsExpr[T: Type](that: Expr[Any], thisFields: List[Expr[Any]])(using Quotes): Expr[Boolean] =
129+
* '{
130+
* $that match
131+
* case that: T @unchecked =>
132+
* ${
133+
* val thatFields: List[Expr[Any]] =
134+
* import quotes.reflect.*
135+
* thisFields.map(field => Select('{that}.asTerm, field.asTerm.symbol).asExpr)
136+
* thisFields.zip(thatFields)
137+
* .map { case (thisField, thatField) => '{ $thisField == $thatField } }
138+
* .reduce { case (pred1, pred2) => '{ $pred1 && $pred2 } }
139+
* }
140+
* case _ => false
141+
* }
142+
*
143+
* private def hashCodeExpr(className: String, thisFields: List[Expr[Any]])(using Quotes): Expr[Int] =
144+
* '{
145+
* var acc: Int = ${ Expr(scala.runtime.Statics.mix(-889275714, className.hashCode)) }
146+
* ${
147+
* Expr.block(
148+
* thisFields.map {
149+
* case '{ $field: Boolean } => '{ if $field then 1231 else 1237 }
150+
* case '{ $field: Byte } => '{ $field.toInt }
151+
* case '{ $field: Char } => '{ $field.toInt }
152+
* case '{ $field: Short } => '{ $field.toInt }
153+
* case '{ $field: Int } => field
154+
* case '{ $field: Long } => '{ scala.runtime.Statics.longHash($field) }
155+
* case '{ $field: Double } => '{ scala.runtime.Statics.doubleHash($field) }
156+
* case '{ $field: Float } => '{ scala.runtime.Statics.floatHash($field) }
157+
* case '{ $field: Null } => '{ 0 }
158+
* case '{ $field: Unit } => '{ 0 }
159+
* case field => '{ scala.runtime.Statics.anyHash($field) }
160+
* }.map(hash => '{ acc = scala.runtime.Statics.mix(acc, $hash) }),
161+
* '{ scala.runtime.Statics.finalizeHash(acc, ${Expr(thisFields.size)}) }
162+
* )
163+
* }
164+
* }
165+
* ```
166+
* with this macro annotation a user can write
167+
* ```scala sc:nocompile
168+
* @equals class User(val name: String, val id: Int)
169+
* ```
170+
* and the macro will modify the class definition to generate the following code
171+
* ```scala
172+
* class User(val name: String, val id: Int):
173+
* override def equals(that: Any): Boolean =
174+
* that match
175+
* case that: User => this.name == that.name && this.id == that.id
176+
* case _ => false
177+
* private lazy val hash$macro$1: Int =
178+
* var acc = 515782504 // scala.runtime.Statics.mix(-889275714, "User".hashCode)
179+
* acc = scala.runtime.Statics.mix(acc, scala.runtime.Statics.anyHash(name))
180+
* acc = scala.runtime.Statics.mix(acc, id)
181+
* scala.runtime.Statics.finalizeHash(acc, 2)
182+
* override def hashCode(): Int = hash$macro$1
183+
* ```
184+
*
75185
* @param Quotes Implicit instance of Quotes used for tree reflection
76186
* @param tree Tree that will be transformed
77187
*/

library/src/scala/quoted/Quotes.scala

Lines changed: 17 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3669,25 +3669,6 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
36693669
*/
36703670
def newMethod(parent: Symbol, name: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol
36713671

3672-
/** Generates a new method symbol with the given parent, name and type.
3673-
*
3674-
* To define a member method of a class, use the `newMethod` within the `decls` function of `newClass`.
3675-
*
3676-
* @param parent The owner of the method
3677-
* @param name The name of the method
3678-
* @param tpe The type of the method (MethodType, PolyType, ByNameType)
3679-
* @param flags extra flags to with which the symbol should be constructed
3680-
* @param privateWithin the symbol within which this new method symbol should be private. May be noSymbol.
3681-
*
3682-
* This symbol starts without an accompanying definition.
3683-
* It is the meta-programmer's responsibility to provide exactly one corresponding definition by passing
3684-
* this symbol to the DefDef constructor.
3685-
*
3686-
* @note As a macro can only splice code into the point at which it is expanded, all generated symbols must be
3687-
* direct or indirect children of the reflection context's owner.
3688-
*/
3689-
@experimental def newUniqueMethod(parent: Symbol, namePrefix: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol
3690-
36913672
/** Generates a new val/var/lazy val symbol with the given parent, name and type.
36923673
*
36933674
* This symbol starts without an accompanying definition.
@@ -3706,25 +3687,6 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
37063687
*/
37073688
def newVal(parent: Symbol, name: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol
37083689

3709-
/** Generates a new val/var/lazy val symbol with the given parent, name prefix and type.
3710-
*
3711-
* This symbol starts without an accompanying definition.
3712-
* It is the meta-programmer's responsibility to provide exactly one corresponding definition by passing
3713-
* this symbol to the ValDef constructor.
3714-
*
3715-
* Note: Also see newVal
3716-
* Note: Also see ValDef.let
3717-
*
3718-
* @param parent The owner of the val/var/lazy val
3719-
* @param name The name of the val/var/lazy val
3720-
* @param tpe The type of the val/var/lazy val
3721-
* @param flags extra flags to with which the symbol should be constructed
3722-
* @param privateWithin the symbol within which this new method symbol should be private. May be noSymbol.
3723-
* @note As a macro can only splice code into the point at which it is expanded, all generated symbols must be
3724-
* direct or indirect children of the reflection context's owner.
3725-
*/
3726-
@experimental def newUniqueVal(parent: Symbol, namePrefix: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol
3727-
37283690
/** Generates a pattern bind symbol with the given parent, name and type.
37293691
*
37303692
* This symbol starts without an accompanying definition.
@@ -3742,6 +3704,18 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
37423704

37433705
/** Definition not available */
37443706
def noSymbol: Symbol
3707+
3708+
/** A fresh name for class or member symbol names.
3709+
*
3710+
* Fresh names are constructed using the following format `prefix + "$macro$" + freshIndex`.
3711+
* The `freshIndex` are unique within the current source file.
3712+
*
3713+
* Examples: See `scala.annotation.MacroAnnotation`
3714+
*
3715+
* @param prefix Prefix of the fresh name
3716+
*/
3717+
@experimental
3718+
def freshName(prefix: String): String
37453719
}
37463720

37473721
/** Makes extension methods on `Symbol` available without any imports */
@@ -3772,6 +3746,10 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
37723746
/** The full name of this symbol up to the root package */
37733747
def fullName: String
37743748

3749+
/** Type of the definition */
3750+
@experimental
3751+
def info: TypeRepr
3752+
37753753
/** The position of this symbol */
37763754
def pos: Option[Position]
37773755

@@ -4411,6 +4389,7 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
44114389
end extension
44124390
}
44134391

4392+
44144393
///////////////
44154394
// POSITIONS //
44164395
///////////////

tests/neg-macros/annot-accessIndirect/Macro_1.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import scala.quoted._
55
class hello extends MacroAnnotation {
66
def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
77
import quotes.reflect._
8-
val helloSymbol = Symbol.newUniqueVal(Symbol.spliceOwner, "hello", TypeRepr.of[String], Flags.EmptyFlags, Symbol.noSymbol)
8+
val helloSymbol = Symbol.newVal(Symbol.spliceOwner, Symbol.freshName("hello"), TypeRepr.of[String], Flags.EmptyFlags, Symbol.noSymbol)
99
val helloVal = ValDef(helloSymbol, Some(Literal(StringConstant("Hello, World!"))))
1010
List(helloVal, tree)
1111
}

0 commit comments

Comments
 (0)