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

Reads clauses on methods #6

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

robin-aws
Copy link
Member

No description provided.

@robin-aws robin-aws marked this pull request as ready for review September 21, 2021 17:09
text/0000-reads-clauses-on-methods.md Outdated Show resolved Hide resolved
text/0000-reads-clauses-on-methods.md Outdated Show resolved Hide resolved
text/0000-reads-clauses-on-methods.md Outdated Show resolved Hide resolved
if n <= 2 {
return 1;
}
var cached := cache.Get(n);
Copy link
Member

Choose a reason for hiding this comment

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

cache is implicit for this.cache, so it reads this, right? Then the call in SafeFibonacci would not compile.

Or are you saying the reads this is not necessary because cache is const? But I would think we don't know whether ExternalMutableMap is mutable or not, so even if the field is const, that doesn't mean reading it is thread-safe.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's right, reads and modifies only care about mutable fields, so vars are in scope but not consts. I agree that's a bit surprising but already how reads works on functions.

Sure, we don't "know" if ExternalMutableMap is mutable since the verifier has no visibility into the external code. But there's no way around that, and modelling the assumptions we might make about external code using ghost state as in https://github.com/dafny-lang/dafny/wiki/Modeling-External-State-Correctly breaks down if that external state can change without Dafny being aware of it. The idea here is to verify that a method does not interact with any mutable state on the Dafny heap, but may still interact with "the outside world", and it's up to the external code to be thread-safe when necessary.

Copy link
Member

Choose a reason for hiding this comment

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

And if ExternalMutableMap was not implemented externally, how would you know that it's immutable?

Copy link
Member Author

Choose a reason for hiding this comment

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

Discussed this offline to understand the question better, and I can clarify how this all works with a few more details.

Note that clauses such as reads and modifies are not recursive. Take these classes as an example:

class A {
  constructor() {}
  var foo: int
  var b: B
}
class B {
  var bar: int
}

If you had an object a: A, the clause reads {a} would only allow you to reference a.foo, but NOT a.b.foo. For the latter to be allowed, you would need reads {a, a.b}. This is why the Valid() / Repr idiom described in Chapters 8 and 9 of Using Dafny, an Automatic Program Verifier is useful for tracking what areas of the heap your code needs to read and write, without depending on the internals of your components.

That means Dafny doesn't allow the cache.Get call because it believes ExternalMutableMap is immutable, it's allowed because the cache.Get method has reads {}, which means the method does not read any state in the Dafny heap.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Btw, I presume that reads clauses are subject to themselves. That is, a reads clause has to be "closed", in the same way that they are for functions. Their well-formedness should then also be allowed to assume the method precondition, just as for functions. (The Dafny verifier has an encoding for this kind of thing. Look for the parameter called delayedReadsChecks or something like that.)


This proposal does not aim to add concurrency primitives to Dafny, or even propose any particular choice of paradigm for asynchronous or concurrent execution. Instead, it supports verifying that a particular method is safe to use in a concurrent environment, by verifying that it does not operate on any mutable state unsafely.

# Guide-level explanation
Copy link
Member

@keyboardDrummer keyboardDrummer Sep 22, 2021

Choose a reason for hiding this comment

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

This section doesn't describe any use-cases that require something other than reads {}, so why not introduce a more restricted construct like threadsafe ? That gives less freedom to the programmer, so less chance of confusion, and you could relax the associated checks in the future, for example to allow reading passed in arguments as long as they are immutable or owned by the callee.

Copy link
Member Author

Choose a reason for hiding this comment

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

Other reads frames are necessary on internal methods (i.e. not exposed to external code) in the control flow though. For example, FibonacciMemoizer.Get has to declare reads this, because otherwise the default spec of reads * would violate the tighter reads {} frame of SafeFirstNFibonaccis.

