diff --git a/scaladoc-testcases/src/tests/captureCheckingSignatures.scala b/scaladoc-testcases/src/tests/captureCheckingSignatures.scala new file mode 100644 index 000000000000..1cdf965cef12 --- /dev/null +++ b/scaladoc-testcases/src/tests/captureCheckingSignatures.scala @@ -0,0 +1,241 @@ +package tests.captureCheckingSignatures + +import language.experimental.captureChecking +import language.experimental.separationChecking +import caps.* + +trait Nested: + val c: AnyRef^ + val next: Nested + +trait Arrows: + val a: AnyRef^ + val b: AnyRef^ + val c: AnyRef^ + + val purev: Int -> Int + val purev2: Int ->{} Int //expected: val purev2: Int -> Int + val impurev: Int => Int + val impurev2: Int ->{a,b,c} Int //expected: val impurev2: Int ->{a, b, c} Int + val impurev3: Int ->{a,b,c} Int => Int //expected: val impurev3: Int ->{a, b, c} Int => Int + val impureAny: Int ->{any} Int //expected: val impureAny: Int => Int + val impureAny2: Int ->{any, a, b, c} Int //expected: val impureAny2: Int ->{any, a, b, c} Int + val contextPureV: Int ?-> Int + val contextPureV2: Int ?->{} Int //expected: val contextPureV2: Int ?-> Int + val contextImpureV: Int ?=> Int + val contextImpureV2: Int ?->{a,b,c} Int //expected: val contextImpureV2: Int ?->{a, b, c} Int + val contextImpureV3: Int ?->{a,b,c} Int ?=> Int //expected: val contextImpureV3: Int ?->{a, b, c} Int ?=> Int + val contextImpureAny: Int ?->{any} Int //expected: val contextImpureAny: Int ?=> Int + val contextImpureAny2: Int ?->{any, a, b, c} Int //expected: val contextImpureAny2: Int ?->{any, a, b, c} Int + + def pure(f: Int -> Int): Int + def pure2(f: Int ->{} Int): Int //expected: def pure2(f: Int -> Int): Int + def impure(f: Int => Int): Int + def impure2(f: Int ->{a,b,c} Int): Int //expected: def impure2(f: Int ->{a, b, c} Int): Int + def impure3(f: Int ->{a,b,c} Int => Int): Int //expected: def impure3(f: Int ->{a, b, c} Int => Int): Int + + def consumes(consume a: AnyRef^): Any + def consumes2(consume x: AnyRef^{a}, consume y: AnyRef^{b}): Any + + def byNamePure(f: -> Int): Int + def byNameImpure(f: ->{a,b,c} Int): Int //expected: 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, any} + + def contextPure(f: AnyRef^{a} ?-> Int): Int + def contextImpure(f: AnyRef^{a} ?=> Int): Int + def contextImpure2(f: AnyRef^{a} ?->{b,c} Int): Int //expected: def contextImpure2(f: AnyRef^{a} ?->{b, c} Int): Int + def contextImpure3(f: AnyRef^{a} ?->{b,c} Int => AnyRef^{a} ?=> Int): Int //expected: def contextImpure3(f: AnyRef^{a} ?->{b, c} Int => AnyRef^{a} ?=> Int): Int + + val noParams: () -> () -> Int + val noParams2: () ->{} () ->{} Int //expected: val noParams2: () -> () -> Int + val noParamsImpure: () => () => Int => Unit + + val uncurried: (x: AnyRef^, y: AnyRef^) -> AnyRef^{x,y} => Int ->{x,y} Int //expected: val uncurried: (x: AnyRef^, y: AnyRef^) -> AnyRef^{x, y} => Int ->{x, y} Int + val uncurried2: (x: AnyRef^, y: AnyRef^) -> AnyRef => Int ->{x,y} Int //expected: 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 //expected: 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 //expected: 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 //expected: val contextUncurried2: (x: AnyRef^{a}, y: AnyRef^{b}) ?-> AnyRef ?-> Int ?->{x, y} Int + val contextUncurried3: (x: AnyRef^{a}, y: AnyRef^{b}) ?=> AnyRef //expected: val contextUncurried3: (AnyRef^{a}, AnyRef^{b}) ?=> AnyRef + val contextUncurried4: (x: AnyRef^{a}, y: AnyRef^{b}) ?->{a,b} AnyRef^ ?=> Int ?->{x,y} Int //expected: 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 //expected: def polyPure2[A](f: A -> Int): Int + def polyImpure[A](f: A => Int): Int + def polyImpure2[A](f: A ->{a,b,c} Int): Int //expected: def polyImpure2[A](f: A ->{a, b, c} Int): Int + def polyImpure3[A](f: A ->{a,b,c} Int => Int): Int //expected: 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 //expected: def polyContextPure2[A](f: A ?-> Int): Int + def polyContextImpure[A](f: A ?=> Int): Int + def polyContextImpure2[A](f: A ?->{a,b,c} Int): Int //expected: def polyContextImpure2[A](f: A ?->{a, b, c} Int): Int + def polyContextImpure3[A](f: A ?->{a,b,c} Int => Int): Int //expected: def polyContextImpure3[A](f: A ?->{a, b, c} Int => Int): Int + + val polyPureV: [A] => A -> Int //expected: val polyPureV: [A] => A => Int + val polyPureV2: [A] => Int => A ->{a,b,c} Int //expected: val polyPureV2: [A] => Int => A ->{a, b, c} Int + val polyImpureV: [A] -> A => Int //expected: val polyImpureV: [A] => A => Int + val polyImpureV2: [A] -> A => Int //expected: val polyImpureV2: [A] => A => Int + +trait SelfTypeCaptures[+A]: + self: SelfTypeCaptures[A]^ => + def concat[B >: A](xs: SelfTypeCaptures[B]^): SelfTypeCaptures[B]^{this, xs} + +// {this} as sole capture set on non-pure traits +trait ThisCaptureOnly: + self: ThisCaptureOnly^ => + def asRef: AnyRef^{this} + def withOther(x: AnyRef^): AnyRef^{this, x} + +trait MutableThisCapture extends Mutable: + def asThis: MutableThisCapture^{this} + +// --- Mutation tracking --- + +import caps.{Mutable, Stateful, Separate, SharedCapability, Classifier} + +class Ref[T](init: T) extends Mutable: + private var x: T = init //unexpected + def get: T = x //expected: def get: T + update def set(v: T): Unit = x = v //expected: update def set(v: T): Unit + +class MyStateful extends Stateful: + private var count: Int = 0 //unexpected + def value: Int = count //expected: def value: Int + update def incr(): Unit = count += 1 //expected: update def incr(): Unit + +class MySeparate(consume val inner: Ref[Int]^) extends Separate + +// Read-only captures (.rd) +trait ReadOnlyExamples: + val r: Ref[Int]^ + def readOnly: Ref[Int]^{any.rd} //expected: def readOnly: Ref[Int]^{any.rd} + def readRef(x: Ref[Int]^{any.rd}): Int //expected: def readRef(x: Ref[Int]^{any.rd}): Int + def specificRd: Ref[Int]^{r.rd} + +// Consume on methods (not just params) +trait ConsumeMethodExamples extends Mutable: + consume def sink: Unit + consume def transfer: ConsumeMethodExamples^ + +// --- Classifiers --- + +class MyIO extends SharedCapability +trait Control extends SharedCapability, Classifier + +// .only[Classifier] restricted capabilities +trait ClassifierExamples: + def restricted(f: () ->{any.only[Control]} Unit): Unit //expected: def restricted(f: () ->{any.only[Control]} Unit): Unit + def sharedOnly: AnyRef^{any.only[Control]} //expected: def sharedOnly: AnyRef^{any.only[Control]} + +// --- Capture set variables and capability members --- + +class Box[X^](val value: AnyRef^{X}) + +trait CaptureSetVarExamples: + def capSetVar[X^](x: AnyRef^{X}): AnyRef^{X} + def multiCapSet[X^, Y^](x: AnyRef^{X}, y: AnyRef^{Y}): AnyRef^{X, Y} //expected: def multiCapSet[X^, Y^](x: AnyRef^{X}, y: AnyRef^{Y}): AnyRef^{X, Y} + +// Capability members (type Cap^) +// Note: scaladoc strips the this. prefix from path-dependent capture set references. +trait HasCapMember: + type Cap^ + +trait HasCapUpperBound: + val io: AnyRef^ + val log: AnyRef^ + type Cap^ <: {io, log} + +trait HasCapLowerBound: + val io: AnyRef^ + type Cap^ >: {io} + +trait HasCapBothBounds: + val io: AnyRef^ + val log: AnyRef^ + type Cap^ >: {io} <: {io, log} + +// Capability member used in method signatures +trait Reactor: + type Cap^ + def onEvent(h: Event ->{this.Cap} Unit): Unit //expected: def onEvent(h: Event ->{Cap} Unit): Unit + def getHandler: () ->{this.Cap} Unit //expected: def getHandler: () ->{Cap} Unit + +class Event + +// Capability member with upper bound used in signatures +trait BoundedReactor: + val io: AnyRef^ + val log: AnyRef^ + type Cap^ <: {io, log} + def onEvent(h: Event ->{this.Cap} Unit): Unit //expected: def onEvent(h: Event ->{Cap} Unit): Unit + +// Capture-set parameters with bounds +trait CapSetBoundsExamples: + val a: AnyRef^ + val b: AnyRef^ + def upperBound[X^ <: {a, b}](x: AnyRef^{X}): AnyRef^{X} + def lowerBound[X^ >: {a}](x: AnyRef^{X}): AnyRef^{X} + def bothBounds[X^ >: {a} <: {a, b}](x: AnyRef^{X}): AnyRef^{X} + +// Multiple bounded capture-set params +trait MultiCapSetBounds: + val a: AnyRef^ + val b: AnyRef^ + def multi[X^ <: {a}, Y^ <: {b}](x: AnyRef^{X}, y: AnyRef^{Y}): AnyRef^{X, Y} //expected: def multi[X^ <: {a}, Y^ <: {b}](x: AnyRef^{X}, y: AnyRef^{Y}): AnyRef^{X, Y} + +// Class with bounded capture-set param +class BoundedBox[X^ <: {any}](val value: AnyRef^{X}) //expected: class BoundedBox[X^](val value: AnyRef^{X}) + +// --- Fresh capabilities --- + +import caps.fresh + +trait FreshExamples: + val a: AnyRef^ + val b: AnyRef^ + + // Basic: zero-param pure with fresh result + def mkRef: () -> Ref[Int]^{fresh} + + // Dependent param, fresh in result (not syntactically dependent on param) + val mkPair: (x: AnyRef^) -> AnyRef^{fresh} + + // Impure zero-param with fresh result + val freshImpure: () => Ref[Int]^{fresh} + + // Multi-param dependent with fresh + val freshMultiParam: (x: AnyRef^, y: AnyRef^) -> AnyRef^{fresh} + + // Fresh combined with param reference in capture set + val freshAndParam: (x: AnyRef^) -> AnyRef^{x, fresh} + + // Nested function types with fresh at inner level + val freshNested: (x: AnyRef^) -> () -> AnyRef^{fresh} + + // Context function with fresh + val freshCtx: (x: AnyRef^) ?-> AnyRef^{fresh} + + // Method taking function-with-fresh as parameter + def takesFresh(f: () -> Ref[Int]^{fresh}): Ref[Int]^ + + // Method returning function type with fresh + def returnsFresh: (x: AnyRef^) -> AnyRef^{fresh} + + // Fresh with named capture set in arrow + val freshArrowCapture: (x: AnyRef^) ->{a} AnyRef^{fresh} + + // Fresh in impure (non-dependent) arrow + val freshImpureNonDep: AnyRef^ => AnyRef^{fresh} + + // By-name with fresh (-> T^{cs} normalizes to ->{cs} T for by-name) + def byNameFresh(f: ->{fresh} Ref[Int]): Ref[Int]^ + + // Polymorphic function with fresh + val freshPoly: [A] => (x: A) -> Ref[A]^{fresh} diff --git a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala index 15470c8a3533..fd24ca9d2a34 100644 --- a/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala +++ b/scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala @@ -91,6 +91,17 @@ extension (using qctx: Quotes)(tpe: qctx.reflect.TypeRepr) // FIXME clean up and case TermRef(TermRef(TermRef(TermRef(NoPrefix(), "_root_"), "scala"), "caps"), CaptureDefs.captureRootName) => true case _ => false + // Recognizes `caps.fresh` — the existentially-bound capability for function type + // results (see scoped-capabilities.md). Analogous to `isCaptureRoot` for `caps.cap`. + // Matches all prefix variants the compiler may produce in TASTY. + def isFreshCap: Boolean = + import qctx.reflect.* + tpe match + case TermRef(ThisType(TypeRef(NoPrefix(), "caps")), CaptureDefs.freshCapName) => true + case TermRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "caps"), CaptureDefs.freshCapName) => true + case TermRef(TermRef(TermRef(TermRef(NoPrefix(), "_root_"), "scala"), "caps"), CaptureDefs.freshCapName) => true + case _ => false + // NOTE: There's something horribly broken with Symbols, and we can't rely on tests like .isContextFunctionType either, // so we do these lame string comparisons instead. def isImpureFunction1: Boolean = tpe.typeSymbol.fullName == "scala.ImpureFunction1" diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala index 0d122bfe37e5..4af069350e2e 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala @@ -483,6 +483,8 @@ trait ClassLikeSupport: case _ => false } + // Detect capture-set type members (type Cap^), which are represented as + // type Cap >: CapSet <: CapSet^{...} in the compiler. val isCaptureVar = ccEnabled && typeDef.derivesFromCapSet val (generics, tpeTree) = typeDef.rhs match @@ -493,6 +495,11 @@ trait ClassLikeSupport: val kind = if symbol.flags.is(Flags.Enum) then Kind.EnumCase(defaultKind) else defaultKind + // For capset members, prepend ^ to the signature (the bounds rendering + // already elides the CapSet lower/upper defaults, so we just need the caret). + val sig = tpeTree.asSignature(classDef, symbol.owner) + val sigWithCaret = if isCaptureVar then Plain("^") :: sig else sig + if symbol.flags.is(Flags.Exported) then { val origin = Some(tpeTree).flatMap { @@ -500,13 +507,13 @@ trait ClassLikeSupport: Some(Link(l.tpe.typeSymbol.owner.name, l.tpe.typeSymbol.owner.dri)) case _ => None } - mkMember(symbol, Kind.Exported(kind), tpeTree.asSignature(classDef, symbol.owner))( + mkMember(symbol, Kind.Exported(kind), sigWithCaret)( deprecated = symbol.isDeprecated(), origin = Origin.ExportedFrom(origin), experimental = symbol.isExperimental() ) } - else mkMember(symbol, kind, tpeTree.asSignature(classDef, symbol.owner))(deprecated = symbol.isDeprecated()) + else mkMember(symbol, kind, sigWithCaret)(deprecated = symbol.isDeprecated()) def parseValDef(c: ClassDef, valDef: ValDef): Member = val symbol = valDef.symbol diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala index 912e1ff92e39..478499ee15d0 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala @@ -219,10 +219,28 @@ trait TypesSupport: case other => noSupported(s"Not supported type in refinement $info") } + // Check whether a type contains `fresh` anywhere in its structure. + // This is used to force dependent rendering for function types like + // `(x: AnyRef^) -> AnyRef^{fresh}` where the result is not syntactically + // dependent on params but the `fresh` existential is semantically scoped + // by the function type (see scoped-capabilities.md). We recurse through + // CapturingType (to look past capture annotations) and AppliedType (to find + // fresh inside type arguments, e.g. `() -> AnyRef^{fresh}` stored as + // `Function0[AnyRef^{fresh}]`). + def resultHasFresh(tp: TypeRepr): Boolean = tp match + case CapturingType(parent, refs) => refs.exists(_.isFreshCap) || resultHasFresh(parent) + case AppliedType(_, args) => args.exists(resultHasFresh) + case _ => false + def parseDependentFunctionType(info: TypeRepr): SSignature = info match { case m: MethodType => val isCtx = isContextualMethod(m) - if isDependentMethod(m) then + // Use dependent rendering (preserving named params and precise arrow) when either: + // 1. The method is syntactically dependent (result references a param), or + // 2. CC is enabled and the result contains `fresh`, because `fresh` in a + // function result is existentially bound by the function type, making the + // dependent form semantically significant (see scoped-capabilities.md). + if isDependentMethod(m) || (ccEnabled && resultHasFresh(m.resType)) then val paramList = getParamList(m) val arrPrefix = if isCtx then "?" else "" val arrow = @@ -543,15 +561,21 @@ trait TypesSupport: case other => other.reduce((r, e) => r ++ (List(Plain(", ")) ++ e)) Plain("{") :: (res1 ++ List(Plain("}"))) - // Within the context of `elideThis`, some capabilities can actually be pure. + // Determines whether a capture set reference should be rendered in the current context. + // Some capabilities (like `this` in a pure class) are elided. We need to handle all + // capability wrappers (reach `c*`, read-only `c.rd`, classifier `.only[C]`) by + // recursing into the underlying capability, and always render root capabilities + // (`cap`/`any`) and `fresh`. private def isCapturedInContext(using Quotes)(ref: reflect.TypeRepr)(using elideThis: reflect.ClassDef): Boolean = import reflect._ ref match - case t if t.isCaptureRoot => true - case ReachCapability(c) => isCapturedInContext(c) - case ReadOnlyCapability(c) => isCapturedInContext(c) - case ThisType(tr) => !elideThis.symbol.typeRef.isPureClass(elideThis) /* is the current class pure? */ - case t => !t.isPureClass(elideThis) + case t if t.isCaptureRoot => true + case t if t.isFreshCap => true + case ReachCapability(c) => isCapturedInContext(c) + case ReadOnlyCapability(c) => isCapturedInContext(c) + case OnlyCapability(c, _) => isCapturedInContext(c) + case ThisType(tr) => !elideThis.symbol.typeRef.isPureClass(elideThis) + case t => !t.isPureClass(elideThis) private def emitCapturing(using Quotes)(refs: List[reflect.TypeRepr], skipThisTypePrefix: Boolean)(using elideThis: reflect.ClassDef, originalOwner: reflect.Symbol): SSignature = import reflect._ diff --git a/scaladoc/test/dotty/tools/scaladoc/signatures/CaptureCheckingSignaturesTest.scala b/scaladoc/test/dotty/tools/scaladoc/signatures/CaptureCheckingSignaturesTest.scala new file mode 100644 index 000000000000..fd49b0117428 --- /dev/null +++ b/scaladoc/test/dotty/tools/scaladoc/signatures/CaptureCheckingSignaturesTest.scala @@ -0,0 +1,7 @@ +package dotty.tools.scaladoc +package signatures + +class CaptureCheckingSignatures extends SignatureTest( + "captureCheckingSignatures", + SignatureTest.all +)