Scala 3 Macro Annotations (SIP-63) discussions

SIP-63 for Scala 3 Macro Annotations can be found here: https://github.com/scala/improvement-proposals/pull/80

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.

6 Likes

Since example code tends to get C&P’d widely, it might be good to add a note in the @memoize section that this pattern can suffer from a race condition where the initialization logic can be called multiple times… Sometimes that’s OK, sometimes it’s not.

ConcurrentHashMap.putIfAbsent is the best way I know of to do this if the parameter is significant, or Suppliers.memoize if it’s not parameterized :slight_smile:

  • 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.
  1. Why this special case? Seems arbitrary: Why only top-level? Why only def, val, var, lazy val?
  2. What if another definition bar references a top-level def foo, where foo is replaced by a class during the macro expansion.
1 Like

Good questions. I answered bellow and will update the SIP document to make this clearer.

  1. Why this special case? Why only top-level? Why only def, val, var, lazy val?

It is related to the encoding of top-level definitions. These definitions are moved into a generated package object. By default, macro annotation top-level definition can be added within this package object.

To support the generation of @main-like annotations on defs, we need to be able to generate a class that is outside the package object.

Example:

@myMain def f: T = ...

is typed as

object filename$package {
  @myMain def f: T = ...
}

In this case we want the macro annotation to generate

object filename$package {
  @myMain def f: T = ...
}
object f {
  def main(args: Array[String]): Unit = ...
}

and not

object filename$package {
  @myMain def f: T = ...
  object f {
    def main(args: Array[String]): Unit = ...
  }
}
  1. What if another definition bar references a top-level def foo, where foo is replaced by a class during the macro expansion.

I should have probably written “Special case: an annotated top-level def, val, var, lazy val can add a class/object definition that is owned by the package or package object.”

In general, a macro annotation cannot modify the signature of the annotated definition. If it was a def it will still be a def after the expansion. The annotation can only add new definitions that are used be the macro annotated definition.

2 Likes

Great point. I will update the example and use a scala.collection.concurrent.Map.

I see. So the main point of the special case is

“owned by the package or package object.”
[As opposed to the synthetic object owning the annotated definition.]

which I thought was saying nothing :slight_smile:, because I didn’t realize that that’s different from the owner of the annotated definition.

Since these generated additional definitions are not going to be visible to Scala code outside of the macro expansion, it’s hard for me to imagine a different example than the already given @main-like annotations. Do you have any other examples?

Do you have any other examples?

Not really, main annotations are the only reason why we added this extra rule. The reason is to allow Java reflection to find the class with the expected name (witouth the package object name in the way). This could apply to other use cases where Java reflection needs to load somthing based on the name.

Have you considered also making the same API with transparent macro annotations, which will do all processing during the typing, in the same way as transparent macros ?

This will allow changes to the annotated object shape to be visible outside.

Yes, we have. These are problematic with soundness for many reasons. In general, there is a double vision problem where a definition can be seen before or after the expansion of the macro. Depending on the order in which macros are expanded, a program may end up with different semantics. All 3 bullet points in the Design constraints section would not be met if we allowed these kinds of macros.