Skip to content

Commit 88961de

Browse files
authored
Scaladoc Support for Capture & Separation Checking (Staging) (#23607)
This is the staging version of #23471 to be merged into main. Fixes #23437
2 parents 2b004ee + 4ba1571 commit 88961de

15 files changed

+449
-55
lines changed

project/ScaladocGeneration.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ object ScaladocGeneration {
141141
def key: String = "-dynamic-side-menu"
142142
}
143143

144+
case class SuppressCC(value: Boolean) extends Arg[Boolean] {
145+
def key: String = "-suppressCC"
146+
}
147+
144148
import _root_.scala.reflect._
145149

146150
trait GenerationConfig {

scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ object Scaladoc:
4747
defaultTemplate: Option[String] = None,
4848
quickLinks: List[QuickLink] = List.empty,
4949
dynamicSideMenu: Boolean = false,
50+
suppressCC: Boolean = false, // suppress rendering anything related to experimental capture checking
5051
)
5152

5253
def run(args: Array[String], rootContext: CompilerContext): Reporter =
@@ -231,6 +232,7 @@ object Scaladoc:
231232
defaultTemplate.nonDefault,
232233
quickLinksParsed,
233234
dynamicSideMenu.get,
235+
suppressCC.get,
234236
)
235237
(Some(docArgs), newContext)
236238
}

scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,5 +144,8 @@ class ScaladocSettings extends SettingGroup with AllScalaSettings:
144144
val dynamicSideMenu: Setting[Boolean] =
145145
BooleanSetting(RootSetting, "dynamic-side-menu", "Generate side menu via JS instead of embedding it in every html file", false)
146146

