@@ -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 */
0 commit comments