I would actually argue that reusing an existing concept in a very similar additional context is a lower cognitive burden on programmers than a new concept like threadsafe (and again I'm trying not to introduce the concept of a "thread" unless we're 100% sure that's the right concurrency model for Dafny).

Copy link
Member

Choose a reason for hiding this comment

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

I would actually argue that reusing an existing concept in a very similar additional context is a lower cognitive burden on programmers than a new concept

I guess that's a difficult thing to determine, but I was thinking that if there is a specific pattern (reads {}) used for a specific use-case (calling externally exposed methods concurrently), it does help to name that use-case and remove the need for the pattern.

Other reads frames are necessary on internal methods (i.e. not exposed to external code) in the control flow though. For example, FibonacciMemoizer.Get has to declare reads this, because otherwise the default spec of reads * would violate the tighter reads {} frame of SafeFirstNFibonaccis.

I see. I'm a little confused by the file text/guide-level-explanation-example-1.dfy which has similar but different content that's what's inline in text/0000-reads-clauses-on-methods.md. The former has reads this but the latter one doesn't.

Given that FibonacciMemoizer.Get doesn't take any reference type parameters, isn't reads this the upper bound of what it can read so reads this can be implicit?

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess that's a difficult thing to determine, but I was thinking that if there is a specific pattern (reads {}) used for a specific use-case (calling externally exposed methods concurrently), it does help to name that use-case and remove the need for the pattern.

That's true. I could imagine something like {:threadsafeExtern}, or some way of parameterizing {:extern}. It wouldn't eliminate the need for reads clauses in general in order to verify this property, though. You'd have to either have that annotation imply reads {} or require that such methods also declare reads {} explicitly, and I'd likely prefer the latter.

Given {:extern} is currently very weakly specified and already has several other sharp edges (https://github.com/dafny-lang/dafny/labels/foreign-function-interface), it's my preference to leave this out of scope for now, just as we also want to leave any concurrency semantics for Dafny out of scope if we can.

I see. I'm a little confused by the file text/guide-level-explanation-example-1.dfy which has similar but different content that's what's inline in text/0000-reads-clauses-on-methods.md. The former has reads this but the latter one doesn't.

Ah that's my fault for letting the two versions get out of sync. I was hoping to find a way to include the source files into the markdown. I've deleted them and put the missing reads this clause into the example.

Some classes will only have const fields, so some class methods can declare reads {} rather than reads this. I'm still thinking making the default reads * everywhere would be better for simplicity.

Copy link
Member

@keyboardDrummer keyboardDrummer Sep 28, 2021

Choose a reason for hiding this comment

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

That's true. I could imagine something like {:threadsafeExtern}, or some way of parameterizing {:extern}. It wouldn't eliminate the need for reads clauses in general in order to verify this property, though. You'd have to either have that annotation imply reads {}

Yes, that was what I was thinking of

or require that such methods also declare reads {} explicitly, and I'd likely prefer the latter.

I think I agree that reads clauses on methods are the more lightweight feature to add since it doesn't introduce any new concept.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If we're okay with a keyword like threadsafe, then the language could say the following:

  • A method that is not declared to be threadsafe is not allowed to have a reads clause. Such a method is allowed to read anything (just like today).
  • A method declared with threadsafe method is allowed to have a reads clause. We now even have the freedom to say that the default reads clause for a threadsafe method is the empty set. This would make it consistent with both reads and modifies clauses today. Furthermore, this would completely address my concern about beginning users getting the wrong idea that they must declare the reads set for method -- if they ever see, or try to declare, a method with a reads clause, then that method is declared as threadsafe method, which the user can identify.

Copy link
Collaborator

@RustanLeino RustanLeino left a comment

Choose a reason for hiding this comment

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

This looks really good. My concerns about beginners developing the incorrect idea that methods must have reads clauses is captured in the RFC. We could consider a command-line option that opts in to using reads clauses, and without which beginners would get an error for trying a reads clause on a method.

I have but one comment of substance, which concerns ghost state. For example, it seems to me that lemmas should be callable from anywhere, even though they may actually read state. Probably, the same goes for ghost methods. But what about reading mutable ghost fields? Do such reads have to be declared as well? (If the answer is not clear, a first implementation could be conservative and treat ghost state in the same way as non-ghost state.)

{
var memoizer := new FibonacciMemoizer();
rs := [];
for i := 1 to n + 1 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you want the first n Fibonacci numbers, you want

Suggested change
for i := 1 to n + 1 {
for i := 0 to n {

As written, you'll get the first n + 1 Fibonacci numbers, except the first one.

//
class {:extern} ExternalMutableMap<K, V> {
constructor {:extern} () reads {}
method {:extern} Put(k: K, v: V) reads {}
Copy link
Collaborator

Choose a reason for hiding this comment

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

You'd expect a nonempty modifies clause for Put. Add modifies this.

This also means that FibonacciMemoizer.Get on L139 needs a modifies clause. (Without the usual Repr/Valid() abstraction, this can be done with modifies cache. With this change, it would then be fair to clients to add the postcondition ensures fresh(cache) to the FibonacciMemoizer constructor.)

Copy link
Member Author

Choose a reason for hiding this comment

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

Believe it or not, no! The entire point here is to NOT model the external state in Dafny frames at all. To Dafny, an ExternalMutableMap is a completely opaque object, and its methods have non-deterministic behavior. This means this has no mutable state and adding modifies this will just add confusion and probably more verification difficulties.


The biggest potential drawback is that inexperienced Dafny programmers might have the incorrect impression that defining a tight `reads` clause for methods is necessary, and hence expend unnecessary effort for no benefit. The verifier would never prompt users to add `reads` clauses unless they already appear elsewhere in a codebase, so the issue is more that they could see them used in documentation or examples and not realize they are largely optional.

Reusable Dafny code such as the modules in https://github.com/dafny-lang/libraries will need to specify `reads` clauses in order to support consumers that need to specify them. This does not appear to be a significant burden: at the time of writing this the `libraries` repo only contained one `method`. All other uses of the `method` keyword were in defining `function method`s instead, which already default to `reads {}` and hence can be used as is in the control flow of methods with `reads` clauses. Also note that adding a `reads` clause to a method that does not already have one will never break existing callers of that method. Again, the issue is more that users could imitate standard library `method` definitions and include superfluous `reads` clauses. The answer is likely to document explicitly why the standard library uses this feature and to emphasize that it should not be copied blindly. There is already a similar issue with the standard library's use of `{:trigger}` attributes, which are not encouraged in most Dafny codebases.
Copy link
Collaborator

Choose a reason for hiding this comment

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

The sentence about function method does not seem relevant. (Functions already have reads clauses, and details of where the letters method appear (perhaps in comments?) have no bearing on the RFC.) I suggest dropping the sentence.

# Unresolved questions
[unresolved-questions]: #unresolved-questions

- Parsing is simplest if `reads` clauses are allowed on all `method`-like definitions, including the various flavours of lemmas. Allowing `reads` clauses on lemmas seems harmless, but are there downsides?
Copy link
Collaborator

Choose a reason for hiding this comment

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

The parser already disallows modifies clauses on lemma declarations. The parser could do something similar for reads.

When sharing state externally really is necessary, it must be implemented externally in native code, where native facilities for synchronizing access to mutable state can be used. Therefore the solution is to push the caching logic into the target language instead:

```dafny
datatype Option<T> = Some(value: T) | None
Copy link
Collaborator

Choose a reason for hiding this comment

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

I suggest moving this type definition down below the ExternalMutableMap. I found myself reading it and the sentence above it several times, trying to figure out their connection, until I realized the sentence above has nothing to do with datatypes and that this datatype is introduced only to support the example.

Copy link
Collaborator

@RustanLeino RustanLeino left a comment

Choose a reason for hiding this comment

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

I suggest that the RFC spell out explicitly that the by method part of a function by method also needs to check reads clauses. In particular, the by method must live up to the same reads clause as the function has. This is required for soundness. More precisely, in order to rely on the proposed method reads clauses when determine a program to be "thread safe", it would not be okay if the by method part read the heap in places where the caller is not aware (which is allowed today).

@seebees
Copy link

seebees commented Feb 9, 2023

I need to read the full RFC, but my question is:
Since a methods reads clause bounds the things it can read
does this mean that I can be much more permissive in state that I return?

When proving things with complex frames I find I have to protect
agains crazy developers who just might randomly add an object into my frame.
(I use Modifies: set<object> because Repr is to opaque)
I do this with fresh(Modifies - old(Modifies)).
But if I understand what is happening here
if I define reads Modifies then I would not need to have the above correct?

If this is true, this would be amazing in terms of simplifying the mental model to verify mutation.


If this class is compiled to C#, for example, and its instances are shared between multiple threads, its verified properties are no longer true. If two threads both invoke `Increment` on a single counter, the effect could easily be to only increment `x` once, since reading and writing `x` does not occur atomically. Even the post-condition on `Get` is no longer valid, because the value of `x` could change in-between reading its value and returning from the compiled body.

This proposal does not aim to add concurrency primitives to Dafny, or even propose any particular choice of paradigm for asynchronous or concurrent execution. Instead, it supports verifying that a particular method is safe to use in a concurrent environment, by verifying that it does not operate on any shared mutable state.
Copy link
Member

@keyboardDrummer keyboardDrummer Aug 17, 2023

Choose a reason for hiding this comment

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

by verifying that it does not operate on any shared mutable state.

Can't you also achieve that by controlling which arguments are passed to the method? If the (static) method doesn't have any pointers to traverse from, then it can't read from or write to any mutable state, can it?

Copy link
Member Author

Choose a reason for hiding this comment

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

That could work too! But there are a few downsides to that approach:

  1. The analysis to traverse all paths from a value to find any potential mutable state doesn't exist as any part of Dafny's implementation already, and would have to deal with some tricky cases where it would have to be overly conservative (e.g. if you have a generic method that takes a T value, you'd probably have to be safe and reject it just in case it ends up making mutable state reachable). The framing approach is quite a bit cheaper to implement since modifies already exists and reads can heavily reuse its verification machinery.
  2. That analysis would not be compatible with modular verification, since you'd need to look inside dependent modules in case there is any mutable state lurking there. Whereas reads and modifies frames will be/are part of the specification of module contents.
  3. Rejecting any input connected to mutable state is more restrictive than necessary, since there's no danger if you don't actually read or write that state.

Copy link
Member

Choose a reason for hiding this comment

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

Ok, thanks !

robin-aws added a commit to dafny-lang/dafny that referenced this pull request Aug 30, 2023
Implements dafny-lang/rfcs#6.

This functionality is guarded by a new `--reads-clauses-on-methods`
option, even though it's almost completely backwards compatible: the
only wrinkle is function-by-methods, where valid code today might be
rejected when the reads clause of the function definition is applied to
the by-method body.

Note that "methods" here is interpreted as it is in the Dafny reference
manual and similar documentation: it includes constructors and ghost
methods, but NOT lemmas.

The `{:concurrent}` attribute, which previously only existed to generate
an auditor warning that Dafny could not verify this property, now
creates assertions that the `reads` and `modifies` clauses on the
function or method are empty.

The core implementation strategy is relatively straightforward: enabling
reads checks (via the `WFOptions` value) in the well-formedness check
for statements as well as expressions. The existing `$_Frame` variable
is now split into `$_ReadsFrame` and `$_ModifiesFrame`, since we now
need both at once in method contexts. To help make reads checks optional
based on the new option, and to optimize by not enabling them when in a
`reads *` context (the default for methods), the
`ExpressionTranslator.readsFrame` field may be null. I performed some
mild refactoring to make this cleaner but open to suggestion on further
improvements.

Documentation changes preview at https://robin-aws.github.io/dafny/.

<small>By submitting this pull request, I confirm that my contribution
is made under the terms of the [MIT
license](https://github.com/dafny-lang/dafny/blob/master/LICENSE.txt).</small>
keyboardDrummer pushed a commit to keyboardDrummer/dafny that referenced this pull request Sep 15, 2023
Implements dafny-lang/rfcs#6.

This functionality is guarded by a new `--reads-clauses-on-methods`
option, even though it's almost completely backwards compatible: the
only wrinkle is function-by-methods, where valid code today might be
rejected when the reads clause of the function definition is applied to
the by-method body.

Note that "methods" here is interpreted as it is in the Dafny reference
manual and similar documentation: it includes constructors and ghost
methods, but NOT lemmas.

The `{:concurrent}` attribute, which previously only existed to generate
an auditor warning that Dafny could not verify this property, now
creates assertions that the `reads` and `modifies` clauses on the
function or method are empty.

The core implementation strategy is relatively straightforward: enabling
reads checks (via the `WFOptions` value) in the well-formedness check
for statements as well as expressions. The existing `$_Frame` variable
is now split into `$_ReadsFrame` and `$_ModifiesFrame`, since we now
need both at once in method contexts. To help make reads checks optional
based on the new option, and to optimize by not enabling them when in a
`reads *` context (the default for methods), the
`ExpressionTranslator.readsFrame` field may be null. I performed some
mild refactoring to make this cleaner but open to suggestion on further
improvements.

Documentation changes preview at https://robin-aws.github.io/dafny/.

<small>By submitting this pull request, I confirm that my contribution
is made under the terms of the [MIT
license](https://github.com/dafny-lang/dafny/blob/master/LICENSE.txt).</small>
keyboardDrummer pushed a commit to dafny-lang/dafny that referenced this pull request Sep 19, 2023
Implements dafny-lang/rfcs#6.

This functionality is guarded by a new `--reads-clauses-on-methods`
option, even though it's almost completely backwards compatible: the
only wrinkle is function-by-methods, where valid code today might be
rejected when the reads clause of the function definition is applied to
the by-method body.

Note that "methods" here is interpreted as it is in the Dafny reference
manual and similar documentation: it includes constructors and ghost
methods, but NOT lemmas.

The `{:concurrent}` attribute, which previously only existed to generate
an auditor warning that Dafny could not verify this property, now
creates assertions that the `reads` and `modifies` clauses on the
function or method are empty.

The core implementation strategy is relatively straightforward: enabling
reads checks (via the `WFOptions` value) in the well-formedness check
for statements as well as expressions. The existing `$_Frame` variable
is now split into `$_ReadsFrame` and `$_ModifiesFrame`, since we now
need both at once in method contexts. To help make reads checks optional
based on the new option, and to optimize by not enabling them when in a
`reads *` context (the default for methods), the
`ExpressionTranslator.readsFrame` field may be null. I performed some
mild refactoring to make this cleaner but open to suggestion on further
improvements.

Documentation changes preview at https://robin-aws.github.io/dafny/.

<small>By submitting this pull request, I confirm that my contribution
is made under the terms of the [MIT
license](https://github.com/dafny-lang/dafny/blob/master/LICENSE.txt).</small>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants