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

Exclusive capabilities #22218

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

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Dec 16, 2024

A draft PR to implement Capybara-like mutation checking

@Linyxus
Copy link
Contributor

Linyxus commented Dec 16, 2024

scapybara

@odersky odersky force-pushed the exclusive-capabilities branch 2 times, most recently from 8cbc022 to 0a73a26 Compare December 21, 2024 14:01
@odersky
Copy link
Contributor Author

odersky commented Dec 21, 2024

The earliest European representation of a capybara from "A Relation of a voyage" (1698)

image

@odersky odersky force-pushed the exclusive-capabilities branch from 0da66e2 to eae9c07 Compare December 31, 2024 17:29
@odersky odersky force-pushed the exclusive-capabilities branch 2 times, most recently from 498f43d to af23b3f Compare January 15, 2025 12:04
@odersky
Copy link
Contributor Author

odersky commented Jan 15, 2025

I squashed commits and arranged them in a more logical order. Ready for review now.

@odersky odersky marked this pull request as ready for review January 15, 2025 12:06
@Linyxus Linyxus self-requested a review January 16, 2025 07:52
@Linyxus
Copy link
Contributor

Linyxus commented Jan 16, 2025

A soundness hole:

import caps.cap
import language.future
import language.experimental.captureChecking

def par(op1: () => Unit)(op2: () => Unit): Unit = ()

def bad(io: Object^): () => Unit =
  val x: () => Unit = () => println(io)
  x

def test(io: Object^): Unit =
  par(() => println(io))(() => println(io)) // error // (1)

  val f = bad(io)
  par(f)(() => println(io))  // no error, but it is equivalent to (1) and should fail

The problem is with bad: it seems to "mutably borrow" the parameter io but in the body it widens {io} to {cap} (which should only be contextual) and returns it.

To properly handle these things I think it is necessary to support consume (or dropping). So maybe it can be addressed by a future PR.

@bracevac bracevac self-requested a review January 16, 2025 11:53
@odersky
Copy link
Contributor Author

odersky commented Jan 16, 2025

@Linyxus This is still a work in progress. Separation checking for parameters and returns is not yet implemented. But we'll get back to the test case once it lands.

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.

LGTM! Just left some minor comments


/** If true, turn on separation checking */
def useFresh(using Context): Boolean =
Copy link
Contributor

Choose a reason for hiding this comment

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

Let it have a more descriptive name? E.g. useSeparationChecking


/** A trait for references in CaptureSets. These can be NamedTypes, ThisTypes or ParamRefs,
* as well as two kinds of AnnotatedTypes representing reach and maybe capabilities.
* as well as three kinds of AnnotatedTypes representing readOnly, reach, and maybe capabilities.
* If there are several annotations they come with an orderL
Copy link
Contributor

Choose a reason for hiding this comment

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

Is orderL a typo?

@@ -976,8 +1051,7 @@ object CaptureSet:
def getElems(v: Var): Option[Refs] = elemsMap.get(v)

/** Record elements, return whether this was allowed.
* By default, recording is allowed but the special state FrozenState
* overrides this.
* By default, recording is allowed in regular both not in frozen states.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* By default, recording is allowed in regular both not in frozen states.
* By default, recording is allowed in regular but not in frozen states.

@@ -988,58 +1062,104 @@ object CaptureSet:
def getDeps(v: Var): Option[Deps] = depsMap.get(v)

/** Record dependent sets, return whether this was allowed.
* By default, recording is allowed but the special state FrozenState
* overrides this.
* By default, recording is allowed in regular both not in frozen states.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* By default, recording is allowed in regular both not in frozen states.
* By default, recording is allowed in regular but not in frozen states.

if seen.add(ref) then
try pred finally seen -= ref
else false

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
end VarState

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I leave out the end VarState if the next line is object VarState. That way, we need only one end. IN a sense object VarState is that static part of class VarState.

* The unboxed condition ensures that the expected is not a type variable
* that's upper bounded by a read-only type. In this case it would not be sound
* to narrow to the read-only set, since that set can be propagated
* by the type variable instantiatiin.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* by the type variable instantiatiin.
* by the type variable instantiation.


var reach = false

private def initHidden =
Copy link
Contributor

Choose a reason for hiding this comment

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

This appears to be a duplicate of ownerToHidden above. Consolidate?

private def transformTT(tree: TypeTree, boxed: Boolean)(using Context): Unit =
private val paramSigChange = util.EqHashSet[Tree]()

/** Transform type of tree, and remember the transformed type as the type the tree
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/** Transform type of tree, and remember the transformed type as the type the tree
/** Transform type of tree, and remember the transformed type as the type of the tree

package internal

/** An annotation used internally for fresh capability wrappers of `cap`
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

Show example just as in readOnlyCapability.scala

@@ -41,6 +43,12 @@ import annotation.{experimental, compileTimeOnly, retainsCap}
*/
extension (x: Any) def reachCapability: Any = x

/** Unique capabilities x! which appear as terms in @retains annotations are encoded
* as `caps.uniqueCapability(x)`. When converted to CaptureRef types in capture sets
* they are represented as `x.type @annotation.internal.uniqueCapability`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you mean freshCapability? I don't see uniqueCapability anywhere.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, the extension this comment is attached is named readOnlyCapability. Is there something missing perhaps?

When printing a type `C^` where `C` extends `Capability`, don't show the `^`.
This is overridden under -Yprint-debug.
 - Add Mutable trait and mut modifier.
 - Add dedicated tests `isMutableVar` and `isMutableVarOrAccessor`
   so that update methods can share the same flag `Mutable` with mutable vars.
 - Disallow update methods overriding normal methods
 - Disallow update methods which are not members of classes extending Mutable
 - Add design document from papers repo to docs/internals
 - Add readOnly capabilities
 - Implement raeadOnly access
 - Check that update methods are only called on references with exclusive capture sets.
 - Use cap.rd as default capture set of Capability subtypes
 - Make Mutable a Capability, this means Mutable class references get {cap.rd} as
   default capture set.
 - Use {cap} as captu
….toCap

If existentials are mapped to fresh, it matters where they are opened. Pure or not
arguments don't have anything to do with that.
These are represented as Fresh.Cap(hidden) where hidden is the set of
capabilities subsumed by a fresh. The underlying representation is as
an annotated type `T @annotation.internal.freshCapability`.

Require -source `3.7` for caps to be converted to Fresh.Cap

Also:

 - Refacture and document CaputureSet
 - Make SimpleIdentitySets showable
 - Refactor VarState
    - Drop Frozen enum
    - Make VarState subclasses inner classes of companion object
    - Rename them
    - Give implicit parameter VarState of subCapture method a default value
 - Fix printing of capturesets containing cap and some other capability
 - Revise handing of @uncheckedAnnotation
Check separation from source 3.7 on.

We currently only check applications, other areas of separation checking
are still to be implemented.
Check that a capability that gets hidden in the (result-)type of some
definition is not used afterwards in the same or a nested scope.
When checking whether two items overlap we should always check their deep capture sets.
Buried aliases should count as well.
This is necessary since capability sets are IdentitySets.
3.7 is already on us, so bump by one.

Two new tests for separation checking. One i15749a required a change in SepCheck:
We need to be able to declare reach capabilities in dependencies. The reach is
imply dropped. But we can't sometimes use a normal capability since its capture
set might be empty.
@odersky odersky force-pushed the exclusive-capabilities branch from 79f8ef1 to 0b9acb3 Compare January 21, 2025 21:19
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.

3 participants