Skip to content

Conversation

@jcp19
Copy link
Contributor

@jcp19 jcp19 commented Dec 23, 2025

This PR improves Gobra's situation in dealing with atomicity, so that we no longer need informal arguments to justify that opening invariants around certain parts of the code is safe. In particular, it brings the following changes:

  • introduces the atomic modifier for abstract methods and functions (non-abstract atomic members are disallowed, as we cannot prove atomicity - at least for now). Atomic methods should be those whose effects occur, logically, at a single linearization point. Interface methods may be marked as atomic too, in which case they may only be implemented by atomic methods.
  • Introduces the notion of invariants as found in other CSLs like Iris. A value P of type pred() is an invariant, written as Invariant(p) if it has been shown to hold using the EstablishInvariant builtin ghost function. Once established, invariants must be preserved by all atomic operations, and thus, by all operations.
  • Fixes a pre-existing issue where one could not previously write P(), where P is a built-in FPredicate like PredTrue. We were forced to write PredTrue!<!>() at all times before.
  • Add support for critical regions for invariants:
critical (P!<!>()) (
S
)

This statement opens invariant P!<!>(), which is assumed at the start of the critical region, and must be shown at its end. Critical regions check that there is no re-entrance, i.e., no invariant is opened twice. Statements in S may contain, at most, a single call to an atomic method (from an interface or otherwise, more on this later) and arbitrary ghost code (which must be shown to terminate). Like the oultine statement, the critical statement does not introduce a block (i.e., it does not introduce its own namespace).

There are two critical decisions I took to simplify the logics here:

  • To avoid re-entrance, we must guarantee that no method called from a critical region of an invariant P opens P again. One way of doing this would be to have some way of tracking the currently open invariants, and specify in the method specifications which invariants are required to not be open. This requires a more complex encoding. Instead, I opted for the following split of concerns, which I think is not very limiting: ghost methods cannot open invariants. The invariants must be opened in the actual code before calling ghost methods that depend on them. Thus, ghost methods may all be called safely from critical regions. This makes checking for reentrance very easy.
  • Calls to atomic interface methods are supported in critical regions, even though they are not atomic (a call to an interface method causes a lookup on the vtable to dispatch the call, and the effects of this may be observable). Nonetheless, I believe it is safe to call these methods. when S contains a call i.M(), where i is of interface type, I believe that reasoning about this program is similar to reasoning about the following:
critical (P!<!>()) (
resolve i.M()             // (1)
call_resolved i.M()   // (2), atomic
)

Step (1) is "transparent", i.e., its effects cannot be observed, if the value stored in i cannot change between (1) and (2), which guarantees that the method that is called still matches the dynamic type of the value stored in i. I believe that is always the case:

  1. the receiver i is either in an exclusive or shared memory location.
  2. If it is exclusive, it may not be changed between (1) and (2) by another thread.
  3. If it is shared it may only be modified by another thread. However, to resolve the call to i.M, there must be at least read permissions to i in the current thread. The permissions may either come from from the surrounding environment or from P!<!>().
    a. If they come the former, then no other thread may ever obtain full permission to i and modify it while the call is being performed.
    b. If they come from the latter, another thread could in principle try to open P!<!>() in parallel and modify i in an atomic step. However, there is no way to do so as far as I can tell. Regular assignments to i are not atomic (and thus, disallowed in critical regions) and the package atomic does not offer a way (as far as I can see) to atomically mutate a variable of interface type.

EDIT: maybe disallowing opening invariants in ghost methods is too restrictive after all. An example where this is very limiting is in the implementation of Iris's ghost locations in gobra-libs, where the only way to implement a model for this requires an invariant. At first sight, a solution to this may be to require an annotation on methods that may open invariants and disallow calling those methods from methods without that annotation or inside critical regions. More experience is required to see if this is permissive enough.

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.

2 participants