Skip to content

Scaladoc Support for Capture & Separation Checking (Staging) #23607

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions scaladoc/src/dotty/tools/scaladoc/api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ enum Modifier(val name: String, val prefix: Boolean):
case Transparent extends Modifier("transparent", true)
case Infix extends Modifier("infix", true)
case AbsOverride extends Modifier("abstract override", true)
case Update extends Modifier("update", true)

case class ExtensionTarget(name: String, typeParams: Seq[TypeParameter], argsLists: Seq[TermParameterList], signature: Signature, dri: DRI, position: Long)
case class ImplicitConversion(from: DRI, to: DRI)
Expand All @@ -69,7 +70,7 @@ enum Kind(val name: String):
case Var extends Kind("var")
case Val extends Kind("val")
case Exported(base: Kind) extends Kind("export")
case Type(concreate: Boolean, opaque: Boolean, typeParams: Seq[TypeParameter])
case Type(concreate: Boolean, opaque: Boolean, typeParams: Seq[TypeParameter], isCaptureVar: Boolean = false)
extends Kind("type") // should we handle opaque as modifier?
case Given(kind: Def | Class | Val.type, as: Option[Signature], conversion: Option[ImplicitConversion])
extends Kind("given") with ImplicitConversionProvider
Expand Down Expand Up @@ -120,7 +121,8 @@ case class TypeParameter(
variance: "" | "+" | "-",
name: String,
dri: DRI,
signature: Signature
signature: Signature,
isCaptureVar: Boolean = false // under capture checking
)

case class Link(name: String, dri: DRI)
Expand Down
191 changes: 191 additions & 0 deletions scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package dotty.tools.scaladoc

package cc

import scala.quoted._

object CaptureDefs:
// these should become part of the reflect API in the distant future
def retains(using qctx: Quotes) =
qctx.reflect.Symbol.requiredClass("scala.annotation.retains")
def retainsCap(using qctx: Quotes) =
qctx.reflect.Symbol.requiredClass("scala.annotation.retainsCap")
def retainsByName(using qctx: Quotes) =
qctx.reflect.Symbol.requiredClass("scala.annotation.retainsByName")
def CapsModule(using qctx: Quotes) =
qctx.reflect.Symbol.requiredPackage("scala.caps")
def captureRoot(using qctx: Quotes) =
qctx.reflect.Symbol.requiredPackage("scala.caps.cap")
def Caps_Capability(using qctx: Quotes) =
qctx.reflect.Symbol.requiredClass("scala.caps.Capability")
def Caps_CapSet(using qctx: Quotes) =
qctx.reflect.Symbol.requiredClass("scala.caps.CapSet")
def Caps_Mutable(using qctx: Quotes) =
qctx.reflect.Symbol.requiredClass("scala.caps.Mutable")
def Caps_SharedCapability(using qctx: Quotes) =
qctx.reflect.Symbol.requiredClass("scala.caps.SharedCapability")
def UseAnnot(using qctx: Quotes) =
qctx.reflect.Symbol.requiredClass("scala.caps.use")
def ConsumeAnnot(using qctx: Quotes) =
qctx.reflect.Symbol.requiredClass("scala.caps.consume")
def ReachCapabilityAnnot(using qctx: Quotes) =
qctx.reflect.Symbol.requiredClass("scala.annotation.internal.reachCapability")
def RootCapabilityAnnot(using qctx: Quotes) =
qctx.reflect.Symbol.requiredClass("scala.caps.internal.rootCapability")
def ReadOnlyCapabilityAnnot(using qctx: Quotes) =
qctx.reflect.Symbol.requiredClass("scala.annotation.internal.readOnlyCapability")
def RequiresCapabilityAnnot(using qctx: Quotes) =
qctx.reflect.Symbol.requiredClass("scala.annotation.internal.requiresCapability")

def LanguageExperimental(using qctx: Quotes) =
qctx.reflect.Symbol.requiredPackage("scala.language.experimental")

