Skip to content

Commit 6e0643e

Browse files
committed
Add add informative scope extrusion check
1 parent 83cb8ff commit 6e0643e

File tree

12 files changed

+178
-46
lines changed

12 files changed

+178
-46
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.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
@@ -1302,7 +1302,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
13021302
case res: Apply if res.symbol == defn.QuotedRuntime_exprSplice
13031303
&& level == 0
13041304
&& !hasInliningErrors =>
1305-
val expanded = expandMacro(res.args.head, tree.span)
1305+
val expanded = expandMacro(res.args.head, tree.srcPos)
13061306
typedExpr(expanded) // Inline calls and constant fold code generated by the macro
13071307
case res =>
13081308
inlineIfNeeded(res)
@@ -1487,7 +1487,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
14871487
}
14881488
}
14891489

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

15041504
val evaluatedSplice = inContext(quoted.MacroExpansion.context(inlinedFrom)) {
1505-
Splicer.splice(body, inlinedFrom.srcPos, MacroClassLoader.fromContext)
1505+
Splicer.splice(body, splicePos, inlinedFrom.srcPos, MacroClassLoader.fromContext)
15061506
}
15071507
val inlinedNormailizer = new TreeMap {
15081508
override def transform(tree: tpd.Tree)(using Context): tpd.Tree = tree match {
@@ -1512,7 +1512,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
15121512
}
15131513
val normalizedSplice = inlinedNormailizer.transform(evaluatedSplice)
15141514
if (normalizedSplice.isEmpty) normalizedSplice
1515-
else normalizedSplice.withSpan(span)
1515+
else normalizedSplice.withSpan(splicePos.span)
15161516
}
15171517

15181518
/** 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: 28 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: SpliceScope) 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,32 @@ 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: SpliceScope)(using Context) =
28+
if !currentScope.contains(scope) then
29+
throw new ScopeException(
30+
if scope.pos == currentScope.pos then
31+
s"""Expression created in a splice, extruded from that splice and then used in a subsequent evaluation of that same splice.
32+
|Splice: $scope
33+
|Expression: ${tree.show}
34+
|
35+
|
36+
|Splice stack:
37+
|${scope.stack.mkString("\t", "\n\t", "\n")}
38+
""".stripMargin
39+
else
40+
s"""Expression created in a splice was used outside of that splice.
41+
|Created in: $scope
42+
|Used in: $currentScope
43+
|Expression: ${tree.show}
44+
|
45+
|
46+
|Creation stack:
47+
|${scope.stack.mkString("\t", "\n\t", "\n")}
48+
|
49+
|Use stack:
50+
|${currentScope.stack.mkString("\t", "\n\t", "\n")}
51+
""".stripMargin)
52+
2653
override def hashCode: Int = tree.hashCode
2754
override def toString: String = "'{ ... }"
2855
}

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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
final class SpliceScope(val pos: SourcePosition, outer: SpliceScope):
9+
def contains(scope: SpliceScope): Boolean =
10+
this.eq(scope) || (outer.ne(null) && this.outer.contains(scope))
11+
12+
13+
def stack: List[String] = this.toString :: (if outer.eq(null) then Nil else outer.stack)
14+
15+
override def toString = s"${pos.source.toString}:${pos.startLine + 1} at column ${pos.startColumn + 1}"
16+
17+
end SpliceScope
18+
19+
20+
object SpliceScope:
21+
22+
/** A key to be used in a context property that tracks current splices we are evaluating */
23+
private val SpliceScopeKey = new Property.Key[SpliceScope]
24+
25+
// def contexWithNewSpliceScope(pos: SourcePosition)(using Context): Int =
26+
// ctx.fresh.setProperty(SpliceScopeKey, new SpliceScope(pos))
27+
28+
def contexWithNewSpliceScope(pos: SourcePosition)(using Context): Context =
29+
ctx.fresh.setProperty(SpliceScopeKey, new SpliceScope(pos, getCurrent))
30+
31+
/** Context with an incremented quotation level. */
32+
def getCurrent(using Context): SpliceScope =
33+
ctx.property(SpliceScopeKey).getOrElse(null)
34+
35+
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: SpliceScope) 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: SpliceScope)(using Context) =
27+
if !currentScope.contains(scope) then
28+
throw new ScopeException(
29+
if scope.pos == currentScope.pos 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
}
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+
}

tests/run-macros/i8007/Macro_3.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ object Eq {
4040
ev match {
4141
case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} =>
4242
val elemInstances = summonAll[elementTypes]
43-
val eqProductBody: (Expr[T], Expr[T]) => Expr[Boolean] = (x, y) => {
43+
def eqProductBody(x: Expr[T], y: Expr[T])(using Quotes): Expr[Boolean] = {
4444
elemInstances.zipWithIndex.foldLeft(Expr(true: Boolean)) {
4545
case (acc, (elem, index)) =>
4646
val e1 = '{$x.asInstanceOf[Product].productElement(${Expr(index)})}
@@ -55,7 +55,7 @@ object Eq {
5555

5656
case '{ $m: Mirror.SumOf[T] { type MirroredElemTypes = elementTypes }} =>
5757
val elemInstances = summonAll[elementTypes]
58-
val eqSumBody: (Expr[T], Expr[T]) => Expr[Boolean] = (x, y) => {
58+
def eqSumBody(x: Expr[T], y: Expr[T])(using Quotes): Expr[Boolean] = {
5959
val ordx = '{ $m.ordinal($x) }
6060
val ordy = '{ $m.ordinal($y) }
6161

0 commit comments

Comments
 (0)