Skip to content

Commit c3ec337

Browse files
committed
Scaladoc support for capture checking and separation checking
1 parent b13d617 commit c3ec337

File tree

11 files changed

+375
-47
lines changed

11 files changed

+375
-47
lines changed

scaladoc/src/dotty/tools/scaladoc/api.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ enum Modifier(val name: String, val prefix: Boolean):
4444
case Transparent extends Modifier("transparent", true)
4545
case Infix extends Modifier("infix", true)
4646
case AbsOverride extends Modifier("abstract override", true)
47+
case Update extends Modifier("update", true)
4748

4849
case class ExtensionTarget(name: String, typeParams: Seq[TypeParameter], argsLists: Seq[TermParameterList], signature: Signature, dri: DRI, position: Long)
4950
case class ImplicitConversion(from: DRI, to: DRI)
@@ -69,7 +70,7 @@ enum Kind(val name: String):
6970
case Var extends Kind("var")
7071
case Val extends Kind("val")
7172
case Exported(base: Kind) extends Kind("export")
72-
case Type(concreate: Boolean, opaque: Boolean, typeParams: Seq[TypeParameter])
73+
case Type(concreate: Boolean, opaque: Boolean, typeParams: Seq[TypeParameter], isCaptureVar: Boolean = false)
7374
extends Kind("type") // should we handle opaque as modifier?
7475
case Given(kind: Def | Class | Val.type, as: Option[Signature], conversion: Option[ImplicitConversion])
7576
extends Kind("given") with ImplicitConversionProvider
@@ -120,7 +121,8 @@ case class TypeParameter(
120121
variance: "" | "+" | "-",
121122
name: String,
122123
dri: DRI,
123-
signature: Signature
124+
signature: Signature,
125+
isCaptureVar: Boolean = false // under capture checking
124126
)
125127

