diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 736e1b08d1e7..af6b4a73be3d 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -54,6 +54,7 @@ class Compiler { List(new Inlining) :: // Inline and execute macros List(new PostInlining) :: // Add mirror support for inlined code List(new Staging) :: // Check staging levels and heal staged types + List(new Splicing) :: // Replace level 1 splices with holes List(new PickleQuotes) :: // Turn quoted trees into explicit run-time data structures Nil diff --git a/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala b/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala index d38ce8ca6888..001461f4a6f1 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala @@ -133,8 +133,11 @@ class TreeTypeMap( val bind1 = tmap.transformSub(bind) val expr1 = tmap.transform(expr) cpy.Labeled(labeled)(bind1, expr1) - case Hole(isTermHole, n, args) => - Hole(isTermHole, n, args.mapConserve(transform)).withSpan(tree.span).withType(mapType(tree.tpe)) + case tree @ Hole(_, _, args, content, tpt) => + val args1 = args.mapConserve(transform) + val content1 = transform(content) + val tpt1 = transform(tpt) + cpy.Hole(tree)(args = args1, content = content1, tpt = tpt1) case lit @ Literal(Constant(tpe: Type)) => cpy.Literal(lit)(Constant(mapType(tpe))) case tree1 => diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala index 7aa4491c31de..26e6265ae8f0 100644 --- a/compiler/src/dotty/tools/dotc/ast/Trees.scala +++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala @@ -503,6 +503,11 @@ object Trees { def forwardTo: Tree[T] = fun } + object GenericApply: + def unapply[T >: Untyped](tree: Tree[T]): Option[(Tree[T], List[Tree[T]])] = tree match + case tree: GenericApply[T] => Some((tree.fun, tree.args)) + case _ => None + /** The kind of application */ enum ApplyKind: case Regular // r.f(x) @@ -525,8 +530,6 @@ object Trees { attachmentOrElse(untpd.KindOfApply, ApplyKind.Regular) } - - /** fun[args] */ case class TypeApply[-T >: Untyped] private[ast] (fun: Tree[T], args: List[Tree[T]])(implicit @constructorOnly src: SourceFile) extends GenericApply[T] { @@ -973,9 +976,15 @@ object Trees { def genericEmptyTree[T >: Untyped]: Thicket[T] = theEmptyTree.asInstanceOf[Thicket[T]] /** Tree that replaces a splice in pickled quotes. - * It is only used when picking quotes (Will never be in a TASTy file). + * It is only used when picking quotes (will never be in a TASTy file). + * + * @param isTermHole If this hole is a term, otherwise it is a type hole. + * @param idx The index of the hole in the quote. + * @param args The arguments of the splice to compute its content + * @param content Lambda that computes the content of the hole. This tree is empty when in a quote pickle. + * @param tpt Type of the hole */ - case class Hole[-T >: Untyped](isTermHole: Boolean, idx: Int, args: List[Tree[T]])(implicit @constructorOnly src: SourceFile) extends Tree[T] { + case class Hole[-T >: Untyped](isTermHole: Boolean, idx: Int, args: List[Tree[T]], content: Tree[T], tpt: Tree[T])(implicit @constructorOnly src: SourceFile) extends Tree[T] { type ThisTree[-T >: Untyped] <: Hole[T] override def isTerm: Boolean = isTermHole override def isType: Boolean = !isTermHole @@ -1331,6 +1340,10 @@ object Trees { case tree: Thicket if (trees eq tree.trees) => tree case _ => finalize(tree, untpd.Thicket(trees)(sourceFile(tree))) } + def Hole(tree: Tree)(isTerm: Boolean, idx: Int, args: List[Tree], content: Tree, tpt: Tree)(using Context): Hole = tree match { + case tree: Hole if isTerm == tree.isTerm && idx == tree.idx && args.eq(tree.args) && content.eq(tree.content) && content.eq(tree.content) => tree + case _ => finalize(tree, untpd.Hole(isTerm, idx, args, content, tpt)(sourceFile(tree))) + } // Copier methods with default arguments; these demand that the original tree // is of the same class as the copy. We only include trees with more than 2 elements here. @@ -1352,6 +1365,9 @@ object Trees { TypeDef(tree: Tree)(name, rhs) def Template(tree: Template)(constr: DefDef = tree.constr, parents: List[Tree] = tree.parents, derived: List[untpd.Tree] = tree.derived, self: ValDef = tree.self, body: LazyTreeList = tree.unforcedBody)(using Context): Template = Template(tree: Tree)(constr, parents, derived, self, body) + def Hole(tree: Hole)(isTerm: Boolean = tree.isTerm, idx: Int = tree.idx, args: List[Tree] = tree.args, content: Tree = tree.content, tpt: Tree = tree.tpt)(using Context): Hole = + Hole(tree: Tree)(isTerm, idx, args, content, tpt) + } /** Hook to indicate that a transform of some subtree should be skipped */ @@ -1481,6 +1497,8 @@ object Trees { case Thicket(trees) => val trees1 = transform(trees) if (trees1 eq trees) tree else Thicket(trees1) + case tree @ Hole(_, _, args, content, tpt) => + cpy.Hole(tree)(args = transform(args), content = transform(content), tpt = transform(tpt)) case _ => transformMoreCases(tree) } @@ -1618,8 +1636,8 @@ object Trees { this(this(x, arg), annot) case Thicket(ts) => this(x, ts) - case Hole(_, _, args) => - this(x, args) + case Hole(_, _, args, content, tpt) => + this(this(this(x, args), content), tpt) case _ => foldMoreCases(x, tree) } diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 2db0bd6de2d4..a48685aedc4b 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -373,6 +373,9 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { def Throw(expr: Tree)(using Context): Tree = ref(defn.throwMethod).appliedTo(expr) + def Hole(isTermHole: Boolean, idx: Int, args: List[Tree], content: Tree, tpt: Tree)(using Context): Hole = + ta.assignType(untpd.Hole(isTermHole, idx, args, content, tpt), tpt) + // ------ Making references ------------------------------------------------------ def prefixIsElidable(tp: NamedType)(using Context): Boolean = { diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 7e00972f354d..118acd85a110 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -411,6 +411,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { def Export(expr: Tree, selectors: List[ImportSelector])(implicit src: SourceFile): Export = new Export(expr, selectors) def PackageDef(pid: RefTree, stats: List[Tree])(implicit src: SourceFile): PackageDef = new PackageDef(pid, stats) def Annotated(arg: Tree, annot: Tree)(implicit src: SourceFile): Annotated = new Annotated(arg, annot) + def Hole(isTermHole: Boolean, idx: Int, args: List[Tree], content: Tree, tpt: Tree)(implicit src: SourceFile): Hole = new Hole(isTermHole, idx, args, content, tpt) // ------ Additional creation methods for untyped only ----------------- diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index d6c713039190..76145786f586 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -789,7 +789,7 @@ class Definitions { @tu lazy val QuotesClass: ClassSymbol = requiredClass("scala.quoted.Quotes") @tu lazy val QuoteUnpicklerClass: ClassSymbol = requiredClass("scala.quoted.runtime.QuoteUnpickler") - @tu lazy val QuoteUnpickler_unpickleExpr: Symbol = QuoteUnpicklerClass.requiredMethod("unpickleExpr") + @tu lazy val QuoteUnpickler_unpickleExprV2: Symbol = QuoteUnpicklerClass.requiredMethod("unpickleExprV2") @tu lazy val QuoteUnpickler_unpickleType: Symbol = QuoteUnpicklerClass.requiredMethod("unpickleType") @tu lazy val QuoteMatchingClass: ClassSymbol = requiredClass("scala.quoted.runtime.QuoteMatching") diff --git a/compiler/src/dotty/tools/dotc/core/Phases.scala b/compiler/src/dotty/tools/dotc/core/Phases.scala index b1268d7034a9..2a205bac90fd 100644 --- a/compiler/src/dotty/tools/dotc/core/Phases.scala +++ b/compiler/src/dotty/tools/dotc/core/Phases.scala @@ -201,7 +201,7 @@ object Phases { private var mySbtExtractDependenciesPhase: Phase = _ private var myPicklerPhase: Phase = _ private var myInliningPhase: Phase = _ - private var myPickleQuotesPhase: Phase = _ + private var mySplicingPhase: Phase = _ private var myFirstTransformPhase: Phase = _ private var myCollectNullableFieldsPhase: Phase = _ private var myRefChecksPhase: Phase = _ @@ -223,7 +223,7 @@ object Phases { final def sbtExtractDependenciesPhase: Phase = mySbtExtractDependenciesPhase final def picklerPhase: Phase = myPicklerPhase final def inliningPhase: Phase = myInliningPhase - final def pickleQuotesPhase: Phase = myPickleQuotesPhase + final def splicingPhase: Phase = mySplicingPhase final def firstTransformPhase: Phase = myFirstTransformPhase final def collectNullableFieldsPhase: Phase = myCollectNullableFieldsPhase final def refchecksPhase: Phase = myRefChecksPhase @@ -248,7 +248,7 @@ object Phases { mySbtExtractDependenciesPhase = phaseOfClass(classOf[sbt.ExtractDependencies]) myPicklerPhase = phaseOfClass(classOf[Pickler]) myInliningPhase = phaseOfClass(classOf[Inlining]) - myPickleQuotesPhase = phaseOfClass(classOf[PickleQuotes]) + mySplicingPhase = phaseOfClass(classOf[Splicing]) myFirstTransformPhase = phaseOfClass(classOf[FirstTransform]) myCollectNullableFieldsPhase = phaseOfClass(classOf[CollectNullableFields]) myRefChecksPhase = phaseOfClass(classOf[RefChecks]) @@ -423,7 +423,7 @@ object Phases { def sbtExtractDependenciesPhase(using Context): Phase = ctx.base.sbtExtractDependenciesPhase def picklerPhase(using Context): Phase = ctx.base.picklerPhase def inliningPhase(using Context): Phase = ctx.base.inliningPhase - def pickleQuotesPhase(using Context): Phase = ctx.base.pickleQuotesPhase + def splicingPhase(using Context): Phase = ctx.base.splicingPhase def firstTransformPhase(using Context): Phase = ctx.base.firstTransformPhase def refchecksPhase(using Context): Phase = ctx.base.refchecksPhase def elimRepeatedPhase(using Context): Phase = ctx.base.elimRepeatedPhase diff --git a/compiler/src/dotty/tools/dotc/core/StagingContext.scala b/compiler/src/dotty/tools/dotc/core/StagingContext.scala index 9fbb009f6e5f..c5ed167de05a 100644 --- a/compiler/src/dotty/tools/dotc/core/StagingContext.scala +++ b/compiler/src/dotty/tools/dotc/core/StagingContext.scala @@ -13,11 +13,11 @@ import scala.collection.mutable object StagingContext { - /** A key to be used in a context property that tracks the quoteation level */ + /** A key to be used in a context property that tracks the quotation level */ private val QuotationLevel = new Property.Key[Int] - /** A key to be used in a context property that tracks the quoteation stack. - * Stack containing the Quotes references recieved by the surrounding quotes. + /** A key to be used in a context property that tracks the quotation stack. + * Stack containing the Quotes references received by the surrounding quotes. */ private val QuotesStack = new Property.Key[List[tpd.Tree]] @@ -31,7 +31,7 @@ object StagingContext { def quoteContext(using Context): Context = ctx.fresh.setProperty(QuotationLevel, level + 1) - /** Context with an incremented quotation level and pushes a refecence to a Quotes on the quote context stack */ + /** Context with an incremented quotation level and pushes a reference to a Quotes on the quote context stack */ def pushQuotes(qctxRef: tpd.Tree)(using Context): Context = val old = ctx.property(QuotesStack).getOrElse(List.empty) ctx.fresh.setProperty(QuotationLevel, level + 1) @@ -48,7 +48,7 @@ object StagingContext { ctx.property(TaggedTypes).get /** Context with a decremented quotation level and pops the Some of top of the quote context stack or None if the stack is empty. - * The quotation stack could be empty if we are in a top level splice or an eroneous splice directly witin a top level splice. + * The quotation stack could be empty if we are in a top level splice or an erroneous splice directly within a top level splice. */ def popQuotes()(using Context): (Option[tpd.Tree], Context) = val ctx1 = ctx.fresh.setProperty(QuotationLevel, level - 1) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index a195b157cacd..0ff5a3aa75a5 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -644,11 +644,11 @@ class TreePickler(pickler: TastyPickler) { pickleTree(hi) pickleTree(alias) } - case Hole(_, idx, args) => + case Hole(_, idx, args, _, tpt) => writeByte(HOLE) withLength { writeNat(idx) - pickleType(tree.tpe, richTypes = true) + pickleType(tpt.tpe, richTypes = true) args.foreach(pickleTree) } } diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index e4a5c0ae8c6d..ee418f9b0ac9 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -1288,7 +1288,7 @@ class TreeUnpickler(reader: TastyReader, val idx = readNat() val tpe = readType() val args = until(end)(readTerm()) - Hole(true, idx, args).withType(tpe) + Hole(true, idx, args, EmptyTree, TypeTree(tpe)).withType(tpe) case _ => readPathTerm() } @@ -1324,7 +1324,7 @@ class TreeUnpickler(reader: TastyReader, val idx = readNat() val tpe = readType() val args = until(end)(readTerm()) - Hole(false, idx, args).withType(tpe) + Hole(false, idx, args, EmptyTree, TypeTree(tpe)).withType(tpe) case _ => if (isTypeTreeTag(nextByte)) readTerm() else { diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index ef11ec9434ae..56247ef5937e 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -699,10 +699,12 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { "Thicket {" ~~ toTextGlobal(trees, "\n") ~~ "}" case MacroTree(call) => keywordStr("macro ") ~ toTextGlobal(call) - case Hole(isTermHole, idx, args) => - val (prefix, postfix) = if isTermHole then ("{{{ ", " }}}") else ("[[[ ", " ]]]") + case Hole(isTermHole, idx, args, content, tpt) => + val (prefix, postfix) = if isTermHole then ("{{{", "}}}") else ("[[[", "]]]") val argsText = toTextGlobal(args, ", ") - prefix ~~ idx.toString ~~ "|" ~~ argsText ~~ postfix + val contentText = toTextGlobal(content) + val tptText = toTextGlobal(tpt) + prefix ~~ idx.toString ~~ "|" ~~ tptText ~~ "|" ~~ argsText ~~ "|" ~~ contentText ~~ postfix case _ => tree.fallbackToText(this) } diff --git a/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala b/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala index 0b323836d59a..4a70c02e754f 100644 --- a/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala +++ b/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala @@ -52,12 +52,12 @@ object PickledQuotes { } /** Unpickle the tree contained in the TastyExpr */ - def unpickleTerm(pickled: String | List[String], typeHole: (Int, Seq[Any]) => scala.quoted.Type[?], termHole: (Int, Seq[Any], scala.quoted.Quotes) => scala.quoted.Expr[?])(using Context): Tree = { + def unpickleTerm(pickled: String | List[String], typeHole: (Int, Seq[Any]) => scala.quoted.Type[?], termHole: (Int, Seq[Any], scala.quoted.Quotes) => scala.quoted.Expr[?], version: Int)(using Context): Tree = { val unpickled = withMode(Mode.ReadPositions)(unpickle(pickled, isType = false)) - val Inlined(call, Nil, expnasion) = unpickled + val Inlined(call, Nil, expansion) = unpickled val inlineCtx = inlineContext(call) - val expansion1 = spliceTypes(expnasion, typeHole, termHole)(using inlineCtx) - val expansion2 = spliceTerms(expansion1, typeHole, termHole)(using inlineCtx) + val expansion1 = spliceTypes(expansion, typeHole, termHole)(using inlineCtx) + val expansion2 = spliceTerms(expansion1, typeHole, termHole, version)(using inlineCtx) cpy.Inlined(unpickled)(call, Nil, expansion2) } @@ -68,16 +68,19 @@ object PickledQuotes { } /** Replace all term holes with the spliced terms */ - private def spliceTerms(tree: Tree, typeHole: (Int, Seq[Any]) => scala.quoted.Type[?], termHole: (Int, Seq[Any], scala.quoted.Quotes) => scala.quoted.Expr[?])(using Context): Tree = { + private def spliceTerms(tree: Tree, typeHole: (Int, Seq[Any]) => scala.quoted.Type[?], termHole: (Int, Seq[Any], scala.quoted.Quotes) => scala.quoted.Expr[?], version: Int)(using Context): Tree = { val evaluateHoles = new TreeMap { override def transform(tree: tpd.Tree)(using Context): tpd.Tree = tree match { - case Hole(isTerm, idx, args) => + case Hole(isTermHole, idx, args, _, _) => inContext(SpliceScope.contextWithNewSpliceScope(tree.sourcePos)) { val reifiedArgs = args.map { arg => - if (arg.isTerm) (q: Quotes) ?=> new ExprImpl(arg, SpliceScope.getCurrent) + if (arg.isTerm) + version match + case 1 => (q: Quotes) ?=> new ExprImpl(arg, SpliceScope.getCurrent) + case 2 => new ExprImpl(arg, SpliceScope.getCurrent) else new TypeImpl(arg, SpliceScope.getCurrent) } - if isTerm then + if isTermHole then val quotedExpr = termHole(idx, reifiedArgs, QuotesImpl()) val filled = PickledQuotes.quotedExprToTree(quotedExpr) @@ -131,11 +134,14 @@ object PickledQuotes { case tdef: TypeDef => assert(tdef.symbol.hasAnnotation(defn.QuotedRuntime_SplicedTypeAnnot)) val tree = tdef.rhs match - case TypeBoundsTree(_, Hole(_, idx, args), _) => + case TypeBoundsTree(_, Hole(_, idx, args, _, _), _) => // to keep for backwards compat val quotedType = typeHole(idx, args) PickledQuotes.quotedTypeToTree(quotedType) - case TypeBoundsTree(_, tpt, _) => + case TypeBoundsTree(_, tpt, _) => // to keep for backwards compat tpt + case Hole(_, idx, args, _, _) => + val quotedType = typeHole(idx, args) + PickledQuotes.quotedTypeToTree(quotedType) (tdef.symbol, tree.tpe) }.toMap class ReplaceSplicedTyped extends TypeMap() { diff --git a/compiler/src/dotty/tools/dotc/transform/PickleQuotes.scala b/compiler/src/dotty/tools/dotc/transform/PickleQuotes.scala index be081c695c2d..e13165ba3e8b 100644 --- a/compiler/src/dotty/tools/dotc/transform/PickleQuotes.scala +++ b/compiler/src/dotty/tools/dotc/transform/PickleQuotes.scala @@ -26,45 +26,52 @@ import dotty.tools.dotc.typer.Inliner import scala.annotation.constructorOnly - -/** Translates quoted terms and types to `unpickleExpr` or `unpickleType` method calls. +/** Translates quoted terms and types to `unpickleExprV2` or `unpickleType` method calls. * * Transforms top level quote * ``` * '{ ... + * @TypeSplice type X0 = {{ 0 | .. | contentsTpe0 | .. }} + * @TypeSplice type X2 = {{ 1 | .. | contentsTpe1 | .. }} * val x1 = ??? * val x2 = ??? * ... - * ${ ... '{ ... x1 ... x2 ...} ... } + * {{{ 3 | x1 | contents0 | T0 }}} // hole + * ... + * {{{ 4 | x2 | contents1 | T1 }}} // hole + * ... + * {{{ 5 | x1, x2 | contents2 | T2 }}} // hole * ... * } * ``` * to * ``` - * unpickleExpr( + * unpickleExprV2( * pickled = [[ // PICKLED TASTY - * ... + * @TypeSplice type X0 // with bounds that do not contain captured types + * @TypeSplice type X1 // with bounds that do not contain captured types * val x1 = ??? * val x2 = ??? * ... - * Hole( | x1, x2) + * {{{ 0 | x1 | | T0 }}} // hole + * ... + * {{{ 1 | x2 | | T1 }}} // hole + * ... + * {{{ 2 | x1, x2 | | T2 }}} // hole * ... * ]], * typeHole = (idx: Int, args: List[Any]) => idx match { - * case 0 => ... + * case 0 => contentsTpe0.apply(args) // beta reduces in following phases + * case 1 => contentsTpe1.apply(args) // beta reduces in following phases * }, - * termHole = (idx: Int, args: List[Any], qctx: Quotes) => idx match { - * case 0 => ... - * ... - * case => - * val x1$1 = args(0).asInstanceOf[Expr[T]] - * val x2$1 = args(1).asInstanceOf[Expr[T]] // can be asInstanceOf[Type[T]] - * ... - * { ... '{ ... ${x1$1} ... ${x2$1} ...} ... } + * termHole = (idx: Int, args: List[Any], quotes: Quotes) => idx match { + * case 3 => content0.apply(args).apply(quotes) // beta reduces in following phases + * case 4 => content1.apply(args).apply(quotes) // beta reduces in following phases + * case 5 => content2.apply(args).apply(quotes) // beta reduces in following phases * }, * ) * ``` - * and then performs the same transformation on `'{ ... ${x1$1} ... ${x2$1} ...}`. + * and then performs the same transformation on any quote contained in the `content`s. * */ class PickleQuotes extends MacroTransform { @@ -76,375 +83,80 @@ class PickleQuotes extends MacroTransform { override def allowsImplicitSearch: Boolean = true override def checkPostCondition(tree: Tree)(using Context): Unit = - tree match { + tree match case tree: RefTree if !Inliner.inInlineMethod => assert(!tree.symbol.isQuote) assert(!tree.symbol.isExprSplice) - case _ : TypeDef => + case _ : TypeDef if !Inliner.inInlineMethod => assert(!tree.symbol.hasAnnotation(defn.QuotedRuntime_SplicedTypeAnnot), s"${tree.symbol} should have been removed by PickledQuotes because it has a @quoteTypeTag") case _ => - } override def run(using Context): Unit = if (ctx.compilationUnit.needsQuotePickling) super.run(using freshStagingContext) protected def newTransformer(using Context): Transformer = new Transformer { override def transform(tree: tpd.Tree)(using Context): tpd.Tree = - new QuoteReifier(null, new mutable.HashMap[Symbol, Tree => Tree], new Embedded, ctx.owner)(ctx).transform(tree) + tree match + case Apply(Select(Apply(TypeApply(fn, List(tpt)), List(code)),nme.apply), List(quotes)) + if fn.symbol == defn.QuotedRuntime_exprQuote => + val (contents, codeWithHoles) = makeHoles(code) + val sourceRef = Inliner.inlineCallTrace(ctx.owner, tree.sourcePos) + val codeWithHoles2 = Inlined(sourceRef, Nil, codeWithHoles) + val pickled = PickleQuotes(quotes, codeWithHoles2, contents, tpt.tpe, false) + transform(pickled) // pickle quotes that are in the contents + case Apply(TypeApply(_, List(tpt)), List(quotes)) if tree.symbol == defn.QuotedTypeModule_of => + tpt match + case Select(t, _) if tpt.symbol == defn.QuotedType_splice => + // `Type.of[t.Underlying](quotes)` --> `t` + ref(t.symbol)(using ctx.withSource(tpt.source)).withSpan(tpt.span) + case _ => + val (contents, tptWithHoles) = makeHoles(tpt) + PickleQuotes(quotes, tptWithHoles, contents, tpt.tpe, true) + case tree: DefDef if tree.symbol.is(Macro) => + // Shrink size of the tree. The methods have already been inlined. + // TODO move to FirstTransform to trigger even without quotes + cpy.DefDef(tree)(rhs = defaultValue(tree.rhs.tpe)) + case _: DefDef if tree.symbol.isInlineMethod => + tree + case _ => + super.transform(tree) } - /** The main transformer class - * @param outer the next outer reifier, null is this is the topmost transformer - * @param embedded a list of embedded quotes (if in a splice) or splices (if in a quote) - * @param owner the owner in the destination lifted lambda - * @param capturers register a reference defined in a quote but used in another quote nested in a splice. - * Returns a version of the reference that needs to be used in its place. - * '{ - * val x = ??? - * ${ ... '{ ... x ... } ... } - * } - * Eta expanding the `x` in `${ ... '{ ... x ... } ... }` will return a `${x$1}` for which the `x$1` - * be created by some outer reifier. - * This transformation is only applied to definitions at staging level 1. - * See `isCaptured`. - */ - private class QuoteReifier(outer: QuoteReifier, capturers: mutable.HashMap[Symbol, Tree => Tree], - val embedded: Embedded, val owner: Symbol)(@constructorOnly ictx: Context) extends TreeMapWithStages(ictx) { self => - - import StagingContext._ - - /** A nested reifier for a quote (if `isQuote = true`) or a splice (if not) */ - def nested(isQuote: Boolean)(using Context): QuoteReifier = { - val nestedEmbedded = if (level > 1 || (level == 1 && isQuote)) embedded else new Embedded - new QuoteReifier(this, capturers, nestedEmbedded, ctx.owner)(ctx) - } - - /** Split `body` into a core and a list of embedded splices. - * Then if inside a splice, make a hole from these parts. - * If outside a splice, generate a call tp `scala.quoted.Unpickler.unpickleType` or - * `scala.quoted.Unpickler.unpickleExpr` that matches `tpe` with - * core and splices as arguments. - */ - override protected def transformQuotation(body: Tree, quote: Apply)(using Context): Tree = { - val isType = quote.symbol eq defn.QuotedTypeModule_of - if (level > 0) { - val body1 = nested(isQuote = true).transform(body)(using quoteContext) - super.transformQuotation(body1, quote) - } - else { - val (body1, splices) = nested(isQuote = true).splitQuote(body)(using quoteContext) - if (level == 0) { - val body2 = - if (body1.isType) body1 - else Inlined(Inliner.inlineCallTrace(ctx.owner, quote.sourcePos), Nil, body1) - pickledQuote(quote, body2, splices, body.tpe, isType).withSpan(quote.span) - } - else - body - } - } - - private def pickledQuote(quote: Apply, body: Tree, splices: List[Tree], originalTp: Type, isType: Boolean)(using Context) = { - /** Encode quote using Reflection.Literal - * - * Generate the code - * ```scala - * qctx => qctx.reflect.TreeMethods.asExpr( - * qctx.reflect.Literal.apply(x$1.reflect.Constant..apply()) - * ).asInstanceOf[scala.quoted.Expr[]] - * ``` - * this closure is always applied directly to the actual context and the BetaReduce phase removes it. - */ - def pickleAsLiteral(lit: Literal) = { - val exprType = defn.QuotedExprClass.typeRef.appliedTo(body.tpe) - val lambdaTpe = MethodType(defn.QuotesClass.typeRef :: Nil, exprType) - def mkConst(ts: List[Tree]) = { - val reflect = ts.head.select("reflect".toTermName) - val typeName = body.tpe.typeSymbol.name - val literalValue = - if lit.const.tag == Constants.NullTag || lit.const.tag == Constants.UnitTag then Nil - else List(body) - val constant = reflect.select(s"${typeName}Constant".toTermName).select(nme.apply).appliedToTermArgs(literalValue) - val literal = reflect.select("Literal".toTermName).select(nme.apply).appliedTo(constant) - reflect.select("TreeMethods".toTermName).select("asExpr".toTermName).appliedTo(literal).asInstance(exprType) - } - Lambda(lambdaTpe, mkConst).withSpan(body.span) - } - - /** Encode quote using Reflection.Literal - * - * Generate the code - * ```scala - * qctx => scala.quoted.ToExpr.{BooleanToExpr,ShortToExpr, ...}.apply()(qctx) - * ``` - * this closure is always applied directly to the actual context and the BetaReduce phase removes it. - */ - def liftedValue(lit: Literal, lifter: Symbol) = - val exprType = defn.QuotedExprClass.typeRef.appliedTo(body.tpe) - val lambdaTpe = MethodType(defn.QuotesClass.typeRef :: Nil, exprType) - def mkToExprCall(ts: List[Tree]) = - ref(lifter).appliedToType(originalTp).select(nme.apply).appliedTo(lit).appliedTo(ts.head) - Lambda(lambdaTpe, mkToExprCall).withSpan(body.span) - - def pickleAsValue(lit: Literal) = { - // TODO should all constants be pickled as Literals? - // Should examime the generated bytecode size to decide and performance - lit.const.tag match { - case Constants.NullTag => pickleAsLiteral(lit) - case Constants.UnitTag => pickleAsLiteral(lit) - case Constants.BooleanTag => liftedValue(lit, defn.ToExprModule_BooleanToExpr) - case Constants.ByteTag => liftedValue(lit, defn.ToExprModule_ByteToExpr) - case Constants.ShortTag => liftedValue(lit, defn.ToExprModule_ShortToExpr) - case Constants.IntTag => liftedValue(lit, defn.ToExprModule_IntToExpr) - case Constants.LongTag => liftedValue(lit, defn.ToExprModule_LongToExpr) - case Constants.FloatTag => liftedValue(lit, defn.ToExprModule_FloatToExpr) - case Constants.DoubleTag => liftedValue(lit, defn.ToExprModule_DoubleToExpr) - case Constants.CharTag => liftedValue(lit, defn.ToExprModule_CharToExpr) - case Constants.StringTag => liftedValue(lit, defn.ToExprModule_StringToExpr) - } - } - - /** Encode quote using QuoteUnpickler.{unpickleExpr, unpickleType} - * - * Generate the code - * ```scala - * qctx => qctx.asInstanceOf[QuoteUnpickler].[]( - * , - * , - * , - * ) - * ``` - * this closure is always applied directly to the actual context and the BetaReduce phase removes it. - */ - def pickleAsTasty() = { - val pickleQuote = PickledQuotes.pickleQuote(body) - val pickledQuoteStrings = pickleQuote match - case x :: Nil => Literal(Constant(x)) - case xs => liftList(xs.map(x => Literal(Constant(x))), defn.StringType) - - // TODO split holes earlier into types and terms. This all holes in each category can have consecutive indices - val (typeSplices, termSplices) = splices.zipWithIndex.partition { case (splice, _) => - splice.tpe match - case defn.FunctionOf(_, res, _, _) => res.typeSymbol == defn.QuotedTypeClass - } - - // This and all closures in typeSplices are removed by the BetaReduce phase - val typeHoles = - if typeSplices.isEmpty then Literal(Constant(null)) // keep pickled quote without splices as small as possible - else - Lambda( - MethodType( - List(defn.IntType, defn.SeqType.appliedTo(defn.AnyType)), - defn.QuotedTypeClass.typeRef.appliedTo(WildcardType)), - args => { - val cases = typeSplices.map { case (splice, idx) => - CaseDef(Literal(Constant(idx)), EmptyTree, splice.select(nme.apply).appliedTo(args(1))) - } - Match(args(0).annotated(New(ref(defn.UncheckedAnnot.typeRef))), cases) - } - ) - - // This and all closures in termSplices are removed by the BetaReduce phase - val termHoles = - if termSplices.isEmpty then Literal(Constant(null)) // keep pickled quote without splices as small as possible - else - Lambda( - MethodType( - List(defn.IntType, defn.SeqType.appliedTo(defn.AnyType), defn.QuotesClass.typeRef), - defn.QuotedExprClass.typeRef.appliedTo(defn.AnyType)), - args => { - val cases = termSplices.map { case (splice, idx) => - val defn.FunctionOf(_, defn.FunctionOf(qctxType :: _, _, _, _), _, _) = splice.tpe - val rhs = splice.select(nme.apply).appliedTo(args(1)).select(nme.apply).appliedTo(args(2).asInstance(qctxType)) - CaseDef(Literal(Constant(idx)), EmptyTree, rhs) - } - Match(args(0).annotated(New(ref(defn.UncheckedAnnot.typeRef))), cases) - } - ) - - val quoteClass = if isType then defn.QuotedTypeClass else defn.QuotedExprClass - val quotedType = quoteClass.typeRef.appliedTo(originalTp) - val lambdaTpe = MethodType(defn.QuotesClass.typeRef :: Nil, quotedType) - def callUnpickle(ts: List[Tree]) = { - val qctx = ts.head.asInstance(defn.QuoteUnpicklerClass.typeRef) - val unpickleMeth = if isType then defn.QuoteUnpickler_unpickleType else defn.QuoteUnpickler_unpickleExpr - qctx.select(unpickleMeth).appliedToType(originalTp).appliedTo(pickledQuoteStrings, typeHoles, termHoles) - } - Lambda(lambdaTpe, callUnpickle).withSpan(body.span) - } - - /** Encode quote using Reflection.TypeRepr.typeConstructorOf - * - * Generate the code - * ```scala - * qctx.reflect.TypeReprMethods.asType( - * qctx.reflect.TypeRepr.typeConstructorOf(classOf[]]) - * ).asInstanceOf[scala.quoted.Type[]] - * ``` - * this closure is always applied directly to the actual context and the BetaReduce phase removes it. - */ - def taggedType() = - val typeType = defn.QuotedTypeClass.typeRef.appliedTo(body.tpe) - val classTree = TypeApply(ref(defn.Predef_classOf.termRef), body :: Nil) - val reflect = quote.args.head.select("reflect".toTermName) - val typeRepr = reflect.select("TypeRepr".toTermName).select("typeConstructorOf".toTermName).appliedTo(classTree) - reflect.select("TypeReprMethods".toTermName).select("asType".toTermName).appliedTo(typeRepr).asInstance(typeType) - - if (isType) { - if (splices.isEmpty && body.symbol.isPrimitiveValueClass) taggedType() - else pickleAsTasty().select(nme.apply).appliedTo(quote.args.head) // TODO do not create lambda - } - else getLiteral(body) match { - case Some(lit) => pickleAsValue(lit) - case _ => pickleAsTasty() - } - } - - /** If inside a quote, split the body of the splice into a core and a list of embedded quotes - * and make a hole from these parts. Otherwise issue an error, unless we - * are in the body of an inline method. - */ - protected def transformSplice(body: Tree, splice: Apply)(using Context): Tree = - if (level > 1) { - val body1 = nested(isQuote = false).transform(body)(using spliceContext) - cpy.Apply(splice)(splice.fun, body1 :: Nil) - } - else { - assert(level == 1, "unexpected top splice outside quote") - val (body1, quotes) = nested(isQuote = false).splitSplice(body)(using spliceContext) - val tpe = outer.embedded.getHoleType(body, splice) - val hole = makeHole(splice.isTerm, body1, quotes, tpe).withSpan(splice.span) - // We do not place add the inline marker for trees that where lifted as they come from the same file as their - // enclosing quote. Any intemediate splice will add it's own Inlined node and cancel it before splicig the lifted tree. - // Note that lifted trees are not necessarily expressions and that Inlined nodes are expected to be expressions. - // For example we can have a lifted tree containing the LHS of an assignment (see tests/run-with-compiler/quote-var.scala). - if (outer.embedded.isLiftedSymbol(body.symbol)) hole - else Inlined(EmptyTree, Nil, hole).withSpan(splice.span) - } - - /** If inside a quote, split the body of the splice into a core and a list of embedded quotes - * and make a hole from these parts. Otherwise issue an error, unless we - * are in the body of an inline method. - */ - protected def transformSpliceType(body: Tree, splice: Select)(using Context): Tree = - if level > 1 then - val body1 = nested(isQuote = false).transform(body)(using spliceContext) - cpy.Select(splice)(body1, splice.name) - else if level == 1 then - val (body1, quotes) = nested(isQuote = false).splitSplice(body)(using spliceContext) - val tpe = outer.embedded.getHoleType(body, splice) - makeHole(splice.isTerm, body1, quotes, tpe).withSpan(splice.span) - else - splice - - /** Transforms the contents of a nested splice - * Assuming - * '{ - * val x = ??? - * val y = ??? - * ${ ... '{ ... x .. y ... } ... } - * } - * then the spliced subexpression - * { ... '{ ... x ... y ... } ... } - * will be transformed to - * (args: Seq[Any]) => { - * val x$1 = args(0).asInstanceOf[Expr[Any]] // or .asInstanceOf[Type[Any]] - * val y$1 = args(1).asInstanceOf[Expr[Any]] // or .asInstanceOf[Type[Any]] - * { ... '{ ... ${x$1} ... ${y$1} ... } ... } - * } - * - * See: `capture` - * - * At the same time register embedded trees `x` and `y` to place as arguments of the hole - * placed in the original code. - * '{ - * val x = ??? - * val y = ??? - * Hole(0 | x, y) - * } - */ - private def makeLambda(tree: Tree)(using Context): Tree = { - def body(arg: Tree)(using Context): Tree = { - var i = 0 - transformWithCapturer(tree)( - (captured: mutable.Map[Symbol, Tree]) => { - (tree: Tree) => { - def newCapture = { - val tpw = tree.tpe.widen match { - case tpw: MethodicType => tpw.toFunctionType(isJava = false) - case tpw => tpw - } - assert(tpw.isInstanceOf[ValueType]) - val argTpe = - if (tree.isType) defn.QuotedTypeClass.typeRef.appliedTo(tpw) - else defn.FunctionType(1, isContextual = true).appliedTo(defn.QuotesClass.typeRef, defn.QuotedExprClass.typeRef.appliedTo(tpw)) - val selectArg = arg.select(nme.apply).appliedTo(Literal(Constant(i))).cast(argTpe) - val capturedArg = SyntheticValDef(UniqueName.fresh(tree.symbol.name.toTermName).toTermName, selectArg) - i += 1 - embedded.addTree(tree, capturedArg.symbol) - captured.put(tree.symbol, capturedArg) - capturedArg - } - val refSym = captured.getOrElseUpdate(tree.symbol, newCapture).symbol - ref(refSym).withSpan(tree.span) + private def makeHoles(tree: tpd.Tree)(using Context): (List[Tree], tpd.Tree) = + + class HoleContentExtractor extends Transformer: + private var contents = List.newBuilder[Tree] + override def transform(tree: tpd.Tree)(using Context): tpd.Tree = + tree match + case tree @ Hole(isTerm, _, _, content, _) => + if !content.isEmpty then + contents += content + val holeType = + if isTerm then getTermHoleType(tree.tpe) else getTypeHoleType(tree.tpe) + val hole = cpy.Hole(tree)(content = EmptyTree, TypeTree(holeType)) + if isTerm then Inlined(EmptyTree, Nil, hole).withSpan(tree.span) else hole + case tree: DefTree => + val newAnnotations = tree.symbol.annotations.mapconserve { annot => + annot.derivedAnnotation(transform(annot.tree)(using ctx.withOwner(tree.symbol))) } - } - ) + tree.symbol.annotations = newAnnotations + super.transform(tree) + case _ => + super.transform(tree).withType(mapAnnots(tree.tpe)) + + private def mapAnnots = new TypeMap { // TODO factor out duplicated logic in Splicing + override def apply(tp: Type): Type = { + tp match + case tp @ AnnotatedType(underlying, annot) => + val underlying1 = this(underlying) + derivedAnnotatedType(tp, underlying1, annot.derivedAnnotation(transform(annot.tree))) + case _ => mapOver(tp) + } } - /* Lambdas are generated outside the quote that is being reified (i.e. in outer.owner). - * In case the case that level == -1 the code is not in a quote, it is in an inline method, - * hence we should take that as owner directly. - */ - val lambdaOwner = if (level == -1) ctx.owner else outer.owner - - val tpe = MethodType(defn.SeqType.appliedTo(defn.AnyType) :: Nil, tree.tpe.widen) - val meth = newSymbol(lambdaOwner, UniqueName.fresh(nme.ANON_FUN), Synthetic | Method, tpe) - Closure(meth, tss => body(tss.head.head)(using ctx.withOwner(meth)).changeNonLocalOwners(meth)).withSpan(tree.span) - } - - private def transformWithCapturer(tree: Tree)(capturer: mutable.Map[Symbol, Tree] => Tree => Tree)(using Context): Tree = { - val captured = mutable.LinkedHashMap.empty[Symbol, Tree] - val captured2 = capturer(captured) - - outer.localSymbols.foreach(sym => if (!sym.isInlineMethod) capturers.put(sym, captured2)) - - val tree2 = transform(tree) - capturers --= outer.localSymbols - - val captures = captured.result().valuesIterator.toList - if (captures.isEmpty) tree2 - else Block(captures, tree2) - } - - /** Returns true if this tree will be captured by `makeLambda`. Checks phase consistency and presence of capturer. */ - private def isCaptured(sym: Symbol, level: Int)(using Context): Boolean = - level == 1 && levelOf(sym) == 1 && capturers.contains(sym) - - /** Transform `tree` and return the resulting tree and all `embedded` quotes - * or splices as a pair. - */ - private def splitQuote(tree: Tree)(using Context): (Tree, List[Tree]) = { - val tree1 = stipTypeAnnotations(transform(tree)) - (tree1, embedded.getTrees) - } - - private def splitSplice(tree: Tree)(using Context): (Tree, List[Tree]) = { - val tree1 = makeLambda(tree) - (tree1, embedded.getTrees) - } - - private def stipTypeAnnotations(tree: Tree)(using Context): Tree = - new TreeTypeMap(typeMap = _.stripAnnots).apply(tree) - - /** Register `body` as an `embedded` quote or splice - * and return a hole with `splices` as arguments and the given type `tpe`. - */ - private def makeHole(isTermHole: Boolean, body: Tree, splices: List[Tree], tpe: Type)(using Context): Hole = { - val idx = embedded.addTree(body, NoSymbol) /** Remove references to local types that will not be defined in this quote */ - def getTypeHoleType(using Context) = new TypeMap() { + private def getTypeHoleType(using Context) = new TypeMap() { override def apply(tp: Type): Type = tp match case tp: TypeRef if tp.typeSymbol.isTypeSplice => apply(tp.dealias) @@ -458,105 +170,208 @@ class PickleQuotes extends MacroTransform { } /** Remove references to local types that will not be defined in this quote */ - def getTermHoleType(using Context) = new TypeMap() { + private def getTermHoleType(using Context) = new TypeMap() { override def apply(tp: Type): Type = tp match - case tp @ TypeRef(NoPrefix, _) if capturers.contains(tp.symbol) => + case tp @ TypeRef(NoPrefix, _) => // reference to term with a type defined in outer quote getTypeHoleType(tp) - case tp @ TermRef(NoPrefix, _) if capturers.contains(tp.symbol) => + case tp @ TermRef(NoPrefix, _) => // widen term refs to terms defined in outer quote apply(tp.widenTermRefExpr) case tp => mapOver(tp) } - val holeType = if isTermHole then getTermHoleType(tpe) else getTypeHoleType(tpe) + /** Get the contents of the transformed tree */ + def getContents() = + val res = contents.result + contents.clear() + res + end HoleContentExtractor - Hole(isTermHole, idx, splices).withType(holeType).asInstanceOf[Hole] - } + val holeMaker = new HoleContentExtractor + val newTree = holeMaker.transform(tree) + (holeMaker.getContents(), newTree) - override def transform(tree: Tree)(using Context): Tree = - if (tree.source != ctx.source && tree.source.exists) - transform(tree)(using ctx.withSource(tree.source)) - else reporting.trace(i"Reifier.transform $tree at $level", show = true) { - tree match { - case Apply(TypeApply(fn, (body: RefTree) :: Nil), _) - if fn.symbol == defn.QuotedTypeModule_of && isCaptured(body.symbol, level + 1) => - // Optimization: avoid the full conversion when capturing `X` with `x$1: Type[X$1]` - // in `Type.of[X]` to `Type.of[x$1.Underlying]` and go directly to `X$1` - capturers(body.symbol)(body) - case Apply(Select(Apply(TypeApply(fn,_), List(ref: RefTree)),nme.apply),List(quotes)) - if fn.symbol == defn.QuotedRuntime_exprQuote && isCaptured(ref.symbol, level + 1) => - // Optimization: avoid the full conversion when capturing `x` with `x$1: Expr[X]` - // in `'{x}` to `'{ ${x$1} }'` and go directly to `x$1` - capturers(ref.symbol)(ref).select(nme.apply).appliedTo(quotes) - case tree: RefTree if isCaptured(tree.symbol, level) => - val body = capturers(tree.symbol).apply(tree) - if (tree.isType) - transformSpliceType(body, body.select(tpnme.Underlying)) - else - val splice = ref(defn.QuotedRuntime_exprSplice).appliedToType(tree.tpe).appliedTo(body) - transformSplice(body, splice) - - case tree: DefDef if tree.symbol.is(Macro) && level == 0 => - // Shrink size of the tree. The methods have already been inlined. - // TODO move to FirstTransform to trigger even without quotes - cpy.DefDef(tree)(rhs = defaultValue(tree.rhs.tpe)) - - case tree: DefTree if level >= 1 => - val newAnnotations = tree.symbol.annotations.mapconserve { annot => - val newAnnotTree = transform(annot.tree)(using ctx.withOwner(tree.symbol)) - if (annot.tree == newAnnotTree) annot - else ConcreteAnnotation(newAnnotTree) - } - tree.symbol.annotations = newAnnotations - super.transform(tree) - case _ => - super.transform(tree) - } - } - private def liftList(list: List[Tree], tpe: Type)(using Context): Tree = - list.foldRight[Tree](ref(defn.NilModule)) { (x, acc) => - acc.select("::".toTermName).appliedToType(tpe).appliedTo(x) - } - } -} + end makeHoles +} object PickleQuotes { import tpd._ val name: String = "pickleQuotes" - def getLiteral(tree: tpd.Tree): Option[Literal] = tree match { - case tree: Literal => Some(tree) - case Block(Nil, e) => getLiteral(e) - case Inlined(_, Nil, e) => getLiteral(e) - case _ => None - } + // TODO move to a better location + def liftList(list: List[Tree], tpe: Type)(using Context): Tree = + list.foldRight[Tree](ref(defn.NilModule)) { (x, acc) => + acc.select("::".toTermName).appliedToType(tpe).appliedTo(x) + } - class Embedded(trees: mutable.ListBuffer[tpd.Tree] = mutable.ListBuffer.empty, map: mutable.Map[Symbol, tpd.Tree] = mutable.Map.empty) { - /** Adds the tree and returns it's index */ - def addTree(tree: tpd.Tree, liftedSym: Symbol): Int = { - trees += tree - if (liftedSym ne NoSymbol) - map.put(liftedSym, tree) - trees.length - 1 + def apply(quotes: Tree, body: Tree, contents: List[Tree], originalTp: Type, isType: Boolean)(using Context) = { + /** Encode quote using Reflection.Literal + * + * Generate the code + * ```scala + * quotes => quotes.reflect.TreeMethods.asExpr( + * quotes.reflect.Literal.apply(x$1.reflect.Constant..apply()) + * ).asInstanceOf[scala.quoted.Expr[]] + * ``` + * this closure is always applied directly to the actual context and the BetaReduce phase removes it. + */ + def pickleAsLiteral(lit: Literal) = { + val exprType = defn.QuotedExprClass.typeRef.appliedTo(body.tpe) + val reflect = quotes.select("reflect".toTermName) + val typeName = body.tpe.typeSymbol.name + val literalValue = + if lit.const.tag == Constants.NullTag || lit.const.tag == Constants.UnitTag then Nil + else List(body) + val constant = reflect.select(s"${typeName}Constant".toTermName).select(nme.apply).appliedToTermArgs(literalValue) + val literal = reflect.select("Literal".toTermName).select(nme.apply).appliedTo(constant) + reflect.select("TreeMethods".toTermName).select("asExpr".toTermName).appliedTo(literal).asInstance(exprType) } - /** Type used for the hole that will replace this splice */ - def getHoleType(body: tpd.Tree, splice: tpd.Tree)(using Context): Type = - // For most expressions the splice.tpe but there are some types that are lost by lifting - // that can be recoverd from the original tree. Currently the cases are: - // * Method types: the splice represents a method reference - map.get(body.symbol).map(_.tpe.widen).getOrElse(splice.tpe) + /** Encode quote using Reflection.Literal + * + * Generate the code + * ```scala + * quotes => scala.quoted.ToExpr.{BooleanToExpr,ShortToExpr, ...}.apply()(quotes) + * ``` + * this closure is always applied directly to the actual context and the BetaReduce phase removes it. + */ + def liftedValue(lit: Literal, lifter: Symbol) = + val exprType = defn.QuotedExprClass.typeRef.appliedTo(body.tpe) + ref(lifter).appliedToType(originalTp).select(nme.apply).appliedTo(lit).appliedTo(quotes) + + def pickleAsValue(lit: Literal) = { + // TODO should all constants be pickled as Literals? + // Should examine the generated bytecode size to decide and performance + lit.const.tag match { + case Constants.NullTag => pickleAsLiteral(lit) + case Constants.UnitTag => pickleAsLiteral(lit) + case Constants.BooleanTag => liftedValue(lit, defn.ToExprModule_BooleanToExpr) + case Constants.ByteTag => liftedValue(lit, defn.ToExprModule_ByteToExpr) + case Constants.ShortTag => liftedValue(lit, defn.ToExprModule_ShortToExpr) + case Constants.IntTag => liftedValue(lit, defn.ToExprModule_IntToExpr) + case Constants.LongTag => liftedValue(lit, defn.ToExprModule_LongToExpr) + case Constants.FloatTag => liftedValue(lit, defn.ToExprModule_FloatToExpr) + case Constants.DoubleTag => liftedValue(lit, defn.ToExprModule_DoubleToExpr) + case Constants.CharTag => liftedValue(lit, defn.ToExprModule_CharToExpr) + case Constants.StringTag => liftedValue(lit, defn.ToExprModule_StringToExpr) + } + } + + /** Encode quote using QuoteUnpickler.{unpickleExpr, unpickleType} + * + * Generate the code + * ```scala + * quotes => quotes.asInstanceOf[QuoteUnpickler].[]( + * , + * , + * , + * ) + * ``` + * this closure is always applied directly to the actual context and the BetaReduce phase removes it. + */ + def pickleAsTasty() = { + val pickleQuote = PickledQuotes.pickleQuote(body) + val pickledQuoteStrings = pickleQuote match + case x :: Nil => Literal(Constant(x)) + case xs => liftList(xs.map(x => Literal(Constant(x))), defn.StringType) + + // TODO split holes earlier into types and terms. This all holes in each category can have consecutive indices + val (typeSplices, termSplices) = contents.zipWithIndex.partition { + case (splice, _) => splice.tpe.derivesFrom(defn.QuotedTypeClass) + } - def isLiftedSymbol(sym: Symbol)(using Context): Boolean = map.contains(sym) + // This and all closures in typeSplices are removed by the BetaReduce phase + val typeHoles = + if typeSplices.isEmpty then Literal(Constant(null)) // keep pickled quote without contents as small as possible + else + Lambda( + MethodType( + List("idx", "contents").map(name => UniqueName.fresh(name.toTermName).toTermName), + List(defn.IntType, defn.SeqType.appliedTo(defn.AnyType)), + defn.QuotedTypeClass.typeRef.appliedTo(WildcardType)), + args => { + val cases = typeSplices.map { case (splice, idx) => + CaseDef(Literal(Constant(idx)), EmptyTree, splice) + } + cases match + case CaseDef(_, _, rhs) :: Nil => rhs + case _ => Match(args(0).annotated(New(ref(defn.UncheckedAnnot.typeRef))), cases) + } + ) - /** Get the list of embedded trees */ - def getTrees: List[tpd.Tree] = trees.toList + // This and all closures in termSplices are removed by the BetaReduce phase + val termHoles = + if termSplices.isEmpty then Literal(Constant(null)) // keep pickled quote without contents as small as possible + else + Lambda( + MethodType( + List("idx", "contents", "quotes").map(name => UniqueName.fresh(name.toTermName).toTermName), + List(defn.IntType, defn.SeqType.appliedTo(defn.AnyType), defn.QuotesClass.typeRef), + defn.QuotedExprClass.typeRef.appliedTo(defn.AnyType)), + args => { + val cases = termSplices.map { case (splice, idx) => + val defn.FunctionOf(argTypes, defn.FunctionOf(quotesType :: _, _, _, _), _, _) = splice.tpe + val rhs = { + val spliceArgs = argTypes.zipWithIndex.map { (argType, i) => + args(1).select(nme.apply).appliedTo(Literal(Constant(i))).select(defn.Any_asInstanceOf).appliedToType(argType) + } + val Block(List(ddef: DefDef), _) = splice + // TODO: beta reduce inner closure? Or wait until BetaReduce phase? + BetaReduce(ddef, spliceArgs).select(nme.apply).appliedTo(args(2).asInstance(quotesType)) + } + CaseDef(Literal(Constant(idx)), EmptyTree, rhs) + } + cases match + case CaseDef(_, _, rhs) :: Nil => rhs + case _ => Match(args(0).annotated(New(ref(defn.UncheckedAnnot.typeRef))), cases) + } + ) + + val quoteClass = if isType then defn.QuotedTypeClass else defn.QuotedExprClass + val quotedType = quoteClass.typeRef.appliedTo(originalTp) + val lambdaTpe = MethodType(defn.QuotesClass.typeRef :: Nil, quotedType) + val unpickleMeth = if isType then defn.QuoteUnpickler_unpickleType else defn.QuoteUnpickler_unpickleExprV2 + quotes + .asInstance(defn.QuoteUnpicklerClass.typeRef) + .select(unpickleMeth).appliedToType(originalTp) + .appliedTo(pickledQuoteStrings, typeHoles, termHoles).withSpan(body.span) + } - override def toString: String = s"Embedded($trees, $map)" + /** Encode quote using Reflection.TypeRepr.typeConstructorOf + * + * Generate the code + * ```scala + * quotes.reflect.TypeReprMethods.asType( + * quotes.reflect.TypeRepr.typeConstructorOf(classOf[]]) + * ).asInstanceOf[scala.quoted.Type[]] + * ``` + * this closure is always applied directly to the actual context and the BetaReduce phase removes it. + */ + def taggedType() = + val typeType = defn.QuotedTypeClass.typeRef.appliedTo(body.tpe) + val classTree = TypeApply(ref(defn.Predef_classOf.termRef), body :: Nil) + val reflect = quotes.select("reflect".toTermName) + val typeRepr = reflect.select("TypeRepr".toTermName).select("typeConstructorOf".toTermName).appliedTo(classTree) + reflect.select("TypeReprMethods".toTermName).select("asType".toTermName).appliedTo(typeRepr).asInstance(typeType) + + def getLiteral(tree: tpd.Tree): Option[Literal] = tree match + case tree: Literal => Some(tree) + case Block(Nil, e) => getLiteral(e) + case Inlined(_, Nil, e) => getLiteral(e) + case _ => None + + if (isType) then + if contents.isEmpty && body.symbol.isPrimitiveValueClass then taggedType() + else pickleAsTasty() + else + getLiteral(body) match + case Some(lit) => pickleAsValue(lit) + case _ => pickleAsTasty() } + } diff --git a/compiler/src/dotty/tools/dotc/transform/Splicing.scala b/compiler/src/dotty/tools/dotc/transform/Splicing.scala new file mode 100644 index 000000000000..43324cdb258c --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/Splicing.scala @@ -0,0 +1,409 @@ +package dotty.tools.dotc +package transform + +import core._ +import Decorators._ +import Flags._ +import Types._ +import Contexts._ +import Symbols._ +import Constants._ +import ast.Trees._ +import ast.{TreeTypeMap, untpd} +import util.Spans._ +import SymUtils._ +import NameKinds._ +import dotty.tools.dotc.ast.tpd +import StagingContext._ + +import scala.collection.mutable +import dotty.tools.dotc.core.Annotations._ +import dotty.tools.dotc.core.Names._ +import dotty.tools.dotc.core.StdNames._ +import dotty.tools.dotc.quoted._ +import dotty.tools.dotc.transform.TreeMapWithStages._ +import dotty.tools.dotc.typer.Inliner + +import scala.annotation.constructorOnly + +object Splicing: + val name: String = "splicing" + +/** Transforms level 1 splices into holes. To do so it transforms the contents of the splice into + * a lambda that receives all cross-quote references. + * + * Cross-quote reference is a reference to a definition that is not defined in the current quote. + * Those references appear in quotes that are nested in a splice. + * + * After this phase we have the invariant where all splices have the following shape + * ``` + * {{{ | | * | (*) => }}} + * ``` + * where `` does not contain any free references to quoted definitions and `*` + * contains the quotes with references to all cross-quote references. There are some special rules + * for references in the LHS of assignments and cross-quote method references. + * + * In the following code example `x1` and `x2` are cross-quote references. + * ``` + * '{ ... + * val x1: T1 = ??? + * val x2: T2 = ??? + * ${ (q: Quotes) ?=> f('{ g(x1, x2) }) }: T3 + * } + * ``` + * + * This phase identifies cross-quote references such as `x1` and replaces it with an `${x1$}`. + * All cross-quote arguments are directly applied in the lambda. + * + * ``` + * '{ ... + * val x1: T1 = ??? + * val x2: T2 = ??? + * {{{ 0 | T3 | x1, x2 | + * (x1$: Expr[T1], x2$: Expr[T2]) => // body of this lambda does not contain references to x1 or x2 + * (q: Quotes) ?=> f('{ g(${x1$}, ${x2$}) }) + * + * }}} + * } + * ``` + * + * and then performs the same transformation on `'{ g(${x1$}, ${x2$}) }`. + * + */ +class Splicing extends MacroTransform: + import tpd._ + + override def phaseName: String = Splicing.name + + override def run(using Context): Unit = + if (ctx.compilationUnit.needsQuotePickling) super.run(using freshStagingContext) + + protected def newTransformer(using Context): Transformer = Level0QuoteTransformer + + /** Transforms all quotes at level 0 using the `QuoteTransformer` */ + private object Level0QuoteTransformer extends Transformer: + override def transform(tree: tpd.Tree)(using Context): tpd.Tree = + assert(level == 0) + tree match + case Apply(Select(Apply(TypeApply(fn,_), List(code)),nme.apply),List(quotes)) + if fn.symbol == defn.QuotedRuntime_exprQuote => + QuoteTransformer().transform(tree) + case TypeApply(_, _) if tree.symbol == defn.QuotedTypeModule_of => + QuoteTransformer().transform(tree) + case tree: DefDef if tree.symbol.is(Inline) => + tree + case _ => + super.transform(tree) + end Level0QuoteTransformer + + + /** Transforms all direct splices in the current quote and replace them with holes. */ + private class QuoteTransformer() extends Transformer: + private val quotedDefs = mutable.Set.empty[Symbol] + private var holeIdx = -1 + private var typeHoles = mutable.Map.empty[Symbol, Hole] + + override def transform(tree: tpd.Tree)(using Context): tpd.Tree = + tree match + case Apply(fn, List(splicedCode)) if fn.symbol == defn.QuotedRuntime_exprNestedSplice => + if level > 1 then + val splicedCode1 = super.transform(splicedCode)(using spliceContext) + cpy.Apply(tree)(fn, List(splicedCode1)) + else + val newSplicedCode1 = SpliceTransformer(ctx.owner).transformSplice(splicedCode, tree.tpe)(using spliceContext) + val newSplicedCode2 = Level0QuoteTransformer.transform(newSplicedCode1)(using spliceContext) + newSplicedCode2 + case tree: TypeDef if tree.symbol.hasAnnotation(defn.QuotedRuntime_SplicedTypeAnnot) => + val tp @ TypeRef(qual: TermRef, _) = tree.rhs.tpe.hiBound + quotedDefs += tree.symbol + val hole = typeHoles.get(qual.symbol) match + case Some (hole) => cpy.Hole(hole)(content = EmptyTree) + case None => + holeIdx += 1 + val hole = tpd.Hole(false, holeIdx, Nil, ref(qual), TypeTree(tp)) + typeHoles.put(qual.symbol, hole) + hole + cpy.TypeDef(tree)(rhs = hole) + case Apply(Select(Apply(TypeApply(fn,_), List(code)),nme.apply),List(quotes)) + if fn.symbol == defn.QuotedRuntime_exprQuote => + super.transform(tree)(using quoteContext) + case tree: DefTree => + quotedDefs += tree.symbol + transformAnnotations(tree) + super.transform(tree) + case _ => + super.transform(tree).withType(mapAnnots(tree.tpe)) + + private def transformAnnotations(tree: DefTree)(using Context): Unit = + tree.symbol.annotations = tree.symbol.annotations.mapconserve { annot => + val newAnnotTree = transform(annot.tree)(using ctx.withOwner(tree.symbol)) + if (annot.tree == newAnnotTree) annot + else ConcreteAnnotation(newAnnotTree) + } + private def mapAnnots(using Context) = new TypeMap { + override def apply(tp: Type): Type = { + tp match + case tp @ AnnotatedType(underlying, annot) => + val underlying1 = this(underlying) + derivedAnnotatedType(tp, underlying1, annot.derivedAnnotation(transform(annot.tree))) + case _ => mapOver(tp) + } + } + + /** Transforms a splice at level 1 into a hole + * + * Finds all terms and types that are defined in the current quote and used within this splice. + * The resulting hole will contain all of these terms and types as arguments. + * Note that these captured variables are stage correct. + * + * For a `x` of type `T1` and a type `X` defined in the current quote + * ```scala + * ${ (using Quotes) ?=> {... x ... X ...} }: T2 + * ``` + * is transformed into + * ```scala + * {{{ | T2 | x, X | (x$1: Expr[T1], X$1: Type[X]) => (using Quotes) ?=> {... ${x$1} ... X$1.Underlying ...} }}} + * ``` + */ + private class SpliceTransformer(spliceOwner: Symbol) extends Transformer: + private var refBindingMap = mutable.Map.empty[Symbol, (Tree, Symbol)] + /** Reference to the `Quotes` instance of the current level 1 splice */ + private var quotes: Tree = null + private var healedTypes: PCPCheckAndHeal.QuoteTypeTags = null + + def transformSplice(tree: tpd.Tree, tpe: Type)(using Context): tpd.Tree = + assert(level == 0) + val newTree = transform(tree) + val (refs, bindings) = refBindingMap.values.toList.unzip + val bindingsTypes = bindings.map(_.termRef.widenTermRefExpr) + val methType = MethodType(bindingsTypes, newTree.tpe) + val meth = newSymbol(spliceOwner, nme.ANON_FUN, Synthetic | Method, methType) + val ddef = DefDef(meth, List(bindings), newTree.tpe, newTree.changeOwner(ctx.owner, meth)) + val fnType = defn.FunctionType(bindings.size, isContextual = false).appliedTo(bindingsTypes :+ newTree.tpe) + val closure = Block(ddef :: Nil, Closure(Nil, ref(meth), TypeTree(fnType))) + holeIdx += 1 + tpd.Hole(true, holeIdx, refs, closure, TypeTree(tpe)) + + override def transform(tree: tpd.Tree)(using Context): tpd.Tree = + tree match + case tree: RefTree => + if tree.isTerm then + if quotedDefs.contains(tree.symbol) then + val tpe = tree.tpe.widenTermRefExpr match { + case tpw: MethodicType => tpw.toFunctionType(isJava = false) + case tpw => tpw + } + spliced(tpe)(capturedTerm(tree)) + else super.transform(tree) + else // tree.isType then + if containsCapturedType(tree.tpe) then + val capturedTypeSym = capturedType(tree) + if level >= 1 then + TypeTree(healedTypes.getTagRef(capturedTypeSym.termRef)) + else + ref(capturedTypeSym).select(defn.QuotedType_splice) + else super.transform(tree) + case tree: TypeTree => + if containsCapturedType(tree.tpe) then + // TODO factor out the common code with the above case + val capturedTypeSym = capturedType(tree) + if level >= 1 then + TypeTree(healedTypes.getTagRef(capturedTypeSym.termRef)) + else + ref(capturedTypeSym).select(defn.QuotedType_splice) + else tree + case tree @ Assign(lhs: RefTree, rhs) => + if quotedDefs.contains(lhs.symbol) then transformSplicedAssign(tree) + else super.transform(tree) + case Apply(fn, args) if fn.symbol == defn.QuotedRuntime_exprNestedSplice => + val newArgs = args.mapConserve(arg => transform(arg)(using spliceContext)) + cpy.Apply(tree)(fn, newArgs) + case Apply(sel @ Select(app @ Apply(fn, args),nme.apply), quotesArgs) + if fn.symbol == defn.QuotedRuntime_exprQuote => + args match + case List(tree: RefTree) if quotedDefs.contains(tree.symbol) => + capturedTerm(tree) + case _ => + val oldQuotes = quotes + if level == 0 then + quotes = quotesArgs.head + val newArgs = args.mapConserve(arg => transformQuotedContent(arg)(using quoteContext)) + quotes = oldQuotes + cpy.Apply(tree)(cpy.Select(sel)(cpy.Apply(app)(fn, newArgs), nme.apply), quotesArgs) + case Apply(TypeApply(_, List(tpt: Ident)), List(quotes)) + if tree.symbol == defn.QuotedTypeModule_of && quotedDefs.contains(tpt.symbol) => + ref(capturedType(tpt)) + case CapturedApplication(fn, argss) => + transformCapturedApplication(tree, fn, argss) + case _ => + super.transform(tree) + + private def transformQuotedContent(tree: Tree)(using Context): Tree = + if level > 1 then transform(tree) + else { + val old = healedTypes + healedTypes = new PCPCheckAndHeal.QuoteTypeTags(tree.span) + val tree1 = transform(tree) + val tree2 = tree1 match + case Block(stats @ (x :: _), expr) if x.symbol.hasAnnotation(defn.QuotedRuntime_SplicedTypeAnnot) => + Block(healedTypes.getTypeTags ::: stats, expr) + case _ => Block(healedTypes.getTypeTags, tree1) + healedTypes = old + tree2 + } + + type ArgsClause = (List[Tree], Boolean) + private object CapturedApplication { + + /** Matches and application `f(...)` (possibly with several argument clauses) where `f` is captured */ + def unapply(tree: Tree)(using Context): Option[(RefTree, List[ArgsClause])] = tree match + case GenericApply(fn: RefTree, args) if quotedDefs.contains(fn.symbol) => + Some((fn, (args, tree.isInstanceOf[TypeApply]) :: Nil)) + case GenericApply(CapturedApplication(fn, argss), args) => + Some((fn, argss :+ (args, tree.isInstanceOf[TypeApply]))) + case _ => + None + } + + private def containsCapturedType(tpe: Type)(using Context): Boolean = + tpe match + case tpe @ TypeRef(prefix, _) => quotedDefs.contains(tpe.symbol) || containsCapturedType(prefix) + case tpe @ TermRef(prefix, _) => quotedDefs.contains(tpe.symbol) || containsCapturedType(prefix) + case AppliedType(tycon, args) => containsCapturedType(tycon) || args.exists(containsCapturedType) + case _ => false + + /** Transform an assignment `x = e` with a captured `x` to + * `${ Assign(x$1.asTerm, '{e}.asTerm).asExpr.asInstanceOf[Expr[T]] }` + * + * Registers `x` as a captured variable in the hole and creates an `x$1` `Expr` reference to it. + */ + private def transformSplicedAssign(tree: Assign)(using Context): Tree = + spliced(tree.tpe) { + reflect.asExpr(tree.tpe)( + reflect.Assign( + reflect.asTerm(capturedTerm(tree.lhs)), + reflect.asTerm(quoted(tree.rhs)) + ) + ) + } + + /** Transform an assignment `f(a1, a2, ...)` with a captured `f` to + * `${ Apply(f$1.asTerm, List('{a1$}.asTerm, '{a2$}.asTerm, ...)).asExpr.asInstanceOf[Expr[T]] }` + * + * Registers `f` as a captured variable in the hole and creates an `f$1` `Expr` reference to it. + * + * It also handles cases with multiple argument clauses using nested `Apply`/`TypeApply`. + */ + private def transformCapturedApplication(tree: Tree, fn: RefTree, argss: List[ArgsClause])(using Context): Tree = + spliced(tree.tpe) { + def TermList(args: List[Tree]): List[Tree] = + args.map(arg => reflect.asTerm(quoted(transform(arg)(using spliceContext)))) + def TypeTreeList(args: List[Tree]): List[Tree] = + args.map(arg => reflect.Inferred(reflect.TypeReprOf(transform(arg)(using spliceContext).tpe))) + reflect.asExpr(tree.tpe) { + argss.foldLeft[Tree](reflect.asTerm(capturedTerm(fn, defn.AnyType))) { case (acc, (args, isTypeClause)) => + if isTypeClause then reflect.TypeApply(acc, TypeTreeList(args)) + else reflect.Apply(acc, TermList(args)) + } + } + } + + private def capturedTerm(tree: Tree)(using Context): Tree = + val tpe = tree.tpe.widenTermRefExpr match { + case tpw: MethodicType => tpw.toFunctionType(isJava = false) + case tpw => tpw + } + capturedTerm(tree, tpe) + + private def capturedTerm(tree: Tree, tpe: Type)(using Context): Tree = + def newBinding = newSymbol( + spliceOwner, + UniqueName.fresh(tree.symbol.name.toTermName).toTermName, + Param, + defn.QuotedExprClass.typeRef.appliedTo(tpe), + ) + val (_, bindingSym) = refBindingMap.getOrElseUpdate(tree.symbol, (tree, newBinding)) + ref(bindingSym) + + private def capturedType(tree: Tree)(using Context): Symbol = + val tpe = tree.tpe.widenTermRefExpr + def newBinding = newSymbol( + spliceOwner, + UniqueName.fresh(nme.Type).toTermName, + Param, + defn.QuotedTypeClass.typeRef.appliedTo(tpe), + ) + val (_, bindingSym) = refBindingMap.getOrElseUpdate(tree.symbol, (TypeTree(tree.tpe), newBinding)) + bindingSym + + private def spliced(tpe: Type)(body: Context ?=> Tree)(using Context): Tree = + val exprTpe = defn.QuotedExprClass.typeRef.appliedTo(tpe) + val closure = + val methTpe = ContextualMethodType(List(defn.QuotesClass.typeRef), exprTpe) + val meth = newSymbol(ctx.owner, nme.ANON_FUN, Synthetic | Method, methTpe) + Closure(meth, argss => { + val old = quotes + quotes = argss.head.head + val res = body(using ctx.withOwner(meth)).changeOwner(ctx.owner, meth) + quotes = old + res + }) + ref(defn.QuotedRuntime_exprNestedSplice) + .appliedToType(tpe) + .appliedTo(Literal(Constant(null))) // Dropped when creating the Hole that contains it + .appliedTo(closure) + + private def quoted(expr: Tree)(using Context): Tree = + val tpe = expr.tpe.widenTermRefExpr + ref(defn.QuotedRuntime_exprQuote) + .appliedToType(tpe) + .appliedTo(expr) + .select(nme.apply) + .appliedTo(quotes) + + /** Helper methods to construct trees calling methods in `Quotes.reflect` based on the current `quotes` tree */ + private object reflect { + private def reflect(using Context) = quotes.select(nme.reflect) + def TermTpe(using Context) = + reflect.select("Term".toTypeName).tpe + def TypeTreeTpe(using Context) = + reflect.select("TypeTree".toTypeName).tpe + /** Create tree for `quotes.reflect.Apply(, List(*))` */ + def Apply(fn: Tree, args: List[Tree])(using Context) = + val argTrees = PickleQuotes.liftList(args, TermTpe) + reflect.select("Apply".toTermName).select(nme.apply).appliedTo(fn, argTrees) + /** Create tree for `quotes.reflect.TypeApply(, List(*))` */ + def TypeApply(fn: Tree, args: List[Tree])(using Context) = + val argTrees = PickleQuotes.liftList(args, TypeTreeTpe) + reflect.select("TypeApply".toTermName).select(nme.apply).appliedTo(fn, argTrees) + /** Create tree for `quotes.reflect.Assing(, )` */ + def Assign(lhs: Tree, rhs: Tree)(using Context) = + reflect.select("Assign".toTermName).select(nme.apply).appliedTo(lhs, rhs) + /** Create tree for `quotes.reflect.Inferred()` */ + def Inferred(typeTree: Tree)(using Context) = + reflect.select("Inferred".toTermName).select(nme.apply).appliedTo(typeTree) + /** Create tree for `quotes.reflect.TypeRepr.of(Type.of[](quotes))` */ + def TypeReprOf(tpe: Type)(using Context) = + reflect.select("TypeRepr".toTermName).select("of".toTermName) + .appliedToType(tpe) + .appliedTo( + ref(defn.QuotedTypeModule_of) + .appliedToType(tpe) + .appliedTo(quotes) + ) + /** Create tree for `quotes.reflect.asTerm()` */ + def asTerm(expr: Tree)(using Context) = + reflect.select("asTerm".toTermName).appliedTo(expr) + /** Create tree for `quotes.reflect.TreeMethods.asExpr().asInstanceOf[]` */ + def asExpr(tpe: Type)(term: Tree)(using Context) = + reflect.select("TreeMethods".toTermName) + .select("asExpr".toTermName) + .appliedTo(term) + .asInstance(defn.QuotedExprClass.typeRef.appliedTo(tpe)) + } + + end SpliceTransformer + + end QuoteTransformer + +end Splicing diff --git a/compiler/src/dotty/tools/dotc/transform/Staging.scala b/compiler/src/dotty/tools/dotc/transform/Staging.scala index b00506c602ad..1a6fc83bde19 100644 --- a/compiler/src/dotty/tools/dotc/transform/Staging.scala +++ b/compiler/src/dotty/tools/dotc/transform/Staging.scala @@ -39,7 +39,7 @@ class Staging extends MacroTransform { override def allowsImplicitSearch: Boolean = true override def checkPostCondition(tree: Tree)(using Context): Unit = - if (ctx.phase <= pickleQuotesPhase) { + if (ctx.phase <= splicingPhase) { // Recheck that PCP holds but do not heal any inconsistent types as they should already have been heald tree match { case PackageDef(pid, _) if tree.symbol.owner == defn.RootClass => diff --git a/compiler/src/dotty/tools/dotc/typer/Inliner.scala b/compiler/src/dotty/tools/dotc/typer/Inliner.scala index 4fede9b6b9e4..b25c11adbc1a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inliner.scala @@ -1603,7 +1603,11 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) { res override def typedTypeApply(tree: untpd.TypeApply, pt: Type)(using Context): Tree = - inlineIfNeeded(constToLiteral(betaReduce(super.typedTypeApply(tree, pt)))) + val tree1 = inlineIfNeeded(constToLiteral(betaReduce(super.typedTypeApply(tree, pt)))) + if tree1.symbol.isQuote then + ctx.compilationUnit.needsStaging = true + ctx.compilationUnit.needsQuotePickling = true + tree1 override def typedMatch(tree: untpd.Match, pt: Type)(using Context): Tree = val tree1 = diff --git a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala index 86cd7e5f24f3..13a3793e236d 100644 --- a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala +++ b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala @@ -194,6 +194,12 @@ trait QuotesAndSplices { using spliceContext.retractMode(Mode.QuotedPattern).withOwner(spliceOwner(ctx))) pat.select(tpnme.Underlying) + def typedHole(tree: untpd.Hole, pt: Type)(using Context): Tree = { + val tpt = typedType(tree.tpt) + // TODO check args and contents + tree.withType(tpt.tpe) + } + private def checkSpliceOutsideQuote(tree: untpd.Tree)(using Context): Unit = if (level == 0 && !ctx.owner.ownersIterator.exists(_.is(Inline))) report.error("Splice ${...} outside quotes '{...} or inline method", tree.srcPos) diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index 3dcec413540f..ea37630779bf 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -525,6 +525,10 @@ trait TypeAssigner { def assignType(tree: untpd.PackageDef, pid: Tree)(using Context): PackageDef = tree.withType(pid.symbol.termRef) + + def assignType(tree: untpd.Hole, tpt: Tree)(using Context): Hole = + tree.withType(tpt.tpe) + } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 6177faf6b6b0..0d9392db7721 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2828,6 +2828,7 @@ class Typer extends Namer case tree: untpd.Splice => typedSplice(tree, pt) case tree: untpd.TypSplice => typedTypSplice(tree, pt) case tree: untpd.MacroTree => report.error("Unexpected macro", tree.srcPos); tpd.nullLiteral // ill-formed code may reach here + case tree: untpd.Hole => typedHole(tree, pt) case _ => typedUnadapted(desugar(tree), pt, locked) } diff --git a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala index d6e13fbbd168..e125951f7766 100644 --- a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala +++ b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala @@ -2994,7 +2994,11 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler end reflect 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) + val tree = PickledQuotes.unpickleTerm(pickled, typeHole, termHole, version = 1) + new ExprImpl(tree, SpliceScope.getCurrent).asInstanceOf[scala.quoted.Expr[T]] + + def unpickleExprV2[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, version = 2) 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] = diff --git a/library/src/scala/quoted/runtime/QuoteUnpickler.scala b/library/src/scala/quoted/runtime/QuoteUnpickler.scala index 2d8ef54eb9e6..ba2b27bd0db0 100644 --- a/library/src/scala/quoted/runtime/QuoteUnpickler.scala +++ b/library/src/scala/quoted/runtime/QuoteUnpickler.scala @@ -10,8 +10,12 @@ trait QuoteUnpickler: */ def unpickleExpr[T](pickled: String | List[String], typeHole: (Int, Seq[Any]) => Type[?], termHole: (Int, Seq[Any], Quotes) => Expr[?]): scala.quoted.Expr[T] + /** Unpickle `repr` which represents a pickled `Expr` tree, + * replacing splice nodes with `holes`. + */ + def unpickleExprV2[T](pickled: String | List[String], typeHole: (Int, Seq[Any]) => Type[?], termHole: (Int, Seq[Any], Quotes) => Expr[?]): scala.quoted.Expr[T] + /** Unpickle `repr` which represents a pickled `Type` tree, * replacing splice nodes with `holes` */ def unpickleType[T <: AnyKind](pickled: String | List[String], typeHole: (Int, Seq[Any]) => Type[?], termHole: (Int, Seq[Any], Quotes) => Expr[?]): scala.quoted.Type[T] - diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index ae3673016db1..abc2dad254f4 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -3,6 +3,10 @@ import com.typesafe.tools.mima.core._ object MiMaFilters { val Library: Seq[ProblemFilter] = Seq( + // APIs that must be added in 3.2.0 + ProblemFilters.exclude[ReversedMissingMethodProblem]("scala.quoted.runtime.QuoteUnpickler.unpickleExprV2"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.runtime.QuoteUnpickler.unpickleExprV2"), + // Experimental APIs that can be added in 3.2.0 ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.init"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.last"), diff --git a/staging/src/scala/quoted/staging/QuoteCompiler.scala b/staging/src/scala/quoted/staging/QuoteCompiler.scala index 7b1899821f96..eee2dacdc5f5 100644 --- a/staging/src/scala/quoted/staging/QuoteCompiler.scala +++ b/staging/src/scala/quoted/staging/QuoteCompiler.scala @@ -16,7 +16,7 @@ import dotty.tools.dotc.core.Symbols._ import dotty.tools.dotc.core.Types.ExprType import dotty.tools.dotc.quoted.PickledQuotes import dotty.tools.dotc.transform.Splicer.checkEscapedVariables -import dotty.tools.dotc.transform.{Inlining, Staging, PickleQuotes} +import dotty.tools.dotc.transform.{Inlining, Staging, Splicing, PickleQuotes} import dotty.tools.dotc.util.Spans.Span import dotty.tools.dotc.util.{SourceFile, NoSourcePosition} import dotty.tools.io.{Path, VirtualFile} @@ -41,6 +41,7 @@ private class QuoteCompiler extends Compiler: override protected def picklerPhases: List[List[Phase]] = List(new Inlining) :: List(new Staging) :: + List(new Splicing) :: List(new PickleQuotes) :: Nil diff --git a/tests/pos-macros/InlinedTypeOf.scala b/tests/pos-macros/InlinedTypeOf.scala new file mode 100644 index 000000000000..cd708eb02322 --- /dev/null +++ b/tests/pos-macros/InlinedTypeOf.scala @@ -0,0 +1,11 @@ +import scala.quoted._ + +class Sm[T](t: T) + +object Foo { + + inline def foo[T] = { compiletime.summonInline[Type[T]]; ??? } + + def toexpr[T: Type](using Quotes) = foo[Sm[T]] + +} diff --git a/tests/pos-macros/captured-quoted-def-1/Macro_1.scala b/tests/pos-macros/captured-quoted-def-1/Macro_1.scala new file mode 100644 index 000000000000..fadde58dd168 --- /dev/null +++ b/tests/pos-macros/captured-quoted-def-1/Macro_1.scala @@ -0,0 +1,6 @@ +import scala.quoted.* + +inline def myMacro: Int = ${ myMacroExpr } + +private def myMacroExpr(using Quotes): Expr[Int] = + '{ def y: Int = 1; ${ identity('y) } } diff --git a/tests/pos-macros/captured-quoted-def-1/Test_2.scala b/tests/pos-macros/captured-quoted-def-1/Test_2.scala new file mode 100644 index 000000000000..76a9e17659db --- /dev/null +++ b/tests/pos-macros/captured-quoted-def-1/Test_2.scala @@ -0,0 +1 @@ +def test = myMacro diff --git a/tests/pos-macros/captured-quoted-def-2/Macro_1.scala b/tests/pos-macros/captured-quoted-def-2/Macro_1.scala new file mode 100644 index 000000000000..3d090311bca1 --- /dev/null +++ b/tests/pos-macros/captured-quoted-def-2/Macro_1.scala @@ -0,0 +1,6 @@ +import scala.quoted.* + +inline def myMacro: Int = ${ myMacroExpr } + +private def myMacroExpr(using Quotes): Expr[Int] = + '{ def y(): Int = 1; ${ identity('{ y() }) } } diff --git a/tests/pos-macros/captured-quoted-def-2/Test_2.scala b/tests/pos-macros/captured-quoted-def-2/Test_2.scala new file mode 100644 index 000000000000..76a9e17659db --- /dev/null +++ b/tests/pos-macros/captured-quoted-def-2/Test_2.scala @@ -0,0 +1 @@ +def test = myMacro diff --git a/tests/pos-macros/captured-quoted-def-3/Macro_1.scala b/tests/pos-macros/captured-quoted-def-3/Macro_1.scala new file mode 100644 index 000000000000..c3ccfdda56a4 --- /dev/null +++ b/tests/pos-macros/captured-quoted-def-3/Macro_1.scala @@ -0,0 +1,6 @@ +import scala.quoted.* + +inline def myMacro: Int = ${ myMacroExpr } + +private def myMacroExpr(using Quotes): Expr[Int] = + '{ def y(i: Int): Int = 1; ${ identity('{ y(1) }) } } diff --git a/tests/pos-macros/captured-quoted-def-3/Test_2.scala b/tests/pos-macros/captured-quoted-def-3/Test_2.scala new file mode 100644 index 000000000000..76a9e17659db --- /dev/null +++ b/tests/pos-macros/captured-quoted-def-3/Test_2.scala @@ -0,0 +1 @@ +def test = myMacro diff --git a/tests/pos-macros/captured-quoted-def-4/Macro_1.scala b/tests/pos-macros/captured-quoted-def-4/Macro_1.scala new file mode 100644 index 000000000000..e88084be84b3 --- /dev/null +++ b/tests/pos-macros/captured-quoted-def-4/Macro_1.scala @@ -0,0 +1,6 @@ +import scala.quoted.* + +inline def myMacro: Int = ${ myMacroExpr } + +private def myMacroExpr(using Quotes): Expr[Int] = + '{ def y(i: Int)(j: Int, k: Int): Int = 1; ${ identity('{ y(1)(2, 3) }) } } diff --git a/tests/pos-macros/captured-quoted-def-4/Test_2.scala b/tests/pos-macros/captured-quoted-def-4/Test_2.scala new file mode 100644 index 000000000000..76a9e17659db --- /dev/null +++ b/tests/pos-macros/captured-quoted-def-4/Test_2.scala @@ -0,0 +1 @@ +def test = myMacro diff --git a/tests/pos-macros/captured-quoted-def-5/Macro_1.scala b/tests/pos-macros/captured-quoted-def-5/Macro_1.scala new file mode 100644 index 000000000000..b047ec26b621 --- /dev/null +++ b/tests/pos-macros/captured-quoted-def-5/Macro_1.scala @@ -0,0 +1,6 @@ +import scala.quoted.* + +inline def myMacro: Int = ${ myMacroExpr } + +private def myMacroExpr(using Quotes): Expr[Int] = + '{ def y[T](i: T): T = i; ${ identity('{ y[Int](1) }) } } diff --git a/tests/pos-macros/captured-quoted-def-5/Test_2.scala b/tests/pos-macros/captured-quoted-def-5/Test_2.scala new file mode 100644 index 000000000000..76a9e17659db --- /dev/null +++ b/tests/pos-macros/captured-quoted-def-5/Test_2.scala @@ -0,0 +1 @@ +def test = myMacro diff --git a/tests/pos-macros/captured-quoted-multy-stage/Macro_1.scala b/tests/pos-macros/captured-quoted-multy-stage/Macro_1.scala new file mode 100644 index 000000000000..533125bdef90 --- /dev/null +++ b/tests/pos-macros/captured-quoted-multy-stage/Macro_1.scala @@ -0,0 +1,9 @@ +import scala.quoted.* + +inline def myMacro: Any = ${ myMacroExpr('{1}) } + +def myMacroExpr(x: Expr[Int])(using Quotes): Expr[Any] = + '{ + def f(using q1: Quotes) = '{ 1 + ${Expr($x)} } + () + } diff --git a/tests/pos-macros/captured-quoted-multy-stage/Test_2.scala b/tests/pos-macros/captured-quoted-multy-stage/Test_2.scala new file mode 100644 index 000000000000..76a9e17659db --- /dev/null +++ b/tests/pos-macros/captured-quoted-multy-stage/Test_2.scala @@ -0,0 +1 @@ +def test = myMacro diff --git a/tests/pos-macros/captured-quoted-type-1/Macro_1.scala b/tests/pos-macros/captured-quoted-type-1/Macro_1.scala new file mode 100644 index 000000000000..fdea3ecf4f7f --- /dev/null +++ b/tests/pos-macros/captured-quoted-type-1/Macro_1.scala @@ -0,0 +1,10 @@ +import scala.quoted.* + +inline def myMacro: Any = ${ myMacroExpr } + +def myMacroExpr(using Quotes): Expr[Any] = + '{ + def f[Z] = + ${ identity('{ val y: Z = ??? }) } + 42 + } diff --git a/tests/pos-macros/captured-quoted-type-1/Test_2.scala b/tests/pos-macros/captured-quoted-type-1/Test_2.scala new file mode 100644 index 000000000000..76a9e17659db --- /dev/null +++ b/tests/pos-macros/captured-quoted-type-1/Test_2.scala @@ -0,0 +1 @@ +def test = myMacro diff --git a/tests/pos-macros/captured-quoted-type-2/Macro_1.scala b/tests/pos-macros/captured-quoted-type-2/Macro_1.scala new file mode 100644 index 000000000000..b9cf18de7ac4 --- /dev/null +++ b/tests/pos-macros/captured-quoted-type-2/Macro_1.scala @@ -0,0 +1,10 @@ +import scala.quoted.* + +inline def myMacro: Any = ${ myMacroExpr } + +def myMacroExpr[T: Type](using Quotes): Expr[Any] = + '{ + def f[Z] = + ${ identity('{ val y: (T, Z) = ??? }) } + 42 + } diff --git a/tests/pos-macros/captured-quoted-type-2/Test_2.scala b/tests/pos-macros/captured-quoted-type-2/Test_2.scala new file mode 100644 index 000000000000..76a9e17659db --- /dev/null +++ b/tests/pos-macros/captured-quoted-type-2/Test_2.scala @@ -0,0 +1 @@ +def test = myMacro diff --git a/tests/pos-macros/i12440.scala b/tests/pos-macros/i12440.scala new file mode 100644 index 000000000000..4b4c56fef568 --- /dev/null +++ b/tests/pos-macros/i12440.scala @@ -0,0 +1,21 @@ +import scala.quoted.* + +trait Mirror: + type ElemTypes <: Tuple + +class Eq: + + def test1(using Quotes): Unit = '{ + val m: Mirror = ??? + ${ summonType[m.ElemTypes]; ??? } + ${ summonType[List[m.ElemTypes]]; ??? } + } + + def test2(using Quotes): Unit = '{ + val m: Mirror = ??? + type ET = m.ElemTypes + ${ summonType[ET]; ??? } + ${ summonType[List[ET]]; ??? } + } + + def summonType[X](using Type[X]) = ??? diff --git a/tests/pos-macros/i13563.scala b/tests/pos-macros/i13563.scala new file mode 100644 index 000000000000..91a36f447d4a --- /dev/null +++ b/tests/pos-macros/i13563.scala @@ -0,0 +1,3 @@ +import scala.quoted.* +def foo(using Quotes): Unit = + '{ def bar[T](): Unit = ${ summon[Type[T]]; ??? }; () } diff --git a/tests/pos-macros/i4774f.scala b/tests/pos-macros/i4774f.scala index 59d21eb2d2d4..336f20e19280 100644 --- a/tests/pos-macros/i4774f.scala +++ b/tests/pos-macros/i4774f.scala @@ -7,4 +7,10 @@ object Test { def loop2[T](x: Expr[T])(implicit t: Type[T], qctx: Quotes): Expr[T] = '{ def y(): T = $x; ${ loop2('{y()}) } } + + def loop3[T](x: Expr[T])(using Type[T], Quotes): Expr[T] = + '{ def y(i: Int): T = $x; ${ loop2('{y(1)}) } } + + def loop4[T](x: Expr[T])(using Type[T], Quotes): Expr[T] = + '{ def y(i: Int)(j: Int): T = $x; ${ loop2('{y(1)(2)}) } } } diff --git a/tests/pos-macros/i8100.scala b/tests/pos-macros/i8100.scala index 0cf80fe920ee..1a6b5f3d5ba6 100644 --- a/tests/pos-macros/i8100.scala +++ b/tests/pos-macros/i8100.scala @@ -14,8 +14,8 @@ def f[T: Type](using Quotes) = ${ g[m.E](using Type.of[ME]) } ${ g[ME](using Type.of[m.E]) } ${ g[m.E](using Type.of[m.E]) } - // ${ g[ME] } // FIXME: issue seems to be in PickleQuotes - // ${ g[m.E] } // FIXME: issue seems to be in PickleQuotes + ${ g[ME] } + ${ g[m.E] } } def g[T](using Type[T]) = ??? diff --git a/tests/pos-macros/typetags.scala b/tests/pos-macros/typetags.scala index 3a6e60c2e91c..2cf285888272 100644 --- a/tests/pos-macros/typetags.scala +++ b/tests/pos-macros/typetags.scala @@ -7,5 +7,9 @@ object Test { implicitly[Type[List[Int]]] implicitly[Type[T]] implicitly[Type[List[T]]] + Type.of[Int] + Type.of[List[Int]] + Type.of[T] + Type.of[List[T]] } }