scalameta / scalagen

WIP - Scalameta powered code generation
Apache License 2.0
40 stars 5 forks source link

Injecting data into generators #21

Open DavidDudson opened 6 years ago

DavidDudson commented 6 years ago

Take for example the following...

What the author wants to write is:

package foo.bar

case class PrintThisString(s: String) extends ExtensionGenerator {
  def extend(c: Defn.Class): List[Stat] = {
     q"println(${Lit.String(s)})"
  }
}

That way, the user writing code which uses the generator, writes valid code as far as the IDE is concerned.

We need a way of injecting foo into the class. We also want to create an instance of this thing, that does not have the parameters supplied.

What we will end up needing (with semantics) is the following

package foo.bar

case class PrintThisString() extends ExtensionGenerator(t"foo.bar.PrintThisString") {
  var foo: String

  def extend(c: Defn.Class): List[Stat] = {
     q"println(${Lit.String(s)})"
  }
}

This is not the only information we need to inject either. Things like a semantic database, will be required in the future.

Theres a few of ways we could do this....

  1. Mirror Elysium Have an annotation of some kind
package foo.bar

@ParameterGenerator
case class PrintThisString(s: String) extends ExtensionGenerator {
  def extend(c: Defn.Class): List[Stat] = {
     q"println(${Lit.String(s)})"
  }
}
  1. Mirror scalameta/paradise
package foo.bar

case class PrintThisString(s: String) extends ExtensionGenerator {
  def extend(c: Defn.Class): List[Stat] = {
     q"case class PrintThisString(${s: Lit.String})" = this
     q"println(${s})"
  }
}
  1. Hide the behaviour from the user.

Effectively the same as 1. Except without an annotation.

Before executing the generator code, we pass over a generator to transform the class into something more usable from the generators perspective.

So scalagen actually transforms generator sources, and actual sources.

I prefer 3. So that's what I'm going to explore, any objections should be discussed here.

olafurpg commented 6 years ago

How about

  def extend(c: Defn.Class, ctx: Context): List[Stat] = {
    ctx.prefix/enclosingClass/parameters/semanticdb
  }

This is probably the easiest option with regards to classloading and is also quite similar to scala-reflect macros. I never like this in scalameta/paradise and in scalacenter/macros I've moved closer to a scala-reflect compatible interface.

DavidDudson commented 6 years ago

@olafurpg For "Universe" like behaviour I agree that we should use context. perhaps even make it implicit.

For parameters specifically, they are named, and on the class that is the generator.

It seems odd for an author to have to write ctx.parameters.get("foo") or similar in order to access a field that is sitting right in the constructor of the class.

For example...

case class Bar(foo: String) {
  def extend(c: Defn.Class, ctx: Context): List[Stat] = {
    ctx.parameters.get("foo")
  }
}

The above seems really peculiar, not to mention that we would have to use HLists or similar for the args in order to get the right types. I think code generation has value here. (Even if it is slightly more magic).

olafurpg commented 6 years ago

The arguments to the annotation are not necessarily literals, they can be any syntax. The way I would do it is to separate the annotation from the expansion method

class Foo(arg: String) extends StaticAnnotation
object Foo extends Generator {
  def extend(defn: Defn.Class, c: Context): List[Stat] = {
    val Lit.String(arg) = c.prefix
    ...
  }
}

I think code generation has value here. (Even if it is slightly more magic).

I would personally start with something more boilerplatey to begin with, and then once the common pattern emerges we can discuss how to improve it. scalagen may be one solution, but shapeless or a custom def macro might also work fine.

DavidDudson commented 6 years ago

Thats fair. Although I think we will want to provide helper methods that provide clearer error messages rather then just a match error. It's a common enough pattern. I see literals used more then any other parameters.

olafurpg commented 6 years ago

provide helper methods that provide clearer error messages rather then just a match error

Absolutely! I never liked the val Lit.String(x) = ... pattern