Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Existential Capabilities #20566

Open
wants to merge 48 commits into
base: main
Choose a base branch
from
Open

Existential Capabilities #20566

wants to merge 48 commits into from

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Jun 13, 2024

The design has changed quite a bit relative to #20470. It is written up as a doc comment on object cc.Existentials.

@natsukagami
Copy link
Contributor

natsukagami commented Jun 14, 2024

scalac is crashing on opaque types aliasing a capability type

import language.experimental.captureChecking

trait A extends caps.Capability

object O:
  opaque type B = A
Stack trace

Exception in thread "main" java.lang.AssertionError: assertion failed: RefinedType(TermRef(ThisType(TypeRef(NoPrefix,module class <empty>)),object O),B,AnnotatedType(TypeAlias(LazyRef(TypeRef(ThisType(TypeRef(NoPrefix,module class <empty>)),trait A))),CaptureAnnotation({TermRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),module class caps$)),val cap)},false)))
	at scala.runtime.Scala3RunTime$.assertFailed(Scala3RunTime.scala:8)
	at dotty.tools.dotc.core.Types$RefinedType.<init>(Types.scala:3312)
	at dotty.tools.dotc.core.Types$CachedRefinedType.<init>(Types.scala:3356)

  unhandled exception while running cc on Test.scala

  An unhandled exception was thrown in the compiler.
  Please file a crash report here:
  https://github.com/scala/scala3/issues/new/choose
  For non-enriched exceptions, compile with -Xno-enrich-error-messages.

     while compiling: Test.scala
        during phase: cc
                mode: Mode(ImplicitsEnabled)
     library version: version 2.13.12
    compiler version: version 3.5.1-RC1-bin-SNAPSHOT-git-5454526
            settings: -classpath /home/nki/.cache/coursier/v1/https/repo1.maven.org/maven2/org/scala-lang/scala-library/2.13.12/scala-library-2.13.12.jar:/home/nki/Projects/dotty/library/../out/bootstrap/scala3-library-bootstrapped/scala-3.5.1-RC1-bin-SNAPSHOT-nonbootstrapped/scala3-library_3-3.5.1-RC1-bin-SNAPSHOT.jar -d /
	at dotty.tools.dotc.core.Types$RefinedType$.apply(Types.scala:3365)
	at dotty.tools.dotc.core.Types$RefinedType.derivedRefinedType(Types.scala:3326)
	at dotty.tools.dotc.core.Types$TypeMap.derivedRefinedType(Types.scala:6200)
	at dotty.tools.dotc.core.Types$TypeMap.mapOver(Types.scala:6322)
	at dotty.tools.dotc.cc.Setup$$anon$4.recur(Setup.scala:304)
	at dotty.tools.dotc.cc.Setup$$anon$4.apply(Setup.scala:329)
	at dotty.tools.dotc.core.Types$DeepTypeMap.mapClassInfo(Types.scala:6411)
	at dotty.tools.dotc.core.Types$DeepTypeMap.mapClassInfo(Types.scala:6406)
	at dotty.tools.dotc.core.Types$TypeMap.mapOver(Types.scala:6351)
	at dotty.tools.dotc.cc.Setup$$anon$4.recur(Setup.scala:304)
	at dotty.tools.dotc.cc.Setup$$anon$4.apply(Setup.scala:329)
	at dotty.tools.dotc.cc.Setup.dotty$tools$dotc$cc$Setup$$transformExplicitType(Setup.scala:332)
	at dotty.tools.dotc.cc.Setup.mappedInfo$1(Setup.scala:114)
	at dotty.tools.dotc.cc.Setup.transformSym(Setup.scala:121)
	at dotty.tools.dotc.core.DenotTransformers$SymTransformer.transform(DenotTransformers.scala:72)
	at dotty.tools.dotc.core.DenotTransformers$SymTransformer.transform$(DenotTransformers.scala:67)
	at dotty.tools.dotc.cc.Setup.transform(Setup.scala:53)
	at dotty.tools.dotc.core.Denotations$SingleDenotation.goForward$1(Denotations.scala:833)
	at dotty.tools.dotc.core.Denotations$SingleDenotation.current(Denotations.scala:879)
	at dotty.tools.dotc.core.Types$NamedType.computeDenot(Types.scala:2551)
	at dotty.tools.dotc.core.Types$NamedType.denot(Types.scala:2514)
	at dotty.tools.dotc.ast.Trees$DenotingTree.denot(Trees.scala:258)
	at dotty.tools.dotc.ast.Trees$Tree.symbol(Trees.scala:147)
	at dotty.tools.dotc.ast.tpd$.localOwner(tpd.scala:597)
	at dotty.tools.dotc.ast.tpd$.localCtx(tpd.scala:601)
	at dotty.tools.dotc.ast.Trees$Instance$TreeAccumulator.foldOver(Trees.scala:1753)
	at dotty.tools.dotc.ast.Trees$Instance$TreeTraverser.traverseChildren(Trees.scala:1799)
	at dotty.tools.dotc.cc.CheckCaptures$$anon$2.traverse(CheckCaptures.scala:673)
	at dotty.tools.dotc.ast.Trees$Instance$TreeTraverser.apply(Trees.scala:1798)
	at dotty.tools.dotc.ast.Trees$Instance$TreeTraverser.apply(Trees.scala:1798)
	at dotty.tools.dotc.ast.Trees$Instance$TreeAccumulator.fold$1(Trees.scala:1662)
	at dotty.tools.dotc.ast.Trees$Instance$TreeAccumulator.apply(Trees.scala:1664)
	at dotty.tools.dotc.ast.Trees$Instance$TreeAccumulator.foldOver(Trees.scala:1763)
	at dotty.tools.dotc.ast.Trees$Instance$TreeTraverser.traverseChildren(Trees.scala:1799)
	at dotty.tools.dotc.cc.CheckCaptures$$anon$2.traverse(CheckCaptures.scala:673)
	at dotty.tools.dotc.cc.CheckCaptures$CaptureChecker.checkUnit(CheckCaptures.scala:1251)
	at dotty.tools.dotc.transform.Recheck.run(Recheck.scala:158)
	at dotty.tools.dotc.cc.CheckCaptures.run(CheckCaptures.scala:193)
	at dotty.tools.dotc.core.Phases$Phase.runOn$$anonfun$1(Phases.scala:380)
	at scala.runtime.function.JProcedure1.apply(JProcedure1.java:15)
	at scala.runtime.function.JProcedure1.apply(JProcedure1.java:10)
	at scala.collection.immutable.List.foreach(List.scala:333)
	at dotty.tools.dotc.core.Phases$Phase.runOn(Phases.scala:373)
	at dotty.tools.dotc.transform.Recheck.runOn(Recheck.scala:162)
	at dotty.tools.dotc.Run.runPhases$1$$anonfun$1(Run.scala:343)
	at scala.runtime.function.JProcedure1.apply(JProcedure1.java:15)
	at scala.runtime.function.JProcedure1.apply(JProcedure1.java:10)
	at scala.collection.ArrayOps$.foreach$extension(ArrayOps.scala:1323)
	at dotty.tools.dotc.Run.runPhases$1(Run.scala:336)
	at dotty.tools.dotc.Run.compileUnits$$anonfun$1(Run.scala:384)
	at dotty.tools.dotc.Run.compileUnits$$anonfun$adapted$1(Run.scala:396)
	at dotty.tools.dotc.util.Stats$.maybeMonitored(Stats.scala:69)
	at dotty.tools.dotc.Run.compileUnits(Run.scala:396)
	at dotty.tools.dotc.Run.compileSources(Run.scala:282)
	at dotty.tools.dotc.Run.compile(Run.scala:267)
	at dotty.tools.dotc.Driver.doCompile(Driver.scala:37)
	at dotty.tools.dotc.Driver.process(Driver.scala:201)
	at dotty.tools.dotc.Driver.process(Driver.scala:169)
	at dotty.tools.dotc.Driver.process(Driver.scala:181)
	at dotty.tools.dotc.Driver.main(Driver.scala:211)
	at dotty.tools.dotc.Main.main(Main.scala)

