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

Merged
merged 23 commits into from
Feb 5, 2025
Merged

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Dec 16, 2024

implement capybara-like mutation checking

  • Add read-only capabilities x.rd
  • Add Mutable and SharedCapability as subclasses of Capability.
  • Add update methods.
  • Implement access rules for Mutable types - exclusive if they can invoke an update method, read-only otherwise.
  • Add Fresh.Cap as a family of new top-types that keep track of references that were widened to them
  • Add a separation checker that checks that references hidden by a Fresh.Cap don't appear as aliases
    • In applications
    • In sequences of statements and definitions
    • in the same type
  • Introduce @consume annotation for parameters
  • Check that hidden references that are parameters are annotated with @consume
  • Check that parameter references passed in arguments to @consume parameters are also annotated with @consume.

To be done in a separate PR: Add qualifiers to capture sets so that we can talk of the read-only part of a Mutable type.

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

* 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

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

@odersky odersky force-pushed the exclusive-capabilities branch 2 times, most recently from 718e03d to ab4d96a Compare January 24, 2025 12:46
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.
TODO:

 - check that only @consume parameters flow to @consume parameters
@odersky odersky force-pushed the exclusive-capabilities branch from 5ed779c to 65a1301 Compare January 29, 2025 22:31
@odersky
Copy link
Contributor Author

odersky commented Jan 31, 2025

@bracevac Ready for review. I still want to document Fresh.scala and SepChecks.scala better, but otherwise all my changes are done for now.

@odersky odersky force-pushed the exclusive-capabilities branch from f3c59cc to 813c585 Compare February 1, 2025 19:26
case _ =>
ReadOnlyCapability(tp)

/** If `x` is a capture ref, replacxe all no-flip covariant occurrences 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.

Suggested change
/** If `x` is a capture ref, replacxe all no-flip covariant occurrences of `cap`
/** If `x` is a capture ref, replace all no-flip covariant occurrences of `cap`

protected def unwrappable(using Context) = Set()

/** An extractor for `ref @readOnlyCapability`, which is used to express
* the rad-only capability `ref.rd` as a type.
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
* the rad-only capability `ref.rd` as a type.
* the read-only capability `ref.rd` as a type.

case _ => false

/** An exclusive capability is a capability that derives
* indirectly from a maximal capability without goinh through
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
* indirectly from a maximal capability without goinh through
* indirectly from a maximal capability without going through

*/
final def isExclusive(using Context): Boolean =
!isReadOnly && (isMaxCapability || captureSetOfInfo.isExclusive)

// With the support of pathes, we don't need to normalize the `TermRef`s anymore.
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
// With the support of pathes, we don't need to normalize the `TermRef`s anymore.
// With the support of paths, we don't need to normalize the `TermRef`s anymore.

@@ -180,6 +242,53 @@ trait CaptureRef extends TypeProxy, ValueType:
case _ => false
end subsumes

/** This is a maximal capabaility that subsumes `y` in given context and VarState.
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
/** This is a maximal capabaility that subsumes `y` in given context and VarState.
/** This is a maximal capability that subsumes `y` in given context and VarState.

@@ -364,7 +407,7 @@ object CaptureSet:
/** If set to `true`, capture stack traces that tell us where sets are created */
private final val debugSets = false

private val emptySet = SimpleIdentitySet.empty
val emptySet = SimpleIdentitySet.empty
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be misleading/confused compared to val empty below. Does this really have to be in object CaptureSet?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's convenient to have it in SepCheck also. I renamed to emptyRefs and pinned the type to Refs.

extends Var(initialElems = initialHidden):

/** Apply function `f` to `elems` while setting `elems` to empty for the
* duration. This is used to escape infinite recursions if two Frash.Caps
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
* duration. This is used to escape infinite recursions if two Frash.Caps
* duration. This is used to escape infinite recursions if two Fresh.Caps

/** The current VarState, as passed by the implicit context */
def varState(using state: VarState): VarState = state

/** Maps `x` to `x?` */
private class MaybeMap(using Context) extends BiTypeMap:
/** A template for maps on capabilities where f(c) <: c and f(f(c)) = c */
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this not be an IdempotentCaptRefMap as well then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's always one or the other. BiTypeMaps are better for type inference.

@@ -573,6 +621,15 @@ class CheckCaptures extends Recheck, SymTransformer:
}
case _ => denot

// Don't allow update methods to be called unless the qualifier captures
// contain an exclusive referenece. TODO This should probabkly rolled into
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
// contain an exclusive referenece. TODO This should probabkly rolled into
// contains an exclusive referenec. TODO This should probably be rolled into

@@ -573,6 +621,15 @@ class CheckCaptures extends Recheck, SymTransformer:
}
case _ => denot

// Don't allow update methods to be called unless the qualifier captures
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
// Don't allow update methods to be called unless the qualifier captures
// Don't allow update methods to be called unless the qualifier

@odersky odersky merged commit a3dde8b into scala:main Feb 5, 2025
30 checks passed
@odersky odersky deleted the exclusive-capabilities branch February 5, 2025 13:09
hamzaremmal added a commit that referenced this pull request Feb 6, 2025
Tests started to timeout when we merged the *Exclusive Capabilities* PR.
We will revert it for now until we inspect it further.

Reverts #22218
Closes #22535
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