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

SIP-63 - Scala 3 Macro Annotations #80

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
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
349 changes: 349 additions & 0 deletions content/macro-annotations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
---
layout: sip
permalink: /sips/:title.html
stage: implementation
status: waiting-for-implementation
presip-thread: https://contributors.scala-lang.org/t/pre-sip-foo-bar/9999
title: SIP-NN - Scala 3 Macro Annotations
---

**By: Nicolas Stucki**

## History

| Date | Version |
|---------------|--------------------|
| Feb 19th 2024 | Initial Draft |

## Summary

Macro annotations are a metaprogramming feature that allows compile-time transformation of annotated definitions.
These annotations extend the existing macro system, they use the reflection API directly and allow integration with the multi-stage programming abstractions (quoted expressions `'{...}`).
They provide the same level of safety guarantees as macros.

The design of these macro annotations prioritizes soundness over expressivity.
Therefore they will be less expressive than the experimental macro annotations in Scala 2, but will avoid unsoundness pitfalls.

This proposal introduces the concrete trait interface to define macro annotations and how these are evaluated by the compiler. The details on multi-stage programming and the reflection API are taken as a given and unmodified by this proposal.

## Motivation

The goal of macro annotation is to provide a way to transform the code of annotated definitions. This can be used to automate code generation, to implement optimizations, or to enforce coding standards. Below we will see a few examples of macro annotations that could be implemented using the proposed interface.

### Example 1: Data class
Many users of Scala use case classes to define data classes. They usually only care about the access to the fields, `equals`, `hashCode`, and `toString` methods. Case classes add methods for pattern matching that might be considered bloat code in these situations.

We could define a data class as follows:

```scala
@data class User(val name: String, val id: Int)
```

The `@data` would override the implementations of `equals`, `hashCode`, and `toString`. The fields are accessible because they are marked as `val`. After the transformation of the `@data` macro annotation, the definition would be equivalent to:

```scala
class User(val name: String, val id: Int) {
override def equals(that: Any): Boolean = that match
case that: User => this.name == that.name && this.id == that.id
case _ => false
override def hashCode(): Int = ...
override def toString(): Int = s"User($name, $id)"
}
```


### Example 2: Memoize

Memoization is a common optimization technique that stores the results of expensive function calls and returns the cached result when the same inputs occur again. This optimization can be implemented using a macro annotation, let us call it `@memoize`. For example, we could define a memoized version of the Fibonacci function as follows:

```scala
@memoize
def fib(n: Int): Int =
if n <= 1 then n else fib(n - 1) + fib(n - 2)
```

The `@memoize` will add a new `val` outside the annotated definition to store the cache. This cache will then be used to
in the modified definition of `fib`.


```scala
private val fibCache$1: scala.collection.mutable.Map[Int, Int] =
scala.collection.mutable.Map.empty[Int, Int]

def fib(n: Int): Int =
val cache = fibCache$1
if !cache.contains(n) then
cache(n) = if n <= 1 then n else fib(n - 1) + fib(n - 2)
cache(n)
```

### Example 3: Main annotation

In this example, we define an `@httpMain` that will generate a `main` method in a similar way to the `@main` method in Scala 3.


```scala
@httpMain def httpGet(url: URL) = println(http.get(url))
```

The `@httpMain` will generate a new class named `httpMain` with a `main` method that will call the annotated definition with the parsed arguments. Note that the syntax below is not valid Scala, but a representation of the TASTy code that would be generated. In particular, the top-level definition is moved into its package object `Example3$package`, and the
`main` method is generated in a class as a static method rather than an object.

```scala
object Example3$package {
def httpGet(url: URL) = println(http.get(url))
}

class httpGet {
<static> def main(args: Array[String]): Unit =
args match
case Array(urlString) => Example3$package.httpGet(URL.parse(url))
case _ => println("Usage: httpGet <url>")
}
```

## Design constraints

Macro annotations must integrate with existing language features.

* Integrate with existing macro system and safety guarantees
* Separate/incremental compilation
* IDE support

