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
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/NameKinds.scala
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ object NameKinds {
val UniqueInlineName: UniqueNameKind = new UniqueNameKind("$i")
val InlineScrutineeName: UniqueNameKind = new UniqueNameKind("$scrutinee")
val InlineBinderName: UniqueNameKind = new UniqueNameKind("$proxy")
val MacroNames: UniqueNameKind = new UniqueNameKind("$macro$")

/** A kind of unique extension methods; Unlike other unique names, these can be
* unmangled.
Expand Down
16 changes: 7 additions & 9 deletions compiler/src/dotty/tools/dotc/transform/MacroAnnotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,10 @@ class MacroAnnotations(thisPhase: DenotTransformer):
def expandAnnotations(tree: MemberDef)(using Context): List[DefTree] =
if !hasMacroAnnotation(tree.symbol) then
List(tree)
else if tree.symbol.is(Module) then
if tree.symbol.isClass then // error only reported on module class
report.error("macro annotations are not supported on object", tree)
else if tree.symbol.is(Module) && !tree.symbol.isClass then
// only class is transformed
List(tree)
else if tree.symbol.isClass then
report.error("macro annotations are not supported on class", tree)
List(tree)
else if tree.symbol.isType then
else if tree.symbol.isType && !tree.symbol.isClass then
report.error("macro annotations are not supported on type", tree)
List(tree)
else
Expand Down Expand Up @@ -126,11 +122,13 @@ class MacroAnnotations(thisPhase: DenotTransformer):
private def checkAndEnter(newTree: Tree, annotated: Symbol, annot: Annotation)(using Context) =
val sym = newTree.symbol
if sym.isClass then
report.error("Generating classes is not supported", annot.tree)
report.error(i"macro annotation returning a `class` is not yet supported. $annot tried to add $sym", annot.tree)
else if sym.isType then
report.error("Generating type is not supported", annot.tree)
report.error(i"macro annotation cannot return a `type`. $annot tried to add $sym", annot.tree)
else if sym.owner != annotated.owner then
report.error(i"macro annotation $annot added $sym with an inconsistent owner. Expected it to be owned by ${annotated.owner} but was owned by ${sym.owner}.", annot.tree)
else if annotated.isClass && annotated.owner.is(Package) /*&& !sym.isClass*/ then
report.error(i"macro annotation can not add top-level ${sym.showKind}. $annot tried to add $sym.", annot.tree)
else
sym.enteredAfter(thisPhase)

Expand Down
11 changes: 5 additions & 6 deletions compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2485,17 +2485,14 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
newMethod(owner, name, tpe, Flags.EmptyFlags, noSymbol)
def newMethod(owner: Symbol, name: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol =
dotc.core.Symbols.newSymbol(owner, name.toTermName, flags | dotc.core.Flags.Method, tpe, privateWithin)
def newUniqueMethod(owner: Symbol, namePrefix: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol =
val name = NameKinds.UniqueName.fresh(namePrefix.toTermName)
dotc.core.Symbols.newSymbol(owner, name, dotc.core.Flags.PrivateMethod | flags, tpe, privateWithin)
def newVal(owner: Symbol, name: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol =
dotc.core.Symbols.newSymbol(owner, name.toTermName, flags, tpe, privateWithin)
def newUniqueVal(owner: Symbol, namePrefix: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol =
val name = NameKinds.UniqueName.fresh(namePrefix.toTermName)
dotc.core.Symbols.newSymbol(owner, name, flags, tpe, privateWithin)
def newBind(owner: Symbol, name: String, flags: Flags, tpe: TypeRepr): Symbol =
dotc.core.Symbols.newSymbol(owner, name.toTermName, flags | Case, tpe)
def noSymbol: Symbol = dotc.core.Symbols.NoSymbol

def freshName(prefix: String): String =
NameKinds.MacroNames.fresh(prefix.toTermName).toString
end Symbol

given SymbolMethods: SymbolMethods with
Expand All @@ -2519,6 +2516,8 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
def name: String = self.denot.name.toString
def fullName: String = self.denot.fullName.toString

def info: TypeRepr = self.denot.info

def pos: Option[Position] =
if self.exists then Some(self.sourcePos) else None

Expand Down
122 changes: 116 additions & 6 deletions library/src/scala/annotation/MacroAnnotation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ trait MacroAnnotation extends StaticAnnotation:
*
* All definitions in the result must have the same owner. The owner can be recovered from `tree.symbol.owner`.
*
* The result cannot contain `class`, `object` or `type` definition. This limitation will be relaxed in the future.
* The result cannot add new `class`, `object` or `type` definition. This limitation will be relaxed in the future.
*
* When developing and testing a macro annotation, you must enable `-Xcheck-macros` and `-Ycheck:all`.
* IMPORTANT: When developing and testing a macro annotation, you must enable `-Xcheck-macros` and `-Ycheck:all`.
*
* Example:
* Example 1:
* This example shows how to modify a `def` and add a `val` next to it using a macro annotation.
* ```scala
* import scala.quoted.*
* import scala.collection.mutable
Expand All @@ -34,7 +35,8 @@ trait MacroAnnotation extends StaticAnnotation:
* case DefDef(name, TermParamClause(param :: Nil) :: Nil, tpt, Some(rhsTree)) =>
* (param.tpt.tpe.asType, tpt.tpe.asType) match
* case ('[t], '[u]) =>
* val cacheSymbol = Symbol.newUniqueVal(Symbol.spliceOwner, name + "Cache", TypeRepr.of[mutable.Map[t, u]], Flags.Private, Symbol.noSymbol)
* val cacheName = Symbol.freshName(name + "Cache")
* val cacheSymbol = Symbol.newVal(Symbol.spliceOwner, cacheName, TypeRepr.of[mutable.Map[t, u]], Flags.Private, Symbol.noSymbol)
* val cacheRhs =
* given Quotes = cacheSymbol.asQuotes
* '{ mutable.Map.empty[t, u] }.asTerm
Expand All @@ -60,10 +62,10 @@ trait MacroAnnotation extends StaticAnnotation:
* ```
* and the macro will modify the definition to create
* ```scala
* val fibCache =
* val fibCache$macro$1 =
* scala.collection.mutable.Map.empty[Int, Int]
* def fib(n: Int): Int =
* fibCache.getOrElseUpdate(
* fibCache$macro$1.getOrElseUpdate(
* n,
* {
* println(s"compute fib of $n")
Expand All @@ -72,6 +74,114 @@ trait MacroAnnotation extends StaticAnnotation:
* )
* ```
*
* Example 2:
* This example shows how to modify a `class` using a macro annotation.
* It shows how to override inherited members and add new ones.
* ```scala
* import scala.annotation.{experimental, MacroAnnotation}
* import scala.quoted.*
*
* @experimental
* class equals extends MacroAnnotation:
* def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
* import quotes.reflect.*
* tree match
* case ClassDef(className, ctr, parents, self, body) =>
* val cls = tree.symbol
*
* val constructorParameters = ctr.paramss.collect { case clause: TermParamClause => clause }
* if constructorParameters.size != 1 || constructorParameters.head.params.isEmpty then
* report.errorAndAbort("@equals class must have a single argument list with at least one argument", ctr.pos)
* def checkNotOverridden(sym: Symbol): Unit =
* if sym.overridingSymbol(cls).exists then
* report.error(s"Cannot override ${sym.name} in a @equals class")
*
* val fields = body.collect {
* case vdef: ValDef if vdef.symbol.flags.is(Flags.ParamAccessor) =>
* Select(This(cls), vdef.symbol).asExpr
* }
*
* val equalsSym = Symbol.requiredMethod("java.lang.Object.equals")
* checkNotOverridden(equalsSym)
* val equalsOverrideSym = Symbol.newMethod(cls, "equals", equalsSym.info, Flags.Override, Symbol.noSymbol)
* def equalsOverrideDefBody(argss: List[List[Tree]]): Option[Term] =
* given Quotes = equalsOverrideSym.asQuotes
* cls.typeRef.asType match
* case '[c] =>
* Some(equalsExpr[c](argss.head.head.asExpr, fields).asTerm)
* val equalsOverrideDef = DefDef(equalsOverrideSym, equalsOverrideDefBody)
*
* val hashSym = Symbol.newVal(cls, Symbol.freshName("hash"), TypeRepr.of[Int], Flags.Private | Flags.Lazy, Symbol.noSymbol)
* val hashVal = ValDef(hashSym, Some(hashCodeExpr(className, fields)(using hashSym.asQuotes).asTerm))
*
* val hashCodeSym = Symbol.requiredMethod("java.lang.Object.hashCode")
* checkNotOverridden(hashCodeSym)
* val hashCodeOverrideSym = Symbol.newMethod(cls, "hashCode", hashCodeSym.info, Flags.Override, Symbol.noSymbol)
* val hashCodeOverrideDef = DefDef(hashCodeOverrideSym, _ => Some(Ref(hashSym)))
*
* val newBody = equalsOverrideDef :: hashVal :: hashCodeOverrideDef :: body
* List(ClassDef.copy(tree)(className, ctr, parents, self, newBody))
* case _ =>
* report.error("Annotation only supports `class`")
* List(tree)
*
* private def equalsExpr[T: Type](that: Expr[Any], thisFields: List[Expr[Any]])(using Quotes): Expr[Boolean] =
* '{
* $that match
* case that: T @unchecked =>
* ${
* val thatFields: List[Expr[Any]] =
* import quotes.reflect.*
* thisFields.map(field => Select('{that}.asTerm, field.asTerm.symbol).asExpr)
* thisFields.zip(thatFields)
* .map { case (thisField, thatField) => '{ $thisField == $thatField } }
* .reduce { case (pred1, pred2) => '{ $pred1 && $pred2 } }
* }
* case _ => false
* }
*
* private def hashCodeExpr(className: String, thisFields: List[Expr[Any]])(using Quotes): Expr[Int] =
* '{
* var acc: Int = ${ Expr(scala.runtime.Statics.mix(-889275714, className.hashCode)) }
* ${
* Expr.block(
* thisFields.map {
* case '{ $field: Boolean } => '{ if $field then 1231 else 1237 }
* case '{ $field: Byte } => '{ $field.toInt }
* case '{ $field: Char } => '{ $field.toInt }
* case '{ $field: Short } => '{ $field.toInt }
* case '{ $field: Int } => field
* case '{ $field: Long } => '{ scala.runtime.Statics.longHash($field) }
* case '{ $field: Double } => '{ scala.runtime.Statics.doubleHash($field) }
* case '{ $field: Float } => '{ scala.runtime.Statics.floatHash($field) }
* case '{ $field: Null } => '{ 0 }
* case '{ $field: Unit } => '{ 0 }
* case field => '{ scala.runtime.Statics.anyHash($field) }
* }.map(hash => '{ acc = scala.runtime.Statics.mix(acc, $hash) }),
* '{ scala.runtime.Statics.finalizeHash(acc, ${Expr(thisFields.size)}) }
* )
* }
* }
* ```
* with this macro annotation a user can write
* ```scala sc:nocompile
* @equals class User(val name: String, val id: Int)
* ```
* and the macro will modify the class definition to generate the following code
* ```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
* private lazy val hash$macro$1: Int =
* var acc = 515782504 // scala.runtime.Statics.mix(-889275714, "User".hashCode)
* acc = scala.runtime.Statics.mix(acc, scala.runtime.Statics.anyHash(name))
* acc = scala.runtime.Statics.mix(acc, id)
* scala.runtime.Statics.finalizeHash(acc, 2)
* override def hashCode(): Int = hash$macro$1
* ```
*
* @param Quotes Implicit instance of Quotes used for tree reflection
* @param tree Tree that will be transformed
*/
Expand Down
55 changes: 17 additions & 38 deletions library/src/scala/quoted/Quotes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3669,25 +3669,6 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
*/
def newMethod(parent: Symbol, name: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol

/** Generates a new method symbol with the given parent, name and type.
*
* To define a member method of a class, use the `newMethod` within the `decls` function of `newClass`.
*
* @param parent The owner of the method
* @param name The name of the method
* @param tpe The type of the method (MethodType, PolyType, ByNameType)
* @param flags extra flags to with which the symbol should be constructed
* @param privateWithin the symbol within which this new method symbol should be private. May be noSymbol.
*
* This symbol starts without an accompanying definition.
* It is the meta-programmer's responsibility to provide exactly one corresponding definition by passing
* this symbol to the DefDef constructor.
*
* @note As a macro can only splice code into the point at which it is expanded, all generated symbols must be
* direct or indirect children of the reflection context's owner.
*/
@experimental def newUniqueMethod(parent: Symbol, namePrefix: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol

/** Generates a new val/var/lazy val symbol with the given parent, name and type.
*
* This symbol starts without an accompanying definition.
Expand All @@ -3706,25 +3687,6 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
*/
def newVal(parent: Symbol, name: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol

/** Generates a new val/var/lazy val symbol with the given parent, name prefix and type.
*
* This symbol starts without an accompanying definition.
* It is the meta-programmer's responsibility to provide exactly one corresponding definition by passing
* this symbol to the ValDef constructor.
*
* Note: Also see newVal
* Note: Also see ValDef.let
*
* @param parent The owner of the val/var/lazy val
* @param name The name of the val/var/lazy val
* @param tpe The type of the val/var/lazy val
* @param flags extra flags to with which the symbol should be constructed
* @param privateWithin the symbol within which this new method symbol should be private. May be noSymbol.
* @note As a macro can only splice code into the point at which it is expanded, all generated symbols must be
* direct or indirect children of the reflection context's owner.
*/
@experimental def newUniqueVal(parent: Symbol, namePrefix: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol

/** Generates a pattern bind symbol with the given parent, name and type.
*
* This symbol starts without an accompanying definition.
Expand All @@ -3742,6 +3704,18 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>

/** Definition not available */
def noSymbol: Symbol

/** A fresh name for class or member symbol names.
*
* Fresh names are constructed using the following format `prefix + "$macro$" + freshIndex`.
* The `freshIndex` are unique within the current source file.
*
* Examples: See `scala.annotation.MacroAnnotation`
*
* @param prefix Prefix of the fresh name
*/
@experimental
def freshName(prefix: String): String
}

/** Makes extension methods on `Symbol` available without any imports */
Expand Down Expand Up @@ -3772,6 +3746,10 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
/** The full name of this symbol up to the root package */
def fullName: String

/** Type of the definition */
@experimental
def info: TypeRepr

/** The position of this symbol */
def pos: Option[Position]

Expand Down Expand Up @@ -4411,6 +4389,7 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
end extension
}


///////////////
// POSITIONS //
///////////////
Expand Down
2 changes: 1 addition & 1 deletion tests/neg-macros/annot-accessIndirect/Macro_1.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import scala.quoted._
class hello extends MacroAnnotation {
def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
import quotes.reflect._
val helloSymbol = Symbol.newUniqueVal(Symbol.spliceOwner, "hello", TypeRepr.of[String], Flags.EmptyFlags, Symbol.noSymbol)
val helloSymbol = Symbol.newVal(Symbol.spliceOwner, Symbol.freshName("hello"), TypeRepr.of[String], Flags.EmptyFlags, Symbol.noSymbol)
val helloVal = ValDef(helloSymbol, Some(Literal(StringConstant("Hello, World!"))))
List(helloVal, tree)
}
Loading