147+
val suppressCC: Setting[Boolean] =
148+
BooleanSetting(RootSetting, "suppressCC", "Suppress rendering anything related to experimental capture checking", false)
149+
147150
def scaladocSpecificSettings: Set[Setting[?]] =
148-
Set(sourceLinks, legacySourceLink, syntax, revision, externalDocumentationMappings, socialLinks, skipById, skipByRegex, deprecatedSkipPackages, docRootContent, snippetCompiler, generateInkuire, defaultTemplate, scastieConfiguration, quickLinks, dynamicSideMenu)
151+
Set(sourceLinks, legacySourceLink, syntax, revision, externalDocumentationMappings, socialLinks, skipById, skipByRegex, deprecatedSkipPackages, docRootContent, snippetCompiler, generateInkuire, defaultTemplate, scastieConfiguration, quickLinks, dynamicSideMenu, suppressCC)

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: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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+
def OnlyCapabilityAnnot(using qctx: Quotes) =
40+
qctx.reflect.Symbol.requiredClass("scala.annotation.internal.onlyCapability")
41+
42+
def LanguageExperimental(using qctx: Quotes) =
43+
qctx.reflect.Symbol.requiredPackage("scala.language.experimental")
44+
45+
def ImpureFunction1(using qctx: Quotes) =
46+
qctx.reflect.Symbol.requiredClass("scala.ImpureFunction1")
47+
48+
def ImpureContextFunction1(using qctx: Quotes) =
49+
qctx.reflect.Symbol.requiredClass("scala.ImpureContextFunction1")
50+
51+
def Function1(using qctx: Quotes) =
52+
qctx.reflect.Symbol.requiredClass("scala.Function1")
53+
54+
def ContextFunction1(using qctx: Quotes) =
55+
qctx.reflect.Symbol.requiredClass("scala.ContextFunction1")
56+
57+
val useAnnotFullName: String = "scala.caps.use.<init>"
58+
val consumeAnnotFullName: String = "scala.caps.consume.<init>"
59+
val ccImportSelector = "captureChecking"
60+
end CaptureDefs
61+
62+
extension (using qctx: Quotes)(ann: qctx.reflect.Symbol)
63+
/** This symbol is one of `retains` or `retainsCap` */
64+
def isRetains: Boolean =
65+
ann == CaptureDefs.retains || ann == CaptureDefs.retainsCap
66+
67+
/** This symbol is one of `retains`, `retainsCap`, or `retainsByName` */
68+
def isRetainsLike: Boolean =
69+
ann.isRetains || ann == CaptureDefs.retainsByName
70+
71+
def isReachCapabilityAnnot: Boolean =
72+
ann == CaptureDefs.ReachCapabilityAnnot
73+
74+
def isReadOnlyCapabilityAnnot: Boolean =
75+
ann == CaptureDefs.ReadOnlyCapabilityAnnot
76+
77+
def isOnlyCapabilityAnnot: Boolean =
78+
ann == CaptureDefs.OnlyCapabilityAnnot
79+
end extension
80+
81+
extension (using qctx: Quotes)(tpe: qctx.reflect.TypeRepr) // FIXME clean up and have versions on Symbol for those
82+
def isCaptureRoot: Boolean =
83+
import qctx.reflect.*
84+
tpe match
85+
case TermRef(ThisType(TypeRef(NoPrefix(), "caps")), "cap") => true
86+
case TermRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "caps"), "cap") => true
87+
case TermRef(TermRef(TermRef(TermRef(NoPrefix(), "_root_"), "scala"), "caps"), "cap") => true
88+
case _ => false
89+
90+
// NOTE: There's something horribly broken with Symbols, and we can't rely on tests like .isContextFunctionType either,
91+
// so we do these lame string comparisons instead.
92+
def isImpureFunction1: Boolean = tpe.typeSymbol.fullName == "scala.ImpureFunction1"
93+
94+
def isImpureContextFunction1: Boolean = tpe.typeSymbol.fullName == "scala.ImpureContextFunction1"
95+
96+
def isFunction1: Boolean = tpe.typeSymbol.fullName == "scala.Function1"
97+
98+
def isContextFunction1: Boolean = tpe.typeSymbol.fullName == "scala.ContextFunction1"
99+
100+
def isAnyImpureFunction: Boolean = tpe.typeSymbol.fullName.startsWith("scala.ImpureFunction")
101+
102+
def isAnyImpureContextFunction: Boolean = tpe.typeSymbol.fullName.startsWith("scala.ImpureContextFunction")
103+
104+
def isAnyFunction: Boolean = tpe.typeSymbol.fullName.startsWith("scala.Function")
105+
106+
def isAnyContextFunction: Boolean = tpe.typeSymbol.fullName.startsWith("scala.ContextFunction")
107+
108+
def isCapSet: Boolean = tpe.typeSymbol == CaptureDefs.Caps_CapSet
109+
110+
def isCapSetPure: Boolean =
111+
tpe.isCapSet && tpe.match
112+
case CapturingType(_, refs) => refs.isEmpty
113+
case _ => true
114+
115+
def isCapSetCap: Boolean =
116+
tpe.isCapSet && tpe.match
117+
case CapturingType(_, List(ref)) => ref.isCaptureRoot
118+
case _ => false
119+
120+
def isPureClass(from: qctx.reflect.ClassDef): Boolean =
121+
import qctx.reflect._
122+
def check(sym: Tree): Boolean = sym match
123+
case ClassDef(name, _, _, Some(ValDef(_, tt, _)), _) => tt.tpe match
124+
case CapturingType(_, refs) => refs.isEmpty
125+
case _ => true
126+
case _ => false
127+
128+
// Horrible hack to basically grab tpe1.asSeenFrom(from)
129+
val tpe1 = from.symbol.typeRef.select(tpe.typeSymbol).simplified
130+
val tpe2 = tpe1.classSymbol.map(_.typeRef).getOrElse(tpe1)
131+
132+
// println(s"${tpe.show} -> (${tpe.typeSymbol} from ${from.symbol}) ${tpe1.show} -> ${tpe2} -> ${tpe2.baseClasses.filter(_.isClassDef)}")
133+
val res = tpe2.baseClasses.exists(c => c.isClassDef && check(c.tree))
134+
// println(s"${tpe.show} is pure class = $res")
135+
res
136+
end extension
137+
138+
extension (using qctx: Quotes)(typedef: qctx.reflect.TypeDef)
139+
def derivesFromCapSet: Boolean =
140+
import qctx.reflect.*
141+
typedef.rhs.match
142+
case t: TypeTree => t.tpe.derivesFrom(CaptureDefs.Caps_CapSet)
143+
case t: TypeBoundsTree => t.tpe.derivesFrom(CaptureDefs.Caps_CapSet)
144+
case _ => false
145+
end extension
146+
147+
/** Matches `import scala.language.experimental.captureChecking` */
148+
object CCImport:
149+
def unapply(using qctx: Quotes)(tree: qctx.reflect.Tree): Boolean =
150+
import qctx.reflect._
151+
tree match
152+
case imprt: Import if imprt.expr.tpe.termSymbol == CaptureDefs.LanguageExperimental =>
153+
imprt.selectors.exists {
154+
case SimpleSelector(s) if s == CaptureDefs.ccImportSelector => true
155+
case _ => false
156+
}
157+
case _ => false
158+
end unapply
159+
end CCImport
160+
161+
object ReachCapability:
162+
def unapply(using qctx: Quotes)(ty: qctx.reflect.TypeRepr): Option[qctx.reflect.TypeRepr] =
163+
import qctx.reflect._
164+
ty match
165+
case AnnotatedType(base, Apply(Select(New(annot), _), Nil)) if annot.symbol.isReachCapabilityAnnot =>
166+
Some(base)
167+
case _ => None
168+
end ReachCapability
169+
170+
object ReadOnlyCapability:
171+
def unapply(using qctx: Quotes)(ty: qctx.reflect.TypeRepr): Option[qctx.reflect.TypeRepr] =
172+
import qctx.reflect._
173+
ty match
174+
case AnnotatedType(base, Apply(Select(New(annot), _), Nil)) if annot.symbol.isReadOnlyCapabilityAnnot =>
175+
Some(base)
176+
case _ => None
177+
end ReadOnlyCapability
178+
179+
object OnlyCapability:
180+
def unapply(using qctx: Quotes)(ty: qctx.reflect.TypeRepr): Option[(qctx.reflect.TypeRepr, qctx.reflect.Symbol)] =
181+
import qctx.reflect._
182+
ty match
183+
case AnnotatedType(base, app @ Apply(TypeApply(Select(New(annot), _), _), Nil)) if annot.tpe.typeSymbol.isOnlyCapabilityAnnot =>
184+
app.tpe.typeArgs.head.classSymbol.match
185+
case Some(clazzsym) => Some((base, clazzsym))
186+
case None => None
187+
case _ => None
188+
end OnlyCapability
189+
190+
/** Decompose capture sets in the union-type-encoding into the sequence of atomic `TypeRepr`s.
191+
* Returns `None` if the type is not a capture set.
192+
*/
193+
def decomposeCaptureRefs(using qctx: Quotes)(typ0: qctx.reflect.TypeRepr): Option[List[qctx.reflect.TypeRepr]] =
194+
import qctx.reflect._
195+
val buffer = collection.mutable.ListBuffer.empty[TypeRepr]
196+
def include(t: TypeRepr): Boolean = { buffer += t; true }
197+
def traverse(typ: TypeRepr): Boolean =
198+
typ match
199+
case t if t.typeSymbol == defn.NothingClass => true
200+
case OrType(t1, t2) => traverse(t1) && traverse(t2)
201+
case t @ ThisType(_) => include(t)
202+
case t @ TermRef(_, _) => include(t)
203+
case t @ ParamRef(_, _) => include(t)
204+
case t @ ReachCapability(_) => include(t)
205+
case t @ ReadOnlyCapability(_) => include(t)
206+
case t @ OnlyCapability(_, _) => include(t)
207+
case t : TypeRef => include(t)
208+
case _ => report.warning(s"Unexpected type tree $typ while trying to extract capture references from $typ0"); false
209+
if traverse(typ0) then Some(buffer.toList) else None
210+
end decomposeCaptureRefs
211+
212+
object CaptureSetType:
213+
def unapply(using qctx: Quotes)(tt: qctx.reflect.TypeTree): Option[List[qctx.reflect.TypeRepr]] = decomposeCaptureRefs(tt.tpe)
214+
end CaptureSetType
215+
216+
object CapturingType:
217+
def unapply(using qctx: Quotes)(typ: qctx.reflect.TypeRepr): Option[(qctx.reflect.TypeRepr, List[qctx.reflect.TypeRepr])] =
218+
import qctx.reflect._
219+
typ match
220+
case AnnotatedType(base, Apply(TypeApply(Select(New(annot), _), List(CaptureSetType(refs))), Nil)) if annot.symbol.isRetainsLike =>
221+
Some((base, refs))
222+
case AnnotatedType(base, Apply(Select(New(annot), _), Nil)) if annot.symbol == CaptureDefs.retainsCap =>
223+
Some((base, List(CaptureDefs.captureRoot.termRef)))
224+
case _ => None
225+
end CapturingType