### Integrate with existing macro system and safety guarantees
We want seamless integration with the existing macro system. This means that macro annotations should be able to use the reflection API and the multi-stage programming abstractions (quoted expressions `'{...}`). This also implies that we do not need to invent yet another new metaprogramming API for macro annotations.

### Separate/incremental compilation
Supporting incremental compilation implies that macro annotations must not affect the typing process.
Otherwise, the order in which files are compiled/typed would introduce non-deterministic typing outcomes.

### IDE/tools support
For this, we rely on the representation of the program in TASTy. The IDEs or other tools should not get confused by changes in definitions caused by macro annotations. In other words, the TASTy representation of the program must contain all the information of the original definitions.


## Proposed solution

The library will define a macro annotation trait that will define a tree transformation operation using the `scala.quotes.Quotes` API.
A macro implementation is a class that extends this trait and implements the `transform` method.
Then a definition annotated with the concrete macro annotation will be transformed by the macro implementation at compile time.

### High-level overview

The `MacroAnnotation` annotation trait would be defined as follows:

```scala
package scala.annotation
import scala.quoted.*

/** Annotation classes that extend this trait will be able to transform the annotated definition. */
trait MacroAnnotation extends StaticAnnotation:
/** Transforms a tree `definition` and optionally adds new definitions.
Copy link
Contributor

@lihaoyi lihaoyi Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cask and MainArgs are perhaps one of the more common libraries using "fake" macro annotations today, and I suspect they wouldn't be implementable using this current proposal as is

That because they require that multiple @annotated methods be considered to generate a single combined routing object. In MainArgs you can have multiple @main methods, and in Cask you can have multiple @cask.get/@cask.post/etc. routes.

In both cases, we currently have a expression-macro that looks at the class body, looks for all the @annotated defs within it, and generates a single expression that references all of them.

These are different usage patterns than the use cases included in this proposal, but I don't think they are unreasonable. It's fundamentally the Registry Pattern, which e.g. is a very common use case for Python's @decorators which Scala macro annotations are very similar to. Consider this example from the popular Flask project:

from flask import Flask

app = Flask(__name__)

@app.route('/') # Registers `def index` in `app`
def index():
    return 'Index Page'

@app.route('/hello') # Registers `def hello` in `app`
def hello():
    return 'Hello, World'

Or Python's Click library:

import click

@click.group() # registers `def cli` with `click`
def cli():
    pass

@cli.command() # registers `def initdb` with `cli`
def initdb():
    click.echo('Initialized the database')

@cli.command()# registers `def dropdb` with `cli`
def dropdb():
    click.echo('Dropped the database')

Is there some way we can support that sort of use case with Macro Annotations?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe an aggregate version that only returns new trees to go at the end of the surrounding scope?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could have a macro annotation @flask that looks for normal annotations @app.route(...) in its scope

@flask def app: Flask = Flask()

@app.route("/")
def index() = "Index Page"

@app.route("/hello")
def hello() = "Hello, World"

it could then generate a main as follows

class app {
  <static> def main(args: Array[String]): Unit = {
    app
      .registerRoute("/", index)
      .registerRoute("/hello", hello)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might also have it as

@flask val app: Flask = Flask()

and modify it to

val app: Flask = 
  Flask()
    .registerRoute("/", index)
    .registerRoute("/hello", hello)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the limitation we have is that we need to instantiate the annotation at compile time. If we have @app.route('/') where app is a reference to a local value that will only exist later (at runtime) we cannot instantiate it as a macro annotation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another approach that would follow closer the module initialization semantics of the Python version might be

object app extends Flask {

  @route("/")
  def index() = "Index Page"

  @route("/hello")
  def hello() = "Hello, World"
}

where the @root macro annotation assumes it is inside a Flask instance and generates something like

object app extends Flask {

  val _ = (this: Flask).registerRoot("/" , index)
  def index() = "Index Page"

  val _ = (this: Flask).registerRoot("/hello", hello)
  def hello() = "Hello, World"
}

Copy link
Contributor

@lihaoyi lihaoyi Mar 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolasstucki perhaps using the raw Python examples was a bit overly specific, so let's not over-fit on that exact syntax. Let's instead consider the Cask/MainArgs equivalents, which are a bit more idiomatic Scala:

// Cask
object MinimalApplication extends cask.MainRoutes{
  @cask.get("/")
  def hello() = {
    "Hello World!"
  }

  @cask.post("/do-thing")
  def doThing(request: cask.Request) = {
    request.text().reverse
  }

  initialize()
}
// MainArgs
object Main{
  @main
  def foo(@arg(short = 'f', doc = "String to print repeatedly")
          foo: String,
          @arg(doc = "How many times to print string")
          myNum: Int = 2,
          @arg(doc = "Example flag")
          bool: Flag) = {
    println(foo * myNum + " " + bool.value)
  }
  @main
  def bar(i: Int,
          @arg(doc = "Pass in a custom `s` to override it")
          s: String  = "lols") = {
    println(s * i)
  }
  def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args)
}

In both Cask and MainArgs, we do a "batch" registration:

  • Cask's initialize() function takes an implicit macro that resolves the macro-annotations, generates the trees, then assigns it to a (private) mutable variable for use later

  • MainArgs' ParserForMethods macro is an explicit macro call that resolves the macro annotations, generates the trees, and we ourselves use it as part of def main

(Mill actually has a similar design and implementation for it's T.command definitions, that's very similar to how MainArgs works. It does a similar explicit macro call, albeit the macro call is performed in generated code so the user doesn't see it)

In fact, the @flask val app: Flask = Flask() example you gave is a bit unnecessary: we can do the same thing without a macro annotation, via val app: Flask = Flask(), by making Flask.apply a macro (what MainArgs does) or making it take an implicit macro (what Cask does)

This is in contrast to the Python examples, which use mutable registries that each decorator appends to, similar to your (this: Flask).registerRoot("/" , index) example. While that is a valid design, I think the use of mutable state is a bit un-idiomatic Scala, especially given the "immutable" approach of registering all the generated trees at once.

I think my ideal scenario would be

object Foo {
  @bar
  def qux() = "Index Page"

  @baz
  def cow() = "Hello, World"
}

Expanding to some kind of generalized (in pseudocode)

object Foo {
  @bar
  def qux() = "Index Page"

  @baz
  def cow() = "Hello, World"

  ${generateDefinition(expandMacro(@bar def qux), expandMacro(@baz def cow))}
}

That gives us flexibility: generateDefinition can generate a:

  • object Main{ def main(args: Array[String]): Unit = ??? }, for Scala 3 @main methods
  • def main(args: Array[String]): Unit = ??? for MainArgs' `@main methods
  • val routes = Seq(???) for Cask's routes

