-
Notifications
You must be signed in to change notification settings - Fork 1k
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
base: main
Are you sure you want to change the base?
Existential Capabilities #20566
Conversation
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
|
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 |
Reach capabilities being widened into //> 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 Last problem should be fixed now. |
0e57d54
to
60b0486
Compare
Similar to the above, simplified from //> 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}
|
@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) |
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 |
@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. |
The reach capability appears in the expression 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.
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
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
Making |
Previously, only asInstanceOf was excluded.
@natsukagami Should be fixed by latest commit |
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.
//> 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 |
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.
@natsukagami Should be fixed by latest commits |
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 |
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
|
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.
@natsukagami Latest problems should be fixed now. Let's see what you come up with next! |
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
The problem is the explicit type annotation Edit: Simplifying Edit2: If we ascribe def test : Any =
prog[Int => Int]:
lam(x => x) |
Simply importing 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 |
The design has changed quite a bit relative to #20470. It is written up as a doc comment on object cc.Existentials.