Skip to content

Commit 2f639e2

Browse files
authored
Make Ref.apply() return trees usable in the largest scope possible (#22240)
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 `pre` 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(). Fixes #20349
2 parents 133dcb3 + 238ba45 commit 2f639e2

File tree

8 files changed

+105
-3
lines changed

8 files changed

+105
-3
lines changed

compiler/src/dotty/tools/dotc/ast/tpd.scala

+15
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,21 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
487487
def ref(sym: Symbol)(using Context): Tree =
488488
ref(NamedType(sym.owner.thisType, sym.name, sym.denot))
489489

490+
// Like `ref`, but avoids wrapping innermost module class references with This(),
491+
// instead mapping those to objects, so that the resulting trees can be used in
492+
// largest scope possible (method added for macros)
493+
def generalisedRef(sym: Symbol)(using Context): Tree =
494+
// Removes ThisType from inner module classes, replacing those with references to objects
495+
def simplifyThisTypePrefix(tpe: Type)(using Context): Type =
496+
tpe match
497+
case ThisType(tref @ TypeRef(prefix, _)) if tref.symbol.flags.is(Module) =>
498+
TermRef(simplifyThisTypePrefix(prefix), tref.symbol.companionModule)
499+
case TypeRef(prefix, designator) =>
500+
TypeRef(simplifyThisTypePrefix(prefix), designator)
501+
case _ =>
502+
tpe
503+
ref(NamedType(simplifyThisTypePrefix(sym.owner.thisType), sym.name, sym.denot))
504+
490505
private def followOuterLinks(t: Tree)(using Context) = t match {
491506
case t: This if ctx.erasedTypes && !(t.symbol == ctx.owner.enclosingClass || t.symbol.isStaticOwner) =>
492507
// after erasure outer paths should be respected

compiler/src/dotty/tools/dotc/core/SymDenotations.scala

-1
Original file line numberDiff line numberDiff line change
@@ -1959,7 +1959,6 @@ object SymDenotations {
19591959

19601960
/** The this-type depends on the kind of class:
19611961
* - for a package class `p`: ThisType(TypeRef(Noprefix, p))
1962-
* - for a module class `m`: A term ref to m's source module.
19631962
* - for all other classes `c` with owner `o`: ThisType(TypeRef(o.thisType, c))
19641963
*/
19651964
override def thisType(using Context): Type = {

compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
473473
withDefaultPos(tpd.ref(tp).asInstanceOf[tpd.RefTree])
474474
def apply(sym: Symbol): Ref =
475475
assert(sym.isTerm, s"expected a term symbol, but received $sym")
476-
val refTree = tpd.ref(sym) match
476+
val refTree = tpd.generalisedRef(sym) match
477477
case t @ tpd.This(ident) => // not a RefTree, so we need to work around this - issue #19732
478478
// ident in `This` can be a TypeIdent of sym, so we manually prepare the ref here,
479479
// knowing that the owner is actually `This`.

library/src/scala/quoted/Quotes.scala

+5-1
Original file line numberDiff line numberDiff line change
@@ -906,10 +906,14 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
906906
* If `sym` refers to a class member `foo` in class `C`,
907907
* returns a tree representing `C.this.foo`.
908908
*
909+
* If `sym` refers to an object member `foo` in object C, itself in prefix
910+
* `pre` (which might include `.this`, if it contains a class),
911+
* returns `pre.C.foo`.
912+
*
909913
* If `sym` refers to a local definition `foo`, returns
910914
* a tree representing `foo`.
911915
*
912-
* @note In both cases, the constructed tree should only
916+
* @note In all cases, the constructed tree should only
913917
* be spliced into the places where such accesses make sense.
914918
* For example, it is incorrect to have `C.this.foo` outside
915919
* the class body of `C`, or have `foo` outside the lexical
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import scala.quoted.*
2+
3+
object Macros {
4+
def valuesImpl[A: Type](using Quotes): Expr[Any] = {
5+
import quotes.reflect.*
6+
val symbol = TypeRepr.of[A].typeSymbol.fieldMember("value")
7+
Ref(symbol).asExprOf[Any]
8+
}
9+
10+
transparent inline def values[A]: Any = ${ valuesImpl[A] }
11+
}

tests/pos-macros/i20349a/Test_2.scala

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
class Cls{
3+
object a {
4+
object domain {
5+
val value = ""
6+
}
7+
}
8+
Macros.values[a.domain.type]
9+
}
10+
11+
object Test {
12+
lazy val script = new Cls()
13+
def main(args: Array[String]): Unit =
14+
val _ = script.hashCode()
15+
???
16+
}
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import scala.quoted.*
2+
3+
object Macros {
4+
5+
6+
def valuesImpl[A: Type](using quotes: Quotes): Expr[List[A]] = {
7+
import quotes.*, quotes.reflect.*
8+
9+
extension (sym: Symbol)
10+
def isPublic: Boolean = !sym.isNoSymbol &&
11+
!(sym.flags.is(Flags.Private) || sym.flags.is(Flags.PrivateLocal) || sym.flags.is(Flags.Protected) ||
12+
sym.privateWithin.isDefined || sym.protectedWithin.isDefined)
13+
14+
def isSealed[A: Type]: Boolean =
15+
TypeRepr.of[A].typeSymbol.flags.is(Flags.Sealed)
16+
17+
def extractSealedSubtypes[A: Type]: List[Type[?]] = {
18+
def extractRecursively(sym: Symbol): List[Symbol] =
19+
if sym.flags.is(Flags.Sealed) then sym.children.flatMap(extractRecursively)
20+
else if sym.flags.is(Flags.Enum) then List(sym.typeRef.typeSymbol)
21+
else if sym.flags.is(Flags.Module) then List(sym.typeRef.typeSymbol.moduleClass)
22+
else List(sym)
23+
24+
extractRecursively(TypeRepr.of[A].typeSymbol).distinct.map(typeSymbol =>
25+
typeSymbol.typeRef.asType
26+
)
27+
}
28+
29+
if isSealed[A] then {
30+
val refs = extractSealedSubtypes[A].flatMap { tpe =>
31+
val sym = TypeRepr.of(using tpe).typeSymbol
32+
val isCaseVal = sym.isPublic && sym.flags
33+
.is(Flags.Case | Flags.Enum) && (sym.flags.is(Flags.JavaStatic) || sym.flags.is(Flags.StableRealizable))
34+
35+
if (isCaseVal) then List(Ref(sym).asExprOf[A])
36+
else Nil
37+
}
38+
Expr.ofList(refs)
39+
} else '{ Nil }
40+
}
41+
42+
inline def values[A]: List[A] = ${ valuesImpl[A] }
43+
}

tests/pos-macros/i20349b/Test_2.scala

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class Test {
2+
object domain {
3+
enum PaymentMethod:
4+
case PayPal(email: String)
5+
case Card(digits: Long, name: String)
6+
case Cash
7+
}
8+
println(Macros.values[domain.PaymentMethod])
9+
}
10+
object Test {
11+
lazy val script = new Test()
12+
def main(args: Array[String]): Unit =
13+
val _ = script.hashCode()
14+
}

0 commit comments

Comments
 (0)