def ImpureFunction1(using qctx: Quotes) =
qctx.reflect.Symbol.requiredClass("scala.ImpureFunction1")

def ImpureContextFunction1(using qctx: Quotes) =
qctx.reflect.Symbol.requiredClass("scala.ImpureContextFunction1")

def Function1(using qctx: Quotes) =
qctx.reflect.Symbol.requiredClass("scala.Function1")

def ContextFunction1(using qctx: Quotes) =
qctx.reflect.Symbol.requiredClass("scala.ContextFunction1")

val useAnnotFullName: String = "scala.caps.use.<init>"
val consumeAnnotFullName: String = "scala.caps.consume.<init>"
val ccImportSelector = "captureChecking"
end CaptureDefs

extension (using qctx: Quotes)(ann: qctx.reflect.Symbol)
/** This symbol is one of `retains` or `retainsCap` */
def isRetains: Boolean =
ann == CaptureDefs.retains || ann == CaptureDefs.retainsCap

/** This symbol is one of `retains`, `retainsCap`, or `retainsByName` */
def isRetainsLike: Boolean =
ann.isRetains || ann == CaptureDefs.retainsByName

def isReachCapabilityAnnot: Boolean =
ann == CaptureDefs.ReachCapabilityAnnot

def isReadOnlyCapabilityAnnot: Boolean =
ann == CaptureDefs.ReadOnlyCapabilityAnnot
end extension

extension (using qctx: Quotes)(tpe: qctx.reflect.TypeRepr) // FIXME clean up and have versions on Symbol for those
def isCaptureRoot: Boolean =
import qctx.reflect.*
tpe match
case TermRef(ThisType(TypeRef(NoPrefix(), "caps")), "cap") => true
case TermRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "caps"), "cap") => true
case TermRef(TermRef(TermRef(TermRef(NoPrefix(), "_root_"), "scala"), "caps"), "cap") => true
case _ => false

// NOTE: There's something horribly broken with Symbols, and we can't rely on tests like .isContextFunctionType either,
// so we do these lame string comparisons instead.
def isImpureFunction1: Boolean = tpe.typeSymbol.fullName == "scala.ImpureFunction1"

def isImpureContextFunction1: Boolean = tpe.typeSymbol.fullName == "scala.ImpureContextFunction1"

def isFunction1: Boolean = tpe.typeSymbol.fullName == "scala.Function1"

def isContextFunction1: Boolean = tpe.typeSymbol.fullName == "scala.ContextFunction1"

def isAnyImpureFunction: Boolean = tpe.typeSymbol.fullName.startsWith("scala.ImpureFunction")

def isAnyImpureContextFunction: Boolean = tpe.typeSymbol.fullName.startsWith("scala.ImpureContextFunction")

def isAnyFunction: Boolean = tpe.typeSymbol.fullName.startsWith("scala.Function")

def isAnyContextFunction: Boolean = tpe.typeSymbol.fullName.startsWith("scala.ContextFunction")

def isCapSet: Boolean = tpe.typeSymbol == CaptureDefs.Caps_CapSet

def isCapSetPure: Boolean =
tpe.isCapSet && tpe.match
case CapturingType(_, refs) => refs.isEmpty
case _ => true

def isCapSetCap: Boolean =
tpe.isCapSet && tpe.match
case CapturingType(_, List(ref)) => ref.isCaptureRoot
case _ => false
end extension

extension (using qctx: Quotes)(typedef: qctx.reflect.TypeDef)
def derivesFromCapSet: Boolean =
import qctx.reflect.*
typedef.rhs.match
case t: TypeTree => t.tpe.derivesFrom(CaptureDefs.Caps_CapSet)
case t: TypeBoundsTree => t.tpe.derivesFrom(CaptureDefs.Caps_CapSet)
case _ => false
end extension

