Skip to content

Add reflect Symbol.info and ClassInfo #11664

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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
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
27 changes: 27 additions & 0 deletions compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2130,6 +2130,32 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
end extension
end MatchCaseMethods

type ClassInfo = dotc.core.Types.ClassInfo

given ClassInfoTypeTest: TypeTest[TypeRepr, ClassInfo] with
def unapply(x: TypeRepr): Option[ClassInfo & x.type] = x match
case x: (Types.ClassInfo & x.type) => Some(x)
case _ => None
end ClassInfoTypeTest

object ClassInfo extends ClassInfoModule:
def unapply(x: ClassInfo): (TypeRepr, Symbol, List[TypeRepr], List[Symbol], Option[TypeRepr]) =
(x.prefix, x.cls, x.declaredParents, ClassInfoMethods.decls(x), ClassInfoMethods.selfInfo(x))
end ClassInfo

given ClassInfoMethods: ClassInfoMethods with
extension (self: ClassInfo)
def qualifier: TypeRepr = self.prefix
def decls: List[Symbol] = self.decls.toList
def declaredParents: List[TypeRepr] = self.declaredParents
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But then we would have to always go to the tree which is the thing we are trying to avoid.

Copy link
Member

Choose a reason for hiding this comment

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

But then we would have to always go to the tree

No, we should expose Type#parents which doesn't involve trees.

def selfInfo: Option[TypeRepr] =
self.selfInfo match
case dotc.core.Types.NoType => None
case info: dotc.core.Types.Type => Some(info)
case sym: dotc.core.Symbols.Symbol => Some(sym.typeRef)
end extension
end ClassInfoMethods

type TypeBounds = dotc.core.Types.TypeBounds

object TypeBoundsTypeTest extends TypeTest[TypeRepr, TypeBounds]:
Expand Down Expand Up @@ -2408,6 +2434,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler

