@@ -18,11 +18,12 @@ trait MacroAnnotation extends StaticAnnotation:
18
18
*
19
19
* All definitions in the result must have the same owner. The owner can be recovered from `tree.symbol.owner`.
20
20
*
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.
22
22
*
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`.
24
24
*
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.
26
27
* ```scala
27
28
* import scala.quoted.*
28
29
* import scala.collection.mutable
@@ -34,7 +35,8 @@ trait MacroAnnotation extends StaticAnnotation:
34
35
* case DefDef(name, TermParamClause(param :: Nil) :: Nil, tpt, Some(rhsTree)) =>
35
36
* (param.tpt.tpe.asType, tpt.tpe.asType) match
36
37
* 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)
38
40
* val cacheRhs =
39
41
* given Quotes = cacheSymbol.asQuotes
40
42
* '{ mutable.Map.empty[t, u] }.asTerm
@@ -60,10 +62,10 @@ trait MacroAnnotation extends StaticAnnotation:
60
62
* ```
61
63
* and the macro will modify the definition to create
62
64
* ```scala
63
- * val fibCache =
65
+ * val fibCache$macro$1 =
64
66
* scala.collection.mutable.Map.empty[Int, Int]
65
67
* def fib(n: Int): Int =
66
- * fibCache.getOrElseUpdate(
68
+ * fibCache$macro$1 .getOrElseUpdate(
67
69
* n,
68
70
* {
69
71
* println(s"compute fib of $n")
@@ -72,6 +74,114 @@ trait MacroAnnotation extends StaticAnnotation:
72
74
* )
73
75
* ```
74
76
*
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
+ *
75
185
* @param Quotes Implicit instance of Quotes used for tree reflection
76
186
* @param tree Tree that will be transformed
77
187
*/
0 commit comments