scaladoc/src/dotty/tools/scaladoc/renderers/MemberRenderer.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ class MemberRenderer(signatureRenderer: SignatureRenderer)(using DocContext) ext
177177
cls := s"documentableName $depStyle",
178178
)
179179

180-
val signature: MemberSignature = signatureProvider.rawSignature(member)()
180+
val ctx = summon[DocContext]
181+
val signature: MemberSignature = signatureProvider.rawSignature(member)(!ctx.args.suppressCC)()
181182
val isSubtype = signature.suffix.exists {
182183
case Keyword(keyword) => keyword.contains("extends")
183184
case _ => false

scaladoc/src/dotty/tools/scaladoc/renderers/Resources.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ trait Resources(using ctx: DocContext) extends Locations, Writer:
213213
val (res, pageName) = page.content match
214214
case m: Member if m.kind != Kind.RootPackage =>
215215
def processMember(member: Member, fqName: List[String]): Seq[(JSON, Seq[String])] =
216-
val signature: MemberSignature = signatureProvider.rawSignature(member)()
216+
val signature: MemberSignature = signatureProvider.rawSignature(member)(!ctx.args.suppressCC)()
217217
val sig = Signature(Plain(member.name)) ++ signature.suffix
218218
val descr = if member.kind == Kind.Package then "" else fqName.mkString(".")
219219
val extraDescr = member.docs.map(d => docPartRenderPlain(d.body)).getOrElse("")

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

Lines changed: 7 additions & 2 deletions
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._
@@ -42,7 +43,7 @@ trait BasicSupport:
4243
def getAnnotations(): List[Annotation] =
4344
// Custom annotations should be documented only if annotated by @java.lang.annotation.Documented
4445
// We allow also some special cases
45-
val fqNameAllowlist = Set(
46+
val fqNameAllowlist0 = Set(
4647
"scala.specialized",
4748
"scala.throws",
4849
"scala.transient",
@@ -52,8 +53,12 @@ trait BasicSupport:
5253
"scala.annotation.static",
5354
"scala.annotation.targetName",
5455
"scala.annotation.threadUnsafe",
55-
"scala.annotation.varargs"
56+
"scala.annotation.varargs",
5657
)
58+
val fqNameAllowlist =
59+
if ccEnabled then
60+
fqNameAllowlist0 + CaptureDefs.useAnnotFullName + CaptureDefs.consumeAnnotFullName
61+
else fqNameAllowlist0
5762
val documentedSymbol = summon[Quotes].reflect.Symbol.requiredClass("java.lang.annotation.Documented")
5863
val annotations = sym.annotations.filter { a =>
5964
a.tpe.typeSymbol.hasAnnotation(documentedSymbol) || fqNameAllowlist.contains(a.symbol.fullName)

0 commit comments

Comments
 (0)