scala/scala3

Drop `@experimental` for `scala.caps.Capability`

Closed this issue · 4 comments

scala.annotation.retains and retainsCap are internally used by the capture checker as the representation of capturing types. While capture checking itself is still evolving, the annotations should be considered stable.
Generally, the annotations does not show up in programs that do not enable experimental.captureChecking, even when interacting with modules that do. However, inlining will surface these annotations. While they do not affect the type checker, they cause the experimental check to fail.

An example of this is the scala.util.boundary package, which would greatly benefit from having its Label tracked and capture-checked.
However, boundary.apply requires its body to be inlined into the caller (so that locally-scoped boundary.break calls turn into efficient jumps), which leaks the tracked label with the retainsCap annotation into the caller.

  /** Run `body` with freshly generated label as implicit argument. Catch any
   *  breaks associated with that label and return their results instead of
   *  `body`'s result.
   */
  inline def apply[T](inline body: Label[T]^ ?=> T): T =
    val local: Label[T]^ = Label[T]() // Label[T] @retainsCap 
    try body(using local)
    catch case ex: Break[T] @unchecked =>
      if ex.label eq local.id then ex.value
      else throw ex

This would prevent code not enabling capture-checking (which includes the Scala compiler itself) from using boundary.

@natsukagami is this a case of needs-minor-release?
I get the feeling it may be too late to do this in 3.7

@Gedochao yes if we do this (not sure if we need to yet)

So per talk with @odersky I tried making Label[T] extends caps.Capability instead, which makes boundary works without leaking retainsCap.

Earlier I thought it wasn't working due to this code passing cc:

val leak = boundary(l ?=> l)

but it seems to just be something to do with inference / Any rather than inlining.

We discussed this in the core meeting and arrived at the following scheme to support non-experimental Capability only.

  • Make scala.caps a package instead of an object.
  • Make Capability a non-experimental trait in caps:
     trait Capability extends Any
  • Make other classes and values visible from source experimental classes and objects in caps.
    @experimental object cap extends Capability
    
    @experimental trait Mutable extends Capability
    
    @experimental trait SharedCapability extends Capability
    
    @experimental trait CapSet extends Any
    
    @experimental sealed trait Contains[+C >: CapSet <: CapSet @retainsCap, R <: Singleton]
    
    @experimental final class untrackedCaptures extends annotation.StaticAnnotation
    
    @experimental final class use extends annotation.StaticAnnotation
    
    @experimental final class consume extends annotation.StaticAnnotation
    
    @experimentlal object unsafe:
       extension [T](x: T) def unsafeAssumePure: T = x
       def unsafeAssumeSeparate(op: Any): op.type = op

The rest of the former caps object just supports the current implementation. Move it into a new experimental object scala.caps.internals.

So, not counting experimentals, we committed to a package scala.caps and a trait Capability in it. We should add wording what the trait is used for independently of capture checking. Something like the following

Base trait for classes that represent capabilities in the object-capability model. A capability is a value representing a permission, access right, resource, or effect. Capabilities are typically passed to code as parameters; they should not be global objects. Often, they come with access restrictions such as scoped lifetimes or limited sharing.

An example is the Label class in scala.util.boundary. It represents a capability in the sense that it gives permission to break to the enclosing boundary represented by the Label. It has a scoped lifetime since breaking to a Label after the associated boundary was exited gives a runtime exception.

Capability has a formal meaning when capture checking is turned on using the language import

    import language.experimental.captureChecking

But even without capture checking, extending trait Capability can be useful for documenting the intended purpose of a class.