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

Add reflect ClassDef.apply and Symbol.newClass #14124

Merged
merged 1 commit into from
Mar 24, 2022
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
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/Symbols.scala
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ object Symbols {
def complete(denot: SymDenotation)(using Context): Unit = {
val cls = denot.asClass.classSymbol
val decls = newScope
denot.info = ClassInfo(owner.thisType, cls, parentTypes.map(_.dealias), decls)
denot.info = ClassInfo(owner.thisType, cls, parentTypes.map(_.dealias), decls, selfInfo)
}
}
newClassSymbol(owner, name, flags, completer, privateWithin, coord, assocFile)
Expand Down
27 changes: 26 additions & 1 deletion compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,11 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
end ClassDefTypeTest

object ClassDef extends ClassDefModule:
def apply(cls: Symbol, parents: List[Tree], body: List[Statement]): ClassDef =
val untpdCtr = untpd.DefDef(nme.CONSTRUCTOR, Nil, tpd.TypeTree(dotc.core.Symbols.defn.UnitClass.typeRef), tpd.EmptyTree)
val ctr = ctx.typeAssigner.assignType(untpdCtr, cls.primaryConstructor)
tpd.ClassDefWithParents(cls.asClass, ctr, parents, body)

def copy(original: Tree)(name: String, constr: DefDef, parents: List[Tree], selfOpt: Option[ValDef], body: List[Statement]): ClassDef = {
val dotc.ast.Trees.TypeDef(_, originalImpl: tpd.Template) = original
tpd.cpy.TypeDef(original)(name.toTypeName, tpd.cpy.Template(originalImpl)(constr, parents, derived = Nil, selfOpt.getOrElse(tpd.EmptyValDef), body))
Expand Down Expand Up @@ -262,6 +267,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler

object DefDef extends DefDefModule:
def apply(symbol: Symbol, rhsFn: List[List[Tree]] => Option[Term]): DefDef =
assert(symbol.isTerm, s"expected a term symbol but received $symbol")
withDefaultPos(tpd.DefDef(symbol.asTerm, prefss =>
xCheckMacroedOwners(xCheckMacroValidExpr(rhsFn(prefss)), symbol).getOrElse(tpd.EmptyTree)
))
Expand Down Expand Up @@ -1049,6 +1055,9 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
object TypeTree extends TypeTreeModule:
def of[T <: AnyKind](using tp: scala.quoted.Type[T]): TypeTree =
tp.asInstanceOf[TypeImpl].typeTree
def ref(sym: Symbol): TypeTree =
assert(sym.isType, "Expected a type symbol, but got " + sym)
tpd.ref(sym)
end TypeTree

given TypeTreeMethods: TypeTreeMethods with
Expand Down Expand Up @@ -1806,7 +1815,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
(x.prefix, x.name.toString)
end TermRef

type TypeRef = dotc.core.Types.NamedType
type TypeRef = dotc.core.Types.TypeRef

object TypeRefTypeTest extends TypeTest[TypeRepr, TypeRef]:
def unapply(x: TypeRepr): Option[TypeRef & x.type] = x match
Expand Down Expand Up @@ -2456,6 +2465,20 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
def requiredModule(path: String): Symbol = dotc.core.Symbols.requiredModule(path)
def requiredMethod(path: String): Symbol = dotc.core.Symbols.requiredMethod(path)
def classSymbol(fullName: String): Symbol = dotc.core.Symbols.requiredClass(fullName)

def newClass(owner: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr]): Symbol =
assert(parents.nonEmpty && !parents.head.typeSymbol.is(dotc.core.Flags.Trait), "First parent must be a class")
val cls = dotc.core.Symbols.newNormalizedClassSymbol(
owner,
name.toTypeName,
dotc.core.Flags.EmptyFlags,
parents,
selfType.getOrElse(Types.NoType),
dotc.core.Symbols.NoSymbol)
cls.enter(dotc.core.Symbols.newConstructor(cls, dotc.core.Flags.Synthetic, Nil, Nil))
for sym <- decls(cls) do cls.enter(sym)
cls

