From 28cb8547f0c86170d6c27bd0cdde796d6622673f Mon Sep 17 00:00:00 2001 From: Jan Chyb Date: Mon, 21 Oct 2024 14:20:36 +0200 Subject: [PATCH] Add class term parameters, flags, and privateWithin to newClass in reflect API --- .../tools/dotc/transform/TreeChecker.scala | 46 ++++++++++--------- .../dotty/tools/dotc/typer/TypeAssigner.scala | 6 ++- .../quoted/runtime/impl/QuotesImpl.scala | 41 +++++++++++++++-- library/src/scala/quoted/Quotes.scala | 10 ++++ .../newClassParamsMissingArgument.check | 32 +++++++++++++ .../Macro_1.scala | 24 ++++++++++ .../Test_2.scala | 5 ++ tests/run-macros/newClassParams.check | 1 + tests/run-macros/newClassParams/Macro_1.scala | 28 +++++++++++ tests/run-macros/newClassParams/Test_2.scala | 5 ++ .../newClassParamsExtendsClassParams.check | 4 ++ .../Macro_1.scala | 30 ++++++++++++ .../Test_2.scala | 10 ++++ 13 files changed, 216 insertions(+), 26 deletions(-) create mode 100644 tests/neg-macros/newClassParamsMissingArgument.check create mode 100644 tests/neg-macros/newClassParamsMissingArgument/Macro_1.scala create mode 100644 tests/neg-macros/newClassParamsMissingArgument/Test_2.scala create mode 100644 tests/run-macros/newClassParams.check create mode 100644 tests/run-macros/newClassParams/Macro_1.scala create mode 100644 tests/run-macros/newClassParams/Test_2.scala create mode 100644 tests/run-macros/newClassParamsExtendsClassParams.check create mode 100644 tests/run-macros/newClassParamsExtendsClassParams/Macro_1.scala create mode 100644 tests/run-macros/newClassParamsExtendsClassParams/Test_2.scala diff --git a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala index c35dc80c04a5..90262bc5da85 100644 --- a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala @@ -854,30 +854,34 @@ object TreeChecker { val phases = ctx.base.allPhases.toList val treeChecker = new LocalChecker(previousPhases(phases)) + def reportMalformedMacroTree(msg: String | Null, err: Throwable) = + val stack = + if !ctx.settings.Ydebug.value then "\nstacktrace available when compiling with `-Ydebug`" + else if err.getStackTrace == null then " no stacktrace" + else err.getStackTrace.nn.mkString(" ", " \n", "") + report.error( + em"""Malformed tree was found while expanding macro with -Xcheck-macros. + |The tree does not conform to the compiler's tree invariants. + | + |Macro was: + |${scala.quoted.runtime.impl.QuotesImpl.showDecompiledTree(original)} + | + |The macro returned: + |${scala.quoted.runtime.impl.QuotesImpl.showDecompiledTree(expansion)} + | + |Error: + |$msg + |$stack + |""", + original + ) + try treeChecker.typed(expansion)(using checkingCtx) catch case err: java.lang.AssertionError => - val stack = - if !ctx.settings.Ydebug.value then "\nstacktrace available when compiling with `-Ydebug`" - else if err.getStackTrace == null then " no stacktrace" - else err.getStackTrace.nn.mkString(" ", " \n", "") - - report.error( - em"""Malformed tree was found while expanding macro with -Xcheck-macros. - |The tree does not conform to the compiler's tree invariants. - | - |Macro was: - |${scala.quoted.runtime.impl.QuotesImpl.showDecompiledTree(original)} - | - |The macro returned: - |${scala.quoted.runtime.impl.QuotesImpl.showDecompiledTree(expansion)} - | - |Error: - |${err.getMessage} - |$stack - |""", - original - ) + reportMalformedMacroTree(err.getMessage(), err) + case err: UnhandledError => + reportMalformedMacroTree(err.diagnostic.message, err) private[TreeChecker] def previousPhases(phases: List[Phase])(using Context): List[Phase] = phases match { case (phase: MegaPhase) :: phases1 => diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index fd16f0de5f3a..4c00aad85e75 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -12,6 +12,7 @@ import collection.mutable import reporting.* import Checking.{checkNoPrivateLeaks, checkNoWildcard} import cc.CaptureSet +import transform.Splicer trait TypeAssigner { import tpd.* @@ -301,7 +302,10 @@ trait TypeAssigner { if fntpe.isResultDependent then safeSubstMethodParams(fntpe, args.tpes) else fntpe.resultType // fast path optimization else - errorType(em"wrong number of arguments at ${ctx.phase.prev} for $fntpe: ${fn.tpe}, expected: ${fntpe.paramInfos.length}, found: ${args.length}", tree.srcPos) + val erroringPhase = + if Splicer.inMacroExpansion then i"${ctx.phase} (while expanding macro)" + else ctx.phase.prev.toString + errorType(em"wrong number of arguments at $erroringPhase for $fntpe: ${fn.tpe}, expected: ${fntpe.paramInfos.length}, found: ${args.length}", tree.srcPos) case err: ErrorType => err case t => diff --git a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala index e8524a193e5a..8a69b8539d3e 100644 --- a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala +++ b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala @@ -241,9 +241,23 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler object ClassDef extends ClassDefModule: def apply(cls: Symbol, parents: List[Tree], body: List[Statement]): ClassDef = - val untpdCtr = untpd.DefDef(nme.CONSTRUCTOR, Nil, tpd.TypeTree(dotc.core.Symbols.defn.UnitClass.typeRef), tpd.EmptyTree) + val paramsDefs: List[untpd.ParamClause] = + cls.primaryConstructor.paramSymss.map { paramSym => + paramSym.map( symm => + ValDef(symm, None) + ) + } + val paramsAccessDefs: List[untpd.ParamClause] = + cls.primaryConstructor.paramSymss.map { paramSym => + paramSym.map( symm => + ValDef(cls.fieldMember(symm.name.toString()), None) // TODO I don't like the toString here + ) + } + + val termSymbol: dotc.core.Symbols.TermSymbol = cls.primaryConstructor.asTerm + val untpdCtr = untpd.DefDef(nme.CONSTRUCTOR, paramsDefs, tpd.TypeTree(dotc.core.Symbols.defn.UnitClass.typeRef), tpd.EmptyTree) val ctr = ctx.typeAssigner.assignType(untpdCtr, cls.primaryConstructor) - tpd.ClassDefWithParents(cls.asClass, ctr, parents, body) + tpd.ClassDefWithParents(cls.asClass, ctr, parents, paramsAccessDefs.flatten ++ body) def copy(original: Tree)(name: String, constr: DefDef, parents: List[Tree], selfOpt: Option[ValDef], body: List[Statement]): ClassDef = { val dotc.ast.Trees.TypeDef(_, originalImpl: tpd.Template) = original: @unchecked @@ -2605,10 +2619,10 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler def requiredMethod(path: String): Symbol = dotc.core.Symbols.requiredMethod(path) def classSymbol(fullName: String): Symbol = dotc.core.Symbols.requiredClass(fullName) - def newClass(owner: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr]): Symbol = + def newClass(parent: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr]): Symbol = assert(parents.nonEmpty && !parents.head.typeSymbol.is(dotc.core.Flags.Trait), "First parent must be a class") val cls = dotc.core.Symbols.newNormalizedClassSymbol( - owner, + parent, name.toTypeName, dotc.core.Flags.EmptyFlags, parents, @@ -2618,6 +2632,22 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler for sym <- decls(cls) do cls.enter(sym) cls + def newClass(parent: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr], paramNames: List[String], paramTypes: List[TypeRepr], flags: Flags, privateWithin: Symbol): Symbol = + assert(parents.nonEmpty && !parents.head.typeSymbol.is(dotc.core.Flags.Trait), "First parent must be a class") + checkValidFlags(flags.toTermFlags, Flags.validClassFlags) + val cls = dotc.core.Symbols.newNormalizedClassSymbol( + parent, + name.toTypeName, + flags, + parents, + selfType.getOrElse(Types.NoType), + privateWithin) + cls.enter(dotc.core.Symbols.newConstructor(cls, dotc.core.Flags.Synthetic, paramNames.map(_.toTermName), paramTypes)) + for (name, tpe) <- paramNames.zip(paramTypes) do + cls.enter(dotc.core.Symbols.newSymbol(cls, name.toTermName, Flags.ParamAccessor, tpe, Symbol.noSymbol)) // add other flags (local, private, privatelocal) and set privateWithin + for sym <- decls(cls) do cls.enter(sym) + cls + def newModule(owner: Symbol, name: String, modFlags: Flags, clsFlags: Flags, parents: List[TypeRepr], decls: Symbol => List[Symbol], privateWithin: Symbol): Symbol = assert(parents.nonEmpty && !parents.head.typeSymbol.is(dotc.core.Flags.Trait), "First parent must be a class") assert(!privateWithin.exists || privateWithin.isType, "privateWithin must be a type symbol or `Symbol.noSymbol`") @@ -3006,6 +3036,9 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler // Keep: aligned with Quotes's `newTypeAlias` doc private[QuotesImpl] def validTypeAliasFlags: Flags = Private | Protected | Override | Final | Infix | Local + // Keep: aligned with Quotes's `newClass` + private[QuotesImpl] def validClassFlags: Flags = Private | Protected | Final // Abstract, AbsOverride Local OPen ? PrivateLocal Protected ? + end Flags given FlagsMethods: FlagsMethods with diff --git a/library/src/scala/quoted/Quotes.scala b/library/src/scala/quoted/Quotes.scala index 7a98d6f6f761..5c9e23811210 100644 --- a/library/src/scala/quoted/Quotes.scala +++ b/library/src/scala/quoted/Quotes.scala @@ -3840,6 +3840,16 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching => // TODO: add flags and privateWithin @experimental def newClass(parent: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr]): Symbol + /* + * @param paramNames constructor parameter names. + * @param paramTypes constructor parameter types. + * @param flags extra flags with which the class symbol should be constructed. + * @param privateWithin the symbol within which this new method symbol should be private. May be noSymbol. + * + * Parameters can be obtained via classSymbol.memberField + */ + @experimental def newClass(parent: Symbol, name: String, parents: List[TypeRepr], decls: Symbol => List[Symbol], selfType: Option[TypeRepr], paramNames: List[String], paramTypes: List[TypeRepr], flags: Flags, privateWithin: Symbol): Symbol + /** Generates a new module symbol with an associated module class symbol, * this is equivalent to an `object` declaration in source code. * This method returns the module symbol. The module class can be accessed calling `moduleClass` on this symbol. diff --git a/tests/neg-macros/newClassParamsMissingArgument.check b/tests/neg-macros/newClassParamsMissingArgument.check new file mode 100644 index 000000000000..2a6b53bd2d79 --- /dev/null +++ b/tests/neg-macros/newClassParamsMissingArgument.check @@ -0,0 +1,32 @@ + +-- Error: tests/neg-macros/newClassParamsMissingArgument/Test_2.scala:4:2 ---------------------------------------------- +4 | makeClass("foo") // error // error + | ^^^^^^^^^^^^^^^^ + |wrong number of arguments at inlining (while expanding macro) for (idx: Int): foo: (foo# : (idx: Int): foo), expected: 1, found: 0 +-- Error: tests/neg-macros/newClassParamsMissingArgument/Test_2.scala:4:11 --------------------------------------------- +4 | makeClass("foo") // error // error + | ^^^^^^^^^^^^^^^^ + |Malformed tree was found while expanding macro with -Xcheck-macros. + |The tree does not conform to the compiler's tree invariants. + | + |Macro was: + |scala.quoted.runtime.Expr.splice[java.lang.Object](((contextual$1: scala.quoted.Quotes) ?=> Macro_1$package.inline$makeClassExpr(scala.quoted.runtime.Expr.quote[scala.Predef.String]("foo").apply(using contextual$1))(contextual$1))) + | + |The macro returned: + |{ + | class foo(val idx: scala.Int) extends java.lang.Object + | + | (new foo(): java.lang.Object) + |} + | + |Error: + |missing argument for parameter idx of constructor foo in class foo: (idx: Int): foo + | + |stacktrace available when compiling with `-Ydebug` + |--------------------------------------------------------------------------------------------------------------------- + |Inline stack trace + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + |This location contains code that was inlined from Macro_1.scala:5 +5 |inline def makeClass(inline name: String): Object = ${ makeClassExpr('name) } + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + --------------------------------------------------------------------------------------------------------------------- diff --git a/tests/neg-macros/newClassParamsMissingArgument/Macro_1.scala b/tests/neg-macros/newClassParamsMissingArgument/Macro_1.scala new file mode 100644 index 000000000000..abb671b1c8bf --- /dev/null +++ b/tests/neg-macros/newClassParamsMissingArgument/Macro_1.scala @@ -0,0 +1,24 @@ +//> using options -experimental + +import scala.quoted._ + +inline def makeClass(inline name: String): Object = ${ makeClassExpr('name) } +private def makeClassExpr(nameExpr: Expr[String])(using Quotes): Expr[Object] = { + import quotes.reflect.* + + val name = nameExpr.valueOrAbort + val parents = List(TypeTree.of[Object]) + def decls(cls: Symbol): List[Symbol] = Nil + + val cls = Symbol.newClass(Symbol.spliceOwner, name, parents = parents.map(_.tpe), decls, selfType = None, List("idx"), List(TypeRepr.of[Int]), Flags.EmptyFlags, Symbol.noSymbol) + + val clsDef = ClassDef(cls, parents, body = Nil) + val newCls = Typed(Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil), TypeTree.of[Object]) + + Block(List(clsDef), newCls).asExprOf[Object] + + // '{ + // class `name`(idx: Int) + // new `name` + // } +} diff --git a/tests/neg-macros/newClassParamsMissingArgument/Test_2.scala b/tests/neg-macros/newClassParamsMissingArgument/Test_2.scala new file mode 100644 index 000000000000..8d7142f7bd72 --- /dev/null +++ b/tests/neg-macros/newClassParamsMissingArgument/Test_2.scala @@ -0,0 +1,5 @@ +//> using options -experimental + +@main def Test: Unit = { + makeClass("foo") // error // error +} diff --git a/tests/run-macros/newClassParams.check b/tests/run-macros/newClassParams.check new file mode 100644 index 000000000000..02e16275a822 --- /dev/null +++ b/tests/run-macros/newClassParams.check @@ -0,0 +1 @@ +Foo method call with (10, test) \ No newline at end of file diff --git a/tests/run-macros/newClassParams/Macro_1.scala b/tests/run-macros/newClassParams/Macro_1.scala new file mode 100644 index 000000000000..e60d4bd97909 --- /dev/null +++ b/tests/run-macros/newClassParams/Macro_1.scala @@ -0,0 +1,28 @@ +//> using options -experimental + +import scala.quoted._ + +inline def makeClassAndCall(inline name: String, idx: Int, str: String): Unit = ${ makeClassAndCallExpr('name, 'idx, 'str) } +private def makeClassAndCallExpr(nameExpr: Expr[String], idxExpr: Expr[Int], strExpr: Expr[String])(using Quotes): Expr[Unit] = { + import quotes.reflect.* + + val name = nameExpr.valueOrAbort + + def decls(cls: Symbol): List[Symbol] = List(Symbol.newMethod(cls, "foo", MethodType(Nil)(_ => Nil, _ => TypeRepr.of[Unit]))) + val parents = List(TypeTree.of[Object]) + val cls = Symbol.newClass(Symbol.spliceOwner, name, parents = parents.map(_.tpe), decls, selfType = None, List("idx", "str"), List(TypeRepr.of[Int], TypeRepr.of[String]), Flags.EmptyFlags, Symbol.noSymbol) + + val fooDef = DefDef(cls.methodMember("foo")(0), argss => Some('{println(s"Foo method call with (${${Ref(cls.fieldMember("idx")).asExpr}}, ${${Ref(cls.fieldMember("str")).asExpr}})")}.asTerm)) + val clsDef = ClassDef(cls, parents, body = List(fooDef)) + val newCls = Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), List(idxExpr.asTerm, strExpr.asTerm)) + + Block(List(clsDef), Apply(Select(newCls, cls.methodMember("foo")(0)), Nil)).asExprOf[Unit] + + // '{ + // class `name`(idx: Int, str: String) { + // def foo() = println("Foo method call with ($idx, $str)") + // } + // new `name`(`idx`, `str`) + // } +} + diff --git a/tests/run-macros/newClassParams/Test_2.scala b/tests/run-macros/newClassParams/Test_2.scala new file mode 100644 index 000000000000..64762f17b92f --- /dev/null +++ b/tests/run-macros/newClassParams/Test_2.scala @@ -0,0 +1,5 @@ +//> using options -experimental + +@main def Test: Unit = { + makeClassAndCall("bar", 10, "test") +} diff --git a/tests/run-macros/newClassParamsExtendsClassParams.check b/tests/run-macros/newClassParamsExtendsClassParams.check new file mode 100644 index 000000000000..86d844c7d00f --- /dev/null +++ b/tests/run-macros/newClassParamsExtendsClassParams.check @@ -0,0 +1,4 @@ +Calling Foo.foo with i = 22 +class Test_2$package$foo$1 +Calling Foo.foo with i = 22 +class Test_2$package$bar$1 diff --git a/tests/run-macros/newClassParamsExtendsClassParams/Macro_1.scala b/tests/run-macros/newClassParamsExtendsClassParams/Macro_1.scala new file mode 100644 index 000000000000..061a0a231e75 --- /dev/null +++ b/tests/run-macros/newClassParamsExtendsClassParams/Macro_1.scala @@ -0,0 +1,30 @@ +//> using options -experimental + +import scala.quoted._ + +inline def makeClass(inline name: String): Foo = ${ makeClassExpr('name) } +private def makeClassExpr(nameExpr: Expr[String])(using Quotes): Expr[Foo] = { + import quotes.reflect.* + + val name = nameExpr.valueOrAbort + val parents = List('{ new Foo(1) }.asTerm) + def decls(cls: Symbol): List[Symbol] = Nil + + val cls = Symbol.newClass(Symbol.spliceOwner, name, parents = parents.map(_.tpe), decls, selfType = None, List("idx"), List(TypeRepr.of[Int]), Flags.EmptyFlags, Symbol.noSymbol) + + val parentsWithSym = List(Apply(Select(New(TypeTree.of[Foo]), TypeRepr.of[Foo].typeSymbol.primaryConstructor), List(Ref(cls.fieldMember("idx"))))) + val clsDef = ClassDef(cls, parentsWithSym, body = Nil) + val newCls = Typed(Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), List(Literal(IntConstant(22)))), TypeTree.of[Foo]) + + Block(List(clsDef), newCls).asExprOf[Foo] + + // '{ + // class `name`(idx: Int) extends Foo(idx) + // new `name`(22) + // } +} + +class Foo(i: Int) { + def foo(): Unit = println(s"Calling Foo.foo with i = $i") +} + diff --git a/tests/run-macros/newClassParamsExtendsClassParams/Test_2.scala b/tests/run-macros/newClassParamsExtendsClassParams/Test_2.scala new file mode 100644 index 000000000000..6e902825fdc6 --- /dev/null +++ b/tests/run-macros/newClassParamsExtendsClassParams/Test_2.scala @@ -0,0 +1,10 @@ +//> using options -experimental + +@main def Test: Unit = { + val foo: Foo = makeClass("foo") + foo.foo() + println(foo.getClass) + val bar: Foo = makeClass("bar") + bar.foo() + println(bar.getClass) +}