Skip to content

Commit 53716bf

Browse files
committed
[Proof of Concept] Code generation via rewriting errors in macro annotations
This commit prototypes the proposal in https://contributors.scala-lang.org/t/scala-3-macro-annotations-and-code-generation/6035 to use a macro annotation to do code generation. Since the -rewrite mechanism isn't currently accessible to macros, we just print an error indicating what part of the source code needs to be rewritten. We implement support for a subset of `@data`: only the `withField` methods are added (note that equals/hashCode/toString wouldn't need to be code-generated since they are already defined in `AnyRef`, so a macro annotation can override them as demonstrated in the existing `tests/run-macros/annot-mod-class-data` test). This is only one possible path forward. Alternatively, we could recommend using scalafix for this usecase, and then the compiler itself wouldn't have to grow code generation abilities, but libraries defining macro annotation might not like to be tied to an external tool.
1 parent e9b246b commit 53716bf

File tree

3 files changed

+105
-0
lines changed

3 files changed

+105
-0
lines changed

tests/neg-macros/annot-codegen.check

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
-- Error: tests/neg-macros/annot-codegen/Test_2.scala:9:4 --------------------------------------------------------------
3+
9 | new Bar("", two, 1, 1.0) // error: body needs to be replaced
4+
| ^^^^^^^^^^^^^^^^^^^^^^^^
5+
| Replace the underline code by:
6+
| data.generated[Bar]()
7+
-- Error: tests/neg-macros/annot-codegen/Test_2.scala:3:80 -------------------------------------------------------------
8+
3 |@data class Bar(val one: String, val two: Int, val three: Int, val four: Double): // error: additional methods needed
9+
| ^
10+
| @data requires the following additional method(s):
11+
|
12+
| def withThree(three: scala.Int): Bar = data.generated[Bar]()
13+
| def withFour(four: scala.Double): Bar = data.generated[Bar]()
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import scala.annotation.{experimental, MacroAnnotation}
2+
import scala.quoted.*
3+
import scala.collection.mutable.ArrayBuffer
4+
5+
@experimental
6+
class data extends MacroAnnotation:
7+
import data.*
8+
def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
9+
import quotes.reflect.*
10+
tree match
11+
case cdef: ClassDef =>
12+
val classPatches = ArrayBuffer[String]()
13+
14+
val cls = tree.symbol
15+
val expectedBody = s"data.generated[${cls.name}]()" // FIXME: handle type parameters
16+
17+
val params = paramNames(cls)
18+
for param <- params do
19+
val withParam = With(param)
20+
val paramType = cls.declaredField(param).info
21+
val existingOpt =
22+
cls.declaredMethod(withParam).find(o =>
23+
val paramss = o.paramSymss
24+
paramss.size == 1 && paramss(0).size == 1
25+
&& paramss(0)(0).name == param // FIXME: if the parameter name is incorrect, propose rewriting it
26+
&& paramss(0)(0).info == paramType // FIXME: if the parameter type changed, propose rewriting it
27+
)
28+
existingOpt match
29+
case Some(existing) => existing.tree match
30+
case tree: DefDef =>
31+
tree.rhs match
32+
case Some(rhs) => rhs.asExpr match
33+
case '{data.generated[t]()} =>
34+
// The correct method is already present, nothing to do
35+
case _ =>
36+
report.error(s"Replace the underline code by:\n$expectedBody", rhs.pos)
37+
case _ =>
38+
report.error("FIXME: Passing -Yretain-trees is currently needed for this macro to work")
39+
case _ =>
40+
case _ =>
41+
// The method is not present
42+
classPatches +=
43+
s"def $withParam($param: ${paramType.show}): ${cls.name} = $expectedBody"
44+
45+
46+
val ctr = cdef.constructor
47+
val endPos = Position(ctr.pos.sourceFile, ctr.pos.end, ctr.pos.end)
48+
// TODO: if the class has no existing body, we also need to add braces or ':'
49+
if classPatches.nonEmpty then
50+
report.error("@data requires the following additional method(s):\n\n" +
51+
classPatches.mkString("\n"), endPos)
52+
case _ =>
53+
report.error("Annotation only supports `class`")
54+
List(tree)
55+
56+
object data:
57+
inline def generated[T](): T = ${generatedImpl[T]}
58+
private def generatedImpl[T: Type](using Quotes): Expr[T] =
59+
import quotes.reflect.*
60+
val meth = Symbol.spliceOwner.owner
61+
val cls = meth.owner
62+
val params = paramNames(cls)
63+
meth.name match
64+
case With(paramName) =>
65+
val localParam = meth.paramSymss(0)(0)
66+
val body =
67+
Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor),
68+
params.map(p => if p == paramName then Ref(localParam) else Ref(cls.declaredField(p))))
69+
body.asExprOf[T]
70+
case _ =>
71+
report.errorAndAbort("@data.generated used in invalid context")
72+
73+
def paramNames(using Quotes)(cls: quotes.reflect.Symbol): List[String] =
74+
cls.primaryConstructor.paramSymss.head.map(_.name)
75+
76+
private object With:
77+
def apply(paramName: String): String =
78+
s"with${paramName.head.toUpper}${paramName.tail}"
79+
def unapply(methodName: String): Option[String] = methodName match
80+
case s"with$rest" =>
81+
Some(s"${rest.head.toLower}${rest.tail}")
82+
case _ =>
83+
None
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// scalac: -Yretain-trees
2+
3+
@data class Bar(val one: String, val two: Int, val three: Int, val four: Double): // error: additional methods needed
4+
// This definition is OK and will be left as is
5+
def withOne(one: String): Bar =
6+
data.generated[Bar]()
7+
8+
def withTwo(two: Int): Bar =
9+
new Bar("", two, 1, 1.0) // error: body needs to be replaced

0 commit comments

Comments
 (0)