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

feat: Functional constructs for methods #29

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
**/Output/
/.DS_Store
/.history
32 changes: 32 additions & 0 deletions src/Action.dfy
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// RUN: %dafny /compile:0 "%s" > "%t"
// RUN: %diff "%s.expect" "%t"

/*******************************************************************************
* Original: Copyright (c) 2020 Secure Foundations Lab
* SPDX-License-Identifier: MIT
*
* Modifications and Extensions: Copyright by the contributors to the Dafny Project
* SPDX-License-Identifier: MIT
*******************************************************************************/

module Action {

trait {:termination false} Action<A, R>
{
method Invoke(a: A)
returns (r: R)
requires Requires(a)
Copy link
Member

Choose a reason for hiding this comment

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

I think you mentioned you were aware already offline, but we'll need a function Reads(a: A): set<object> too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup. And I think we can have some nice helpers so people can mix in and not have so much typing :)

Copy link
Member

Choose a reason for hiding this comment

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

What about having a "modifies" first ?

ensures Ensures(a, r)

predicate Requires(a: A)
predicate Ensures(a: A, r: R)
}

trait {:termination false} NothingToRequire<A>
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps TotalAction instead, to align with -> being a "total" function?

Once you add Reads you'll want that to be just {} here too. You could then have a PartialAction which reads nothing but CAN require something.

{
predicate Requires(a: A){
true
}
}

}
2 changes: 2 additions & 0 deletions src/Action.dfy.expect
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

Dafny program verifier finished with 0 verified, 0 errors
261 changes: 261 additions & 0 deletions src/Collections/Sequences/Actions.dfy
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
// RUN: %dafny /compile:0 "%s" > "%t"
// RUN: %diff "%s.expect" "%t"

/*******************************************************************************
* Original: Copyright (c) 2020 Secure Foundations Lab
* SPDX-License-Identifier: MIT
*
* Modifications and Extensions: Copyright by the contributors to the Dafny Project
* SPDX-License-Identifier: MIT
*******************************************************************************/

include "../../Wrappers.dfy"
include "../../Action.dfy"
include "Seq.dfy"

module Actions {
import opened Wrappers
import opened A = Action
import opened Seq

method Map<A, R>(
action: Action<A, R>,
s: seq<A>
)
returns (res: seq<R>)
requires forall i | i in s :: action.Requires(i)
Copy link
Member

Choose a reason for hiding this comment

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

(Just food for thought as I THINK we can address this later) - I wonder if we could make this less restrictive, so later elements could depend on earlier ones. Something like:

requires |s| > 0 ==> action.Requires(s[0])
requires forall i | 1 <= i < |s| :: 
  (exists r | action.Ensures(s[i - 1], r) ==> action.Requires(s[i]))

Copy link
Member

Choose a reason for hiding this comment

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

This looks a bit suspicious. Should it be parenthesized differently?

requires forall i | 1 <= i < |s| :: 
  ((exists r | action.Ensures(s[i - 1], r)) ==> action.Requires(s[i]))

Which is then equivalent to

requires forall i | 1 <= i < |s| :: 
  (forall r :: action.Ensures(s[i - 1], r) ==> action.Requires(s[i]))

?

ensures |s| == |res|
ensures
forall i ::
&& 0 <= i < |s|
==>
action.Ensures(s[i], res[i])
{
var rs := [];
for i := 0 to |s|
invariant |s[..i]| == |rs|
invariant forall j ::
&& 0 <= j < i
==>
action.Ensures(s[j], rs[j])
{
var r := action.Invoke(s[i]);
rs := rs + [r];
}
return rs;
}

method MapWithResult<A, R, E>(
action: Action<A, Result<R, E>>,
s: seq<A>
)
returns (res: Result<seq<R>, E>)
requires forall i | i in s :: action.Requires(i)
ensures
res.Success?
==>
|s| == |res.value|
ensures
res.Success?
==>
(forall i ::
&& 0 <= i < |s|
==>
action.Ensures(s[i], Success(res.value[i])))
{
var rs := [];
for i := 0 to |s|
invariant |s[..i]| == |rs|
invariant forall j ::
&& 0 <= j < i
==>
action.Ensures(s[j], Success(rs[j]))
{
var maybeR := action.Invoke(s[i]);
if maybeR.Failure? {
return Failure(maybeR.error);
}
Comment on lines +74 to +77
Copy link
Member

Choose a reason for hiding this comment

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

Worth a comment about how you're having to work around the issue with :- here?

var r := maybeR.value;
rs := rs + [r];
}
return Success(rs);
}

method FlatMap<A, R>(
action: Action<A, seq<R>>,
s: seq<A>
)
// The ghost parts is returned to facilitate
// threading proof obligations.
// Idealy, it would be great to remove this
// and simply prove everything about `res`.
// However in practice this has proven to be difficult.
// Given how flexible FlatMap is,
// there may not be a prcatical general solution.
returns (res: seq<R>, ghost parts: seq<seq<R>>)
requires forall i | i in s :: action.Requires(i)
ensures
&& |s| == |parts|
&& res == Flatten(parts)
&& (forall i :: 0 <= i < |s|
==>
&& action.Ensures(s[i], parts[i])
&& multiset(parts[i]) <= multiset(res))
{
parts := [];
var rs := [];
for i := 0 to |s|
invariant |s[..i]| == |parts|
invariant forall j ::
&& 0 <= j < i
==>
&& action.Ensures(s[j], parts[j])
&& multiset(parts[j]) <= multiset(rs)
invariant Flatten(parts) == rs
{
var r := action.Invoke(s[i]);
rs := rs + r;
LemmaFlattenConcat(parts, [r]);
parts := parts + [r];
}
return rs, parts;
}

method FlatMapWithResult<A, R, E>(
Copy link
Member

Choose a reason for hiding this comment

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

It's definitely bothering me that we need this and can't just use Flatten(MapWithResult(action, s)) but I know you warned me it's not that simple. Let me play around with it a bit. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You should. It may be that with #28 many things are easier.
The difficulty was crossing the flatten boundary.

I will also note, that there may be a runtime efficiency "thing" in here.
If one or the other of these is easier to fold into a lazy seq creation,
then we should go with that one :)

action: Action<A, Result<seq<R>, E>>,
s: seq<A>
)
// The ghost parts is returned to facilitate
// threading proof obligations.
// Idealy, it would be great to remove this
// and simply prove everything about `res`.
// However in practice this has proven to be difficult.
// Given how flexible FlatMap is,
// there may not be a prcatical general solution.
returns (res: Result<seq<R>, E>, ghost parts: seq<seq<R>>)
requires forall i | i in s :: action.Requires(i)
ensures
res.Success?
==>
&& |s| == |parts|
&& res.value == Flatten(parts)
&& (forall i :: 0 <= i < |s|
==>
&& action.Ensures(s[i], Success(parts[i]))
&& multiset(parts[i]) <= multiset(res.value)
)
{
parts := [];
var rs := [];
for i := 0 to |s|
invariant |s[..i]| == |parts|
invariant forall j ::
&& 0 <= j < i
==>
&& action.Ensures(s[j], Success(parts[j]))
&& multiset(parts[j]) <= multiset(rs)
invariant Flatten(parts) == rs
{
var maybeR := action.Invoke(s[i]);
if maybeR.Failure? {
return Failure(maybeR.error), parts;
}
var r := maybeR.value;
rs := rs + r;
LemmaFlattenConcat(parts, [r]);
parts := parts + [r];
}
return Success(rs), parts;
}

method Filter<A>(
action: Action<A, bool>,
s: seq<A>
)
returns (res: seq<A>)
requires forall i | i in s :: action.Requires(i)
ensures |s| >= |res|
ensures
forall j ::
j in res
==>
&& j in s
&& action.Ensures(j, true)
{
var rs := [];
for i := 0 to |s|
invariant |s[..i]| >= |rs|
invariant forall j ::
j in rs
==>
&& j in s
&& action.Ensures(j, true)
{
var r := action.Invoke(s[i]);
if r {
rs := rs + [s[i]];
}
}
return rs;
}

method FilterWithResult<A, E>(
action: Action<A, Result<bool, E>>,
s: seq<A>
)
returns (res: Result<seq<A>, E>)
requires forall i | i in s :: action.Requires(i)
ensures
res.Success?
==>
&& |s| >= |res.value|
&& forall j ::
j in res.value
==>
&& j in s
&& action.Ensures(j, Success(true))
{
var rs := [];
for i := 0 to |s|
invariant |s[..i]| >= |rs|
invariant forall j ::
j in rs
==>
&& j in s
&& action.Ensures(j, Success(true))
{
var maybeR := action.Invoke(s[i]);
if maybeR.Failure? {
return Failure(maybeR.error);
}
var r := maybeR.value;
if r {
rs := rs + [s[i]];
}
}
return Success(rs);
}

method ReduceToSuccess<A, B, E>(
action: Action<A, Result<B, E>>,
s: seq<A>
)
returns (res: Result<B, seq<E>>)
requires forall i | i in s :: action.Requires(i)
ensures
res.Success?
==>
exists a | a in s :: action.Ensures(a, Success(res.value))
Copy link
Member

Choose a reason for hiding this comment

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

Don't you want to make this stronger and say it's the FIRST such element?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I do! But I did not know how.

Copy link
Member

Choose a reason for hiding this comment

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

exists i | 0 <= i < |s| :: action.Ensures(s[i], Success(res.value)) && forall j | 0 <= j < i ==> exist f | action.Ensures(s[i], Failure(f))

Maybe there is a better way?

{
var errors := [];
for i := 0 to |s| {
var attempt := action.Invoke(s[i]);
if attempt.Success? {
return Success(attempt.value);
} else {
errors := errors + [attempt.error];
}
}
return Failure(errors);
}
}
2 changes: 2 additions & 0 deletions src/Collections/Sequences/Actions.dfy.expect
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

Dafny program verifier finished with 88 verified, 0 errors