def newMethod(owner: Symbol, name: String, tpe: TypeRepr): Symbol =
newMethod(owner, name, tpe, Flags.EmptyFlags, noSymbol)
def newMethod(owner: Symbol, name: String, tpe: TypeRepr, flags: Flags, privateWithin: Symbol): Symbol =
Expand Down Expand Up @@ -2624,6 +2647,8 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
def companionClass: Symbol = self.denot.companionClass
def companionModule: Symbol = self.denot.companionModule
def children: List[Symbol] = self.denot.children
def typeRef: TypeRef = self.denot.typeRef
def termRef: TermRef = self.denot.termRef

def show(using printer: Printer[Symbol]): String = printer.show(self)

Expand Down
114 changes: 105 additions & 9 deletions library/src/scala/quoted/Quotes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,15 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>

/** Methods of the module object `val ClassDef` */
trait ClassDefModule { this: ClassDef.type =>
/** Create a class definition tree
*
* @param cls The class symbol. A new class symbol can be created using `Symbol.newClass`.
* @param parents The parents trees class. The trees must align with the parent types of `cls`.
Copy link
Member

Choose a reason for hiding this comment

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

This description should be expanded (or some examples should be added) to explain that the parents can either be TypeTrees or Terms, and the terms are new calls so that arguments can be passed to them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

* Parents can be `TypeTree`s if they don't have term parameter,
* otherwise the can be `Term` containing the `New` applied to the parameters of the extended class.
* @param body List of members of the class. The members must align with the members of `cls`.
*/
@experimental def apply(cls: Symbol, parents: List[Tree /* Term | TypeTree */], body: List[Statement]): ClassDef
nicolasstucki marked this conversation as resolved.
Show resolved Hide resolved
nicolasstucki marked this conversation as resolved.
Show resolved Hide resolved
def copy(original: Tree)(name: String, constr: DefDef, parents: List[Tree /* Term | TypeTree */], selfOpt: Option[ValDef], body: List[Statement]): ClassDef
def unapply(cdef: ClassDef): (String, DefDef, List[Tree /* Term | TypeTree */], Option[ValDef], List[Statement])
}
Expand Down Expand Up @@ -1719,6 +1728,13 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
trait TypeTreeModule { this: TypeTree.type =>
/** Returns the tree of type or kind (TypeTree) of T */
def of[T <: AnyKind](using Type[T]): TypeTree

/** Returns a type tree reference to the symbol
*
* @param sym The type symbol for which we are creating a type tree reference.
*/
@experimental
def ref(typeSymbol: Symbol): TypeTree
}

/** Makes extension methods on `TypeTree` available without any imports */
Expand Down Expand Up @@ -2571,6 +2587,11 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
def typeSymbol: Symbol
def termSymbol: Symbol
def isSingleton: Boolean

/** The type of `member` as seen from prefix `self`.
*
* Also see `typeRef` and `termRef`
*/
def memberType(member: Symbol): TypeRepr

/** The base classes of this type with the class itself as first element. */
Expand Down Expand Up @@ -3578,19 +3599,73 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
/** The class Symbol of a global class definition */
def classSymbol(fullName: String): Symbol

