Skip to content

Commit

Permalink
Make Ref.apply() return trees usable in the largest scope possible
Browse files Browse the repository at this point in the history
Previously for symbols contained in objects (prefixed by, let's say,
'pre'), we would return:
* an Ident if pre contained only static
object and packages;
* Select(This(moduleClassSymbol), sym) if a prefix contained a class.

However, this meant that in the second case, the generated tree would
require the macro to be expanded inside of the object, even though it
should be enough to just expand inside of the innermost class.
This was unexpected and confusing, so it was changed to not return
innermost module classes wrapped with This().
  • Loading branch information
jchyb committed Dec 18, 2024
1 parent 8cfa37a commit 238ba45
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 3 deletions.
15 changes: 15 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/tpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,21 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
def ref(sym: Symbol)(using Context): Tree =
ref(NamedType(sym.owner.thisType, sym.name, sym.denot))

// Like `ref`, but avoids wrapping innermost module class references with This(),
// instead mapping those to objects, so that the resulting trees can be used in
// largest scope possible (method added for macros)
def generalisedRef(sym: Symbol)(using Context): Tree =
// Removes ThisType from inner module classes, replacing those with references to objects
def simplifyThisTypePrefix(tpe: Type)(using Context): Type =
tpe match
case ThisType(tref @ TypeRef(prefix, _)) if tref.symbol.flags.is(Module) =>
TermRef(simplifyThisTypePrefix(prefix), tref.symbol.companionModule)
case TypeRef(prefix, designator) =>
TypeRef(simplifyThisTypePrefix(prefix), designator)
case _ =>
tpe
ref(NamedType(simplifyThisTypePrefix(sym.owner.thisType), sym.name, sym.denot))

private def followOuterLinks(t: Tree)(using Context) = t match {
case t: This if ctx.erasedTypes && !(t.symbol == ctx.owner.enclosingClass || t.symbol.isStaticOwner) =>
// after erasure outer paths should be respected
Expand Down
1 change: 0 additions & 1 deletion compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1950,7 +1950,6 @@ object SymDenotations {

/** The this-type depends on the kind of class:
* - for a package class `p`: ThisType(TypeRef(Noprefix, p))
* - for a module class `m`: A term ref to m's source module.
* - for all other classes `c` with owner `o`: ThisType(TypeRef(o.thisType, c))
*/
override def thisType(using Context): Type = {
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
withDefaultPos(tpd.ref(tp).asInstanceOf[tpd.RefTree])
def apply(sym: Symbol): Ref =
assert(sym.isTerm, s"expected a term symbol, but received $sym")
val refTree = tpd.ref(sym) match
val refTree = tpd.generalisedRef(sym) match
case t @ tpd.This(ident) => // not a RefTree, so we need to work around this - issue #19732
// ident in `This` can be a TypeIdent of sym, so we manually prepare the ref here,
// knowing that the owner is actually `This`.
Expand Down
6 changes: 5 additions & 1 deletion library/src/scala/quoted/Quotes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -907,10 +907,14 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
* If `sym` refers to a class member `foo` in class `C`,
* returns a tree representing `C.this.foo`.
*
* If `sym` refers to an object member `foo` in object C, itself in prefix
* `pre` (which might include `.this`, if it contains a class),
* returns `pre.C.foo`.
*
* If `sym` refers to a local definition `foo`, returns
* a tree representing `foo`.
*
* @note In both cases, the constructed tree should only
* @note In all cases, the constructed tree should only
* be spliced into the places where such accesses make sense.
* For example, it is incorrect to have `C.this.foo` outside
* the class body of `C`, or have `foo` outside the lexical
Expand Down
11 changes: 11 additions & 0 deletions tests/pos-macros/i20349a/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import scala.quoted.*

object Macros {
def valuesImpl[A: Type](using Quotes): Expr[Any] = {
import quotes.reflect.*
val symbol = TypeRepr.of[A].typeSymbol.fieldMember("value")
Ref(symbol).asExprOf[Any]
}

transparent inline def values[A]: Any = ${ valuesImpl[A] }
}
16 changes: 16 additions & 0 deletions tests/pos-macros/i20349a/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

class Cls{
object a {
object domain {
val value = ""
}
}
Macros.values[a.domain.type]
}

object Test {
lazy val script = new Cls()
def main(args: Array[String]): Unit =
val _ = script.hashCode()
???
}
43 changes: 43 additions & 0 deletions tests/pos-macros/i20349b/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import scala.quoted.*

object Macros {


def valuesImpl[A: Type](using quotes: Quotes): Expr[List[A]] = {
import quotes.*, quotes.reflect.*

extension (sym: Symbol)
def isPublic: Boolean = !sym.isNoSymbol &&
!(sym.flags.is(Flags.Private) || sym.flags.is(Flags.PrivateLocal) || sym.flags.is(Flags.Protected) ||
sym.privateWithin.isDefined || sym.protectedWithin.isDefined)

def isSealed[A: Type]: Boolean =
TypeRepr.of[A].typeSymbol.flags.is(Flags.Sealed)

def extractSealedSubtypes[A: Type]: List[Type[?]] = {
def extractRecursively(sym: Symbol): List[Symbol] =
if sym.flags.is(Flags.Sealed) then sym.children.flatMap(extractRecursively)
else if sym.flags.is(Flags.Enum) then List(sym.typeRef.typeSymbol)
else if sym.flags.is(Flags.Module) then List(sym.typeRef.typeSymbol.moduleClass)
else List(sym)

extractRecursively(TypeRepr.of[A].typeSymbol).distinct.map(typeSymbol =>
typeSymbol.typeRef.asType
)
}

if isSealed[A] then {
val refs = extractSealedSubtypes[A].flatMap { tpe =>
val sym = TypeRepr.of(using tpe).typeSymbol
val isCaseVal = sym.isPublic && sym.flags
.is(Flags.Case | Flags.Enum) && (sym.flags.is(Flags.JavaStatic) || sym.flags.is(Flags.StableRealizable))

if (isCaseVal) then List(Ref(sym).asExprOf[A])
else Nil
}
Expr.ofList(refs)
} else '{ Nil }
}

inline def values[A]: List[A] = ${ valuesImpl[A] }
}
14 changes: 14 additions & 0 deletions tests/pos-macros/i20349b/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class Test {
object domain {
enum PaymentMethod:
case PayPal(email: String)
case Card(digits: Long, name: String)
case Cash
}
println(Macros.values[domain.PaymentMethod])
}
object Test {
lazy val script = new Test()
def main(args: Array[String]): Unit =
val _ = script.hashCode()
}

0 comments on commit 238ba45

Please sign in to comment.