@natsukagami
Copy link
Contributor

Strange compiler error on this code

import language.experimental.captureChecking

trait Suspend:
  type Suspension

  def resume(s: Suspension): Unit

trait Async(val support: Suspend) extends caps.Capability

class CancelSuspension(ac: Async, suspension: ac.support.Suspension):
  ac.support.resume(suspension)

gives

-- [E007] Type Mismatch Error: Test.scala:11:20 --------------------------------
11 |  ac.support.resume(suspension)
   |                    ^^^^^^^^^^
   |Found:    ((CancelSuspension.this.ac : Async^)^{CancelSuspension.this.suspension*})#
   |  support.Suspension
   |Required: CancelSuspension.this.ac.support.Suspension
   |
   | longer explanation available when compiling with `-explain`
1 error found

The error doesn't come up if Async does not extend Capability and CancelSuspension takes Async^ instead.

@odersky odersky changed the title Existential Capabilities Design Draft V3 Existential Capabilities Jun 18, 2024
@natsukagami
Copy link
Contributor

Reach capabilities being widened into cap:

//> using scala 3.5.1-RC1-bin-SNAPSHOT
import language.experimental.captureChecking

class Box[T](items: Seq[T^]):
  def getOne: T^{items*} = ???

object Box:
  def getOne[T](items: Seq[T^]): T^{items*} =
    Box(items).getOne

