Skip to content
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

Macro annotations class modifications (part 2) #16454

Merged

Conversation

nicolasstucki
Copy link
Contributor

@nicolasstucki nicolasstucki commented Dec 1, 2022

Enable modification of classes with MacroAnnotation:

  • Can annotate class to transform it
  • Can annotate object to transform the companion class

Supported class modifications:

  • Modify the implementations of def, val, var, lazy val, class, object in the class
  • Add new def, val, var, lazy val, class, object members to the class
  • Add a new override for a def, val, var, lazy val members in the class

Restrictions:

  • An annotation on a top-level class cannot return a top-level def, val, var, lazy val.

Related PRs:

Fixes #16266

@nicolasstucki nicolasstucki self-assigned this Dec 1, 2022
@nicolasstucki nicolasstucki force-pushed the macro-annotations-modify-class branch 3 times, most recently from 1dc36c6 to 51c7a41 Compare December 5, 2022 13:57
@nicolasstucki nicolasstucki changed the title TASTy macro annotations class modifications (part 2) Macro annotations class modifications (part 2) Dec 5, 2022
@nicolasstucki nicolasstucki force-pushed the macro-annotations-modify-class branch 12 times, most recently from bf08d00 to 1599314 Compare December 9, 2022 16:29
odersky added a commit that referenced this pull request Dec 12, 2022
#### Add basic support for macro annotations

* Introduce experimental `scala.annotations.MacroAnnotation`
* Macro annotations can analyze or modify definitions
* Macro annotation can add definition around the annotated definition
  * Added members are not visible while typing
  * Added members are not visible to other macro annotations
  * Added definition must have the same owner
* Implement macro annotation expansion
  * Implemented at `Inlining` phase 
* Can use macro annotations in staged expressions (expanded when at
stage 0)
    * Can use staged expression to implement macro annotations 
    * Can insert calls to inline methods in macro annotations
  * Current limitations (to be loosened in following PRs)
    * Can only be used on `def`, `val`, `lazy val` and `var`
    * Can only add `def`, `val`, `lazy val` and `var` definitions

#### Example
```scala
class memoize extends MacroAnnotation:
  def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
    import quotes.reflect._
    tree match
      case DefDef(name, TermParamClause(param :: Nil) :: Nil, tpt, Some(rhsTree)) =>
        (Ref(param.symbol).asExpr, rhsTree.asExpr) match
          case ('{ $paramRefExpr: t }, '{ $rhsExpr: u }) =>
            val cacheTpe = TypeRepr.of[Map[t, u]]
            val cacheSymbol = 
              Symbol.newVal(tree.symbol.owner, name + "Cache", cacheTpe, Flags.Private, Symbol.noSymbol)
            val cacheRhs = '{ Map.empty[t, u] }.asTerm
            val cacheVal = ValDef(cacheSymbol, Some(cacheRhs))
            val cacheRefExpr = Ref(cacheSymbol).asExprOf[Map[t, u]]
            val newRhs = '{ $cacheRefExpr.getOrElseUpdate($paramRefExpr, $rhsExpr) }.asTerm
            val newTree = DefDef.copy(tree)(name, TermParamClause(param :: Nil) :: Nil, tpt, Some(newRhs))
            List(cacheVal, newTree)
      case _ =>
        report.error("Annotation only supported on `def` with a single argument are supported")
        List(tree)
```

with this macro annotation a user can write
```scala
@memoize
def fib(n: Int): Int =
  println(s"compute fib of $n")
  if n <= 1 then n else fib(n - 1) + fib(n - 2)
```
and the macro will modify the definition to create
```scala
val fibCache = mutable.Map.empty[Int, Int]
def fib(n: Int): Int =
  fibCache.getOrElseUpdate(
    n,
    {
      println(s"compute fib of $n")
      if n <= 1 then n else fib(n - 1) + fib(n - 2)
    }
  )
```

#### Based on
* #15626
* https://infoscience.epfl.ch/record/294615?ln=en

#### Followed by
* #16454
@Kordyjan Kordyjan added this to the 3.3.0-RC1 milestone Dec 12, 2022
@nicolasstucki nicolasstucki force-pushed the macro-annotations-modify-class branch 6 times, most recently from e4d781a to 86c6d54 Compare December 14, 2022 10:58
nicolasstucki added a commit to dotty-staging/dotty that referenced this pull request Dec 15, 2022
Enable the addition of classes from a `MacroAnnotation`:
 * Can add new `class` definitions next to the annotated definition

Special cases:
 * A top-level `def`, `val`, `var`, `lazy val` can return a `class`
   definition that is owned by the package or package object.

Related PRs:
 * Follows scala#16454
nicolasstucki added a commit to dotty-staging/dotty that referenced this pull request Dec 15, 2022
Enable the addition of classes from a `MacroAnnotation`:
 * Can add new `class` definitions next to the annotated definition

Special cases:
 * An annotated top-level `def`, `val`, `var`, `lazy val` can return a `class`
   definition that is owned by the package or package object.

Related PRs:
 * Follows scala#16454
* Add `Symbol.freshName`
* Remove `Symbol.newUniqueMethod`
* Remove `Symbol.newUniqueVal`

This API is necessary to be able to create new class members without
having name clashes. It should also be usable to create new top level
definitions that have names that do not clash.

This version of unique name can be used for `val`, `def` and `class` symbols.
Enable modification of classes with `MacroAnnotation`:
 * Can annotate `class` to transform it
 * Can annotate `object` to transform the companion class

