-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Macro annotations class modifications (part 2) #16454
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
Conversation
1dc36c6
to
51c7a41
Compare
bf08d00
to
1599314
Compare
#### 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
e4d781a
to
86c6d54
Compare
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
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`
88c6f14
to
52c9461
Compare
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
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
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
commented
Feb 17, 2023
Is it possible to transform companion object while annotate on class ?
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 Tree
s, 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.
Is it possible to transform companion object while annotate on class ?
It is not possible with this version. What is the use case?
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
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
?
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).
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).
Another case is the zio accessor generation.
Also, it would be great for scala2 migration (e.g. JsonCodec
in circe.)
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.
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
commented
Mar 7, 2023
That seems like a great idea @smarter! I'll give it a try :)
Cool! In case you haven't seen it, I have a prototype up at #16545
kitlangton
commented
Mar 7, 2023
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?
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.
Uh oh!
There was an error while loading. Please reload this page.
Enable modification of classes with
MacroAnnotation
:class
to transform itobject
to transform the companion classSupported class modifications:
def
,val
,var
,lazy val
,class
,object
in the classdef
,val
,var
,lazy val
,class
,object
members to the classdef
,val
,var
,lazy val
members in the classRestrictions:
def
,val
,var
,lazy val
.Related PRs:
spliceOwner
#16513Fixes #16266