Skip to content

Commit

Permalink
Fix quotes with references to path dependent types
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolasstucki committed Mar 23, 2023
1 parent ed57e32 commit 31d94d7
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 20 deletions.
43 changes: 28 additions & 15 deletions compiler/src/dotty/tools/dotc/staging/HealType.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,39 +31,52 @@ class HealType(pos: SrcPos)(using Context) extends TypeMap {
*/
def apply(tp: Type): Type =
tp match
case tp: TypeRef =>
tp.underlying match
case TypeAlias(alias)
if !tp.symbol.isTypeSplice && !tp.typeSymbol.hasAnnotation(defn.QuotedRuntime_SplicedTypeAnnot) =>
this.apply(alias)
case _ =>
healTypeRef(tp)
case tp @ TermRef(NoPrefix, _) if !tp.symbol.isStatic && level > levelOf(tp.symbol) =>
levelError(tp.symbol, tp, pos)
case NonSpliceAlias(aliased) => this.apply(aliased)
case tp: TypeRef => healTypeRef(tp)
case tp: TermRef =>
val inconsistentRoot = levelInconsistentRootOfPath(tp)
if inconsistentRoot.exists then levelError(inconsistentRoot, tp, pos)
else tp
case tp: AnnotatedType =>
derivedAnnotatedType(tp, apply(tp.parent), tp.annot)
case _ =>
mapOver(tp)

private def healTypeRef(tp: TypeRef): Type =
tp.prefix match
case NoPrefix if tp.typeSymbol.hasAnnotation(defn.QuotedRuntime_SplicedTypeAnnot) =>
tp
case prefix: TermRef if tp.symbol.isTypeSplice =>
checkNotWildcardSplice(tp)
if level == 0 then tp else getQuoteTypeTags.getTagRef(prefix)
case prefix: TermRef if !prefix.symbol.isStatic && level > levelOf(prefix.symbol) =>
tryHeal(prefix.symbol, tp, pos)
case NoPrefix if level > levelOf(tp.symbol) && !tp.typeSymbol.hasAnnotation(defn.QuotedRuntime_SplicedTypeAnnot) =>
tryHeal(tp.symbol, tp, pos)
case prefix: ThisType if level > levelOf(prefix.cls) && !tp.symbol.isStatic =>
tryHeal(tp.symbol, tp, pos)
case _: NamedType | _: ThisType | NoPrefix =>
if levelInconsistentRootOfPath(tp).exists then
tryHeal(tp.symbol, tp, pos)
else
tp
case _ =>
mapOver(tp)

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

private def checkNotWildcardSplice(splice: TypeRef): Unit =
splice.prefix.termSymbol.info.argInfos match
case (tb: TypeBounds) :: _ => report.error(em"Cannot splice $splice because it is a wildcard type", pos)
case _ =>

/** Return the root of this path if it is a variable defined in a previous level.
* If the path is consistent, return NoSymbol.
*/
private def levelInconsistentRootOfPath(tp: Type)(using Context): Symbol =
tp match
case tp @ NamedType(NoPrefix, _) if level > levelOf(tp.symbol) => tp.symbol
case tp: NamedType if !tp.symbol.isStatic => levelInconsistentRootOfPath(tp.prefix)
case tp: ThisType if level > levelOf(tp.cls) => tp.cls
case _ => NoSymbol

/** Try to heal reference to type `T` used in a higher level than its definition.
* Returns a reference to a type tag generated by `QuoteTypeTags` that contains a
* reference to a type alias containing the equivalent of `${summon[quoted.Type[T]]}`.
Expand Down
7 changes: 6 additions & 1 deletion compiler/src/dotty/tools/dotc/transform/PickleQuotes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,18 @@ class PickleQuotes extends MacroTransform {
override def apply(tp: Type): Type = tp match
case tp: TypeRef if tp.typeSymbol.isTypeSplice =>
apply(tp.dealias)
case tp @ TypeRef(pre, _) if pre == NoPrefix || pre.termSymbol.isLocal =>
case tp @ TypeRef(pre, _) if isLocalPath(pre) =>
val hiBound = tp.typeSymbol.info match
case info: ClassInfo => info.parents.reduce(_ & _)
case info => info.hiBound
apply(hiBound)
case tp =>
mapOver(tp)

private def isLocalPath(tp: Type): Boolean = tp match
case NoPrefix => true
case tp: TermRef if !tp.symbol.is(Package) => isLocalPath(tp.prefix)
case tp => false
}

/** Remove references to local types that will not be defined in this quote */
Expand Down
8 changes: 4 additions & 4 deletions compiler/src/dotty/tools/dotc/transform/TreeChecker.scala
Original file line number Diff line number Diff line change
Expand Up @@ -657,8 +657,8 @@ object TreeChecker {

// Check that we only add the captured type `T` instead of a more complex type like `List[T]`.
// If we have `F[T]` with captured `F` and `T`, we should list `F` and `T` separately in the args.
for arg <- args do
assert(arg.isTerm || arg.tpe.isInstanceOf[TypeRef], "Expected TypeRef in Hole type args but got: " + arg.tpe)
for arg <- args if arg.isType do
assert(arg.tpe.isInstanceOf[TypeRef] || arg.tpe.isInstanceOf[TermRef], "Expected TypeRef or TermRef in Hole type args but got: " + arg.tpe)

// Check result type of the hole
if isTermHole then assert(tpt.typeOpt <:< pt)
Expand All @@ -674,7 +674,7 @@ object TreeChecker {
defn.AnyType
case tpe => tpe
defn.QuotedExprClass.typeRef.appliedTo(tpe)
else defn.QuotedTypeClass.typeRef.appliedTo(arg.typeOpt)
else defn.QuotedTypeClass.typeRef.appliedTo(arg.typeOpt.widenTermRefExpr)
}
val expectedResultType =
if isTermHole then defn.QuotedExprClass.typeRef.appliedTo(tpt.typeOpt)
Expand All @@ -683,7 +683,7 @@ object TreeChecker {
defn.FunctionOf(List(defn.QuotesClass.typeRef), expectedResultType, isContextual = true)
val expectedContentType =
defn.FunctionOf(argQuotedTypes, contextualResult)
assert(content.typeOpt =:= expectedContentType, i"expected content of the hole to be ${expectedContentType} but got ${content.typeOpt}")
assert(content.typeOpt =:= expectedContentType, i"unexpected content of hole\nexpected: ${expectedContentType}\nwas: ${content.typeOpt}")

tree1
}
Expand Down
70 changes: 70 additions & 0 deletions tests/pos-macros/path-dependent-type-capture/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import scala.quoted.*

trait A:
type T
val b: B

trait B:
type T
def f: Unit

trait C0:
type U
val d: D0
trait D0:
type U
def h: Unit
object Macro:
inline def generateCode: Unit = ${ generateCodeExpr }

def generateCodeExpr(using Quotes): Expr[Unit] =
'{
$testLocalPathsGlobalClasses
$testLocalPathsLocalClasses
}

def testLocalPathsGlobalClasses(using Quotes): Expr[Unit] =
'{
type T
val a: A = ???
${
val expr = '{
val t: T = ???
val aT: a.T = ???
val abT: a.b.T = ???
val aRef: a.type = ???
aRef.b
aRef.b.f
val abRef: a.b.type = ???
abRef.f
()
}
expr
}
}

def testLocalPathsLocalClasses(using Quotes): Expr[Unit] =
'{
type U
trait C extends C0:
type U
val d: D
trait D extends D0:
type U
def h: Unit
val c: C = ???
${
val expr = '{
val u: U = ???
val cU: c.U = ???
val cdU: c.d.U = ???
val cRef: c.type = ???
cRef.d
cRef.d.h
val cdRef: c.d.type = ???
cdRef.h
()
}
expr
}
}
1 change: 1 addition & 0 deletions tests/pos-macros/path-dependent-type-capture/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@main def test = Macro.generateCode

0 comments on commit 31d94d7

Please sign in to comment.