Skip to content

Commit 95e8f9d

Browse files
committed
Add add informative scope extrusion check
1 parent a4f3436 commit 95e8f9d

31 files changed

+229
-68
lines changed

compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@ object PickledQuotes {
4242
def quotedExprToTree[T](expr: quoted.Expr[T])(using Context): Tree = {
4343
val expr1 = expr.asInstanceOf[ExprImpl]
4444
expr1.checkScopeId(QuotesImpl.scopeId)
45+
ScopeException.checkInCorrectScope(expr1.scope, SpliceScope.getCurrent, expr1.tree, "Expr")
4546
changeOwnerOfTree(expr1.tree, ctx.owner)
4647
}
4748

4849
/** Transform the expression into its fully spliced TypeTree */
4950
def quotedTypeToTree(tpe: quoted.Type[?])(using Context): Tree = {
5051
val tpe1 = tpe.asInstanceOf[TypeImpl]
5152
tpe1.checkScopeId(QuotesImpl.scopeId)
53+
ScopeException.checkInCorrectScope(tpe1.scope, SpliceScope.getCurrent, tpe1.typeTree, "Expr")
5254
changeOwnerOfTree(tpe1.typeTree, ctx.owner)
5355
}
5456

@@ -73,23 +75,25 @@ object PickledQuotes {
7375
val evaluateHoles = new TreeMap {
7476
override def transform(tree: tpd.Tree)(using Context): tpd.Tree = tree match {
7577
case Hole(isTerm, idx, args) =>
76-
val reifiedArgs = args.map { arg =>
77-
if (arg.isTerm) (q: Quotes) ?=> new ExprImpl(arg, QuotesImpl.scopeId)
78-
else new TypeImpl(arg, QuotesImpl.scopeId)
78+
inContext(SpliceScope.contextWithNewSpliceScope(tree.sourcePos)) {
79+
val reifiedArgs = args.map { arg =>
80+
if (arg.isTerm) (q: Quotes) ?=> new ExprImpl(arg, QuotesImpl.scopeId, SpliceScope.getCurrent)
81+
else new TypeImpl(arg, QuotesImpl.scopeId, SpliceScope.getCurrent)
82+
}
83+
if isTerm then
84+
val quotedExpr = termHole(idx, reifiedArgs, QuotesImpl())
85+
val filled = PickledQuotes.quotedExprToTree(quotedExpr)
86+
87+
// We need to make sure a hole is created with the source file of the surrounding context, even if
88+
// it filled with contents a different source file.
89+
if filled.source == ctx.source then filled
90+
else filled.cloneIn(ctx.source).withSpan(tree.span)
91+
else
92+
// Replaces type holes generated by PickleQuotes (non-spliced types).
93+
// These are types defined in a quote and used at the same level in a nested quote.
94+
val quotedType = typeHole(idx, reifiedArgs)
95+
PickledQuotes.quotedTypeToTree(quotedType)
7996
}
80-
if isTerm then
81-
val quotedExpr = termHole(idx, reifiedArgs, QuotesImpl())
82-
val filled = PickledQuotes.quotedExprToTree(quotedExpr)
83-
84-
// We need to make sure a hole is created with the source file of the surrounding context, even if
85-
// it filled with contents a different source file.
86-
if filled.source == ctx.source then filled
87-
else filled.cloneIn(ctx.source).withSpan(tree.span)
88-
else
89-
// Replaces type holes generated by PickleQuotes (non-spliced types).
90-
// These are types defined in a quote and used at the same level in a nested quote.
91-
val quotedType = typeHole(idx, reifiedArgs)
92-
PickledQuotes.quotedTypeToTree(quotedType)
9397
case tree: Select =>
9498
// Retain selected members
9599
val qual = transform(tree.qualifier)

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,17 @@ object Splicer {
3939
*
4040
* See: `Staging`
4141
*/
42-
def splice(tree: Tree, pos: SrcPos, classLoader: ClassLoader)(using Context): Tree = tree match {
42+
def splice(tree: Tree, splicePos: SrcPos, spliceExpansionPos: SrcPos, classLoader: ClassLoader)(using Context): Tree = tree match {
4343
case Quoted(quotedTree) => quotedTree
4444
case _ =>
4545
val macroOwner = newSymbol(ctx.owner, nme.MACROkw, Macro | Synthetic, defn.AnyType, coord = tree.span)
4646
try
47-
inContext(ctx.withOwner(macroOwner)) {
47+
val sliceContext = SpliceScope.contextWithNewSpliceScope(splicePos.sourcePos).withOwner(macroOwner)
48+
inContext(sliceContext) {
4849
val oldContextClassLoader = Thread.currentThread().getContextClassLoader
4950
Thread.currentThread().setContextClassLoader(classLoader)
5051
try {
51-
val interpreter = new Interpreter(pos, classLoader)
52+
val interpreter = new Interpreter(spliceExpansionPos, classLoader)
5253

5354
// Some parts of the macro are evaluated during the unpickling performed in quotedExprToTree
5455
val interpretedExpr = interpreter.interpret[Quotes => scala.quoted.Expr[Any]](tree)
@@ -74,7 +75,7 @@ object Splicer {
7475
| Caused by ${ex.getClass}: ${if (ex.getMessage == null) "" else ex.getMessage}
7576
| ${ex.getStackTrace.takeWhile(_.getClassName != "dotty.tools.dotc.transform.Splicer$").drop(1).mkString("\n ")}
7677
""".stripMargin
77-
report.error(msg, pos)
78+
report.error(msg, spliceExpansionPos)
7879
ref(defn.Predef_undefined).withType(ErrorType(msg))
7980
}
8081
}
@@ -325,10 +326,10 @@ object Splicer {
325326
}
326327

327328
private def interpretQuote(tree: Tree)(implicit env: Env): Object =
328-
new ExprImpl(Inlined(EmptyTree, Nil, QuoteUtils.changeOwnerOfTree(tree, ctx.owner)).withSpan(tree.span), QuotesImpl.scopeId)
329+
new ExprImpl(Inlined(EmptyTree, Nil, QuoteUtils.changeOwnerOfTree(tree, ctx.owner)).withSpan(tree.span), QuotesImpl.scopeId, SpliceScope.getCurrent)
329330

330331
private def interpretTypeQuote(tree: Tree)(implicit env: Env): Object =
331-
new TypeImpl(QuoteUtils.changeOwnerOfTree(tree, ctx.owner), QuotesImpl.scopeId)
332+
new TypeImpl(QuoteUtils.changeOwnerOfTree(tree, ctx.owner), QuotesImpl.scopeId, SpliceScope.getCurrent)
332333

333334
private def interpretLiteral(value: Any)(implicit env: Env): Object =
334335
value.asInstanceOf[Object]

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,7 +1303,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
13031303
case res: Apply if res.symbol == defn.QuotedRuntime_exprSplice
13041304
&& level == 0
13051305
&& !hasInliningErrors =>
1306-
val expanded = expandMacro(res.args.head, tree.span)
1306+
val expanded = expandMacro(res.args.head, tree.srcPos)
13071307
typedExpr(expanded) // Inline calls and constant fold code generated by the macro
13081308
case res =>
13091309
inlineIfNeeded(res)
@@ -1488,7 +1488,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
14881488
}
14891489
}
14901490

1491-
private def expandMacro(body: Tree, span: Span)(using Context) = {
1491+
private def expandMacro(body: Tree, splicePos: SrcPos)(using Context) = {
14921492
assert(level == 0)
14931493
val inlinedFrom = enclosingInlineds.last
14941494
val dependencies = macroDependencies(body)
@@ -1503,7 +1503,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
15031503
ctx.compilationUnit.suspend() // this throws a SuspendException
15041504

15051505
val evaluatedSplice = inContext(quoted.MacroExpansion.context(inlinedFrom)) {
1506-
Splicer.splice(body, inlinedFrom.srcPos, MacroClassLoader.fromContext)
1506+
Splicer.splice(body, splicePos, inlinedFrom.srcPos, MacroClassLoader.fromContext)
15071507
}
15081508
val inlinedNormailizer = new TreeMap {
15091509
override def transform(tree: tpd.Tree)(using Context): tpd.Tree = tree match {
@@ -1513,7 +1513,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
15131513
}
15141514
val normalizedSplice = inlinedNormailizer.transform(evaluatedSplice)
15151515
if (normalizedSplice.isEmpty) normalizedSplice
1516-
else normalizedSplice.withSpan(span)
1516+
else normalizedSplice.withSpan(splicePos.span)
15171517
}
15181518

15191519
/** Return the set of symbols that are referred at level -1 by the tree and defined in the current run.

compiler/src/scala/quoted/runtime/impl/ExprImpl.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import dotty.tools.dotc.ast.tpd
1010
*
1111
* May contain references to code defined outside this Expr instance.
1212
*/
13-
final class ExprImpl(val tree: tpd.Tree, val scopeId: Int) extends Expr[Any] {
13+
final class ExprImpl(val tree: tpd.Tree, val scopeId: Int, val scope: Scope) extends Expr[Any] {
1414
override def equals(that: Any): Boolean = that match {
1515
case that: ExprImpl =>
1616
// Expr are wrappers around trees, therefore they are equals if their trees are equal.

compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
105105
case _ => false
106106
def asExpr: scala.quoted.Expr[Any] =
107107
if self.isExpr then
108-
new ExprImpl(self, QuotesImpl.this.hashCode)
108+
new ExprImpl(self, QuotesImpl.this.hashCode, SpliceScope.getCurrent)
109109
else self match
110110
case TermTypeTest(self) => throw new Exception("Expected an expression. This is a partially applied Term. Try eta-expanding the term first.")
111111
case _ => throw new Exception("Expected a Term but was: " + self)
@@ -372,11 +372,11 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
372372
given TermMethods: TermMethods with
373373
extension (self: Term)
374374
def seal: scala.quoted.Expr[Any] =
375-
if self.isExpr then new ExprImpl(self, QuotesImpl.this.hashCode)
375+
if self.isExpr then new ExprImpl(self, QuotesImpl.this.hashCode, SpliceScope.getCurrent)
376376
else throw new Exception("Cannot seal a partially applied Term. Try eta-expanding the term first.")
377377

378378
def sealOpt: Option[scala.quoted.Expr[Any]] =
379-
if self.isExpr then Some(new ExprImpl(self, QuotesImpl.this.hashCode))
379+
if self.isExpr then Some(new ExprImpl(self, QuotesImpl.this.hashCode, SpliceScope.getCurrent))
380380
else None
381381

382382
def tpe: TypeRepr = self.tpe
@@ -1666,7 +1666,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
16661666
def seal: scala.quoted.Type[_] = self.asType
16671667

16681668
def asType: scala.quoted.Type[?] =
1669-
new TypeImpl(Inferred(self), QuotesImpl.this.hashCode)
1669+
new TypeImpl(Inferred(self), QuotesImpl.this.hashCode, SpliceScope.getCurrent)
16701670

16711671
def =:=(that: TypeRepr): Boolean = self =:= that
16721672
def <:<(that: TypeRepr): Boolean = self <:< that
@@ -2877,11 +2877,11 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
28772877

28782878
def unpickleExpr[T](pickled: String | List[String], typeHole: (Int, Seq[Any]) => scala.quoted.Type[?], termHole: (Int, Seq[Any], scala.quoted.Quotes) => scala.quoted.Expr[?]): scala.quoted.Expr[T] =
28792879
val tree = PickledQuotes.unpickleTerm(pickled, typeHole, termHole)
2880-
new ExprImpl(tree, hash).asInstanceOf[scala.quoted.Expr[T]]
2880+
new ExprImpl(tree, hash, SpliceScope.getCurrent).asInstanceOf[scala.quoted.Expr[T]]
28812881

28822882
def unpickleType[T <: AnyKind](pickled: String | List[String], typeHole: (Int, Seq[Any]) => scala.quoted.Type[?], termHole: (Int, Seq[Any], scala.quoted.Quotes) => scala.quoted.Expr[?]): scala.quoted.Type[T] =
28832883
val tree = PickledQuotes.unpickleTypeTree(pickled, typeHole, termHole)
2884-
new TypeImpl(tree, hash).asInstanceOf[scala.quoted.Type[T]]
2884+
new TypeImpl(tree, hash, SpliceScope.getCurrent).asInstanceOf[scala.quoted.Type[T]]
28852885

28862886
object ExprMatch extends ExprMatchModule:
28872887
def unapply[TypeBindings <: Tuple, Tup <: Tuple](scrutinee: scala.quoted.Expr[Any])(using pattern: scala.quoted.Expr[Any]): Option[Tup] =
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,34 @@
11
package scala.quoted.runtime.impl
22

3+
import dotty.tools.dotc.ast.tpd.Tree
4+
import dotty.tools.dotc.core.Contexts._
5+
36
class ScopeException(msg: String) extends Exception(msg)
7+
8+
object ScopeException:
9+
def checkInCorrectScope(scope: Scope, currentScope: Scope, tree: Tree, kind: String)(using Context): Unit =
10+
val yCheck = ctx.settings.Ycheck.value(using ctx).exists(x => x == "all" || x == "macros")
11+
if yCheck && !scope.isOuterScopeOf(currentScope) then
12+
throw new ScopeException(
13+
if scope.atSameLocation(currentScope) then
14+
s"""Type created in a splice, extruded from that splice and then used in a subsequent evaluation of that same splice.
15+
|Splice: $scope
16+
|$kind: ${tree.show}
17+
|
18+
|
19+
|Splice stack:
20+
|${scope.stack.mkString("\t", "\n\t", "\n")}
21+
""".stripMargin
22+
else
23+
s"""Expression created in a splice was used outside of that splice.
24+
|Created in: $scope
25+
|Used in: $currentScope
26+
|$kind: ${tree.show}
27+
|
28+
|
29+
|Creation stack:
30+
|${scope.stack.mkString("\t", "\n\t", "\n")}
31+
|
32+
|Use stack:
33+
|${currentScope.stack.mkString("\t", "\n\t", "\n")}
34+
""".stripMargin)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package scala.quoted
2+
package runtime.impl
3+
4+
import dotty.tools.dotc.core.Contexts._
5+
import dotty.tools.dotc.util.Property
6+
import dotty.tools.dotc.util.SourcePosition
7+
8+
/** Unique identifier of the evaluation of a splice.
9+
*
10+
* A nested splice gets a new Scope with the outer one as an owner.
11+
* This also applies for recursive splices.
12+
*/
13+
trait Scope {
14+
/** Outer scope that was used to create the quote containing this splice.
15+
* NoScope if there is no scope
16+
*/
17+
def outer: Scope = NoScope
18+
/** Is this is a outer scope of the given scope */
19+
def isOuterScopeOf(scope: Scope): Boolean =
20+
this.eq(scope) || (scope.ne(NoScope) && isOuterScopeOf(scope.outer))
21+
/** Stack of locations where scopes where evaluated */
22+
def stack: List[String] =
23+
this.toString :: (if outer.eq(NoScope) then Nil else outer.stack)
24+
/** If the two scopes correspond to the same splice in source. */
25+
def atSameLocation(scope: Scope): Boolean = false
26+
}
27+
28+
/** Only used for outer scope of top level splice and staging `run` */
29+
object NoScope extends Scope:
30+
override def outer: Scope = throw UnsupportedOperationException("NoScope.outer")
31+
32+
class SpliceScope(val pos: SourcePosition, override val outer: Scope) extends Scope:
33+
34+
override def atSameLocation(scope: Scope): Boolean = scope match
35+
case scope: SpliceScope => this.pos == scope.pos
36+
case _ => false
37+
38+
override def toString =
39+
if pos.exists then
40+
s"${pos.source.toString}:${pos.startLine + 1} at column ${pos.startColumn + 1}"
41+
else
42+
"Unknown location"
43+
44+
end SpliceScope
45+
46+
47+
object SpliceScope:
48+
49+
/** A key to be used in a context property that tracks current splices we are evaluating */
50+
private val ScopeKey = new Property.Key[Scope]
51+
52+
def setSpliceScope(scope: Scope)(using Context): Context =
53+
ctx.fresh.setProperty(ScopeKey, scope)
54+
55+
def contextWithNewSpliceScope(pos: SourcePosition)(using Context): Context =
56+
ctx.fresh.setProperty(ScopeKey, new SpliceScope(pos, getCurrent))
57+
58+
/** Context with an incremented quotation level. */
59+
def getCurrent(using Context): Scope =
60+
ctx.property(ScopeKey).getOrElse(null)
61+
62+
end SpliceScope

compiler/src/scala/quoted/runtime/impl/TypeImpl.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ package runtime.impl
44
import dotty.tools.dotc.ast.tpd
55

66
/** Quoted type (or kind) `T` backed by a tree */
7-
final class TypeImpl(val typeTree: tpd.Tree, val scopeId: Int) extends Type[?] {
7+
final class TypeImpl(val typeTree: tpd.Tree, val scopeId: Int, val scope: Scope) extends Type[?] {
88
override def equals(that: Any): Boolean = that match {
99
case that: TypeImpl => typeTree ==
1010
// TastyTreeExpr are wrappers around trees, therfore they are equals if their trees are equal.

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ import dotty.tools.dotc.quoted.PickledQuotes
1818
import dotty.tools.dotc.transform.Splicer.checkEscapedVariables
1919
import dotty.tools.dotc.transform.{Inlining, PickleQuotes}
2020
import dotty.tools.dotc.util.Spans.Span
21-
import dotty.tools.dotc.util.SourceFile
21+
import dotty.tools.dotc.util.{SourceFile, NoSourcePosition}
2222
import dotty.tools.io.{Path, VirtualFile}
2323

24-
import scala.quoted.runtime.impl.QuotesImpl
24+
import scala.quoted.runtime.impl._
2525

2626
import scala.annotation.tailrec
2727
import scala.concurrent.Promise
@@ -49,6 +49,10 @@ private class QuoteCompiler extends Compiler:
4949

5050
def outputClassName: TypeName = "Generated$Code$From$Quoted".toTypeName
5151

52+
class RunScope extends Scope {
53+
override def toString: String = "scala.quoted.staging"
54+
}
55+
5256
/** Frontend that receives a scala.quoted.Expr or scala.quoted.Type as input */
5357
class QuotedFrontend extends Phase:
5458
import tpd._
@@ -58,7 +62,8 @@ private class QuoteCompiler extends Compiler:
5862
override def runOn(units: List[CompilationUnit])(implicit ctx: Context): List[CompilationUnit] =
5963
units.flatMap {
6064
case exprUnit: ExprCompilationUnit =>
61-
implicit val unitCtx: Context = ctx.fresh.setPhase(this.start).setCompilationUnit(exprUnit)
65+
val ctx1 = ctx.fresh.setPhase(this.start).setCompilationUnit(exprUnit)
66+
implicit val unitCtx: Context = SpliceScope.setSpliceScope(new RunScope)(using ctx1)
6267

6368
val pos = Span(0)
6469
val assocFile = new VirtualFile("<quote>")

tests/neg-macros/i8216/Macro_1.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package macros
2+
import scala.quoted._
3+
4+
var saved = Option.empty[Expr[Any]]
5+
6+
def oops(c: Expr[Any])(using Quotes) = {
7+
if saved.isEmpty then
8+
saved = Some(c)
9+
c
10+
else saved.get
11+
}
12+
inline def test(c: Any) = ${oops('c)}

tests/neg-macros/i8216/Test_2.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
object Test {
2+
class A(x: Int) {
3+
macros.test(x)
4+
}
5+
class B(y: String) {
6+
macros.test(y) // error
7+
}
8+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import quoted.*
2+
3+
inline def test1(): Int = ${ testExtrusion1 }
4+
private def testExtrusion1(using Quotes): Expr[Int] =
5+
var extruded: Expr[Int] = null
6+
'{ (x: Int) =>
7+
${
8+
extruded = '{x}
9+
extruded
10+
}
11+
}
12+
extruded
13+
14+
inline def test2(): Int = ${ testExtrusion2 }
15+
private def testExtrusion2(using Quotes): Expr[Int] =
16+
'{ 1 +
17+
${ var extruded: Expr[Int] = null; '{ (y: Int) => ${ extruded = '{y}; extruded } }; extruded }
18+
}
19+
20+
inline def test3(): Int = ${ testExtrusion3 }
21+
private def testExtrusion3(using Quotes): Expr[Int] = {
22+
var extruded: Expr[Int] = null
23+
for i <- 1 to 3 do
24+
'{ (x: Int) =>
25+
${
26+
if extruded == null then
27+
extruded = '{x}
28+
extruded
29+
}
30+
}
31+
extruded
32+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
object Test_2 {
3+
test1() // error
4+
test2() // error
5+
test3() // error
6+
}

0 commit comments

Comments
 (0)