diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 19dab5bac91a..84c89f727255 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1047,7 +1047,7 @@ class Definitions { // private val unboxedTypeRef = mutable.Map[TypeName, TypeRef]() // private val javaTypeToValueTypeRef = mutable.Map[Class[_], TypeRef]() -// private val valueTypeNameToJavaType = mutable.Map[TypeName, Class[_]]() + private val valueTypeNamesToJavaType = mutable.Map[TypeName, Class[_]]() private def valueTypeRef(name: String, boxed: TypeRef, jtype: Class[_], enc: Int, tag: Name): TypeRef = { val vcls = ctx.requiredClassRef(name) @@ -1056,7 +1056,7 @@ class Definitions { typeTags(vcls.name) = tag // unboxedTypeRef(boxed.name) = vcls // javaTypeToValueTypeRef(jtype) = vcls -// valueTypeNameToJavaType(vcls.name) = jtype + valueTypeNamesToJavaType(vcls.name) = jtype vcls } @@ -1066,6 +1066,10 @@ class Definitions { /** The JVM tag for `tp` if it's a primitive, `java.lang.Object` otherwise. */ def typeTag(tp: Type)(implicit ctx: Context): Name = typeTags(scalaClassName(tp)) + /** The `Class[_]` of a primitive value type name */ + def valueTypeNameToJavaType(name: TypeName)(implicit ctx: Context): Option[Class[_]] = + valueTypeNamesToJavaType.get(if (name.firstPart eq nme.scala_) name.lastPart.toTypeName else name) + type PrimitiveClassEnc = Int val ByteEnc = 2 diff --git a/compiler/src/dotty/tools/dotc/core/quoted/PickledQuotes.scala b/compiler/src/dotty/tools/dotc/core/quoted/PickledQuotes.scala new file mode 100644 index 000000000000..293c0c843426 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/core/quoted/PickledQuotes.scala @@ -0,0 +1,97 @@ +package dotty.tools.dotc.core.quoted + +import dotty.tools.dotc.ast.Trees._ +import dotty.tools.dotc.ast.{tpd, untpd} +import dotty.tools.dotc.config.Printers._ +import dotty.tools.dotc.core.Constants.Constant +import dotty.tools.dotc.core.Contexts._ +import dotty.tools.dotc.core.Decorators._ +import dotty.tools.dotc.core.Flags._ +import dotty.tools.dotc.core.Symbols._ +import dotty.tools.dotc.core.tasty.{TastyPickler, TastyPrinter, TastyString} +import dotty.tools.dotc.interpreter.RawQuoted + +object PickledQuotes { + import tpd._ + + /** Pickle the quote into a TASTY string */ + def pickleQuote(tree: Tree)(implicit ctx: Context): String = { + if (ctx.reporter.hasErrors) "" + else { + val encapsulated = encapsulateQuote(tree) + val pickled = pickle(encapsulated) + TastyString.tastyToString(pickled) + } + } + + /** Transform the expression into its fully spliced Tree */ + def quotedToTree(expr: quoted.Quoted)(implicit ctx: Context): Tree = expr match { + case expr: quoted.TastyQuoted => unpickleQuote(expr) + case expr: quoted.Liftable.ConstantExpr[_] => Literal(Constant(expr.value)) + case expr: RawQuoted => expr.tree + } + + /** Unpickle the tree contained in the TastyQuoted */ + private def unpickleQuote(expr: quoted.TastyQuoted)(implicit ctx: Context): Tree = { + val tastyBytes = TastyString.stringToTasty(expr.tasty) + val unpickled = unpickle(tastyBytes, expr.args) + unpickled match { + case PackageDef(_, (vdef: ValDef) :: Nil) => vdef.rhs + case PackageDef(_, (tdef: TypeDef) :: Nil) => tdef.rhs + } + } + + /** Encapsulate the tree in a top level `val` or `type` + * `` ==> `package _root_ { val ': Any = }` + * or + * `` ==> `package _root_ { type ' = }` + */ + private def encapsulateQuote(tree: Tree)(implicit ctx: Context): Tree = { + def encapsulatedTerm = { + val sym = ctx.newSymbol(ctx.owner, "'".toTermName, Synthetic, defn.AnyType, coord = tree.pos) + ValDef(sym, tree).withPos(tree.pos) + } + + def encapsulatedType = + untpd.TypeDef("'".toTypeName, tree).withPos(tree.pos).withType(defn.AnyType) + + val quoted = if (tree.isTerm) encapsulatedTerm else encapsulatedType + PackageDef(ref(defn.RootPackage).asInstanceOf[Ident], quoted :: Nil).withPos(tree.pos) + } + + // TASTY picklingtests/pos/quoteTest.scala + + /** Pickle tree into it's TASTY bytes s*/ + private def pickle(tree: Tree)(implicit ctx: Context): Array[Byte] = { + val pickler = new TastyPickler(defn.RootClass) + val treePkl = pickler.treePkl + treePkl.pickle(tree :: Nil) + treePkl.compactify() + pickler.addrOfTree = treePkl.buf.addrOfTree + pickler.addrOfSym = treePkl.addrOfSym + // if (tree.pos.exists) + // new PositionPickler(pickler, treePkl.buf.addrOfTree).picklePositions(tree :: Nil) + + // other pickle sections go here. + val pickled = pickler.assembleParts() + + if (pickling ne noPrinter) { + println(i"**** pickled quote of \n${tree.show}") + new TastyPrinter(pickled).printContents() + } + + pickled + } + + /** Unpickle TASTY bytes into it's tree */ + private def unpickle(bytes: Array[Byte], splices: Seq[Any])(implicit ctx: Context): Tree = { + val unpickler = new TastyUnpickler(bytes, splices) + unpickler.enter(roots = Set(defn.RootPackage)) + val tree = unpickler.body.head + if (pickling ne noPrinter) { + println(i"**** unpickled quote for \n${tree.show}") + new TastyPrinter(bytes).printContents() + } + tree + } +} diff --git a/compiler/src/dotty/tools/dotc/core/quoted/Quoted.scala b/compiler/src/dotty/tools/dotc/core/quoted/Quoted.scala new file mode 100644 index 000000000000..db2b6ecf50ff --- /dev/null +++ b/compiler/src/dotty/tools/dotc/core/quoted/Quoted.scala @@ -0,0 +1,20 @@ +package dotty.tools.dotc.core.quoted + +import dotty.tools.dotc.ast.Trees.GenericApply +import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Types.Type +import dotty.tools.dotc.transform.SymUtils._ + +/** Extractors for quotes */ +object Quoted { + + /** Extracts the content of a quoted tree. + * The result can be the contents of a term ot type quote, which + * will return a term or type tree respectively. + */ + def unapply(tree: tpd.Tree)(implicit ctx: Context): Option[tpd.Tree] = tree match { + case tree: GenericApply[Type] if tree.symbol.isQuote => Some(tree.args.head) + case _ => None + } +} diff --git a/compiler/src/dotty/tools/dotc/core/quoted/TastyUnpickler.scala b/compiler/src/dotty/tools/dotc/core/quoted/TastyUnpickler.scala new file mode 100644 index 000000000000..d47294ad7d76 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/core/quoted/TastyUnpickler.scala @@ -0,0 +1,24 @@ +package dotty.tools.dotc.core.quoted + +import dotty.tools.dotc.core.tasty._ +import dotty.tools.dotc.core.tasty.TastyUnpickler.NameTable + +object TastyUnpickler { + class QuotedTreeSectionUnpickler(posUnpickler: Option[PositionUnpickler], splices: Seq[Any]) + extends DottyUnpickler.TreeSectionUnpickler(posUnpickler) { + override def unpickle(reader: TastyReader, nameAtRef: NameTable) = + new TreeUnpickler(reader, nameAtRef, posUnpickler, splices) + } +} + +/** A class for unpickling quoted Tasty trees and symbols. + * @param bytes the bytearray containing the Tasty file from which we unpickle + * @param splices splices that will fill the holes in the quote + */ +class TastyUnpickler(bytes: Array[Byte], splices: Seq[Any]) extends DottyUnpickler(bytes) { + import DottyUnpickler._ + import TastyUnpickler._ + + protected override def treeSectionUnpickler(posUnpicklerOpt: Option[PositionUnpickler]): TreeSectionUnpickler = + new QuotedTreeSectionUnpickler(posUnpicklerOpt, splices) +} diff --git a/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala index a4847e644765..94ee7897a90e 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala @@ -38,7 +38,7 @@ class DottyUnpickler(bytes: Array[Byte]) extends ClassfileParser.Embedded { val unpickler = new TastyUnpickler(bytes) private val posUnpicklerOpt = unpickler.unpickle(new PositionsSectionUnpickler) - private val treeUnpickler = unpickler.unpickle(new TreeSectionUnpickler(posUnpicklerOpt)).get + private val treeUnpickler = unpickler.unpickle(treeSectionUnpickler(posUnpicklerOpt)).get /** Enter all toplevel classes and objects into their scopes * @param roots a set of SymDenotations that should be overwritten by unpickling @@ -46,6 +46,10 @@ class DottyUnpickler(bytes: Array[Byte]) extends ClassfileParser.Embedded { def enter(roots: Set[SymDenotation])(implicit ctx: Context): Unit = treeUnpickler.enterTopLevel(roots) + protected def treeSectionUnpickler(posUnpicklerOpt: Option[PositionUnpickler]): TreeSectionUnpickler = { + new TreeSectionUnpickler(posUnpicklerOpt) + } + /** Only used if `-Yretain-trees` is set. */ private[this] var myBody: List[Tree] = _ /** The unpickled trees, and the source file they come from. */ diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala index bde686b1522e..1d0ab3ac40e9 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala @@ -67,7 +67,7 @@ class TastyPrinter(bytes: Array[Byte])(implicit ctx: Context) { printName(); printTrees() case REFINEDtype => printName(); printTree(); printTrees() - case RETURN => + case RETURN | HOLE => printNat(); printTrees() case METHODtype | POLYtype | TYPELAMBDAtype => printTree() diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyString.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyString.scala new file mode 100644 index 000000000000..57c3a1e2955a --- /dev/null +++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyString.scala @@ -0,0 +1,20 @@ +package dotty.tools.dotc.core.tasty + +/** Utils for String representation of TASTY */ +object TastyString { + + /** Decode the TASTY String into TASTY bytes */ + def stringToTasty(str: String): Array[Byte] = { + val bytes = new Array[Byte](str.length) + for (i <- str.indices) bytes(i) = str.charAt(i).toByte + bytes + } + + /** Encode TASTY bytes into a TASTY String */ + def tastyToString(bytes: Array[Byte]): String = { + val chars = new Array[Char](bytes.length) + for (i <- bytes.indices) chars(i) = (bytes(i) & 0xff).toChar + new String(chars) + } + +} diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index 87c3cbb554b7..e7babd50ca31 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -300,7 +300,7 @@ class TreePickler(pickler: TastyPickler) { pickleName(sym.name) pickleParams tpt match { - case templ: Template => pickleTree(tpt) + case _: Template | _: Hole => pickleTree(tpt) case _ if tpt.isType => pickleTpt(tpt) } pickleTreeUnlessEmpty(rhs) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 51b33d90fc0f..f07f1204ed18 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -19,6 +19,9 @@ import scala.collection.{ mutable, immutable } import config.Printers.pickling import typer.Checking import config.Config +import dotty.tools.dotc.core.quoted.PickledQuotes +import dotty.tools.dotc.interpreter.RawQuoted +import scala.quoted.Expr /** Unpickler for typed trees * @param reader the reader from which to unpickle @@ -1030,8 +1033,10 @@ class TreeUnpickler(reader: TastyReader, val idx = readNat() val args = until(end)(readTerm()) val splice = splices(idx) - if (args.isEmpty) splice.asInstanceOf[Tree] - else splice.asInstanceOf[Seq[Any] => Tree](args) + val expr = + if (args.isEmpty) splice.asInstanceOf[Expr[_]] + else splice.asInstanceOf[Seq[Any] => Expr[_]](args.map(RawQuoted.apply)) + PickledQuotes.quotedToTree(expr) case _ => readPathTerm() } diff --git a/compiler/src/dotty/tools/dotc/interpreter/Interpreter.scala b/compiler/src/dotty/tools/dotc/interpreter/Interpreter.scala new file mode 100644 index 000000000000..b00552f9511c --- /dev/null +++ b/compiler/src/dotty/tools/dotc/interpreter/Interpreter.scala @@ -0,0 +1,186 @@ +package dotty.tools.dotc +package interpreter + +import java.io.{PrintWriter, StringWriter} + +import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.ast.Trees._ +import dotty.tools.dotc.core.Constants._ +import dotty.tools.dotc.core.Contexts._ +import dotty.tools.dotc.core.Decorators._ +import dotty.tools.dotc.core.Names._ +import dotty.tools.dotc.core.Symbols._ +import dotty.tools.dotc.core.quoted.Quoted +import dotty.tools.dotc.util.Positions.Position + +import scala.reflect.ClassTag +import java.net.URLClassLoader +import java.lang.reflect.Constructor +import java.lang.reflect.Method + +/** Tree interpreter that can interpret + * * Literal constants + * * Calls to static methods + * * New objects with explicit `new` keyword + * * Quoted code + * + * The interpreter assumes that all calls in the trees are to code that was + * previously compiled and is present in the classpath of the current context. + */ +class Interpreter(implicit ctx: Context) { + import tpd._ + + type Env = Map[Symbol, Object] + + private[this] val classLoader = { + val urls = ctx.settings.classpath.value.split(':').map(cp => java.nio.file.Paths.get(cp).toUri.toURL) + new URLClassLoader(urls, getClass.getClassLoader) + } + + /** Returns the interpreted result of interpreting the code represented by the tree. + * Return Some of the result or None if some error happen during the interpretation. + */ + def interpretTree[T](tree: Tree)(implicit ct: ClassTag[T]): Option[T] = { + try { + interpretTreeImpl(tree, Map.empty) match { + case obj: T => Some(obj) + case obj => + // TODO upgrade to a full type tag check or something similar + ctx.error(s"Interpreted tree returned a result of an unexpected type. Expected ${ct.runtimeClass} but was ${obj.getClass}", tree.pos) + None + } + } catch { + case ex: StopInterpretation => + ctx.error(ex.msg, ex.pos) + None + } + } + + /** Returns the interpreted result of interpreting the code represented by the tree. + * Returns the result of the interpreted tree. + * + * If some error is encountered while interpreting a ctx.error is emitted and a StopInterpretation is thrown. + */ + private def interpretTreeImpl(tree: Tree, env: Env): Object = { + ctx.debuglog( + s"""Interpreting: + |${tree.show} + |$env + """.stripMargin) + + implicit val pos: Position = tree.pos + + tree match { + case Quoted(quotedTree) => RawQuoted(quotedTree) + + case Literal(Constant(c)) => c.asInstanceOf[Object] + + case Apply(fn, args) if fn.symbol.isConstructor => + val clazz = loadClass(fn.symbol.owner.symbol.fullName) + val paramClasses = paramsSig(fn.symbol) + val interpretedArgs = args.map(arg => interpretTreeImpl(arg, env)) + val constructor = getConstructor(clazz, paramClasses) + interpreted(constructor.newInstance(interpretedArgs: _*)) + + case _: RefTree | _: Apply if tree.symbol.isStatic => + val clazz = loadClass(tree.symbol.owner.companionModule.fullName) + val paramClasses = paramsSig(tree.symbol) + val interpretedArgs = Array.newBuilder[Object] + def interpretArgs(tree: Tree): Unit = tree match { + case Apply(fn, args) => + interpretArgs(fn) + args.foreach(arg => interpretedArgs += interpretTreeImpl(arg, env)) + case _ => + } + interpretArgs(tree) + + val method = getMethod(clazz, tree.symbol.name, paramClasses) + interpreted(method.invoke(null, interpretedArgs.result(): _*)) + + case tree: Ident if env.contains(tree.symbol) => + env(tree.symbol) + + case Block(stats, expr) => + val env2 = stats.foldLeft(env)((acc, x) => interpretStat(x, acc)) + interpretTreeImpl(expr, env2) + + case tree: NamedArg => + interpretTreeImpl(tree.arg, env) + + case Inlined(_, bindings, expansion) => + val env2 = bindings.foldLeft(env)((acc, x) => interpretStat(x, acc)) + interpretTreeImpl(expansion, env2) + + case _ => + // TODO Add more precise descriptions of why it could not be interpreted. + // This should be done after the full interpreter is implemented. + throw new StopInterpretation(s"Could not interpret ${tree.show}\n${tree}", tree.pos) + } + } + + /** Interprets the statement and returns the updated environment */ + private def interpretStat(stat: Tree, env: Env): Env = stat match { + case tree: ValDef => + val obj = interpretTreeImpl(tree.rhs, env) + env.updated(tree.symbol, obj) + + case _ => + interpretTreeImpl(stat, env) + env + } + + private def loadClass(name: Name)(implicit pos: Position): Class[_] = { + try classLoader.loadClass(name.toString) + catch { + case _: ClassNotFoundException => + val msg = s"Could not find interpreted class $name in classpath" + throw new StopInterpretation(msg, pos) + } + } + + private def getMethod(clazz: Class[_], name: Name, paramClasses: List[Class[_]])(implicit pos: Position): Method = { + try clazz.getMethod(name.toString, paramClasses: _*) + catch { + case _: NoSuchMethodException => + val msg = s"Could not find interpreted method ${clazz.getCanonicalName}.$name with parameters $paramClasses" + throw new StopInterpretation(msg, pos) + } + } + + private def getConstructor(clazz: Class[_], paramClasses: List[Class[_]])(implicit pos: Position): Constructor[Object] = { + try clazz.getConstructor(paramClasses: _*).asInstanceOf[Constructor[Object]] + catch { + case _: NoSuchMethodException => + val msg = s"Could not find interpreted constructor of ${clazz.getCanonicalName} with parameters $paramClasses" + throw new StopInterpretation(msg, pos) + } + } + + private def interpreted[T](thunk: => T)(implicit pos: Position): T = { + try thunk + catch { + case ex: RuntimeException => + val sw = new StringWriter() + sw.write("A runtime exception occurred while interpreting\n") + sw.write(ex.getMessage) + sw.write("\n") + ex.printStackTrace(new PrintWriter(sw)) + sw.write("\n") + throw new StopInterpretation(sw.toString, pos) + } + } + + /** List of classes of the parameters of the signature of `sym` */ + private def paramsSig(sym: Symbol): List[Class[_]] = { + sym.signature.paramsSig.map { param => + defn.valueTypeNameToJavaType(param) match { + case Some(clazz) => clazz + case None => classLoader.loadClass(param.toString) + } + } + } + + /** Exception that stops interpretation if some issue is found */ + private class StopInterpretation(val msg: String, val pos: Position) extends Exception + +} diff --git a/compiler/src/dotty/tools/dotc/interpreter/RawQuoted.scala b/compiler/src/dotty/tools/dotc/interpreter/RawQuoted.scala new file mode 100644 index 000000000000..db669d0c5f5e --- /dev/null +++ b/compiler/src/dotty/tools/dotc/interpreter/RawQuoted.scala @@ -0,0 +1,22 @@ +package dotty.tools.dotc.interpreter + +import dotty.tools.dotc.ast.tpd + +/** Quoted `quoted.Quoted` for which its internal representation is its tree. + * - Used for trees that cannot be serialized, such as references to local symbols that will be spliced in. + * - Used for trees that do not need to be serialized to avoid the overhead of serialization/deserialization. + */ +trait RawQuoted extends quoted.Quoted { + def tree: tpd.Tree + override def toString: String = s"${this.getClass.getName}(${tree.toString})" +} + +object RawQuoted { + + def apply(tree: tpd.Tree): RawQuoted = + if (tree.isTerm) new RawExpr(tree) + else new RawType(tree) + + private final class RawExpr(val tree: tpd.Tree) extends quoted.Expr[Any] with RawQuoted + private final class RawType(val tree: tpd.Tree) extends quoted.Type[Any] with RawQuoted +} diff --git a/compiler/src/dotty/tools/dotc/transform/ReifyQuotes.scala b/compiler/src/dotty/tools/dotc/transform/ReifyQuotes.scala index 49decaf67949..202db64662b8 100644 --- a/compiler/src/dotty/tools/dotc/transform/ReifyQuotes.scala +++ b/compiler/src/dotty/tools/dotc/transform/ReifyQuotes.scala @@ -13,7 +13,11 @@ import tasty.TreePickler.Hole import MegaPhase.MiniPhase import SymUtils._ import NameKinds.OuterSelectName + import scala.collection.mutable +import dotty.tools.dotc.core.StdNames._ +import dotty.tools.dotc.core.quoted._ + /** Translates quoted terms and types to `unpickle` method calls. * Checks that the phase consistency principle (PCP) holds. @@ -29,17 +33,6 @@ class ReifyQuotes extends MacroTransform { protected def newTransformer(implicit ctx: Context): Transformer = new Reifier(inQuote = false, null, 0, new LevelInfo) - /** Serialize `tree`. Embedded splices are represented as nodes of the form - * - * Select(qual, sym) - * - * where `sym` is either `defn.QuotedExpr_~` or `defn.QuotedType_~`. For any splice, - * the `qual` part should not be pickled, since it will be added separately later - * as a splice. - */ - def pickleTree(tree: Tree, isType: Boolean)(implicit ctx: Context): String = - tree.show // TODO: replace with TASTY - private class LevelInfo { /** A map from locally defined symbols to the staging levels of their definitions */ val levelOf = new mutable.HashMap[Symbol, Int] @@ -276,7 +269,7 @@ class ReifyQuotes extends MacroTransform { ref(if (isType) defn.Unpickler_unpickleType else defn.Unpickler_unpickleExpr) .appliedToType(if (isType) body1.tpe else body1.tpe.widen) .appliedTo( - Literal(Constant(pickleTree(body1, isType))), + Literal(Constant(PickledQuotes.pickleQuote(body1))), SeqLiteral(splices, TypeTree(defn.AnyType))) } }.withPos(quote.pos) @@ -323,10 +316,8 @@ class ReifyQuotes extends MacroTransform { enteredSyms = enteredSyms.tail } tree match { - case Apply(fn, arg :: Nil) if fn.symbol == defn.quoteMethod => - quotation(arg, tree) - case TypeApply(fn, arg :: Nil) if fn.symbol == defn.typeQuoteMethod => - quotation(arg, tree) + case Quoted(quotedTree) => + quotation(quotedTree, tree) case Select(body, _) if tree.symbol.isSplice => splice(body, tree) case Block(stats, _) => @@ -336,7 +327,8 @@ class ReifyQuotes extends MacroTransform { case Inlined(call, bindings, expansion @ Select(body, name)) if expansion.symbol.isSplice => // To maintain phase consistency, convert inlined expressions of the form // `{ bindings; ~expansion }` to `~{ bindings; expansion }` - transform(cpy.Select(expansion)(cpy.Inlined(tree)(call, bindings, body), name)) + if (level == 0) transform(Splicer.splice(cpy.Inlined(tree)(call, bindings, body))) + else transform(cpy.Select(expansion)(cpy.Inlined(tree)(call, bindings, body), name)) case _: Import => tree case tree: DefDef if tree.symbol.is(Macro) && level == 0 => diff --git a/compiler/src/dotty/tools/dotc/transform/Splicer.scala b/compiler/src/dotty/tools/dotc/transform/Splicer.scala new file mode 100644 index 000000000000..271c807a8ec1 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/Splicer.scala @@ -0,0 +1,30 @@ +package dotty.tools.dotc +package transform + +import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.core.Contexts._ +import dotty.tools.dotc.core.quoted._ +import dotty.tools.dotc.interpreter._ + +/** Utility class to slice quoted expressions */ +object Splicer { + import tpd._ + + /** Splice the Tree for a Quoted expression. `~'(xyz)` becomes `xyz` + * and for `~xyz` the tree of `xyz` is interpreted for which the + * resulting expression is return as a `Tree` + */ + def splice(tree: Tree)(implicit ctx: Context): Tree = tree match { + case Quoted(quotedTree) => quotedTree + case tree: RefTree => reflectiveSplice(tree) + case tree: Apply => reflectiveSplice(tree) + case tree: Inlined => reflectiveSplice(tree) + } + + /** Splice the Tree for a Quoted expression which is constructed via a reflective call to the given method */ + private def reflectiveSplice(tree: Tree)(implicit ctx: Context): Tree = { + val interpreter = new Interpreter + interpreter.interpretTree[quoted.Expr[_]](tree).map(PickledQuotes.quotedToTree(_)).getOrElse(tree) + } + +} diff --git a/compiler/test/dotty/tools/dotc/CompilerTest.scala b/compiler/test/dotty/tools/dotc/CompilerTest.scala index 259e1487c1a7..2a3b3253742e 100644 --- a/compiler/test/dotty/tools/dotc/CompilerTest.scala +++ b/compiler/test/dotty/tools/dotc/CompilerTest.scala @@ -109,8 +109,12 @@ abstract class CompilerTest { if (runTest) log(s"WARNING: run tests can only be run by partest, JUnit just verifies compilation: $prefix$dirName") val (filePaths, javaFilePaths, normArgs, expErrors) = computeFilePathsAndExpErrors - compileWithJavac(javaFilePaths, Array.empty) // javac needs to run first on dotty-library - compileArgs(javaFilePaths ++ filePaths ++ normArgs, expErrors) + if (filePaths.exists(_.endsWith("_1.scala"))) { + log(s"WARNING: separate compilation test can not be run as legacy test: $prefix$dirName") + } else { + compileWithJavac(javaFilePaths, Array.empty) // javac needs to run first on dotty-library + compileArgs(javaFilePaths ++ filePaths ++ normArgs, expErrors) + } } def runDir(prefix: String, dirName: String, args: List[String] = Nil) (implicit defaultOptions: List[String]): Unit = diff --git a/compiler/test/dotty/tools/dotc/FromTastyTests.scala b/compiler/test/dotty/tools/dotc/FromTastyTests.scala index d08789d72a92..bf1f58b760f7 100644 --- a/compiler/test/dotty/tools/dotc/FromTastyTests.scala +++ b/compiler/test/dotty/tools/dotc/FromTastyTests.scala @@ -38,10 +38,10 @@ class FromTastyTests extends ParallelTesting { "i3000.scala", "i536.scala", "i974.scala", - "liftable.scala", - "quoteTest.scala", - "quoted.scala", - "stagedInterpreter.scala", + "quote-liftable.scala", + "quote-0.scala", + "quote-1.scala", + "quote-stagedInterpreter.scala", "superacc.scala", "t0231.scala", "t1203a.scala", diff --git a/library/src/scala/quoted/Expr.scala b/library/src/scala/quoted/Expr.scala index ef6652782dc7..58241792ddc1 100644 --- a/library/src/scala/quoted/Expr.scala +++ b/library/src/scala/quoted/Expr.scala @@ -1,7 +1,7 @@ package scala.quoted -class Expr[T] extends Quoted { - def unary_~ : T = ??? +abstract class Expr[T] extends Quoted { + def unary_~ : T = throw new Error("~ should have been compiled away") def run: T = ??? } diff --git a/library/src/scala/quoted/Liftable.scala b/library/src/scala/quoted/Liftable.scala index 9e64a7c1ed8e..f9fd08ef6a74 100644 --- a/library/src/scala/quoted/Liftable.scala +++ b/library/src/scala/quoted/Liftable.scala @@ -13,6 +13,17 @@ abstract class Liftable[T] { * gives an alternative implementation using just the basic staging system. */ object Liftable { - implicit def IntIsLiftable: Liftable[Int] = ??? - implicit def BooleanIsLiftable: Liftable[Boolean] = ??? + + final class ConstantExpr[T] private[Liftable](val value: T) extends Expr[T] + + implicit def BooleanIsLiftable: Liftable[Boolean] = (x: Boolean) => new ConstantExpr(x) + implicit def ByteLiftable: Liftable[Byte] = (x: Byte) => new ConstantExpr(x) + implicit def CharIsLiftable: Liftable[Char] = (x: Char) => new ConstantExpr(x) + implicit def ShortIsLiftable: Liftable[Short] = (x: Short) => new ConstantExpr(x) + implicit def IntIsLiftable: Liftable[Int] = (x: Int) => new ConstantExpr(x) + implicit def LongIsLiftable: Liftable[Long] = (x: Long) => new ConstantExpr(x) + implicit def FloatIsLiftable: Liftable[Float] = (x: Float) => new ConstantExpr(x) + implicit def DoubleIsLiftable: Liftable[Double] = (x: Double) => new ConstantExpr(x) + + implicit def StringIsLiftable: Liftable[String] = (x: String) => new ConstantExpr(x) } diff --git a/library/src/scala/quoted/Quoted.scala b/library/src/scala/quoted/Quoted.scala index b425c0bfb874..537dd6221d51 100644 --- a/library/src/scala/quoted/Quoted.scala +++ b/library/src/scala/quoted/Quoted.scala @@ -1,6 +1,4 @@ package scala.quoted /** Common superclass of Expr and Type */ -class Quoted - - +abstract class Quoted diff --git a/library/src/scala/quoted/TastyExpr.scala b/library/src/scala/quoted/TastyExpr.scala new file mode 100644 index 000000000000..4dd5448da031 --- /dev/null +++ b/library/src/scala/quoted/TastyExpr.scala @@ -0,0 +1,8 @@ +package scala.quoted + +import scala.runtime.quoted.Unpickler.Pickled + +/** An Expr backed by a pickled TASTY tree */ +final case class TastyExpr[T](tasty: Pickled, args: Seq[Any]) extends Expr[T] with TastyQuoted { + override def toString(): String = s"TastyExpr(, $args)" +} diff --git a/library/src/scala/quoted/TastyQuoted.scala b/library/src/scala/quoted/TastyQuoted.scala new file mode 100644 index 000000000000..729fedd9a998 --- /dev/null +++ b/library/src/scala/quoted/TastyQuoted.scala @@ -0,0 +1,9 @@ +package scala.quoted + +import scala.runtime.quoted.Unpickler.Pickled + +/** A quote backed by a pickled TASTY tree */ +trait TastyQuoted extends Quoted { + def tasty: Pickled + def args: Seq[Any] +} diff --git a/library/src/scala/quoted/TastyType.scala b/library/src/scala/quoted/TastyType.scala new file mode 100644 index 000000000000..b63b30745ace --- /dev/null +++ b/library/src/scala/quoted/TastyType.scala @@ -0,0 +1,8 @@ +package scala.quoted + +import scala.runtime.quoted.Unpickler.Pickled + +/** A Type backed by a pickled TASTY tree */ +final case class TastyType[T](tasty: Pickled, args: Seq[Any]) extends Type[T] with TastyQuoted { + override def toString(): String = s"TastyType(, $args)" +} diff --git a/library/src/scala/runtime/quoted/Unpickler.scala b/library/src/scala/runtime/quoted/Unpickler.scala index 2073f7516f0c..ab24c9e7250f 100644 --- a/library/src/scala/runtime/quoted/Unpickler.scala +++ b/library/src/scala/runtime/quoted/Unpickler.scala @@ -13,10 +13,10 @@ object Unpickler { /** Unpickle `repr` which represents a pickled `Expr` tree, * replacing splice nodes with `args` */ - def unpickleExpr[T](repr: Pickled, args: Seq[Any]): Expr[T] = ??? + def unpickleExpr[T](repr: Pickled, args: Seq[Any]): Expr[T] = new TastyExpr[T](repr, args) /** Unpickle `repr` which represents a pickled `Type` tree, * replacing splice nodes with `args` */ - def unpickleType[T](repr: Pickled, args: Seq[Any]): Type[T] = ??? + def unpickleType[T](repr: Pickled, args: Seq[Any]): Type[T] = new TastyType[T](repr, args) } diff --git a/tests/neg/quoteTest.scala b/tests/neg/quote-0.scala similarity index 100% rename from tests/neg/quoteTest.scala rename to tests/neg/quote-0.scala diff --git a/tests/neg/quotedMacroOverride.scala b/tests/neg/quote-MacroOverride.scala similarity index 100% rename from tests/neg/quotedMacroOverride.scala rename to tests/neg/quote-MacroOverride.scala diff --git a/tests/neg/quote-spliceNonStaged.scala b/tests/neg/quote-spliceNonStaged.scala new file mode 100644 index 000000000000..b328aabac7b9 --- /dev/null +++ b/tests/neg/quote-spliceNonStaged.scala @@ -0,0 +1,7 @@ +package quotes +import scala.quoted._ + +object Quotes_1 { + def printHello: Expr[Unit] = '{ println("Hello") } + ~printHello // error +} diff --git a/tests/pending/pos/quotedSepComp/Macro_1.scala b/tests/pending/pos/quotedSepComp/Macro_1.scala deleted file mode 100644 index 205f63c937d8..000000000000 --- a/tests/pending/pos/quotedSepComp/Macro_1.scala +++ /dev/null @@ -1,5 +0,0 @@ -import scala.quoted._ -object Macros { - inline def assert(expr: => Boolean): Unit = ~ assertImpl('(expr)) - def assertImpl(expr: Expr[Boolean]) = '{ () } -} diff --git a/tests/pending/pos/quotedSepComp/Test_2.scala b/tests/pending/pos/quotedSepComp/Test_2.scala deleted file mode 100644 index 42a3748830a0..000000000000 --- a/tests/pending/pos/quotedSepComp/Test_2.scala +++ /dev/null @@ -1,5 +0,0 @@ -class Test { - import Macros._ - val x = 1 - assert(x != 0) -} diff --git a/tests/pos/quoted.scala b/tests/pos/quote-0.scala similarity index 100% rename from tests/pos/quoted.scala rename to tests/pos/quote-0.scala diff --git a/tests/pos/quoteTest.scala b/tests/pos/quote-1.scala similarity index 99% rename from tests/pos/quoteTest.scala rename to tests/pos/quote-1.scala index e3671a2ea3e3..8d2a3ee243a8 100644 --- a/tests/pos/quoteTest.scala +++ b/tests/pos/quote-1.scala @@ -13,4 +13,3 @@ object Test { def g(es: Expr[String], t: Type[String]) = f('{ (~es + "!") :: Nil })('[List[~t]]) } - diff --git a/tests/pos/quote-assert/quoted_1.scala b/tests/pos/quote-assert/quoted_1.scala new file mode 100644 index 000000000000..7cb0e73f5bc4 --- /dev/null +++ b/tests/pos/quote-assert/quoted_1.scala @@ -0,0 +1,6 @@ +import scala.quoted._ + +object Macros { + def assertImpl(expr: Expr[Boolean]) = + '{ if !(~expr) then throw new AssertionError(s"failed assertion: ${~expr}") } +} diff --git a/tests/pos/quote-assert/quoted_2.scala b/tests/pos/quote-assert/quoted_2.scala new file mode 100644 index 000000000000..1b6cde45c700 --- /dev/null +++ b/tests/pos/quote-assert/quoted_2.scala @@ -0,0 +1,18 @@ +import scala.quoted._ +import Macros._ + +class Test { + + inline def assert(expr: => Boolean): Unit = + ~ assertImpl('(expr)) + + + val program = '{ + val x = 1 + assert(x != 0) + + ~assertImpl('(x != 0)) + } + + program.run +} diff --git a/tests/pos/liftable.scala b/tests/pos/quote-liftable.scala similarity index 81% rename from tests/pos/liftable.scala rename to tests/pos/quote-liftable.scala index 02023994d9e7..d3dd2ff57bf5 100644 --- a/tests/pos/liftable.scala +++ b/tests/pos/quote-liftable.scala @@ -24,5 +24,15 @@ object Test { } } + (true: Expr[Boolean]) + (1: Expr[Byte]) + ('a': Expr[Char]) + (1: Expr[Short]) + (1: Expr[Int]) + (1L: Expr[Long]) + (1.0f: Expr[Float]) + (1.0: Expr[Double]) + ("abc": Expr[String]) + val xs: Expr[List[Int]] = 1 :: 2 :: 3 :: Nil } diff --git a/tests/pos/stagedInterpreter.scala b/tests/pos/quote-stagedInterpreter.scala similarity index 100% rename from tests/pos/stagedInterpreter.scala rename to tests/pos/quote-stagedInterpreter.scala diff --git a/tests/run/quote-and-splice.check b/tests/run/quote-and-splice.check new file mode 100644 index 000000000000..c559bdbee84f --- /dev/null +++ b/tests/run/quote-and-splice.check @@ -0,0 +1,10 @@ +3 +3 +4 +3 +5 +9 +1.0 +5.0 +25.0 +125.0 diff --git a/tests/run/quote-and-splice/Macros_1.scala b/tests/run/quote-and-splice/Macros_1.scala new file mode 100644 index 000000000000..d8aa24abeeaa --- /dev/null +++ b/tests/run/quote-and-splice/Macros_1.scala @@ -0,0 +1,27 @@ +import scala.quoted._ + +object Macros { + + inline def macro1 = ~ macro1Impl + def macro1Impl = '(3) + + inline def macro2(inline p: Boolean) = ~ macro2Impl(p) + def macro2Impl(p: Boolean) = if (p) '(3) else '(4) + + inline def macro3(n: Int) = ~ macro3Impl('(n)) + def macro3Impl(p: Expr[Int]) = '{ 2 + ~p } + + inline def macro4(i: Int)(j: Int) = ~ macro4Impl('(i))('(j)) + def macro4Impl(i: Expr[Int])(j: Expr[Int]) = '{ ~i + ~j } + + inline def macro5(i: Int, j: Int) = ~ macro5Impl(j = '(j), i = '(i)) + def macro5Impl(i: Expr[Int], j: Expr[Int]) = '{ ~i + ~j } + + inline def power(inline n: Int, x: Double) = ~powerCode(n, '(x)) + + def powerCode(n: Int, x: Expr[Double]): 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)) } } + else '{ ~x * ~powerCode(n - 1, x) } +} diff --git a/tests/run/quote-and-splice/Test_2.scala b/tests/run/quote-and-splice/Test_2.scala new file mode 100644 index 000000000000..962441add308 --- /dev/null +++ b/tests/run/quote-and-splice/Test_2.scala @@ -0,0 +1,17 @@ +object Test { + import Macros._ + + def main(args: Array[String]): Unit = { + println(macro1) + println(macro2(true)) + println(macro2(false)) + println(macro3(1)) + println(macro4(2)(3)) + println(macro5(4, 5)) + println(power(0, 5)) + println(power(1, 5)) + println(power(2, 5)) + println(power(3, 5)) + } + +} diff --git a/tests/run/quote-sep-comp.check b/tests/run/quote-sep-comp.check new file mode 100644 index 000000000000..da29283aaa47 --- /dev/null +++ b/tests/run/quote-sep-comp.check @@ -0,0 +1,2 @@ +true +false diff --git a/tests/run/quote-sep-comp/Macro_1.scala b/tests/run/quote-sep-comp/Macro_1.scala new file mode 100644 index 000000000000..c3ce7e2e1d53 --- /dev/null +++ b/tests/run/quote-sep-comp/Macro_1.scala @@ -0,0 +1,5 @@ +import scala.quoted._ +object Macros { + inline def assert2(expr: => Boolean): Unit = ~ assertImpl('(expr)) + def assertImpl(expr: Expr[Boolean]) = '{ println(~expr) } +} diff --git a/tests/run/quote-sep-comp/Test_2.scala b/tests/run/quote-sep-comp/Test_2.scala new file mode 100644 index 000000000000..805fb8a6b079 --- /dev/null +++ b/tests/run/quote-sep-comp/Test_2.scala @@ -0,0 +1,8 @@ +import Macros._ +object Test { + def main(args: Array[String]): Unit = { + val x = 1 + assert2(x != 0) + assert2(x == 0) + } +}