diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 362b7815db6a..dded9fec75f6 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -501,7 +501,6 @@ object StdNames { val lang: N = "lang" val length: N = "length" val lengthCompare: N = "lengthCompare" - val `macro` : N = "macro" val macroThis : N = "_this" val macroContext : N = "c" val main: N = "main" diff --git a/compiler/src/dotty/tools/dotc/transform/Splicer.scala b/compiler/src/dotty/tools/dotc/transform/Splicer.scala index e4c734695cf7..6f1dd3c82abf 100644 --- a/compiler/src/dotty/tools/dotc/transform/Splicer.scala +++ b/compiler/src/dotty/tools/dotc/transform/Splicer.scala @@ -37,14 +37,17 @@ object Splicer { * * See: `Staging` */ - def splice(tree: Tree, pos: SourcePosition, classLoader: ClassLoader)(implicit ctx: Context): Tree = tree match { + def splice(tree: Tree, pos: SourcePosition, classLoader: ClassLoader)(given ctx: Context): Tree = tree match { case Quoted(quotedTree) => quotedTree case _ => val interpreter = new Interpreter(pos, classLoader) + val macroOwner = ctx.newSymbol(ctx.owner, NameKinds.UniqueName.fresh(nme.MACROkw), Synthetic, defn.AnyType, coord = tree.span) try { + given Context = ctx.withOwner(macroOwner) // Some parts of the macro are evaluated during the unpickling performed in quotedExprToTree val interpretedExpr = interpreter.interpret[scala.quoted.QuoteContext => scala.quoted.Expr[Any]](tree) - interpretedExpr.fold(tree)(macroClosure => PickledQuotes.quotedExprToTree(macroClosure(QuoteContext()))) + val interpretedTree = interpretedExpr.fold(tree)(macroClosure => PickledQuotes.quotedExprToTree(macroClosure(QuoteContext()))) + checkEscapedVariables(interpretedTree, macroOwner).changeOwner(macroOwner, ctx.owner) } catch { case ex: CompilationUnit.SuspendException => @@ -63,6 +66,50 @@ object Splicer { } } + /** Checks that no symbol that whas generated within the macro expansion has an out of scope reference */ + def checkEscapedVariables(tree: Tree, expansionOwner: Symbol)(given ctx: Context): tree.type = + new TreeTraverser { + private[this] var locals = Set.empty[Symbol] + private def markDef(tree: Tree)(implicit ctx: Context): Unit = tree match { + case tree: DefTree => + val sym = tree.symbol + if (!locals.contains(sym)) + locals = locals + sym + case _ => + } + def traverse(tree: Tree)(given ctx: Context): Unit = + def traverseOver(lastEntered: Set[Symbol]) = + try traverseChildren(tree) + finally locals = lastEntered + tree match + case tree: Ident if isEscapedVariable(tree.symbol) => + val sym = tree.symbol + ctx.error(em"While expanding a macro, a reference to $sym was used outside the scope where it was defined", tree.sourcePos) + case Block(stats, _) => + val last = locals + stats.foreach(markDef) + traverseOver(last) + case CaseDef(pat, guard, body) => + val last = locals + // mark all bindings + new TreeTraverser { + def traverse(tree: Tree)(implicit ctx: Context): Unit = { + markDef(tree) + traverseChildren(tree) + } + }.traverse(pat) + traverseOver(last) + case _ => + markDef(tree) + traverseChildren(tree) + private def isEscapedVariable(sym: Symbol)(given ctx: Context): Boolean = + sym.exists && !sym.is(Package) + && sym.owner.ownersIterator.contains(expansionOwner) // symbol was generated within the macro expansion + && !locals.contains(sym) // symbol is not in current scope + }.traverse(tree) + tree + + /** Check that the Tree can be spliced. `${'{xyz}}` becomes `xyz` * and for `$xyz` the tree of `xyz` is interpreted for which the * resulting expression is returned as a `Tree` diff --git a/staging/src/scala/quoted/staging/QuoteCompiler.scala b/staging/src/scala/quoted/staging/QuoteCompiler.scala index c886e878002c..753f9db129af 100644 --- a/staging/src/scala/quoted/staging/QuoteCompiler.scala +++ b/staging/src/scala/quoted/staging/QuoteCompiler.scala @@ -16,6 +16,7 @@ import dotty.tools.dotc.core.Symbols.defn import dotty.tools.dotc.core.Types.ExprType import dotty.tools.dotc.core.quoted.PickledQuotes import dotty.tools.dotc.tastyreflect.ReflectionImpl +import dotty.tools.dotc.transform.Splicer.checkEscapedVariables import dotty.tools.dotc.transform.ReifyQuotes import dotty.tools.dotc.util.Spans.Span import dotty.tools.dotc.util.SourceFile @@ -67,8 +68,12 @@ private class QuoteCompiler extends Compiler { cls.enter(unitCtx.newDefaultConstructor(cls), EmptyScope) val meth = unitCtx.newSymbol(cls, nme.apply, Method, ExprType(defn.AnyType), coord = pos).entered - val qctx = dotty.tools.dotc.quoted.QuoteContext() - val quoted = PickledQuotes.quotedExprToTree(exprUnit.exprBuilder.apply(qctx))(unitCtx.withOwner(meth)) + val quoted = { + given Context = unitCtx.withOwner(meth) + val qctx = dotty.tools.dotc.quoted.QuoteContext() + val quoted = PickledQuotes.quotedExprToTree(exprUnit.exprBuilder.apply(qctx)) + checkEscapedVariables(quoted, meth) + } getLiteral(quoted) match { case Some(value) => diff --git a/tests/neg-macros/i7142/Macro_1.scala b/tests/neg-macros/i7142/Macro_1.scala new file mode 100644 index 000000000000..ebdf818baa54 --- /dev/null +++ b/tests/neg-macros/i7142/Macro_1.scala @@ -0,0 +1,9 @@ +package macros +import scala.quoted._ + +def oops(given QuoteContext) = { + var v = '{0}; + val q = '{ (x: Int) => ${ v = '{x}; v } } + '{$q($v)} +} +inline def test = ${oops} diff --git a/tests/neg-macros/i7142/Test_2.scala b/tests/neg-macros/i7142/Test_2.scala new file mode 100644 index 000000000000..06ccb017a659 --- /dev/null +++ b/tests/neg-macros/i7142/Test_2.scala @@ -0,0 +1,4 @@ + +object Test { + macros.test // error +} diff --git a/tests/neg-macros/i7142b/Macro_1.scala b/tests/neg-macros/i7142b/Macro_1.scala new file mode 100644 index 000000000000..0da378b536d2 --- /dev/null +++ b/tests/neg-macros/i7142b/Macro_1.scala @@ -0,0 +1,8 @@ +package macros +import scala.quoted._ +import scala.util.control.NonLocalReturns._ + +def oops(given QuoteContext): Expr[Int] = + returning('{ { (x: Int) => ${ throwReturn('x) }} apply 0 }) + +inline def test = ${oops} diff --git a/tests/neg-macros/i7142b/Test_2.scala b/tests/neg-macros/i7142b/Test_2.scala new file mode 100644 index 000000000000..06ccb017a659 --- /dev/null +++ b/tests/neg-macros/i7142b/Test_2.scala @@ -0,0 +1,4 @@ + +object Test { + macros.test // error +} diff --git a/tests/neg-macros/i7142c/Macro_1.scala b/tests/neg-macros/i7142c/Macro_1.scala new file mode 100644 index 000000000000..3b6638b9036d --- /dev/null +++ b/tests/neg-macros/i7142c/Macro_1.scala @@ -0,0 +1,9 @@ +package macros +import scala.quoted._ + +def oops(given QuoteContext) = { + var v = '{0}; + val q = '{ val x: Int = 8; ${ v = '{x}; v } } + v +} +inline def test = ${oops} diff --git a/tests/neg-macros/i7142c/Test_2.scala b/tests/neg-macros/i7142c/Test_2.scala new file mode 100644 index 000000000000..06ccb017a659 --- /dev/null +++ b/tests/neg-macros/i7142c/Test_2.scala @@ -0,0 +1,4 @@ + +object Test { + macros.test // error +} diff --git a/tests/neg-macros/i7142d/Macro_1.scala b/tests/neg-macros/i7142d/Macro_1.scala new file mode 100644 index 000000000000..3322ff93b514 --- /dev/null +++ b/tests/neg-macros/i7142d/Macro_1.scala @@ -0,0 +1,9 @@ +package macros +import scala.quoted._ + +def oops(given QuoteContext) = { + var v = '{0}; + val q = '{ ??? match { case x => ${ v = '{x}; v } } } + v +} +inline def test = ${oops} diff --git a/tests/neg-macros/i7142d/Test_2.scala b/tests/neg-macros/i7142d/Test_2.scala new file mode 100644 index 000000000000..06ccb017a659 --- /dev/null +++ b/tests/neg-macros/i7142d/Test_2.scala @@ -0,0 +1,4 @@ + +object Test { + macros.test // error +} diff --git a/tests/neg-macros/i7142e/Macro_1.scala b/tests/neg-macros/i7142e/Macro_1.scala new file mode 100644 index 000000000000..7ba07db645e8 --- /dev/null +++ b/tests/neg-macros/i7142e/Macro_1.scala @@ -0,0 +1,9 @@ +package macros +import scala.quoted._ + +def oops(given QuoteContext) = { + var v = '{0}; + val q = '{ def x: Int = 8; ${ v = '{x}; v } } + v +} +inline def test = ${oops} diff --git a/tests/neg-macros/i7142e/Test_2.scala b/tests/neg-macros/i7142e/Test_2.scala new file mode 100644 index 000000000000..06ccb017a659 --- /dev/null +++ b/tests/neg-macros/i7142e/Test_2.scala @@ -0,0 +1,4 @@ + +object Test { + macros.test // error +} diff --git a/tests/neg-macros/i7142f/Macro_1.scala b/tests/neg-macros/i7142f/Macro_1.scala new file mode 100644 index 000000000000..e1fdb0ed83a9 --- /dev/null +++ b/tests/neg-macros/i7142f/Macro_1.scala @@ -0,0 +1,9 @@ +package macros +import scala.quoted._ + +def oops(given QuoteContext) = { + var v = '{0}; + val q = '{ def f(x: Int): Int = ${ v = '{x}; v } } + v +} +inline def test = ${oops} diff --git a/tests/neg-macros/i7142f/Test_2.scala b/tests/neg-macros/i7142f/Test_2.scala new file mode 100644 index 000000000000..06ccb017a659 --- /dev/null +++ b/tests/neg-macros/i7142f/Test_2.scala @@ -0,0 +1,4 @@ + +object Test { + macros.test // error +} diff --git a/tests/neg-macros/i7142g/Macro_1.scala b/tests/neg-macros/i7142g/Macro_1.scala new file mode 100644 index 000000000000..ea6f966cf420 --- /dev/null +++ b/tests/neg-macros/i7142g/Macro_1.scala @@ -0,0 +1,9 @@ +package macros +import scala.quoted._ + +def oops(given QuoteContext) = { + var v = '{}; + val q = '{ var x: Int = 8; ${ v = '{x = 9}; v } } + v +} +inline def test = ${oops} diff --git a/tests/neg-macros/i7142g/Test_2.scala b/tests/neg-macros/i7142g/Test_2.scala new file mode 100644 index 000000000000..06ccb017a659 --- /dev/null +++ b/tests/neg-macros/i7142g/Test_2.scala @@ -0,0 +1,4 @@ + +object Test { + macros.test // error +} diff --git a/tests/run-macros/tasty-location.check b/tests/run-macros/tasty-location.check index adaee27a91a3..c066a966ebdb 100644 --- a/tests/run-macros/tasty-location.check +++ b/tests/run-macros/tasty-location.check @@ -1,6 +1,6 @@ -foo Location(List(Test$, loc1)) -foo Location(List(Test$, main)) -foo Location(List(Test$, main)) -foo Location(List(Test$, main, bar)) -foo Location(List(Test$, main, bar, baz)) -foo Location(List(Test$, main, f, $anonfun)) +foo Location(List(Test$, loc1, macro$1)) +foo Location(List(Test$, main, macro$2)) +foo Location(List(Test$, main, macro$3)) +foo Location(List(Test$, main, bar, macro$4)) +foo Location(List(Test$, main, bar, baz, macro$5)) +foo Location(List(Test$, main, f, $anonfun, macro$6)) diff --git a/tests/run-staging/i7142.scala b/tests/run-staging/i7142.scala new file mode 100644 index 000000000000..ba30d3e0832c --- /dev/null +++ b/tests/run-staging/i7142.scala @@ -0,0 +1,13 @@ +import scala.quoted._ +import scala.quoted.staging._ +import scala.util.control.NonLocalReturns._ + +object Test { + given Toolbox = Toolbox.make(getClass.getClassLoader) + def main(args: Array[String]): Unit = + try run {returning('{ { (x: Int) => ${ throwReturn('x) }} apply 0 })} + catch { + case ex: dotty.tools.dotc.reporting.diagnostic.messages.Error => + assert(ex.getMessage == "While expanding a macro, a reference to value x was used outside the scope where it was defined", ex.getMessage) + } +}