Skip to content

Commit 86c6d54

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 5aa558c commit 86c6d54

File tree

42 files changed

+994
-137
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

+994
-137
lines changed

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

+8-9
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
@@ -98,11 +95,13 @@ class MacroAnnotations(thisPhase: DenotTransformer):
9895
private def checkAndEnter(newTree: Tree, annotated: Symbol, annot: Annotation)(using Context) =
9996
val sym = newTree.symbol
10097
if sym.isClass then
101-
report.error("Generating classes is not supported", annot.tree)
98+
report.error(i"macro annotation returning a `class` is not yet supported. $annot tried to add $sym", annot.tree)
10299
else if sym.isType then
103-
report.error("Generating type is not supported", annot.tree)
100+
report.error(i"macro annotation cannot return a `type`. $annot tried to add $sym", annot.tree)
104101
else if sym.owner != annotated.owner then
105102
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)
103+
else if annotated.isClass && annotated.owner.is(Package) /*&& !sym.isClass*/ then
104+
report.error(i"macro annotation can not add top-level ${sym.showKind}. $annot tried to add $sym.", annot.tree)
106105
else
107106
sym.enteredAfter(thisPhase)
108107

library/src/scala/annotation/MacroAnnotation.scala

+112-3
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
@@ -73,6 +74,114 @@ trait MacroAnnotation extends StaticAnnotation:
7374
* )
7475
* ```
7576
*
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+
*
76185
* @param Quotes Implicit instance of Quotes used for tree reflection
77186
* @param tree Tree that will be transformed
78187
*/
+78-48
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

tests/neg-macros/annot-error-annot/Test_2.scala

+23-6
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@ def dGlobal: Int = 1 // error
77
@error
88
given gGlobal: Int = 1 // error
99
@error
10-
given gGlobal2: Num[Int] with // error: object not supported (TODO support)
10+
given gGlobal2: Num[Int] with // error
1111
def zero = 0
1212
@error
13-
given gGlobal3(using DummyImplicit): Num[Int] with // error: class not supported (TODO support)
13+
given gGlobal3(using DummyImplicit): Num[Int] with // error
1414
def zero = 0
1515

16+
@error
17+
class cGlobal // error
18+
19+
@error
20+
object oGlobal // error
21+
1622
class B:
1723
@error
1824
val vMember: Int = 1 // error
@@ -23,12 +29,18 @@ class B:
2329
@error
2430
given gMember: Int = 1 // error
2531
@error
26-
given gMember2: Num[Int] with // error: object not supported (TODO support)
32+
given gMember2: Num[Int] with // error
2733
def zero = 0
2834
@error
29-
given gMember3(using DummyImplicit): Num[Int] with // error: class not supported (TODO support)
35+
given gMember3(using DummyImplicit): Num[Int] with // error
3036
def zero = 0
3137

38+
@error
39+
class cMember // error
40+
41+
@error
42+
object oMember // error
43+
3244
def locals: Unit =
3345
@error
3446
val vLocal: Int = 1 // error
@@ -39,11 +51,16 @@ class B:
3951
@error
4052
given gLocal: Int = 1 // error
4153
@error
42-
given gLocal2: Num[Int] with // error: object not supported (TODO support)
54+
given gLocal2: Num[Int] with // error
4355
def zero = 0
4456
@error
45-
given gLocal3(using DummyImplicit): Num[Int] with // error: class not supported (TODO support)
57+
given gLocal3(using DummyImplicit): Num[Int] with // error
4658
def zero = 0
59+
60+
@error
61+
class cLocal // error
62+
@error
63+
object oLocal // error
4764
()
4865

4966
trait Num[T]:

0 commit comments

Comments
 (0)