Skip to content

Commit 66b8c5e

Browse files
committed
Add add informative scope extrusion check
1 parent 12f34c2 commit 66b8c5e

29 files changed

+235
-63
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+
expr1.checkInScope(SpliceScope.getCurrent)
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+
tpe1.checkInScope(SpliceScope.getCurrent)
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.contexWithNewSpliceScope(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.contexWithNewSpliceScope(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: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package scala.quoted
22
package runtime.impl
33

44
import dotty.tools.dotc.ast.tpd
5+
import dotty.tools.dotc.core.Contexts._
56

67
/** An Expr backed by a tree. Only the current compiler trees are allowed.
78
*
@@ -10,7 +11,7 @@ import dotty.tools.dotc.ast.tpd
1011
*
1112
* May contain references to code defined outside this Expr instance.
1213
*/
13-
final class ExprImpl(val tree: tpd.Tree, val scopeId: Int) extends Expr[Any] {
14+
final class ExprImpl(val tree: tpd.Tree, val scopeId: Int, val scope: Scope) extends Expr[Any] {
1415
override def equals(that: Any): Boolean = that match {
1516
case that: ExprImpl =>
1617
// Expr are wrappers around trees, therefore they are equals if their trees are equal.
@@ -23,6 +24,33 @@ final class ExprImpl(val tree: tpd.Tree, val scopeId: Int) extends Expr[Any] {
2324
if expectedScopeId != scopeId then
2425
throw new ScopeException("Cannot call `scala.quoted.staging.run(...)` within a macro or another `run(...)`")
2526

27+
def checkInScope(currentScope: Scope)(using Context) =
28+
val yCheck = ctx.settings.Ycheck.value(using ctx).exists(x => x == "all" || x == "macros")
29+
if yCheck && !currentScope.contains(scope) then
30+
throw new ScopeException(
31+
if scope.atSameLocation(currentScope) then
32+
s"""Expression created in a splice, extruded from that splice and then used in a subsequent evaluation of that same splice.
33+
|Splice: $scope
34+
|Expression: ${tree.show}
35+
|
36+
|
37+
|Splice stack:
38+
|${scope.stack.mkString("\t", "\n\t", "\n")}
39+
""".stripMargin
40+
else
41+
s"""Expression created in a splice was used outside of that splice.
42+
|Created in: $scope
43+
|Used in: $currentScope
44+
|Expression: ${tree.show}
45+
|
46+
|
47+
|Creation stack:
48+
|${scope.stack.mkString("\t", "\n\t", "\n")}
49+
|
50+
|Use stack:
51+
|${currentScope.stack.mkString("\t", "\n\t", "\n")}
52+
""".stripMargin)
53+
2654
override def hashCode: Int = tree.hashCode
2755
override def toString: String = "'{ ... }"
2856
}

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: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
trait Scope {
9+
def outer: Scope
10+
def contains(scope: Scope): Boolean =
11+
this.eq(scope) || (outer.ne(null) && this.outer.contains(scope))
12+
def stack: List[String] =
13+
this.toString :: (if outer.eq(null) then Nil else outer.stack)
14+
def atSameLocation(scope: Scope): Boolean
15+
}
16+
17+
class SpliceScope(val pos: SourcePosition, val outer: Scope) extends Scope:
18+
19+
def atSameLocation(scope: Scope): Boolean = scope match
20+
case scope: SpliceScope => this.pos == scope.pos
21+
case _ => false
22+
23+
override def toString =
24+
if pos.exists then
25+
s"${pos.source.toString}:${pos.startLine + 1} at column ${pos.startColumn + 1}"
26+
else
27+
"Unknown location"
28+
29+
end SpliceScope
30+
31+
32+
object SpliceScope:
33+
34+
/** A key to be used in a context property that tracks current splices we are evaluating */
35+
private val ScopeKey = new Property.Key[Scope]
36+
37+
def setSpliceScope(scope: Scope)(using Context): Context =
38+
ctx.fresh.setProperty(ScopeKey, scope)
39+
40+
def contexWithNewSpliceScope(pos: SourcePosition)(using Context): Context =
41+
ctx.fresh.setProperty(ScopeKey, new SpliceScope(pos, getCurrent))
42+
43+
/** Context with an incremented quotation level. */
44+
def getCurrent(using Context): Scope =
45+
ctx.property(ScopeKey).getOrElse(null)
46+
47+
end SpliceScope

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ package scala.quoted
22
package runtime.impl
33

44
import dotty.tools.dotc.ast.tpd
5+
import dotty.tools.dotc.core.Contexts._
56

67
/** Quoted type (or kind) `T` backed by a tree */
7-
final class TypeImpl(val typeTree: tpd.Tree, val scopeId: Int) extends Type[?] {
8+
final class TypeImpl(val typeTree: tpd.Tree, val scopeId: Int, val scope: Scope) extends Type[?] {
89
override def equals(that: Any): Boolean = that match {
910
case that: TypeImpl => typeTree ==
1011
// TastyTreeExpr are wrappers around trees, therfore they are equals if their trees are equal.
@@ -22,6 +23,32 @@ final class TypeImpl(val typeTree: tpd.Tree, val scopeId: Int) extends Type[?] {
2223
if expectedScopeId != scopeId then
2324
throw new ScopeException("Cannot call `scala.quoted.staging.run(...)` within a macro or another `run(...)`")
2425

26+
def checkInScope(currentScope: Scope)(using Context) =
27+
if !currentScope.contains(scope) then
28+
throw new ScopeException(
29+
if scope.atSameLocation(currentScope) then
30+
s"""Type created in a splice, extruded from that splice and then used in a subsequent evaluation of that same splice.
31+
|Splice: $scope
32+
|Type: ${typeTree.show}
33+
|
34+
|
35+
|Splice stack:
36+
|${scope.stack.mkString("\t", "\n\t", "\n")}
37+
""".stripMargin
38+
else
39+
s"""Expression created in a splice was used outside of that splice.
40+
|Created in: $scope
41+
|Used in: $currentScope
42+
|Type: ${typeTree.show}
43+
|
44+
|
45+
|Creation stack:
46+
|${scope.stack.mkString("\t", "\n\t", "\n")}
47+
|
48+
|Use stack:
49+
|${currentScope.stack.mkString("\t", "\n\t", "\n")}
50+
""".stripMargin)
51+
2552
override def hashCode: Int = typeTree.hashCode
2653
override def toString: String = "Type.of[...]"
2754
}

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

Lines changed: 10 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,12 @@ private class QuoteCompiler extends Compiler:
4949

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

52+
class RunScope extends Scope {
53+
def outer: Scope = null
54+
def atSameLocation(scope: Scope): Boolean = false
55+
override def toString: String = "scala.quoted.staging"
56+
}
57+
5258
/** Frontend that receives a scala.quoted.Expr or scala.quoted.Type as input */
5359
class QuotedFrontend extends Phase:
5460
import tpd._
@@ -58,7 +64,8 @@ private class QuoteCompiler extends Compiler:
5864
override def runOn(units: List[CompilationUnit])(implicit ctx: Context): List[CompilationUnit] =
5965
units.flatMap {
6066
case exprUnit: ExprCompilationUnit =>
61-
implicit val unitCtx: Context = ctx.fresh.setPhase(this.start).setCompilationUnit(exprUnit)
67+
val ctx1 = ctx.fresh.setPhase(this.start).setCompilationUnit(exprUnit)
68+
implicit val unitCtx: Context = SpliceScope.setSpliceScope(new RunScope)(using ctx1)
6269

6370
val pos = Span(0)
6471
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+
}

0 commit comments

Comments
 (0)