Perhaps the next questions would be, assuming bar and qux contain their own logic for macroExpand, then:

  1. Who defines the generateDefinition logic? Is it on the enclosing template? Or some property of bar or qux?
  2. If we need to generate multiple definitions, e.g. we have an object foo that contains both @cask.gets and MainArgs @main methods, how do we ensure the @cask.gets all get passed to Cask's generateDefinition logic, while all the MainArgs @mains get passed to MainArgs' generateDefinition logic?

Going with the mutable-registration-pattern approach sidesteps these questions - we can generate a separate statement before each annotated def that performs the registration when executed - it's just a bit awkward and unidiomatic. But if we cannot find a better solution then it's certainly an option.

This is all a bit convoluted, but I think it's doable to find an elegant approach here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that macro annotations might not be the best tool to get an elegant solution to this particular problem. Based on these examples, inline traits (report, prototype) are a much better fit.

Cask with inline traits
object MinimalApplication extends MainAutoRoot {
  @get("/")
  def hello() = "Hello World!"

  @post("/do-thing")
  def doThing(request: Request) = request.text().reverse

  // generated code
  // def routes: Route = Seq(
  //   Route("hello", hello),
  //   Route("doThing", doThing, ...),
  // )
}
inline trait MainAutoRoot {
   def routes: Route = generateRoutes // macro `generateRoutes` finds the routes defined in the class that extends `Routes`
}