/** Matches `import scala.language.experimental.captureChecking` */
object CCImport:
def unapply(using qctx: Quotes)(tree: qctx.reflect.Tree): Boolean =
import qctx.reflect._
tree match
case imprt: Import if imprt.expr.tpe.termSymbol == CaptureDefs.LanguageExperimental =>
imprt.selectors.exists {
case SimpleSelector(s) if s == CaptureDefs.ccImportSelector => true
case _ => false
}
case _ => false
end unapply
end CCImport

object ReachCapability:
def unapply(using qctx: Quotes)(ty: qctx.reflect.TypeRepr): Option[qctx.reflect.TypeRepr] =
import qctx.reflect._
ty match
case AnnotatedType(base, Apply(Select(New(annot), _), Nil)) if annot.symbol.isReachCapabilityAnnot =>
Some(base)
case _ => None
end ReachCapability

object ReadOnlyCapability:
def unapply(using qctx: Quotes)(ty: qctx.reflect.TypeRepr): Option[qctx.reflect.TypeRepr] =
import qctx.reflect._
ty match
case AnnotatedType(base, Apply(Select(New(annot), _), Nil)) if annot.symbol.isReadOnlyCapabilityAnnot =>
Some(base)
case _ => None
end ReadOnlyCapability

/** Decompose capture sets in the union-type-encoding into the sequence of atomic `TypeRepr`s.
* Returns `None` if the type is not a capture set.
*/
def decomposeCaptureRefs(using qctx: Quotes)(typ0: qctx.reflect.TypeRepr): Option[List[qctx.reflect.TypeRepr]] =
import qctx.reflect._
val buffer = collection.mutable.ListBuffer.empty[TypeRepr]
def include(t: TypeRepr): Boolean = { buffer += t; true }
def traverse(typ: TypeRepr): Boolean =
typ match
case t if t.typeSymbol == defn.NothingClass => true
case OrType(t1, t2) => traverse(t1) && traverse(t2)
case t @ ThisType(_) => include(t)
case t @ TermRef(_, _) => include(t)
case t @ ParamRef(_, _) => include(t)
case t @ ReachCapability(_) => include(t)
case t @ ReadOnlyCapability(_) => include(t)
case t : TypeRef => include(t) // FIXME: does this need a more refined check?
case _ => report.warning(s"Unexpected type tree $typ while trying to extract capture references from $typ0"); false // TODO remove warning eventually
if traverse(typ0) then Some(buffer.toList) else None
end decomposeCaptureRefs

object CaptureSetType:
def unapply(using qctx: Quotes)(tt: qctx.reflect.TypeTree): Option[List[qctx.reflect.TypeRepr]] = decomposeCaptureRefs(tt.tpe)
end CaptureSetType

object CapturingType:
def unapply(using qctx: Quotes)(typ: qctx.reflect.TypeRepr): Option[(qctx.reflect.TypeRepr, List[qctx.reflect.TypeRepr])] =
import qctx.reflect._
typ match
case AnnotatedType(base, Apply(TypeApply(Select(New(annot), _), List(CaptureSetType(refs))), Nil)) if annot.symbol.isRetainsLike =>
Some((base, refs))
case AnnotatedType(base, Apply(Select(New(annot), _), Nil)) if annot.symbol == CaptureDefs.retainsCap =>
Some((base, List(CaptureDefs.captureRoot.termRef)))
case _ => None
end CapturingType
5 changes: 4 additions & 1 deletion scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tasty

import scala.jdk.CollectionConverters._
import dotty.tools.scaladoc._
import dotty.tools.scaladoc.cc.CaptureDefs
import scala.quoted._

