From c3ec337cff35db0166d37595fe3841e0b84f75b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Sat, 26 Jul 2025 16:42:27 +0200 Subject: [PATCH 1/6] Scaladoc support for capture checking and separation checking --- scaladoc/src/dotty/tools/scaladoc/api.scala | 6 +- .../dotty/tools/scaladoc/cc/CaptureOps.scala | 191 ++++++++++++++++++ .../tools/scaladoc/tasty/BasicSupport.scala | 5 +- .../scaladoc/tasty/ClassLikeSupport.scala | 15 +- .../tools/scaladoc/tasty/NameNormalizer.scala | 6 +- .../tools/scaladoc/tasty/PackageSupport.scala | 7 + .../dotty/tools/scaladoc/tasty/SymOps.scala | 1 + .../tools/scaladoc/tasty/TastyParser.scala | 3 + .../tools/scaladoc/tasty/TypesSupport.scala | 176 ++++++++++++---- .../translators/ScalaSignatureProvider.scala | 2 +- .../translators/ScalaSignatureUtils.scala | 10 +- 11 files changed, 375 insertions(+), 47 deletions(-) create mode 100644 scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala diff --git a/scaladoc/src/dotty/tools/scaladoc/api.scala b/scaladoc/src/dotty/tools/scaladoc/api.scala index b39fdf157347..41ccd8fb2280 100644 --- a/scaladoc/src/dotty/tools/scaladoc/api.scala +++ b/scaladoc/src/dotty/tools/scaladoc/api.scala @@ -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) @@ -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 @@ -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) diff --git a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala new file mode 100644 index 000000000000..bd55798d000c --- /dev/null +++ b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala @@ -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." + val consumeAnnotFullName: String = "scala.caps.consume." + 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 diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala index 81415377beeb..81309018718c 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala @@ -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._ @@ -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 => diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala index 99aac7010d8b..00635951fb69 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala @@ -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._ @@ -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 => @@ -479,7 +483,8 @@ trait ClassLikeSupport: variancePrefix, normalizedName, symbol.dri, - signature + signature, + isCaptureVar, ) def parseTypeDef(typeDef: TypeDef, classDef: ClassDef): Member = @@ -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) @@ -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() ) diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/NameNormalizer.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/NameNormalizer.scala index 196c3e056b36..8f42a28c2c35 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/NameNormalizer.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/NameNormalizer.scala @@ -17,7 +17,7 @@ object NameNormalizer { val escaped = escapedName(constructorNormalizedName) escaped } - + def ownerNameChain: List[String] = { import reflect.* if s.isNoSymbol then List.empty @@ -25,8 +25,8 @@ object NameNormalizer { 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(".") diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/PackageSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/PackageSupport.scala index c0308336a2bf..8de2ab6b8539 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/PackageSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/PackageSupport.scala @@ -5,6 +5,8 @@ import scala.jdk.CollectionConverters._ import SymOps._ +import dotty.tools.scaladoc.cc.CCImport + trait PackageSupport: self: TastyParser => import qctx.reflect._ @@ -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) = diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala index 969b1d6462c2..0464da450f05 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala @@ -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 } diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TastyParser.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TastyParser.scala index 1a8337e0c6b7..741147ebfe2e 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TastyParser.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TastyParser.scala @@ -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, diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index 06a3c29e6805..f91b57945bd4 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -6,8 +6,10 @@ import scala.jdk.CollectionConverters.* import scala.quoted.* import scala.util.control.NonFatal -import NameNormalizer.* -import SyntheticsSupport.* +import dotty.tools.scaladoc.cc.* + +import NameNormalizer._ +import SyntheticsSupport._ trait TypesSupport: self: TastyParser => @@ -19,7 +21,7 @@ trait TypesSupport: def asSignature(elideThis: reflect.ClassDef, originalOwner: reflect.Symbol, skipThisTypePrefix: Boolean): SSignature = import reflect._ tpeTree match - case TypeBoundsTree(low, high) => typeBoundsTreeOfHigherKindedType(low.tpe, high.tpe, skipThisTypePrefix)(using elideThis, originalOwner) + case TypeBoundsTree(low, high) => typeBoundsTreeOfHigherKindedType(low.tpe, high.tpe, skipThisTypePrefix)(using elideThis, originalOwner, inCC = None) case tpeTree: TypeTree => topLevelProcess(tpeTree.tpe, skipThisTypePrefix)(using elideThis, originalOwner) case term: Term => topLevelProcess(term.tpe, skipThisTypePrefix)(using elideThis, originalOwner) def asSignature(elideThis: reflect.ClassDef, originalOwner: reflect.Symbol): SSignature = @@ -37,19 +39,30 @@ trait TypesSupport: private def keyword(str: String): SignaturePart = Keyword(str) - private def tpe(str: String, dri: DRI): SignaturePart = dotty.tools.scaladoc.Type(str, Some(dri)) + private def tpe(str: String, dri: DRI)(using inCC: Option[Any]): SignaturePart = + if inCC.isDefined then + dotty.tools.scaladoc.Plain(str) + else + dotty.tools.scaladoc.Type(str, Some(dri)) - private def tpe(str: String): SignaturePart = dotty.tools.scaladoc.Type(str, None) + private def tpe(str: String)(using inCC: Option[Any]): SignaturePart = + if inCC.isDefined then + dotty.tools.scaladoc.Plain(str) + else + dotty.tools.scaladoc.Type(str, None) protected def inParens(s: SSignature, wrap: Boolean = true) = if wrap then plain("(").l ++ s ++ plain(")").l else s extension (on: SignaturePart) def l: List[SignaturePart] = List(on) - private def tpe(using Quotes)(symbol: reflect.Symbol): SSignature = + private def tpe(using Quotes)(symbol: reflect.Symbol)(using inCC: Option[Any]): SSignature = import SymOps._ val dri: Option[DRI] = Option(symbol).filterNot(_.isHiddenByVisibility).map(_.dri) - dotty.tools.scaladoc.Type(symbol.normalizedName, dri).l + if inCC.isDefined then // we are in the context of a capture set and want paths to be rendered plainly + dotty.tools.scaladoc.Plain(symbol.normalizedName).l + else + dotty.tools.scaladoc.Type(symbol.normalizedName, dri).l private def commas(lists: List[SSignature]) = lists match case List(single) => single @@ -82,7 +95,7 @@ trait TypesSupport: // TODO #23 add support for all types signatures that make sense private def inner( - using Quotes, + using qctx: Quotes, )( tp: reflect.TypeRepr, skipThisTypePrefix: Boolean @@ -91,6 +104,8 @@ trait TypesSupport: originalOwner: reflect.Symbol, indent: Int = 0, skipTypeSuffix: Boolean = false, + // inCC means in capture-checking context. If defined, it carries the current capture-set contents. + inCC: Option[List[reflect.TypeRepr]] = None, ): SSignature = import reflect._ def noSupported(name: String): SSignature = @@ -105,7 +120,10 @@ trait TypesSupport: inParens(inner(left, skipThisTypePrefix), shouldWrapInParens(left, tp, true)) ++ keyword(" & ").l ++ inParens(inner(right, skipThisTypePrefix), shouldWrapInParens(right, tp, false)) - case ByNameType(tpe) => keyword("=> ") :: inner(tpe, skipThisTypePrefix) + case ByNameType(CapturingType(tpe, refs)) => + emitByNameArrow(using qctx)(Some(refs), skipThisTypePrefix) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix)) + case ByNameType(tpe) => + emitByNameArrow(using qctx)(None, skipThisTypePrefix) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix)) case ConstantType(constant) => plain(constant.show).l case ThisType(tpe) => @@ -116,6 +134,14 @@ trait TypesSupport: inner(tpe, skipThisTypePrefix) :+ plain("*") case AppliedType(repeatedClass, Seq(tpe)) if isRepeated(repeatedClass) => inner(tpe, skipThisTypePrefix) :+ plain("*") + case CapturingType(base, refs) => + base match + case t @ AppliedType(base, args) if t.isFunctionType => + functionType(base, args, skipThisTypePrefix)(using inCC = Some(refs)) + case t : Refinement if t.isFunctionType => + inner(base, skipThisTypePrefix)(using indent = indent, skipTypeSuffix = skipTypeSuffix, inCC = Some(refs)) + case t if t.isCapSet => emitCaptureSet(refs, skipThisTypePrefix, omitCap = false) + case t => inner(base, skipThisTypePrefix) ++ emitCapturing(refs, skipThisTypePrefix) case AnnotatedType(tpe, _) => inner(tpe, skipThisTypePrefix) case FlexibleType(tpe) => @@ -129,7 +155,8 @@ trait TypesSupport: case tl @ TypeLambda(params, paramBounds, resType) => plain("[").l ++ commas(params.zip(paramBounds).map { (name, typ) => val normalizedName = if name.matches("_\\$\\d*") then "_" else name - tpe(normalizedName).l ++ inner(typ, skipThisTypePrefix) + val suffix = if ccEnabled && typ.derivesFrom(CaptureDefs.Caps_CapSet) then List(Keyword("^")) else Nil + tpe(normalizedName).l ++ suffix ++ inner(typ, skipThisTypePrefix) }) ++ plain("]").l ++ keyword(" =>> ").l ++ inner(resType, skipThisTypePrefix) @@ -141,14 +168,19 @@ trait TypesSupport: inner(Refinement(at, "apply", mt), skipThisTypePrefix) case r: Refinement => { //(parent, name, info) + val inCC0 = inCC + given Option[List[TypeRepr]] = None // do not propagate capture set beyond this point def getRefinementInformation(t: TypeRepr): List[TypeRepr] = t match { case r: Refinement => getRefinementInformation(r.parent) :+ r case t => List(t) } def getParamBounds(t: PolyType): SSignature = commas( - t.paramNames.zip(t.paramBounds.map(inner(_, skipThisTypePrefix))) - .map(b => tpe(b(0)).l ++ b(1)) + t.paramNames.zip(t.paramBounds.map(inner(_, skipThisTypePrefix))).zipWithIndex + .map { case ((name, bound), idx) => + val suffix = if ccEnabled && t.param(idx).derivesFrom(CaptureDefs.Caps_CapSet) then List(Keyword("^")) else Nil + tpe(name).l ++ suffix ++ bound + } ) def getParamList(m: MethodType): SSignature = @@ -191,9 +223,16 @@ trait TypesSupport: val isCtx = isContextualMethod(m) if isDependentMethod(m) then val paramList = getParamList(m) - val arrow = keyword(if isCtx then " ?=> " else " => ").l + val arrPrefix = if isCtx then "?" else "" + val arrow = + if ccEnabled then + inCC0 match + case None | Some(Nil) => keyword(arrPrefix + "->").l + case Some(List(c)) if c.isCaptureRoot => keyword(arrPrefix + "=>").l + case Some(refs) => keyword(arrPrefix + "->") :: emitCaptureSet(refs, skipThisTypePrefix) + else keyword(arrPrefix + "=>").l val resType = inner(m.resType, skipThisTypePrefix) - paramList ++ arrow ++ resType + paramList ++ (plain(" ") :: arrow) ++ (plain(" ") :: resType) else val sym = defn.FunctionClass(m.paramTypes.length, isCtx) inner(sym.typeRef.appliedTo(m.paramTypes :+ m.resType), skipThisTypePrefix) @@ -236,18 +275,7 @@ trait TypesSupport: ++ inParens(inner(rhs, skipThisTypePrefix), shouldWrapInParens(rhs, t, false)) case t @ AppliedType(tpe, args) if t.isFunctionType => - val arrow = if t.isContextFunctionType then " ?=> " else " => " - args match - case Nil => Nil - case List(rtpe) => plain("()").l ++ keyword(arrow).l ++ inner(rtpe, skipThisTypePrefix) - case List(arg, rtpe) => - val wrapInParens = stripAnnotated(arg) match - case _: TermRef | _: TypeRef | _: ConstantType | _: ParamRef => false - case at: AppliedType if !isInfix(at) && !at.isFunctionType && !at.isTupleN => false - case _ => true - inParens(inner(arg, skipThisTypePrefix), wrapInParens) ++ keyword(arrow).l ++ inner(rtpe, skipThisTypePrefix) - case _ => - plain("(").l ++ commas(args.init.map(inner(_, skipThisTypePrefix))) ++ plain(")").l ++ keyword(arrow).l ++ inner(args.last, skipThisTypePrefix) + functionType(tpe, args, skipThisTypePrefix) case t @ AppliedType(tpe, typeList) => inner(tpe, skipThisTypePrefix) ++ plain("[").l ++ commas(typeList.map { t => t match @@ -255,6 +283,8 @@ trait TypesSupport: case _ => topLevelProcess(t, skipThisTypePrefix) }) ++ plain("]").l + case t : TypeRef if t.isCapSet => emitCaptureSet(Nil, skipThisTypePrefix) + case tp @ TypeRef(qual, typeName) => inline def wrapping = shouldWrapInParens(inner = qual, outer = tp, isLeft = true) qual match { @@ -274,7 +304,7 @@ trait TypesSupport: tpe(tp.typeSymbol) else val sig = inParens( - inner(qual, skipThisTypePrefix)(using indent = indent, skipTypeSuffix = true), wrapping) + inner(qual, skipThisTypePrefix)(using indent = indent, skipTypeSuffix = true, inCC = inCC), wrapping) sig ++ plain(".").l ++ tpe(tp.typeSymbol) @@ -283,7 +313,7 @@ trait TypesSupport: tpe(tp.typeSymbol) case _: TermRef | _: ParamRef => val suffix = if tp.typeSymbol == Symbol.noSymbol then tpe(typeName).l else tpe(tp.typeSymbol) - inner(qual, skipThisTypePrefix)(using indent = indent, skipTypeSuffix = true) + inner(qual, skipThisTypePrefix)(using indent = indent, skipTypeSuffix = true, inCC = inCC) ++ plain(".").l ++ suffix case _ => @@ -294,7 +324,7 @@ trait TypesSupport: case tr @ TermRef(qual, typeName) => val prefix = qual match case t if skipPrefix(t, elideThis, originalOwner, skipThisTypePrefix) => Nil - case tp => inner(tp, skipThisTypePrefix)(using indent = indent, skipTypeSuffix = true) ++ plain(".").l + case tp => inner(tp, skipThisTypePrefix)(using indent = indent, skipTypeSuffix = true, inCC = inCC) ++ plain(".").l val suffix = if skipTypeSuffix then Nil else List(plain("."), keyword("type")) val typeSig = tr.termSymbol.tree match case vd: ValDef if tr.termSymbol.flags.is(Flags.Module) => @@ -316,13 +346,13 @@ trait TypesSupport: keyword(caseSpaces + "case ").l ++ inner(from, skipThisTypePrefix) ++ keyword(" => ").l - ++ inner(to, skipThisTypePrefix)(using indent = indent + 2, skipTypeSuffix = skipTypeSuffix) + ++ inner(to, skipThisTypePrefix)(using indent = indent + 2, skipTypeSuffix = skipTypeSuffix, inCC = inCC) ++ plain("\n").l case TypeLambda(_, _, MatchCase(from, to)) => keyword(caseSpaces + "case ").l ++ inner(from, skipThisTypePrefix) ++ keyword(" => ").l - ++ inner(to, skipThisTypePrefix)(using indent = indent + 2, skipTypeSuffix = skipTypeSuffix) + ++ inner(to, skipThisTypePrefix)(using indent = indent + 2, skipTypeSuffix = skipTypeSuffix, inCC = inCC) ++ plain("\n").l } inner(sc, skipThisTypePrefix) ++ keyword(" match ").l ++ plain("{\n").l ++ casesTexts ++ plain(spaces + "}").l @@ -349,9 +379,34 @@ trait TypesSupport: s"${tpe.show(using Printer.TypeReprStructure)}" throw MatchError(msg) + private def functionType(using qctx: Quotes)(funTy: reflect.TypeRepr, args: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)(using + elideThis: reflect.ClassDef, + originalOwner: reflect.Symbol, + indent: Int, + skipTypeSuffix: Boolean, + inCC: Option[List[reflect.TypeRepr]], + ): SSignature = + import reflect._ + val arrow = plain(" ") :: (emitFunctionArrow(using qctx)(funTy, inCC, skipThisTypePrefix) ++ plain(" ").l) + given Option[List[TypeRepr]] = None // do not propagate capture set beyond this point + args match + case Nil => Nil + case List(rtpe) => plain("()").l ++ arrow ++ inner(rtpe, skipThisTypePrefix) + case List(arg, rtpe) => + val wrapInParens = stripAnnotated(arg) match + case _: TermRef | _: TypeRef | _: ConstantType | _: ParamRef => false + case at: AppliedType if !isInfix(at) && !at.isFunctionType && !at.isTupleN => false + case _ => true + inParens(inner(arg, skipThisTypePrefix), wrapInParens) ++ arrow ++ inner(rtpe, skipThisTypePrefix) + case _ => + plain("(").l ++ commas(args.init.map(inner(_, skipThisTypePrefix))) ++ plain(")").l ++ arrow ++ inner(args.last, skipThisTypePrefix) + private def typeBound(using Quotes)(t: reflect.TypeRepr, low: Boolean, skipThisTypePrefix: Boolean)(using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol) = import reflect._ - val ignore = if (low) t.typeSymbol == defn.NothingClass else t.typeSymbol == defn.AnyClass + val ignore = low && (ccEnabled && t.isCapSetPure + || t.typeSymbol == defn.NothingClass) + || !low && (ccEnabled && t.isCapSetCap + || t.typeSymbol == defn.AnyClass) val prefix = keyword(if low then " >: " else " <: ") t match { case l: TypeLambda => prefix :: inParens(inner(l, skipThisTypePrefix)(using elideThis, originalOwner)) @@ -361,18 +416,18 @@ trait TypesSupport: } private def typeBoundsTreeOfHigherKindedType(using Quotes)(low: reflect.TypeRepr, high: reflect.TypeRepr, skipThisTypePrefix: Boolean)( - using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol + using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol, inCC: Option[List[reflect.TypeRepr]] ) = import reflect._ def regularTypeBounds(low: TypeRepr, high: TypeRepr) = - if low == high then keyword(" = ").l ++ inner(low, skipThisTypePrefix)(using elideThis, originalOwner) + if low == high then keyword(" = ").l ++ inner(low, skipThisTypePrefix)(using elideThis, originalOwner, inCC = inCC) else typeBound(low, low = true, skipThisTypePrefix)(using elideThis, originalOwner) ++ typeBound(high, low = false, skipThisTypePrefix)(using elideThis, originalOwner) high.match case TypeLambda(params, paramBounds, resType) => if resType.typeSymbol == defn.AnyClass then plain("[").l ++ commas(params.zip(paramBounds).map { (name, typ) => val normalizedName = if name.matches("_\\$\\d*") then "_" else name - tpe(normalizedName).l ++ inner(typ, skipThisTypePrefix)(using elideThis, originalOwner) + tpe(normalizedName)(using inCC).l ++ inner(typ, skipThisTypePrefix)(using elideThis, originalOwner, inCC = inCC) }) ++ plain("]").l else regularTypeBounds(low, high) @@ -454,3 +509,54 @@ trait TypesSupport: tr match case AnnotatedType(tr, _) => stripAnnotated(tr) case other => other + + private def emitCapability(using Quotes)(ref: reflect.TypeRepr, skipThisTypePrefix: Boolean)(using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol): SSignature = + import reflect._ + ref match + case ReachCapability(c) => emitCapability(c, skipThisTypePrefix) :+ Keyword("*") + case ReadOnlyCapability(c) => emitCapability(c, skipThisTypePrefix) :+ Keyword(".rd") + case ThisType(_) => List(Keyword("this")) + case t => inner(t, skipThisTypePrefix)(using skipTypeSuffix = true, inCC = Some(Nil)) + + private def emitCaptureSet(using Quotes)(refs: List[reflect.TypeRepr], skipThisTypePrefix: Boolean, omitCap: Boolean = true)(using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol): SSignature = + import reflect._ + refs match + case List(ref) if omitCap && ref.isCaptureRoot => Nil + case refs => + val res0 = refs.map(x => emitCapability(x, skipThisTypePrefix)) + val res1 = res0 match + case Nil => Nil + case other => other.reduce((r, e) => r ++ (List(Plain(", ")) ++ e)) + Plain("{") :: (res1 ++ List(Plain("}"))) + + private def emitCapturing(using Quotes)(refs: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)(using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol): SSignature = + import reflect._ + Keyword("^") :: emitCaptureSet(refs, skipThisTypePrefix) + + private def emitFunctionArrow(using Quotes)(funTy: reflect.TypeRepr, captures: Option[List[reflect.TypeRepr]], skipThisTypePrefix: Boolean)(using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol): SSignature = + import reflect._ + val isContextFun = funTy.isAnyContextFunction || funTy.isAnyImpureContextFunction + val prefix = if isContextFun then "?" else "" + if !ccEnabled then + List(Keyword(prefix + "=>")) + else + val isPureFun = funTy.isAnyFunction || funTy.isAnyContextFunction + val isImpureFun = funTy.isAnyImpureFunction || funTy.isAnyImpureContextFunction + captures match + case None => // means an explicit retains* annotation is missing + if isPureFun then + List(Keyword(prefix + "->")) + else if isImpureFun then + List(Keyword(prefix + "=>")) + else + report.error(s"Cannot emit function arrow: expected a (Context)Function* or Impure(Context)Function*, but got: ${funTy.show}") + Nil + case Some(refs) => + // there is some capture set + refs match + case Nil => List(Keyword(prefix + "->")) + case List(ref) if ref.isCaptureRoot => List(Keyword(prefix + "=>")) + case refs => Keyword(prefix + "->") :: emitCaptureSet(refs, skipThisTypePrefix) + + private def emitByNameArrow(using Quotes)(captures: Option[List[reflect.TypeRepr]], skipThisTypePrefix: Boolean)(using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol): SSignature = + emitFunctionArrow(CaptureDefs.Function1.typeRef, captures, skipThisTypePrefix) \ No newline at end of file diff --git a/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureProvider.scala b/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureProvider.scala index a3ce15d70c64..d62ce4693575 100644 --- a/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureProvider.scala +++ b/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureProvider.scala @@ -149,7 +149,7 @@ class ScalaSignatureProvider: MemberSignature( builder.modifiersAndVisibility(typeDef), builder.kind(tpe), - builder.name(typeDef.name, typeDef.dri), + builder.name(typeDef.name, typeDef.dri, isCaptureVar = tpe.isCaptureVar), builder.typeParamList(tpe.typeParams).pipe { bdr => if (!tpe.opaque) { (if tpe.concreate then bdr.plain(" = ") else bdr) diff --git a/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureUtils.scala b/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureUtils.scala index d28dd6ca18fe..7b3f2fa44acf 100644 --- a/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureUtils.scala +++ b/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureUtils.scala @@ -3,8 +3,12 @@ package translators case class SignatureBuilder(content: Signature = Nil) extends ScalaSignatureUtils: def plain(str: String): SignatureBuilder = copy(content = content :+ Plain(str)) - def name(str: String, dri: DRI): SignatureBuilder = copy(content = content :+ Name(str, dri)) - def tpe(text: String, dri: Option[DRI]): SignatureBuilder = copy(content = content :+ Type(text, dri)) + def name(str: String, dri: DRI, isCaptureVar: Boolean = false/*under CC*/): SignatureBuilder = + val suffix = if isCaptureVar then List(Keyword("^")) else Nil + copy(content = content ++ (Name(str, dri) :: suffix)) + def tpe(text: String, dri: Option[DRI], isCaptureVar: Boolean = false/*under CC*/): SignatureBuilder = + val suffix = if isCaptureVar then List(Keyword("^")) else Nil + copy(content = content ++ (Type(text, dri) :: suffix)) def keyword(str: String): SignatureBuilder = copy(content = content :+ Keyword(str)) def tpe(text: String, dri: DRI): SignatureBuilder = copy(content = content :+ Type(text, Some(dri))) def signature(s: Signature): SignatureBuilder = copy(content = content ++ s) @@ -90,7 +94,7 @@ case class SignatureBuilder(content: Signature = Nil) extends ScalaSignatureUtil } def typeParamList(on: TypeParameterList) = list(on.toList, List(Plain("[")), List(Plain("]"))){ (bdr, e) => - bdr.annotationsInline(e).keyword(e.variance).tpe(e.name, Some(e.dri)).signature(e.signature) + bdr.annotationsInline(e).keyword(e.variance).tpe(e.name, Some(e.dri), e.isCaptureVar).signature(e.signature) } def functionTermParameters(paramss: Seq[TermParameterList]) = From 922af79fe13cb787b8656169b87f3ad35ee59a64 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Mon, 4 Aug 2025 18:12:50 +0200 Subject: [PATCH 2/6] isPureClass for checking whether a TypeRepr is pure from a given context --- .../dotty/tools/scaladoc/cc/CaptureOps.scala | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala index bd55798d000c..7e8f9e8bf6a2 100644 --- a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala +++ b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala @@ -111,6 +111,23 @@ extension (using qctx: Quotes)(tpe: qctx.reflect.TypeRepr) // FIXME clean up and tpe.isCapSet && tpe.match case CapturingType(_, List(ref)) => ref.isCaptureRoot case _ => false + + def isPureClass(from: qctx.reflect.ClassDef): Boolean = + import qctx.reflect._ + def check(sym: Tree): Boolean = sym match + case ClassDef(name, _, _, Some(ValDef(_, tt, _)), _) => tt.tpe match + case CapturingType(_, refs) => refs.isEmpty + case _ => true + case _ => false + + // Horrible hack to basically grab tpe1.asSeenFrom(from) + val tpe1 = from.symbol.typeRef.select(tpe.typeSymbol).simplified + val tpe2 = tpe1.classSymbol.map(_.typeRef).getOrElse(tpe1) + + // println(s"${tpe.show} -> (${tpe.typeSymbol} from ${from.symbol}) ${tpe1.show} -> ${tpe2} -> ${tpe2.baseClasses.filter(_.isClassDef)}") + val res = tpe2.baseClasses.exists(c => c.isClassDef && check(c.tree)) + // println(s"${tpe.show} is pure class = $res") + res end extension extension (using qctx: Quotes)(typedef: qctx.reflect.TypeDef) From d145f9e760f7e97bb1e55f04ac791c8ff34e88f1 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Mon, 4 Aug 2025 18:13:22 +0200 Subject: [PATCH 3/6] Drop captures from the capture set if the ref is pure, or the shape type is pure --- .../dotty/tools/scaladoc/tasty/TypesSupport.scala | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index f91b57945bd4..ad14009c5829 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -141,6 +141,7 @@ trait TypesSupport: case t : Refinement if t.isFunctionType => inner(base, skipThisTypePrefix)(using indent = indent, skipTypeSuffix = skipTypeSuffix, inCC = Some(refs)) case t if t.isCapSet => emitCaptureSet(refs, skipThisTypePrefix, omitCap = false) + case t if t.isPureClass(elideThis) => inner(base, skipThisTypePrefix) case t => inner(base, skipThisTypePrefix) ++ emitCapturing(refs, skipThisTypePrefix) case AnnotatedType(tpe, _) => inner(tpe, skipThisTypePrefix) @@ -529,9 +530,19 @@ trait TypesSupport: case other => other.reduce((r, e) => r ++ (List(Plain(", ")) ++ e)) Plain("{") :: (res1 ++ List(Plain("}"))) + // Within the context of `elideThis`, some capabilities can actually be pure. + private def isCapturedInContext(using Quotes)(ref: reflect.TypeRepr)(using elideThis: reflect.ClassDef): Boolean = + import reflect._ + ref match + case ReachCapability(c) => isCapturedInContext(c) + case ReadOnlyCapability(c) => isCapturedInContext(c) + case ThisType(tr) => !elideThis.symbol.typeRef.isPureClass(elideThis) /* is the current class pure? */ + case t => !t.isPureClass(elideThis) + private def emitCapturing(using Quotes)(refs: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)(using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol): SSignature = import reflect._ - Keyword("^") :: emitCaptureSet(refs, skipThisTypePrefix) + val refs0 = refs.filter(isCapturedInContext) + if refs0.isEmpty then Nil else Keyword("^") :: emitCaptureSet(refs0, skipThisTypePrefix) private def emitFunctionArrow(using Quotes)(funTy: reflect.TypeRepr, captures: Option[List[reflect.TypeRepr]], skipThisTypePrefix: Boolean)(using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol): SSignature = import reflect._ @@ -559,4 +570,4 @@ trait TypesSupport: case refs => Keyword(prefix + "->") :: emitCaptureSet(refs, skipThisTypePrefix) private def emitByNameArrow(using Quotes)(captures: Option[List[reflect.TypeRepr]], skipThisTypePrefix: Boolean)(using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol): SSignature = - emitFunctionArrow(CaptureDefs.Function1.typeRef, captures, skipThisTypePrefix) \ No newline at end of file + emitFunctionArrow(CaptureDefs.Function1.typeRef, captures, skipThisTypePrefix) From cd9f0fb79c4b88d7c16d24bd92c539764d1ad1dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Mon, 11 Aug 2025 13:02:59 +0200 Subject: [PATCH 4/6] Render classifier capabilities in scaladoc --- local/project/dummy/arrows.scala | 85 +++++++++++++++++++ local/project/dummy/capturevars.scala | 30 +++++++ local/project/dummy/colltest.scala | 46 ++++++++++ local/project/dummy/nocc.scala | 7 ++ local/project/dummy/sep-pairs.scala | 27 ++++++ mystuff.sbt | 43 ++++++++++ .../dotty/tools/scaladoc/cc/CaptureOps.scala | 21 ++++- .../tools/scaladoc/tasty/TypesSupport.scala | 10 ++- 8 files changed, 263 insertions(+), 6 deletions(-) create mode 100644 local/project/dummy/arrows.scala create mode 100644 local/project/dummy/capturevars.scala create mode 100644 local/project/dummy/colltest.scala create mode 100644 local/project/dummy/nocc.scala create mode 100644 local/project/dummy/sep-pairs.scala create mode 100644 mystuff.sbt diff --git a/local/project/dummy/arrows.scala b/local/project/dummy/arrows.scala new file mode 100644 index 000000000000..85818c84f541 --- /dev/null +++ b/local/project/dummy/arrows.scala @@ -0,0 +1,85 @@ +package dummy + +import language.experimental.captureChecking +import caps.* + +trait Nested: + val c: AnyRef^ + val next: Nested + +trait Arrows: + val a: AnyRef^ + val b: AnyRef^ + val c: AnyRef^ + + val purev: Int -> Int + val purev2: Int ->{} Int + val impurev: Int => Int + val impurev2: Int ->{a,b,c} Int + val impurev3: Int ->{a,b,c} Int => Int + val impureCap: Int ->{cap} Int + val impureCap2: Int ->{cap, a, b, c} Int + val contextPureV: Int ?-> Int + val contextPureV2: Int ?->{} Int + val contextImpureV: Int ?=> Int + val contextImpureV2: Int ?->{a,b,c} Int + val contextImpureV3: Int ?->{a,b,c} Int ?=> Int + val contextImpureCap: Int ?->{cap} Int + val contextImpureCap2: Int ?->{cap, a, b, c} Int + + def pure(f: Int -> Int): Int + def pure2(f: Int ->{} Int): Int + def impure(f: Int => Int): Int + def impure2(f: Int ->{a,b,c} Int): Int + def impure3(f: Int ->{a,b,c} Int => Int): Int + + def consumes(@consume a: AnyRef^): Any + def consumes2(@consume x: AnyRef^{a}, @consume y: AnyRef^{b}): Any + + def reachThis: AnyRef^{this*} + + def byNamePure(f: -> Int): Int + def byNameImpure(f: ->{a,b,c} Int): Int + def byNameImpure2(f: => Int): Int + + def pathDependent(n: Nested^)(g: AnyRef^{n.c} => Any): Any + def pathDependent2(n: Nested^)(g: AnyRef^{n.next.c} => Any): Any + def pathDependent3(n: Nested^)(g: AnyRef^{n.c} => AnyRef^{n.next.c} ->{n.c} Any): Any + def pathDependent4(n: Nested^)(g: AnyRef^{n.c} => AnyRef^{n.next.c} ->{n.c} Any): AnyRef^{n.next.next.c} + def pathDependent5(n: Nested^)(g: AnyRef^{n.c} => AnyRef^{n.next.c} ->{n.c} Any): AnyRef^{n.next.next.c*, n.c, cap} + + def contextPure(f: AnyRef^{a} ?-> Int): Int + def contextImpure(f: AnyRef^{a} ?=> Int): Int + def contextImpure2(f: AnyRef^{a} ?->{b,c} Int): Int + def contextImpure3(f: AnyRef^{a} ?->{b,c} Int => AnyRef^{a} ?=> Int): Int + + val noParams: () -> () -> Int + val noParams2: () ->{} () ->{} Int + val noParamsImpure: () => () => Int => Unit + + val uncurried: (x: AnyRef^, y: AnyRef^) -> AnyRef^{x,y} => Int ->{x,y} Int + val uncurried2: (x: AnyRef^, y: AnyRef^) -> AnyRef => Int ->{x,y} Int + val uncurried3: (x: AnyRef^, y: AnyRef^) => AnyRef + val uncurried4: (x: AnyRef^, y: AnyRef^) ->{a,b} AnyRef^ => Int ->{x,y} Int + + val contextUncurried: (x: AnyRef^{a}, y: AnyRef^{b}) ?-> AnyRef^{x,y} ?-> Int ?->{x,y} Int + val contextUncurried2: (x: AnyRef^{a}, y: AnyRef^{b}) ?-> AnyRef ?-> Int ?->{x,y} Int + val contextUncurried3: (x: AnyRef^{a}, y: AnyRef^{b}) ?=> AnyRef + val contextUncurried4: (x: AnyRef^{a}, y: AnyRef^{b}) ?->{a,b} AnyRef^ ?=> Int ?->{x,y} Int + + def polyPure[A](f: A -> Int): Int + def polyPure2[A](f: A ->{} Int): Int + def polyImpure[A](f: A => Int): Int + def polyImpure2[A](f: A ->{a,b,c} Int): Int + def polyImpure3[A](f: A ->{a,b,c} Int => Int): Int + + def polyContextPure[A](f: A ?-> Int): Int + def polyContextPure2[A](f: A ?->{} Int): Int + def polyContextImpure[A](f: A ?=> Int): Int + def polyContextImpure2[A](f: A ?->{a,b,c} Int): Int + def polyContextImpure3[A](f: A ?->{a,b,c} Int => Int): Int + + val polyPureV: [A] => A -> Int + val polyPureV2: [A] => Int => A ->{a,b,c} Int + val polyImpureV: [A] -> A => Int + val polyImpureV2: [A] -> A => Int \ No newline at end of file diff --git a/local/project/dummy/capturevars.scala b/local/project/dummy/capturevars.scala new file mode 100644 index 000000000000..5cc9e529ada6 --- /dev/null +++ b/local/project/dummy/capturevars.scala @@ -0,0 +1,30 @@ +package dummy + +import language.experimental.captureChecking +import caps.* + +trait Test: + val a: AnyRef^ + val b: AnyRef^ + type Ordinary + type Ordinary2 >: Int <: String + type T[-C^ >: {a,b}] + type U[+C^] + type Foo = [C^ >: {a,b} <: {a,b,cap}] =>> AnyRef^{C} + type C^ + type D^ >: {C} <: {a,b} + type E^ <: C + type F^ <: {D,E} + type G^ = C + type H^ = {C} + def foo[C^ >: {a,b}](x: T[C]): Unit + def bar(x: T[{a,b}]): Unit + def baz(x: T[{a,b,caps.cap}]): Unit + def foo2[C^](x: U[C]): Unit + def bar2(x: U[{a,b,cap}]): Unit + def baz2(x: U[{caps.cap}]): Unit + def test[E^, F^ >: {caps.cap} <: {}, G <: [C^ >: {a,b} <: {a,b}] =>> AnyRef^{C}](x: T[{E,a,b}], y: U[F]): Unit + val poly: [C^ >: {a,b}] => (f: () ->{C} Unit) -> Int ->{C} Unit + + def readup[T^ <: {cap.rd}]() = () + def readlo[T^ >: {cap.rd}]() = () diff --git a/local/project/dummy/colltest.scala b/local/project/dummy/colltest.scala new file mode 100644 index 000000000000..7572806de995 --- /dev/null +++ b/local/project/dummy/colltest.scala @@ -0,0 +1,46 @@ +package dummy + +import language.experimental.captureChecking +// Showing a problem with recursive references +object CollectionStrawMan5 { + + /** Base trait for generic collections */ + trait Iterable[+A] extends IterableLike[A] { + def iterator: Iterator[A]^{this} + def coll: Iterable[A]^{this} = this + } + + trait IterableLike[+A]: + def coll: Iterable[A]^{this} + def partition(p: A => Boolean): Unit = + val pn = Partition(coll, p) + () + + /** Concrete collection type: View */ + trait View[+A] extends Iterable[A] with IterableLike[A] + + case class Partition[A](val underlying: Iterable[A]^, p: A => Boolean) { + + class Partitioned(expected: Boolean) extends View[A]: + this: Partitioned^{Partition.this} => + def iterator: Iterator[A]^{this} = + underlying.iterator.filter((x: A) => p(x) == expected) + + val left: Partitioned^{Partition.this} = Partitioned(true) + val right: Partitioned^{Partition.this} = Partitioned(false) + } + +} + +object CollectionWithPureSelf { + trait Concat[+A, CC[_]] { + def concat[B >: A](other: CC[B]^): CC[B]^{this, other} + def apply[B](f: A => B): CollectionStrawMan5.Iterable[B]^{f, this} + } + + trait Seq[+A] extends Concat[A, Seq] { + self: Seq[A] => + } + + trait ImmutSeq[+A] extends Seq[A] { } +} diff --git a/local/project/dummy/nocc.scala b/local/project/dummy/nocc.scala new file mode 100644 index 000000000000..2f34304c7015 --- /dev/null +++ b/local/project/dummy/nocc.scala @@ -0,0 +1,7 @@ +package dummy + +trait NoCaptureChecking: + def byName(f: => Int): Int + def impure(f: Int => Int): Int + def context(f: Int ?=> Int): Int + def dependent(f: (x: Int) => x.type): Int diff --git a/local/project/dummy/sep-pairs.scala b/local/project/dummy/sep-pairs.scala new file mode 100644 index 000000000000..e3beb1d5c626 --- /dev/null +++ b/local/project/dummy/sep-pairs.scala @@ -0,0 +1,27 @@ +package dummy +import language.experimental.captureChecking +import caps.Mutable +import caps.{cap, consume, use} + +class Ref extends Mutable: + var x = 0 + def get: Int = x + update def put(y: Int): Unit = x = y + +case class Pair[+A, +B](fst: A, snd: B) + +def mkPair: Pair[Ref^, Ref^] = + val r1 = Ref() + val r2 = Ref() + val p_exact: Pair[Ref^{r1}, Ref^{r2}] = Pair(r1, r2) + p_exact + +def copyPair[C^, D^](@consume p: Pair[Ref^{C}, Ref^{D}]): Pair[Ref^{C}, Ref^{D}] = + val x: Ref^{C} = p.fst + val y: Ref^{D} = p.snd + Pair[Ref^{C}, Ref^{D}](x, y) + +trait TestRd: + @consume def copyPair(p: Pair[Ref^, Ref^]): Pair[Ref^{p.fst*}, Ref^{p.snd*}] + def rdPair(@consume p: Pair[Ref^, Ref^]): Int ->{p.fst*.rd} Int + val rdPairV: (p: Pair[Ref^, Ref^]) => Int ->{p.fst*, p.snd*.rd} Int diff --git a/mystuff.sbt b/mystuff.sbt new file mode 100644 index 000000000000..30da6fd54da1 --- /dev/null +++ b/mystuff.sbt @@ -0,0 +1,43 @@ +import sbt._ +import sbt.io.IO + +import sbt.dsl.LinterLevel.Ignore + +lazy val compileSrcTree = taskKey[Unit]("Example project") + +compileSrcTree := { + val log = streams.value.log + val baseDir = baseDirectory.value + val srcTreeDir = baseDir / "local" / "project" + val outDir = baseDir / "local" / "out" + + IO.delete(outDir) + IO.createDirectory(outDir) // mkdir -p + + val sources: Seq[String] = + (srcTreeDir ** "*.scala").get.map(_.getPath) // find all .scala + + if (sources.isEmpty) + streams.value.log.warn(s"No .scala files found under $srcTreeDir") + else { + val cmd = ("scalac" +: "-d" +: outDir.getPath +: sources).mkString(" ") + Command.process(cmd, state.value) + } +} + +lazy val ensureApiDir = taskKey[Unit]("Create /local/api if it’s missing") + +ensureApiDir := { + val dir = (ThisBuild / baseDirectory).value / "local" / "api" + IO.createDirectory(dir) +} + +addCommandAlias( + "myrefresh", + ";compileSrcTree; ensureApiDir ; scaladoc/runMain dotty.tools.scaladoc.Main -siteroot /dev/null -project Foo -project-version 0.0.1 -d local/api local/out" +) + +addCommandAlias( + "myscaladoc", + "; ensureApiDir ; scaladoc/runMain dotty.tools.scaladoc.Main -siteroot /dev/null -project Foo -project-version 0.0.1 -d local/api local/out" +) \ No newline at end of file diff --git a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala index 7e8f9e8bf6a2..626162db55ec 100644 --- a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala +++ b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala @@ -36,6 +36,8 @@ object CaptureDefs: qctx.reflect.Symbol.requiredClass("scala.annotation.internal.readOnlyCapability") def RequiresCapabilityAnnot(using qctx: Quotes) = qctx.reflect.Symbol.requiredClass("scala.annotation.internal.requiresCapability") + def OnlyCapabilityAnnot(using qctx: Quotes) = + qctx.reflect.Symbol.requiredClass("scala.annotation.internal.onlyCapability") def LanguageExperimental(using qctx: Quotes) = qctx.reflect.Symbol.requiredPackage("scala.language.experimental") @@ -71,6 +73,9 @@ extension (using qctx: Quotes)(ann: qctx.reflect.Symbol) def isReadOnlyCapabilityAnnot: Boolean = ann == CaptureDefs.ReadOnlyCapabilityAnnot + + def isOnlyCapabilityAnnot: Boolean = + ann == CaptureDefs.OnlyCapabilityAnnot end extension extension (using qctx: Quotes)(tpe: qctx.reflect.TypeRepr) // FIXME clean up and have versions on Symbol for those @@ -171,6 +176,17 @@ object ReadOnlyCapability: case _ => None end ReadOnlyCapability +object OnlyCapability: + def unapply(using qctx: Quotes)(ty: qctx.reflect.TypeRepr): Option[(qctx.reflect.TypeRepr, qctx.reflect.Symbol)] = + import qctx.reflect._ + ty match + case AnnotatedType(base, app @ Apply(TypeApply(Select(New(annot), _), _), Nil)) if annot.tpe.typeSymbol.isOnlyCapabilityAnnot => + app.tpe.typeArgs.head.classSymbol.match + case Some(clazzsym) => Some((base, clazzsym)) + case None => None + case _ => None +end OnlyCapability + /** 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. */ @@ -187,8 +203,9 @@ def decomposeCaptureRefs(using qctx: Quotes)(typ0: qctx.reflect.TypeRepr): Optio 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 + case t @ OnlyCapability(_, _) => include(t) + case t : TypeRef => include(t) + case _ => report.warning(s"Unexpected type tree $typ while trying to extract capture references from $typ0"); false if traverse(typ0) then Some(buffer.toList) else None end decomposeCaptureRefs diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index ad14009c5829..92145b44b207 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -10,6 +10,7 @@ import dotty.tools.scaladoc.cc.* import NameNormalizer._ import SyntheticsSupport._ +import java.awt.RenderingHints.Key trait TypesSupport: self: TastyParser => @@ -514,10 +515,11 @@ trait TypesSupport: private def emitCapability(using Quotes)(ref: reflect.TypeRepr, skipThisTypePrefix: Boolean)(using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol): SSignature = import reflect._ ref match - case ReachCapability(c) => emitCapability(c, skipThisTypePrefix) :+ Keyword("*") - case ReadOnlyCapability(c) => emitCapability(c, skipThisTypePrefix) :+ Keyword(".rd") - case ThisType(_) => List(Keyword("this")) - case t => inner(t, skipThisTypePrefix)(using skipTypeSuffix = true, inCC = Some(Nil)) + case ReachCapability(c) => emitCapability(c, skipThisTypePrefix) :+ Keyword("*") + case ReadOnlyCapability(c) => emitCapability(c, skipThisTypePrefix) :+ Keyword(".rd") + case OnlyCapability(c, cls) => emitCapability(c, skipThisTypePrefix) ++ List(Plain("."), Keyword("only"), Plain("[")) ++ inner(cls.typeRef, skipThisTypePrefix) :+ Plain("]") + case ThisType(_) => List(Keyword("this")) + case t => inner(t, skipThisTypePrefix)(using skipTypeSuffix = true, inCC = Some(Nil)) private def emitCaptureSet(using Quotes)(refs: List[reflect.TypeRepr], skipThisTypePrefix: Boolean, omitCap: Boolean = true)(using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol): SSignature = import reflect._ From 766725cb108a3cb5b4a29b41cdc13b1d5b604f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Mon, 11 Aug 2025 14:01:37 +0200 Subject: [PATCH 5/6] adapt Build.scala to scala2-library-cc --- project/Build.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index fc588d45de58..29430a48c693 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -3765,7 +3765,7 @@ object ScaladocConfigs { lazy val Scala3 = Def.task { val dottyJars: Seq[java.io.File] = Seq( - (`scala2-library-bootstrapped`/Compile/products).value, + (`scala2-library-cc`/Compile/products).value, (`scala3-library-bootstrapped`/Compile/products).value, (`scala3-interfaces`/Compile/products).value, (`tasty-core-bootstrapped`/Compile/products).value, @@ -3774,7 +3774,7 @@ object ScaladocConfigs { val roots = dottyJars.map(_.getAbsolutePath) val managedSources = - (`scala2-library-bootstrapped`/Compile/sourceManaged).value / "scala-library-src" + (`scala2-library-cc`/Compile/sourceManaged).value / "scala-library-src" val projectRoot = (ThisBuild/baseDirectory).value.toPath val stdLibRoot = projectRoot.relativize(managedSources.toPath.normalize()) val docRootFile = stdLibRoot.resolve("rootdoc.txt") @@ -3810,7 +3810,7 @@ object ScaladocConfigs { } def stableScala3(version: String) = Def.task { - val scalaLibrarySrc = s"out/bootstrap/scala2-library-bootstrapped/scala-$version-bin-SNAPSHOT-nonbootstrapped/src_managed" + val scalaLibrarySrc = s"out/bootstrap/scala2-library-cc/scala-$version-bin-SNAPSHOT-nonbootstrapped/src_managed" val dottyLibrarySrc = "library/src" Scala3.value .add(defaultSourceLinks(version + "-bin-SNAPSHOT-nonbootstrapped", version).value) @@ -3831,7 +3831,7 @@ object ScaladocConfigs { .add(DocRootContent(s"$scalaLibrarySrc/rootdoc.txt")) .withTargets( Seq( - s"out/bootstrap/scala2-library-bootstrapped/scala-$version-bin-SNAPSHOT-nonbootstrapped/classes", + s"out/bootstrap/scala2-library-cc/scala-$version-bin-SNAPSHOT-nonbootstrapped/classes", s"out/bootstrap/scala3-library-bootstrapped/scala-$version-bin-SNAPSHOT-nonbootstrapped/classes", s"tmp/interfaces/target/classes", s"out/bootstrap/tasty-core-bootstrapped/scala-$version-bin-SNAPSHOT-nonbootstrapped/classes" From cdfe298ef4f8511801b09f711468ea94a087366d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Mon, 11 Aug 2025 18:57:14 +0200 Subject: [PATCH 6/6] Scaladoc: Option to fully disable capture checking --- project/Build.scala | 2 +- project/ScaladocGeneration.scala | 4 ++++ scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala | 2 ++ .../src/dotty/tools/scaladoc/ScaladocSettings.scala | 5 ++++- .../tools/scaladoc/renderers/MemberRenderer.scala | 3 ++- .../dotty/tools/scaladoc/renderers/Resources.scala | 2 +- .../src/dotty/tools/scaladoc/tasty/BasicSupport.scala | 8 +++++--- .../dotty/tools/scaladoc/tasty/ClassLikeSupport.scala | 9 +++++++++ .../dotty/tools/scaladoc/tasty/PackageSupport.scala | 2 +- scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala | 1 - .../src/dotty/tools/scaladoc/tasty/TastyParser.scala | 2 +- .../src/dotty/tools/scaladoc/tasty/TypesSupport.scala | 11 +++++------ .../scaladoc/translators/ScalaSignatureProvider.scala | 10 +++++----- 13 files changed, 40 insertions(+), 21 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 29430a48c693..c32f194c1992 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -3062,7 +3062,7 @@ object Build { val overrideFunc = outputDirOverride.andThen(justAPI) val config = Def.task { - overrideFunc(Scala3.value) + overrideFunc(Scala3.value) // .add(SuppressCC(true)) // <- turns off CC rendering completely } val writeAdditionalFiles = Def.task { diff --git a/project/ScaladocGeneration.scala b/project/ScaladocGeneration.scala index aac9f187a888..c06122f5fadf 100644 --- a/project/ScaladocGeneration.scala +++ b/project/ScaladocGeneration.scala @@ -141,6 +141,10 @@ object ScaladocGeneration { def key: String = "-dynamic-side-menu" } + case class SuppressCC(value: Boolean) extends Arg[Boolean] { + def key: String = "-suppressCC" + } + import _root_.scala.reflect._ trait GenerationConfig { diff --git a/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala b/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala index a2485085a927..50e7589d92fe 100644 --- a/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala +++ b/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala @@ -47,6 +47,7 @@ object Scaladoc: defaultTemplate: Option[String] = None, quickLinks: List[QuickLink] = List.empty, dynamicSideMenu: Boolean = false, + suppressCC: Boolean = false, // suppress rendering anything related to experimental capture checking ) def run(args: Array[String], rootContext: CompilerContext): Reporter = @@ -231,6 +232,7 @@ object Scaladoc: defaultTemplate.nonDefault, quickLinksParsed, dynamicSideMenu.get, + suppressCC.get, ) (Some(docArgs), newContext) } diff --git a/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala b/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala index 5acfac03d52c..8189d4f45286 100644 --- a/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala +++ b/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala @@ -144,5 +144,8 @@ class ScaladocSettings extends SettingGroup with AllScalaSettings: val dynamicSideMenu: Setting[Boolean] = BooleanSetting(RootSetting, "dynamic-side-menu", "Generate side menu via JS instead of embedding it in every html file", false) + val suppressCC: Setting[Boolean] = + BooleanSetting(RootSetting, "suppressCC", "Suppress rendering anything related to experimental capture checking", false) + def scaladocSpecificSettings: Set[Setting[?]] = - Set(sourceLinks, legacySourceLink, syntax, revision, externalDocumentationMappings, socialLinks, skipById, skipByRegex, deprecatedSkipPackages, docRootContent, snippetCompiler, generateInkuire, defaultTemplate, scastieConfiguration, quickLinks, dynamicSideMenu) + Set(sourceLinks, legacySourceLink, syntax, revision, externalDocumentationMappings, socialLinks, skipById, skipByRegex, deprecatedSkipPackages, docRootContent, snippetCompiler, generateInkuire, defaultTemplate, scastieConfiguration, quickLinks, dynamicSideMenu, suppressCC) diff --git a/scaladoc/src/dotty/tools/scaladoc/renderers/MemberRenderer.scala b/scaladoc/src/dotty/tools/scaladoc/renderers/MemberRenderer.scala index af39870d87b5..119ec4fca3f8 100644 --- a/scaladoc/src/dotty/tools/scaladoc/renderers/MemberRenderer.scala +++ b/scaladoc/src/dotty/tools/scaladoc/renderers/MemberRenderer.scala @@ -177,7 +177,8 @@ class MemberRenderer(signatureRenderer: SignatureRenderer)(using DocContext) ext cls := s"documentableName $depStyle", ) - val signature: MemberSignature = signatureProvider.rawSignature(member)() + val ctx = summon[DocContext] + val signature: MemberSignature = signatureProvider.rawSignature(member)(!ctx.args.suppressCC)() val isSubtype = signature.suffix.exists { case Keyword(keyword) => keyword.contains("extends") case _ => false diff --git a/scaladoc/src/dotty/tools/scaladoc/renderers/Resources.scala b/scaladoc/src/dotty/tools/scaladoc/renderers/Resources.scala index 3e49af2e0576..af30c3479d81 100644 --- a/scaladoc/src/dotty/tools/scaladoc/renderers/Resources.scala +++ b/scaladoc/src/dotty/tools/scaladoc/renderers/Resources.scala @@ -213,7 +213,7 @@ trait Resources(using ctx: DocContext) extends Locations, Writer: val (res, pageName) = page.content match case m: Member if m.kind != Kind.RootPackage => def processMember(member: Member, fqName: List[String]): Seq[(JSON, Seq[String])] = - val signature: MemberSignature = signatureProvider.rawSignature(member)() + val signature: MemberSignature = signatureProvider.rawSignature(member)(!ctx.args.suppressCC)() val sig = Signature(Plain(member.name)) ++ signature.suffix val descr = if member.kind == Kind.Package then "" else fqName.mkString(".") val extraDescr = member.docs.map(d => docPartRenderPlain(d.body)).getOrElse("") diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala index 81309018718c..06fe658b850e 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala @@ -43,7 +43,7 @@ trait BasicSupport: def getAnnotations(): List[Annotation] = // Custom annotations should be documented only if annotated by @java.lang.annotation.Documented // We allow also some special cases - val fqNameAllowlist = Set( + val fqNameAllowlist0 = Set( "scala.specialized", "scala.throws", "scala.transient", @@ -54,9 +54,11 @@ trait BasicSupport: "scala.annotation.targetName", "scala.annotation.threadUnsafe", "scala.annotation.varargs", - CaptureDefs.useAnnotFullName, - CaptureDefs.consumeAnnotFullName, ) + val fqNameAllowlist = + if ccEnabled then + fqNameAllowlist0 + CaptureDefs.useAnnotFullName + CaptureDefs.consumeAnnotFullName + else fqNameAllowlist0 val documentedSymbol = summon[Quotes].reflect.Symbol.requiredClass("java.lang.annotation.Documented") val annotations = sym.annotations.filter { a => a.tpe.typeSymbol.hasAnnotation(documentedSymbol) || fqNameAllowlist.contains(a.symbol.fullName) diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala index 00635951fb69..86202e53c782 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala @@ -21,6 +21,15 @@ trait ClassLikeSupport: private given qctx.type = qctx + extension (symbol: Symbol) { + def getExtraModifiers(): Seq[Modifier] = + val mods = SymOps.getExtraModifiers(symbol)() + if ccEnabled && symbol.flags.is(Flags.Mutable) then + mods :+ Modifier.Update + else + mods + } + private def bareClasslikeKind(using Quotes)(symbol: reflect.Symbol): Kind = import reflect._ if symbol.flags.is(Flags.Module) then Kind.Object diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/PackageSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/PackageSupport.scala index 8de2ab6b8539..234c2c3cc4f2 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/PackageSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/PackageSupport.scala @@ -15,7 +15,7 @@ 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 + ccFlag = false pck.stats.foreach { case CCImport() => ccFlag = true case _ => diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala index 0464da450f05..969b1d6462c2 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala @@ -100,7 +100,6 @@ 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 } diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TastyParser.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TastyParser.scala index 741147ebfe2e..f7cdf62c5458 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TastyParser.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TastyParser.scala @@ -188,7 +188,7 @@ case class TastyParser( private given qctx.type = qctx protected var ccFlag: Boolean = false - def ccEnabled: Boolean = ccFlag + def ccEnabled: Boolean = !ctx.args.suppressCC && ccFlag val intrinsicClassDefs = Set( defn.AnyClass, diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index 92145b44b207..0ec9a150235f 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -10,7 +10,6 @@ import dotty.tools.scaladoc.cc.* import NameNormalizer._ import SyntheticsSupport._ -import java.awt.RenderingHints.Key trait TypesSupport: self: TastyParser => @@ -41,13 +40,13 @@ trait TypesSupport: private def keyword(str: String): SignaturePart = Keyword(str) private def tpe(str: String, dri: DRI)(using inCC: Option[Any]): SignaturePart = - if inCC.isDefined then + if ccEnabled && inCC.isDefined then dotty.tools.scaladoc.Plain(str) else dotty.tools.scaladoc.Type(str, Some(dri)) private def tpe(str: String)(using inCC: Option[Any]): SignaturePart = - if inCC.isDefined then + if ccEnabled && inCC.isDefined then dotty.tools.scaladoc.Plain(str) else dotty.tools.scaladoc.Type(str, None) @@ -60,7 +59,7 @@ trait TypesSupport: private def tpe(using Quotes)(symbol: reflect.Symbol)(using inCC: Option[Any]): SSignature = import SymOps._ val dri: Option[DRI] = Option(symbol).filterNot(_.isHiddenByVisibility).map(_.dri) - if inCC.isDefined then // we are in the context of a capture set and want paths to be rendered plainly + if ccEnabled && inCC.isDefined then // we are in the context of a capture set and want paths to be rendered plainly dotty.tools.scaladoc.Plain(symbol.normalizedName).l else dotty.tools.scaladoc.Type(symbol.normalizedName, dri).l @@ -135,7 +134,7 @@ trait TypesSupport: inner(tpe, skipThisTypePrefix) :+ plain("*") case AppliedType(repeatedClass, Seq(tpe)) if isRepeated(repeatedClass) => inner(tpe, skipThisTypePrefix) :+ plain("*") - case CapturingType(base, refs) => + case CapturingType(base, refs) if ccEnabled => base match case t @ AppliedType(base, args) if t.isFunctionType => functionType(base, args, skipThisTypePrefix)(using inCC = Some(refs)) @@ -285,7 +284,7 @@ trait TypesSupport: case _ => topLevelProcess(t, skipThisTypePrefix) }) ++ plain("]").l - case t : TypeRef if t.isCapSet => emitCaptureSet(Nil, skipThisTypePrefix) + case t : TypeRef if ccEnabled && t.isCapSet => emitCaptureSet(Nil, skipThisTypePrefix) case tp @ TypeRef(qual, typeName) => inline def wrapping = shouldWrapInParens(inner = qual, outer = tp, isLeft = true) diff --git a/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureProvider.scala b/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureProvider.scala index d62ce4693575..31d57fe9a697 100644 --- a/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureProvider.scala +++ b/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureProvider.scala @@ -6,12 +6,12 @@ import scala.util.chaining._ class ScalaSignatureProvider: val builder = SignatureBuilder() given Conversion[SignatureBuilder, Signature] = bdr => bdr.content - def rawSignature(documentable: Member)(kind: Kind = documentable.kind): MemberSignature = + def rawSignature(documentable: Member)(allowCC: Boolean /*capture checking enabled?*/)(kind: Kind = documentable.kind): MemberSignature = kind match case Kind.Extension(_, m) => extensionSignature(documentable, m) case Kind.Exported(d) => - rawSignature(documentable)(d) + rawSignature(documentable)(allowCC)(d) case d: Kind.Def => methodSignature(documentable, d) case Kind.Constructor(d) => @@ -39,7 +39,7 @@ class ScalaSignatureProvider: case Kind.Val | Kind.Var | Kind.Implicit(Kind.Val, _) => fieldSignature(documentable, kind) case tpe: Kind.Type => - typeSignature(tpe, documentable) + typeSignature(tpe, documentable)(allowCC) case Kind.Package => MemberSignature( Nil, @@ -145,11 +145,11 @@ class ScalaSignatureProvider: case _ => fieldLikeSignature(field, field.kind, None) - private def typeSignature(tpe: Kind.Type, typeDef: Member): MemberSignature = + private def typeSignature(tpe: Kind.Type, typeDef: Member)(allowCC: Boolean = false): MemberSignature = MemberSignature( builder.modifiersAndVisibility(typeDef), builder.kind(tpe), - builder.name(typeDef.name, typeDef.dri, isCaptureVar = tpe.isCaptureVar), + builder.name(typeDef.name, typeDef.dri, isCaptureVar = allowCC && tpe.isCaptureVar), builder.typeParamList(tpe.typeParams).pipe { bdr => if (!tpe.opaque) { (if tpe.concreate then bdr.plain(" = ") else bdr)