-
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.
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
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 |
The design has changed quite a bit relative to #20470. It is written up as a doc comment on object cc.Existentials.