gives

-- Error: /tmp/tmp.yIHCJZJOce/test.scala:9:15 ----------------------------------
9 |    Box(items).getOne
  |    ^^^^^^^^^^^^^^^^^
  |The expression's type (box T^?)^ is not allowed to capture the root capability `cap`.
  |This usually means that a capability persists longer than its allowed lifetime.
1 error found

@noti0na1 noti0na1 self-requested a review June 20, 2024 14:50
@odersky
Copy link
Contributor Author

odersky commented Jun 21, 2024

@natsukagami Last problem should be fixed now.

@natsukagami
Copy link
Contributor

Similar to the above, simplified from gears

//> using scala 3.5.1-RC1-bin-SNAPSHOT
import language.experimental.captureChecking

trait Future[+T]:
  def await: T

trait Channel[T]:
  def read(): Either[Nothing, T]

class Collector[T](val futures: Seq[Future[T]^]):
  val results: Channel[Future[T]^{futures*}] = ???
end Collector

extension [T](fs: Seq[Future[T]^])
  def awaitAll =
    val collector = Collector(fs)
    // val ch = collector.results // also errors
    val fut: Future[T]^{fs*} = collector.results.read().right.get // found ...^{caps.cap}
Compiling project (Scala 3.5.1-RC1-bin-SNAPSHOT, JVM (21))
[error] ./main.scala:19:32
[error] Found:    Future[box T^?]^{caps.cap?}
[error] Required: Future[T]^{fs*}
[error]     val fut: Future[T]^{fs*} = collector.results.read().right.get // found ...^{caps.cap}
[error]                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

@odersky
Copy link
Contributor Author

odersky commented Jul 5, 2024

@natsukagami I noted that the last example typechecks if the type of collector is given explicitly:

val collector: Collector[T]{val futures: Seq[Future[T]^{fs*}]}
       = Collector(fs)

@Linyxus
Copy link
Contributor

Linyxus commented Jul 5, 2024

An unsound snippet that should not have compiled:

import language.experimental.captureChecking

// Some capabilities that should be used locally
trait Async:
  //  some method
  def read(): Unit
def usingAsync[X](op: Async^ => X): X = ???

case class Box[+T](get: T)

def useBoxedAsync(x: Box[Async^]): Unit = x.get.read()

def test(): Unit =
  val f: Box[Async^] => Unit = useBoxedAsync
  
  def boom(x: Async^): () ->{f} Unit =
    () => f(Box(x))

  val leaked = usingAsync[() ->{f} Unit](boom)

  leaked()  // scope violation

Functions like usingBoxedAsync should not type-check, as x.get.read() is charging {x*} to the closure of usingBoxedAsync, which should be rejected.

@odersky
Copy link
Contributor Author

odersky commented Jul 6, 2024

@Linyxus I need some more explanations why this is unsound. The typeckecked example is here:

  @SourceFile("leak-problem.scala") final module class leak-problem$package()
     extends Object() {
    private[this] type $this = leak-problem$package.type
    private def writeReplace(): AnyRef =
      new scala.runtime.ModuleSerializationProxy(
        classOf[leak-problem$package.type])
    def usingAsync[X](op: Async^ => X): X = ???
    def useBoxedAsync(x: Box[box Async^]): Unit = x.Async.read()
    def test(): Unit =
      {
        val f: Box[box Async^] => Unit =
          {
            def $anonfun(x: Box[box Async^]^?): Unit = useBoxedAsync(x)
            closure($anonfun)
          }
        def boom(x: Async^): () ->{f} Unit =
          {
            def $anonfun(): Unit =
              {
                f.apply(Box.apply[box Async^{x}](x))
              }
            closure($anonfun)
          }
        val leaked: () ->{f} Unit =
          usingAsync[box () ->{f} Unit](
            {
              def $anonfun(x: Async^): () ->{f} Unit = boom(x)
              closure($anonfun)
            }
          )
        leaked.apply()
      }
  }

There's not a single reach capability in that program.

@Linyxus
Copy link
Contributor

Linyxus commented Jul 6, 2024

The reach capability appears in the expression x.get.read(), since x's type is Box[Async^] and the type parameter of Box is covariant, it gets reach-refined to Box[Async^{x*}].

I have a modified version of this snippet which better shows the problem:

import language.experimental.captureChecking

// Some capabilities that should be used locally
trait Async:
  //  some method
  def read(): Unit
def usingAsync[X](op: Async^ => X): X = ???

case class Box[+T](get: T)

def useBoxedAsync(x: Box[Async^]): Unit = 
  val t0 = x
  val t1 = x.get
  t1.read()