/** Generates a new class symbol for a class with a parameterless constructor.
*
* Example usage:
* ```
* val name: String = "myClass"
* val parents = List(TypeTree.of[Object], TypeTree.of[Foo])
* def decls(cls: Symbol): List[Symbol] =
* List(Symbol.newMethod(cls, "foo", MethodType(Nil)(_ => Nil, _ => TypeRepr.of[Unit])))
*
* val cls = Symbol.newClass(Symbol.spliceOwner, name, parents = parents.map(_.tpe), decls, selfInfo = None)
* val fooSym = cls.declaredMethod("foo").head
*
* val fooDef = DefDef(fooSym, argss => Some('{println(s"Calling foo")}.asTerm))
* val clsDef = ClassDef(cls, parents, body = List(fooDef))
* val newCls = Typed(Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil), TypeTree.of[Foo])
*
* Block(List(clsDef), newCls).asExprOf[Foo]
* ```
* constructs the equivalent to
* ```
* '{
* class myClass() extends Object with Foo {
* def foo(): Unit = println("Calling foo")
* }
* new myClass(): Foo
* }
* ```
*
* @param parent The owner of the class
* @param name The name of the class
* @param parents The parent classes of the class. The first parent must not be a trait.
* @param decls The member declarations of the class provided the symbol of this class
* @param selfType The self type of the class if it has one
*
* 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 ClassDef 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 newClass(parent: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr]): Symbol

/** Generates a new method symbol with the given parent, name 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 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.
*/
*
* 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)
*
* 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.
*/
def newMethod(parent: Symbol, name: String, tpe: TypeRepr): Symbol

/** Works as the other newMethod, but with additional parameters.
*
* 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.
*/
Expand All @@ -3604,6 +3679,9 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
*
* Note: Also see reflect.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
Expand All @@ -3617,7 +3695,10 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
* It is the meta-programmer's responsibility to provide exactly one corresponding definition by passing
* this symbol to the BindDef constructor.
*
* @param parent The owner of the binding
* @param name The name of the binding
* @param flags extra flags to with which the symbol should be constructed
* @param tpe The type of the binding
* @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.
*/
Expand Down Expand Up @@ -3679,9 +3760,11 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
*
* symbol.tree.tpe
*
* It should be replaced by the following code:
* It should be replaced by one of the following:
*
* tp.memberType(symbol)
* symbol.typeRef
* symbol.termRef
*
*/
def tree: Tree
Expand Down Expand Up @@ -3880,6 +3963,19 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
@experimental
def asQuotes: Nested

/** Type reference to the symbol usable in the scope of its owner.
*
* To get a reference to a symbol from a specific prefix `tp`, use `tp.select(symbol)` instead.
* @see TypeReprMethods.select
*
* @pre symbol.isType returns true
*/
@experimental
def typeRef: TypeRef
Copy link
Member

Choose a reason for hiding this comment

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

It seems that we're missing an API to create a TypeRef/TermRef from a specific prefix: with foo.typeRef I can get a TypeRef this.foo and with pre.memberType(foo) I can get the info corresponding to pre.foo, but is there a way to generate the TypeRef pre.foo itself?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Doesn't TypeRepr.select provide that?

Copy link
Member

Choose a reason for hiding this comment

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

Oh indeed, I missed that one, maybe each of their documentation should @see the other one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


/** Term reference to the symbol usable in the scope of its owner. */
@experimental
def termRef: TermRef
end extension
}

Expand Down
10 changes: 10 additions & 0 deletions project/MiMaFilters.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ object MiMaFilters {
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.Quotes#reflectModule#CompilationInfoModule.XmacroSettings"),
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.deriving.Mirror.fromProductTyped"),
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.deriving.Mirror.fromTuple"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#ClassDefModule.apply"),
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.Quotes#reflectModule#ClassDefModule.apply"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolModule.newClass"),
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolModule.newClass"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.typeRef"),
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.typeRef"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.termRef"),
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.termRef"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#TypeTreeModule.ref"),
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.Quotes#reflectModule#TypeTreeModule.ref"),

// Private to the compiler - needed for forward binary compatibility
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.since"),
Expand Down
23 changes: 23 additions & 0 deletions tests/neg-macros/newClassExtendsNoParents/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import scala.quoted.*

