Skip to content

Commit 9632f6b

Browse files
Merge pull request #11442 from dotty-staging/add-stonger-scope-extrusion-check
Add add informative scope extrusion check
2 parents 0d6c926 + 7e2e6d3 commit 9632f6b

31 files changed

+242
-104
lines changed

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

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,14 @@ object PickledQuotes {
4141
/** Transform the expression into its fully spliced Tree */
4242
def quotedExprToTree[T](expr: quoted.Expr[T])(using Context): Tree = {
4343
val expr1 = expr.asInstanceOf[ExprImpl]
44-
expr1.checkScopeId(QuotesImpl.scopeId)
44+
ScopeException.checkInCorrectScope(expr1.scope, SpliceScope.getCurrent, expr1.tree, "Expr")
4545
changeOwnerOfTree(expr1.tree, ctx.owner)
4646
}
4747

4848
/** Transform the expression into its fully spliced TypeTree */
4949
def quotedTypeToTree(tpe: quoted.Type[?])(using Context): Tree = {
5050
val tpe1 = tpe.asInstanceOf[TypeImpl]
51-
tpe1.checkScopeId(QuotesImpl.scopeId)
51+
ScopeException.checkInCorrectScope(tpe1.scope, SpliceScope.getCurrent, tpe1.typeTree, "Type")
5252
changeOwnerOfTree(tpe1.typeTree, ctx.owner)
5353
}
5454

@@ -73,23 +73,25 @@ object PickledQuotes {
7373
val evaluateHoles = new TreeMap {
7474
override def transform(tree: tpd.Tree)(using Context): tpd.Tree = tree match {
7575
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)
76+
inContext(SpliceScope.contextWithNewSpliceScope(tree.sourcePos)) {
77+
val reifiedArgs = args.map { arg =>
78+
if (arg.isTerm) (q: Quotes) ?=> new ExprImpl(arg, SpliceScope.getCurrent)
79+
else new TypeImpl(arg, SpliceScope.getCurrent)
80+
}
81+
if isTerm then
82+
val quotedExpr = termHole(idx, reifiedArgs, QuotesImpl())
83+
val filled = PickledQuotes.quotedExprToTree(quotedExpr)
84+
85+
// We need to make sure a hole is created with the source file of the surrounding context, even if
86+
// it filled with contents a different source file.
87+
if filled.source == ctx.source then filled
88+
else filled.cloneIn(ctx.source).withSpan(tree.span)
89+
else
90+
// Replaces type holes generated by PickleQuotes (non-spliced types).
91+
// These are types defined in a quote and used at the same level in a nested quote.
92+
val quotedType = typeHole(idx, reifiedArgs)
93+
PickledQuotes.quotedTypeToTree(quotedType)
7994
}
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)
9395
case tree: Select =>
9496
// Retain selected members
9597
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), 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), 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
@@ -1304,7 +1304,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
13041304
case res: Apply if res.symbol == defn.QuotedRuntime_exprSplice
13051305
&& level == 0
13061306
&& !hasInliningErrors =>
1307-
val expanded = expandMacro(res.args.head, tree.span)
1307+
val expanded = expandMacro(res.args.head, tree.srcPos)
13081308
typedExpr(expanded) // Inline calls and constant fold code generated by the macro
13091309
case res =>
13101310
inlineIfNeeded(res)
@@ -1489,7 +1489,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
14891489
}
14901490
}
14911491

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

15061506
val evaluatedSplice = inContext(quoted.MacroExpansion.context(inlinedFrom)) {
1507-
Splicer.splice(body, inlinedFrom.srcPos, MacroClassLoader.fromContext)
1507+
Splicer.splice(body, splicePos, inlinedFrom.srcPos, MacroClassLoader.fromContext)
15081508
}
15091509
val inlinedNormailizer = new TreeMap {
15101510
override def transform(tree: tpd.Tree)(using Context): tpd.Tree = tree match {
@@ -1514,7 +1514,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
15141514
}
15151515
val normalizedSplice = inlinedNormailizer.transform(evaluatedSplice)
15161516
if (normalizedSplice.isEmpty) normalizedSplice
1517-
else normalizedSplice.withSpan(span)
1517+
else normalizedSplice.withSpan(splicePos.span)
15181518
}
15191519

