How do I add a no-arg constructor to a Scala case class with a macro annotation?
Asked Answered
C

1

8

I'm trying to answer this question.

Instead of writing:

case class Person(name: String, age: Int) {
  def this() = this("",1)
}

I thought I'd use macro annotations to expand it from:

@Annotation
case class Person(name: String, age: Int)

So I tried adding the new constructor as a plain-old DefDef using quasiquotes in a macro annotation's impl, like:

val newCtor = q"""def this() = this("", 1)"""
val newBody = body :+ newCtor
q"$mods class $name[..$tparams](..$first)(...$rest) extends ..$parents { $self => ..$newBody }"

But that returns an error: called constructor's definition must precede calling constructor's definition

Is there a way to fix that? What did I miss?

Thanks for taking a look, -Julian

Cream answered 31/3, 2014 at 8:10 Comment(0)
S
7

It turns out that a very natural intent to generate a secondary constructor in a macro annotation has exposed two different issues.

1) The first issue (https://issues.scala-lang.org/browse/SI-8451) is about quasiquotes emitting wrong tree shapes for secondary constructors. That was fixed in 2.11.0-RC4 (yet to be released, currently available as 2.11.0-SNAPSHOT) and in paradise 2.0.0-M6 for 2.10.x (released yesterday).

2) The second issue is about unassigned positions wreaking havoc during typechecker. Curiously enough, when typechecking calls to constructors, typer uses positions in order to decide whether these calls are legal or not. That can't be patched that easily, and we have to work around:

         val newCtor = q"""def this() = this(List(Some("")))"""
-        val newBody = body :+ newCtor
+
+        // It looks like typer sometimes uses positions to decide whether stuff
+        // (secondary constructors in this case) typechecks or not (?!!):
+        // https://github.com/xeno-by/scala/blob/c74e1325ff1514b1042c959b0b268b3c6bf8d349/src/compiler/scala/tools/nsc/typechecker/Typers.scala#L2932
+        //
+        // In general, positions are important in getting error messages and debug
+        // information right, but maintaining positions is too hard, so macro writers typically don't care.
+        //
+        // This has never been a problem up until now, but here we're forced to work around
+        // by manually setting an artificial position for the secondary constructor to be greater
+        // than the position that the default constructor is going to get after macro expansion.
+        //
+        // We have a few ideas how to fix positions in a principled way in Palladium,
+        // but we'll have to see how it goes.
+        val defaultCtorPos = c.enclosingPosition
+        val newCtorPos = defaultCtorPos.withEnd(defaultCtorPos.endOrPoint + 1).withStart(defaultCtorPos.startOrPoint + 1).withPoint(defaultCtorPos.    point + 1)
+        val newBody = body :+ atPos(newCtorPos)(newCtor)
Sanchez answered 31/3, 2014 at 9:23 Comment(3)
Alas, I'm using paradise 2.0.0-M6 for 2.10.3. Here's a minimal runnable example. And here's the error in M4.Cream
Oh I see. So we had two bugs here. One fixed by M6, and another one that still remains.Sanchez
Looks like the workaround succeeds for doubly annotated classes as well, as long as the @NewCtorAnnotation is immediately preceding the annotee. However the same error returns if @AnotherAnnotation is in between them (not an issue for me, just fyi).Cream

© 2022 - 2024 — McMap. All rights reserved.