From 2a7e9106185251b9100e96eea0b1c0e2d6f6e985 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Wed, 17 Feb 2021 14:36:34 +0100 Subject: [PATCH 1/3] Add add informative scope extrusion check --- .../tools/dotc/quoted/PickledQuotes.scala | 36 ++++++----- .../dotty/tools/dotc/transform/Splicer.scala | 13 ++-- .../src/dotty/tools/dotc/typer/Inliner.scala | 8 +-- .../scala/quoted/runtime/impl/ExprImpl.scala | 2 +- .../quoted/runtime/impl/QuotesImpl.scala | 12 ++-- .../quoted/runtime/impl/ScopeException.scala | 31 ++++++++++ .../quoted/runtime/impl/SpliceScope.scala | 62 +++++++++++++++++++ .../scala/quoted/runtime/impl/TypeImpl.scala | 2 +- .../scala/quoted/staging/QuoteCompiler.scala | 11 +++- tests/neg-macros/i8216/Macro_1.scala | 12 ++++ tests/neg-macros/i8216/Test_2.scala | 8 +++ .../neg-macros/scope-extrusion/Macro_1.scala | 32 ++++++++++ tests/neg-macros/scope-extrusion/Test_2.scala | 6 ++ tests/run-macros/i8007/Macro_3.scala | 4 +- .../quote-matcher-symantics-2/quoted_1.scala | 18 +++--- .../quote-matcher-symantics-3/quoted_1.scala | 2 +- tests/run-staging/i3947.scala | 2 +- tests/run-staging/i3947b.scala | 2 +- tests/run-staging/i3947b2.scala | 2 +- tests/run-staging/i3947b3.scala | 2 +- tests/run-staging/i3947c.scala | 2 +- tests/run-staging/i3947d.scala | 2 +- tests/run-staging/i3947d2.scala | 2 +- tests/run-staging/i3947e.scala | 2 +- tests/run-staging/i3947f.scala | 2 +- tests/run-staging/i3947g.scala | 2 +- tests/run-staging/i3947i.scala | 2 +- tests/run-staging/i3947j.scala | 2 +- tests/run-staging/quote-run-2.scala | 2 +- .../quote-run-staged-interpreter.scala | 2 +- tests/run-staging/staged-streams_1.scala | 10 +-- 31 files changed, 229 insertions(+), 68 deletions(-) create mode 100644 compiler/src/scala/quoted/runtime/impl/SpliceScope.scala create mode 100644 tests/neg-macros/i8216/Macro_1.scala create mode 100644 tests/neg-macros/i8216/Test_2.scala create mode 100644 tests/neg-macros/scope-extrusion/Macro_1.scala create mode 100644 tests/neg-macros/scope-extrusion/Test_2.scala diff --git a/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala b/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala index 3e472ed93846..bc5e7cdc4dc1 100644 --- a/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala +++ b/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala @@ -42,6 +42,7 @@ object PickledQuotes { def quotedExprToTree[T](expr: quoted.Expr[T])(using Context): Tree = { val expr1 = expr.asInstanceOf[ExprImpl] expr1.checkScopeId(QuotesImpl.scopeId) + ScopeException.checkInCorrectScope(expr1.scope, SpliceScope.getCurrent, expr1.tree, "Expr") changeOwnerOfTree(expr1.tree, ctx.owner) } @@ -49,6 +50,7 @@ object PickledQuotes { def quotedTypeToTree(tpe: quoted.Type[?])(using Context): Tree = { val tpe1 = tpe.asInstanceOf[TypeImpl] tpe1.checkScopeId(QuotesImpl.scopeId) + ScopeException.checkInCorrectScope(tpe1.scope, SpliceScope.getCurrent, tpe1.typeTree, "Type") changeOwnerOfTree(tpe1.typeTree, ctx.owner) } @@ -73,23 +75,25 @@ object PickledQuotes { val evaluateHoles = new TreeMap { override def transform(tree: tpd.Tree)(using Context): tpd.Tree = tree match { case Hole(isTerm, idx, args) => - val reifiedArgs = args.map { arg => - if (arg.isTerm) (q: Quotes) ?=> new ExprImpl(arg, QuotesImpl.scopeId) - else new TypeImpl(arg, QuotesImpl.scopeId) + inContext(SpliceScope.contextWithNewSpliceScope(tree.sourcePos)) { + val reifiedArgs = args.map { arg => + if (arg.isTerm) (q: Quotes) ?=> new ExprImpl(arg, QuotesImpl.scopeId, SpliceScope.getCurrent) + else new TypeImpl(arg, QuotesImpl.scopeId, SpliceScope.getCurrent) + } + if isTerm then + val quotedExpr = termHole(idx, reifiedArgs, QuotesImpl()) + val filled = PickledQuotes.quotedExprToTree(quotedExpr) + + // We need to make sure a hole is created with the source file of the surrounding context, even if + // it filled with contents a different source file. + if filled.source == ctx.source then filled + else filled.cloneIn(ctx.source).withSpan(tree.span) + else + // Replaces type holes generated by PickleQuotes (non-spliced types). + // These are types defined in a quote and used at the same level in a nested quote. + val quotedType = typeHole(idx, reifiedArgs) + PickledQuotes.quotedTypeToTree(quotedType) } - if isTerm then - val quotedExpr = termHole(idx, reifiedArgs, QuotesImpl()) - val filled = PickledQuotes.quotedExprToTree(quotedExpr) - - // We need to make sure a hole is created with the source file of the surrounding context, even if - // it filled with contents a different source file. - if filled.source == ctx.source then filled - else filled.cloneIn(ctx.source).withSpan(tree.span) - else - // Replaces type holes generated by PickleQuotes (non-spliced types). - // These are types defined in a quote and used at the same level in a nested quote. - val quotedType = typeHole(idx, reifiedArgs) - PickledQuotes.quotedTypeToTree(quotedType) case tree: Select => // Retain selected members val qual = transform(tree.qualifier) diff --git a/compiler/src/dotty/tools/dotc/transform/Splicer.scala b/compiler/src/dotty/tools/dotc/transform/Splicer.scala index 66419c8c2006..864c06faa72c 100644 --- a/compiler/src/dotty/tools/dotc/transform/Splicer.scala +++ b/compiler/src/dotty/tools/dotc/transform/Splicer.scala @@ -39,16 +39,17 @@ object Splicer { * * See: `Staging` */ - def splice(tree: Tree, pos: SrcPos, classLoader: ClassLoader)(using Context): Tree = tree match { + def splice(tree: Tree, splicePos: SrcPos, spliceExpansionPos: SrcPos, classLoader: ClassLoader)(using Context): Tree = tree match { case Quoted(quotedTree) => quotedTree case _ => val macroOwner = newSymbol(ctx.owner, nme.MACROkw, Macro | Synthetic, defn.AnyType, coord = tree.span) try - inContext(ctx.withOwner(macroOwner)) { + val sliceContext = SpliceScope.contextWithNewSpliceScope(splicePos.sourcePos).withOwner(macroOwner) + inContext(sliceContext) { val oldContextClassLoader = Thread.currentThread().getContextClassLoader Thread.currentThread().setContextClassLoader(classLoader) try { - val interpreter = new Interpreter(pos, classLoader) + val interpreter = new Interpreter(spliceExpansionPos, classLoader) // Some parts of the macro are evaluated during the unpickling performed in quotedExprToTree val interpretedExpr = interpreter.interpret[Quotes => scala.quoted.Expr[Any]](tree) @@ -74,7 +75,7 @@ object Splicer { | Caused by ${ex.getClass}: ${if (ex.getMessage == null) "" else ex.getMessage} | ${ex.getStackTrace.takeWhile(_.getClassName != "dotty.tools.dotc.transform.Splicer$").drop(1).mkString("\n ")} """.stripMargin - report.error(msg, pos) + report.error(msg, spliceExpansionPos) ref(defn.Predef_undefined).withType(ErrorType(msg)) } } @@ -325,10 +326,10 @@ object Splicer { } private def interpretQuote(tree: Tree)(implicit env: Env): Object = - new ExprImpl(Inlined(EmptyTree, Nil, QuoteUtils.changeOwnerOfTree(tree, ctx.owner)).withSpan(tree.span), QuotesImpl.scopeId) + new ExprImpl(Inlined(EmptyTree, Nil, QuoteUtils.changeOwnerOfTree(tree, ctx.owner)).withSpan(tree.span), QuotesImpl.scopeId, SpliceScope.getCurrent) private def interpretTypeQuote(tree: Tree)(implicit env: Env): Object = - new TypeImpl(QuoteUtils.changeOwnerOfTree(tree, ctx.owner), QuotesImpl.scopeId) + new TypeImpl(QuoteUtils.changeOwnerOfTree(tree, ctx.owner), QuotesImpl.scopeId, SpliceScope.getCurrent) private def interpretLiteral(value: Any)(implicit env: Env): Object = value.asInstanceOf[Object] diff --git a/compiler/src/dotty/tools/dotc/typer/Inliner.scala b/compiler/src/dotty/tools/dotc/typer/Inliner.scala index d89ba6128274..c130c592cd81 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inliner.scala @@ -1303,7 +1303,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) { case res: Apply if res.symbol == defn.QuotedRuntime_exprSplice && level == 0 && !hasInliningErrors => - val expanded = expandMacro(res.args.head, tree.span) + val expanded = expandMacro(res.args.head, tree.srcPos) typedExpr(expanded) // Inline calls and constant fold code generated by the macro case res => inlineIfNeeded(res) @@ -1488,7 +1488,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) { } } - private def expandMacro(body: Tree, span: Span)(using Context) = { + private def expandMacro(body: Tree, splicePos: SrcPos)(using Context) = { assert(level == 0) val inlinedFrom = enclosingInlineds.last val dependencies = macroDependencies(body) @@ -1503,7 +1503,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) { ctx.compilationUnit.suspend() // this throws a SuspendException val evaluatedSplice = inContext(quoted.MacroExpansion.context(inlinedFrom)) { - Splicer.splice(body, inlinedFrom.srcPos, MacroClassLoader.fromContext) + Splicer.splice(body, splicePos, inlinedFrom.srcPos, MacroClassLoader.fromContext) } val inlinedNormailizer = new TreeMap { 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) { } val normalizedSplice = inlinedNormailizer.transform(evaluatedSplice) if (normalizedSplice.isEmpty) normalizedSplice - else normalizedSplice.withSpan(span) + else normalizedSplice.withSpan(splicePos.span) } /** Return the set of symbols that are referred at level -1 by the tree and defined in the current run. diff --git a/compiler/src/scala/quoted/runtime/impl/ExprImpl.scala b/compiler/src/scala/quoted/runtime/impl/ExprImpl.scala index 6965165dc3a2..d04cb9a160f3 100644 --- a/compiler/src/scala/quoted/runtime/impl/ExprImpl.scala +++ b/compiler/src/scala/quoted/runtime/impl/ExprImpl.scala @@ -10,7 +10,7 @@ import dotty.tools.dotc.ast.tpd * * May contain references to code defined outside this Expr instance. */ -final class ExprImpl(val tree: tpd.Tree, val scopeId: Int) extends Expr[Any] { +final class ExprImpl(val tree: tpd.Tree, val scopeId: Int, val scope: Scope) extends Expr[Any] { override def equals(that: Any): Boolean = that match { case that: ExprImpl => // Expr are wrappers around trees, therefore they are equals if their trees are equal. diff --git a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala index 746ea78e0ba9..0e70e9ea4c75 100644 --- a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala +++ b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala @@ -105,7 +105,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler case _ => false def asExpr: scala.quoted.Expr[Any] = if self.isExpr then - new ExprImpl(self, QuotesImpl.this.hashCode) + new ExprImpl(self, QuotesImpl.this.hashCode, SpliceScope.getCurrent) else self match case TermTypeTest(self) => throw new Exception("Expected an expression. This is a partially applied Term. Try eta-expanding the term first.") case _ => throw new Exception("Expected a Term but was: " + self) @@ -372,11 +372,11 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler given TermMethods: TermMethods with extension (self: Term) def seal: scala.quoted.Expr[Any] = - if self.isExpr then new ExprImpl(self, QuotesImpl.this.hashCode) + if self.isExpr then new ExprImpl(self, QuotesImpl.this.hashCode, SpliceScope.getCurrent) else throw new Exception("Cannot seal a partially applied Term. Try eta-expanding the term first.") def sealOpt: Option[scala.quoted.Expr[Any]] = - if self.isExpr then Some(new ExprImpl(self, QuotesImpl.this.hashCode)) + if self.isExpr then Some(new ExprImpl(self, QuotesImpl.this.hashCode, SpliceScope.getCurrent)) else None def tpe: TypeRepr = self.tpe @@ -1666,7 +1666,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler def seal: scala.quoted.Type[_] = self.asType def asType: scala.quoted.Type[?] = - new TypeImpl(Inferred(self), QuotesImpl.this.hashCode) + new TypeImpl(Inferred(self), QuotesImpl.this.hashCode, SpliceScope.getCurrent) def =:=(that: TypeRepr): Boolean = self =:= that def <:<(that: TypeRepr): Boolean = self <:< that @@ -2877,11 +2877,11 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler 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] = val tree = PickledQuotes.unpickleTerm(pickled, typeHole, termHole) - new ExprImpl(tree, hash).asInstanceOf[scala.quoted.Expr[T]] + new ExprImpl(tree, hash, SpliceScope.getCurrent).asInstanceOf[scala.quoted.Expr[T]] 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] = val tree = PickledQuotes.unpickleTypeTree(pickled, typeHole, termHole) - new TypeImpl(tree, hash).asInstanceOf[scala.quoted.Type[T]] + new TypeImpl(tree, hash, SpliceScope.getCurrent).asInstanceOf[scala.quoted.Type[T]] object ExprMatch extends ExprMatchModule: def unapply[TypeBindings <: Tuple, Tup <: Tuple](scrutinee: scala.quoted.Expr[Any])(using pattern: scala.quoted.Expr[Any]): Option[Tup] = diff --git a/compiler/src/scala/quoted/runtime/impl/ScopeException.scala b/compiler/src/scala/quoted/runtime/impl/ScopeException.scala index 73715ae111e5..7dc84343238e 100644 --- a/compiler/src/scala/quoted/runtime/impl/ScopeException.scala +++ b/compiler/src/scala/quoted/runtime/impl/ScopeException.scala @@ -1,3 +1,34 @@ package scala.quoted.runtime.impl +import dotty.tools.dotc.ast.tpd.Tree +import dotty.tools.dotc.core.Contexts._ + class ScopeException(msg: String) extends Exception(msg) + +object ScopeException: + def checkInCorrectScope(scope: Scope, currentScope: Scope, tree: Tree, kind: String)(using Context): Unit = + val yCheck = ctx.settings.Ycheck.value(using ctx).exists(x => x == "all" || x == "macros") + if yCheck && !scope.isOuterScopeOf(currentScope) then + throw new ScopeException( + if scope.atSameLocation(currentScope) then + s"""Type created in a splice, extruded from that splice and then used in a subsequent evaluation of that same splice. + |Splice: $scope + |$kind: ${tree.show} + | + | + |Splice stack: + |${scope.stack.mkString("\t", "\n\t", "\n")} + """.stripMargin + else + s"""Expression created in a splice was used outside of that splice. + |Created in: $scope + |Used in: $currentScope + |$kind: ${tree.show} + | + | + |Creation stack: + |${scope.stack.mkString("\t", "\n\t", "\n")} + | + |Use stack: + |${currentScope.stack.mkString("\t", "\n\t", "\n")} + """.stripMargin) diff --git a/compiler/src/scala/quoted/runtime/impl/SpliceScope.scala b/compiler/src/scala/quoted/runtime/impl/SpliceScope.scala new file mode 100644 index 000000000000..b44b1c2c5b80 --- /dev/null +++ b/compiler/src/scala/quoted/runtime/impl/SpliceScope.scala @@ -0,0 +1,62 @@ +package scala.quoted +package runtime.impl + +import dotty.tools.dotc.core.Contexts._ +import dotty.tools.dotc.util.Property +import dotty.tools.dotc.util.SourcePosition + +/** Unique identifier of the evaluation of a splice. + * + * A nested splice gets a new Scope with the outer one as an owner. + * This also applies for recursive splices. + */ +trait Scope { + /** Outer scope that was used to create the quote containing this splice. + * NoScope if there is no scope + */ + def outer: Scope = NoScope + /** Is this is a outer scope of the given scope */ + def isOuterScopeOf(scope: Scope): Boolean = + this.eq(scope) || (scope.ne(NoScope) && isOuterScopeOf(scope.outer)) + /** Stack of locations where scopes where evaluated */ + def stack: List[String] = + this.toString :: (if outer.eq(NoScope) then Nil else outer.stack) + /** If the two scopes correspond to the same splice in source. */ + def atSameLocation(scope: Scope): Boolean = false +} + +/** Only used for outer scope of top level splice and staging `run` */ +object NoScope extends Scope: + override def outer: Scope = throw UnsupportedOperationException("NoScope.outer") + +class SpliceScope(val pos: SourcePosition, override val outer: Scope) extends Scope: + + override def atSameLocation(scope: Scope): Boolean = scope match + case scope: SpliceScope => this.pos == scope.pos + case _ => false + + override def toString = + if pos.exists then + s"${pos.source.toString}:${pos.startLine + 1} at column ${pos.startColumn + 1}" + else + "Unknown location" + +end SpliceScope + + +object SpliceScope: + + /** A key to be used in a context property that tracks current splices we are evaluating */ + private val ScopeKey = new Property.Key[Scope] + + def setSpliceScope(scope: Scope)(using Context): Context = + ctx.fresh.setProperty(ScopeKey, scope) + + def contextWithNewSpliceScope(pos: SourcePosition)(using Context): Context = + ctx.fresh.setProperty(ScopeKey, new SpliceScope(pos, getCurrent)) + + /** Context with an incremented quotation level. */ + def getCurrent(using Context): Scope = + ctx.property(ScopeKey).getOrElse(null) + +end SpliceScope diff --git a/compiler/src/scala/quoted/runtime/impl/TypeImpl.scala b/compiler/src/scala/quoted/runtime/impl/TypeImpl.scala index 58bb4af297aa..9add4060f1ee 100644 --- a/compiler/src/scala/quoted/runtime/impl/TypeImpl.scala +++ b/compiler/src/scala/quoted/runtime/impl/TypeImpl.scala @@ -4,7 +4,7 @@ package runtime.impl import dotty.tools.dotc.ast.tpd /** Quoted type (or kind) `T` backed by a tree */ -final class TypeImpl(val typeTree: tpd.Tree, val scopeId: Int) extends Type[?] { +final class TypeImpl(val typeTree: tpd.Tree, val scopeId: Int, val scope: Scope) extends Type[?] { override def equals(that: Any): Boolean = that match { case that: TypeImpl => typeTree == // TastyTreeExpr are wrappers around trees, therfore they are equals if their trees are equal. diff --git a/staging/src/scala/quoted/staging/QuoteCompiler.scala b/staging/src/scala/quoted/staging/QuoteCompiler.scala index bc0f346b37a8..9279e21f3166 100644 --- a/staging/src/scala/quoted/staging/QuoteCompiler.scala +++ b/staging/src/scala/quoted/staging/QuoteCompiler.scala @@ -18,10 +18,10 @@ import dotty.tools.dotc.quoted.PickledQuotes import dotty.tools.dotc.transform.Splicer.checkEscapedVariables import dotty.tools.dotc.transform.{Inlining, PickleQuotes} import dotty.tools.dotc.util.Spans.Span -import dotty.tools.dotc.util.SourceFile +import dotty.tools.dotc.util.{SourceFile, NoSourcePosition} import dotty.tools.io.{Path, VirtualFile} -import scala.quoted.runtime.impl.QuotesImpl +import scala.quoted.runtime.impl._ import scala.annotation.tailrec import scala.concurrent.Promise @@ -49,6 +49,10 @@ private class QuoteCompiler extends Compiler: def outputClassName: TypeName = "Generated$Code$From$Quoted".toTypeName + class RunScope extends Scope { + override def toString: String = "scala.quoted.staging" + } + /** Frontend that receives a scala.quoted.Expr or scala.quoted.Type as input */ class QuotedFrontend extends Phase: import tpd._ @@ -58,7 +62,8 @@ private class QuoteCompiler extends Compiler: override def runOn(units: List[CompilationUnit])(implicit ctx: Context): List[CompilationUnit] = units.flatMap { case exprUnit: ExprCompilationUnit => - implicit val unitCtx: Context = ctx.fresh.setPhase(this.start).setCompilationUnit(exprUnit) + val ctx1 = ctx.fresh.setPhase(this.start).setCompilationUnit(exprUnit) + implicit val unitCtx: Context = SpliceScope.setSpliceScope(new RunScope)(using ctx1) val pos = Span(0) val assocFile = new VirtualFile("") diff --git a/tests/neg-macros/i8216/Macro_1.scala b/tests/neg-macros/i8216/Macro_1.scala new file mode 100644 index 000000000000..9105abd01d28 --- /dev/null +++ b/tests/neg-macros/i8216/Macro_1.scala @@ -0,0 +1,12 @@ +package macros +import scala.quoted._ + +var saved = Option.empty[Expr[Any]] + +def oops(c: Expr[Any])(using Quotes) = { + if saved.isEmpty then + saved = Some(c) + c + else saved.get +} +inline def test(c: Any) = ${oops('c)} diff --git a/tests/neg-macros/i8216/Test_2.scala b/tests/neg-macros/i8216/Test_2.scala new file mode 100644 index 000000000000..523017b42edf --- /dev/null +++ b/tests/neg-macros/i8216/Test_2.scala @@ -0,0 +1,8 @@ +object Test { + class A(x: Int) { + macros.test(x) + } + class B(y: String) { + macros.test(y) // error + } +} \ No newline at end of file diff --git a/tests/neg-macros/scope-extrusion/Macro_1.scala b/tests/neg-macros/scope-extrusion/Macro_1.scala new file mode 100644 index 000000000000..b3cba5477863 --- /dev/null +++ b/tests/neg-macros/scope-extrusion/Macro_1.scala @@ -0,0 +1,32 @@ +import quoted.* + +inline def test1(): Int = ${ testExtrusion1 } +private def testExtrusion1(using Quotes): Expr[Int] = + var extruded: Expr[Int] = null + '{ (x: Int) => + ${ + extruded = '{x} + extruded + } + } + extruded + +inline def test2(): Int = ${ testExtrusion2 } +private def testExtrusion2(using Quotes): Expr[Int] = + '{ 1 + + ${ var extruded: Expr[Int] = null; '{ (y: Int) => ${ extruded = '{y}; extruded } }; extruded } + } + +inline def test3(): Int = ${ testExtrusion3 } +private def testExtrusion3(using Quotes): Expr[Int] = { + var extruded: Expr[Int] = null + for i <- 1 to 3 do + '{ (x: Int) => + ${ + if extruded == null then + extruded = '{x} + extruded + } + } + extruded +} diff --git a/tests/neg-macros/scope-extrusion/Test_2.scala b/tests/neg-macros/scope-extrusion/Test_2.scala new file mode 100644 index 000000000000..29badd1dc01c --- /dev/null +++ b/tests/neg-macros/scope-extrusion/Test_2.scala @@ -0,0 +1,6 @@ + +object Test_2 { + test1() // error + test2() // error + test3() // error +} diff --git a/tests/run-macros/i8007/Macro_3.scala b/tests/run-macros/i8007/Macro_3.scala index 8a11dccb9a20..a0e52e93b618 100644 --- a/tests/run-macros/i8007/Macro_3.scala +++ b/tests/run-macros/i8007/Macro_3.scala @@ -40,7 +40,7 @@ object Eq { ev match { case '{ $m: Mirror.ProductOf[T] { type MirroredElemTypes = elementTypes }} => val elemInstances = summonAll[elementTypes] - val eqProductBody: (Expr[T], Expr[T]) => Expr[Boolean] = (x, y) => { + def eqProductBody(x: Expr[T], y: Expr[T])(using Quotes): Expr[Boolean] = { elemInstances.zipWithIndex.foldLeft(Expr(true: Boolean)) { case (acc, (elem, index)) => val e1 = '{$x.asInstanceOf[Product].productElement(${Expr(index)})} @@ -55,7 +55,7 @@ object Eq { case '{ $m: Mirror.SumOf[T] { type MirroredElemTypes = elementTypes }} => val elemInstances = summonAll[elementTypes] - val eqSumBody: (Expr[T], Expr[T]) => Expr[Boolean] = (x, y) => { + def eqSumBody(x: Expr[T], y: Expr[T])(using Quotes): Expr[Boolean] = { val ordx = '{ $m.ordinal($x) } val ordy = '{ $m.ordinal($y) } diff --git a/tests/run-macros/quote-matcher-symantics-2/quoted_1.scala b/tests/run-macros/quote-matcher-symantics-2/quoted_1.scala index 3485f40cca37..5f53fd1a7ade 100644 --- a/tests/run-macros/quote-matcher-symantics-2/quoted_1.scala +++ b/tests/run-macros/quote-matcher-symantics-2/quoted_1.scala @@ -19,7 +19,7 @@ object Macros { private def impl[T: Type](sym: Symantics[T], a: Expr[DSL])(using Quotes): Expr[T] = { - def lift(e: Expr[DSL])(implicit env: Map[Int, Expr[T]]): Expr[T] = e match { + def lift(e: Expr[DSL])(using env: Map[Int, Expr[T]])(using Quotes): Expr[T] = e match { case '{ LitDSL(${Expr(c)}) } => sym.value(c) @@ -32,7 +32,7 @@ object Macros { case '{ val x: DSL = $value; $bodyFn(x): DSL } => UnsafeExpr.open(bodyFn) { (body1, close) => val (i, nEnvVar) = freshEnvVar() - lift(close(body1)(nEnvVar))(env + (i -> lift(value))) + lift(close(body1)(nEnvVar))(using env + (i -> lift(value))) } case '{ envVar(${Expr(i)}) } => env(i) @@ -43,12 +43,12 @@ object Macros { ??? } - def liftFun(e: Expr[DSL => DSL])(implicit env: Map[Int, Expr[T]]): Expr[T => T] = e match { + def liftFun(e: Expr[DSL => DSL])(using env: Map[Int, Expr[T]])(using Quotes): Expr[T => T] = e match { case '{ (x: DSL) => $bodyFn(x): DSL } => sym.lam((y: Expr[T]) => UnsafeExpr.open(bodyFn) { (body1, close) => val (i, nEnvVar) = freshEnvVar() - lift(close(body1)(nEnvVar))(env + (i -> y)) + lift(close(body1)(nEnvVar))(using env + (i -> y)) } ) case _ => @@ -57,7 +57,7 @@ object Macros { ??? } - lift(a)(Map.empty) + lift(a)(using Map.empty) } } @@ -112,7 +112,7 @@ trait Symantics[Num] { def plus(x: Expr[Num], y: Expr[Num])(using Quotes): Expr[Num] def times(x: Expr[Num], y: Expr[Num])(using Quotes): Expr[Num] def app(f: Expr[Num => Num], x: Expr[Num])(using Quotes): Expr[Num] - def lam(body: Expr[Num] => Expr[Num])(using Quotes): Expr[Num => Num] + def lam(body: Quotes ?=> Expr[Num] => Expr[Num])(using Quotes): Expr[Num => Num] } object StringNum extends Symantics[String] { @@ -120,7 +120,7 @@ object StringNum extends Symantics[String] { def plus(x: Expr[String], y: Expr[String])(using Quotes): Expr[String] = '{ s"${$x} + ${$y}" } // '{ x + " + " + y } def times(x: Expr[String], y: Expr[String])(using Quotes): Expr[String] = '{ s"${$x} * ${$y}" } def app(f: Expr[String => String], x: Expr[String])(using Quotes): Expr[String] = Expr.betaReduce('{ $f($x) }) - def lam(body: Expr[String] => Expr[String])(using Quotes): Expr[String => String] = '{ (x: String) => ${body('x)} } + def lam(body: Quotes ?=> Expr[String] => Expr[String])(using Quotes): Expr[String => String] = '{ (x: String) => ${body('x)} } } object ComputeNum extends Symantics[Int] { @@ -128,7 +128,7 @@ object ComputeNum extends Symantics[Int] { def plus(x: Expr[Int], y: Expr[Int])(using Quotes): Expr[Int] = '{ $x + $y } def times(x: Expr[Int], y: Expr[Int])(using Quotes): Expr[Int] = '{ $x * $y } def app(f: Expr[Int => Int], x: Expr[Int])(using Quotes): Expr[Int] = '{ $f($x) } - def lam(body: Expr[Int] => Expr[Int])(using Quotes): Expr[Int => Int] = '{ (x: Int) => ${body('x)} } + def lam(body: Quotes ?=> Expr[Int] => Expr[Int])(using Quotes): Expr[Int => Int] = '{ (x: Int) => ${body('x)} } } object ASTNum extends Symantics[ASTNum] { @@ -136,7 +136,7 @@ object ASTNum extends Symantics[ASTNum] { def plus(x: Expr[ASTNum], y: Expr[ASTNum])(using Quotes): Expr[ASTNum] = '{ PlusAST($x, $y) } def times(x: Expr[ASTNum], y: Expr[ASTNum])(using Quotes): Expr[ASTNum] = '{ TimesAST($x, $y) } def app(f: Expr[ASTNum => ASTNum], x: Expr[ASTNum])(using Quotes): Expr[ASTNum] = '{ AppAST($f, $x) } - def lam(body: Expr[ASTNum] => Expr[ASTNum])(using Quotes): Expr[ASTNum => ASTNum] = '{ (x: ASTNum) => ${body('x)} } + def lam(body: Quotes ?=> Expr[ASTNum] => Expr[ASTNum])(using Quotes): Expr[ASTNum => ASTNum] = '{ (x: ASTNum) => ${body('x)} } } trait ASTNum diff --git a/tests/run-macros/quote-matcher-symantics-3/quoted_1.scala b/tests/run-macros/quote-matcher-symantics-3/quoted_1.scala index 1855fb10f856..5aa3a1f700ea 100644 --- a/tests/run-macros/quote-matcher-symantics-3/quoted_1.scala +++ b/tests/run-macros/quote-matcher-symantics-3/quoted_1.scala @@ -24,7 +24,7 @@ object Macros { None } - def lift[T: Type](e: Expr[T])(using env: Env): Expr[R[T]] = ((e: Expr[Any]) match { + def lift[T: Type](e: Expr[T])(using env: Env)(using Quotes): Expr[R[T]] = ((e: Expr[Any]) match { case Const(e: Int) => '{ $sym.int(${Expr(e)}).asInstanceOf[R[T]] } case Const(e: Boolean) => '{ $sym.bool(${Expr(e)}).asInstanceOf[R[T]] } diff --git a/tests/run-staging/i3947.scala b/tests/run-staging/i3947.scala index cf83bae1ccb9..4e5519b47576 100644 --- a/tests/run-staging/i3947.scala +++ b/tests/run-staging/i3947.scala @@ -6,7 +6,7 @@ object Test { given Compiler = Compiler.make(getClass.getClassLoader) def main(args: Array[String]): Unit = run { - def test[T: Type](clazz: java.lang.Class[T]) = { + def test[T: Type](clazz: java.lang.Class[T])(using Quotes) = { val lclazz = Expr(clazz) val name = '{ ($lclazz).getCanonicalName } println(name.show) diff --git a/tests/run-staging/i3947b.scala b/tests/run-staging/i3947b.scala index fb1d0f19d995..e370741af58b 100644 --- a/tests/run-staging/i3947b.scala +++ b/tests/run-staging/i3947b.scala @@ -7,7 +7,7 @@ object Test { given Compiler = Compiler.make(getClass.getClassLoader) def main(args: Array[String]): Unit = run { - def test[T: Type](clazz: java.lang.Class[T]) = { + def test[T: Type](clazz: java.lang.Class[T])(using Quotes) = { val lclazz = Expr(clazz) val name = '{ ($lclazz).getCanonicalName } println() diff --git a/tests/run-staging/i3947b2.scala b/tests/run-staging/i3947b2.scala index 33d76753b1a4..27a82641c74b 100644 --- a/tests/run-staging/i3947b2.scala +++ b/tests/run-staging/i3947b2.scala @@ -7,7 +7,7 @@ object Test { given Compiler = Compiler.make(getClass.getClassLoader) def main(args: Array[String]): Unit = run { - def test[T: Type](clazz: Quotes ?=> java.lang.Class[T]) = { + def test[T: Type](clazz: Quotes ?=> java.lang.Class[T])(using Quotes) = { val lclazz = Expr(clazz) val name = '{ ($lclazz).getCanonicalName } println() diff --git a/tests/run-staging/i3947b3.scala b/tests/run-staging/i3947b3.scala index fd9ec26d15a6..ea7cead059d3 100644 --- a/tests/run-staging/i3947b3.scala +++ b/tests/run-staging/i3947b3.scala @@ -7,7 +7,7 @@ object Test { given Compiler = Compiler.make(getClass.getClassLoader) def main(args: Array[String]): Unit = run { - def test[T: Type](clazz: java.lang.Class[T]) = { + def test[T: Type](clazz: java.lang.Class[T])(using Quotes) = { val lclazz = Expr(clazz) val name = '{ ($lclazz).getCanonicalName } println(name.show) diff --git a/tests/run-staging/i3947c.scala b/tests/run-staging/i3947c.scala index 963ff3750fbd..9cab4fe3275a 100644 --- a/tests/run-staging/i3947c.scala +++ b/tests/run-staging/i3947c.scala @@ -6,7 +6,7 @@ object Test { given Compiler = Compiler.make(getClass.getClassLoader) def main(args: Array[String]): Unit = run { - def test[T: Type](clazz: java.lang.Class[T]) = { + def test[T: Type](clazz: java.lang.Class[T])(using Quotes) = { val lclazz = Expr(clazz) val name = '{ ($lclazz).getCanonicalName } println(name.show) diff --git a/tests/run-staging/i3947d.scala b/tests/run-staging/i3947d.scala index 44c46ee169ef..5bb66e764bbb 100644 --- a/tests/run-staging/i3947d.scala +++ b/tests/run-staging/i3947d.scala @@ -5,7 +5,7 @@ object Test { given Compiler = Compiler.make(getClass.getClassLoader) def main(args: Array[String]): Unit = run { - def test[T: Type](clazz: java.lang.Class[T]) = { + def test[T: Type](clazz: java.lang.Class[T])(using Quotes) = { val lclazz = Expr(clazz) val name = '{ ($lclazz).getCanonicalName } println(name.show) diff --git a/tests/run-staging/i3947d2.scala b/tests/run-staging/i3947d2.scala index 0108893a7b78..b6d1ff971575 100644 --- a/tests/run-staging/i3947d2.scala +++ b/tests/run-staging/i3947d2.scala @@ -6,7 +6,7 @@ object Test { given Compiler = Compiler.make(getClass.getClassLoader) def main(args: Array[String]): Unit = run { - def test[T: Type](clazz: java.lang.Class[T]) = { + def test[T: Type](clazz: java.lang.Class[T])(using Quotes) = { val lclazz = Expr(clazz) val name = '{ ($lclazz).getCanonicalName } println(name.show) diff --git a/tests/run-staging/i3947e.scala b/tests/run-staging/i3947e.scala index 64f4e51dfdda..f16c5ddd8134 100644 --- a/tests/run-staging/i3947e.scala +++ b/tests/run-staging/i3947e.scala @@ -7,7 +7,7 @@ object Test { def main(args: Array[String]): Unit = run { - def test[T: Type](clazz: java.lang.Class[T]) = { + def test[T: Type](clazz: java.lang.Class[T])(using Quotes) = { val lclazz = Expr(clazz) val name = '{ ($lclazz).getCanonicalName } println(name.show) diff --git a/tests/run-staging/i3947f.scala b/tests/run-staging/i3947f.scala index 8acbb2b9678b..174da2a121cc 100644 --- a/tests/run-staging/i3947f.scala +++ b/tests/run-staging/i3947f.scala @@ -7,7 +7,7 @@ object Test { given Compiler = Compiler.make(getClass.getClassLoader) def main(args: Array[String]): Unit = run { - def test[T: Type](clazz: java.lang.Class[T]) = { + def test[T: Type](clazz: java.lang.Class[T])(using Quotes) = { val lclazz = Expr(clazz) val name = '{ ($lclazz).getCanonicalName } println(name.show) diff --git a/tests/run-staging/i3947g.scala b/tests/run-staging/i3947g.scala index 548566213321..5f6d6275fc6e 100644 --- a/tests/run-staging/i3947g.scala +++ b/tests/run-staging/i3947g.scala @@ -5,7 +5,7 @@ import scala.quoted.staging.* object Test { given Compiler = Compiler.make(getClass.getClassLoader) def main(args: Array[String]): Unit = run { - def test[T: Type](clazz: java.lang.Class[T]) = { + def test[T: Type](clazz: java.lang.Class[T])(using Quotes) = { val lclazz = Expr(clazz) val name = '{ ($lclazz).getCanonicalName } println(name.show) diff --git a/tests/run-staging/i3947i.scala b/tests/run-staging/i3947i.scala index bc5151528de6..dd2f6f1824c0 100644 --- a/tests/run-staging/i3947i.scala +++ b/tests/run-staging/i3947i.scala @@ -6,7 +6,7 @@ object Test { given Compiler = Compiler.make(getClass.getClassLoader) def main(args: Array[String]): Unit = run { - def test[T: Type](clazz: java.lang.Class[T]) = { + def test[T: Type](clazz: java.lang.Class[T])(using Quotes) = { val lclazz = Expr(clazz) val name = '{ ($lclazz).getCanonicalName } println(name.show) diff --git a/tests/run-staging/i3947j.scala b/tests/run-staging/i3947j.scala index 19b7c4a7f77d..164b93f150f5 100644 --- a/tests/run-staging/i3947j.scala +++ b/tests/run-staging/i3947j.scala @@ -6,7 +6,7 @@ object Test { given Compiler = Compiler.make(getClass.getClassLoader) def main(args: Array[String]): Unit = run { - def test[T: Type](clazz: java.lang.Class[T]) = { + def test[T: Type](clazz: java.lang.Class[T])(using Quotes) = { val lclazz = Expr(clazz) val name = '{ ($lclazz).getCanonicalName } println(name.show) diff --git a/tests/run-staging/quote-run-2.scala b/tests/run-staging/quote-run-2.scala index 3b69514cf908..3686e37f70c9 100644 --- a/tests/run-staging/quote-run-2.scala +++ b/tests/run-staging/quote-run-2.scala @@ -5,7 +5,7 @@ import scala.quoted.staging.* object Test { given Compiler = Compiler.make(getClass.getClassLoader) def main(args: Array[String]): Unit = withQuotes { - def powerCode(n: Int, x: Expr[Double]): Expr[Double] = + def powerCode(n: Int, x: Expr[Double])(using Quotes): Expr[Double] = if (n == 0) '{1.0} else if (n == 1) x else if (n % 2 == 0) '{ { val y = $x * $x; ${powerCode(n / 2, 'y)} } } diff --git a/tests/run-staging/quote-run-staged-interpreter.scala b/tests/run-staging/quote-run-staged-interpreter.scala index 2f2d977f6d4e..3aedb4a8429e 100644 --- a/tests/run-staging/quote-run-staged-interpreter.scala +++ b/tests/run-staging/quote-run-staged-interpreter.scala @@ -12,7 +12,7 @@ object Test { import Exp.* def compile(e: Exp, env: Map[String, Expr[Int]], keepLets: Boolean)(using Quotes): Expr[Int] = { - def compileImpl(e: Exp, env: Map[String, Expr[Int]]): Expr[Int] = e match { + def compileImpl(e: Exp, env: Map[String, Expr[Int]])(using Quotes): Expr[Int] = e match { case Num(n) => Expr(n) case Plus(e1, e2) => '{${compileImpl(e1, env)} + ${compileImpl(e2, env)}} case Var(x) => env(x) diff --git a/tests/run-staging/staged-streams_1.scala b/tests/run-staging/staged-streams_1.scala index 3018f85aaf21..5315f66871ca 100644 --- a/tests/run-staging/staged-streams_1.scala +++ b/tests/run-staging/staged-streams_1.scala @@ -681,18 +681,18 @@ object Test { } sealed trait Var[T] { - def get(using Quotes): Expr[T] - def update(e: Expr[T])(using Quotes): Expr[Unit] + def get: Expr[T] + def update(e: Quotes ?=> Expr[T]): Expr[Unit] } object Var { - def apply[T: Type, U: Type](init: Expr[T])(body: Var[T] => Expr[U])(using Quotes): Expr[U] = '{ + def apply[T: Type, U: Type](init: Expr[T])(body: Quotes ?=> Var[T] => Expr[U])(using Quotes): Expr[U] = '{ var x = $init ${ body( new Var[T] { - def get(using Quotes): Expr[T] = 'x - def update(e: Expr[T])(using Quotes): Expr[Unit] = '{ x = $e } + def get: Expr[T] = 'x + def update(e: Quotes ?=> Expr[T]): Expr[Unit] = '{ x = $e } } ) } From 6e18e45c59e55131b3e28309a5da7114a439cff4 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Fri, 26 Feb 2021 12:00:03 +0100 Subject: [PATCH 2/3] Use SpliceScope instead of ScopeId and remove it --- .../tools/dotc/quoted/PickledQuotes.scala | 6 ++--- .../dotty/tools/dotc/transform/Splicer.scala | 4 +-- .../scala/quoted/runtime/impl/ExprImpl.scala | 11 +++----- .../quoted/runtime/impl/QuotesImpl.scala | 27 +++++-------------- .../quoted/runtime/impl/ScopeException.scala | 3 +++ .../quoted/runtime/impl/SpliceScope.scala | 6 ++++- .../scala/quoted/runtime/impl/TypeImpl.scala | 18 +++---------- 7 files changed, 26 insertions(+), 49 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala b/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala index bc5e7cdc4dc1..fd118eb43bb5 100644 --- a/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala +++ b/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala @@ -41,7 +41,6 @@ object PickledQuotes { /** Transform the expression into its fully spliced Tree */ def quotedExprToTree[T](expr: quoted.Expr[T])(using Context): Tree = { val expr1 = expr.asInstanceOf[ExprImpl] - expr1.checkScopeId(QuotesImpl.scopeId) ScopeException.checkInCorrectScope(expr1.scope, SpliceScope.getCurrent, expr1.tree, "Expr") changeOwnerOfTree(expr1.tree, ctx.owner) } @@ -49,7 +48,6 @@ object PickledQuotes { /** Transform the expression into its fully spliced TypeTree */ def quotedTypeToTree(tpe: quoted.Type[?])(using Context): Tree = { val tpe1 = tpe.asInstanceOf[TypeImpl] - tpe1.checkScopeId(QuotesImpl.scopeId) ScopeException.checkInCorrectScope(tpe1.scope, SpliceScope.getCurrent, tpe1.typeTree, "Type") changeOwnerOfTree(tpe1.typeTree, ctx.owner) } @@ -77,8 +75,8 @@ object PickledQuotes { case Hole(isTerm, idx, args) => inContext(SpliceScope.contextWithNewSpliceScope(tree.sourcePos)) { val reifiedArgs = args.map { arg => - if (arg.isTerm) (q: Quotes) ?=> new ExprImpl(arg, QuotesImpl.scopeId, SpliceScope.getCurrent) - else new TypeImpl(arg, QuotesImpl.scopeId, SpliceScope.getCurrent) + if (arg.isTerm) (q: Quotes) ?=> new ExprImpl(arg, SpliceScope.getCurrent) + else new TypeImpl(arg, SpliceScope.getCurrent) } if isTerm then val quotedExpr = termHole(idx, reifiedArgs, QuotesImpl()) diff --git a/compiler/src/dotty/tools/dotc/transform/Splicer.scala b/compiler/src/dotty/tools/dotc/transform/Splicer.scala index 864c06faa72c..4acc0e5e38ed 100644 --- a/compiler/src/dotty/tools/dotc/transform/Splicer.scala +++ b/compiler/src/dotty/tools/dotc/transform/Splicer.scala @@ -326,10 +326,10 @@ object Splicer { } private def interpretQuote(tree: Tree)(implicit env: Env): Object = - new ExprImpl(Inlined(EmptyTree, Nil, QuoteUtils.changeOwnerOfTree(tree, ctx.owner)).withSpan(tree.span), QuotesImpl.scopeId, SpliceScope.getCurrent) + new ExprImpl(Inlined(EmptyTree, Nil, QuoteUtils.changeOwnerOfTree(tree, ctx.owner)).withSpan(tree.span), SpliceScope.getCurrent) private def interpretTypeQuote(tree: Tree)(implicit env: Env): Object = - new TypeImpl(QuoteUtils.changeOwnerOfTree(tree, ctx.owner), QuotesImpl.scopeId, SpliceScope.getCurrent) + new TypeImpl(QuoteUtils.changeOwnerOfTree(tree, ctx.owner), SpliceScope.getCurrent) private def interpretLiteral(value: Any)(implicit env: Env): Object = value.asInstanceOf[Object] diff --git a/compiler/src/scala/quoted/runtime/impl/ExprImpl.scala b/compiler/src/scala/quoted/runtime/impl/ExprImpl.scala index d04cb9a160f3..ffd12b2e89b0 100644 --- a/compiler/src/scala/quoted/runtime/impl/ExprImpl.scala +++ b/compiler/src/scala/quoted/runtime/impl/ExprImpl.scala @@ -10,19 +10,14 @@ import dotty.tools.dotc.ast.tpd * * May contain references to code defined outside this Expr instance. */ -final class ExprImpl(val tree: tpd.Tree, val scopeId: Int, val scope: Scope) extends Expr[Any] { +final class ExprImpl(val tree: tpd.Tree, val scope: Scope) extends Expr[Any] { override def equals(that: Any): Boolean = that match { case that: ExprImpl => // Expr are wrappers around trees, therefore they are equals if their trees are equal. - // All scopeId should be equal unless two different runs of the compiler created the trees. - tree == that.tree && scopeId == that.scopeId + // All scope should be equal unless two different runs of the compiler created the trees. + tree == that.tree && scope == that.scope case _ => false } - def checkScopeId(expectedScopeId: Int): Unit = - if expectedScopeId != scopeId then - throw new ScopeException("Cannot call `scala.quoted.staging.run(...)` within a macro or another `run(...)`") - - override def hashCode: Int = tree.hashCode override def toString: String = "'{ ... }" } diff --git a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala index 0e70e9ea4c75..528cd762bb73 100644 --- a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala +++ b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala @@ -23,8 +23,6 @@ import scala.reflect.TypeTest object QuotesImpl { - type ScopeId = Int - def apply()(using Context): Quotes = new QuotesImpl @@ -34,11 +32,6 @@ object QuotesImpl { if ctx.settings.color.value == "always" then TreeAnsiCode.show(tree) else TreeCode.show(tree) - // TODO Explore more fine grained scope ids. - // This id can only differentiate scope extrusion from one compiler instance to another. - def scopeId(using Context): ScopeId = - ctx.outersIterator.toList.last.hashCode() - } 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 end CompilationInfo extension (expr: Expr[Any]) - def asTerm: Term = - val exprImpl = expr.asInstanceOf[ExprImpl] - exprImpl.checkScopeId(QuotesImpl.this.hashCode) - exprImpl.tree + def asTerm: Term = expr.asInstanceOf[ExprImpl].tree end extension type Tree = tpd.Tree @@ -105,7 +95,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler case _ => false def asExpr: scala.quoted.Expr[Any] = if self.isExpr then - new ExprImpl(self, QuotesImpl.this.hashCode, SpliceScope.getCurrent) + new ExprImpl(self, SpliceScope.getCurrent) else self match case TermTypeTest(self) => throw new Exception("Expected an expression. This is a partially applied Term. Try eta-expanding the term first.") case _ => throw new Exception("Expected a Term but was: " + self) @@ -372,11 +362,11 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler given TermMethods: TermMethods with extension (self: Term) def seal: scala.quoted.Expr[Any] = - if self.isExpr then new ExprImpl(self, QuotesImpl.this.hashCode, SpliceScope.getCurrent) + if self.isExpr then new ExprImpl(self, SpliceScope.getCurrent) else throw new Exception("Cannot seal a partially applied Term. Try eta-expanding the term first.") def sealOpt: Option[scala.quoted.Expr[Any]] = - if self.isExpr then Some(new ExprImpl(self, QuotesImpl.this.hashCode, SpliceScope.getCurrent)) + if self.isExpr then Some(new ExprImpl(self, SpliceScope.getCurrent)) else None def tpe: TypeRepr = self.tpe @@ -1666,7 +1656,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler def seal: scala.quoted.Type[_] = self.asType def asType: scala.quoted.Type[?] = - new TypeImpl(Inferred(self), QuotesImpl.this.hashCode, SpliceScope.getCurrent) + new TypeImpl(Inferred(self), SpliceScope.getCurrent) def =:=(that: TypeRepr): Boolean = self =:= that def <:<(that: TypeRepr): Boolean = self <:< that @@ -2877,11 +2867,11 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler 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] = val tree = PickledQuotes.unpickleTerm(pickled, typeHole, termHole) - new ExprImpl(tree, hash, SpliceScope.getCurrent).asInstanceOf[scala.quoted.Expr[T]] + new ExprImpl(tree, SpliceScope.getCurrent).asInstanceOf[scala.quoted.Expr[T]] 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] = val tree = PickledQuotes.unpickleTypeTree(pickled, typeHole, termHole) - new TypeImpl(tree, hash, SpliceScope.getCurrent).asInstanceOf[scala.quoted.Type[T]] + new TypeImpl(tree, SpliceScope.getCurrent).asInstanceOf[scala.quoted.Type[T]] object ExprMatch extends ExprMatchModule: def unapply[TypeBindings <: Tuple, Tup <: Tuple](scrutinee: scala.quoted.Expr[Any])(using pattern: scala.quoted.Expr[Any]): Option[Tup] = @@ -2948,7 +2938,4 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler } } - private[this] val hash = QuotesImpl.scopeId(using ctx) - override def hashCode: Int = hash - end QuotesImpl diff --git a/compiler/src/scala/quoted/runtime/impl/ScopeException.scala b/compiler/src/scala/quoted/runtime/impl/ScopeException.scala index 7dc84343238e..c0507495cbc2 100644 --- a/compiler/src/scala/quoted/runtime/impl/ScopeException.scala +++ b/compiler/src/scala/quoted/runtime/impl/ScopeException.scala @@ -7,6 +7,9 @@ class ScopeException(msg: String) extends Exception(msg) object ScopeException: def checkInCorrectScope(scope: Scope, currentScope: Scope, tree: Tree, kind: String)(using Context): Unit = + if scope.root != currentScope.root then + throw new ScopeException(s"Cannot use $kind oustide of the macro splice `$${...}` or the scala.quoted.staging.run(...)` where it was defined") + val yCheck = ctx.settings.Ycheck.value(using ctx).exists(x => x == "all" || x == "macros") if yCheck && !scope.isOuterScopeOf(currentScope) then throw new ScopeException( diff --git a/compiler/src/scala/quoted/runtime/impl/SpliceScope.scala b/compiler/src/scala/quoted/runtime/impl/SpliceScope.scala index b44b1c2c5b80..6ba8e97a2c71 100644 --- a/compiler/src/scala/quoted/runtime/impl/SpliceScope.scala +++ b/compiler/src/scala/quoted/runtime/impl/SpliceScope.scala @@ -18,6 +18,9 @@ trait Scope { /** Is this is a outer scope of the given scope */ def isOuterScopeOf(scope: Scope): Boolean = this.eq(scope) || (scope.ne(NoScope) && isOuterScopeOf(scope.outer)) + /** Scope of he top level splice or staging `run` */ + def root: Scope = + if outer.eq(NoScope) then this else outer.root /** Stack of locations where scopes where evaluated */ def stack: List[String] = this.toString :: (if outer.eq(NoScope) then Nil else outer.stack) @@ -27,6 +30,7 @@ trait Scope { /** Only used for outer scope of top level splice and staging `run` */ object NoScope extends Scope: + override def root: Scope = this override def outer: Scope = throw UnsupportedOperationException("NoScope.outer") class SpliceScope(val pos: SourcePosition, override val outer: Scope) extends Scope: @@ -57,6 +61,6 @@ object SpliceScope: /** Context with an incremented quotation level. */ def getCurrent(using Context): Scope = - ctx.property(ScopeKey).getOrElse(null) + ctx.property(ScopeKey).getOrElse(NoScope) end SpliceScope diff --git a/compiler/src/scala/quoted/runtime/impl/TypeImpl.scala b/compiler/src/scala/quoted/runtime/impl/TypeImpl.scala index 9add4060f1ee..51df479e4663 100644 --- a/compiler/src/scala/quoted/runtime/impl/TypeImpl.scala +++ b/compiler/src/scala/quoted/runtime/impl/TypeImpl.scala @@ -4,24 +4,14 @@ package runtime.impl import dotty.tools.dotc.ast.tpd /** Quoted type (or kind) `T` backed by a tree */ -final class TypeImpl(val typeTree: tpd.Tree, val scopeId: Int, val scope: Scope) extends Type[?] { +final class TypeImpl(val typeTree: tpd.Tree, val scope: Scope) extends Type[?] { override def equals(that: Any): Boolean = that match { case that: TypeImpl => typeTree == - // TastyTreeExpr are wrappers around trees, therfore they are equals if their trees are equal. - // All scopeId should be equal unless two different runs of the compiler created the trees. - that.typeTree && scopeId == that.scopeId + // TastyTreeExpr are wrappers around trees, therefore they are equals if their trees are equal. + // All scope should be equal unless two different runs of the compiler created the trees. + that.typeTree && scope == that.scope case _ => false } - /** View this expression `q.Type[T]` as a `TypeTree` */ - def unseal(using q: Quotes): q.reflect.TypeTree = - checkScopeId(q.hashCode) - typeTree.asInstanceOf[q.reflect.TypeTree] - - def checkScopeId(expectedScopeId: Int): Unit = - if expectedScopeId != scopeId then - throw new ScopeException("Cannot call `scala.quoted.staging.run(...)` within a macro or another `run(...)`") - - override def hashCode: Int = typeTree.hashCode override def toString: String = "Type.of[...]" } From 7e2e6d31030bba0b3f2fd526eef4733375283bf7 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Mon, 1 Mar 2021 14:27:58 +0100 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Fengyun Liu --- compiler/src/scala/quoted/runtime/impl/SpliceScope.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/scala/quoted/runtime/impl/SpliceScope.scala b/compiler/src/scala/quoted/runtime/impl/SpliceScope.scala index 6ba8e97a2c71..797b38be2743 100644 --- a/compiler/src/scala/quoted/runtime/impl/SpliceScope.scala +++ b/compiler/src/scala/quoted/runtime/impl/SpliceScope.scala @@ -5,20 +5,20 @@ import dotty.tools.dotc.core.Contexts._ import dotty.tools.dotc.util.Property import dotty.tools.dotc.util.SourcePosition -/** Unique identifier of the evaluation of a splice. +/** A scope uniquely identifies the context for evaluating a splice * - * A nested splice gets a new Scope with the outer one as an owner. + * A nested splice gets a new scope with the enclosing scope as its `outer`. * This also applies for recursive splices. */ trait Scope { /** Outer scope that was used to create the quote containing this splice. - * NoScope if there is no scope + * NoScope otherwise. */ def outer: Scope = NoScope /** Is this is a outer scope of the given scope */ def isOuterScopeOf(scope: Scope): Boolean = this.eq(scope) || (scope.ne(NoScope) && isOuterScopeOf(scope.outer)) - /** Scope of he top level splice or staging `run` */ + /** Scope of the top level splice or staging `run` */ def root: Scope = if outer.eq(NoScope) then this else outer.root /** Stack of locations where scopes where evaluated */