Skip to content

[Proof of Concept] Code generation via rewriting errors in macro annotations #16545

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions tests/neg-macros/annot-codegen.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

-- Error: tests/neg-macros/annot-codegen/Test_2.scala:7:4 --------------------------------------------------------------
7 | new Bar("", two, 1, 1.0) // error: body needs to be replaced
| ^^^^^^^^^^^^^^^^^^^^^^^^
| Replace the underline code by:
| data.generated[Bar]()
-- Error: tests/neg-macros/annot-codegen/Test_2.scala:9:6 --------------------------------------------------------------
9 | def withThree(three: Int): Bar // error: body needs to be data.generate[Bar]()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| Replace the underline code by:
| def withThree(three: scala.Int): Bar = data.generated[Bar]()
-- Error: tests/neg-macros/annot-codegen/Test_2.scala:1:80 -------------------------------------------------------------
1 |@data class Bar(val one: String, val two: Int, val three: Int, val four: Double): // error: additional methods needed
| ^
| @data requires the following additional method(s):
|
| def withFour(four: scala.Double): Bar = data.generated[Bar]()
-- Error: tests/neg-macros/annot-codegen/Test_2.scala:12:32 ------------------------------------------------------------
12 | def withOne(one: T): Baz[T] = ??? // error
| ^^^
| Replace the underline code by:
| data.generated[Baz[Baz.this.T]]()
-- Error: tests/neg-macros/annot-codegen/Test_2.scala:13:6 -------------------------------------------------------------
13 | def withTwo(two: Int): Baz[T] // error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| Replace the underline code by:
| def withTwo(two: scala.Int): Baz[Baz.this.T] = data.generated[Baz[Baz.this.T]]()
-- Error: tests/neg-macros/annot-codegen/Test_2.scala:14:38 ------------------------------------------------------------
14 | def withThree(three: Int): Baz[T] = data.generated[Baz[T]]() // error // FIXME: should be supported
| ^^^^^^^^^^^^^^^^^^^^^^^^
| constructor Baz in class Baz does not take parameters
-- Error: tests/neg-macros/annot-codegen/Test_2.scala:15:36 ------------------------------------------------------------
15 | def withFour(four: Int): Baz[T] = data.generated[Nothing]() // error // FIXME: wrong error
| ^^^^^^^^^^^^^^^^^^^^^^^^^
| constructor Baz in class Baz does not take parameters
87 changes: 87 additions & 0 deletions tests/neg-macros/annot-codegen/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import scala.annotation.{experimental, MacroAnnotation}
import scala.quoted.*
import scala.collection.mutable.ArrayBuffer

@experimental
class data extends MacroAnnotation:
import data.*
def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
import quotes.reflect.*
tree match
case cdef: ClassDef =>
val classPatches = ArrayBuffer[String]()

val cls = tree.symbol
val typeParams = cdef.body.map(_.symbol).filter(_.isType)
val clsTpe =
if typeParams.isEmpty then cls.typeRef
else AppliedType(cls.typeRef, typeParams.map(_.typeRef))
val expectedBody =
clsTpe.asType match
case '[t] => '{ data.generated[t]() }

val params = paramNames(cls)
for param <- params do
val withParam = With(param)
val paramType = cls.declaredField(param).info
val existingOpt =
cdef.body.find(stat =>
val paramss = stat.symbol.paramSymss
stat.symbol.name == withParam
&& paramss.size == 1 && paramss(0).size == 1
&& paramss(0)(0).name == param // FIXME: if the parameter name is incorrect, propose rewriting it
&& paramss(0)(0).info == paramType // FIXME: if the parameter type changed, propose rewriting it
)
existingOpt match
case Some(tree: DefDef) =>
tree.rhs match
case Some(rhs) =>
if !rhs.asExpr.matches(expectedBody) then
report.error(s"Replace the underline code by:\n${expectedBody.show}", rhs.pos)
case _ =>
report.error(s"Replace the underline code by:\n${tree.show} = ${expectedBody.show}", tree.pos)
case _ =>
// The method is not present
classPatches +=
s"def $withParam($param: ${paramType.show}): ${clsTpe.show} = ${expectedBody.show}"


val ctr = cdef.constructor
val endPos = Position(ctr.pos.sourceFile, ctr.pos.end, ctr.pos.end)
// TODO: if the class has no existing body, we also need to add braces or ':'
if classPatches.nonEmpty then
report.error("@data requires the following additional method(s):\n\n" +
classPatches.mkString("\n"), endPos)
case _ =>
report.error("Annotation only supports `class`")
List(tree)

object data:
// TODO: Is T necessary? Could this be transparent?
inline def generated[T](): T = ${generatedImpl[T]}
private def generatedImpl[T: Type](using Quotes): Expr[T] =
import quotes.reflect.*
val meth = Symbol.spliceOwner.owner
val cls = meth.owner
val params = paramNames(cls)
meth.name match
case With(paramName) =>
val localParam = meth.paramSymss(0)(0)
val body = // FIXME handle type parameters
Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor),
params.map(p => if p == paramName then Ref(localParam) else Ref(cls.declaredField(p))))
body.asExprOf[T]
case _ =>
report.errorAndAbort("@data.generated used in invalid context")

def paramNames(using Quotes)(cls: quotes.reflect.Symbol): List[String] =
cls.primaryConstructor.paramSymss.dropWhile(_.headOption.exists(_.isType)).head.map(_.name)

private object With:
def apply(paramName: String): String =
s"with${paramName.head.toUpper}${paramName.tail}"
def unapply(methodName: String): Option[String] = methodName match
case s"with$rest" =>
Some(s"${rest.head.toLower}${rest.tail}")
case _ =>
None
15 changes: 15 additions & 0 deletions tests/neg-macros/annot-codegen/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@data class Bar(val one: String, val two: Int, val three: Int, val four: Double): // error: additional methods needed
// This definition is OK and will be left as is
def withOne(one: String): Bar =
data.generated[Bar]()

def withTwo(two: Int): Bar =
new Bar("", two, 1, 1.0) // error: body needs to be replaced

def withThree(three: Int): Bar // error: body needs to be data.generate[Bar]()

@data class Baz[T](val one: T, val two: Int, val three: Int, val four: Int):
def withOne(one: T): Baz[T] = ??? // error
def withTwo(two: Int): Baz[T] // error
def withThree(three: Int): Baz[T] = data.generated[Baz[T]]() // error // FIXME: should be supported
def withFour(four: Int): Baz[T] = data.generated[Nothing]() // error // FIXME: wrong error