given SymbolMethods: SymbolMethods with
extension (self: Symbol)
def info: TypeRepr = self.denot.info
def owner: Symbol = self.denot.owner
def maybeOwner: Symbol = self.denot.maybeOwner
def flags: Flags = self.denot.flags
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ object Extractors {
case ByNameType(underlying) =>
this += "ByNameType(" += underlying += ")"
case ParamRef(binder, idx) =>
this += "ParamRef(" += binder += ", " += idx += ")"
this += "ParamRef(binder, " += idx += ")"
case ThisType(tp) =>
this += "ThisType(" += tp += ")"
case SuperType(thistpe, supertpe) =>
Expand All @@ -228,8 +228,9 @@ object Extractors {
case PolyType(argNames, argBounds, resType) =>
this += "PolyType(" ++= argNames += ", " ++= argBounds += ", " += resType += ")"
case TypeLambda(argNames, argBounds, resType) =>
// resType is not printed to avoid cycles
this += "TypeLambda(" ++= argNames += ", " ++= argBounds += ", _)"
this += "TypeLambda(" ++= argNames += ", " ++= argBounds += ", " += resType += ")"
case ClassInfo(prefix, cls, parents, decls, selfInfo) =>
this += "ClassInfo(" += prefix += ", cls/*" += cls.fullName += "*/, " ++= parents += ", decls, " += selfInfo += ")"
Copy link
Contributor

Choose a reason for hiding this comment

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

Is cls added by accident?

Suggested change
this += "ClassInfo(" += prefix += ", cls/*" += cls.fullName += "*/, " ++= parents += ", decls, " += selfInfo += ")"
this += "ClassInfo(" += prefix += ", /*" += cls.fullName += "*/, " ++= parents += ", decls, " += selfInfo += ")"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, we want to generate something like ClassInfo(X, cls /*...*/, ..., ...). If we remove the cls it would not parse as valid scala code. The comment is there to hint at what is in that parameter but there is no structure to it.

case TypeBounds(lo, hi) =>
this += "TypeBounds(" += lo += ", " += hi += ")"
case NoPrefix() =>
Expand Down
10 changes: 10 additions & 0 deletions compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,16 @@ object SourceCode {
this += "] => "
printType(tpe.resType)

case ClassInfo(prefix, cls, parents, decls, selfInfo) =>
this += (if cls.flags.is(Flags.Trait) then "trait " else "class ")
this += cls.fullName += " extends "
printList(parents, " with ", printType)
for info <- selfInfo do
this += " { _: "
printType(info)
this += " => }"
this

case tpe@TypeBounds(lo, hi) =>
this += "_ >: "
printType(lo)
Expand Down
38 changes: 38 additions & 0 deletions library/src/scala/quoted/Quotes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
* | | +- PolyType
* | +- TypeLambda
* +- MatchCase
* +- ClassInfo
* +- TypeBounds
* +- NoPrefix
*
Expand Down Expand Up @@ -2994,6 +2995,40 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
end extension
end MatchCaseMethods

/** Type of the definition of a type lambda taking a list of type parameters. It's return type may be a TypeLambda. */
type ClassInfo <: TypeRepr

/** `TypeTest` that allows testing at runtime in a pattern match if a `TypeRepr` is a `TypeLambda` */
given ClassInfoTypeTest: TypeTest[TypeRepr, ClassInfo]

/** Module object of `type ClassInfo` */
val ClassInfo: ClassInfoModule

/** Methods of the module object `val ClassInfo` */
trait ClassInfoModule { this: ClassInfo.type =>
def unapply(x: ClassInfo): (TypeRepr, Symbol, List[TypeRepr], List[Symbol], Option[TypeRepr])
}

/** Makes extension methods on `ClassInfo` available without any imports */
given ClassInfoMethods: ClassInfoMethods

/** Extension methods of `ClassInfo` */
trait ClassInfoMethods:
extension (self: ClassInfo)
/** The qualifier on which parents, decls, and selfType need to be rebased. */
def qualifier: TypeRepr
/** The symbols defined directly in this class. */
def decls: List[Symbol]
/** The parent types of this class.
* These are all normalized to be TypeRefs by moving any refinements
* to be member definitions of the class itself.
* Unlike `parents`, the types are not seen as seen from `prefix`.
*/
def declaredParents: List[TypeRepr]
/** The type of `this` in this class, if explicitly given, None otherwise. */
def selfInfo: Option[TypeRepr]
end extension
end ClassInfoMethods

// ----- TypeBounds -----------------------------------------------

Expand Down Expand Up @@ -3437,6 +3472,9 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
trait SymbolMethods {
extension (self: Symbol)

/** TypeRepr of the definitions of this symbol */
def info: TypeRepr

Copy link
Contributor

Choose a reason for hiding this comment

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

If we expose info on symbols, logically we need to expose Type.asSeenFrom as well to rebase the prefix.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe add a document here saying

Warning: you may need to perform asSeenFrom in case Symbol.info contains parameterized types, such as class type parameter, types depend on this, etc.

For example, if the owner of the current symbol is a class, using the following code to rebase the type:

symbol.info.asSeenFrom(prefix, symbol.owner)

What about a method like symbol.infoAsSeenFrom(prefix)?

/** Owner of this symbol. The owner is the symbol in which this symbol is defined. Throws if this symbol does not have an owner. */
def owner: Symbol

Expand Down
49 changes: 49 additions & 0 deletions tests/run-macros/symbol-info.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
a:
scala.Int
TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int")
b:
scala.Int
TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int")
c:
=> scala.Int
ByNameType(TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int"))
f1:
()scala.Int
MethodType(Nil, Nil, TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int"))
f2:
(i: scala.Int)scala.Int
MethodType(List(i), List(TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int")), TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int"))
f3:
(i: scala.Int)(j: scala.Int)scala.Int
MethodType(List(i), List(TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int")), MethodType(List(j), List(TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int")), TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int")))
f4:
[T >: scala.Nothing <: scala.Any] => (x: T)scala.Int
PolyType(List(T), List(TypeBounds(TypeRef(ThisType(TypeRef(NoPrefix(), "scala")), "Nothing"), TypeRef(ThisType(TypeRef(NoPrefix(), "scala")), "Any"))), MethodType(List(x), List(ParamRef(binder, 0)), TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int")))
f5:
(i: scala.Int)(x$2: scala.Int)scala.Int
MethodType(List(i), List(TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int")), MethodType(List(x$2), List(TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int")), TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int")))
T1:
_ >: scala.Nothing <: scala.Any
TypeBounds(TypeRef(ThisType(TypeRef(NoPrefix(), "scala")), "Nothing"), TypeRef(ThisType(TypeRef(NoPrefix(), "scala")), "Any"))
T2:
_ >: scala.Int <: scala.Int
TypeBounds(TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int"), TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int"))
T3:
_ >: scala.Nothing <: scala.Int
TypeBounds(TypeRef(ThisType(TypeRef(NoPrefix(), "scala")), "Nothing"), TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int"))
T4:
_ >: scala.Nothing <: [X >: scala.Nothing <: scala.Any] => scala.Any
TypeBounds(TypeRef(ThisType(TypeRef(NoPrefix(), "scala")), "Nothing"), TypeLambda(List(X), List(TypeBounds(TypeRef(ThisType(TypeRef(NoPrefix(), "scala")), "Nothing"), TypeRef(ThisType(TypeRef(NoPrefix(), "scala")), "Any"))), TypeRef(ThisType(TypeRef(NoPrefix(), "scala")), "Any")))
A:
A
TypeRef(NoPrefix(), "A$")
A$:
class Test$._$_$A$ extends java.lang.Object { _: A => }
ClassInfo(NoPrefix(), cls/*Test$._$_$A$*/, List(TypeRef(ThisType(TypeRef(NoPrefix(), "lang")), "Object")), decls, Some(TermRef(NoPrefix(), "A")))
A:
trait Test$._$_$A extends java.lang.Object
ClassInfo(NoPrefix(), cls/*Test$._$_$A*/, List(TypeRef(ThisType(TypeRef(NoPrefix(), "lang")), "Object")), decls, None)
B:
class Test$._$_$B extends java.lang.Object with A
ClassInfo(NoPrefix(), cls/*Test$._$_$B*/, List(TypeRef(ThisType(TypeRef(NoPrefix(), "lang")), "Object"), TypeRef(NoPrefix(), "A")), decls, None)

22 changes: 22 additions & 0 deletions tests/run-macros/symbol-info/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import quoted.*

inline def printInfos(inline x: Any): Unit = ${ infoExpr('x) }

private def infoExpr(x: Expr[Any])(using Quotes): Expr[Unit] = {
import quotes.reflect.*
val sb = StringBuilder()
new TreeTraverser {
override def traverseTree(tree: Tree)(owner: Symbol): Unit =
tree match
case tree: Definition =>
sb.append(
s"""${tree.name}:
| ${tree.symbol.info.show}
| ${tree.symbol.info.show(using Printer.TypeReprStructure)}
|""".stripMargin)
case _ =>
super.traverseTree(tree)(owner)
}.traverseTree(x.asTerm)(Symbol.spliceOwner)
val infos = Expr(sb.toString)
'{ println($infos) }
}
22 changes: 22 additions & 0 deletions tests/run-macros/symbol-info/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

object Test {
def main(args : Array[String]) : Unit =
printInfos {
val a: Int = 1
var b: Int = 1
def c: Int = 1
def f1(): Int = 1
def f2(i: Int): Int = 1
def f3(i: Int)(j: Int): Int = 1
def f4[T](x: T): Int = 1
def f5(i: Int)(using Int): Int = 1
type T1
type T2 = Int
type T3 <: Int
type T4[X]
object A
trait A
class B[X] extends A
}
}