scala3 icon indicating copy to clipboard operation
scala3 copied to clipboard

Existential Capabilities

Open odersky opened this issue 1 year ago • 4 comments
trafficstars

The design has changed quite a bit relative to https://github.com/scala/scala3/pull/20470. It is written up as a doc comment on object cc.Existentials.

odersky avatar Jun 13 '24 14:06 odersky

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 avatar Jun 14 '24 12:06 natsukagami

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.

natsukagami avatar Jun 18 '24 10:06 natsukagami

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

natsukagami avatar Jun 20 '24 13:06 natsukagami

@natsukagami Last problem should be fixed now.

odersky avatar Jun 21 '24 18:06 odersky

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]                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

natsukagami avatar Jul 04 '24 14:07 natsukagami

@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)

odersky avatar Jul 05 '24 17:07 odersky

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.

Linyxus avatar Jul 05 '24 18:07 Linyxus

@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.

odersky avatar Jul 06 '24 18:07 odersky

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()
  }

Linyxus avatar Jul 06 '24 21:07 Linyxus

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.

natsukagami avatar Jul 10 '24 14:07 natsukagami

@natsukagami Should be fixed by latest commit

odersky avatar Jul 10 '24 16:07 odersky

//> 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.

natsukagami avatar Jul 11 '24 09:07 natsukagami

@natsukagami Should be fixed by latest commits

odersky avatar Jul 11 '24 15:07 odersky

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

natsukagami avatar Jul 12 '24 11:07 natsukagami

Inline functions capturing a parameter on the result will fail if passed a pure argument:

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

import language.experimental.captureChecking

trait Listener[+T]

inline def consume[T](f: T => Unit): Listener[T]^{f} = ???

val consumePure = consume(_ => ())

errors with

-- Error: test.scala:7:50 ------------------------------------------------------
7 |inline def consume[T](f: T => Unit): Listener[T]^{f} = ???
  |                                                  ^
  |(f$proxy1 : (x$0: Any) ->? Unit) cannot be tracked since its capture set is empty

natsukagami avatar Jul 12 '24 12:07 natsukagami

@natsukagami Latest problems should be fixed now. Let's see what you come up with next!

odersky avatar Jul 14 '24 11:07 odersky

Not sure if this is a bug, but how should programmers deal with the following:

import language.experimental.captureChecking // compiles just fine *without* this import

trait DSL:
    type Now[+A]

    def lam[A,B](fun: Now[A] => Now[B]): Now[A => B]
    def app[A,B](fun: Now[A => B], arg: Now[A]): Now[B]

    def prog[A](e: Now[A]): Now[A]

    def test =
        prog[Int => Int]:
            lam(x => x)

With capture checking enabled, the meaning of => is changed to be a tracked type and we get the following error:

-- [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.

The problem is the explicit type annotation Int => Int, and there is no way to express in the surface syntax (that I'm aware of) we want prog[box Int => Int]. A rather drastic way to get it to compile under capture checking is making all the arrows in the file pure ->.

Edit: Simplifying test to def test: Now[Int => Int] = lam(x => x) compiles properly. So perhaps type applications such as prog[Int => Int] aren't handled correctly?

Edit2: If we ascribe Any to test, the example compiles again with capture checking:

def test : Any =
        prog[Int => Int]:
            lam(x => x)

bracevac avatar Jul 16 '24 09:07 bracevac

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

import language.experimental.captureChecking

trait Dsl:

    sealed trait Nat
    case object Zero extends Nat
    case class Succ[N <: Nat](n: N) extends Nat

    type Stable[+l <: Nat, +b <: Nat, +A]
    type Now[+l <: Nat, +b <: Nat, +A]
    type Box[+A]
    def stable[l <: Nat, b <: Nat, A](e: Stable[l, b, A]): Now[l, b, Box[A]]

    def program[A](prog: Now[Zero.type, Zero.type, A]): Now[Zero.type, Zero.type, A]

    //val conforms: Zero.type <:< Nat = summon 
    // ^ need to uncomment this line to compile with captureChecking enabled

    def test =
        program:
            val v  : Stable[Zero.type, Zero.type, Int] = ???
            stable(v)
//          ^
// Type argument Dsl.this.Zero.type does not conform to upper bound Dsl.this.Nat

bracevac avatar Jul 16 '24 11:07 bracevac

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

import language.experimental.captureChecking

trait Async extends caps.Capability

class Future[+T](x: () => T)(using val a: Async)

class Collector[T](val futs: Seq[Future[T]^]):
  def add(fut: Future[T]^{futs*}) = ???

def main() =
  given async: Async = ???
  val futs = (1 to 20).map(x => Future(() => x))
  val col: Collector[Int] { val futs: Seq[Future[Int]^{async}] } = Collector(futs)

  col.add(Future(() => 25))

gives

-- [E007] Type Mismatch Error: Test.scala:15:10 --------------------------------
15 |  col.add(Future(() => 25))
   |          ^^^^^^^^^^^^^^^^
   |          Found:    Future[Int]{val a: (async : Async^)}^{async}
   |          Required: Future[Int]
   |
   | longer explanation available when compiling with `-explain`

It doesn't work even with the explicit annotation on Collector.

natsukagami avatar Jul 16 '24 14:07 natsukagami

@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.

odersky avatar Jul 18 '24 11:07 odersky

@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 avatar Jul 18 '24 12:07 odersky

@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 avatar Jul 18 '24 12:07 odersky

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

odersky avatar Jul 18 '24 20:07 odersky

@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.

odersky avatar Jul 19 '24 10:07 odersky