Supported class modifications:
 * Modify the implementations of `def`, `val`, `var`, `lazy val`, `class`, `object` in the class
 * Add new `def`, `val`, `var`, `lazy val`, `class`, `object` members to the class
 * Add a new override for a `def`, `val`, `var`, `lazy val` members in the class

Restrictions:
 * An annotation on a top-level class cannot return a top-level `def`, `val`, `var`, `lazy val`
@nicolasstucki nicolasstucki merged commit 0555491 into scala:main Dec 19, 2022
@nicolasstucki nicolasstucki deleted the macro-annotations-modify-class branch December 19, 2022 17:02
nicolasstucki added a commit to dotty-staging/dotty that referenced this pull request Dec 20, 2022
Enable the addition of classes from a `MacroAnnotation`:
 * Can add new `class` definitions next to the annotated definition

Special cases:
 * An annotated top-level `def`, `val`, `var`, `lazy val` can return a `class`
   definition that is owned by the package or package object.

Related PRs:
 * Follows scala#16454
smarter added a commit that referenced this pull request Jan 12, 2023
Enable the addition of classes from a `MacroAnnotation`:
* Can add new `class`/`object` definitions next to the annotated
definition

Special cases:
* An annotated top-level `def`, `val`, `var`, `lazy val` can return a
`class`/`object`
   definition that is owned by the package or package object.

Related PRs:
 * Follows #16454
little-inferno pushed a commit to little-inferno/dotty that referenced this pull request Jan 25, 2023
Enable the addition of classes from a `MacroAnnotation`:
 * Can add new `class` definitions next to the annotated definition

Special cases:
 * An annotated top-level `def`, `val`, `var`, `lazy val` can return a `class`
   definition that is owned by the package or package object.

Related PRs:
 * Follows scala#16454
@jilen
Copy link

jilen commented Feb 17, 2023

Is it possible to transform companion object while annotate on class ?

@alexarchambault
Copy link
Contributor

Is it possible to transform companion object while annotate on class ?

Would love to know this too. In Scala 2 macro annotations, the transform method accepts a list of Trees, like def transform(annottees: Tree*): Tree, that is passed both a ClassDef and a ModuleDef. But it only accepts a single quotes.reflect.Definition in Scala 3.

@nicolasstucki
Copy link
Contributor Author

Is it possible to transform companion object while annotate on class ?

It is not possible with this version. What is the use case?

@alexarchambault
Copy link
Contributor

A use case for me would be a macro annotation acting as a more refined derives, or more generally, adding given instances in the companion depending on what's in the class body, like

@IsMapEntry
class Foo {
  @Key
  def id: String = "a"
  @Value
  def value: Int = 2
}

where @IsMapEntry would look at the body of Foo, find the @Key and @Value annotated methods, read their types, and put something like this in the companion of Foo:

given AsMapEntry[Foo, String, Int] = AsMapEntry.derive // String and Int are the @Key and @Value annotated method types

@nicolasstucki
Copy link
Contributor Author

In this version of macro annotations, you cannot generate definitions that will be visible to users. The generated given AsMapEntry could only be accessed from your macro implementation.

Why not use class Foo(val id: String = "a", val value: Int = 2) derives IsMapEntry ?

@alexarchambault
Copy link
Contributor

Why not use class Foo(val id: String = "a", val value: Int = 2) derives IsMapEntry ?

Because IsMapEntry has more than one type parameter. Getting an error like Foo cannot be unified with the type argument of IsMapEntry when using derives (and things like derives IsMapEntry[?, String, String] don't seem to work).

@alexarchambault
Copy link
Contributor

In more complex cases, I wish I would have been able to use parameters of the annotation in the additional definitions (derives wouldn't cover that case either).

@jilen
Copy link

jilen commented Feb 23, 2023

Another case is the zio accessor generation.
Also, it would be great for scala2 migration (e.g. JsonCodec in circe.)

@kitlangton
Copy link

kitlangton commented Mar 7, 2023

Similar to the accessor macro above, generating lenses on the companion object would also be an example use case.

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

// Would expand into

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

object Person:
  val name: Lens[Person, String] = Lens.make(_.name)
  val age: List[Person, Int] = Lens.make(_.age)

It seems like a trivial thing, but it'd be nice to avoid the boilerplate 😄. I wonder if there's a solution to this that doesn't require full power Scala 2-style pre-type-checked annotations.

@smarter
Copy link
Member

smarter commented Mar 7, 2023

I wonder if there's a solution to this that doesn't require full power Scala 2-style pre-type-checked annotations.

My best attempt: https://contributors.scala-lang.org/t/scala-3-macro-annotations-and-code-generation/6035

@kitlangton
Copy link

That seems like a great idea @smarter! I'll give it a try :)

@smarter
Copy link
Member

smarter commented Mar 7, 2023

Cool! In case you haven't seen it, I have a prototype up at #16545

@kitlangton
Copy link

I was actually just looking at that! It would appear that it's not yet using the -rewrite mechanism, is that correct? If not, is there anything preventing that at the moment, if I were to try to hook into that ability?

@smarter
Copy link
Member

smarter commented Mar 7, 2023

If not, is there anything preventing that at the moment

No, it's just a matter of doing the work, so you're more than welcome to take a stab at it :). This ties in with work started by @ckipp01 on structured diagnostics since we want to expose that information to IDEs, but that part should be orthogonal to the API we use in macros hopefully.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Need some way to get newMethod tpt (MethodType, PolyType, ByNameType) of existing DefDef
6 participants