126128
case class Link(name: String, dri: DRI)
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package dotty.tools.scaladoc
2+
3+
package cc
4+
5+
import scala.quoted._
6+
7+
object CaptureDefs:
8+
// these should become part of the reflect API in the distant future
9+
def retains(using qctx: Quotes) =
10+
qctx.reflect.Symbol.requiredClass("scala.annotation.retains")
11+
def retainsCap(using qctx: Quotes) =
12+
qctx.reflect.Symbol.requiredClass("scala.annotation.retainsCap")
13+
def retainsByName(using qctx: Quotes) =
14+
qctx.reflect.Symbol.requiredClass("scala.annotation.retainsByName")
15+
def CapsModule(using qctx: Quotes) =
16+
qctx.reflect.Symbol.requiredPackage("scala.caps")
17+
def captureRoot(using qctx: Quotes) =
18+
qctx.reflect.Symbol.requiredPackage("scala.caps.cap")
19+
def Caps_Capability(using qctx: Quotes) =
20+
qctx.reflect.Symbol.requiredClass("scala.caps.Capability")
21+
def Caps_CapSet(using qctx: Quotes) =
22+
qctx.reflect.Symbol.requiredClass("scala.caps.CapSet")
23+
def Caps_Mutable(using qctx: Quotes) =
24+
qctx.reflect.Symbol.requiredClass("scala.caps.Mutable")
25+
def Caps_SharedCapability(using qctx: Quotes) =
26+
qctx.reflect.Symbol.requiredClass("scala.caps.SharedCapability")
27+
def UseAnnot(using qctx: Quotes) =
28+
qctx.reflect.Symbol.requiredClass("scala.caps.use")
29+
def ConsumeAnnot(using qctx: Quotes) =
30+
qctx.reflect.Symbol.requiredClass("scala.caps.consume")
31+
def ReachCapabilityAnnot(using qctx: Quotes) =
32+
qctx.reflect.Symbol.requiredClass("scala.annotation.internal.reachCapability")
33+
def RootCapabilityAnnot(using qctx: Quotes) =
34+
qctx.reflect.Symbol.requiredClass("scala.caps.internal.rootCapability")
35+
def ReadOnlyCapabilityAnnot(using qctx: Quotes) =
36+
qctx.reflect.Symbol.requiredClass("scala.annotation.internal.readOnlyCapability")
37+
def RequiresCapabilityAnnot(using qctx: Quotes) =
38+
qctx.reflect.Symbol.requiredClass("scala.annotation.internal.requiresCapability")
39+
40+
def LanguageExperimental(using qctx: Quotes) =
41+
qctx.reflect.Symbol.requiredPackage("scala.language.experimental")
42+
43+
def ImpureFunction1(using qctx: Quotes) =
44+
qctx.reflect.Symbol.requiredClass("scala.ImpureFunction1")
45+
46+
def ImpureContextFunction1(using qctx: Quotes) =
47+
qctx.reflect.Symbol.requiredClass("scala.ImpureContextFunction1")
48+
49+
def Function1(using qctx: Quotes) =
50+
qctx.reflect.Symbol.requiredClass("scala.Function1")
51+
52+
def ContextFunction1(using qctx: Quotes) =
53+
qctx.reflect.Symbol.requiredClass("scala.ContextFunction1")
54+
55+
val useAnnotFullName: String = "scala.caps.use.<init>"
56+
val consumeAnnotFullName: String = "scala.caps.consume.<init>"
57+
val ccImportSelector = "captureChecking"
58+
end CaptureDefs
59+
60+
extension (using qctx: Quotes)(ann: qctx.reflect.Symbol)
61+
/** This symbol is one of `retains` or `retainsCap` */
62+
def isRetains: Boolean =
63+
ann == CaptureDefs.retains || ann == CaptureDefs.retainsCap
64+
65+
/** This symbol is one of `retains`, `retainsCap`, or `retainsByName` */
66+
def isRetainsLike: Boolean =
67+
ann.isRetains || ann == CaptureDefs.retainsByName
68+
69+
def isReachCapabilityAnnot: Boolean =
70+
ann == CaptureDefs.ReachCapabilityAnnot
71+
72+
def isReadOnlyCapabilityAnnot: Boolean =
73+
ann == CaptureDefs.ReadOnlyCapabilityAnnot
74+
end extension
75+
76+
extension (using qctx: Quotes)(tpe: qctx.reflect.TypeRepr) // FIXME clean up and have versions on Symbol for those
77+
def isCaptureRoot: Boolean =
78+
import qctx.reflect.*
79+
tpe match
80+
case TermRef(ThisType(TypeRef(NoPrefix(), "caps")), "cap") => true
81+
case TermRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "caps"), "cap") => true
82+
case TermRef(TermRef(TermRef(TermRef(NoPrefix(), "_root_"), "scala"), "caps"), "cap") => true
83+
case _ => false
84+
85+
// NOTE: There's something horribly broken with Symbols, and we can't rely on tests like .isContextFunctionType either,
86+
// so we do these lame string comparisons instead.
87+
def isImpureFunction1: Boolean = tpe.typeSymbol.fullName == "scala.ImpureFunction1"
88+
89+
def isImpureContextFunction1: Boolean = tpe.typeSymbol.fullName == "scala.ImpureContextFunction1"
90+
91+
def isFunction1: Boolean = tpe.typeSymbol.fullName == "scala.Function1"
92+
93+
def isContextFunction1: Boolean = tpe.typeSymbol.fullName == "scala.ContextFunction1"
94+
95+
def isAnyImpureFunction: Boolean = tpe.typeSymbol.fullName.startsWith("scala.ImpureFunction")
96+
97+
def isAnyImpureContextFunction: Boolean = tpe.typeSymbol.fullName.startsWith("scala.ImpureContextFunction")
98+
99+
def isAnyFunction: Boolean = tpe.typeSymbol.fullName.startsWith("scala.Function")
100+
101+
def isAnyContextFunction: Boolean = tpe.typeSymbol.fullName.startsWith("scala.ContextFunction")
102+
103+
def isCapSet: Boolean = tpe.typeSymbol == CaptureDefs.Caps_CapSet
104+
105+
def isCapSetPure: Boolean =
106+
tpe.isCapSet && tpe.match
107+
case CapturingType(_, refs) => refs.isEmpty
108+
case _ => true
109+
110+
def isCapSetCap: Boolean =
111+
tpe.isCapSet && tpe.match
112+
case CapturingType(_, List(ref)) => ref.isCaptureRoot
113+
case _ => false
114+
end extension
115+
116+
extension (using qctx: Quotes)(typedef: qctx.reflect.TypeDef)
117+
def derivesFromCapSet: Boolean =
118+
import qctx.reflect.*
119+
typedef.rhs.match
120+
case t: TypeTree => t.tpe.derivesFrom(CaptureDefs.Caps_CapSet)
121+
case t: TypeBoundsTree => t.tpe.derivesFrom(CaptureDefs.Caps_CapSet)
122+
case _ => false
123+
end extension
124+
125+
/** Matches `import scala.language.experimental.captureChecking` */
126+
object CCImport:
127+
def unapply(using qctx: Quotes)(tree: qctx.reflect.Tree): Boolean =
128+
import qctx.reflect._
129+
tree match
130+
case imprt: Import if imprt.expr.tpe.termSymbol == CaptureDefs.LanguageExperimental =>
131+
imprt.selectors.exists {
132+
case SimpleSelector(s) if s == CaptureDefs.ccImportSelector => true
133+
case _ => false
134+
}
135+
case _ => false
136+
end unapply
137+
end CCImport
138+
139+
object ReachCapability:
140+
def unapply(using qctx: Quotes)(ty: qctx.reflect.TypeRepr): Option[qctx.reflect.TypeRepr] =
141+
import qctx.reflect._
142+
ty match
143+
case AnnotatedType(base, Apply(Select(New(annot), _), Nil)) if annot.symbol.isReachCapabilityAnnot =>
144+
Some(base)
145+
case _ => None
146+
end ReachCapability
147+
148+
object ReadOnlyCapability:
149+
def unapply(using qctx: Quotes)(ty: qctx.reflect.TypeRepr): Option[qctx.reflect.TypeRepr] =
150+
import qctx.reflect._
151+
ty match
152+
case AnnotatedType(base, Apply(Select(New(annot), _), Nil)) if annot.symbol.isReadOnlyCapabilityAnnot =>
153+
Some(base)
154+
case _ => None
155+
end ReadOnlyCapability
156+
157+
/** Decompose capture sets in the union-type-encoding into the sequence of atomic `TypeRepr`s.
158+
* Returns `None` if the type is not a capture set.
159+
*/
160+
def decomposeCaptureRefs(using qctx: Quotes)(typ0: qctx.reflect.TypeRepr): Option[List[qctx.reflect.TypeRepr]] =
161+
import qctx.reflect._
162+
val buffer = collection.mutable.ListBuffer.empty[TypeRepr]
163+
def include(t: TypeRepr): Boolean = { buffer += t; true }
164+
def traverse(typ: TypeRepr): Boolean =
165+
typ match
166+
case t if t.typeSymbol == defn.NothingClass => true
167+
case OrType(t1, t2) => traverse(t1) && traverse(t2)
168+
case t @ ThisType(_) => include(t)
169+
case t @ TermRef(_, _) => include(t)
170+
case t @ ParamRef(_, _) => include(t)
171+
case t @ ReachCapability(_) => include(t)
172+
case t @ ReadOnlyCapability(_) => include(t)
173+
case t : TypeRef => include(t) // FIXME: does this need a more refined check?
174+
case _ => report.warning(s"Unexpected type tree $typ while trying to extract capture references from $typ0"); false // TODO remove warning eventually
175+
if traverse(typ0) then Some(buffer.toList) else None
176+
end decomposeCaptureRefs
177+
178+
object CaptureSetType:
179+
def unapply(using qctx: Quotes)(tt: qctx.reflect.TypeTree): Option[List[qctx.reflect.TypeRepr]] = decomposeCaptureRefs(tt.tpe)
180+
end CaptureSetType
181+
182+
object CapturingType:
183+
def unapply(using qctx: Quotes)(typ: qctx.reflect.TypeRepr): Option[(qctx.reflect.TypeRepr, List[qctx.reflect.TypeRepr])] =
184+
import qctx.reflect._
185+
typ match
186+
case AnnotatedType(base, Apply(TypeApply(Select(New(annot), _), List(CaptureSetType(refs))), Nil)) if annot.symbol.isRetainsLike =>
187+
Some((base, refs))
188+
case AnnotatedType(base, Apply(Select(New(annot), _), Nil)) if annot.symbol == CaptureDefs.retainsCap =>
189+
Some((base, List(CaptureDefs.captureRoot.termRef)))
190+
case _ => None
191+
end CapturingType

scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package tasty
33

44
import scala.jdk.CollectionConverters._
55
import dotty.tools.scaladoc._
6+
import dotty.tools.scaladoc.cc.CaptureDefs
67
import scala.quoted._
78

89
import SymOps._
@@ -52,7 +53,9 @@ trait BasicSupport:
5253
"scala.annotation.static",
5354
"scala.annotation.targetName",
5455
"scala.annotation.threadUnsafe",
55-
"scala.annotation.varargs"
56+
"scala.annotation.varargs",
57+
CaptureDefs.useAnnotFullName,
58+
CaptureDefs.consumeAnnotFullName,
5659
)
5760
val documentedSymbol = summon[Quotes].reflect.Symbol.requiredClass("java.lang.annotation.Documented")
5861
val annotations = sym.annotations.filter { a =>

scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package dotty.tools.scaladoc.tasty
33
import dotty.tools.scaladoc._
44
import dotty.tools.scaladoc.{Signature => DSignature}
55

6+
import dotty.tools.scaladoc.cc.*
7+
68
import scala.quoted._
79

810
import SymOps._
@@ -465,6 +467,8 @@ trait ClassLikeSupport:
465467
else ""
466468

467469
val name = symbol.normalizedName
470+
val isCaptureVar = ccEnabled && argument.derivesFromCapSet
471+
468472
val normalizedName = if name.matches("_\\$\\d*") then "_" else name
469473
val boundsSignature = argument.rhs.asSignature(classDef, symbol.owner)
470474
val signature = boundsSignature ++ contextBounds.flatMap(tr =>
@@ -479,7 +483,8 @@ trait ClassLikeSupport:
479483
variancePrefix,
480484
normalizedName,
481485
symbol.dri,
482-
signature
486+
signature,
487+
isCaptureVar,
483488
)
484489

485490
def parseTypeDef(typeDef: TypeDef, classDef: ClassDef): Member =
@@ -489,6 +494,9 @@ trait ClassLikeSupport:
489494
case LambdaTypeTree(params, body) => isTreeAbstract(body)
490495
case _ => false
491496
}
497+
498+
val isCaptureVar = ccEnabled && typeDef.derivesFromCapSet
499+
492500
val (generics, tpeTree) = typeDef.rhs match
493501
case LambdaTypeTree(params, body) => (params.map(mkTypeArgument(_, classDef)), body)
494502
case tpe => (Nil, tpe)
@@ -528,7 +536,10 @@ trait ClassLikeSupport:
528536
case _ => symbol.getExtraModifiers()
529537