trait MainRoot {
  def roots: Roots
  def main(...) = // do something with routes
}
  1. Who defines the generateDefinition logic? Is it on the enclosing template? Or some property of bar or qux?

In an inline trait, it would be the trait itself.

  1. If we need to generate multiple definitions, e.g. we have an object foo that contains both @cask.gets and MainArgs @main methods, how do we ensure the @cask.gets all get passed to Cask's generateDefinition logic, while all the MainArgs @mains get passed to MainArgs' generateDefinition logic?

With inline traits it becomes simpler because there is a single point of expansion for the definition. The definition expansion can collect all (non-macro) annotated definitions in scope.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolasstucki I think using an inline trait sounds reasonable. But if that's our stance, then that basically cuts out Example 3: Main annotation from the use cases of macro annotations: it doesn't make sense to say macro annotations provide a way to provide a worse version of MainArgs. We'll need to find other use cases to replace it, to make sure that macro annotations have enough motivating use cases to be worth it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it does not work for the particular API design choices of MainArgs, it allows for many other kinds of main annotation use cases. For example, it can be used to implement the generalized main annotation in scala/scala3#19937.

*
* This method takes as an argument the `definition` that will be transformed by the macro annotation.
* It returns a non-empty list containing the modified version of the annotated definition.
* The new tree for the definition must use the original symbol.
* New definitions can be added to the list before or after the transformed definitions, this order
* will be retained. New definitions will not be visible from outside the macro expansion.
*
* For classes or objects annotated with a macro annotation, we also provide an optional `companion`
* containing the class or object companion. It is `None` if the definition does not have a companion.
* If `definition` represents the class, then the companion is the module class of the object; if the
* `definition` is a module class, then the companion is the class itself. This companion can be
* transformed in the same way as the `definition`. If transformed it should be added to the resulting
* list of definitions.
*
* If the definition is a `def` with a macro annotated parameter, the `companion` will contain the macro
* annotated parameter.
* ...
*/
def transform(using Quotes)(
definition: quotes.reflect.Definition,
companion: Option[quotes.reflect.Definition]
): List[quotes.reflect.Definition]
```

The definition of the `@memoize` macro annotation of _Example 2_ would be defined as follows:


```scala
import scala.quoted.*

class memoize extends MacroAnnotation:
def transform(using Quotes)(
definition: quotes.reflect.Definition,
companion: Option[quotes.reflect.Definition]
): List[quotes.reflect.Definition] =
... // Returns 2 trees: the cache definition and the modified `tree` definition
```
A full implementation of a `@memoize` macro annotation can be found on the [MacroAnnotation documentation][MemoizeMacro].


### Specification

The specification of macro annotations consists of 3 parts: the definition of the macro annotation trait, the evaluation of macro annotations, and the restrictions of macro annotations


#### Additions to the Standard Library

The only addition we need is the interface definition for the macro annotation.

```scala
package scala.annotation
import scala.quoted.*

trait MacroAnnotation extends StaticAnnotation:
def transform(using Quotes)(
definition: quotes.reflect.Definition,
companion: Option[quotes.reflect.Definition]
): List[quotes.reflect.Definition]
```

The other library definition we need is `Quotes`, which is already part of the standard library for inline macro definitions.
This interface can be used as it is, but we might need to add some new methods to it.
For example, we need to extend the symbol constructors to allow the creation of any kind of definition.
Some of these were not implemented as they were not useful in the context of inline macros (local definitions).

#### Evaluation of Macro Annotations

##### Macro Implementation Definition
Compiling a macro annotation definition does not have any special requirements.

These are just regular Scala definitions that implement the `MacroAnnotation` trait.
This is similar to a macro implementation of an inline macro, in that case, implementation is
just a `def` that returns an `Expr`, in our case it returns a `List[Definition]`.

##### Macro Expansion

The expansion of macro annotations should be performed at the same time as inlining.
Macro annotations might generate code that needs to be inlined, for example: a reference to the `assert` method.
Inlined code could also generate code that contains a macro annotation that needs to be expanded, for example: `'{ @memoize def f(x: T): U = ...; f }`.
Macro annotations in quoted code will not be expanded until that code is inlined.
However, the reflection API currently restricts the addition of annotations to new symbols.

