@@ -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
@@ -66,6 +67,114 @@ trait MacroAnnotation extends StaticAnnotation:
66
67
* )
67
68
* ```
68
69
*
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
+ *
69
178
* @param Quotes Implicit instance of Quotes used for tree reflection
70
179
* @param tree Tree that will be transformed
71
180
*/
0 commit comments