-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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 annotation (part 1) #16392
Macro annotation (part 1) #16392
Conversation
7e5a5c3
to
74d7433
Compare
0cc6b48
to
fe4769a
Compare
Why macro annotations cannot be used for It will be great for migration if macro annotations can be used with |
That will be added in the next PR. |
fe4769a
to
a603341
Compare
a603341
to
33d1889
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, I played around with some macro annotations and they behave how I expected. I put some clarification questions in the comments but none of them are blockers.
|
||
/** Transform the `tree` definition and add other definitions | ||
* | ||
* This method takes as argument the reflected representation of the annotated definition. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is a reflected representation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems it should just be
* This method takes as argument the annotated definition.
@@ -32,7 +32,7 @@ import dotty.tools.dotc.reporting.Message | |||
import dotty.tools.repl.AbstractFileClassLoader | |||
|
|||
/** Tree interpreter for metaprogramming constructs */ | |||
abstract class Interpreter(pos: SrcPos, classLoader: ClassLoader)(using Context): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Background knowledge question - other than SpliceInterpreter
in Splicer.scala, prior to this PR, was this class used anywhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The splice interpreter was the only use of this class. This class was factored out in a separate PR to make this PR simpler to review.
@@ -65,7 +65,7 @@ abstract class Interpreter(pos: SrcPos, classLoader: ClassLoader)(using Context) | |||
|
|||
// TODO disallow interpreted method calls as arguments | |||
case Call(fn, args) => | |||
if (fn.symbol.isConstructor && fn.symbol.owner.owner.is(Package)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you mind explaining why was this check removed (or actually, what was it doing before and why is it not needed anymore?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check was there to make sure we only called interpretNew
on a top-level class. I noticed that our current implementation of interpretNew
also supports classes nested in objects.
@@ -185,7 +185,8 @@ abstract class Interpreter(pos: SrcPos, classLoader: ClassLoader)(using Context) | |||
loadModule(fn.moduleClass) | |||
|
|||
private def interpretNew(fn: Symbol, args: => List[Object])(implicit env: Env): Object = { | |||
val clazz = loadClass(fn.owner.fullName.toString) | |||
val className = fn.owner.fullName.toString.replaceAll("\\$\\.", "\\$") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this to handle the names generated by newUniqueMethod/Val?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. It is to handle classes that are nested withing objects.
For example if we have the class B.A
defined in object B
(which has a companion class B$
), we would see the name B$.A
at this point in the compiler. But we lift those classes and therfore we will get a class B$A
in the bytecode. This transforms B$.A
into B$A
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably use mangledString
and not toString
to account for name encoding (e.g., try definining and using class +
). But also, what about inner classes? If I have class Outer { class Inner }; val o = new Outer; new o.Inner
, then the binary class name is also Outer$Inner
. I have logic in https://github.com/lampepfl/dotty/blob/e8428102f8372e9c4227a3a13970e15bfc5d94c1/compiler/src/dotty/tools/dotc/sbt/ExtractDependencies.scala#L147-L171 to do this which could be extracted in its own method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will use mangleString
.
We do not need to handle inner classes for now. We do not allow macro annotations to be defined as inner classes (restriction). We wont encounter new
in top-level splices neither.
val sym = staticRef(className.toTypeName).symbol | ||
if (sym.isDefinedInCurrentRun) Some(sym) else None | ||
} | ||
def unapply(targetException: Throwable)(using Context): Option[Symbol] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this because you may end up in a situation where the class defined by the macro annotation does not yet have a definition?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, does not have a definition in bytecode.
This can happen when we compile the macro definition and use at the same time. In that case we have the definition as a symbol in the compiler but the bytecode for the macro definition has not been generated yet.
@@ -483,6 +487,15 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase | |||
private def normalizeErasedRhs(rhs: Tree, sym: Symbol)(using Context) = | |||
if (sym.isEffectivelyErased) dropInlines.transform(rhs) else rhs | |||
|
|||
private def checkForMacrosAnnotations(tree: Tree)(using Context) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand the distinction between MacroAnnotations and TastyAnnotations, other than one gets checked on a tree the other gets checked on a symbol. what's the difference, conceptually?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a mistake on my part. It should be TastyAnnotations. I will update that.
We started with the name Macro Annotation following the Scala 2 name of the feature. But then decided we should use a different name to make it clear that they are the same. This will help users when they google documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed back to MacroAnnotation
after #16392 (comment)
@@ -47,6 +47,7 @@ class Compiler { | |||
/** Phases dealing with TASTY tree pickling and unpickling */ | |||
protected def picklerPhases: List[List[Phase]] = | |||
List(new Pickler) :: // Generate TASTY info | |||
List(new ExpandAnnotations) :: // Execute TASTy macro annotation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why move the annotation expansion into a separate phase now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pushed a commit I should not. The macro annotation expansion will be done in Inlining.
I was experimenting to see if it would be possible to extract it in the future to support some extra features.
33d1889
to
3ff58dc
Compare
3ff58dc
to
6853a75
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise LGTM
@@ -160,6 +164,9 @@ object CompilationUnit { | |||
Feature.handleGlobalLanguageImport(prefix, imported) | |||
case _ => | |||
case _ => | |||
for annot <- tree.symbol.annotations do | |||
if annot.tree.symbol.denot != NoDenotation && annot.tree.symbol.owner.derivesFrom(defn.TastyAnnotationClass) then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if annot.tree.symbol.denot != NoDenotation && annot.tree.symbol.owner.derivesFrom(defn.TastyAnnotationClass) then | |
if annot.tree.symbol.maybeOwner.derivesFrom(defn.TastyAnnotationClass) then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated. Also updated the same pattern in TastyAnnotations.isTastyAnnotation
|
||
/** Base trait for TASTy annotation that will transform a definition */ | ||
@experimental | ||
trait TastyAnnotation extends StaticAnnotation: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think TastyAnnotation is too unclear. I would never think of a macro when seeing that name. Instead I would think it's something to do with the internals of Tasty files.
I think the best thing to do is to go back to MacroAnnotation
. We will have to explain how they are different from Scala 2 macro annotations, but it's better to do that directly. Otherwise, when someone googles for Scala 3 macro annotation, they will only find Scala 2 macro annotation stuff. I don't think we can establish TastyAnnotation as an obvious term that people will search for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed back to MacroAnnotation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yearning for that content to google for ;)
The thesis linked above is a good intro; I assume the test cases (chapter 5) which were only outlined there, not provided, are the ones in these PRs? In any case, documentation or a further reading list would be appreciated in due time :) Excited for this feature!
&& StagingContext.level == 0 | ||
&& TastyAnnotations.hasTastyAnnotation(tree.symbol) | ||
then | ||
val trees = new TastyAnnotations(Inlining.this).expandAnnotations(tree) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style: We usually prefer an explicit self name like thisPhase
over a qualified Inlining.this
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
ea3c06c
to
07e395d
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) * Can only be used on `def`, `val`, `lazy val` and `var` * Can only add `def`, `val`, `lazy val` and `var` definitions Based on: * scala#15626 * https://infoscience.epfl.ch/record/294615?ln=en
07e395d
to
19e37c8
Compare
Rebased and fixed conflicts in the |
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: * Includes #16513 * Follows #16392 * Followed by #16534
@nicolasstucki thank you for all your work on this! Apologies if this isn't the best place to ask, but I was testing out the 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)
| }
def fib(n: Int): Int
scala> fibCache
-- [E006] Not Found Error: -----------------------------------------------------
1 |fibCache
|^^^^^^^^
|Not found: fibCache |
It is not possible to refer to the generated code from external code. This is by design. |
@mrdziuban see also the discussion in https://contributors.scala-lang.org/t/scala-3-macro-annotations-and-code-generation/6035 |
Add basic support for macro annotations
scala.annotations.MacroAnnotation
Inlining
phasedef
,val
,lazy val
andvar
def
,val
,lazy val
andvar
definitionsExample
with this macro annotation a user can write
and the macro will modify the definition to create
Based on
Followed by