530538
mkMember(symbol, kind, sig)(
531-
modifiers = modifiers,
539+
// Due to how capture checking encodes update methods (recycling the mutable flag for methods),
540+
// we need to filter out the update modifier here. Otherwise, mutable fields will
541+
// be documented as having the update modifier, which is not correct.
542+
modifiers = modifiers.filterNot(_ == Modifier.Update),
532543
deprecated = symbol.isDeprecated(),
533544
experimental = symbol.isExperimental()
534545
)

scaladoc/src/dotty/tools/scaladoc/tasty/NameNormalizer.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,16 @@ object NameNormalizer {
1717
val escaped = escapedName(constructorNormalizedName)
1818
escaped
1919
}
20-
20+
2121
def ownerNameChain: List[String] = {
2222
import reflect.*
2323
if s.isNoSymbol then List.empty
2424
else if s == defn.EmptyPackageClass then List.empty
2525
else if s == defn.RootPackage then List.empty
2626
else if s == defn.RootClass then List.empty
2727
else s.owner.ownerNameChain :+ s.normalizedName
28-
}
29-
28+
}
29+
3030
def normalizedFullName: String =
3131
s.ownerNameChain.mkString(".")
3232

scaladoc/src/dotty/tools/scaladoc/tasty/PackageSupport.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import scala.jdk.CollectionConverters._
55

66
import SymOps._
77

8+
import dotty.tools.scaladoc.cc.CCImport
9+
810
trait PackageSupport:
911
self: TastyParser =>
1012
import qctx.reflect._
@@ -13,6 +15,11 @@ trait PackageSupport:
1315

1416
def parsePackage(pck: PackageClause): (String, Member) =
1517
val name = pck.symbol.fullName
18+
ccFlag = false // FIXME: would be better if we had access to the tasty attribute
19+
pck.stats.foreach {
20+
case CCImport() => ccFlag = true
21+
case _ =>
22+
}
1623
(name, Member(name, "", pck.symbol.dri, Kind.Package))
1724

1825
def parsePackageObject(pckObj: ClassDef): (String, Member) =

scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ object SymOps:
100100
Flags.Case -> Modifier.Case,
101101
Flags.Opaque -> Modifier.Opaque,
102102
Flags.AbsOverride -> Modifier.AbsOverride,
103+
Flags.Mutable -> Modifier.Update, // under CC
103104
).collect {
104105
case (flag, mod) if sym.flags.is(flag) => mod
105106
}

scaladoc/src/dotty/tools/scaladoc/tasty/TastyParser.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ case class TastyParser(
187187

188188
private given qctx.type = qctx
189189

190+
protected var ccFlag: Boolean = false
191+
def ccEnabled: Boolean = ccFlag
192+
190193
val intrinsicClassDefs = Set(
191194
defn.AnyClass,
192195
defn.MatchableClass,

0 commit comments

Comments
 (0)