From abdf293ff25f2920dccfb50b9a6cb5cee4629f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Thu, 3 Jul 2025 15:32:05 +0200 Subject: [PATCH 01/20] Trying Scaladoc + scala2-library-cc [skip ci] --- project/Build.scala | 10 +++---- .../tools/scaladoc/tasty/TypesSupport.scala | 29 ++++++++++++++++++- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index e04f334c32b7..98420382c866 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2860,7 +2860,7 @@ object ScaladocConfigs { def defaultSourceLinks(version: String = dottyNonBootstrappedVersion, refVersion: String = dottyVersion) = Def.task { def stdLibVersion = stdlibVersion(NonBootstrapped) - def srcManaged(v: String, s: String) = s"out/bootstrap/scala2-library-bootstrapped/scala-$v/src_managed/main/$s-library-src" + def srcManaged(v: String, s: String) = s"out/bootstrap/scala2-library-cc/scala-$v/src_managed/main/$s-library-src" SourceLinks( List( scalaSrcLink(stdLibVersion, srcManaged(version, "scala") + "="), @@ -2949,7 +2949,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, @@ -2958,7 +2958,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") @@ -2994,7 +2994,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) @@ -3015,7 +3015,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" diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index 30a5ac22be0d..26fc3a356ac5 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -116,7 +116,34 @@ trait TypesSupport: inner(tpe, skipThisTypePrefix) :+ plain("*") case AppliedType(repeatedClass, Seq(tpe)) if isRepeated(repeatedClass) => inner(tpe, skipThisTypePrefix) :+ plain("*") - case AnnotatedType(tpe, _) => + case tp @ AnnotatedType(tpe, annotTerm) => + val retainsSym = Symbol.requiredClass("_root_.scala.annotation.retains") + val retainsCapSym = Symbol.requiredClass("_root_.scala.annotation.retainsCap") + val retainsByNameSym = Symbol.requiredClass("_root_.scala.annotation.retainsByName") + val retainsSym2 = Symbol.requiredClass("scala.annotation.retains") + val retainsCapSym2 = Symbol.requiredClass("scala.annotation.retainsCap") + val retainsByNameSym2 = Symbol.requiredClass("scala.annotation.retainsByName") + val sym = annotTerm.tpe match + case AppliedType(base, _) => base.typeSymbol + case other => other.typeSymbol + if sym.name.contains("retains") then + println("Annot: " + sym) + println("name: " + sym.fullName) + println("id: " + System.identityHashCode(sym)) + println("isClassDef: " + sym.isClassDef) + println("isRetains: " + sym == retainsSym) + println("isRetainsCap: " + sym == retainsCapSym) + println("isRetainsByName: " + sym == retainsByNameSym) + println("isRetains2: " + sym == retainsSym2) + println("isRetainsCap2: " + sym == retainsCapSym2) + println("isRetainsByName2: " + sym == retainsByNameSym2) + println("retainsSym " + retainsSym) + println("retainsSym2 " + retainsSym2) + println("retainsSym.isClassDef " + retainsSym.isClassDef) + println("retainsSym2.isClassDef " + retainsSym2.isClassDef) + println("retainsSym.id " + System.identityHashCode(retainsSym)) + println("retainsSym2.id " + System.identityHashCode(retainsSym2)) + println() inner(tpe, skipThisTypePrefix) case tl @ TypeLambda(params, paramBounds, AppliedType(tpe, args)) if paramBounds.forall { case TypeBounds(low, hi) => low.typeSymbol == defn.NothingClass && hi.typeSymbol == defn.AnyClass } From 4cdcb2945e309f494dfa70afe68a62437aac55b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Thu, 3 Jul 2025 16:58:51 +0200 Subject: [PATCH 02/20] Hello retains --- .../dotty/tools/scaladoc/cc/CaptureOps.scala | 29 ++++++++++++++++ .../tools/scaladoc/tasty/TypesSupport.scala | 33 +++---------------- 2 files changed, 34 insertions(+), 28 deletions(-) create mode 100644 scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala 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..d9f05ecbf5e0 --- /dev/null +++ b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala @@ -0,0 +1,29 @@ +package dotty.tools.scaladoc.cc + +import scala.quoted._ + +object CaptureDefs: + /** The name of the `retains` annotation. */ + val RetainsName: String = "scala.annotation.retains" + + /** The name of the `retainsCap` annotation. */ + val RetainsCapName: String = "scala.annotation.retainsCap" + + /** The name of the `retainsByName` annotation. */ + val RetainsByNameName: String = "scala.annotation.retainsByName" + + def retains(using qctx: Quotes) = qctx.reflect.Symbol.requiredClass(RetainsName) + def retainsCap(using qctx: Quotes) = qctx.reflect.Symbol.requiredClass(RetainsCapName) + def retainsByName(using qctx: Quotes) = qctx.reflect.Symbol.requiredClass(RetainsByNameName) +end CaptureDefs + +extension (using qctx: Quotes)(term: qctx.reflect.Term) + + /** Is this term a `retains`* annotations from capturing types? */ + def isRetains: Boolean = + val sym = term.tpe match + case qctx.reflect.AppliedType(base, _) => base.typeSymbol + case other => other.typeSymbol + sym == CaptureDefs.retains + || sym == CaptureDefs.retainsCap + || sym == CaptureDefs.retainsByName \ No newline at end of file diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index 26fc3a356ac5..0ddb7a76fcb7 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -6,6 +6,8 @@ import scala.jdk.CollectionConverters._ import scala.quoted._ import scala.util.control.NonFatal +import dotty.tools.scaladoc.cc.* + import NameNormalizer._ import SyntheticsSupport._ @@ -116,34 +118,9 @@ trait TypesSupport: inner(tpe, skipThisTypePrefix) :+ plain("*") case AppliedType(repeatedClass, Seq(tpe)) if isRepeated(repeatedClass) => inner(tpe, skipThisTypePrefix) :+ plain("*") - case tp @ AnnotatedType(tpe, annotTerm) => - val retainsSym = Symbol.requiredClass("_root_.scala.annotation.retains") - val retainsCapSym = Symbol.requiredClass("_root_.scala.annotation.retainsCap") - val retainsByNameSym = Symbol.requiredClass("_root_.scala.annotation.retainsByName") - val retainsSym2 = Symbol.requiredClass("scala.annotation.retains") - val retainsCapSym2 = Symbol.requiredClass("scala.annotation.retainsCap") - val retainsByNameSym2 = Symbol.requiredClass("scala.annotation.retainsByName") - val sym = annotTerm.tpe match - case AppliedType(base, _) => base.typeSymbol - case other => other.typeSymbol - if sym.name.contains("retains") then - println("Annot: " + sym) - println("name: " + sym.fullName) - println("id: " + System.identityHashCode(sym)) - println("isClassDef: " + sym.isClassDef) - println("isRetains: " + sym == retainsSym) - println("isRetainsCap: " + sym == retainsCapSym) - println("isRetainsByName: " + sym == retainsByNameSym) - println("isRetains2: " + sym == retainsSym2) - println("isRetainsCap2: " + sym == retainsCapSym2) - println("isRetainsByName2: " + sym == retainsByNameSym2) - println("retainsSym " + retainsSym) - println("retainsSym2 " + retainsSym2) - println("retainsSym.isClassDef " + retainsSym.isClassDef) - println("retainsSym2.isClassDef " + retainsSym2.isClassDef) - println("retainsSym.id " + System.identityHashCode(retainsSym)) - println("retainsSym2.id " + System.identityHashCode(retainsSym2)) - println() + case AnnotatedType(tpe, annotTerm) if annotTerm.isRetains => + inner(tpe, skipThisTypePrefix) :+ plain(" @retains") // FIXME + case AnnotatedType(tpe, _) => inner(tpe, skipThisTypePrefix) case tl @ TypeLambda(params, paramBounds, AppliedType(tpe, args)) if paramBounds.forall { case TypeBounds(low, hi) => low.typeSymbol == defn.NothingClass && hi.typeSymbol == defn.AnyClass } From 3ad644bb8bffe7839343270ff4b4dedb9299c6a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Fri, 4 Jul 2025 16:54:26 +0200 Subject: [PATCH 03/20] Nice capture-set rendering --- .../dotty/tools/scaladoc/cc/CaptureOps.scala | 97 +++++++++++++++---- .../tools/scaladoc/tasty/NameNormalizer.scala | 6 +- .../tools/scaladoc/tasty/TypesSupport.scala | 13 ++- 3 files changed, 90 insertions(+), 26 deletions(-) diff --git a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala index d9f05ecbf5e0..3db18a6e1074 100644 --- a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala +++ b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala @@ -1,29 +1,86 @@ -package dotty.tools.scaladoc.cc +package dotty.tools.scaladoc + +package cc import scala.quoted._ object CaptureDefs: - /** The name of the `retains` annotation. */ - val RetainsName: String = "scala.annotation.retains" - - /** The name of the `retainsCap` annotation. */ - val RetainsCapName: String = "scala.annotation.retainsCap" + // 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") - /** The name of the `retainsByName` annotation. */ - val RetainsByNameName: String = "scala.annotation.retainsByName" + 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 retains(using qctx: Quotes) = qctx.reflect.Symbol.requiredClass(RetainsName) - def retainsCap(using qctx: Quotes) = qctx.reflect.Symbol.requiredClass(RetainsCapName) - def retainsByName(using qctx: Quotes) = qctx.reflect.Symbol.requiredClass(RetainsByNameName) end CaptureDefs -extension (using qctx: Quotes)(term: qctx.reflect.Term) - - /** Is this term a `retains`* annotations from capturing types? */ +extension (using qctx: Quotes)(ann: qctx.reflect.Symbol) + /** This symbol is one of `retains` or `retainsCap` */ def isRetains: Boolean = - val sym = term.tpe match - case qctx.reflect.AppliedType(base, _) => base.typeSymbol - case other => other.typeSymbol - sym == CaptureDefs.retains - || sym == CaptureDefs.retainsCap - || sym == CaptureDefs.retainsByName \ No newline at end of file + ann == CaptureDefs.retains || ann == CaptureDefs.retainsCap + + /** This symbol is one of `retains`, `retainsCap`, or `retainsByName` */ + def isRetainsLike: Boolean = + ann.isRetains || ann == CaptureDefs.retainsByName +end extension + +extension (using qctx: Quotes)(tpe: qctx.reflect.TypeRepr) + def isCaptureRoot: Boolean = tpe.termSymbol == CaptureDefs.captureRoot +end extension + +/** 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 traverse(typ: TypeRepr): Boolean = + typ match + case OrType(t1, t2) => traverse(t1) && traverse(t2) + case t @ ThisType(_) => buffer += t; true + case t @ TermRef(_, _) => buffer += t; true + case t @ ParamRef(_, _) => buffer += t; true + // TODO: are atoms only ever the above? Then we could refine the return type + case _ => report.warning(s"Unexpected type tree $typ while trying to extract capture references from $typ0"); System.exit(1); 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 + +def renderCaptureSet(using qctx: Quotes)(refs: List[qctx.reflect.TypeRepr]): List[SignaturePart] = + import dotty.tools.scaladoc.tasty.NameNormalizer._ + import qctx.reflect._ + refs match + case List(ref) if ref.isCaptureRoot => List(Keyword("^")) + case refs => + val res0 = refs.map { ref => + ref match + case ThisType(_) => List(Keyword("this")) + case TermRef(_, sym) => List(Plain(sym)) // FIXME: use type other than Plain, can we have clickable links to say, caps.cap and other things? + case pf @ ParamRef(tpe, i) => List(Plain(tpe.asInstanceOf[MethodType].paramNames(i))) // FIXME: not sure if this covers all cases + case _ => List(Plain("")) + } + val res1 = res0 match + case Nil => Nil + case other => other.reduce((r, e) => r ++ (List(Plain(", ")) ++ e)) + Keyword("^") :: Plain("{") :: (res1 ++ List(Plain("}"))) \ No newline at end of file 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/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index 0ddb7a76fcb7..c0006df6e23c 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -107,7 +107,16 @@ 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 CapturingType(base, refs) => + inner(base, skipThisTypePrefix) ++ renderCaptureSet(refs) + case ByNameType(CapturingType(tpe, refs)) => + refs match + case Nil => keyword("-> ") :: inner(tpe, skipThisTypePrefix) + case List(ref) if ref.isCaptureRoot => + keyword("=> ") :: inner(tpe, skipThisTypePrefix) + case refs => + keyword("->") :: (renderCaptureSet(refs) ++ inner(tpe, skipThisTypePrefix)) + case ByNameType(tpe) => keyword("=> ") :: inner(tpe, skipThisTypePrefix) // FIXME: does it need change for CC? case ConstantType(constant) => plain(constant.show).l case ThisType(tpe) => @@ -118,8 +127,6 @@ trait TypesSupport: inner(tpe, skipThisTypePrefix) :+ plain("*") case AppliedType(repeatedClass, Seq(tpe)) if isRepeated(repeatedClass) => inner(tpe, skipThisTypePrefix) :+ plain("*") - case AnnotatedType(tpe, annotTerm) if annotTerm.isRetains => - inner(tpe, skipThisTypePrefix) :+ plain(" @retains") // FIXME case AnnotatedType(tpe, _) => inner(tpe, skipThisTypePrefix) case tl @ TypeLambda(params, paramBounds, AppliedType(tpe, args)) From 88934a3b7c76eba84a65d3cae4cd5c22aa08bb78 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Tue, 15 Jul 2025 17:54:01 +0200 Subject: [PATCH 04/20] Add small local test project --- local/project/dummy/arrows.scala | 35 +++++++++++++++ local/project/dummy/capturevars.scala | 16 +++++++ local/project/dummy/colltest.scala | 34 +++++++++++++++ mystuff.sbt | 43 +++++++++++++++++++ .../dotty/tools/scaladoc/cc/CaptureOps.scala | 3 +- .../tools/scaladoc/tasty/TypesSupport.scala | 4 +- 6 files changed, 132 insertions(+), 3 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 mystuff.sbt diff --git a/local/project/dummy/arrows.scala b/local/project/dummy/arrows.scala new file mode 100644 index 000000000000..78a0217a67c0 --- /dev/null +++ b/local/project/dummy/arrows.scala @@ -0,0 +1,35 @@ +package dummy + +import language.experimental.captureChecking +import caps.* + +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 + + 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 uses(@use a: AnyRef^): Any + def uses2(@use x: AnyRef^{a}, @use y: AnyRef^{b}): Any + + def consumes(@consume a: AnyRef^): Any + def consumes2(@consume x: AnyRef^{a}, @consume y: AnyRef^{b}): Any + + def usesAndConsumes(@use a: AnyRef^, @consume b: AnyRef^): Any + def usesAndConsumes2(@use @consume x: AnyRef^{a}): Any + def consumesAndUses(@consume @use x: AnyRef^{a}): Any + + def byNamePure(f: -> Int): Int + def byNameImpure(f: ->{a,b,c} Int): Int + def byNameImpure2(f: => Int): Int diff --git a/local/project/dummy/capturevars.scala b/local/project/dummy/capturevars.scala new file mode 100644 index 000000000000..920275411e08 --- /dev/null +++ b/local/project/dummy/capturevars.scala @@ -0,0 +1,16 @@ +package dummy + +import language.experimental.captureChecking + +trait Test: + type T[-C^] + type U[+C^] + type C^ + type D^ + def foo[C^](x: T[C]): Unit + def bar(x: T[{}]): Unit + def baz(x: T[{caps.cap}]): Unit + def foo2[C^](x: U[C]): Unit + def bar2(x: U[{}]): Unit + def baz2(x: U[{caps.cap}]): Unit + def test[E^, F^ >: {caps.cap} <: {}](x: T[E], y: U[F]): Unit diff --git a/local/project/dummy/colltest.scala b/local/project/dummy/colltest.scala new file mode 100644 index 000000000000..32561aad6754 --- /dev/null +++ b/local/project/dummy/colltest.scala @@ -0,0 +1,34 @@ +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) + } + + +} \ No newline at end of file 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 3db18a6e1074..b5f34253f81c 100644 --- a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala +++ b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala @@ -47,8 +47,9 @@ def decomposeCaptureRefs(using qctx: Quotes)(typ0: qctx.reflect.TypeRepr): Optio case t @ ThisType(_) => buffer += t; true case t @ TermRef(_, _) => buffer += t; true case t @ ParamRef(_, _) => buffer += t; true + case t if t.typeSymbol == defn.NothingClass => true // TODO: are atoms only ever the above? Then we could refine the return type - case _ => report.warning(s"Unexpected type tree $typ while trying to extract capture references from $typ0"); System.exit(1); false // TODO remove warning eventually + 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 diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index c0006df6e23c..01f29def4d06 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -115,8 +115,8 @@ trait TypesSupport: case List(ref) if ref.isCaptureRoot => keyword("=> ") :: inner(tpe, skipThisTypePrefix) case refs => - keyword("->") :: (renderCaptureSet(refs) ++ inner(tpe, skipThisTypePrefix)) - case ByNameType(tpe) => keyword("=> ") :: inner(tpe, skipThisTypePrefix) // FIXME: does it need change for CC? + keyword("->") :: (renderCaptureSet(refs) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix))) + case ByNameType(tpe) => keyword("=>!! ") :: inner(tpe, skipThisTypePrefix) // FIXME: does it need change for CC? case ConstantType(constant) => plain(constant.show).l case ThisType(tpe) => From 5e6b805d2bc1517f2564459574417ef6cc620f12 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Tue, 15 Jul 2025 17:58:11 +0200 Subject: [PATCH 05/20] Fix rendering paths and reach capabilities --- local/project/dummy/arrows.scala | 13 +++ .../dotty/tools/scaladoc/cc/CaptureOps.scala | 85 +++++++++++-------- .../tools/scaladoc/tasty/TypesSupport.scala | 54 ++++++++++-- 3 files changed, 108 insertions(+), 44 deletions(-) diff --git a/local/project/dummy/arrows.scala b/local/project/dummy/arrows.scala index 78a0217a67c0..d5ef1859c61b 100644 --- a/local/project/dummy/arrows.scala +++ b/local/project/dummy/arrows.scala @@ -3,6 +3,10 @@ package dummy import language.experimental.captureChecking import caps.* +trait Nested: + val c: AnyRef^ + val next: Nested + trait Arrows: val a: AnyRef^ val b: AnyRef^ @@ -29,7 +33,16 @@ trait Arrows: def usesAndConsumes(@use a: AnyRef^, @consume b: AnyRef^): Any def usesAndConsumes2(@use @consume x: AnyRef^{a}): Any def consumesAndUses(@consume @use x: AnyRef^{a}): Any + def consumesAndUses2(@consume @use x: List[AnyRef^]): Array[AnyRef^{x*}] + + 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} \ 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 b5f34253f81c..4589f266e485 100644 --- a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala +++ b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala @@ -6,19 +6,36 @@ 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 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") end CaptureDefs extension (using qctx: Quotes)(ann: qctx.reflect.Symbol) @@ -29,24 +46,38 @@ extension (using qctx: Quotes)(ann: qctx.reflect.Symbol) /** This symbol is one of `retains`, `retainsCap`, or `retainsByName` */ def isRetainsLike: Boolean = ann.isRetains || ann == CaptureDefs.retainsByName + + def isReachCapabilityAnnot: Boolean = + ann == CaptureDefs.ReachCapabilityAnnot end extension extension (using qctx: Quotes)(tpe: qctx.reflect.TypeRepr) def isCaptureRoot: Boolean = tpe.termSymbol == CaptureDefs.captureRoot end extension +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 + /** 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 OrType(t1, t2) => traverse(t1) && traverse(t2) - case t @ ThisType(_) => buffer += t; true - case t @ TermRef(_, _) => buffer += t; true - case t @ ParamRef(_, _) => buffer += t; 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 if t.typeSymbol == defn.NothingClass => true // TODO: are atoms only ever the above? Then we could refine the return type case _ => report.warning(s"Unexpected type tree $typ while trying to extract capture references from $typ0"); false // TODO remove warning eventually @@ -66,22 +97,4 @@ object CapturingType: case AnnotatedType(base, Apply(Select(New(annot), _), Nil)) if annot.symbol == CaptureDefs.retainsCap => Some((base, List(CaptureDefs.captureRoot.termRef))) case _ => None -end CapturingType - -def renderCaptureSet(using qctx: Quotes)(refs: List[qctx.reflect.TypeRepr]): List[SignaturePart] = - import dotty.tools.scaladoc.tasty.NameNormalizer._ - import qctx.reflect._ - refs match - case List(ref) if ref.isCaptureRoot => List(Keyword("^")) - case refs => - val res0 = refs.map { ref => - ref match - case ThisType(_) => List(Keyword("this")) - case TermRef(_, sym) => List(Plain(sym)) // FIXME: use type other than Plain, can we have clickable links to say, caps.cap and other things? - case pf @ ParamRef(tpe, i) => List(Plain(tpe.asInstanceOf[MethodType].paramNames(i))) // FIXME: not sure if this covers all cases - case _ => List(Plain("")) - } - val res1 = res0 match - case Nil => Nil - case other => other.reduce((r, e) => r ++ (List(Plain(", ")) ++ e)) - Keyword("^") :: Plain("{") :: (res1 ++ List(Plain("}"))) \ No newline at end of file +end CapturingType \ No newline at end of file diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index 01f29def4d06..76980c28a472 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -21,12 +21,13 @@ 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 = tpeTree.asSignature(elideThis, originalOwner, skipThisTypePrefix = false) + given TypeSyntax: AnyRef with extension (using Quotes)(tpe: reflect.TypeRepr) def asSignature(elideThis: reflect.ClassDef, originalOwner: reflect.Symbol, skipThisTypePrefix: Boolean): SSignature = @@ -39,19 +40,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[Unit]): 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[Unit]): 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[Unit]): 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 + 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 @@ -93,6 +105,9 @@ trait TypesSupport: originalOwner: reflect.Symbol, indent: Int = 0, skipTypeSuffix: Boolean = false, + // inCC means in capture-checking context. + // Somewhat hacky, because it should be a Boolean, but then it'd clash with skipTypeSuffix + inCC: Option[Unit] = None, ): SSignature = import reflect._ def noSupported(name: String): SSignature = @@ -108,7 +123,7 @@ trait TypesSupport: ++ keyword(" & ").l ++ inParens(inner(right, skipThisTypePrefix), shouldWrapInParens(right, tp, false)) case CapturingType(base, refs) => - inner(base, skipThisTypePrefix) ++ renderCaptureSet(refs) + inner(base, skipThisTypePrefix) ++ renderCapturing(refs) case ByNameType(CapturingType(tpe, refs)) => refs match case Nil => keyword("-> ") :: inner(tpe, skipThisTypePrefix) @@ -355,7 +370,7 @@ 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[Unit] ) = import reflect._ def regularTypeBounds(low: TypeRepr, high: TypeRepr) = @@ -366,7 +381,7 @@ trait TypesSupport: 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) }) ++ plain("]").l else regularTypeBounds(low, high) @@ -448,3 +463,26 @@ trait TypesSupport: tr match case AnnotatedType(tr, _) => stripAnnotated(tr) case other => other + + private def renderCapability(using Quotes)(ref: reflect.TypeRepr)(using elideThis: reflect.ClassDef): List[SignaturePart] = + import reflect._ + ref match + case ReachCapability(c) => renderCapability(c) :+ Keyword("*") + case ThisType(_) => List(Keyword("this")) + case t => inner(t)(using skipTypeSuffix = true, inCC = Some(())) + + private def renderCaptureSet(using Quotes)(refs: List[reflect.TypeRepr])(using elideThis: reflect.ClassDef): List[SignaturePart] = + import dotty.tools.scaladoc.tasty.NameNormalizer._ + import reflect._ + refs match + case List(ref) if ref.isCaptureRoot => Nil + case refs => + val res0 = refs.map(renderCapability) + val res1 = res0 match + case Nil => Nil + case other => other.reduce((r, e) => r ++ (List(Plain(", ")) ++ e)) + Plain("{") :: (res1 ++ List(Plain("}"))) + + private def renderCapturing(using Quotes)(refs: List[reflect.TypeRepr])(using elideThis: reflect.ClassDef): List[SignaturePart] = + import reflect._ + Keyword("^") :: renderCaptureSet(refs) From 2533881d9471e9a03f163da7daa0a12c0a0ae01a Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Tue, 15 Jul 2025 21:00:14 +0200 Subject: [PATCH 06/20] Fix function-arrowing rendering --- .../tools/scaladoc/tasty/TypesSupport.scala | 98 ++++++++++++------- 1 file changed, 63 insertions(+), 35 deletions(-) diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index 76980c28a472..28061941d9b4 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -40,13 +40,13 @@ trait TypesSupport: private def keyword(str: String): SignaturePart = Keyword(str) - private def tpe(str: String, dri: DRI)(using inCC: Option[Unit]): SignaturePart = + 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)(using inCC: Option[Unit]): SignaturePart = + private def tpe(str: String)(using inCC: Option[Any]): SignaturePart = if inCC.isDefined then dotty.tools.scaladoc.Plain(str) else @@ -57,7 +57,7 @@ trait TypesSupport: extension (on: SignaturePart) def l: List[SignaturePart] = List(on) - private def tpe(using Quotes)(symbol: reflect.Symbol)(using inCC: Option[Unit]): 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) if inCC.isDefined then @@ -105,9 +105,8 @@ trait TypesSupport: originalOwner: reflect.Symbol, indent: Int = 0, skipTypeSuffix: Boolean = false, - // inCC means in capture-checking context. - // Somewhat hacky, because it should be a Boolean, but then it'd clash with skipTypeSuffix - inCC: Option[Unit] = None, + // 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 = @@ -122,15 +121,8 @@ trait TypesSupport: inParens(inner(left, skipThisTypePrefix), shouldWrapInParens(left, tp, true)) ++ keyword(" & ").l ++ inParens(inner(right, skipThisTypePrefix), shouldWrapInParens(right, tp, false)) - case CapturingType(base, refs) => - inner(base, skipThisTypePrefix) ++ renderCapturing(refs) case ByNameType(CapturingType(tpe, refs)) => - refs match - case Nil => keyword("-> ") :: inner(tpe, skipThisTypePrefix) - case List(ref) if ref.isCaptureRoot => - keyword("=> ") :: inner(tpe, skipThisTypePrefix) - case refs => - keyword("->") :: (renderCaptureSet(refs) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix))) + renderCaptureArrow(refs, skipThisTypePrefix) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix)) case ByNameType(tpe) => keyword("=>!! ") :: inner(tpe, skipThisTypePrefix) // FIXME: does it need change for CC? case ConstantType(constant) => plain(constant.show).l @@ -142,6 +134,10 @@ 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(t, base, args, skipThisTypePrefix)(using inCC = Some(refs)) + case _ => inner(base, skipThisTypePrefix) ++ renderCapturing(refs, skipThisTypePrefix) case AnnotatedType(tpe, _) => inner(tpe, skipThisTypePrefix) case tl @ TypeLambda(params, paramBounds, AppliedType(tpe, args)) @@ -258,19 +254,7 @@ trait TypesSupport: ++ plain(" ").l ++ 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) + case t @ AppliedType(tpe, args) if t.isFunctionType => functionType(t, tpe, args, skipThisTypePrefix) case t @ AppliedType(tpe, typeList) => inner(tpe, skipThisTypePrefix) ++ plain("[").l ++ commas(typeList.map { t => t match @@ -358,6 +342,27 @@ trait TypesSupport: s"${tpe.show(using Printer.TypeReprStructure)}" throw MatchError(msg) + private def functionType(using Quotes)(t: reflect.TypeRepr, tpe: reflect.TypeRepr, args: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)(using + elideThis: reflect.ClassDef, + indent: Int, + originalOwner: reflect.Symbol, + inCC: Option[List[reflect.TypeRepr]], + ): SSignature = + import reflect._ + val arrow = if t.isContextFunctionType then keyword(" ?=> ").l // FIXME: can we have contextual functions with capture sets? + else plain(" ") :: (renderCaptureArrow(inCC, skipThisTypePrefix) ++ plain(" ").l) + 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 @@ -370,7 +375,7 @@ trait TypesSupport: } private def typeBoundsTreeOfHigherKindedType(using Quotes)(low: reflect.TypeRepr, high: reflect.TypeRepr, skipThisTypePrefix: Boolean)( - using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol, inCC: Option[Unit] + using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol, inCC: Option[List[reflect.TypeRepr]] ) = import reflect._ def regularTypeBounds(low: TypeRepr, high: TypeRepr) = @@ -464,25 +469,48 @@ trait TypesSupport: case AnnotatedType(tr, _) => stripAnnotated(tr) case other => other - private def renderCapability(using Quotes)(ref: reflect.TypeRepr)(using elideThis: reflect.ClassDef): List[SignaturePart] = + private def renderCapability(using Quotes)(ref: reflect.TypeRepr, skipThisTypePrefix: Boolean)( + using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol + ): SSignature = import reflect._ ref match - case ReachCapability(c) => renderCapability(c) :+ Keyword("*") + case ReachCapability(c) => renderCapability(c, skipThisTypePrefix) :+ Keyword("*") case ThisType(_) => List(Keyword("this")) - case t => inner(t)(using skipTypeSuffix = true, inCC = Some(())) + case t => inner(t, skipThisTypePrefix)(using skipTypeSuffix = true, inCC = Some(Nil)) - private def renderCaptureSet(using Quotes)(refs: List[reflect.TypeRepr])(using elideThis: reflect.ClassDef): List[SignaturePart] = + private def renderCaptureSet(using Quotes)(refs: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)( + using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol + ): SSignature = import dotty.tools.scaladoc.tasty.NameNormalizer._ import reflect._ refs match case List(ref) if ref.isCaptureRoot => Nil case refs => - val res0 = refs.map(renderCapability) + val res0 = refs.map(renderCapability(_, skipThisTypePrefix)) val res1 = res0 match case Nil => Nil case other => other.reduce((r, e) => r ++ (List(Plain(", ")) ++ e)) Plain("{") :: (res1 ++ List(Plain("}"))) - private def renderCapturing(using Quotes)(refs: List[reflect.TypeRepr])(using elideThis: reflect.ClassDef): List[SignaturePart] = + private def renderCapturing(using Quotes)(refs: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)( + using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol + ): SSignature = import reflect._ - Keyword("^") :: renderCaptureSet(refs) + Keyword("^") :: renderCaptureSet(refs, skipThisTypePrefix) + + private def renderCaptureArrow(using Quotes)(refs: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)( + using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol + ): SSignature = + import reflect._ + refs match + case Nil => List(Keyword("->")) + case List(ref) if ref.isCaptureRoot => List(Keyword("=>")) + case refs => Keyword("->") :: renderCaptureSet(refs, skipThisTypePrefix) + + private def renderCaptureArrow(using Quotes)(refs: Option[List[reflect.TypeRepr]], skipThisTypePrefix: Boolean)( + using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol + ): SSignature = + import reflect._ + refs match + case None => List(Keyword("=>")) // FIXME: is this correct? or should it be `->` by default? + case Some(refs) => renderCaptureArrow(refs, skipThisTypePrefix) From edc1c4dd200a0b23511ac6c7464126fbfc0f6d26 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Wed, 16 Jul 2025 12:47:27 +0200 Subject: [PATCH 07/20] Fix rendering bug in arrow TypesSupport We should not propagate the capture status beyond the current arrow. Under a capture context, types are rendered plainly. --- .../tools/scaladoc/tasty/TypesSupport.scala | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index 28061941d9b4..daa8a5318f92 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -96,7 +96,7 @@ trait TypesSupport: // TODO #23 add support for all types signatures that make sense private def inner( - using Quotes, + using q: Quotes, )( tp: reflect.TypeRepr, skipThisTypePrefix: Boolean @@ -122,7 +122,7 @@ trait TypesSupport: ++ keyword(" & ").l ++ inParens(inner(right, skipThisTypePrefix), shouldWrapInParens(right, tp, false)) case ByNameType(CapturingType(tpe, refs)) => - renderCaptureArrow(refs, skipThisTypePrefix) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix)) + renderCaptureArrow(using q)(Some(refs), skipThisTypePrefix)(using elideThis, originalOwner) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix)) case ByNameType(tpe) => keyword("=>!! ") :: inner(tpe, skipThisTypePrefix) // FIXME: does it need change for CC? case ConstantType(constant) => plain(constant.show).l @@ -342,7 +342,7 @@ trait TypesSupport: s"${tpe.show(using Printer.TypeReprStructure)}" throw MatchError(msg) - private def functionType(using Quotes)(t: reflect.TypeRepr, tpe: reflect.TypeRepr, args: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)(using + private def functionType(using q: Quotes)(t: reflect.TypeRepr, tpe: reflect.TypeRepr, args: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)(using elideThis: reflect.ClassDef, indent: Int, originalOwner: reflect.Symbol, @@ -350,7 +350,8 @@ trait TypesSupport: ): SSignature = import reflect._ val arrow = if t.isContextFunctionType then keyword(" ?=> ").l // FIXME: can we have contextual functions with capture sets? - else plain(" ") :: (renderCaptureArrow(inCC, skipThisTypePrefix) ++ plain(" ").l) + else plain(" ") :: (renderCaptureArrow(using q)(inCC, skipThisTypePrefix) ++ plain(" ").l) + given Option[List[TypeRepr]] = None args match case Nil => Nil case List(rtpe) => plain("()").l ++ arrow ++ inner(rtpe, skipThisTypePrefix) @@ -498,19 +499,19 @@ trait TypesSupport: import reflect._ Keyword("^") :: renderCaptureSet(refs, skipThisTypePrefix) - private def renderCaptureArrow(using Quotes)(refs: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)( + private def renderCaptureArrow(using q: Quotes)(refs: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)( using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol ): SSignature = - import reflect._ + import reflect.* refs match case Nil => List(Keyword("->")) case List(ref) if ref.isCaptureRoot => List(Keyword("=>")) - case refs => Keyword("->") :: renderCaptureSet(refs, skipThisTypePrefix) + case refs => Keyword("->") :: renderCaptureSet(using q)(refs, skipThisTypePrefix) - private def renderCaptureArrow(using Quotes)(refs: Option[List[reflect.TypeRepr]], skipThisTypePrefix: Boolean)( + private def renderCaptureArrow(using q: Quotes)(refs: Option[List[reflect.TypeRepr]], skipThisTypePrefix: Boolean)( using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol ): SSignature = - import reflect._ + import reflect.* refs match case None => List(Keyword("=>")) // FIXME: is this correct? or should it be `->` by default? - case Some(refs) => renderCaptureArrow(refs, skipThisTypePrefix) + case Some(refs) => renderCaptureArrow(using q)(refs, skipThisTypePrefix) From 7658cbae7b9fd7a9c0efda77eae97594fa1a88d6 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Wed, 16 Jul 2025 12:54:27 +0200 Subject: [PATCH 08/20] Fix rendering of context function types --- local/project/dummy/arrows.scala | 7 +++- .../tools/scaladoc/tasty/TypesSupport.scala | 36 +++++++++++-------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/local/project/dummy/arrows.scala b/local/project/dummy/arrows.scala index d5ef1859c61b..43b6f48c332f 100644 --- a/local/project/dummy/arrows.scala +++ b/local/project/dummy/arrows.scala @@ -45,4 +45,9 @@ trait Arrows: 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} \ No newline at end of file + def pathDependent5(n: Nested^)(g: AnyRef^{n.c} => AnyRef^{n.next.c} ->{n.c} Any): AnyRef^{n.next.next.c*, n.c} + + 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 \ No newline at end of file diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index daa8a5318f92..c2dba61b4dd0 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -122,8 +122,10 @@ trait TypesSupport: ++ keyword(" & ").l ++ inParens(inner(right, skipThisTypePrefix), shouldWrapInParens(right, tp, false)) case ByNameType(CapturingType(tpe, refs)) => - renderCaptureArrow(using q)(Some(refs), skipThisTypePrefix)(using elideThis, originalOwner) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix)) - case ByNameType(tpe) => keyword("=>!! ") :: inner(tpe, skipThisTypePrefix) // FIXME: does it need change for CC? + renderCaptureArrow(using q)(refs, false, skipThisTypePrefix) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix)) + case ByNameType(tpe) => + tpe.typeSymbol.pos.map(p => report.warning(s"Pure ByNameType at ${p}")) + keyword("-> ") :: inner(tpe, skipThisTypePrefix) // FIXME need to check if cc is enabled in current file first!!! case ConstantType(constant) => plain(constant.show).l case ThisType(tpe) => @@ -349,9 +351,13 @@ trait TypesSupport: inCC: Option[List[reflect.TypeRepr]], ): SSignature = import reflect._ - val arrow = if t.isContextFunctionType then keyword(" ?=> ").l // FIXME: can we have contextual functions with capture sets? - else plain(" ") :: (renderCaptureArrow(using q)(inCC, skipThisTypePrefix) ++ plain(" ").l) - given Option[List[TypeRepr]] = None + val refs = if !inCC.isDefined && t.isContextFunctionType then + // This'll ensure that an impure context function type is rendered correctly + Some(List(CaptureDefs.captureRoot.termRef)) + else + inCC + val arrow = plain(" ") :: (renderCaptureArrow(using q)(refs, t.isContextFunctionType, skipThisTypePrefix) ++ plain(" ").l) + given Option[List[TypeRepr]] = None // FIXME: this is ugly args match case Nil => Nil case List(rtpe) => plain("()").l ++ arrow ++ inner(rtpe, skipThisTypePrefix) @@ -499,19 +505,21 @@ trait TypesSupport: import reflect._ Keyword("^") :: renderCaptureSet(refs, skipThisTypePrefix) - private def renderCaptureArrow(using q: Quotes)(refs: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)( + private def renderCaptureArrow(using q: Quotes)(refs: List[reflect.TypeRepr], isImplicitFun: Boolean, skipThisTypePrefix: Boolean)( using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol ): SSignature = - import reflect.* + import reflect._ + val prefix = if isImplicitFun then "?" else "" refs match - case Nil => List(Keyword("->")) - case List(ref) if ref.isCaptureRoot => List(Keyword("=>")) - case refs => Keyword("->") :: renderCaptureSet(using q)(refs, skipThisTypePrefix) + case Nil => List(Keyword(prefix + "->")) // FIXME need to check if cc is enabled in current file first!!! + case List(ref) if ref.isCaptureRoot => List(Keyword(prefix + "=>")) + case refs => Keyword(prefix + "->") :: renderCaptureSet(using q)(refs, skipThisTypePrefix) - private def renderCaptureArrow(using q: Quotes)(refs: Option[List[reflect.TypeRepr]], skipThisTypePrefix: Boolean)( + private def renderCaptureArrow(using q: Quotes)(refs: Option[List[reflect.TypeRepr]], isImplicitFun: Boolean, skipThisTypePrefix: Boolean)( using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol ): SSignature = - import reflect.* + import reflect._ + val prefix = if isImplicitFun then "?" else "" refs match - case None => List(Keyword("=>")) // FIXME: is this correct? or should it be `->` by default? - case Some(refs) => renderCaptureArrow(using q)(refs, skipThisTypePrefix) + case None => List(Keyword(prefix + "->")) // FIXME need to check if cc is enabled in current file first!!! + case Some(refs) => renderCaptureArrow(using q)(refs, isImplicitFun, skipThisTypePrefix) From 85af83d4a363e8e6b626dbc0e443d58dd1d74bb7 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Wed, 16 Jul 2025 13:01:34 +0200 Subject: [PATCH 09/20] Scaladoc: Detect if CC was imported --- local/project/dummy/arrows.scala | 1 + local/project/dummy/nocc.scala | 6 +++ scaladoc/src/dotty/tools/scaladoc/api.scala | 1 + .../dotty/tools/scaladoc/cc/CaptureOps.scala | 34 ++++++++++++++++ .../tools/scaladoc/tasty/PackageSupport.scala | 7 ++++ .../tools/scaladoc/tasty/TastyParser.scala | 3 ++ .../tools/scaladoc/tasty/TypesSupport.scala | 40 +++++++++++-------- 7 files changed, 76 insertions(+), 16 deletions(-) create mode 100644 local/project/dummy/nocc.scala diff --git a/local/project/dummy/arrows.scala b/local/project/dummy/arrows.scala index 43b6f48c332f..571919c3c6f2 100644 --- a/local/project/dummy/arrows.scala +++ b/local/project/dummy/arrows.scala @@ -1,6 +1,7 @@ package dummy import language.experimental.captureChecking +import language.experimental.pureFunctions import caps.* trait Nested: diff --git a/local/project/dummy/nocc.scala b/local/project/dummy/nocc.scala new file mode 100644 index 000000000000..9a2ba3c26ebf --- /dev/null +++ b/local/project/dummy/nocc.scala @@ -0,0 +1,6 @@ +package dummy + +trait NoCaptureChecking: + def byName(f: => Int): Int + def impure(f: Int => Int): Int + def context(f: Int ?=> Int): Int diff --git a/scaladoc/src/dotty/tools/scaladoc/api.scala b/scaladoc/src/dotty/tools/scaladoc/api.scala index b39fdf157347..8db95818cb1c 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 Mut extends Modifier("mut", 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) diff --git a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala index 4589f266e485..f782adece91c 100644 --- a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala +++ b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala @@ -36,6 +36,20 @@ object CaptureDefs: 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") + + val ccImportSelector = "captureChecking" end CaptureDefs extension (using qctx: Quotes)(ann: qctx.reflect.Symbol) @@ -53,8 +67,28 @@ end extension extension (using qctx: Quotes)(tpe: qctx.reflect.TypeRepr) def isCaptureRoot: Boolean = tpe.termSymbol == CaptureDefs.captureRoot + + def isImpureFunction1: Boolean = tpe.derivesFrom(CaptureDefs.ImpureFunction1) + + def isImpureContextFunction1: Boolean = tpe.derivesFrom(CaptureDefs.ImpureContextFunction1) + + def isFunction1: Boolean = tpe.derivesFrom(CaptureDefs.Function1) 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._ 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/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 c2dba61b4dd0..e8ff7e08c0c0 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -11,6 +11,8 @@ import dotty.tools.scaladoc.cc.* import NameNormalizer._ import SyntheticsSupport._ +private case class FunKind(isPure: Boolean, isImplicit: Boolean) + trait TypesSupport: self: TastyParser => @@ -122,10 +124,9 @@ trait TypesSupport: ++ keyword(" & ").l ++ inParens(inner(right, skipThisTypePrefix), shouldWrapInParens(right, tp, false)) case ByNameType(CapturingType(tpe, refs)) => - renderCaptureArrow(using q)(refs, false, skipThisTypePrefix) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix)) + renderFunctionArrow(using q)(refs, FunKind(true, false), skipThisTypePrefix) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix)) case ByNameType(tpe) => - tpe.typeSymbol.pos.map(p => report.warning(s"Pure ByNameType at ${p}")) - keyword("-> ") :: inner(tpe, skipThisTypePrefix) // FIXME need to check if cc is enabled in current file first!!! + (if ccEnabled then keyword("-> ") else keyword("=> ")):: inner(tpe, skipThisTypePrefix) case ConstantType(constant) => plain(constant.show).l case ThisType(tpe) => @@ -256,7 +257,8 @@ trait TypesSupport: ++ plain(" ").l ++ inParens(inner(rhs, skipThisTypePrefix), shouldWrapInParens(rhs, t, false)) - case t @ AppliedType(tpe, args) if t.isFunctionType => functionType(t, tpe, args, skipThisTypePrefix) + case t @ AppliedType(tpe, args) if t.isFunctionType => + functionType(t, tpe, args, skipThisTypePrefix) case t @ AppliedType(tpe, typeList) => inner(tpe, skipThisTypePrefix) ++ plain("[").l ++ commas(typeList.map { t => t match @@ -356,7 +358,7 @@ trait TypesSupport: Some(List(CaptureDefs.captureRoot.termRef)) else inCC - val arrow = plain(" ") :: (renderCaptureArrow(using q)(refs, t.isContextFunctionType, skipThisTypePrefix) ++ plain(" ").l) + val arrow = plain(" ") :: (renderFunctionArrow(using q)(refs, FunKind(isPure = t.isFunction1, isImplicit = t.isContextFunctionType), skipThisTypePrefix) ++ plain(" ").l) given Option[List[TypeRepr]] = None // FIXME: this is ugly args match case Nil => Nil @@ -505,21 +507,27 @@ trait TypesSupport: import reflect._ Keyword("^") :: renderCaptureSet(refs, skipThisTypePrefix) - private def renderCaptureArrow(using q: Quotes)(refs: List[reflect.TypeRepr], isImplicitFun: Boolean, skipThisTypePrefix: Boolean)( + private def renderFunctionArrow(using q: Quotes)(refs: List[reflect.TypeRepr], fun: FunKind, skipThisTypePrefix: Boolean)( using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol ): SSignature = import reflect._ - val prefix = if isImplicitFun then "?" else "" - refs match - case Nil => List(Keyword(prefix + "->")) // FIXME need to check if cc is enabled in current file first!!! - case List(ref) if ref.isCaptureRoot => List(Keyword(prefix + "=>")) - case refs => Keyword(prefix + "->") :: renderCaptureSet(using q)(refs, skipThisTypePrefix) + val prefix = if fun.isImplicit then "?" else "" + if !ccEnabled then + List(Keyword(prefix + "=>")) + else + refs match + case Nil => if fun.isPure then List(Keyword(prefix + "->")) else List(Keyword(prefix + "=>")) + case List(ref) if ref.isCaptureRoot => List(Keyword(prefix + "=>")) + case refs => Keyword(prefix + "->") :: renderCaptureSet(using q)(refs, skipThisTypePrefix) - private def renderCaptureArrow(using q: Quotes)(refs: Option[List[reflect.TypeRepr]], isImplicitFun: Boolean, skipThisTypePrefix: Boolean)( + private def renderFunctionArrow(using q: Quotes)(refs: Option[List[reflect.TypeRepr]], fun: FunKind, skipThisTypePrefix: Boolean)( using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol ): SSignature = import reflect._ - val prefix = if isImplicitFun then "?" else "" - refs match - case None => List(Keyword(prefix + "->")) // FIXME need to check if cc is enabled in current file first!!! - case Some(refs) => renderCaptureArrow(using q)(refs, isImplicitFun, skipThisTypePrefix) + val prefix = if fun.isImplicit then "?" else "" + if !ccEnabled then + List(Keyword(prefix + "=>")) + else + refs match + case None => if fun.isPure then List(Keyword(prefix + "->")) else List(Keyword(prefix + "=>")) + case Some(refs) => renderFunctionArrow(using q)(refs, fun, skipThisTypePrefix) From 23164bc70fe0f7e59c8805219a4edf52cf6ad6a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Sun, 6 Jul 2025 17:43:48 +0200 Subject: [PATCH 10/20] Bullet-proof function-arrow rendering --- local/project/dummy/arrows.scala | 12 +++- .../dotty/tools/scaladoc/cc/CaptureOps.scala | 23 ++++++-- .../tools/scaladoc/tasty/TypesSupport.scala | 55 ++++++++++--------- 3 files changed, 57 insertions(+), 33 deletions(-) diff --git a/local/project/dummy/arrows.scala b/local/project/dummy/arrows.scala index 571919c3c6f2..418c3282ea40 100644 --- a/local/project/dummy/arrows.scala +++ b/local/project/dummy/arrows.scala @@ -1,7 +1,6 @@ package dummy import language.experimental.captureChecking -import language.experimental.pureFunctions import caps.* trait Nested: @@ -18,6 +17,15 @@ trait Arrows: 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 @@ -46,7 +54,7 @@ trait Arrows: 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} + 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 diff --git a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala index f782adece91c..c8a91b6e85c2 100644 --- a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala +++ b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala @@ -49,6 +49,9 @@ object CaptureDefs: def Function1(using qctx: Quotes) = qctx.reflect.Symbol.requiredClass("scala.Function1") + def ContextFunction1(using qctx: Quotes) = + qctx.reflect.Symbol.requiredClass("scala.ContextFunction1") + val ccImportSelector = "captureChecking" end CaptureDefs @@ -65,14 +68,26 @@ extension (using qctx: Quotes)(ann: qctx.reflect.Symbol) ann == CaptureDefs.ReachCapabilityAnnot end extension -extension (using qctx: Quotes)(tpe: qctx.reflect.TypeRepr) +extension (using qctx: Quotes)(tpe: qctx.reflect.TypeRepr) // FIXME clean up and have versions on Symbol for those def isCaptureRoot: Boolean = tpe.termSymbol == CaptureDefs.captureRoot - def isImpureFunction1: Boolean = tpe.derivesFrom(CaptureDefs.ImpureFunction1) + // 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 isImpureContextFunction1: Boolean = tpe.derivesFrom(CaptureDefs.ImpureContextFunction1) + def isAnyFunction: Boolean = tpe.typeSymbol.fullName.startsWith("scala.Function") - def isFunction1: Boolean = tpe.derivesFrom(CaptureDefs.Function1) + def isAnyContextFunction: Boolean = tpe.typeSymbol.fullName.startsWith("scala.ContextFunction") end extension /** Matches `import scala.language.experimental.captureChecking` */ diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index e8ff7e08c0c0..84f1547711ef 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -124,9 +124,9 @@ trait TypesSupport: ++ keyword(" & ").l ++ inParens(inner(right, skipThisTypePrefix), shouldWrapInParens(right, tp, false)) case ByNameType(CapturingType(tpe, refs)) => - renderFunctionArrow(using q)(refs, FunKind(true, false), skipThisTypePrefix) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix)) + renderByNameArrow(using q)(Some(refs), skipThisTypePrefix) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix)) case ByNameType(tpe) => - (if ccEnabled then keyword("-> ") else keyword("=> ")):: inner(tpe, skipThisTypePrefix) + renderByNameArrow(using q)(None, skipThisTypePrefix) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix)) case ConstantType(constant) => plain(constant.show).l case ThisType(tpe) => @@ -139,7 +139,7 @@ trait TypesSupport: inner(tpe, skipThisTypePrefix) :+ plain("*") case CapturingType(base, refs) => base match case t @ AppliedType(base, args) if t.isFunctionType => - functionType(t, base, args, skipThisTypePrefix)(using inCC = Some(refs)) + functionType(base, args, skipThisTypePrefix)(using inCC = Some(refs)) case _ => inner(base, skipThisTypePrefix) ++ renderCapturing(refs, skipThisTypePrefix) case AnnotatedType(tpe, _) => inner(tpe, skipThisTypePrefix) @@ -258,7 +258,7 @@ trait TypesSupport: ++ inParens(inner(rhs, skipThisTypePrefix), shouldWrapInParens(rhs, t, false)) case t @ AppliedType(tpe, args) if t.isFunctionType => - functionType(t, tpe, args, skipThisTypePrefix) + functionType(tpe, args, skipThisTypePrefix) case t @ AppliedType(tpe, typeList) => inner(tpe, skipThisTypePrefix) ++ plain("[").l ++ commas(typeList.map { t => t match @@ -346,19 +346,14 @@ trait TypesSupport: s"${tpe.show(using Printer.TypeReprStructure)}" throw MatchError(msg) - private def functionType(using q: Quotes)(t: reflect.TypeRepr, tpe: reflect.TypeRepr, args: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)(using + private def functionType(using q: Quotes)(funTy: reflect.TypeRepr, args: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)(using elideThis: reflect.ClassDef, indent: Int, originalOwner: reflect.Symbol, inCC: Option[List[reflect.TypeRepr]], ): SSignature = import reflect._ - val refs = if !inCC.isDefined && t.isContextFunctionType then - // This'll ensure that an impure context function type is rendered correctly - Some(List(CaptureDefs.captureRoot.termRef)) - else - inCC - val arrow = plain(" ") :: (renderFunctionArrow(using q)(refs, FunKind(isPure = t.isFunction1, isImplicit = t.isContextFunctionType), skipThisTypePrefix) ++ plain(" ").l) + val arrow = plain(" ") :: (renderFunctionArrow(using q)(funTy, inCC, skipThisTypePrefix) ++ plain(" ").l) given Option[List[TypeRepr]] = None // FIXME: this is ugly args match case Nil => Nil @@ -507,27 +502,33 @@ trait TypesSupport: import reflect._ Keyword("^") :: renderCaptureSet(refs, skipThisTypePrefix) - private def renderFunctionArrow(using q: Quotes)(refs: List[reflect.TypeRepr], fun: FunKind, skipThisTypePrefix: Boolean)( + private def renderFunctionArrow(using q: Quotes)(funTy: reflect.TypeRepr, captures: Option[List[reflect.TypeRepr]], skipThisTypePrefix: Boolean)( using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol ): SSignature = import reflect._ - val prefix = if fun.isImplicit then "?" else "" + val isContextFun = funTy.isAnyContextFunction || funTy.isAnyImpureContextFunction + val prefix = if isContextFun then "?" else "" if !ccEnabled then List(Keyword(prefix + "=>")) else - refs match - case Nil => if fun.isPure then List(Keyword(prefix + "->")) else List(Keyword(prefix + "=>")) - case List(ref) if ref.isCaptureRoot => List(Keyword(prefix + "=>")) - case refs => Keyword(prefix + "->") :: renderCaptureSet(using q)(refs, skipThisTypePrefix) - - private def renderFunctionArrow(using q: Quotes)(refs: Option[List[reflect.TypeRepr]], fun: FunKind, skipThisTypePrefix: Boolean)( + 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 render 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 + "->") :: renderCaptureSet(using q)(refs, skipThisTypePrefix) + + private def renderByNameArrow(using q: Quotes)(captures: Option[List[reflect.TypeRepr]], skipThisTypePrefix: Boolean)( using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol ): SSignature = - import reflect._ - val prefix = if fun.isImplicit then "?" else "" - if !ccEnabled then - List(Keyword(prefix + "=>")) - else - refs match - case None => if fun.isPure then List(Keyword(prefix + "->")) else List(Keyword(prefix + "=>")) - case Some(refs) => renderFunctionArrow(using q)(refs, fun, skipThisTypePrefix) + renderFunctionArrow(using q)(CaptureDefs.Function1.typeRef, captures, skipThisTypePrefix) From 80e6d506f2a80ab95b4a2394b59ad455d247f320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Sun, 6 Jul 2025 19:19:15 +0200 Subject: [PATCH 11/20] Enable rendering of `update` methods --- local/project/dummy/sep-pairs.scala | 23 +++++++++++++++++++ scaladoc/src/dotty/tools/scaladoc/api.scala | 2 +- .../scaladoc/tasty/ClassLikeSupport.scala | 7 ++++-- .../dotty/tools/scaladoc/tasty/SymOps.scala | 1 + 4 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 local/project/dummy/sep-pairs.scala diff --git a/local/project/dummy/sep-pairs.scala b/local/project/dummy/sep-pairs.scala new file mode 100644 index 000000000000..344b1ab2bc10 --- /dev/null +++ b/local/project/dummy/sep-pairs.scala @@ -0,0 +1,23 @@ +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(@consume @use p: Pair[Ref^, Ref^]): Pair[Ref^, Ref^] = + val x: Ref^{p.fst*} = p.fst + val y: Ref^{p.snd*} = p.snd + Pair(x, y) + diff --git a/scaladoc/src/dotty/tools/scaladoc/api.scala b/scaladoc/src/dotty/tools/scaladoc/api.scala index 8db95818cb1c..bc40aacf5256 100644 --- a/scaladoc/src/dotty/tools/scaladoc/api.scala +++ b/scaladoc/src/dotty/tools/scaladoc/api.scala @@ -44,7 +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 Mut extends Modifier("mut", 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) diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala index 99aac7010d8b..ffa0f242e602 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala @@ -527,8 +527,11 @@ trait ClassLikeSupport: .filterNot(m => m == Modifier.Lazy || m == Modifier.Final) case _ => symbol.getExtraModifiers() - mkMember(symbol, kind, sig)( - modifiers = modifiers, + mkMember(valDef.symbol, kind, sig)( + // 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/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 } From a8074e340023fe5b18d1bfa3084537a229b2d5a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Sun, 6 Jul 2025 19:37:23 +0200 Subject: [PATCH 12/20] Support rendering of `.rd` capabilities --- local/project/dummy/sep-pairs.scala | 4 ++++ .../dotty/tools/scaladoc/cc/CaptureOps.scala | 23 +++++++++++++++---- .../tools/scaladoc/tasty/TypesSupport.scala | 7 +++--- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/local/project/dummy/sep-pairs.scala b/local/project/dummy/sep-pairs.scala index 344b1ab2bc10..d593ec6391a4 100644 --- a/local/project/dummy/sep-pairs.scala +++ b/local/project/dummy/sep-pairs.scala @@ -21,3 +21,7 @@ def copyPair(@consume @use p: Pair[Ref^, Ref^]): Pair[Ref^, Ref^] = val y: Ref^{p.snd*} = p.snd Pair(x, y) +trait TestRd: + def copyPair(@use 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/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala index c8a91b6e85c2..08bb91233cb4 100644 --- a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala +++ b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala @@ -66,6 +66,9 @@ extension (using qctx: Quotes)(ann: qctx.reflect.Symbol) 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 @@ -113,6 +116,15 @@ object ReachCapability: 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. */ @@ -122,11 +134,12 @@ def decomposeCaptureRefs(using qctx: Quotes)(typ0: qctx.reflect.TypeRepr): Optio def include(t: TypeRepr): Boolean = { buffer += t; true } def traverse(typ: TypeRepr): Boolean = typ match - 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 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 if t.typeSymbol == defn.NothingClass => true // TODO: are atoms only ever the above? Then we could refine the return type case _ => report.warning(s"Unexpected type tree $typ while trying to extract capture references from $typ0"); false // TODO remove warning eventually diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index 84f1547711ef..80b657e3e8f5 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -478,9 +478,10 @@ trait TypesSupport: ): SSignature = import reflect._ ref match - case ReachCapability(c) => renderCapability(c, skipThisTypePrefix) :+ Keyword("*") - case ThisType(_) => List(Keyword("this")) - case t => inner(t, skipThisTypePrefix)(using skipTypeSuffix = true, inCC = Some(Nil)) + case ReachCapability(c) => renderCapability(c, skipThisTypePrefix) :+ Keyword("*") + case ReadOnlyCapability(c) => renderCapability(c, skipThisTypePrefix) :+ Keyword(".rd") + case ThisType(_) => List(Keyword("this")) + case t => inner(t, skipThisTypePrefix)(using skipTypeSuffix = true, inCC = Some(Nil)) private def renderCaptureSet(using Quotes)(refs: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)( using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol From dc786bad10f0c3530e59b86af0ca39cd69198888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Sun, 6 Jul 2025 21:00:43 +0200 Subject: [PATCH 13/20] Support rendering of use and consume annotations --- local/project/dummy/sep-pairs.scala | 2 +- scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala | 2 ++ scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala | 5 ++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/local/project/dummy/sep-pairs.scala b/local/project/dummy/sep-pairs.scala index d593ec6391a4..bf02242e8aa3 100644 --- a/local/project/dummy/sep-pairs.scala +++ b/local/project/dummy/sep-pairs.scala @@ -22,6 +22,6 @@ def copyPair(@consume @use p: Pair[Ref^, Ref^]): Pair[Ref^, Ref^] = Pair(x, y) trait TestRd: - def copyPair(@use p: Pair[Ref^, Ref^]): Pair[Ref^{p.fst*}, Ref^{p.snd*}] + @consume def copyPair(@use 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/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala index 08bb91233cb4..0f3b205907e9 100644 --- a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala +++ b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala @@ -52,6 +52,8 @@ object CaptureDefs: 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 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 => From 20954714dfbe9768ec54374a661d209aa738bde0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Mon, 7 Jul 2025 01:08:13 +0200 Subject: [PATCH 14/20] Fix rendering of dependent function types --- local/project/dummy/arrows.scala | 16 ++++++++++++- local/project/dummy/nocc.scala | 1 + .../tools/scaladoc/tasty/TypesSupport.scala | 24 +++++++++++++------ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/local/project/dummy/arrows.scala b/local/project/dummy/arrows.scala index 418c3282ea40..cfc29c366270 100644 --- a/local/project/dummy/arrows.scala +++ b/local/project/dummy/arrows.scala @@ -59,4 +59,18 @@ trait Arrows: 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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/local/project/dummy/nocc.scala b/local/project/dummy/nocc.scala index 9a2ba3c26ebf..2f34304c7015 100644 --- a/local/project/dummy/nocc.scala +++ b/local/project/dummy/nocc.scala @@ -4,3 +4,4 @@ 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/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index 80b657e3e8f5..fbae078af3c4 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -140,6 +140,8 @@ trait TypesSupport: 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 inCC = Some(refs)) case _ => inner(base, skipThisTypePrefix) ++ renderCapturing(refs, skipThisTypePrefix) case AnnotatedType(tpe, _) => inner(tpe, skipThisTypePrefix) @@ -213,12 +215,19 @@ trait TypesSupport: val isCtx = isContextualMethod(m) if isDependentMethod(m) then val paramList = getParamList(m) - val arrow = keyword(if isCtx then " ?=> " else " => ").l - val resType = inner(m.resType, skipThisTypePrefix) - paramList ++ arrow ++ resType + val arrPrefix = if isCtx then "?" else "" + val arrow = + if ccEnabled then + inCC match + case None | Some(Nil) => keyword(arrPrefix + "->").l + case Some(List(c)) if c.isCaptureRoot => keyword(arrPrefix + "=>").l + case Some(refs) => keyword(arrPrefix + "->") :: renderCaptureSet(refs, skipThisTypePrefix) + else keyword(arrPrefix + "=>").l + val resType = inner(m.resType, skipThisTypePrefix)(using inCC = None) + paramList ++ (plain(" ") :: arrow) ++ (plain(" ") :: resType) else val sym = defn.FunctionClass(m.paramTypes.length, isCtx) - inner(sym.typeRef.appliedTo(m.paramTypes :+ m.resType), skipThisTypePrefix) + inner(sym.typeRef.appliedTo(m.paramTypes :+ m.resType), skipThisTypePrefix)(using inCC = None) case other => noSupported("Dependent function type without MethodType refinement") } @@ -523,13 +532,14 @@ trait TypesSupport: else report.error(s"Cannot render function arrow: expected a (Context)Function* or Impure(Context)Function*, but got: ${funTy.show}") Nil - case Some(refs) => // there is some capture set + 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 + "->") :: renderCaptureSet(using q)(refs, skipThisTypePrefix) - private def renderByNameArrow(using q: Quotes)(captures: Option[List[reflect.TypeRepr]], skipThisTypePrefix: Boolean)( + private def renderByNameArrow(using Quotes)(captures: Option[List[reflect.TypeRepr]], skipThisTypePrefix: Boolean)( using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol ): SSignature = - renderFunctionArrow(using q)(CaptureDefs.Function1.typeRef, captures, skipThisTypePrefix) + renderFunctionArrow(CaptureDefs.Function1.typeRef, captures, skipThisTypePrefix) From 088d043416c7a4c92bf58718b3a672a725efe851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Mon, 7 Jul 2025 10:23:17 +0200 Subject: [PATCH 15/20] Polyfun examples --- local/project/dummy/arrows.scala | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/local/project/dummy/arrows.scala b/local/project/dummy/arrows.scala index cfc29c366270..c367bbc78f13 100644 --- a/local/project/dummy/arrows.scala +++ b/local/project/dummy/arrows.scala @@ -73,4 +73,21 @@ trait Arrows: 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 \ No newline at end of file + 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 From c9a16b150621069317d813ade763de25c8413749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Fri, 11 Jul 2025 12:09:23 +0200 Subject: [PATCH 16/20] Rendering of CapSet types --- local/project/dummy/capturevars.scala | 21 ++++--- .../dotty/tools/scaladoc/cc/CaptureOps.scala | 16 ++++- .../scaladoc/tasty/ClassLikeSupport.scala | 2 + .../tools/scaladoc/tasty/TypesSupport.scala | 58 ++++++++++--------- 4 files changed, 62 insertions(+), 35 deletions(-) diff --git a/local/project/dummy/capturevars.scala b/local/project/dummy/capturevars.scala index 920275411e08..29d62aec42fc 100644 --- a/local/project/dummy/capturevars.scala +++ b/local/project/dummy/capturevars.scala @@ -1,16 +1,23 @@ package dummy import language.experimental.captureChecking +import caps.* trait Test: - type T[-C^] + val a: AnyRef^ + val b: AnyRef^ + type Ordinary + type Ordinary2 >: Int <: String + type T[-C^ >: {a,b}] type U[+C^] type C^ - type D^ - def foo[C^](x: T[C]): Unit - def bar(x: T[{}]): Unit - def baz(x: T[{caps.cap}]): Unit + type D^ >: {C} <: {a,b} + type E^ <: C + type F^ <: {D,E} + 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[{}]): Unit + def bar2(x: U[{a,b,cap}]): Unit def baz2(x: U[{caps.cap}]): Unit - def test[E^, F^ >: {caps.cap} <: {}](x: T[E], y: U[F]): Unit + def test[E^, F^ >: {caps.cap} <: {}](x: T[{E,a,b}], y: U[F]): Unit diff --git a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala index 0f3b205907e9..71602f9ad039 100644 --- a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala +++ b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala @@ -93,6 +93,18 @@ extension (using qctx: Quotes)(tpe: qctx.reflect.TypeRepr) // FIXME clean up and 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 /** Matches `import scala.language.experimental.captureChecking` */ @@ -136,14 +148,14 @@ def decomposeCaptureRefs(using qctx: Quotes)(typ0: qctx.reflect.TypeRepr): Optio 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 if t.typeSymbol == defn.NothingClass => true - // TODO: are atoms only ever the above? Then we could refine the return type + 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 diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala index ffa0f242e602..0ab35c6879f4 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.CaptureDefs + import scala.quoted._ import SymOps._ diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index fbae078af3c4..04007bb7e310 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -62,7 +62,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 + 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 @@ -124,9 +124,9 @@ trait TypesSupport: ++ keyword(" & ").l ++ inParens(inner(right, skipThisTypePrefix), shouldWrapInParens(right, tp, false)) case ByNameType(CapturingType(tpe, refs)) => - renderByNameArrow(using q)(Some(refs), skipThisTypePrefix) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix)) + emitByNameArrow(using q)(Some(refs), skipThisTypePrefix) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix)) case ByNameType(tpe) => - renderByNameArrow(using q)(None, skipThisTypePrefix) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix)) + emitByNameArrow(using q)(None, skipThisTypePrefix) ++ (plain(" ") :: inner(tpe, skipThisTypePrefix)) case ConstantType(constant) => plain(constant.show).l case ThisType(tpe) => @@ -137,12 +137,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 inCC = Some(refs)) - case _ => inner(base, skipThisTypePrefix) ++ renderCapturing(refs, skipThisTypePrefix) + 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 inCC = Some(refs)) + case t if t.isCapSet => emitCaptureSet(refs, skipThisTypePrefix, omitCap = false) + case _ => inner(base, skipThisTypePrefix) ++ emitCapturing(refs, skipThisTypePrefix) case AnnotatedType(tpe, _) => inner(tpe, skipThisTypePrefix) case tl @ TypeLambda(params, paramBounds, AppliedType(tpe, args)) @@ -221,7 +223,7 @@ trait TypesSupport: inCC match case None | Some(Nil) => keyword(arrPrefix + "->").l case Some(List(c)) if c.isCaptureRoot => keyword(arrPrefix + "=>").l - case Some(refs) => keyword(arrPrefix + "->") :: renderCaptureSet(refs, skipThisTypePrefix) + case Some(refs) => keyword(arrPrefix + "->") :: emitCaptureSet(refs, skipThisTypePrefix) else keyword(arrPrefix + "=>").l val resType = inner(m.resType, skipThisTypePrefix)(using inCC = None) paramList ++ (plain(" ") :: arrow) ++ (plain(" ") :: resType) @@ -275,6 +277,8 @@ trait TypesSupport: case _ => topLevelProcess(t, skipThisTypePrefix) }) ++ plain("]").l + case t : TypeRef if t.isCapSet => emitCaptureSet(Nil, skipThisTypePrefix) + case tp @ TypeRef(qual, typeName) => qual match { case r: RecursiveThis => tpe(s"this.$typeName").l @@ -362,7 +366,7 @@ trait TypesSupport: inCC: Option[List[reflect.TypeRepr]], ): SSignature = import reflect._ - val arrow = plain(" ") :: (renderFunctionArrow(using q)(funTy, inCC, skipThisTypePrefix) ++ plain(" ").l) + val arrow = plain(" ") :: (emitFunctionArrow(using q)(funTy, inCC, skipThisTypePrefix) ++ plain(" ").l) given Option[List[TypeRepr]] = None // FIXME: this is ugly args match case Nil => Nil @@ -378,7 +382,10 @@ trait TypesSupport: 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)) @@ -482,37 +489,36 @@ trait TypesSupport: case AnnotatedType(tr, _) => stripAnnotated(tr) case other => other - private def renderCapability(using Quotes)(ref: reflect.TypeRepr, skipThisTypePrefix: Boolean)( + 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) => renderCapability(c, skipThisTypePrefix) :+ Keyword("*") - case ReadOnlyCapability(c) => renderCapability(c, skipThisTypePrefix) :+ Keyword(".rd") + 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 renderCaptureSet(using Quotes)(refs: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)( + private def emitCaptureSet(using Quotes)(refs: List[reflect.TypeRepr], skipThisTypePrefix: Boolean, omitCap: Boolean = true)( using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol ): SSignature = - import dotty.tools.scaladoc.tasty.NameNormalizer._ import reflect._ refs match - case List(ref) if ref.isCaptureRoot => Nil + case List(ref) if omitCap && ref.isCaptureRoot => Nil case refs => - val res0 = refs.map(renderCapability(_, skipThisTypePrefix)) + val res0 = refs.map(emitCapability(_, skipThisTypePrefix)) val res1 = res0 match case Nil => Nil case other => other.reduce((r, e) => r ++ (List(Plain(", ")) ++ e)) Plain("{") :: (res1 ++ List(Plain("}"))) - private def renderCapturing(using Quotes)(refs: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)( + private def emitCapturing(using Quotes)(refs: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)( using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol ): SSignature = import reflect._ - Keyword("^") :: renderCaptureSet(refs, skipThisTypePrefix) + Keyword("^") :: emitCaptureSet(refs, skipThisTypePrefix) - private def renderFunctionArrow(using q: Quotes)(funTy: reflect.TypeRepr, captures: Option[List[reflect.TypeRepr]], skipThisTypePrefix: Boolean)( + 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._ @@ -530,16 +536,16 @@ trait TypesSupport: else if isImpureFun then List(Keyword(prefix + "=>")) else - report.error(s"Cannot render function arrow: expected a (Context)Function* or Impure(Context)Function*, but got: ${funTy.show}") + 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 + "->") :: renderCaptureSet(using q)(refs, skipThisTypePrefix) + case refs => Keyword(prefix + "->") :: emitCaptureSet(refs, skipThisTypePrefix) - private def renderByNameArrow(using Quotes)(captures: Option[List[reflect.TypeRepr]], skipThisTypePrefix: Boolean)( + private def emitByNameArrow(using Quotes)(captures: Option[List[reflect.TypeRepr]], skipThisTypePrefix: Boolean)( using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol ): SSignature = - renderFunctionArrow(CaptureDefs.Function1.typeRef, captures, skipThisTypePrefix) + emitFunctionArrow(CaptureDefs.Function1.typeRef, captures, skipThisTypePrefix) From 07e9bdaa9e0352b7499441e00e25dff1639b2fb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Fri, 11 Jul 2025 15:01:27 +0200 Subject: [PATCH 17/20] Fix rendering of capture members --- local/project/dummy/capturevars.scala | 1 + scaladoc/src/dotty/tools/scaladoc/api.scala | 2 +- .../src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala | 7 ++++++- .../scaladoc/translators/ScalaSignatureProvider.scala | 2 +- .../tools/scaladoc/translators/ScalaSignatureUtils.scala | 4 +++- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/local/project/dummy/capturevars.scala b/local/project/dummy/capturevars.scala index 29d62aec42fc..d86222dfe945 100644 --- a/local/project/dummy/capturevars.scala +++ b/local/project/dummy/capturevars.scala @@ -14,6 +14,7 @@ trait Test: type D^ >: {C} <: {a,b} type E^ <: C type F^ <: {D,E} + type G^ = 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 diff --git a/scaladoc/src/dotty/tools/scaladoc/api.scala b/scaladoc/src/dotty/tools/scaladoc/api.scala index bc40aacf5256..8eeaa84dfecd 100644 --- a/scaladoc/src/dotty/tools/scaladoc/api.scala +++ b/scaladoc/src/dotty/tools/scaladoc/api.scala @@ -70,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 diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala index 0ab35c6879f4..23c7bf173594 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala @@ -495,7 +495,12 @@ trait ClassLikeSupport: case LambdaTypeTree(params, body) => (params.map(mkTypeArgument(_, classDef)), body) case tpe => (Nil, tpe) - val defaultKind = Kind.Type(!isTreeAbstract(typeDef.rhs), symbol.isOpaque, generics).asInstanceOf[Kind.Type] + val isCaptureVar = ccEnabled && typeDef.rhs.match + case t: TypeTree => t.tpe.derivesFrom(CaptureDefs.Caps_CapSet) + case t: TypeBoundsTree => t.tpe.derivesFrom(CaptureDefs.Caps_CapSet) + case _ => false + + val defaultKind = Kind.Type(!isTreeAbstract(typeDef.rhs), symbol.isOpaque, generics, isCaptureVar).asInstanceOf[Kind.Type] val kind = if symbol.flags.is(Flags.Enum) then Kind.EnumCase(defaultKind) else defaultKind 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..a477d6dabbea 100644 --- a/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureUtils.scala +++ b/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureUtils.scala @@ -3,7 +3,9 @@ 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 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]): SignatureBuilder = copy(content = content :+ Type(text, dri)) def keyword(str: String): SignatureBuilder = copy(content = content :+ Keyword(str)) def tpe(text: String, dri: DRI): SignatureBuilder = copy(content = content :+ Type(text, Some(dri))) From 94fd10c3c87c5ecab28884c83dbe0384c7d2a13e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Bra=C4=8Devac?= Date: Fri, 11 Jul 2025 16:06:15 +0200 Subject: [PATCH 18/20] Fix rendering of capture variables in paramlists --- local/project/dummy/capturevars.scala | 5 ++++- scaladoc/src/dotty/tools/scaladoc/api.scala | 3 ++- .../src/dotty/tools/scaladoc/cc/CaptureOps.scala | 9 +++++++++ .../tools/scaladoc/tasty/ClassLikeSupport.scala | 15 ++++++++------- .../dotty/tools/scaladoc/tasty/TypesSupport.scala | 10 +++++++--- .../translators/ScalaSignatureUtils.scala | 6 ++++-- 6 files changed, 34 insertions(+), 14 deletions(-) diff --git a/local/project/dummy/capturevars.scala b/local/project/dummy/capturevars.scala index d86222dfe945..ea351e889dfe 100644 --- a/local/project/dummy/capturevars.scala +++ b/local/project/dummy/capturevars.scala @@ -10,15 +10,18 @@ trait Test: 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} <: {}](x: T[{E,a,b}], y: U[F]): 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 diff --git a/scaladoc/src/dotty/tools/scaladoc/api.scala b/scaladoc/src/dotty/tools/scaladoc/api.scala index 8eeaa84dfecd..41ccd8fb2280 100644 --- a/scaladoc/src/dotty/tools/scaladoc/api.scala +++ b/scaladoc/src/dotty/tools/scaladoc/api.scala @@ -121,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 index 71602f9ad039..9ddba3b028b9 100644 --- a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala +++ b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala @@ -107,6 +107,15 @@ extension (using qctx: Quotes)(tpe: qctx.reflect.TypeRepr) // FIXME clean up and 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 = diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala index 23c7bf173594..1adcfa0e1534 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala @@ -3,7 +3,7 @@ package dotty.tools.scaladoc.tasty import dotty.tools.scaladoc._ import dotty.tools.scaladoc.{Signature => DSignature} -import dotty.tools.scaladoc.cc.CaptureDefs +import dotty.tools.scaladoc.cc.* import scala.quoted._ @@ -466,6 +466,8 @@ trait ClassLikeSupport: else if symbol.flags.is(Flags.Contravariant) then "-" else "" + val isCaptureVar = ccEnabled && argument.derivesFromCapSet + val name = symbol.normalizedName val normalizedName = if name.matches("_\\$\\d*") then "_" else name val boundsSignature = argument.rhs.asSignature(classDef, symbol.owner) @@ -481,7 +483,8 @@ trait ClassLikeSupport: variancePrefix, normalizedName, symbol.dri, - signature + signature, + isCaptureVar, ) def parseTypeDef(typeDef: TypeDef, classDef: ClassDef): Member = @@ -491,15 +494,13 @@ 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) - val isCaptureVar = ccEnabled && typeDef.rhs.match - case t: TypeTree => t.tpe.derivesFrom(CaptureDefs.Caps_CapSet) - case t: TypeBoundsTree => t.tpe.derivesFrom(CaptureDefs.Caps_CapSet) - case _ => false - val defaultKind = Kind.Type(!isTreeAbstract(typeDef.rhs), symbol.isOpaque, generics, isCaptureVar).asInstanceOf[Kind.Type] val kind = if symbol.flags.is(Flags.Enum) then Kind.EnumCase(defaultKind) else defaultKind diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index 04007bb7e310..034975f6e10a 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -156,7 +156,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) @@ -174,8 +175,11 @@ trait TypesSupport: } 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 = diff --git a/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureUtils.scala b/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureUtils.scala index a477d6dabbea..7b3f2fa44acf 100644 --- a/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureUtils.scala +++ b/scaladoc/src/dotty/tools/scaladoc/translators/ScalaSignatureUtils.scala @@ -6,7 +6,9 @@ case class SignatureBuilder(content: Signature = Nil) extends ScalaSignatureUtil 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]): SignatureBuilder = copy(content = content :+ Type(text, dri)) + 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) @@ -92,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 470616b9a5dd324d7c4451fce8e2d483ac42039f Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Wed, 16 Jul 2025 13:44:38 +0200 Subject: [PATCH 19/20] Fix subtle rendering issues with CC in scaladoc The information that we are within a capture context was not propperly passed to recursive calls of `inner`, which caused some types to be rendered incorrectly. This was due to a bad interplay with implicit parameters and default arguments. --- .../tools/scaladoc/tasty/TypesSupport.scala | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index 034975f6e10a..cbd9b3c2c2f8 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -11,8 +11,6 @@ import dotty.tools.scaladoc.cc.* import NameNormalizer._ import SyntheticsSupport._ -private case class FunKind(isPure: Boolean, isImplicit: Boolean) - trait TypesSupport: self: TastyParser => @@ -142,9 +140,9 @@ trait TypesSupport: 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 inCC = Some(refs)) + inner(base, skipThisTypePrefix)(using indent = indent, skipTypeSuffix = skipTypeSuffix, inCC = Some(refs)) case t if t.isCapSet => emitCaptureSet(refs, skipThisTypePrefix, omitCap = false) - case _ => inner(base, skipThisTypePrefix) ++ emitCapturing(refs, skipThisTypePrefix) + case t => inner(base, skipThisTypePrefix) ++ emitCapturing(refs, skipThisTypePrefix) case AnnotatedType(tpe, _) => inner(tpe, skipThisTypePrefix) case tl @ TypeLambda(params, paramBounds, AppliedType(tpe, args)) @@ -169,6 +167,8 @@ 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) @@ -224,16 +224,16 @@ trait TypesSupport: val arrPrefix = if isCtx then "?" else "" val arrow = if ccEnabled then - inCC match + 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)(using inCC = None) + val resType = inner(m.resType, skipThisTypePrefix) paramList ++ (plain(" ") :: arrow) ++ (plain(" ") :: resType) else val sym = defn.FunctionClass(m.paramTypes.length, isCtx) - inner(sym.typeRef.appliedTo(m.paramTypes :+ m.resType), skipThisTypePrefix)(using inCC = None) + inner(sym.typeRef.appliedTo(m.paramTypes :+ m.resType), skipThisTypePrefix) case other => noSupported("Dependent function type without MethodType refinement") } @@ -286,25 +286,27 @@ trait TypesSupport: case tp @ TypeRef(qual, typeName) => qual match { case r: RecursiveThis => tpe(s"this.$typeName").l + case t if skipPrefix(t, elideThis, originalOwner, skipThisTypePrefix) => + 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 skipTypeSuffix = true, inCC = inCC) ++ plain(".").l ++ suffix case ThisType(tr) => - val typeFromSupertypeConstructor = findSupertype(elideThis, tr.typeSymbol) match + findSupertype(elideThis, tr.typeSymbol) match case Some((sym, AppliedType(tr2, args))) => sym.tree.asInstanceOf[ClassDef].constructor.paramss.headOption match case Some(TypeParamClause(tpc)) => tpc.zip(args).collectFirst { case (TypeDef(name, _), arg) if name == typeName => arg - }.map(inner(_, skipThisTypePrefix)) - case _ => None - case _ => None - typeFromSupertypeConstructor.getOrElse: - if skipPrefix(qual, elideThis, originalOwner, skipThisTypePrefix) then - tpe(tp.typeSymbol) - else - val sig = inParens(inner(qual, skipThisTypePrefix)(using skipTypeSuffix = true), shouldWrapInParens(qual, tp, true)) + } match + case Some(tr) => inner(tr, skipThisTypePrefix) + case None => tpe(tp.typeSymbol) + case _ => tpe(tp.typeSymbol) + case Some(_) => tpe(tp.typeSymbol) + case None => + val sig = inParens(inner(qual, skipThisTypePrefix)(using skipTypeSuffix = true, inCC = inCC), shouldWrapInParens(qual, tp, true)) sig ++ plain(".").l ++ tpe(tp.typeSymbol) - case t if skipPrefix(t, elideThis, originalOwner, skipThisTypePrefix) => - 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 skipTypeSuffix = true) ++ plain(".").l ++ suffix @@ -316,7 +318,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 skipTypeSuffix = true) ++ plain(".").l + case tp => inner(tp, skipThisTypePrefix)(using 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) => @@ -335,9 +337,9 @@ trait TypesSupport: val spaces = " " * (indent) val casesTexts = cases.flatMap { case MatchCase(from, to) => - keyword(caseSpaces + "case ").l ++ inner(from, skipThisTypePrefix) ++ keyword(" => ").l ++ inner(to, skipThisTypePrefix)(using indent = indent + 2) ++ plain("\n").l + keyword(caseSpaces + "case ").l ++ inner(from, skipThisTypePrefix) ++ keyword(" => ").l ++ inner(to, skipThisTypePrefix)(using indent = indent + 2, 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) ++ plain("\n").l + keyword(caseSpaces + "case ").l ++ inner(from, skipThisTypePrefix) ++ keyword(" => ").l ++ inner(to, skipThisTypePrefix)(using indent = indent + 2, inCC = inCC) ++ plain("\n").l } inner(sc, skipThisTypePrefix) ++ keyword(" match ").l ++ plain("{\n").l ++ casesTexts ++ plain(spaces + "}").l @@ -371,7 +373,7 @@ trait TypesSupport: ): SSignature = import reflect._ val arrow = plain(" ") :: (emitFunctionArrow(using q)(funTy, inCC, skipThisTypePrefix) ++ plain(" ").l) - given Option[List[TypeRepr]] = None // FIXME: this is ugly + 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) @@ -403,14 +405,14 @@ trait TypesSupport: ) = 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)(using inCC).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) From 6801483bbede94939d4f0bf01bc1e4c3638af28e Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Mon, 14 Jul 2025 18:33:27 +0200 Subject: [PATCH 20/20] Make isCapSetCap reject cap* and cap.rd --- local/project/dummy/capturevars.scala | 3 +++ scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/local/project/dummy/capturevars.scala b/local/project/dummy/capturevars.scala index ea351e889dfe..5cc9e529ada6 100644 --- a/local/project/dummy/capturevars.scala +++ b/local/project/dummy/capturevars.scala @@ -25,3 +25,6 @@ trait Test: 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/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala index 9ddba3b028b9..bd55798d000c 100644 --- a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala +++ b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala @@ -74,7 +74,13 @@ extension (using qctx: Quotes)(ann: qctx.reflect.Symbol) end extension extension (using qctx: Quotes)(tpe: qctx.reflect.TypeRepr) // FIXME clean up and have versions on Symbol for those - def isCaptureRoot: Boolean = tpe.termSymbol == CaptureDefs.captureRoot + 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. @@ -182,4 +188,4 @@ object CapturingType: case AnnotatedType(base, Apply(Select(New(annot), _), Nil)) if annot.symbol == CaptureDefs.retainsCap => Some((base, List(CaptureDefs.captureRoot.termRef))) case _ => None -end CapturingType \ No newline at end of file +end CapturingType