Skip to content

Commit 7ca5a2c

Browse files
Merge pull request #4457 from dotty-staging/fix-#4456
Fix #4456: Pickle quote before compiling
2 parents c38a6b7 + 68e32ab commit 7ca5a2c

File tree

15 files changed

+131
-10
lines changed

15 files changed

+131
-10
lines changed

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,26 @@ import scala.reflect.ClassTag
2222
object PickledQuotes {
2323
import tpd._
2424

25-
/** Pickle the quote into strings */
25+
/** Pickle the tree of the quoted.Expr */
26+
def pickleExpr(tree: Tree)(implicit ctx: Context): scala.quoted.Expr[Any] = {
27+
// Check that there are no free variables
28+
new TreeTraverser {
29+
private val definedHere = scala.collection.mutable.Set.empty[Symbol]
30+
def traverse(tree: tpd.Tree)(implicit ctx: Context): Unit = tree match {
31+
case tree: Ident if tree.symbol.exists && !definedHere(tree.symbol) =>
32+
throw new scala.quoted.FreeVariableError(tree.name.toString)
33+
case tree: DefTree =>
34+
definedHere += tree.symbol
35+
traverseChildren(tree)
36+
case _ =>
37+
traverseChildren(tree)
38+
}
39+
}.traverse(tree)
40+
val pickled = pickleQuote(tree)
41+
scala.runtime.quoted.Unpickler.unpickleExpr(pickled, Nil)
42+
}
43+
44+
/** Pickle the tree of the quote into strings */
2645
def pickleQuote(tree: Tree)(implicit ctx: Context): scala.runtime.quoted.Unpickler.Pickled = {
2746
if (ctx.reporter.hasErrors) Nil
2847
else {

compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import scala.collection.{ mutable, immutable }
1919
import config.Printers.pickling
2020
import typer.Checking
2121
import config.Config
22-
import dotty.tools.dotc.core.quoted.PickledQuotes
22+
import core.quoted.PickledQuotes
2323
import scala.quoted
2424
import scala.quoted.Types.TreeType
2525
import scala.quoted.Exprs.TreeExpr
@@ -1142,7 +1142,7 @@ class TreeUnpickler(reader: TastyReader,
11421142
val idx = readNat()
11431143
val args = until(end)(readTerm())
11441144
val splice = splices(idx)
1145-
val reifiedArgs = args.map(arg => if (arg.isTerm) new TreeExpr(arg) else new TreeType(arg))
1145+
val reifiedArgs = args.map(arg => if (arg.isTerm) new TreeExpr(arg, PickledQuotes.pickleExpr(arg)) else new TreeType(arg))
11461146
if (isType) {
11471147
val quotedType = splice.asInstanceOf[Seq[Any] => quoted.Type[_]](reifiedArgs)
11481148
PickledQuotes.quotedTypeToTree(quotedType)

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package dotty.tools.dotc.quoted
33
import dotty.tools.dotc.ast.Trees._
44
import dotty.tools.dotc.ast.tpd
55
import dotty.tools.dotc.core.Constants._
6+
import dotty.tools.dotc.core.Contexts.Context
7+
import dotty.tools.dotc.core.quoted.PickledQuotes
68
import dotty.tools.dotc.printing.RefinedPrinter
79

810
import scala.quoted.Expr
911
import scala.runtime.BoxedUnit
10-
import scala.quoted.Exprs.LiftedExpr
12+
import scala.quoted.Exprs.{LiftedExpr, TreeExpr}
1113
import scala.runtime.quoted._
1214

1315
/** Default runners for quoted expressions */
@@ -23,8 +25,12 @@ object Toolbox {
2325
): Toolbox[T] = new Toolbox[T] {
2426

2527
def run(expr: Expr[T]): T = expr match {
26-
case expr: LiftedExpr[T] => expr.value
27-
case _ => new QuoteDriver().run(expr, runSettings)
28+
case expr: LiftedExpr[T] =>
29+
expr.value
30+
case expr: TreeExpr[Tree] @unchecked =>
31+
new QuoteDriver().run(expr.pickled, runSettings)
32+
case _ =>
33+
new QuoteDriver().run(expr, runSettings)
2834
}
2935

3036
def show(expr: Expr[T]): String = expr match {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,8 @@ class ReifyQuotes extends MacroTransformWithImplicits with InfoTransformer {
569569
// see PostTyper `case Inlined(...) =>` for description of the simplification
570570
val call2 = Ident(call.symbol.topLevelClass.typeRef).withPos(call.pos)
571571
val spliced = Splicer.splice(body, call, bindings, tree.pos, macroClassLoader).withPos(tree.pos)
572-
transform(cpy.Inlined(tree)(call2, bindings, spliced))
572+
if (ctx.reporter.hasErrors) EmptyTree
573+
else transform(cpy.Inlined(tree)(call2, bindings, spliced))
573574
}
574575
else super.transform(tree)
575576

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ object Splicer {
6060
case (arg, tp) =>
6161
assert(!tp.hasAnnotation(defn.InlineParamAnnot))
6262
// Replace argument by its binding
63-
new scala.quoted.Exprs.TreeExpr(bindMap.getOrElse(arg, arg))
63+
val arg1 = bindMap.getOrElse(arg, arg)
64+
new scala.quoted.Exprs.TreeExpr(arg1, PickledQuotes.pickleExpr(arg1))
6465
}
6566
args1 ::: liftArgs(tp.resType, args.tail)
6667
case tp: PolyType =>

library/src/scala/quoted/Expr.scala

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ import scala.runtime.quoted.Unpickler.Pickled
55

66
sealed abstract class Expr[T] {
77
final def unary_~ : T = throw new Error("~ should have been compiled away")
8+
9+
/** Evaluate the contents of this expression and return the result.
10+
*
11+
* May throw a FreeVariableError on expressions that came from an inline macro.
12+
*/
813
final def run(implicit toolbox: Toolbox[T]): T = toolbox.run(this)
14+
15+
/** Show a source code like representation of this expression */
916
final def show(implicit toolbox: Toolbox[T]): String = toolbox.show(this)
1017
}
1118

@@ -36,8 +43,15 @@ object Exprs {
3643
override def toString: String = s"Expr($value)"
3744
}
3845

39-
/** An Expr backed by a tree. Only the current compiler trees are allowed. */
40-
final class TreeExpr[Tree](val tree: Tree) extends quoted.Expr[Any] {
46+
/** An Expr backed by a tree. Only the current compiler trees are allowed.
47+
*
48+
* These expressions are used for arguments of inline macros. They contain and actual tree
49+
* from the program that is being expanded by the macro.
50+
*
51+
* May contain references to code defined outside this Expr instance.
52+
*/
53+
final class TreeExpr[Tree](val tree: Tree, pickle: => Expr[_]) extends quoted.Expr[Any] {
54+
def pickled[T]: Expr[T] = pickle.asInstanceOf[Expr[T]]
4155
override def toString: String = s"Expr(<raw>)"
4256
}
4357

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package scala.quoted
2+
3+
/** Exception is thrown when trying to evaluate the contents of an expression
4+
* that has free variables (i.e. has some local references to code that is
5+
* outside of the expression). This can happen on Expr that passed as arguments to
6+
* an inline macro.
7+
*/
8+
class FreeVariableError(val name: String) extends QuoteError("Free variable " + name + " could not be handled")
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import scala.quoted._
2+
3+
import dotty.tools.dotc.quoted.Toolbox._
4+
5+
object Macros {
6+
inline def foo(i: => Int): Int = ~{
7+
val y: Int = ('(i)).run
8+
y.toExpr
9+
}
10+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Macros._
2+
object Test {
3+
def main(args: Array[String]): Unit = {
4+
val x = 3
5+
println(foo(x)) // error
6+
}
7+
}

tests/run/quote-run-in-macro-1.check

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
1
2+
4
3+
5
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import scala.quoted._
2+
3+
import dotty.tools.dotc.quoted.Toolbox._
4+
5+
object Macros {
6+
inline def foo(i: => Int): Int = ~{
7+
val y: Int = ('(i)).run
8+
y.toExpr
9+
}
10+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Macros._
2+
object Test {
3+
def main(args: Array[String]): Unit = {
4+
println(foo(1))
5+
println(foo(1 + 3))
6+
val x = 3
7+
println(foo {
8+
val x = 5
9+
x
10+
})
11+
}
12+
}

tests/run/quote-run-in-macro-2.check

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"> 3"
2+
"> 12"
3+
(x: scala.Int) => "> ".+(y).apply(3)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import scala.quoted._
2+
3+
import dotty.tools.dotc.quoted.Toolbox._
4+
5+
object Macros {
6+
inline def foo(f: => Int => String): String = ~bar('(f))
7+
def bar(f: Expr[Int => String]): Expr[String] = {
8+
try {
9+
val y: Int => String = f.run
10+
val res = y(3).toExpr // evaluate at compile time
11+
res.show.toExpr
12+
} catch {
13+
case ex: scala.quoted.FreeVariableError =>
14+
val res = ('((~f)(3))) // evaluate at run time
15+
res.show.toExpr
16+
}
17+
}
18+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Macros._
2+
object Test {
3+
def main(args: Array[String]): Unit = {
4+
println(foo(x => "> " + x))
5+
println(foo(x => "> " + 4 * x))
6+
val y = 9
7+
println(foo(x => "> " + y))
8+
}
9+
}

0 commit comments

Comments
 (0)