Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 241 additions & 0 deletions scaladoc-testcases/src/tests/captureCheckingSignatures.scala
Original file line number Diff line number Diff line change
@@ -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}
11 changes: 11 additions & 0 deletions scaladoc/src/dotty/tools/scaladoc/cc/CaptureOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
11 changes: 9 additions & 2 deletions scaladoc/src/dotty/tools/scaladoc/tasty/ClassLikeSupport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -493,20 +495,25 @@ 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
Comment on lines +500 to +501
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note: This seems to be the bounds of the members, so this is more like prepending to >: Lower <: Upper to make ^ >: Lower <: Upper


if symbol.flags.is(Flags.Exported)
then {
val origin = Some(tpeTree).flatMap {
case TypeBoundsTree(l: TypeTree, h: TypeTree) if l.tpe == h.tpe =>
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
Expand Down
38 changes: 31 additions & 7 deletions scaladoc/src/dotty/tools/scaladoc/tasty/TypesSupport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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._
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dotty.tools.scaladoc
package signatures

class CaptureCheckingSignatures extends SignatureTest(
"captureCheckingSignatures",
SignatureTest.all
)
Loading