Skip to content

Commit c720e5a

Browse files
Place staged type captures in Quote AST (#17424)
This change makes it easier to handle instances `Type[T]` that are inserted into a level 0 quote. These used to be encoded in the staging phase as type aliases `@TypeSplice type A = t.Underlying`. This makes it hard to debug and check invariants. Instead, we can collect the list of type tags needed and store them in the quote. At the same time, we can convert references to those types into `t.Underlying`. We can generate the `@TypeSplice type A = t.Underlying` just before pickling and replace all references to `t.Underlying` with `A` at that point. This is only an internal representation change while transforming the quote. It will not affect TASTy.
2 parents e8c6ebe + 0ee80f9 commit c720e5a

25 files changed

+249
-251
lines changed

compiler/src/dotty/tools/dotc/CompilationUnit.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ object CompilationUnit {
157157
if tree.symbol.is(Flags.Inline) then
158158
containsInline = true
159159
tree match
160-
case tpd.Quote(_) =>
160+
case _: tpd.Quote =>
161161
containsQuote = true
162162
case tree: tpd.Apply if tree.symbol == defn.QuotedTypeModule_of =>
163163
containsQuote = true

compiler/src/dotty/tools/dotc/ast/Desugar.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1976,7 +1976,7 @@ object desugar {
19761976
trees foreach collect
19771977
case Block(Nil, expr) =>
19781978
collect(expr)
1979-
case Quote(body) =>
1979+
case Quote(body, _) =>
19801980
new UntypedTreeTraverser {
19811981
def traverse(tree: untpd.Tree)(using Context): Unit = tree match {
19821982
case Splice(expr) => collect(expr)

compiler/src/dotty/tools/dotc/ast/Trees.scala

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -690,9 +690,14 @@ object Trees {
690690
* when type checking. TASTy files will not contain type quotes. Type quotes are used again
691691
* in the `staging` phase to represent the reification of `Type.of[T]]`.
692692
*
693+
* Type tags `tags` are always empty before the `staging` phase. Tags for stage inconsistent
694+
* types are added in the `staging` phase to level 0 quotes. Tags for types that refer to
695+
* definitions in an outer quote are added in the `splicing` phase
696+
*
693697
* @param body The tree that was quoted
698+
* @param tags Term references to instances of `Type[T]` for `T`s that are used in the quote
694699
*/
695-
case class Quote[+T <: Untyped] private[ast] (body: Tree[T])(implicit @constructorOnly src: SourceFile)
700+
case class Quote[+T <: Untyped] private[ast] (body: Tree[T], tags: List[Tree[T]])(implicit @constructorOnly src: SourceFile)
696701
extends TermTree[T] {
697702
type ThisTree[+T <: Untyped] = Quote[T]
698703

@@ -1313,9 +1318,9 @@ object Trees {
13131318
case tree: Inlined if (call eq tree.call) && (bindings eq tree.bindings) && (expansion eq tree.expansion) => tree
13141319
case _ => finalize(tree, untpd.Inlined(call, bindings, expansion)(sourceFile(tree)))
13151320
}
1316-
def Quote(tree: Tree)(body: Tree)(using Context): Quote = tree match {
1317-
case tree: Quote if (body eq tree.body) => tree
1318-
case _ => finalize(tree, untpd.Quote(body)(sourceFile(tree)))
1321+
def Quote(tree: Tree)(body: Tree, tags: List[Tree])(using Context): Quote = tree match {
1322+
case tree: Quote if (body eq tree.body) && (tags eq tree.tags) => tree
1323+
case _ => finalize(tree, untpd.Quote(body, tags)(sourceFile(tree)))
13191324
}
13201325
def Splice(tree: Tree)(expr: Tree)(using Context): Splice = tree match {
13211326
case tree: Splice if (expr eq tree.expr) => tree
@@ -1558,8 +1563,8 @@ object Trees {
15581563
case Thicket(trees) =>
15591564
val trees1 = transform(trees)
15601565
if (trees1 eq trees) tree else Thicket(trees1)
1561-
case tree @ Quote(body) =>
1562-
cpy.Quote(tree)(transform(body)(using quoteContext))
1566+
case Quote(body, tags) =>
1567+
cpy.Quote(tree)(transform(body)(using quoteContext), transform(tags))
15631568
case tree @ Splice(expr) =>
15641569
cpy.Splice(tree)(transform(expr)(using spliceContext))
15651570
case tree @ Hole(isTerm, idx, args, content, tpt) =>
@@ -1703,8 +1708,8 @@ object Trees {
17031708
this(this(x, arg), annot)
17041709
case Thicket(ts) =>
17051710
this(x, ts)
1706-
case Quote(body) =>
1707-
this(x, body)(using quoteContext)
1711+
case Quote(body, tags) =>
1712+
this(this(x, body)(using quoteContext), tags)
17081713
case Splice(expr) =>
17091714
this(x, expr)(using spliceContext)
17101715
case Hole(_, _, args, content, tpt) =>

compiler/src/dotty/tools/dotc/ast/tpd.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,8 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
170170
def Inlined(call: Tree, bindings: List[MemberDef], expansion: Tree)(using Context): Inlined =
171171
ta.assignType(untpd.Inlined(call, bindings, expansion), bindings, expansion)
172172

173-
def Quote(body: Tree)(using Context): Quote =
174-
untpd.Quote(body).withBodyType(body.tpe)
173+
def Quote(body: Tree, tags: List[Tree])(using Context): Quote =
174+
untpd.Quote(body, tags).withBodyType(body.tpe)
175175

176176
def Splice(expr: Tree, tpe: Type)(using Context): Splice =
177177
untpd.Splice(expr).withType(tpe)

compiler/src/dotty/tools/dotc/ast/untpd.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
397397
def SeqLiteral(elems: List[Tree], elemtpt: Tree)(implicit src: SourceFile): SeqLiteral = new SeqLiteral(elems, elemtpt)
398398
def JavaSeqLiteral(elems: List[Tree], elemtpt: Tree)(implicit src: SourceFile): JavaSeqLiteral = new JavaSeqLiteral(elems, elemtpt)
399399
def Inlined(call: tpd.Tree, bindings: List[MemberDef], expansion: Tree)(implicit src: SourceFile): Inlined = new Inlined(call, bindings, expansion)
400-
def Quote(body: Tree)(implicit src: SourceFile): Quote = new Quote(body)
400+
def Quote(body: Tree, tags: List[Tree])(implicit src: SourceFile): Quote = new Quote(body, tags)
401401
def Splice(expr: Tree)(implicit src: SourceFile): Splice = new Splice(expr)
402402
def TypeTree()(implicit src: SourceFile): TypeTree = new TypeTree()
403403
def InferredTypeTree()(implicit src: SourceFile): TypeTree = new InferredTypeTree()

compiler/src/dotty/tools/dotc/core/Phases.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ object Phases {
211211
private var mySbtExtractDependenciesPhase: Phase = _
212212
private var myPicklerPhase: Phase = _
213213
private var myInliningPhase: Phase = _
214+
private var myStagingPhase: Phase = _
214215
private var mySplicingPhase: Phase = _
215216
private var myFirstTransformPhase: Phase = _
216217
private var myCollectNullableFieldsPhase: Phase = _
@@ -235,6 +236,7 @@ object Phases {
235236
final def sbtExtractDependenciesPhase: Phase = mySbtExtractDependenciesPhase
236237
final def picklerPhase: Phase = myPicklerPhase
237238
final def inliningPhase: Phase = myInliningPhase
239+
final def stagingPhase: Phase = myStagingPhase
238240
final def splicingPhase: Phase = mySplicingPhase
239241
final def firstTransformPhase: Phase = myFirstTransformPhase
240242
final def collectNullableFieldsPhase: Phase = myCollectNullableFieldsPhase
@@ -262,6 +264,7 @@ object Phases {
262264
mySbtExtractDependenciesPhase = phaseOfClass(classOf[sbt.ExtractDependencies])
263265
myPicklerPhase = phaseOfClass(classOf[Pickler])
264266
myInliningPhase = phaseOfClass(classOf[Inlining])
267+
myStagingPhase = phaseOfClass(classOf[Staging])
265268
mySplicingPhase = phaseOfClass(classOf[Splicing])
266269
myFirstTransformPhase = phaseOfClass(classOf[FirstTransform])
267270
myCollectNullableFieldsPhase = phaseOfClass(classOf[CollectNullableFields])
@@ -449,6 +452,7 @@ object Phases {
449452
def sbtExtractDependenciesPhase(using Context): Phase = ctx.base.sbtExtractDependenciesPhase
450453
def picklerPhase(using Context): Phase = ctx.base.picklerPhase
451454
def inliningPhase(using Context): Phase = ctx.base.inliningPhase
455+
def stagingPhase(using Context): Phase = ctx.base.stagingPhase
452456
def splicingPhase(using Context): Phase = ctx.base.splicingPhase
453457
def firstTransformPhase(using Context): Phase = ctx.base.firstTransformPhase
454458
def refchecksPhase(using Context): Phase = ctx.base.refchecksPhase

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,7 @@ class TreePickler(pickler: TastyPickler) {
665665
pickleTree(hi)
666666
pickleTree(alias)
667667
}
668-
case tree @ Quote(body) =>
668+
case tree @ Quote(body, Nil) =>
669669
// TODO: Add QUOTE tag to TASTy
670670
assert(body.isTerm,
671671
"""Quote with type should not be pickled.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1269,7 +1269,7 @@ class TreeUnpickler(reader: TastyReader,
12691269

12701270
def quotedExpr(fn: Tree, args: List[Tree]): Tree =
12711271
val TypeApply(_, targs) = fn: @unchecked
1272-
untpd.Quote(args.head).withBodyType(targs.head.tpe)
1272+
untpd.Quote(args.head, Nil).withBodyType(targs.head.tpe)
12731273

12741274
def splicedExpr(fn: Tree, args: List[Tree]): Tree =
12751275
val TypeApply(_, targs) = fn: @unchecked

compiler/src/dotty/tools/dotc/inlines/Inliner.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,7 @@ class Inliner(val call: tpd.Tree)(using Context):
827827

828828
override def typedQuote(tree: untpd.Quote, pt: Type)(using Context): Tree =
829829
super.typedQuote(tree, pt) match
830-
case Quote(Splice(inner)) => inner
830+
case Quote(Splice(inner), _) => inner
831831
case tree1 =>
832832
ctx.compilationUnit.needsStaging = true
833833
tree1

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,7 +1243,7 @@ object Parsers {
12431243
}
12441244
}
12451245
in.nextToken()
1246-
Quote(t)
1246+
Quote(t, Nil)
12471247
}
12481248
else
12491249
if !in.featureEnabled(Feature.symbolLiterals) then
@@ -2480,7 +2480,7 @@ object Parsers {
24802480
val body =
24812481
if (in.token == LBRACKET) inBrackets(typ())
24822482
else stagedBlock()
2483-
Quote(body)
2483+
Quote(body, Nil)
24842484
}
24852485
}
24862486
case NEW =>

compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -726,11 +726,12 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
726726
"Thicket {" ~~ toTextGlobal(trees, "\n") ~~ "}"
727727
case MacroTree(call) =>
728728
keywordStr("macro ") ~ toTextGlobal(call)
729-
case tree @ Quote(body) =>
729+
case tree @ Quote(body, tags) =>
730+
val tagsText = (keywordStr("<") ~ toTextGlobal(tags, ", ") ~ keywordStr(">")).provided(tree.tags.nonEmpty)
730731
val exprTypeText = (keywordStr("[") ~ toTextGlobal(tree.bodyType) ~ keywordStr("]")).provided(printDebug && tree.typeOpt.exists)
731732
val open = if (body.isTerm) keywordStr("{") else keywordStr("[")
732733
val close = if (body.isTerm) keywordStr("}") else keywordStr("]")
733-
keywordStr("'") ~ exprTypeText ~ open ~ toTextGlobal(body) ~ close
734+
keywordStr("'") ~ tagsText ~ exprTypeText ~ open ~ toTextGlobal(body) ~ close
734735
case Splice(expr) =>
735736
val spliceTypeText = (keywordStr("[") ~ toTextGlobal(tree.typeOpt) ~ keywordStr("]")).provided(printDebug && tree.typeOpt.exists)
736737
keywordStr("$") ~ spliceTypeText ~ keywordStr("{") ~ toTextGlobal(expr) ~ keywordStr("}")

compiler/src/dotty/tools/dotc/staging/CrossStageSafety.scala

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,36 @@ import dotty.tools.dotc.util.Property
1616
import dotty.tools.dotc.util.Spans._
1717
import dotty.tools.dotc.util.SrcPos
1818

19-
/** Checks that staging level consistency holds and heals staged types .
19+
/** Checks that staging level consistency holds and heals staged types.
2020
*
2121
* Local term references are level consistent if and only if they are used at the same level as their definition.
2222
*
2323
* Local type references can be used at the level of their definition or lower. If used used at a higher level,
2424
* it will be healed if possible, otherwise it is inconsistent.
2525
*
26-
* Type healing consists in transforming a level inconsistent type `T` into `summon[Type[T]].Underlying`.
26+
* Healing a type consists in replacing locally defined types defined at staging level 0 and used in higher levels.
27+
* For each type local `T` that is defined at level 0 and used in a quote, we summon a tag `t: Type[T]`. This `t`
28+
* tag must be defined at level 0. The tags will be listed in the `tags` of the level 0 quote (`'<t>{ ... }`) and
29+
* each reference to `T` will be replaced by `t.Underlying` in the body of the quote.
30+
*
31+
* We delay the healing of types in quotes at level 1 or higher until those quotes reach level 0. At this point
32+
* more types will be statically known and fewer types will need to be healed. This also keeps the nested quotes
33+
* in their original form, we do not want macro users to see any artifacts of this phase in quoted expressions
34+
* they might inspect.
35+
*
36+
* Type heal example:
2737
*
28-
* As references to types do not necessarily have an associated tree it is not always possible to replace the types directly.
29-
* Instead we always generate a type alias for it and place it at the start of the surrounding quote. This also avoids duplication.
30-
* For example:
3138
* '{
3239
* val x: List[T] = List[T]()
40+
* '{ .. T .. }
3341
* ()
3442
* }
3543
*
3644
* is transformed to
3745
*
38-
* '{
39-
* type t$1 = summon[Type[T]].Underlying
40-
* val x: List[t$1] = List[t$1]();
46+
* '<t>{ // where `t` is a given term of type `Type[T]`
47+
* val x: List[t.Underlying] = List[t.Underlying]();
48+
* '{ .. t.Underlying .. }
4149
* ()
4250
* }
4351
*
@@ -56,11 +64,18 @@ class CrossStageSafety extends TreeMapWithStages {
5664
case tree: Quote =>
5765
if (ctx.property(InAnnotation).isDefined)
5866
report.error("Cannot have a quote in an annotation", tree.srcPos)
59-
val body1 = transformQuoteBody(tree.body, tree.span)
60-
val stripAnnotationsDeep: TypeMap = new TypeMap:
61-
def apply(tp: Type): Type = mapOver(tp.stripAnnots)
62-
val bodyType1 = healType(tree.srcPos)(stripAnnotationsDeep(tree.bodyType))
63-
cpy.Quote(tree)(body1).withBodyType(bodyType1)
67+
68+
val tree1 =
69+
val stripAnnotationsDeep: TypeMap = new TypeMap:
70+
def apply(tp: Type): Type = mapOver(tp.stripAnnots)
71+
val bodyType1 = healType(tree.srcPos)(stripAnnotationsDeep(tree.bodyType))
72+
tree.withBodyType(bodyType1)
73+
74+
if level == 0 then
75+
val (tags, body1) = inContextWithQuoteTypeTags { transform(tree1.body)(using quoteContext) }
76+
cpy.Quote(tree1)(body1, tags)
77+
else
78+
super.transform(tree1)
6479

6580
case CancelledSplice(tree) =>
6681
transform(tree) // Optimization: `${ 'x }` --> `x`
@@ -74,22 +89,18 @@ class CrossStageSafety extends TreeMapWithStages {
7489
case tree @ QuotedTypeOf(body) =>
7590
if (ctx.property(InAnnotation).isDefined)
7691
report.error("Cannot have a quote in an annotation", tree.srcPos)
77-
body.tpe match
78-
case DirectTypeOf(termRef) =>
79-
// Optimization: `quoted.Type.of[x.Underlying](quotes)` --> `x`
80-
ref(termRef).withSpan(tree.span)
81-
case _ =>
82-
transformQuoteBody(body, tree.span) match
83-
case DirectTypeOf.Healed(termRef) =>
84-
// Optimization: `quoted.Type.of[@SplicedType type T = x.Underlying; T](quotes)` --> `x`
85-
ref(termRef).withSpan(tree.span)
86-
case transformedBody =>
87-
val quotes = transform(tree.args.head)
88-
// `quoted.Type.of[<body>](quotes)` --> `quoted.Type.of[<body2>](quotes)`
89-
val TypeApply(fun, _) = tree.fun: @unchecked
90-
if level != 0 then cpy.Apply(tree)(cpy.TypeApply(tree.fun)(fun, transformedBody :: Nil), quotes :: Nil)
91-
else tpd.Quote(transformedBody).select(nme.apply).appliedTo(quotes).withSpan(tree.span)
9292

93+
if level == 0 then
94+
val (tags, body1) = inContextWithQuoteTypeTags { transform(body)(using quoteContext) }
95+
val quotes = transform(tree.args.head)
96+
tags match
97+
case tag :: Nil if body1.isType && body1.tpe =:= tag.tpe.select(tpnme.Underlying) =>
98+
tag // Optimization: `quoted.Type.of[x.Underlying](quotes)` --> `x`
99+
case _ =>
100+
// `quoted.Type.of[<body>](<quotes>)` --> `'[<body1>].apply(<quotes>)`
101+
tpd.Quote(body1, tags).select(nme.apply).appliedTo(quotes).withSpan(tree.span)
102+
else
103+
super.transform(tree)
93104
case _: DefDef if tree.symbol.isInlineMethod =>
94105
tree
95106

@@ -137,17 +148,6 @@ class CrossStageSafety extends TreeMapWithStages {
137148
super.transform(tree)
138149
end transform
139150

140-
private def transformQuoteBody(body: Tree, span: Span)(using Context): Tree = {
141-
val taggedTypes = new QuoteTypeTags(span)
142-
val contextWithQuote =
143-
if level == 0 then contextWithQuoteTypeTags(taggedTypes)(using quoteContext)
144-
else quoteContext
145-
val transformedBody = transform(body)(using contextWithQuote)
146-
taggedTypes.getTypeTags match
147-
case Nil => transformedBody
148-
case tags => tpd.Block(tags, transformedBody).withSpan(body.span)
149-
}
150-
151151
def transformTypeAnnotationSplices(tp: Type)(using Context) = new TypeMap {
152152
def apply(tp: Type): Type = tp match
153153
case tp: AnnotatedType =>
@@ -234,7 +234,7 @@ class CrossStageSafety extends TreeMapWithStages {
234234
def unapply(tree: Splice): Option[Tree] =
235235
def rec(tree: Tree): Option[Tree] = tree match
236236
case Block(Nil, expr) => rec(expr)
237-
case Quote(inner) => Some(inner)
237+
case Quote(inner, _) => Some(inner)
238238
case _ => None
239239
rec(tree.expr)
240240
}

compiler/src/dotty/tools/dotc/staging/DirectTypeOf.scala

Lines changed: 0 additions & 25 deletions
This file was deleted.

compiler/src/dotty/tools/dotc/staging/HealType.scala

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class HealType(pos: SrcPos)(using Context) extends TypeMap {
2525
*
2626
* If `T` is a reference to a type at the wrong level, try to heal it by replacing it with
2727
* a type tag of type `quoted.Type[T]`.
28-
* The tag is generated by an instance of `QuoteTypeTags` directly if the splice is explicit
28+
* The tag is recorded by an instance of `QuoteTypeTags` directly if the splice is explicit
2929
* or indirectly by `tryHeal`.
3030
*/
3131
def apply(tp: Type): Type =
@@ -43,11 +43,9 @@ class HealType(pos: SrcPos)(using Context) extends TypeMap {
4343

4444
private def healTypeRef(tp: TypeRef): Type =
4545
tp.prefix match
46-
case NoPrefix if tp.typeSymbol.hasAnnotation(defn.QuotedRuntime_SplicedTypeAnnot) =>
47-
tp
4846
case prefix: TermRef if tp.symbol.isTypeSplice =>
4947
checkNotWildcardSplice(tp)
50-
if level == 0 then tp else getQuoteTypeTags.getTagRef(prefix)
48+
if level == 0 then tp else getTagRef(prefix)
5149
case _: NamedType | _: ThisType | NoPrefix =>
5250
if levelInconsistentRootOfPath(tp).exists then
5351
tryHeal(tp)
@@ -58,7 +56,7 @@ class HealType(pos: SrcPos)(using Context) extends TypeMap {
5856

5957
private object NonSpliceAlias:
6058
def unapply(tp: TypeRef)(using Context): Option[Type] = tp.underlying match
61-
case TypeAlias(alias) if !tp.symbol.isTypeSplice && !tp.typeSymbol.hasAnnotation(defn.QuotedRuntime_SplicedTypeAnnot) => Some(alias)
59+
case TypeAlias(alias) if !tp.symbol.isTypeSplice => Some(alias)
6260
case _ => None
6361

6462
private def checkNotWildcardSplice(splice: TypeRef): Unit =
@@ -78,7 +76,7 @@ class HealType(pos: SrcPos)(using Context) extends TypeMap {
7876

7977
/** Try to heal reference to type `T` used in a higher level than its definition.
8078
* Returns a reference to a type tag generated by `QuoteTypeTags` that contains a
81-
* reference to a type alias containing the equivalent of `${summon[quoted.Type[T]]}`.
79+
* reference to a type alias containing the equivalent of `${summon[quoted.Type[T]]}.Underlying`.
8280
* Emits an error if `T` cannot be healed and returns `T`.
8381
*/
8482
protected def tryHeal(tp: TypeRef): Type = {
@@ -88,7 +86,7 @@ class HealType(pos: SrcPos)(using Context) extends TypeMap {
8886
case tp: TermRef =>
8987
ctx.typer.checkStable(tp, pos, "type witness")
9088
if levelOf(tp.symbol) > 0 then tp.select(tpnme.Underlying)
91-
else getQuoteTypeTags.getTagRef(tp)
89+
else getTagRef(tp)
9290
case _: SearchFailureType =>
9391
report.error(
9492
ctx.typer.missingArgMsg(tag, reqType, "")

0 commit comments

Comments
 (0)