Skip to content

Add Scala 2 macro compat to Scala2Compat mode #8199

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Foo.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
object Foo {
def foo(): Int = macro Bar.fooImpl
}

object Bar {
def fooImpl(x: Int): Int = ???
}
8 changes: 7 additions & 1 deletion compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down Expand Up @@ -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?
*/
Expand Down
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/untpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
23 changes: 14 additions & 9 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Namer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
55 changes: 54 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions compiler/test/dotty/Properties.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down
8 changes: 8 additions & 0 deletions compiler/test/dotty/tools/vulpix/TestConfiguration.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 =
Expand Down
2 changes: 2 additions & 0 deletions project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
)
Expand Down Expand Up @@ -726,6 +727,7 @@ object Build {
"-Ddotty.tests.classes.dottyTastyInspector=" + jars("dotty-tasty-inspector"),
)
},
libraryDependencies += "org.scala-lang" % "scala-reflect" % stdlibVersion % "test",
packageAll := {
packageAll.in(`dotty-compiler`).value ++ Seq(
"dotty-compiler" -> packageBin.in(Compile).value.getAbsolutePath,
Expand Down
16 changes: 16 additions & 0 deletions tests/neg-macro-compat/Macro1.scala
Original file line number Diff line number Diff line change
@@ -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 = ???

}
20 changes: 20 additions & 0 deletions tests/pos-macro-compat/Macro1.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
12 changes: 12 additions & 0 deletions tests/pos-macro-compat/Macro2.scala
Original file line number Diff line number Diff line change
@@ -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] = ???

}