From 5ff03259d6736caa4f0ba52151b1db5dee554261 Mon Sep 17 00:00:00 2001 From: fhackett Date: Fri, 24 Jan 2020 19:49:09 -0500 Subject: [PATCH] Allow macros to generate method symbols, add missing method type constructors Since there already exists the machinery to generate a new tree for an existing method symbol, this change simply adds the ability to generate a fresh method symbol with the given name, type and flags. Then, `DefDef.apply` can be used to generate the full tree. Missing type constructors for `PolyType` and `ByNameType` are added, as well as corresponding `.param(Int)` accessors in order to make references to a `MethodType` or a `PolyType`'s parameter types. Testing is achieved by synthesizing a series of method definitions, along with references to these methods and some basic correctness assertions. The purpose of each test is described in a comment above the corresponding block of code. Note 1: it is possible to additionaly specify flags and private in the `Symbol.newMethod` function. These seem unnecessary for just defining local functions, but may be useful in synthesizing more complex declarations that may be supported later, such as local classes. Note 2: implicit/given method types are not exposed by this change. Since these types are structurally identical to normal method types, perhaps some optional flags could be added to the MethodType constructor for this purpose? --- .../ReflectionCompilerInterface.scala | 12 ++ .../tasty/reflect/CompilerInterface.scala | 8 + .../src/scala/tasty/reflect/SymbolOps.scala | 19 ++ .../scala/tasty/reflect/TypeOrBoundsOps.scala | 5 + .../tasty-create-method-symbol.check | 8 + .../tasty-create-method-symbol/Macro_1.scala | 199 ++++++++++++++++++ .../tasty-create-method-symbol/Test_2.scala | 6 + 7 files changed, 257 insertions(+) create mode 100644 tests/run-macros/tasty-create-method-symbol.check create mode 100644 tests/run-macros/tasty-create-method-symbol/Macro_1.scala create mode 100644 tests/run-macros/tasty-create-method-symbol/Test_2.scala diff --git a/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala b/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala index 339ea20dbae6..006ff0450662 100644 --- a/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala +++ b/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala @@ -1372,6 +1372,8 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend case _ => None } + def ByNameType_apply(underlying: Type)(given Context): Type = Types.ExprType(underlying) + def ByNameType_underlying(self: ByNameType)(given Context): Type = self.resType.stripTypeVar type ParamRef = Types.ParamRef @@ -1437,6 +1439,7 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend def MethodType_isErased(self: MethodType): Boolean = self.isErasedMethod def MethodType_isImplicit(self: MethodType): Boolean = self.isImplicitMethod + def MethodType_param(self: MethodType, idx: Int)(given Context): Type = self.newParamRef(idx) def MethodType_paramNames(self: MethodType)(given Context): List[String] = self.paramNames.map(_.toString) def MethodType_paramTypes(self: MethodType)(given Context): List[Type] = self.paramInfos def MethodType_resType(self: MethodType)(given Context): Type = self.resType @@ -1450,6 +1453,10 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend case _ => None } + def PolyType_apply(paramNames: List[String])(paramBoundsExp: PolyType => List[TypeBounds], resultTypeExp: PolyType => Type)(given Context): PolyType = + Types.PolyType(paramNames.map(_.toTypeName))(paramBoundsExp, resultTypeExp) + + def PolyType_param(self: PolyType, idx: Int)(given Context): Type = self.newParamRef(idx) def PolyType_paramNames(self: PolyType)(given Context): List[String] = self.paramNames.map(_.toString) def PolyType_paramBounds(self: PolyType)(given Context): List[TypeBounds] = self.paramInfos def PolyType_resType(self: PolyType)(given Context): Type = self.resType @@ -1717,6 +1724,11 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend def Symbol_of(fullName: String)(given ctx: Context): Symbol = ctx.requiredClass(fullName) + def Symbol_newMethod(parent: Symbol, name: String, flags: Flags, tpe: Type, privateWithin: Symbol)(given ctx: Context): Symbol = { + val computedFlags = flags | Flags.Method + ctx.newSymbol(parent, name.toTermName, computedFlags, tpe, privateWithin) + } + def Symbol_isTypeParam(self: Symbol)(given Context): Boolean = self.isTypeParam diff --git a/library/src/scala/tasty/reflect/CompilerInterface.scala b/library/src/scala/tasty/reflect/CompilerInterface.scala index 4ec673e9754f..7cd5e1fbdc6a 100644 --- a/library/src/scala/tasty/reflect/CompilerInterface.scala +++ b/library/src/scala/tasty/reflect/CompilerInterface.scala @@ -986,6 +986,8 @@ trait CompilerInterface { def isInstanceOfByNameType(given ctx: Context): IsInstanceOf[ByNameType] + def ByNameType_apply(underlying: Type)(given ctx: Context): Type + def ByNameType_underlying(self: ByNameType)(given ctx: Context): Type /** Type of a parameter reference */ @@ -1031,6 +1033,7 @@ trait CompilerInterface { def MethodType_isErased(self: MethodType): Boolean def MethodType_isImplicit(self: MethodType): Boolean + def MethodType_param(self: MethodType, ids: Int)(given ctx: Context): Type def MethodType_paramNames(self: MethodType)(given ctx: Context): List[String] def MethodType_paramTypes(self: MethodType)(given ctx: Context): List[Type] def MethodType_resType(self: MethodType)(given ctx: Context): Type @@ -1040,6 +1043,9 @@ trait CompilerInterface { def isInstanceOfPolyType(given ctx: Context): IsInstanceOf[PolyType] + def PolyType_apply(paramNames: List[String])(paramBoundsExp: PolyType => List[TypeBounds], resultTypeExp: PolyType => Type)(given ctx: Context): PolyType + + def PolyType_param(self: PolyType, idx: Int)(given ctx: Context): Type def PolyType_paramNames(self: PolyType)(given ctx: Context): List[String] def PolyType_paramBounds(self: PolyType)(given ctx: Context): List[TypeBounds] def PolyType_resType(self: PolyType)(given ctx: Context): Type @@ -1264,6 +1270,8 @@ trait CompilerInterface { def Symbol_of(fullName: String)(given ctx: Context): Symbol + def Symbol_newMethod(parent: Symbol, name: String, flags: Flags, tpe: Type, privateWithin: Symbol)(given ctx: Context): Symbol + def Symbol_isTypeParam(self: Symbol)(given ctx: Context): Boolean def Symbol_isPackageDef(symbol: Symbol)(given ctx: Context): Boolean diff --git a/library/src/scala/tasty/reflect/SymbolOps.scala b/library/src/scala/tasty/reflect/SymbolOps.scala index 7b22c57e9dbd..98eddfc57216 100644 --- a/library/src/scala/tasty/reflect/SymbolOps.scala +++ b/library/src/scala/tasty/reflect/SymbolOps.scala @@ -9,6 +9,25 @@ trait SymbolOps extends Core { selfSymbolOps: FlagsOps => def classSymbol(fullName: String)(given ctx: Context): Symbol = internal.Symbol_of(fullName) + /** Generates a new method symbol with the given parent, name and type. + * + * This symbol starts without an accompanying definition. + * It is the meta-programmer's responsibility to provide exactly one corresponding definition by passing + * this symbol to the DefDef constructor. + * + * @note As a macro can only splice code into the point at which it is expanded, all generated symbols must be + * direct or indirect children of the reflection context's owner. */ + def newMethod(parent: Symbol, name: String, tpe: Type)(given ctx: Context): Symbol = + newMethod(parent, name, tpe, Flags.EmptyFlags, noSymbol) + + /** Works as the other newMethod, but with additional parameters. + * + * @param flags extra flags to with which the symbol should be constructed + * @param privateWithin the symbol within which this new method symbol should be private. May be noSymbol. + * */ + def newMethod(parent: Symbol, name: String, tpe: Type, flags: Flags, privateWithin: Symbol)(given ctx: Context): Symbol = + internal.Symbol_newMethod(parent, name, flags, tpe, privateWithin) + /** Definition not available */ def noSymbol(given ctx: Context): Symbol = internal.Symbol_noSymbol diff --git a/library/src/scala/tasty/reflect/TypeOrBoundsOps.scala b/library/src/scala/tasty/reflect/TypeOrBoundsOps.scala index dbf9cd39ee57..b8205fa0ed2a 100644 --- a/library/src/scala/tasty/reflect/TypeOrBoundsOps.scala +++ b/library/src/scala/tasty/reflect/TypeOrBoundsOps.scala @@ -286,6 +286,7 @@ trait TypeOrBoundsOps extends Core { def unapply(x: ByNameType)(given ctx: Context): Option[ByNameType] = Some(x) object ByNameType { + def apply(underlying: Type)(given ctx: Context): Type = internal.ByNameType_apply(underlying) def unapply(x: ByNameType)(given ctx: Context): Option[Type] = Some(x.underlying) } @@ -368,6 +369,7 @@ trait TypeOrBoundsOps extends Core { given MethodTypeOps: extension (self: MethodType) { def isImplicit: Boolean = internal.MethodType_isImplicit(self) def isErased: Boolean = internal.MethodType_isErased(self) + def param(idx: Int)(given ctx: Context): Type = internal.MethodType_param(self, idx) def paramNames(given ctx: Context): List[String] = internal.MethodType_paramNames(self) def paramTypes(given ctx: Context): List[Type] = internal.MethodType_paramTypes(self) def resType(given ctx: Context): Type = internal.MethodType_resType(self) @@ -380,11 +382,14 @@ trait TypeOrBoundsOps extends Core { def unapply(x: PolyType)(given ctx: Context): Option[PolyType] = Some(x) object PolyType { + def apply(paramNames: List[String])(paramBoundsExp: PolyType => List[TypeBounds], resultTypeExp: PolyType => Type)(given ctx: Context): PolyType = + internal.PolyType_apply(paramNames)(paramBoundsExp, resultTypeExp) def unapply(x: PolyType)(given ctx: Context): Option[(List[String], List[TypeBounds], Type)] = Some((x.paramNames, x.paramBounds, x.resType)) } given PolyTypeOps: extension (self: PolyType) { + def param(idx: Int)(given ctx: Context): Type = internal.PolyType_param(self, idx) def paramNames(given ctx: Context): List[String] = internal.PolyType_paramNames(self) def paramBounds(given ctx: Context): List[TypeBounds] = internal.PolyType_paramBounds(self) def resType(given ctx: Context): Type = internal.PolyType_resType(self) diff --git a/tests/run-macros/tasty-create-method-symbol.check b/tests/run-macros/tasty-create-method-symbol.check new file mode 100644 index 000000000000..a7b45ea3f6ca --- /dev/null +++ b/tests/run-macros/tasty-create-method-symbol.check @@ -0,0 +1,8 @@ +sym6_2: 6 +sym6_1: 5 +sym6_2: 4 +sym6_1: 3 +sym6_2: 2 +sym6_1: 1 +sym6_2: 0 +Ok diff --git a/tests/run-macros/tasty-create-method-symbol/Macro_1.scala b/tests/run-macros/tasty-create-method-symbol/Macro_1.scala new file mode 100644 index 000000000000..8296d95f962b --- /dev/null +++ b/tests/run-macros/tasty-create-method-symbol/Macro_1.scala @@ -0,0 +1,199 @@ +import quoted._ + +object Macros { + + inline def theTestBlock : Unit = ${ theTestBlockImpl } + + def theTestBlockImpl(given qctx: QuoteContext) : Expr[Unit] = { + import qctx.tasty.{_,given} + + // simple smoke test + val sym1 : Symbol = Symbol.newMethod( + rootContext.owner, + "sym1", + MethodType(List("a","b"))( + _ => List(typeOf[Int], typeOf[Int]), + _ => typeOf[Int])) + assert(sym1.isDefDef) + assert(sym1.name == "sym1") + val sym1Statements : List[Statement] = List( + DefDef(sym1, { + case List() => { + case List(List(a, b)) => + Some('{ ${ a.seal.asInstanceOf[Expr[Int]] } - ${ b.seal.asInstanceOf[Expr[Int]] } }.unseal) + } + }), + '{ assert(${ Apply(Ref(sym1), List(Literal(Constant(2)), Literal(Constant(3)))).seal.asInstanceOf[Expr[Int]] } == -1) }.unseal) + + // test for no argument list (no Apply node) + val sym2 : Symbol = Symbol.newMethod( + rootContext.owner, + "sym2", + ByNameType(typeOf[Int])) + assert(sym2.isDefDef) + assert(sym2.name == "sym2") + val sym2Statements : List[Statement] = List( + DefDef(sym2, { + case List() => { + case List() => + Some(Literal(Constant(2))) + } + }), + '{ assert(${ Ref(sym2).seal.asInstanceOf[Expr[Int]] } == 2) }.unseal) + + // test for multiple argument lists + val sym3 : Symbol = Symbol.newMethod( + rootContext.owner, + "sym3", + MethodType(List("a"))( + _ => List(typeOf[Int]), + mt => MethodType(List("b"))( + _ => List(mt.param(0)), + _ => mt.param(0)))) + assert(sym3.isDefDef) + assert(sym3.name == "sym3") + val sym3Statements : List[Statement] = List( + DefDef(sym3, { + case List() => { + case List(List(a), List(b)) => + Some(a) + } + }), + '{ assert(${ Apply(Apply(Ref(sym3), List(Literal(Constant(3)))), List(Literal(Constant(3)))).seal.asInstanceOf[Expr[Int]] } == 3) }.unseal) + + // test for recursive references + val sym4 : Symbol = Symbol.newMethod( + rootContext.owner, + "sym4", + MethodType(List("x"))( + _ => List(typeOf[Int]), + _ => typeOf[Int])) + assert(sym4.isDefDef) + assert(sym4.name == "sym4") + val sym4Statements : List[Statement] = List( + DefDef(sym4, { + case List() => { + case List(List(x)) => + Some('{ + if ${ x.seal.asInstanceOf[Expr[Int]] } == 0 + then 0 + else ${ Apply(Ref(sym4), List('{ ${ x.seal.asInstanceOf[Expr[Int]] } - 1 }.unseal)).seal.asInstanceOf[Expr[Int]] } + }.unseal) + } + }), + '{ assert(${ Apply(Ref(sym4), List(Literal(Constant(4)))).seal.asInstanceOf[Expr[Int]] } == 0) }.unseal) + + // test for nested functions (one symbol is the other's parent, and we use a Closure) + val sym5 : Symbol = Symbol.newMethod( + rootContext.owner, + "sym5", + MethodType(List("x"))( + _ => List(typeOf[Int]), + _ => typeOf[Int=>Int])) + assert(sym5.isDefDef) + assert(sym5.name == "sym5") + val sym5Statements : List[Statement] = List( + DefDef(sym5, { + case List() => { + case List(List(x)) => + Some { + val sym51 : Symbol = Symbol.newMethod( + sym5, + "sym51", + MethodType(List("x"))( + _ => List(typeOf[Int]), + _ => typeOf[Int])) + Block( + List( + DefDef(sym51, { + case List() => { + case List(List(xx)) => + Some('{ ${ x.seal.asInstanceOf[Expr[Int]] } - ${ xx.seal.asInstanceOf[Expr[Int]] } }.unseal) + } + })), + Closure(Ref(sym51), None)) + } + } + }), + '{ assert(${ Apply(Ref(sym5), List(Literal(Constant(5)))).seal.asInstanceOf[Expr[Int=>Int]] }(4) == 1) }.unseal) + + // test mutually recursive definitions + val sym6_1 : Symbol = Symbol.newMethod( + rootContext.owner, + "sym6_1", + MethodType(List("x"))( + _ => List(typeOf[Int]), + _ => typeOf[Int])) + val sym6_2 : Symbol = Symbol.newMethod( + rootContext.owner, + "sym6_2", + MethodType(List("x"))( + _ => List(typeOf[Int]), + _ => typeOf[Int])) + assert(sym6_1.isDefDef) + assert(sym6_2.isDefDef) + assert(sym6_1.name == "sym6_1") + assert(sym6_2.name == "sym6_2") + val sym6Statements : List[Statement] = List( + DefDef(sym6_1, { + case List() => { + case List(List(x)) => + Some { + '{ + println(s"sym6_1: ${ ${ x.seal.asInstanceOf[Expr[Int]] } }") + if ${ x.seal.asInstanceOf[Expr[Int]] } == 0 + then 0 + else ${ Apply(Ref(sym6_2), List('{ ${ x.seal.asInstanceOf[Expr[Int]] } - 1 }.unseal)).seal.asInstanceOf[Expr[Int]] } + }.unseal + } + } + }), + DefDef(sym6_2, { + case List() => { + case List(List(x)) => + Some { + '{ + println(s"sym6_2: ${ ${ x.seal.asInstanceOf[Expr[Int]] } }") + if ${ x.seal.asInstanceOf[Expr[Int]] } == 0 + then 0 + else ${ Apply(Ref(sym6_1), List('{ ${ x.seal.asInstanceOf[Expr[Int]] } - 1 }.unseal)).seal.asInstanceOf[Expr[Int]] } + }.unseal + } + } + + }), + '{ assert(${ Apply(Ref(sym6_2), List(Literal(Constant(6)))).seal.asInstanceOf[Expr[Int]] } == 0) }.unseal) + + // test polymorphic methods by synthesizing an identity method + val sym7 : Symbol = Symbol.newMethod( + rootContext.owner, + "sym7", + PolyType(List("T"))( + tp => List(TypeBounds(typeOf[Nothing], typeOf[Any])), + tp => MethodType(List("t"))( + _ => List(tp.param(0)), + _ => tp.param(0)))) + assert(sym7.isDefDef) + assert(sym7.name == "sym7") + val sym7Statements : List[Statement] = List( + DefDef(sym7, { + case List(t) => { + case List(List(x)) => + Some(Typed(x, Inferred(t))) + } + }), + '{ assert(${ Apply(TypeApply(Ref(sym7), List(Inferred(typeOf[Int]))), List(Literal(Constant(7)))).seal.asInstanceOf[Expr[Int]] } == 7) }.unseal) + + Block( + sym1Statements ++ + sym2Statements ++ + sym3Statements ++ + sym4Statements ++ + sym5Statements ++ + sym6Statements ++ + sym7Statements ++ + List('{ println("Ok") }.unseal), + Literal(Constant(()))).seal.asInstanceOf[Expr[Unit]] + } +} + diff --git a/tests/run-macros/tasty-create-method-symbol/Test_2.scala b/tests/run-macros/tasty-create-method-symbol/Test_2.scala new file mode 100644 index 000000000000..63e15a06e189 --- /dev/null +++ b/tests/run-macros/tasty-create-method-symbol/Test_2.scala @@ -0,0 +1,6 @@ + +object Test { + def main(argv: Array[String]): Unit = + Macros.theTestBlock +} +