Skip to content

Commit beadb88

Browse files
committed
Macro annotations class modifications (part 2)
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`
1 parent 5a28f42 commit beadb88

File tree

42 files changed

+1003
-138
lines changed

Some content is hidden

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

42 files changed

+1003
-138
lines changed

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

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import dotty.tools.dotc.config.Printers.{macroAnnot => debug}
99
import dotty.tools.dotc.core.Annotations.*
1010
import dotty.tools.dotc.core.Contexts.*
1111
import dotty.tools.dotc.core.Decorators.*
12+
import dotty.tools.dotc.core.Decorators.*
1213
import dotty.tools.dotc.core.DenotTransformers.DenotTransformer
1314
import dotty.tools.dotc.core.Flags.*
1415
import dotty.tools.dotc.core.MacroClassLoader
@@ -29,14 +30,10 @@ class MacroAnnotations(thisPhase: DenotTransformer):
2930
def expandAnnotations(tree: MemberDef)(using Context): List[DefTree] =
3031
if !hasMacroAnnotation(tree.symbol) then
3132
List(tree)
32-
else if tree.symbol.is(Module) then
33-
if tree.symbol.isClass then // error only reported on module class
34-
report.error("macro annotations are not supported on object", tree)
35-
List(tree)
36-
else if tree.symbol.isClass then
37-
report.error("macro annotations are not supported on class", tree)
33+
else if tree.symbol.is(Module) && !tree.symbol.isClass then
34+
// only class is transformed
3835
List(tree)
39-
else if tree.symbol.isType then
36+
else if tree.symbol.isType && !tree.symbol.isClass then
4037
report.error("macro annotations are not supported on type", tree)
4138
List(tree)
4239
else
@@ -70,7 +67,6 @@ class MacroAnnotations(thisPhase: DenotTransformer):
7067
else
7168
tree
7269
}
73-
7470
allTrees += transformedTree
7571
insertedAfter.foreach(allTrees.++=)
7672

@@ -98,11 +94,13 @@ class MacroAnnotations(thisPhase: DenotTransformer):
9894
private def checkAndEnter(newTree: Tree, annotated: Symbol, annot: Annotation)(using Context) =
9995
val sym = newTree.symbol
10096
if sym.isClass then
101-
report.error("Generating classes is not supported", annot.tree)
97+
report.error(i"macro annotation returning a `class` is not yet supported. $annot tried to add $sym", annot.tree)
10298
else if sym.isType then
103-
report.error("Generating type is not supported", annot.tree)
99+
report.error(i"macro annotation cannot return a `type`. $annot tried to add $sym", annot.tree)
104100
else if sym.owner != annotated.owner then
105101
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)
102+
else if annotated.isClass && annotated.owner.is(Package) /*&& !sym.isClass*/ then
103+
report.error(i"macro annotation can not add top-level ${sym.showKind}. $annot tried to add $sym.", annot.tree)
106104
else
107105
sym.enteredAfter(thisPhase)
108106

library/src/scala/annotation/MacroAnnotation.scala

Lines changed: 112 additions & 3 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
@@ -66,6 +67,114 @@ trait MacroAnnotation extends StaticAnnotation:
6667
* )
6768
* ```
6869
*
70+
* Example 2:
71+
* This example shows how to modify a `class` using a macro annotation.
72+
* It shows how to override inherited members and add new ones.
73+
* ```scala
74+
* import scala.annotation.{experimental, MacroAnnotation}
75+
* import scala.quoted.*
76+
*
77+
* @experimental
78+
* class equals extends MacroAnnotation:
79+
* def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
80+
* import quotes.reflect.*
81+
* tree match
82+
* case ClassDef(className, ctr, parents, self, body) =>
83+
* val cls = tree.symbol
84+
*
85+
* val constructorParameters = ctr.paramss.collect { case clause: TermParamClause => clause }
86+
* if constructorParameters.size != 1 || constructorParameters.head.params.isEmpty then
87+
* report.errorAndAbort("@equals class must have a single argument list with at least one argument", ctr.pos)
88+
* def checkNotOverridden(sym: Symbol): Unit =
89+
* if sym.overridingSymbol(cls).exists then
90+
* report.error(s"Cannot override ${sym.name} in a @equals class")
91+
*
92+
* val fields = body.collect {
93+
* case vdef: ValDef if vdef.symbol.flags.is(Flags.ParamAccessor) =>
94+
* Select(This(cls), vdef.symbol).asExpr
95+
* }
96+
*
97+
* val equalsSym = Symbol.requiredMethod("java.lang.Object.equals")
98+
* checkNotOverridden(equalsSym)
99+
* val equalsOverrideSym = Symbol.newMethod(cls, "equals", equalsSym.info, Flags.Override, Symbol.noSymbol)
100+
* def equalsOverrideDefBody(argss: List[List[Tree]]): Option[Term] =
101+
* given Quotes = equalsOverrideSym.asQuotes
102+
* cls.typeRef.asType match
103+
* case '[c] =>
104+
* Some(equalsExpr[c](argss.head.head.asExpr, fields).asTerm)
105+
* val equalsOverrideDef = DefDef(equalsOverrideSym, equalsOverrideDefBody)
106+
*
107+
* val hashSym = Symbol.newUniqueVal(cls, "hash", TypeRepr.of[Int], Flags.Private | Flags.Lazy, Symbol.noSymbol)
108+
* val hashVal = ValDef(hashSym, Some(hashCodeExpr(className, fields)(using hashSym.asQuotes).asTerm))
109+
*
110+
* val hashCodeSym = Symbol.requiredMethod("java.lang.Object.hashCode")
111+
* checkNotOverridden(hashCodeSym)
112+
* val hashCodeOverrideSym = Symbol.newMethod(cls, "hashCode", hashCodeSym.info, Flags.Override, Symbol.noSymbol)
113+
* val hashCodeOverrideDef = DefDef(hashCodeOverrideSym, _ => Some(Ref(hashSym)))
114+
*
115+
* val newBody = equalsOverrideDef :: hashVal :: hashCodeOverrideDef :: body
116+
* List(ClassDef.copy(tree)(className, ctr, parents, self, newBody))
117+
* case _ =>
118+
* report.error("Annotation only supports `class`")
119+
* List(tree)
120+
*
121+
* private def equalsExpr[T: Type](that: Expr[Any], thisFields: List[Expr[Any]])(using Quotes): Expr[Boolean] =
122+
* '{
123+
* $that match
124+
* case that: T @unchecked =>
125+
* ${
126+
* val thatFields: List[Expr[Any]] =
127+
* import quotes.reflect.*
128+
* thisFields.map(field => Select('{that}.asTerm, field.asTerm.symbol).asExpr)
129+
* thisFields.zip(thatFields)
130+
* .map { case (thisField, thatField) => '{ $thisField == $thatField } }
131+
* .reduce { case (pred1, pred2) => '{ $pred1 && $pred2 } }
132+
* }
133+
* case _ => false
134+
* }
135+
*
136+
* private def hashCodeExpr(className: String, thisFields: List[Expr[Any]])(using Quotes): Expr[Int] =
137+
* '{
138+
* var acc: Int = ${ Expr(scala.runtime.Statics.mix(-889275714, className.hashCode)) }
139+
* ${
140+
* Expr.block(
141+
* thisFields.map {
142+
* case '{ $field: Boolean } => '{ if $field then 1231 else 1237 }
143+
* case '{ $field: Byte } => '{ $field.toInt }
144+
* case '{ $field: Char } => '{ $field.toInt }
145+
* case '{ $field: Short } => '{ $field.toInt }
146+
* case '{ $field: Int } => field
147+
* case '{ $field: Long } => '{ scala.runtime.Statics.longHash($field) }
148+
* case '{ $field: Double } => '{ scala.runtime.Statics.doubleHash($field) }
149+
* case '{ $field: Float } => '{ scala.runtime.Statics.floatHash($field) }
150+
* case '{ $field: Null } => '{ 0 }
151+
* case '{ $field: Unit } => '{ 0 }
152+
* case field => '{ scala.runtime.Statics.anyHash($field) }
153+
* }.map(hash => '{ acc = scala.runtime.Statics.mix(acc, $hash) }),
154+
* '{ scala.runtime.Statics.finalizeHash(acc, ${Expr(thisFields.size)}) }
155+
* )
156+
* }
157+
* }
158+
* ```
159+
* with this macro annotation a user can write
160+
* ```scala sc:nocompile
161+
* @equals class User(val name: String, val id: Int)
162+
* ```
163+
* and the macro will modify the class definition to generate the following code
164+
* ```scala
165+
* class User(val name: String, val id: Int):
166+
* override def equals(that: Any): Boolean =
167+
* that match
168+
* case that: User => this.name == that.name && this.id == that.id
169+
* case _ => false
170+
* private lazy val hash$1: Int =
171+
* var acc = 515782504 // scala.runtime.Statics.mix(-889275714, "User".hashCode)
172+
* acc = scala.runtime.Statics.mix(acc, scala.runtime.Statics.anyHash(name))
173+
* acc = scala.runtime.Statics.mix(acc, id)
174+
* scala.runtime.Statics.finalizeHash(acc, 2)
175+
* override def hashCode(): Int = hash$1
176+
* ```
177+
*
69178
* @param Quotes Implicit instance of Quotes used for tree reflection
70179
* @param tree Tree that will be transformed
71180
*/
Lines changed: 78 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,98 @@
11