def test(): Unit =
  val f: Box[Async^] => Unit = useBoxedAsync
  
  def boom(x: Async^): () ->{f} Unit =
    () => f(Box(x))

  val leaked = usingAsync[() ->{f} Unit](boom)

  leaked()  // scope violation

The tree after cc:

def useBoxedAsync(x: Box[box Async^]): Unit =
  {
    val t0: Box[box Async^{x*}]^? = x
    val t1: Async^{x*} = x.Async
    t1.read()
  }

 - isBoxedCaptured no longer requires the construction of intermediate capture sets.
 - isAlwaysEmpty is also true for solved variables that have no elements
 - Use a uniform criterion when to add them
 - Don't add them for @constructorOnly or @cc.untrackedCaptures arguments

@untrackedCaptures is a new annotation
 - Improve error messages
 - Better propagation of @uncheckedCaptures
 - -un-deprecacte caps.unsafeUnbox and friends.
We go back to the original lifetime restriction that box/unbox cannot
apply to universal capture sets, and drop the later restriction that
type variable instantiations may not deeply capture cap.

The original restriction is proven to be sound and is probably expressive
enough when we add reach capabilities.

This required some changes in tests and also in the standard library.

The original restriction is in place for source <= 3.2 and >= 3.5. Source
3.3 and 3.4 use the alternative restriction on type variable instances.

Some neg tests have not been brought forward to 3.4. They are all in
tests/neg-customargs/captures and start with

//> using options -source 3.4

We need to look at these tests one-by-one and analyze whether the new 3.5 behavior
is correct.
The previous scheme relied on subtle and unstated assumptions between
symbol updates and re-checking. If they were violated some definitions
could not be rechecked at all. The new scheme is more robust. We always
re-check except when the checker implementation returns true for `skipRecheck`.
And that test is based on an explicitly maintained set of completed symbols.
odersky added 14 commits July 8, 2024 16:47
These should be no longer necessary with existentials.
A parameter accessor with a nonempty deep capture set needs to be tracked in refinements
even if it is pure, as long as it might contain captures that can be referenced using
a reach capability.
When we take `{elem} <: B ++ C` where `elem` is not yet included in `B ++ C`,
B is a constant and C is a variable, propagate with `{elem} <: C`.
Likewise if C is a constant and B is a variable.

This tries to minimize the slack between a union and its operands.

Note: Propagation does not happen very often in our test suite so far: Once in pos tests and
15 times in scala2-library-cc.
Don't treat user-defined capabilities deriving from caps.Capability as
maximal. That was a vestige from when we treated capability classes natively.
It caused code that should compile to fail because if `x extends Capability` then
`x` could not be widened to `x*`.

As a consequence we have one missed error in effect-swaps again, which re-establishes
the original (faulty) situation.
We might want to treat it specially since a RefiningVar should ideally be closed for
further additions when the constructor has been analyzed.
Use the syntactic sugar instead of expanding with capsOf
Add an option to avoid the type intersection when we do a select of a parameter
accessor that is mentioned in a class refinement type. It seems to give us a little
bit if performance, but nothing significant. So the option is off by default.
The condition on capturing types did not make sense. In a type T^{} with an empty capture set
`T` can still be a type variable that's instantiated to a type with a capture set. Instead,
T^cs is always pure if T is always pure. For instance `List[T]^{p}` is always pure. That's
important in the context of the standard library, where such a type usually results from
an instantiation of a type variable such as `C[T]^{p}`.
Step1: refactor

The logic was querying the original types of trees, but we want the rechecked types instead.
Step 2: Change the logic. The previous one was unsound. The new logic is a bot too
conservative. I left comments in tests where it could be improved.
Make all operations final methods on Type or CaptureRef
Move extension methods on CaptureRef into CaptureRef itself or into CaptureOps
@natsukagami
Copy link
Contributor

Some weird interaction between caps, opaque types and inlines...

//> using scala 3.6.0-RC1-bin-SNAPSHOT

import language.experimental.captureChecking

trait Async extends caps.Capability:
  def group: Int

object Async:
  inline def current(using async: Async): async.type = async
  opaque type Spawn <: Async = Async
  def blocking[T](f: Spawn ?=> T): T = ???

def main() =
  Async.blocking:
    val a = Async.current.group

gives

-- Error: test.scala:15:25 -----------------------------------------------------
15 |    val a = Async.current.group
   |                         ^
   |The expression's type box ((contextual$1 : Async.Spawn^) & $proxy1.Spawn)^ is not allowed to capture the root capability `cap`.
   |This usually means that a capability persists longer than its allowed lifetime.

Making Spawn not opaque or making current not-inline would make the code compile.

Previously, only asInstanceOf was excluded.
@odersky
Copy link
Contributor Author

odersky commented Jul 10, 2024

@natsukagami Should be fixed by latest commit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants