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

fix: Change reads clause of filter to * #99

Merged
merged 4 commits into from
Feb 22, 2023

Conversation

RustanLeino
Copy link
Collaborator

This PR changes Seq.Filter's frame specification from reads f.reads to reads *.

The reason for the change is that there are problems with using a function (here, the function f.reads) in a frame specification, because such a function desugars to, essentially, a big union and, depending on the parameters of the function, that big union may depend on the allocation state. In the case of Seq.Filter<T>(f: T ~> bool, xs: seq<T>), that desugaring was

set t: T, obj: object | obj in f.reads(t) :: obj

Since T may depend on the allocation state, so does the set comprehension, and that is not allowed in a the declaration of function Filter.

There are four ways to fix this problem.

  • Change the type of f from f ~> bool to f --> bool (or f -> bool). This restricts f to functions that don't read the heap, and hence the reads clause of Filter can be removed altogether.
  • Restrict the type parameter T to types that don't include heap references; that is, declare T with T(!new). This will allow the previous reads clause of Filter, but it also restricts the kinds of sequences Filter can be applied to.
  • Change the reads clause of Filter to reads *. This allows Filter to read anything in the allocated heap, but also weakens the power of the automatically generated frame axiom for Filter.
  • Declare more than one Filter function, each accommodating one of the possible fixes above.

This PR implements the third fix, reads *.

To date, Dafny erroneously didn't complain about the reads f.reads. That is currently being fixed, so this PR is needed to keep libraries verifying.

By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.

robin-aws
robin-aws previously approved these changes Feb 22, 2023
Copy link
Member

@robin-aws robin-aws left a comment

Choose a reason for hiding this comment

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

Great catch, and I think this option is the best tradeoff among the options you listed.

One thing to be careful about in the future: this looks like it would be a breaking change for code that specifically depended on the tighter reads clause. In this case since it's fixing a soundness issue and it's hard to imagine code that couldn't be quickly fixed if affected, I think it's reasonable, but otherwise we're relying on the fact that this codebase is still marked as subject to change, which we can't get away with forever.

In other words, the next time we hit this kind of issue we may need to pick your last option of forking code to avoid unbounded work for consumers to upgrade. :)

atomb
atomb previously approved these changes Feb 22, 2023
Copy link
Member

@atomb atomb left a comment

Choose a reason for hiding this comment

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

I agree with @robin-aws that this is the most pragmatic step to take at the moment. While we're at it, I notice that there are a few other instances of reads f.reads in the repository. Should we fix those, too?

@RustanLeino RustanLeino dismissed stale reviews from atomb and robin-aws via 7739717 February 22, 2023 21:12
@RustanLeino
Copy link
Collaborator Author

Thanks for the comments and quick reviews.

@atomb I took a look at the others (in Sets.dfy and ISets.dfy). Those instances are okay, because the element type is already declared as X(!new) (as required also by the quantifications in the preconditions of those functions). So, I think we can just leave those as they are.

I also looked at Map and MapWithResult in Seq.dfy (I hadn't looked at those before, because they didn't give errors with my new checking). Those are also okay, because they quantify over the sequence index. That made me realize that the reads f.reads I changed to reads * has yet another possible fix:

reads set i, o | 0 <= i < |xs| && o in f.reads(xs[i]) :: o

This is exact and has none of the drawbacks. (I should have thought of this obvious fix before.) I updated the PR to use this reads clause instead of the reads *.

While I was at it, I also removed many manual :trigger attributes from Seq.dfy, in the cases where auto-triggers would compute the same trigger. So, this change in Seq.dfy should not affect any verification.

@robin-aws
Copy link
Member

Nice, even better!

So, this change in Seq.dfy should not affect any verification.

Another phrase that triggers (ha!) my worries about eventual backwards compatibility. I think we'll need a mechanism to verify your assertion in the future. :)

Copy link
Member

@atomb atomb 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 very nice now. I like the reduction in {:trigger ...} annotations.

@RustanLeino RustanLeino merged commit 20d90ec into dafny-lang:master Feb 22, 2023
@RustanLeino RustanLeino deleted the fix-filter branch February 22, 2023 22:20
jtristan added a commit to dafny-lang/dafny that referenced this pull request Feb 23, 2023
Fixes #3606 
Fixes #3607 
Under the now-fixed `/allocated:4`, also fixes #19 and #3605.

This PR discovered a necessary change in the standard library:
dafny-lang/libraries#99 (which has been merged)

Also, this PR is what's referred to in #3600.

This PR fixes several previous problems with quantifications and
comprehensions over reference types. For example, previously, the
verifier encoded `set o: object | true` as the set of _all_ objects,
both allocated and unallocated, which is wrong in spirit and also
allowed for unsound verification.

