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

Merged
merged 61 commits into from
Jul 19, 2024
Merged

Existential Capabilities #20566

merged 61 commits into from
Jul 19, 2024

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.

@odersky odersky force-pushed the cc-ex-2 branch 4 times, most recently from 0e57d54 to 60b0486 Compare June 26, 2024 21:21
@odersky odersky requested a review from bracevac June 29, 2024 18:25
@Linyxus Linyxus self-requested a review July 3, 2024 16:08
@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()
  }

@odersky odersky force-pushed the cc-ex-2 branch 2 times, most recently from d06a5e6 to 8a504aa Compare July 9, 2024 17:59
@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.

@odersky
Copy link
Contributor Author

odersky commented Jul 10, 2024

@natsukagami Should be fixed by latest commit

@natsukagami
Copy link
Contributor

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

import language.experimental.captureChecking

trait Source[+T]

def race[T](@caps.unboxed sources: Seq[Source[T]^]): Source[T]^{sources*} = ???

def raceTwo[T](src1: Source[T]^, src2: Source[T]^): Source[T]^{} = race(Seq(src1, src2))

this compiles and returns a Source that does not capture src1 and src2.

@odersky
Copy link
Contributor Author

odersky commented Jul 11, 2024

@natsukagami Should be fixed by latest commits

@natsukagami
Copy link
Contributor

Seems to not work with spread parameters

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

import language.experimental.captureChecking

trait Source[+T]

def race[T](@caps.unbox sources: (Source[T]^)*): Source[T]^{sources*} = ???

def raceTwo[T](src1: Source[T]^, src2: Source[T]^): Source[T]^{} =
  // race(Seq(src1, src2)*) // this fails 
  race(src1, src2) // this compiles

odersky added 17 commits July 17, 2024 17:58
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
Previously, only asInstanceOf was excluded.
This reverts commit f1f5a05.

# Conflicts:
#	compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Step 2: Change the logic. The previous one was unsound. The new logic is makes use of the
distinction between regular and unboxed parameters.
Fix the capture set computation of a type T @reachCapability where T
is not a singleton captureRef. Such types can be the results of typemaps.
The capture set in this case should be the deep capture set of T.
Drop the config option that enables it.
Also, fix Seq rechecking so that elements are always box adapted
Capability references in inlined code might end up not being tracked or being redundant.
Don't flag this as an error.
Give some explanation if an empty capture set was the result of an under-approximation.
@odersky
Copy link
Contributor Author

odersky commented Jul 18, 2024

@bracevac

-- [E007] Type Mismatch Error: local/dsl.scala:14:12 ---------------------------
13 |        prog[Int => Int]:
14 |            lam(x => x)
   |        ^
   |        Found:    DSL.this.Now[box Int => Int]
   |        Required: DSL.this.Now[Int => Int]
   |
   |        Note that the expected type DSL.this.Now[Int => Int]
   |        is the previously inferred result type of method test
   |        which is also the type seen in separately compiled sources.
   |        The new inferred type DSL.this.Now[box Int => Int]
   |        must conform to this type.

Here it simply requires that you need to give an explicit type to test. It could be Any or it could be Now[Int => Int]. Both work.

Perform bounds checking before checking self types. Checking self types interpolates them,
which may give an upper approximation solution that failes subsequent bounds checks. On the
other hand, well-formedness checkimng should come after self types checking since otherwise
we get spurious "has empty capture set, cannot be tracked" messages.
@odersky
Copy link
Contributor Author

odersky commented Jul 18, 2024

@natsukagami The latest example you have is not supposed to compile. What I could do is add some way to give a hint why it failed:

15 |  col.add(Future(() => 25)) // error
   |          ^^^^^^^^^^^^^^^^
   |Found:    Future[Int]{val a: (async : Async^)}^{async}
   |Required: Future[Int]^{}
   |
   |Note that a capability Collector.this.futs* in a capture set appearing in contravariant position
   |was mapped to col.futs* which is not a capability. Therefore, it was under-approximated to the empty set.

@odersky
Copy link
Contributor Author

odersky commented Jul 18, 2024

@bracevac Your latest problem should be fixed with last commit.

Simply importing language.experimental.captureChecking appears to break conformance checks

The conformance check was done by the capture checker. And that is run only if the language import is present. So that explains it. But it was still a bug since no error should have been issued.

@odersky
Copy link
Contributor Author

odersky commented Jul 18, 2024

@natsukagami For the latest example, maybe you could try explicit capture set polymorphism?

@odersky
Copy link
Contributor Author

odersky commented Jul 19, 2024

@bracevac I think it would be good to get this merged soon. We are getter further and further away from the main branch, in a good way.

Copy link
Contributor

@bracevac bracevac left a comment

Choose a reason for hiding this comment

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

Thanks, lgtm.

@bracevac bracevac merged commit af933c4 into scala:main Jul 19, 2024
25 checks passed
@bracevac bracevac deleted the cc-ex-2 branch July 19, 2024 23:21
@WojciechMazur WojciechMazur added this to the 3.6.0 milestone Oct 8, 2024
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.

5 participants