The inlining phase is located after the pickler phases, where the tree representation aligns with the TASTy.
This is important because the macro annotation implementation will need to manipulate the TASTy representation of the annotated definition.
The `Inlining` follows just after the `Pickler` phase, and therefore TASTy does not contain the expanded representation of the trees.
By making this expansion after pickling we guarantee that the TASTy representation does not contain any generated definition.
This is important to make the typing process deterministic, a new definition would affect the typing of the rest of the program.

The process of expanding a macro annotation is similar to the process of evaluating the top-level splice of an inline macro.
The macro annotation `@myMacroAnnot(x, y, z)` is evaluated/interpreted by the compiler,
with the same interpreter used to interpret the contents of a top-level splice.
This implies that all arguments to the macro annotation must be constant values.
In this case result of the interpretation is an instance of `MacroAnnotation`.
We then call the `transform` method of this instance with the `tree` of the annotated definition.
The result of this call is a list of definitions that will replace the original definition.

Once we have the list of definitions that the macro annotation has generated we need to insert them into the current context.
If it was a local definition we will just insert all generated definitions in order.
Otherwise, if the definition was in a class, we need to enter them into that class.
Note that the reflection API is prohibited from modifying symbols and therefore cannot enter the new definitions into the class.
The symbols created with reflection will just state its the owner; it is up to the compiler to perform sanity checks and then enter the new definitions into the class.
As we do not want to allow a macro annotation to see new definitions added by another macro annotation, we will enter the symbol at the end of the inlining phase.


##### Expansion order

A definition might be annotated with multiple macro annotations, for example: `@log @memoize def fib(n: Int) = ...`.
We need to expand them in some reliable order, either left to right or right to left.
These two orders are equivalently acceptable, as a tiebreaker we use the right to left that Scala 2 macro annotations chose.
It is not encouraged to write one macro annotation relying on another that annotates the same definition.
If so, the user could write a new macro annotation that does this transformation calling into the implementation of the other macro annotations.
If we have macro annotated parameters we start from the last one, to follow the same order as the annotations of the definition itself.

We can also have a macro annotation on a definition that is nested in a macro annotated definition.
We can also follow the Scala 2 design and expand the outer macro annotation first, then the inner ones.

#### Restrictions

The `MacroAnnotation.transform` methods imposed some restrictions on what can be returned in the list of definitions.

* The list must contain the transformed definition (a definition with the same symbol as the original definition).
* If a companion is modified, it must be returned in the list of definitions and must have the original symbol.
* All definitions in the result must have the same owner. The owner can be recovered from `Symbol.spliceOwner`.
* Special case: an annotated top-level `def`, `val`, `var`, `lazy val` can return a `class`/`object`
definition that is owned by the package or package object.
* Can not return a `type`. At this abstraction level, they are not really useful.
* Annotated top-level `class`/`object` can not return top-level `def`, `val`, `var`, `lazy val`.
* Trees must be well-formed TASTy definition. `-Xcheck-macros` and `-Ycheck` will help to enforce this.



### Compatibility
The introduction of the `MacroAnnotation` trait does not affect binary nor TASTy compatibility.

Crucially, macro annotations cannot break TASTy compatibility because they are evaluated after pickling.
They should also not break binary compatibility as newly added definitions by macro annotations are assumed to only be used from within the macro expanded code.

Macro annotations can be used to patch binary incompatibilities by generating an old definition that is not generated anymore.


### Feature Interactions


#### Typer
As we transform the definitions after pickling, the typer only needs to be able to type the reference to the macro annotation.

#### Separate/incremental compilation
As we transform the definitions after pickling, separate/incremental compilation is not affected by the macro annotations.
Comment on lines +287 to +288
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if a macro annotated member of a trait generates a new private val/var/lazy val in the trait? That would still affect classes extending that trait, in a way that incremental compilation should be aware of.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure how we would support these. They might need to be disallowed.


#### Compilation suspension
Inline macros introduced the concept of compilation suspension to be able to define and use a macro in the same project.
If a call to a macro discovers statically or dynamically that it cannot expand because the macro implementation has not been compiled yet, then the compiler will suspend the compilation of the file that contains the macro call.
Once it finishes the compilation of other files, the compiler will retry the compilation of the suspended file.
This is repeated until all files are compiled or no progress is made due to a cyclic macro dependency.

This same idea applies to macro annotations. The macro annotation `transform` method must be compiled before we can expand the macro annotation in another file.

#### Inlining and macros
Given that macro annotations can generate code that contains inline calls and that inline code can contain macro annotations,
we need to ensure that after any expansion of a macro annotation or inline call we check for further macro expansions.
This is already the case with inline call, we just need to make sure the same happens for macro annotations.

Transparent inline method calls generated by macro annotations will expand in the same way as a transparent inline method call inside a non-transparent inline method definition.
This implies that the type refinement of the transparent inline method will be limited and will not affect the typing process.


#### IDE and other tools

Given that we only generate code after pickling, the IDEs that consume TASTy will be agnostic to these changes. All
definitions will align with source and typer as it does for non-annotated classes.


## Alternatives

### Change the order of evaluation of nested macro annotations
There might be reasons to evaluate the inner macro annotation first, then the outer one.
In that case, we have two options: we reverse the order of evaluation, or we add the ability to specify the order of evaluation of each macro annotation.

### Allow new definitions to be macro annotated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what this means.

Copy link
Contributor Author

@nicolasstucki nicolasstucki Mar 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This refers to new definitions created by the macro expansion. I will reword it.

This would complicate the expansion process. The traversal of macro annotations and expansion order would need to be refined.
The motivation to not add this feature is performance and simplicity.
If users are allowed to do this they would use it as a simple crutch in all their implementation.
But they can do the same by just instantiating the macro annotation and calling the `transform` method themselves.

### Expand macro annotations while typing

I a way, these would be equivalent to transparent macro annotations.
The issue is that they should not expose new APIs to avoid problems with compilation order.
This would defeat the purpose of adding transparent macro annotation.


## Related work

* The initial design space exploration was done by Zhendong Ang as part of his Master's Thesis at EPFL. His work was reported in [Macro Annotations for Scala 3][Zhendong].
* The design of the Scala 3 inlining, multi-stage programming, and reflection can be found in [Scalable Metaprogramming in Scala 3][Stucki].
* An experimental version of `MacroAnnotation` is already available in 3.3 and 3.4. This version does not yet include the `companion` parameter (see [#19676][Companion]).
* Scala 3 macro annotations were motivated by the [Scala 2 macro annotations][Scala2Macros]. Detailed description of Scala 2 macros can be found in [Ch 5.3 of Unification of Compile-Time and Runtime Metaprogramming in Scala][Burmako]


## FAQ


[Zhendong]: https://infoscience.epfl.ch/record/294615?ln=en
[Stucki]: https://infoscience.epfl.ch/record/299370?ln=en
[Scala2Macros]: https://docs.scala-lang.org/overviews/macros/annotations.html
[Burmako]: https://infoscience.epfl.ch/record/226166?ln=en
[MemoizeMacro]: https://github.com/lampepfl/dotty/blob/cba1cfc3191835dd6821ef5bcd204fc4459697da/library/src/scala/annotation/MacroAnnotation.scala#L43-L71
[EqualsMacro]: https://github.com/lampepfl/dotty/blob/cba1cfc3191835dd6821ef5bcd204fc4459697da/library/src/scala/annotation/MacroAnnotation.scala#L99-L184
[Companion]: https://github.com/lampepfl/dotty/issues/19676