15201520
/** 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: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,14 @@ 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 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.
17-
// All scopeId should be equal unless two different runs of the compiler created the trees.
18-
tree == that.tree && scopeId == that.scopeId
17+
// All scope should be equal unless two different runs of the compiler created the trees.
18+
tree == that.tree && scope == that.scope
1919
case _ => false
2020
}
2121

22-
def checkScopeId(expectedScopeId: Int): Unit =
23-
if expectedScopeId != scopeId then
24-
throw new ScopeException("Cannot call `scala.quoted.staging.run(...)` within a macro or another `run(...)`")
25-
26-
override def hashCode: Int = tree.hashCode
2722
override def toString: String = "'{ ... }"
2823
}

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

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ import scala.reflect.TypeTest
2323

2424
object QuotesImpl {
2525

26-
type ScopeId = Int
27-
2826
def apply()(using Context): Quotes =
2927
new QuotesImpl
3028

@@ -34,11 +32,6 @@ object QuotesImpl {
3432
if ctx.settings.color.value == "always" then TreeAnsiCode.show(tree)
3533
else TreeCode.show(tree)
3634

37-
// TODO Explore more fine grained scope ids.
38-
// This id can only differentiate scope extrusion from one compiler instance to another.
39-
def scopeId(using Context): ScopeId =
40-
ctx.outersIterator.toList.last.hashCode()
41-
4235
}
4336

4437
class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler, QuoteMatching:
@@ -81,10 +74,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
8174
end CompilationInfo
8275

8376
extension (expr: Expr[Any])
84-
def asTerm: Term =
85-
val exprImpl = expr.asInstanceOf[ExprImpl]
86-
exprImpl.checkScopeId(QuotesImpl.this.hashCode)
87-
exprImpl.tree
77+
def asTerm: Term = expr.asInstanceOf[ExprImpl].tree
8878
end extension
8979

9080
type Tree = tpd.Tree
@@ -105,7 +95,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
10595
case _ => false
10696
def asExpr: scala.quoted.Expr[Any] =
10797
if self.isExpr then
108-
new ExprImpl(self, QuotesImpl.this.hashCode)
98+
new ExprImpl(self, SpliceScope.getCurrent)
10999
else self match
110100
case TermTypeTest(self) => throw new Exception("Expected an expression. This is a partially applied Term. Try eta-expanding the term first.")
111101
case _ => throw new Exception("Expected a Term but was: " + self)
@@ -372,11 +362,11 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
372362
given TermMethods: TermMethods with
373363
extension (self: Term)
374364
def seal: scala.quoted.Expr[Any] =
375-
if self.isExpr then new ExprImpl(self, QuotesImpl.this.hashCode)
365+
if self.isExpr then new ExprImpl(self, SpliceScope.getCurrent)
376366
else throw new Exception("Cannot seal a partially applied Term. Try eta-expanding the term first.")
377367

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

382372
def tpe: TypeRepr = self.tpe
@@ -1670,7 +1660,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
16701660
def seal: scala.quoted.Type[_] = self.asType
16711661

16721662
def asType: scala.quoted.Type[?] =
1673-
new TypeImpl(Inferred(self), QuotesImpl.this.hashCode)
1663+
new TypeImpl(Inferred(self), SpliceScope.getCurrent)
16741664

16751665
def =:=(that: TypeRepr): Boolean = self =:= that
16761666
def <:<(that: TypeRepr): Boolean = self <:< that
@@ -2894,11 +2884,11 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
28942884

28952885
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] =
28962886
val tree = PickledQuotes.unpickleTerm(pickled, typeHole, termHole)
2897-
new ExprImpl(tree, hash).asInstanceOf[scala.quoted.Expr[T]]
2887+
new ExprImpl(tree, SpliceScope.getCurrent).asInstanceOf[scala.quoted.Expr[T]]
28982888

28992889
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] =
29002890
val tree = PickledQuotes.unpickleTypeTree(pickled, typeHole, termHole)
2901-
new TypeImpl(tree, hash).asInstanceOf[scala.quoted.Type[T]]
2891+
new TypeImpl(tree, SpliceScope.getCurrent).asInstanceOf[scala.quoted.Type[T]]
29022892

29032893
object ExprMatch extends ExprMatchModule:
29042894
def unapply[TypeBindings <: Tuple, Tup <: Tuple](scrutinee: scala.quoted.Expr[Any])(using pattern: scala.quoted.Expr[Any]): Option[Tup] =
@@ -2965,7 +2955,4 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler
29652955
}
29662956
}
29672957

2968-
private[this] val hash = QuotesImpl.scopeId(using ctx)
2969-
override def hashCode: Int = hash
2970-
29712958
end QuotesImpl
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,37 @@
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+
if scope.root != currentScope.root then
11+
throw new ScopeException(s"Cannot use $kind oustide of the macro splice `$${...}` or the scala.quoted.staging.run(...)` where it was defined")
12+
13+
val yCheck = ctx.settings.Ycheck.value(using ctx).exists(x => x == "all" || x == "macros")
14+
if yCheck && !scope.isOuterScopeOf(currentScope) then
15+
throw new ScopeException(
16+
if scope.atSameLocation(currentScope) then
17+
s"""Type created in a splice, extruded from that splice and then used in a subsequent evaluation of that same splice.
18+
|Splice: $scope
19+
|$kind: ${tree.show}
20+
|
21+
|
22+
|Splice stack:
23+
|${scope.stack.mkString("\t", "\n\t", "\n")}
24+
""".stripMargin
25+
else
26+
s"""Expression created in a splice was used outside of that splice.
27+
|Created in: $scope
28+
|Used in: $currentScope
29+
|$kind: ${tree.show}
30+
|
31+
|
32+
|Creation stack:
33+
|${scope.stack.mkString("\t", "\n\t", "\n")}
34+
|
35+
|Use stack:
36+
|${currentScope.stack.mkString("\t", "\n\t", "\n")}
37+
""".stripMargin)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
/** A scope uniquely identifies the context for evaluating a splice
9+
*
10+
* A nested splice gets a new scope with the enclosing scope as its `outer`.
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 otherwise.
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+
/** Scope of the top level splice or staging `run` */
22+
def root: Scope =
23+
if outer.eq(NoScope) then this else outer.root
24+
/** Stack of locations where scopes where evaluated */
25+
def stack: List[String] =
26+
this.toString :: (if outer.eq(NoScope) then Nil else outer.stack)
27+
/** If the two scopes correspond to the same splice in source. */
28+
def atSameLocation(scope: Scope): Boolean = false
29+
}
30+
31+
/** Only used for outer scope of top level splice and staging `run` */
32+
object NoScope extends Scope:
33+
override def root: Scope = this
34+
override def outer: Scope = throw UnsupportedOperationException("NoScope.outer")
35+
36+
class SpliceScope(val pos: SourcePosition, override val outer: Scope) extends Scope:
37+
38+
override def atSameLocation(scope: Scope): Boolean = scope match
39+
case scope: SpliceScope => this.pos == scope.pos
40+
case _ => false
41+
42+
override def toString =
43+
if pos.exists then
44+
s"${pos.source.toString}:${pos.startLine + 1} at column ${pos.startColumn + 1}"
45+
else
46+
"Unknown location"
47+
48+
end SpliceScope
49+
50+
51+
object SpliceScope:
52+
53+
/** A key to be used in a context property that tracks current splices we are evaluating */
54+
private val ScopeKey = new Property.Key[Scope]
55+
56+
def setSpliceScope(scope: Scope)(using Context): Context =
57+
ctx.fresh.setProperty(ScopeKey, scope)
58+
59+
def contextWithNewSpliceScope(pos: SourcePosition)(using Context): Context =
60+
ctx.fresh.setProperty(ScopeKey, new SpliceScope(pos, getCurrent))
61+
62+
/** Context with an incremented quotation level. */
63+
def getCurrent(using Context): Scope =
64+
ctx.property(ScopeKey).getOrElse(NoScope)
65+
66+
end SpliceScope

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

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,14 @@ 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 scope: Scope) extends Type[?] {
88
override def equals(that: Any): Boolean = that match {
99
case that: TypeImpl => typeTree ==
10-
// TastyTreeExpr are wrappers around trees, therfore they are equals if their trees are equal.
11-
// All scopeId should be equal unless two different runs of the compiler created the trees.
12-
that.typeTree && scopeId == that.scopeId
10+
// TastyTreeExpr are wrappers around trees, therefore they are equals if their trees are equal.
11+
// All scope should be equal unless two different runs of the compiler created the trees.
12+
that.typeTree && scope == that.scope
1313
case _ => false
1414
}
1515

16-
/** View this expression `q.Type[T]` as a `TypeTree` */
17-
def unseal(using q: Quotes): q.reflect.TypeTree =
18-
checkScopeId(q.hashCode)
19-
typeTree.asInstanceOf[q.reflect.TypeTree]
20-
21-
def checkScopeId(expectedScopeId: Int): Unit =
22-
if expectedScopeId != scopeId then
23-
throw new ScopeException("Cannot call `scala.quoted.staging.run(...)` within a macro or another `run(...)`")
24-
25-
override def hashCode: Int = typeTree.hashCode
2616
override def toString: String = "Type.of[...]"
2717
}

0 commit comments

Comments
 (0)