Skip to content

Commit 42cd971

Browse files
committed
Fix #7142: Detect scope extrusions in macros and run
1 parent f1091e8 commit 42cd971

File tree

22 files changed

+172
-17
lines changed

22 files changed

+172
-17
lines changed

compiler/src/dotty/tools/dotc/core/StdNames.scala

-1
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,6 @@ object StdNames {
500500
val lang: N = "lang"
501501
val length: N = "length"
502502
val lengthCompare: N = "lengthCompare"
503-
val `macro` : N = "macro"
504503
val macroThis : N = "_this"
505504
val macroContext : N = "c"
506505
val main: N = "main"

compiler/src/dotty/tools/dotc/transform/Splicer.scala

+49-2
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,17 @@ object Splicer {
3737
*
3838
* See: `Staging`
3939
*/
40-
def splice(tree: Tree, pos: SourcePosition, classLoader: ClassLoader)(implicit ctx: Context): Tree = tree match {
40+
def splice(tree: Tree, pos: SourcePosition, classLoader: ClassLoader)(given ctx: Context): Tree = tree match {
4141
case Quoted(quotedTree) => quotedTree
4242
case _ =>
4343
val interpreter = new Interpreter(pos, classLoader)
44+
val macroOwner = ctx.newSymbol(ctx.owner, NameKinds.UniqueName.fresh(nme.MACROkw), Synthetic, defn.AnyType, coord = tree.span)
4445
try {
46+
given Context = ctx.withOwner(macroOwner)
4547
// Some parts of the macro are evaluated during the unpickling performed in quotedExprToTree
4648
val interpretedExpr = interpreter.interpret[scala.quoted.QuoteContext => scala.quoted.Expr[Any]](tree)
47-
interpretedExpr.fold(tree)(macroClosure => PickledQuotes.quotedExprToTree(macroClosure(QuoteContext())))
49+
val interpretedTree = interpretedExpr.fold(tree)(macroClosure => PickledQuotes.quotedExprToTree(macroClosure(QuoteContext())))
50+
checkEscapedVariables(interpretedTree, macroOwner).changeOwner(macroOwner, ctx.owner)
4851
}
4952
catch {
5053
case ex: CompilationUnit.SuspendException =>
@@ -63,6 +66,50 @@ object Splicer {
6366
}
6467
}
6568

69+
/** Checks that no symbol that whas generated within the macro expansion has an out of scope reference */
70+
def checkEscapedVariables(tree: Tree, expansionOwner: Symbol)(given ctx: Context): tree.type =
71+
new TreeTraverser {
72+
private[this] var locals = Set.empty[Symbol]
73+
private def markDef(tree: Tree)(implicit ctx: Context): Unit = tree match {
74+
case tree: DefTree =>
75+
val sym = tree.symbol
76+
if (!locals.contains(sym))
77+
locals = locals + sym
78+
case _ =>
79+
}
80+
def traverse(tree: Tree)(given ctx: Context): Unit =
81+
def traverseOver(lastEntered: Set[Symbol]) =
82+
try traverseChildren(tree)
83+
finally locals = lastEntered
84+
tree match
85+
case tree: Ident if isEscapedVariable(tree.symbol) =>
86+
val sym = tree.symbol
87+
ctx.error(em"While expanding a macro, a reference to $sym was used outside the scope where it was defined", tree.sourcePos)
88+
case Block(stats, _) =>
89+
val last = locals
90+
stats.foreach(markDef)
91+
traverseOver(last)
92+
case CaseDef(pat, guard, body) =>
93+
val last = locals
94+
// mark all bindings
95+
new TreeTraverser {
96+
def traverse(tree: Tree)(implicit ctx: Context): Unit = {
97+
markDef(tree)
98+
traverseChildren(tree)
99+
}
100+
}.traverse(pat)
101+
traverseOver(last)
102+
case _ =>
103+
markDef(tree)
104+
traverseChildren(tree)
105+
private def isEscapedVariable(sym: Symbol)(given ctx: Context): Boolean =
106+
sym.exists && !sym.is(Package)
107+
&& sym.owner.ownersIterator.contains(expansionOwner) // symbol was generated within the macro expansion
108+
&& !locals.contains(sym) // symbol is not in current scope
109+
}.traverse(tree)
110+
tree
111+
112+
66113
/** Check that the Tree can be spliced. `${'{xyz}}` becomes `xyz`
67114
* and for `$xyz` the tree of `xyz` is interpreted for which the
68115
* resulting expression is returned as a `Tree`

compiler/src/dotty/tools/dotc/typer/Inliner.scala

+4-3
Original file line numberDiff line numberDiff line change
@@ -1273,7 +1273,6 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(implicit ctx: Context) {
12731273
private def expandMacro(body: Tree, span: Span)(implicit ctx: Context) = {
12741274
assert(level == 0)
12751275
val inlinedFrom = enclosingInlineds.last
1276-
val ctx1 = tastyreflect.MacroExpansion.context(inlinedFrom)
12771276
val dependencies = macroDependencies(body)
12781277

12791278
if dependencies.nonEmpty && !ctx.reporter.errorsReported then
@@ -1284,8 +1283,10 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(implicit ctx: Context) {
12841283
ctx.echo(i"suspension triggered by macro call to ${sym.showLocated} in ${sym.associatedFile}", call.sourcePos)
12851284
ctx.compilationUnit.suspend() // this throws a SuspendException
12861285

1287-
val evaluatedSplice = Splicer.splice(body, inlinedFrom.sourcePos, MacroClassLoader.fromContext)(ctx1)
1288-
1286+
val evaluatedSplice = {
1287+
given Context = tastyreflect.MacroExpansion.context(inlinedFrom)(given ctx)
1288+
Splicer.splice(body, inlinedFrom.sourcePos, MacroClassLoader.fromContext)
1289+
}
12891290
val inlinedNormailizer = new TreeMap {
12901291
override def transform(tree: tpd.Tree)(implicit ctx: Context): tpd.Tree = tree match {
12911292
case Inlined(EmptyTree, Nil, expr) if enclosingInlineds.isEmpty => transform(expr)

staging/src/scala/quoted/staging/QuoteCompiler.scala

+7-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import dotty.tools.dotc.core.Symbols.defn
1616
import dotty.tools.dotc.core.Types.ExprType
1717
import dotty.tools.dotc.core.quoted.PickledQuotes
1818
import dotty.tools.dotc.tastyreflect.ReflectionImpl
19+
import dotty.tools.dotc.transform.Splicer.checkEscapedVariables
1920
import dotty.tools.dotc.transform.ReifyQuotes
2021
import dotty.tools.dotc.util.Spans.Span
2122
import dotty.tools.dotc.util.SourceFile
@@ -67,8 +68,12 @@ private class QuoteCompiler extends Compiler {
6768
cls.enter(unitCtx.newDefaultConstructor(cls), EmptyScope)
6869
val meth = unitCtx.newSymbol(cls, nme.apply, Method, ExprType(defn.AnyType), coord = pos).entered
6970

70-
val qctx = dotty.tools.dotc.quoted.QuoteContext()
71-
val quoted = PickledQuotes.quotedExprToTree(exprUnit.exprBuilder.apply(qctx))(unitCtx.withOwner(meth))
71+
val quoted = {
72+
given Context = unitCtx.withOwner(meth)
73+
val qctx = dotty.tools.dotc.quoted.QuoteContext()
74+
val quoted = PickledQuotes.quotedExprToTree(exprUnit.exprBuilder.apply(qctx))
75+
checkEscapedVariables(quoted, meth)
76+
}
7277

7378
getLiteral(quoted) match {
7479
case Some(value) =>

tests/neg-macros/i7142/Macro_1.scala

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package macros
2+
import scala.quoted._
3+
4+
def oops(given QuoteContext) = {
5+
var v = '{0};
6+
val q = '{ (x: Int) => ${ v = '{x}; v } }
7+
'{$q($v)}
8+
}
9+
inline def test = ${oops}

tests/neg-macros/i7142/Test_2.scala

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
object Test {
3+
macros.test // error
4+
}

tests/neg-macros/i7142b/Macro_1.scala

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package macros
2+
import scala.quoted._
3+
import scala.util.control.NonLocalReturns._
4+
5+
def oops(given QuoteContext): Expr[Int] =
6+
returning('{ { (x: Int) => ${ throwReturn('x) }} apply 0 })
7+
8+
inline def test = ${oops}

tests/neg-macros/i7142b/Test_2.scala

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
object Test {
3+
macros.test // error
4+
}

tests/neg-macros/i7142c/Macro_1.scala

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package macros
2+
import scala.quoted._
3+
4+
def oops(given QuoteContext) = {
5+
var v = '{0};
6+
val q = '{ val x: Int = 8; ${ v = '{x}; v } }
7+
v
8+
}
9+
inline def test = ${oops}

tests/neg-macros/i7142c/Test_2.scala

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
object Test {
3+
macros.test // error
4+
}

tests/neg-macros/i7142d/Macro_1.scala

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package macros
2+
import scala.quoted._
3+
4+
def oops(given QuoteContext) = {
5+
var v = '{0};
6+
val q = '{ ??? match { case x => ${ v = '{x}; v } } }
7+
v
8+
}
9+
inline def test = ${oops}

tests/neg-macros/i7142d/Test_2.scala

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
object Test {
3+
macros.test // error
4+
}

tests/neg-macros/i7142e/Macro_1.scala

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package macros
2+
import scala.quoted._
3+
4+
def oops(given QuoteContext) = {
5+
var v = '{0};
6+
val q = '{ def x: Int = 8; ${ v = '{x}; v } }
7+
v
8+
}
9+
inline def test = ${oops}

tests/neg-macros/i7142e/Test_2.scala

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
object Test {
3+
macros.test // error
4+
}

tests/neg-macros/i7142f/Macro_1.scala

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package macros
2+
import scala.quoted._
3+
4+
def oops(given QuoteContext) = {
5+
var v = '{0};
6+
val q = '{ def f(x: Int): Int = ${ v = '{x}; v } }
7+
v
8+
}
9+
inline def test = ${oops}

tests/neg-macros/i7142f/Test_2.scala

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
object Test {
3+
macros.test // error
4+
}

tests/neg-macros/i7142g/Macro_1.scala

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package macros
2+
import scala.quoted._
3+
4+
def oops(given QuoteContext) = {
5+
var v = '{};
6+
val q = '{ var x: Int = 8; ${ v = '{x = 9}; v } }
7+
v
8+
}
9+
inline def test = ${oops}

tests/neg-macros/i7142g/Test_2.scala

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
object Test {
3+
macros.test // error
4+
}

tests/run-custom-args/Yretain-trees/tasty-extractors-owners.check

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
foo
2-
DefDef("main", Nil, List(List(ValDef("args", Inferred(), None))), Inferred(), None)
2+
ValDef("macro$1", Inferred(), None)
33

44
bar
55
DefDef("foo", Nil, Nil, Inferred(), None)
@@ -8,7 +8,7 @@ bar2
88
DefDef("foo", Nil, Nil, Inferred(), None)
99

1010
foo2
11-
DefDef("main", Nil, List(List(ValDef("args", Inferred(), None))), Inferred(), None)
11+
ValDef("macro$1", Inferred(), None)
1212

1313
baz
1414
ValDef("foo2", Inferred(), None)

tests/run-macros/tasty-location.check

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
foo Location(List(Test$, loc1))
2-
foo Location(List(Test$, main))
3-
foo Location(List(Test$, main))
4-
foo Location(List(Test$, main, bar))
5-
foo Location(List(Test$, main, bar, baz))
6-
foo Location(List(Test$, main, f, $anonfun))
1+
foo Location(List(Test$, loc1, macro$1))
2+
foo Location(List(Test$, main, macro$2))
3+
foo Location(List(Test$, main, macro$3))
4+
foo Location(List(Test$, main, bar, macro$4))
5+
foo Location(List(Test$, main, bar, baz, macro$5))
6+
foo Location(List(Test$, main, f, $anonfun, macro$6))

tests/run-staging/i7142.scala

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import scala.quoted._
2+
import scala.quoted.staging._
3+
import scala.util.control.NonLocalReturns._
4+
5+
object Test {
6+
given Toolbox = Toolbox.make(getClass.getClassLoader)
7+
def main(args: Array[String]): Unit =
8+
try run {returning('{ { (x: Int) => ${ throwReturn('x) }} apply 0 })}
9+
catch {
10+
case ex: dotty.tools.dotc.reporting.diagnostic.messages.Error =>
11+
assert(ex.getMessage == "While expanding a macro, a reference to value x was used outside the scope where it was defined", ex.getMessage)
12+
}
13+
}

0 commit comments

Comments
 (0)