import SymOps._
Expand Down Expand Up @@ -52,7 +53,9 @@ trait BasicSupport:
"scala.annotation.static",
"scala.annotation.targetName",
"scala.annotation.threadUnsafe",
"scala.annotation.varargs"
"scala.annotation.varargs",
CaptureDefs.useAnnotFullName,
CaptureDefs.consumeAnnotFullName,
)
val documentedSymbol = summon[Quotes].reflect.Symbol.requiredClass("java.lang.annotation.Documented")
val annotations = sym.annotations.filter { a =>
Expand Down
15 changes: 13 additions & 2 deletions scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package dotty.tools.scaladoc.tasty
import dotty.tools.scaladoc._
import dotty.tools.scaladoc.{Signature => DSignature}

import dotty.tools.scaladoc.cc.*

import scala.quoted._

import SymOps._
Expand Down Expand Up @@ -465,6 +467,8 @@ trait ClassLikeSupport:
else ""

val name = symbol.normalizedName
val isCaptureVar = ccEnabled && argument.derivesFromCapSet

val normalizedName = if name.matches("_\\$\\d*") then "_" else name
val boundsSignature = argument.rhs.asSignature(classDef, symbol.owner)
val signature = boundsSignature ++ contextBounds.flatMap(tr =>
Expand All @@ -479,7 +483,8 @@ trait ClassLikeSupport:
variancePrefix,
normalizedName,
symbol.dri,
signature
signature,
isCaptureVar,
)

def parseTypeDef(typeDef: TypeDef, classDef: ClassDef): Member =
Expand All @@ -489,6 +494,9 @@ trait ClassLikeSupport:
case LambdaTypeTree(params, body) => isTreeAbstract(body)
case _ => false
}

val isCaptureVar = ccEnabled && typeDef.derivesFromCapSet

val (generics, tpeTree) = typeDef.rhs match
case LambdaTypeTree(params, body) => (params.map(mkTypeArgument(_, classDef)), body)
case tpe => (Nil, tpe)
Expand Down Expand Up @@ -528,7 +536,10 @@ trait ClassLikeSupport:
case _ => symbol.getExtraModifiers()

mkMember(symbol, kind, sig)(
modifiers = modifiers,
// Due to how capture checking encodes update methods (recycling the mutable flag for methods),
// we need to filter out the update modifier here. Otherwise, mutable fields will
// be documented as having the update modifier, which is not correct.
modifiers = modifiers.filterNot(_ == Modifier.Update),
deprecated = symbol.isDeprecated(),
experimental = symbol.isExperimental()
)
Expand Down
6 changes: 3 additions & 3 deletions scaladoc/src/dotty/tools/scaladoc/tasty/NameNormalizer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ object NameNormalizer {
val escaped = escapedName(constructorNormalizedName)
escaped
}

def ownerNameChain: List[String] = {
import reflect.*
if s.isNoSymbol then List.empty
else if s == defn.EmptyPackageClass then List.empty
else if s == defn.RootPackage then List.empty
else if s == defn.RootClass then List.empty
else s.owner.ownerNameChain :+ s.normalizedName
}
}

def normalizedFullName: String =
s.ownerNameChain.mkString(".")

Expand Down
7 changes: 7 additions & 0 deletions scaladoc/src/dotty/tools/scaladoc/tasty/PackageSupport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import scala.jdk.CollectionConverters._

import SymOps._

import dotty.tools.scaladoc.cc.CCImport

trait PackageSupport:
self: TastyParser =>
import qctx.reflect._
Expand All @@ -13,6 +15,11 @@ trait PackageSupport:

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

def parsePackageObject(pckObj: ClassDef): (String, Member) =
Expand Down
1 change: 1 addition & 0 deletions scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ object SymOps:
Flags.Case -> Modifier.Case,
Flags.Opaque -> Modifier.Opaque,
Flags.AbsOverride -> Modifier.AbsOverride,
Flags.Mutable -> Modifier.Update, // under CC
).collect {
case (flag, mod) if sym.flags.is(flag) => mod
}
Expand Down
3 changes: 3 additions & 0 deletions scaladoc/src/dotty/tools/scaladoc/tasty/TastyParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ case class TastyParser(

private given qctx.type = qctx

protected var ccFlag: Boolean = false
def ccEnabled: Boolean = ccFlag

val intrinsicClassDefs = Set(
defn.AnyClass,
defn.MatchableClass,
Expand Down
Loading
Loading