From 5120755e3423db61ebc12ec7fe0784735407bbea Mon Sep 17 00:00:00 2001 From: Jan Chyb <48855024+jchyb@users.noreply.github.com> Date: Wed, 13 Aug 2025 14:56:00 +0200 Subject: [PATCH] Quotes reflect: Allow to return DefDef from a val symbol tree (#22603) During the `Getters` phase, the compiler replaces class member vals with defs. Meanwhile `.tree` returns a tree most recently associated with a symbol (if currently compiled), or one read from tasty (if compiled before). An interesting thing happens in the issue - since all of the files are tried being compiled in one compilation run, the file with a macro call is suspended, and then the macro and the MyClass class definition are compiled to the end (also including the `Getters` phase, where the tree stops being a ValDef). Initially I thought about manually replacing that returned DefDef with a ValDef in quotes reflection (like what happens when the tree is not available), but that would have been pointless - there would be little use of calling `.tree` on a symbol if we cannot read the rhs of the definition. Returning raw DefDef seems like a less bad choice, especially `.tree` is meant to be for power users anyway, and is full of other, more dangerous warts. [Cherry-picked 4788641eca466c889214d16d024e6a83b2274991] --- .../dotc/quoted/reflect/FromSymbol.scala | 8 ++++- library/src/scala/quoted/Quotes.scala | 11 +++---- tests/pos-macros/i22584/Macro.scala | 30 +++++++++++++++++++ tests/pos-macros/i22584/Main.scala | 4 +++ tests/pos-macros/i22584/Types.scala | 9 ++++++ 5 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 tests/pos-macros/i22584/Macro.scala create mode 100644 tests/pos-macros/i22584/Main.scala create mode 100644 tests/pos-macros/i22584/Types.scala diff --git a/compiler/src/dotty/tools/dotc/quoted/reflect/FromSymbol.scala b/compiler/src/dotty/tools/dotc/quoted/reflect/FromSymbol.scala index cfc09a8ed836..32c978073194 100644 --- a/compiler/src/dotty/tools/dotc/quoted/reflect/FromSymbol.scala +++ b/compiler/src/dotty/tools/dotc/quoted/reflect/FromSymbol.scala @@ -45,8 +45,14 @@ object FromSymbol { case tpd.EmptyTree => tpd.DefDef(sym) } - def valDefFromSym(sym: TermSymbol)(using Context): tpd.ValDef = sym.defTree match { + def valDefFromSym(sym: TermSymbol)(using Context): tpd.ValOrDefDef = sym.defTree match { case tree: tpd.ValDef => tree + case tree: tpd.DefDef => + // `Getters` phase replaces val class members with defs, + // so we may see a defdef here if we are running this on a symbol compiled + // in the same compilation (but before suspension, so that + // the symbol could have reached `Getters`). + tree case tpd.EmptyTree => tpd.ValDef(sym) } diff --git a/library/src/scala/quoted/Quotes.scala b/library/src/scala/quoted/Quotes.scala index 70d09b7e4841..c2609eea143c 100644 --- a/library/src/scala/quoted/Quotes.scala +++ b/library/src/scala/quoted/Quotes.scala @@ -3945,11 +3945,12 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching => /** Tree of this definition * - * If this symbol `isClassDef` it will return `a `ClassDef`, - * if this symbol `isTypeDef` it will return `a `TypeDef`, - * if this symbol `isValDef` it will return `a `ValDef`, - * if this symbol `isDefDef` it will return `a `DefDef` - * if this symbol `isBind` it will return `a `Bind`, + * If this symbol `isClassDef` it will return a `ClassDef`, + * if this symbol `isTypeDef` it will return a `TypeDef`, + * if this symbol `isDefDef` it will return a `DefDef`, + * if this symbol `isBind` it will return a `Bind`, + * if this symbol `isValDef` it will return a `ValDef`, + * or a `DefDef` (as the compiler can replace val class members with defs during compilation), * else will throw * * **Warning**: avoid using this method in macros. diff --git a/tests/pos-macros/i22584/Macro.scala b/tests/pos-macros/i22584/Macro.scala new file mode 100644 index 000000000000..f0b9c7409520 --- /dev/null +++ b/tests/pos-macros/i22584/Macro.scala @@ -0,0 +1,30 @@ +//> using options -Yretain-trees + +import scala.quoted.* + +object Macros { + + inline def myMacro[A]: Unit = ${ myMacroImpl[A] } + + private def myMacroImpl[A](using quotes: Quotes, tpe: Type[A]): Expr[Unit] = { + import quotes.reflect.* + + val typeRepr = TypeRepr.of[A] + val typeSymbol = typeRepr.typeSymbol + + val caseFieldSymbols: List[Symbol] = typeSymbol.fieldMembers + val caseFieldValOrDefDefs: List[ValDef | DefDef] = + caseFieldSymbols.sortBy(_.toString).map { + _.tree match { + case valDef: ValDef => valDef + case defDef: DefDef => defDef + case _ => report.errorAndAbort("???") + } + } + + val expected = "List(DefDef(boolean,List(List()),TypeTree[TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Boolean)],Select(This(Ident(MyClass1)),boolean)), DefDef(finalVal,List(List()),TypeTree[TypeRef(ThisType(TypeRef(NoPrefix,module class lang)),class String)],Select(This(Ident(MyClass1)),finalVal)), DefDef(int,List(List()),TypeTree[TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Int)],Select(This(Ident(MyClass1)),int)), DefDef(string,List(List()),TypeTree[TypeRef(ThisType(TypeRef(NoPrefix,module class lang)),class String)],Select(This(Ident(MyClass1)),string)))" + assert(caseFieldValOrDefDefs.toString == expected) + + '{ () } + } +} diff --git a/tests/pos-macros/i22584/Main.scala b/tests/pos-macros/i22584/Main.scala new file mode 100644 index 000000000000..4b6442078f96 --- /dev/null +++ b/tests/pos-macros/i22584/Main.scala @@ -0,0 +1,4 @@ +import Types.* + +@main def main() = + Macros.myMacro[MyClass1] diff --git a/tests/pos-macros/i22584/Types.scala b/tests/pos-macros/i22584/Types.scala new file mode 100644 index 000000000000..01560035ca60 --- /dev/null +++ b/tests/pos-macros/i22584/Types.scala @@ -0,0 +1,9 @@ +object Types { + final case class MyClass1( + int: Int, + string: String, + boolean: Boolean, + ) { + final val finalVal: String = "result" + } +}