inline def makeClass(inline name: String): Any = ${ makeClassExpr('name) }
private def makeClassExpr(nameExpr: Expr[String])(using Quotes): Expr[Any] = {
import quotes.reflect.*

val name = nameExpr.valueOrAbort
val parents = List.empty[Tree] // BUG: first parent is not a class
def decls(cls: Symbol): List[Symbol] = Nil

val cls = Symbol.newClass(Symbol.spliceOwner, name, parents = Nil, decls, selfType = None)
val clsDef = ClassDef(cls, parents, body = List())
val newCls = Typed(Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil), TypeTree.of[Object])
Copy link
Member

Choose a reason for hiding this comment

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

I guess this is orthogonal but in tpd we have a New overload that takes constructor arguments this could be useful here too


Block(List(clsDef), newCls).asExpr

// '{
// class `name`() {
// def foo(): Unit = println("Calling `name`.foo")
// }
// new `name`()
// }
}
1 change: 1 addition & 0 deletions tests/neg-macros/newClassExtendsNoParents/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
def test: Any = makeClass("foo") // error
31 changes: 31 additions & 0 deletions tests/neg-macros/newClassExtendsOnlyTrait/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import scala.quoted.*

inline def makeClass(inline name: String): Foo = ${ makeClassExpr('name) }
private def makeClassExpr(nameExpr: Expr[String])(using Quotes): Expr[Foo] = {
import quotes.reflect.*

val name = nameExpr.valueOrAbort
val parents = List(TypeTree.of[Foo]) // BUG: first parent is not a class
def decls(cls: Symbol): List[Symbol] =
List(Symbol.newMethod(cls, "foo", MethodType(Nil)(_ => Nil, _ => TypeRepr.of[Unit])))

val cls = Symbol.newClass(Symbol.spliceOwner, name, parents = parents.map(_.tpe), decls, selfType = None)
val fooSym = cls.declaredMethod("foo").head

val fooDef = DefDef(fooSym, argss => Some('{println(s"Calling ${$nameExpr}.foo")}.asTerm))
val clsDef = ClassDef(cls, parents, body = List(fooDef))
val newCls = Typed(Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil), TypeTree.of[Foo])

Block(List(clsDef), newCls).asExprOf[Foo]

// '{
// class `name`() extends Foo {
// def foo(): Unit = println("Calling `name`.foo")
// }
// new `name`()
// }
}

trait Foo {
def foo(): Unit
}
1 change: 1 addition & 0 deletions tests/neg-macros/newClassExtendsOnlyTrait/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
def test: Foo = makeClass("foo") // error
4 changes: 4 additions & 0 deletions tests/run-macros/newClass.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Constructing foo
class Test_2$package$foo$1
Constructing bar
class Test_2$package$bar$1
21 changes: 21 additions & 0 deletions tests/run-macros/newClass/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import scala.quoted.*

inline def makeClass(inline name: String): Any = ${ makeClassExpr('name) }
private def makeClassExpr(nameExpr: Expr[String])(using Quotes): Expr[Any] = {
import quotes.reflect.*

val name = nameExpr.valueOrAbort
val parents = List(TypeTree.of[Object])
def decls(cls: Symbol): List[Symbol] = Nil

val cls = Symbol.newClass(Symbol.spliceOwner, name, parents = parents.map(_.tpe), decls, selfType = None)

val clsDef = ClassDef(cls, parents, body = List('{println(s"Constructing ${$nameExpr}")}.asTerm))
val newCls = Typed(Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil), TypeTree.of[Object])

Block(List(clsDef), newCls).asExpr
// '{
// class `name`() { println("Constructing `name`") }
// new `name`()
// }
}
6 changes: 6 additions & 0 deletions tests/run-macros/newClass/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@main def Test: Unit = {
val foo = makeClass("foo")
println(foo.getClass)
val bar = makeClass("bar")
println(bar.getClass)
}
4 changes: 4 additions & 0 deletions tests/run-macros/newClassExtends.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Calling foo.foo
class Test_2$package$foo$1
Calling bar.foo
class Test_2$package$bar$1
Loading