From 75f0588bbac73bf4b4e8f59caa8060a3a2ba0cfc Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Tue, 4 Feb 2020 16:12:02 +0100 Subject: [PATCH 1/2] Add Scala 2 macro compat to Scala2Compat mode --- Foo.scala | 7 +++ .../src/dotty/tools/dotc/ast/Desugar.scala | 8 ++- compiler/src/dotty/tools/dotc/ast/untpd.scala | 3 + .../tools/dotc/core/SymDenotations.scala | 2 +- .../dotty/tools/dotc/parsing/Parsers.scala | 23 +++++--- .../tools/dotc/printing/RefinedPrinter.scala | 2 + .../src/dotty/tools/dotc/typer/Namer.scala | 2 +- .../src/dotty/tools/dotc/typer/Typer.scala | 55 ++++++++++++++++++- compiler/test/dotty/Properties.scala | 3 + .../BootstrappedOnlyCompilationTests.scala | 10 +++- .../tools/vulpix/TestConfiguration.scala | 8 +++ project/Build.scala | 2 + tests/neg-macro-compat/Macro1.scala | 16 ++++++ tests/pos-macro-compat/Macro1.scala | 20 +++++++ tests/pos-macro-compat/Macro2.scala | 12 ++++ .../pos => pos-macro-compat}/t5706.scala | 0 16 files changed, 158 insertions(+), 15 deletions(-) create mode 100644 Foo.scala create mode 100644 tests/neg-macro-compat/Macro1.scala create mode 100644 tests/pos-macro-compat/Macro1.scala create mode 100644 tests/pos-macro-compat/Macro2.scala rename tests/{disabled/macro/pos => pos-macro-compat}/t5706.scala (100%) diff --git a/Foo.scala b/Foo.scala new file mode 100644 index 000000000000..8ecfef1a8122 --- /dev/null +++ b/Foo.scala @@ -0,0 +1,7 @@ +object Foo { + def foo(): Int = macro Bar.fooImpl +} + +object Bar { + def fooImpl(x: Int): Int = ??? +} \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 88714849587b..36114ef427a5 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -259,6 +259,12 @@ object desugar { case _ => } + meth1.rhs match { + case MacroTree(call) => + meth1 = cpy.DefDef(meth1)(rhs = call).withMods(meth1.mods | Macro | Erased) + case _ => + } + /** The longest prefix of parameter lists in vparamss whose total length does not exceed `n` */ def takeUpTo(vparamss: List[List[ValDef]], n: Int): List[List[ValDef]] = vparamss match { case vparams :: vparamss1 => @@ -684,7 +690,7 @@ object desugar { val mods = constr1.mods mods.is(Private) || (!mods.is(Protected) && mods.hasPrivateWithin) } - + /** Does one of the parameter's types (in the first param clause) * mention a preceding parameter? */ diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 0cea2c2160c0..2a9ac718f7f1 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -111,6 +111,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case class ContextBounds(bounds: TypeBoundsTree, cxBounds: List[Tree])(implicit @constructorOnly src: SourceFile) extends TypTree case class PatDef(mods: Modifiers, pats: List[Tree], tpt: Tree, rhs: Tree)(implicit @constructorOnly src: SourceFile) extends DefTree case class Export(expr: Tree, selectors: List[ImportSelector])(implicit @constructorOnly src: SourceFile) extends Tree + case class MacroTree(call: Tree)(implicit @constructorOnly src: SourceFile) extends Tree case class ImportSelector(imported: Ident, renamed: Tree = EmptyTree, bound: Tree = EmptyTree)(implicit @constructorOnly src: SourceFile) extends Tree { // TODO: Make bound a typed tree? @@ -732,6 +733,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { x case TypedSplice(splice) => this(x, splice) + case MacroTree(call) => + this(x, call) case _ => super.foldMoreCases(x, tree) } diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 0b5780a52886..cd06f1b5bbaf 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -933,7 +933,7 @@ object SymDenotations { isAllOf(InlineMethod, butNot = Accessor) /** Is this a Scala 2 macro */ - final def isScala2Macro(implicit ctx: Context): Boolean = is(Macro) && symbol.owner.is(Scala2x) + final def isScala2Macro(implicit ctx: Context): Boolean = is(Macro, butNot = Inline) /** An erased value or an inline method. */ diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 166d44ecb4ff..2c89d4d14dcb 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2225,15 +2225,20 @@ object Parsers { newExpr() case MACRO => val start = in.skipToken() - val call = ident() - // The standard library uses "macro ???" to denote "fast track" macros - // hardcoded in the compiler, don't issue an error for those macros - // since we want to be able to compile the standard library. - if (call `ne` nme.???) - syntaxError( - "Scala 2 macros are not supported, see https://dotty.epfl.ch/docs/reference/dropped-features/macros.html", - start) - unimplementedExpr + val call = simpleExpr() + call match + case Ident(name) if name eq nme.??? => + // The standard library uses "macro ???" to denote "fast track" macros + // hardcoded in the compiler, don't issue an error for those macros + // since we want to be able to compile the standard library. + MacroTree(call) + case _ if ctx.scala2CompatMode => + MacroTree(call) + case _ => + syntaxError( + "Scala 2 macros are not supported, see https://dotty.epfl.ch/docs/reference/dropped-features/macros.html", + start) + unimplementedExpr case COLONEOL => syntaxError("':' not allowed here") in.nextToken() diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index e6937033c68c..0ef99d2615b0 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -634,6 +634,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { toText(tree.app) ~ Str("(with integrated type args)").provided(printDebug) case Thicket(trees) => "Thicket {" ~~ toTextGlobal(trees, "\n") ~~ "}" + case MacroTree(call) => + keywordStr("macro ") ~ toTextGlobal(call) case _ => tree.fallbackToText(this) } diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index a57c5b39a244..12e6711aca07 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -437,7 +437,7 @@ class Namer { typer: Typer => * package members are not entered twice in the same run. */ def enterSymbol(sym: Symbol)(implicit ctx: Context): Symbol = { - if (sym.exists) { + if (sym.exists && !sym.isScala2Macro) { typr.println(s"entered: $sym in ${ctx.owner}") ctx.enter(sym) } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index cb770d5619f2..6df509db2c5e 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1675,8 +1675,61 @@ class Typer extends Namer } } + def typeScala2MacroBody(tree: untpd.Tree)(implicit ctx: Context): Tree = + def checked(rhs: Tree): rhs.type = { + if ctx.phase.isTyper && rhs.symbol.ne(defn.Predef_undefined) then + val reflectWContextType = ctx.requiredClassRef("scala.reflect.macros.whitebox.Context") + val reflectBContextType = ctx.requiredClassRef("scala.reflect.macros.blackbox.Context") + def checkResType(res: Type, cParam: TermParamRef): Unit = + res match + case AppliedType(expr, res) if expr <:< cParam.select(tpnme.Expr) => + // TODO check res type + case _ => + ctx.error(s"Expected macro implementation to return a ${cParam.paramName}.Expr", tree.sourcePos) + + def checkParams(tpe: Type, paramss: List[List[ValDef]], cParam: TermParamRef): Unit = { + tpe match + case mt: MethodType => + paramss match + case params :: paramss2 => + if mt.paramInfos.size != params.size then + ctx.error("Wrong number of macro arguments", tree.sourcePos) + else + val expected = params.map(x => cParam.select(tpnme.Expr).appliedTo(x.tpt.tpe)) + for ((e, a) <- expected.zip(mt.paramInfos)) + if !(a =:= e) then ctx.error("Expected macro implementation to match parameters", tree.sourcePos) + checkParams(mt.resType, paramss2, cParam) + case Nil => + ctx.error("Expected macro implementation has too many parameter groups", tree.sourcePos) + + case res => + if paramss.isEmpty then checkResType(res, cParam) + else ctx.error("Expected macro implementation to not have parameters after Context", tree.sourcePos) + } + rhs.tpe.widenDealias match + case mt: PolyType => + // TODO + ctx.error("Scala 2 macro definitions with type parameters not supported yet", tree.sourcePos) + case mt: MethodType => + mt.paramInfos match + case c :: Nil if c <:< reflectWContextType || c <:< reflectBContextType => + checkParams(mt.resType, vparamss1, mt.paramRefs(0)) + case _ => + ctx.error("Expected macro implementation first parameter to be Context parameter", tree.sourcePos) + case _ => + ctx.error("Expected macro implementation to have at least a Context parameter", tree.sourcePos) + rhs + } + val rhs1 = ddef.rhs match + case rhs0: Ident => typedIdent(rhs0, defn.AnyType) + case rhs0: Select => typedSelect(rhs0, defn.AnyType) + case rhs0: TypeApply => typedTypeApply(rhs0, defn.AnyType) + checked(rhs1) + if (sym.isInlineMethod) rhsCtx.addMode(Mode.InlineableBody) - val rhs1 = typedExpr(ddef.rhs, tpt1.tpe.widenExpr)(rhsCtx) + val rhs1 = + if sym.isScala2Macro then typeScala2MacroBody(ddef.rhs)(rhsCtx) // Scala 2 macro definition + else typedExpr(ddef.rhs, tpt1.tpe.widenExpr)(rhsCtx) if (sym.isInlineMethod) PrepareInlineable.registerInlineInfo(sym, _ => rhs1) diff --git a/compiler/test/dotty/Properties.scala b/compiler/test/dotty/Properties.scala index cf9a92d58832..9e3e9985de0d 100644 --- a/compiler/test/dotty/Properties.scala +++ b/compiler/test/dotty/Properties.scala @@ -68,6 +68,9 @@ object Properties { /** scala-asm jar */ def scalaAsm: String = sys.props("dotty.tests.classes.scalaAsm") + /** scala-reflect jar */ + def scalaReflect: String = sys.props("dotty.tests.classes.scalaReflect") + /** jline-terminal jar */ def jlineTerminal: String = sys.props("dotty.tests.classes.jlineTerminal") diff --git a/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala b/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala index 6f8d4c162b88..430bacd12c49 100644 --- a/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala @@ -32,7 +32,10 @@ class BootstrappedOnlyCompilationTests extends ParallelTesting { @Test def posMacros: Unit = { implicit val testGroup: TestGroup = TestGroup("compilePosMacros") - compileFilesInDir("tests/pos-macros", defaultOptions).checkCompile() + aggregateTests( + compileFilesInDir("tests/pos-macros", defaultOptions), + compileFilesInDir("tests/pos-macro-compat", withReflectOptions), + ).checkCompile() } @Test def posWithCompiler: Unit = { @@ -100,7 +103,10 @@ class BootstrappedOnlyCompilationTests extends ParallelTesting { @Test def negMacros: Unit = { implicit val testGroup: TestGroup = TestGroup("compileNegWithCompiler") - compileFilesInDir("tests/neg-macros", defaultOptions).checkExpectedErrors() + aggregateTests( + compileFilesInDir("tests/neg-macros", defaultOptions), + compileFilesInDir("tests/neg-macro-compat", withReflectOptions), + ).checkExpectedErrors() } @Test def negWithCompiler: Unit = { diff --git a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala index cdea285eceb0..9d788e7fd27f 100644 --- a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala +++ b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala @@ -24,6 +24,12 @@ object TestConfiguration { Properties.dottyLibrary )) + val withReflectClasspath = mkClasspath(List( + Properties.scalaLibrary, + Properties.dottyLibrary, + Properties.scalaReflect, + )) + val withCompilerClasspath = mkClasspath(List( Properties.scalaLibrary, Properties.scalaAsm, @@ -55,6 +61,8 @@ object TestConfiguration { val defaultOptions = TestFlags(basicClasspath, commonOptions) val withCompilerOptions = defaultOptions.withClasspath(withCompilerClasspath).withRunClasspath(withCompilerClasspath) + lazy val withReflectOptions = + defaultOptions.withClasspath(withReflectClasspath).and("-language:Scala2Compat") lazy val withStagingOptions = defaultOptions.withClasspath(withStagingClasspath).withRunClasspath(withStagingClasspath) lazy val withTastyInspectorOptions = diff --git a/project/Build.scala b/project/Build.scala index 2996c8ac4874..47053dd61338 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -536,6 +536,7 @@ object Build { "-Ddotty.tests.classes.compilerInterface=" + findArtifactPath(externalDeps, "compiler-interface"), "-Ddotty.tests.classes.scalaLibrary=" + findArtifactPath(externalDeps, "scala-library"), "-Ddotty.tests.classes.scalaAsm=" + findArtifactPath(externalDeps, "scala-asm"), + "-Ddotty.tests.classes.scalaReflect=" + findArtifactPath(externalDeps, "scala-reflect"), "-Ddotty.tests.classes.jlineTerminal=" + findArtifactPath(externalDeps, "jline-terminal"), "-Ddotty.tests.classes.jlineReader=" + findArtifactPath(externalDeps, "jline-reader"), ) @@ -726,6 +727,7 @@ object Build { "-Ddotty.tests.classes.dottyTastyInspector=" + jars("dotty-tasty-inspector"), ) }, + libraryDependencies += "org.scala-lang" % "scala-reflect" % "2.13.1", packageAll := { packageAll.in(`dotty-compiler`).value ++ Seq( "dotty-compiler" -> packageBin.in(Compile).value.getAbsolutePath, diff --git a/tests/neg-macro-compat/Macro1.scala b/tests/neg-macro-compat/Macro1.scala new file mode 100644 index 000000000000..0918cbab22a8 --- /dev/null +++ b/tests/neg-macro-compat/Macro1.scala @@ -0,0 +1,16 @@ +import scala.language.experimental.macros +import scala.reflect.macros._ + +object Macros { + + def foo0(x: Int): Int = macro foo0Impl // error + def foo1(x: Int): Int = macro foo1Impl // error + def foo2(x: Int, y: String): Int = macro foo2Impl // error + def foo3(x: Int): Int = macro foo3Impl // error + + def foo0Impl: Nothing = ??? + def foo1Impl(context: scala.reflect.macros.Context)(x: context.Expr[String]): context.Expr[Int] = ??? + def foo2Impl(context: scala.reflect.macros.Context)(x: context.Expr[Int], y: context.Expr[String]): Nothing = ??? + def foo3Impl(x: Int): Int = ??? + +} diff --git a/tests/pos-macro-compat/Macro1.scala b/tests/pos-macro-compat/Macro1.scala new file mode 100644 index 000000000000..2ce9048cfb0a --- /dev/null +++ b/tests/pos-macro-compat/Macro1.scala @@ -0,0 +1,20 @@ +object Macro1 { + import scala.language.experimental.macros + def lineNumber: Int = macro LineNumberMacro.thisLineNumberImpl + inline def lineNumber: Int = ${ LineNumberMacro.thisLineNumberExpr } +} + +object LineNumberMacro { + import scala.reflect.macros._ + def thisLineNumberImpl(context: Context): context.Expr[Int] = { + val lineNumber = context.enclosingPosition.line + context.literal(lineNumber) + } + + import scala.quoted._ + def thisLineNumberExpr(using qctx: QuoteContext): Expr[Int] = { + // import qctx.tasty.{_, given _} + // Expr(rootPosition.startLine + 1) + Expr(3) + } +} diff --git a/tests/pos-macro-compat/Macro2.scala b/tests/pos-macro-compat/Macro2.scala new file mode 100644 index 000000000000..abe9eee66f54 --- /dev/null +++ b/tests/pos-macro-compat/Macro2.scala @@ -0,0 +1,12 @@ +import scala.language.experimental.macros +import scala.reflect.macros._ + +object Macros { + + def foo1(x: Int): Int = macro foo1Impl + def foo2(x: Int, y: String): Int = macro foo2Impl + + def foo1Impl(context: scala.reflect.macros.Context)(x: context.Expr[Int]): context.Expr[Int] = ??? + def foo2Impl(context: scala.reflect.macros.Context)(x: context.Expr[Int], y: context.Expr[String]): context.Expr[Int] = ??? + +} diff --git a/tests/disabled/macro/pos/t5706.scala b/tests/pos-macro-compat/t5706.scala similarity index 100% rename from tests/disabled/macro/pos/t5706.scala rename to tests/pos-macro-compat/t5706.scala From 0f342e62dceb5d9ac65250b27e0dbb197deebad1 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Thu, 6 Feb 2020 11:35:48 +0100 Subject: [PATCH 2/2] Update project/Build.scala Co-Authored-By: Guillaume Martres --- project/Build.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Build.scala b/project/Build.scala index 47053dd61338..05606905e2c4 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -727,7 +727,7 @@ object Build { "-Ddotty.tests.classes.dottyTastyInspector=" + jars("dotty-tasty-inspector"), ) }, - libraryDependencies += "org.scala-lang" % "scala-reflect" % "2.13.1", + libraryDependencies += "org.scala-lang" % "scala-reflect" % stdlibVersion % "test", packageAll := { packageAll.in(`dotty-compiler`).value ++ Seq( "dotty-compiler" -> packageBin.in(Compile).value.getAbsolutePath,