From 68459a7030d83eaf0d76d05e9cc33ff223d6d354 Mon Sep 17 00:00:00 2001 From: Jan Chyb Date: Wed, 18 Dec 2024 15:17:07 +0100 Subject: [PATCH] Make Ref.apply() return trees usable in the largest scope possible 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(). [Cherry-picked 238ba458b491c06e9f492673607157619b61d007] --- compiler/src/dotty/tools/dotc/ast/tpd.scala | 15 +++++++ .../tools/dotc/core/SymDenotations.scala | 1 - .../quoted/runtime/impl/QuotesImpl.scala | 2 +- library/src/scala/quoted/Quotes.scala | 6 ++- tests/pos-macros/i20349a/Macro_1.scala | 11 +++++ tests/pos-macros/i20349a/Test_2.scala | 16 +++++++ tests/pos-macros/i20349b/Macro_1.scala | 43 +++++++++++++++++++ tests/pos-macros/i20349b/Test_2.scala | 14 ++++++ 8 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 tests/pos-macros/i20349a/Macro_1.scala create mode 100644 tests/pos-macros/i20349a/Test_2.scala create mode 100644 tests/pos-macros/i20349b/Macro_1.scala create mode 100644 tests/pos-macros/i20349b/Test_2.scala diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index c119dfd8d982..6ce8d93af76a 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -477,6 +477,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 diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 0e068eff2cfd..340197ffc4a0 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -1945,7 +1945,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 = { diff --git a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala index a666feace741..b6d0eae2fbbb 100644 --- a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala +++ b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala @@ -454,7 +454,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`. diff --git a/library/src/scala/quoted/Quotes.scala b/library/src/scala/quoted/Quotes.scala index c5d0022b6cf5..544e9119a770 100644 --- a/library/src/scala/quoted/Quotes.scala +++ b/library/src/scala/quoted/Quotes.scala @@ -878,10 +878,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 diff --git a/tests/pos-macros/i20349a/Macro_1.scala b/tests/pos-macros/i20349a/Macro_1.scala new file mode 100644 index 000000000000..41c5083b328c --- /dev/null +++ b/tests/pos-macros/i20349a/Macro_1.scala @@ -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] } +} diff --git a/tests/pos-macros/i20349a/Test_2.scala b/tests/pos-macros/i20349a/Test_2.scala new file mode 100644 index 000000000000..4569c501e055 --- /dev/null +++ b/tests/pos-macros/i20349a/Test_2.scala @@ -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() + ??? +} diff --git a/tests/pos-macros/i20349b/Macro_1.scala b/tests/pos-macros/i20349b/Macro_1.scala new file mode 100644 index 000000000000..413274b4b516 --- /dev/null +++ b/tests/pos-macros/i20349b/Macro_1.scala @@ -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] } +} diff --git a/tests/pos-macros/i20349b/Test_2.scala b/tests/pos-macros/i20349b/Test_2.scala new file mode 100644 index 000000000000..a392a636dc44 --- /dev/null +++ b/tests/pos-macros/i20349b/Test_2.scala @@ -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() +}