2-
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:18:6 ---------------------------------------------------------
3-
17 | @error
4-
18 | val vMember: Int = 1 // error
2+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:17:6 ---------------------------------------------------------
3+
16 |@error
4+
17 |class cGlobal // error
5+
|^
6+
|MACRO ERROR
7+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:20:7 ---------------------------------------------------------
8+
19 |@error
9+
20 |object oGlobal // error
10+
|^
11+
|MACRO ERROR
12+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:24:6 ---------------------------------------------------------
13+
23 | @error
14+
24 | val vMember: Int = 1 // error
515
| ^
616
| MACRO ERROR
7-
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:20:11 --------------------------------------------------------
8-
19 | @error
9-
20 | lazy val lvMember: Int = 1 // error
17+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:26:11 --------------------------------------------------------
18+
25 | @error
19+
26 | lazy val lvMember: Int = 1 // error
1020
| ^
1121
| MACRO ERROR
12-
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:22:6 ---------------------------------------------------------
13-
21 | @error
14-
22 | def dMember: Int = 1 // error
22+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:28:6 ---------------------------------------------------------
23+
27 | @error
24+
28 | def dMember: Int = 1 // error
1525
| ^
1626
| MACRO ERROR
17-
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:24:8 ---------------------------------------------------------
18-
23 | @error
19-
24 | given gMember: Int = 1 // error
27+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:30:8 ---------------------------------------------------------
28+
29 | @error
29+
30 | given gMember: Int = 1 // error
2030
| ^
2131
| MACRO ERROR
22-
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:26:8 ---------------------------------------------------------
23-
25 | @error
24-
26 | given gMember2: Num[Int] with // error: object not supported (TODO support)
32+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:32:8 ---------------------------------------------------------
33+
31 | @error
34+
32 | given gMember2: Num[Int] with // error
35+
| ^
36+
| MACRO ERROR
37+
33 | def zero = 0
38+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:35:8 ---------------------------------------------------------
39+
34 | @error
40+
35 | given gMember3(using DummyImplicit): Num[Int] with // error
41+
| ^
42+
| MACRO ERROR
43+
36 | def zero = 0
44+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:39:8 ---------------------------------------------------------
45+
38 | @error
46+
39 | class cMember // error
2547
| ^
26-
| macro annotations are not supported on object
27-
27 | def zero = 0
28-
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:29:8 ---------------------------------------------------------
29-
28 | @error
30-
29 | given gMember3(using DummyImplicit): Num[Int] with // error: class not supported (TODO support)
48+
| MACRO ERROR
49+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:42:9 ---------------------------------------------------------
50+
41 | @error
51+
42 | object oMember // error
3152
| ^
32-
| macro annotations are not supported on class
33-
30 | def zero = 0
34-
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:34:8 ---------------------------------------------------------
35-
33 | @error
36-
34 | val vLocal: Int = 1 // error
53+
| MACRO ERROR
54+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:46:8 ---------------------------------------------------------
55+
45 | @error
56+
46 | val vLocal: Int = 1 // error
3757
| ^
3858
| MACRO ERROR
39-
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:36:13 --------------------------------------------------------
40-
35 | @error
41-
36 | lazy val lvLocal: Int = 1 // error
59+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:48:13 --------------------------------------------------------
60+
47 | @error
61+
48 | lazy val lvLocal: Int = 1 // error
4262
| ^
4363
| MACRO ERROR
44-
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:38:8 ---------------------------------------------------------
45-
37 | @error
46-
38 | def dLocal: Int = 1 // error
64+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:50:8 ---------------------------------------------------------
65+
49 | @error
66+
50 | def dLocal: Int = 1 // error
4767
| ^
4868
| MACRO ERROR
49-
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:40:10 --------------------------------------------------------
50-
39 | @error
51-
40 | given gLocal: Int = 1 // error
69+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:52:10 --------------------------------------------------------
70+
51 | @error
71+
52 | given gLocal: Int = 1 // error
5272
| ^
5373
| MACRO ERROR
54-
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:42:10 --------------------------------------------------------
55-
41 | @error
56-
42 | given gLocal2: Num[Int] with // error: object not supported (TODO support)
74+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:54:10 --------------------------------------------------------
75+
53 | @error
76+
54 | given gLocal2: Num[Int] with // error
5777
| ^
58-
| macro annotations are not supported on object
59-
43 | def zero = 0
60-
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:45:10 --------------------------------------------------------
61-
44 | @error
62-
45 | given gLocal3(using DummyImplicit): Num[Int] with // error: class not supported (TODO support)
78+
| MACRO ERROR
79+
55 | def zero = 0
80+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:57:10 --------------------------------------------------------
81+
56 | @error
82+
57 | given gLocal3(using DummyImplicit): Num[Int] with // error
6383
| ^
64-
| macro annotations are not supported on class
65-
46 | def zero = 0
84+
| MACRO ERROR
85+
58 | def zero = 0
86+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:61:10 --------------------------------------------------------
87+
60 | @error
88+
61 | class cLocal // error
89+
| ^
90+
| MACRO ERROR
91+
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:63:11 --------------------------------------------------------
92+
62 | @error
93+
63 | object oLocal // error
94+
| ^
95+
| MACRO ERROR
6696
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:2:4 ----------------------------------------------------------
6797
1 |@error
6898
2 |val vGlobal: Int = 1 // error
@@ -85,13 +115,13 @@
85115
|MACRO ERROR
86116
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:10:6 ---------------------------------------------------------
87117
9 |@error
88-
10 |given gGlobal2: Num[Int] with // error: object not supported (TODO support)
118+
10 |given gGlobal2: Num[Int] with // error
89119
|^
90-
|macro annotations are not supported on object
120+
|MACRO ERROR
91121
11 | def zero = 0
92122
-- Error: tests/neg-macros/annot-error-annot/Test_2.scala:13:6 ---------------------------------------------------------
93123
12 |@error
94-
13 |given gGlobal3(using DummyImplicit): Num[Int] with // error: class not supported (TODO support)
124+
13 |given gGlobal3(using DummyImplicit): Num[Int] with // error
95125
|^
96-
|macro annotations are not supported on class
126+
|MACRO ERROR
97127
14 | def zero = 0

0 commit comments

Comments
 (0)