The PR makes several general fixes and some fixes under just
`/allocated:4` (which is not yet the default). We should make
`/allocated:4` the default and remove the possibility for users to
choose how allocation state is encoded (that is, we should remove the
`allocated` flag altogether). However, it seems best to get this PR into
Dafny version 3.Last, then make `/allocated:4` the default in Dafny 4.0
(and probably deprecate the `/allocated` flag at that time, too), and
later remove the `/allocated` switch.

The general changes in this PR include three breaking changes and one
change that may affect verification.
* One breaking change is that some functions used in `reads` clauses are
no longer allowed. It was a bug that these were allowed before. This is
checked by the resolver.
* Another breaking change is that lambda expressions, including the
`requires` and `reads` clauses of lambda expressions, are no longer
allowed to depend on the allocation state. The previous omission of this
check was a bug. This is checked by the resolver.
* The third breaking change is that the verifier now include allocation
antecedents for bound variables whose allocation isn't already implied
by the body of the quantifier/comprehension. This is a virtue of (the
buggy design of) `/allocated:3` and is fixed in `/allocated:4`.
(Historically, this problem was not known at the time `/allocated:3` was
designed. The intended design of `/allocated:4` does not have the
problem, but the implementation of `/allocated:4` was previously
incomplete and buggy.) So, switching from the default `/allocated:3` to
`/allocated:4` (which will soon be the new default) changes (fixes!) the
encoding of these allocation antecedents, which may be a breaking change
for programs that (intentionally or inadvertently) made use of the
previous, buggy encoding.
* The possible change to verification is that this PR generalizes one
axiom. Logically, adding an axiom does not make any verifying program
not verify. However, as with any change in the verifier, in cases where
verification was previously unstable, the presence of this new axiom may
change verification behavior (e.g., change in verification performance
or verification outcome). This axiom is added for both `/allocated:3`
and `/allocated:4`.

Unfortunately, there were no tests for `/allocated:4`, so no wonder it
didn't work. Fortunately, this PR fixes `/allocated:4`. Unfortunately,
the PR adds only a scant amount of specific `/allocated:4` tests.
Fortunately, I did run the entire test suite locally with `/allocated:4`
and it passes. So, the lack of many `/allocated:4` tests will be
remedied as soon as we switch to `/allocated:4` as the default (in a
separate PR).

The long-standing issue #19 is fixed with this PR. Actually, the
original program reported in #19 has been fixed for a while, but the
long discussion of #19 showed many more issues. With `/allocated:4`, all
of those have been fixed (and all of them are recorded in tests in
`git-issue-19[ab].dfy`).

## More details

The rewrite of functions in `reads` clauses was previously done in more
than place. Now, this happens as a desugaring that's part of the
resolver's bounds-discovery phase.

Here are descriptions of the changes in the PR, listed more or less in
the order that github shows the changes:

* (code improvement) Add `ASTVisitor.VisitTopLevelFrameExpression`.
* (code improvement) Remove `requiredVirtues` parameter to
`ComprehensionExpr.GetBest`, since it was always passed in as `0`.
* In `FrameExpression`, capture user-supplied frame expressions into
`.OriginalExpression`, add `.DesugaredExpression` to hold any possible
frame-expression rewrite (in particular, functions in `reads` clauses
get desugared during resolution, and this desugaring is stored in
`.DesugaredExpression`), and let `.E` return the "better" of these two
fields.
* (code improvement) Move `FrameArrowToObjectSet` method from
`ArrowType` to `BoundsDiscoveryVisitor`.
* (code improvement) Specialized the `AstVisitor` `Context` type
parameter in the implementation of `BoundsDiscoveryVisitor`.
* (code improvement) Specialized the `AstVisitor` `Context` type
parameter in the implementation of `TypeInferenceChecker`. This makes it
clearer what parts of the context are important to type-inference
checking.
* (fix) Fill in `.Bounds` for various
quantifiers/comprehensions/forall-statements that are generated
automatically. (These are needed, because `/allocated:4` expects
`.Bounds` to be set.)
* Previously, each datatype constructor gave rise to an allocation
axiom, roughly `forall x, xs :: IsAlloc(x) && IsAlloc(xs) ==>
IsAlloc(Cons(x, xs))`. Now, if no constructor depends on the allocation
state, the entire type gets one allocation axiom, rather than having a
separate one for each constructor (roughly `forall c: Color ::
IsAlloc(c)`). This makes the axiom more useful where there isn't already
a case split for each constructor.
* (fix) The previous translation of `allocated(_)` made it sometimes not
provable. (This has to do with boxing and has now been fixed.)
* (fix) There was a code path for generating an allocation-consequence
axiom for functions. However, it was broken in many places. Apparently,
this was not detected, because the axiom is generated only for
`/allocated:4` and there were no tests for `/allocated:4`. This PR fixes
the code.
* Make some error messages more specific.

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

---------

Co-authored-by: John Tristan <[email protected]>
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