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

WIP: Enumerator<T> trait #37

Draft
wants to merge 69 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
6a32b86
Partial work, almost verifying
robin-aws Feb 2, 2022
0be0251
Got an iterator -> enumerator adaptor to verify
robin-aws Feb 5, 2022
6c2088a
Add Enumerator.Done()
robin-aws Feb 8, 2022
2545eae
More partial work
robin-aws Feb 9, 2022
6baa11f
Stepper approach seems to work better!
robin-aws Feb 12, 2022
73cc542
More partially-implemented enumerators
robin-aws Feb 13, 2022
768df4e
Restricted Step post-condition and got Filter to verify!
robin-aws Feb 13, 2022
c7aaacc
Defined Rust-style Iterator
robin-aws Feb 14, 2022
dc59bea
Improve while loop idiom, start cleaning up
robin-aws Feb 15, 2022
4b8307e
CLEANUP
robin-aws Feb 15, 2022
eee7d1d
Add some todos
robin-aws Feb 15, 2022
37015dd
(Mostly) converted Step() to Next()
robin-aws Feb 16, 2022
924d685
Progress on fixing Filter
robin-aws Feb 16, 2022
323abfd
Replace Done() with HasNext()
robin-aws Feb 16, 2022
aae5518
Partial progress on proving enumerator semantics
robin-aws Feb 18, 2022
2ebc186
Progress on fixing FilterEnumerator
robin-aws Feb 19, 2022
5e63373
Proved semantics of FilteredEnumerator
robin-aws Feb 19, 2022
a0e370f
Fixed termination guarantee on FilteredEnumerator
robin-aws Feb 19, 2022
bcfbdb1
Progress on iterator example
robin-aws Feb 19, 2022
9d52831
Fixed iterator adaptor example
robin-aws Feb 21, 2022
5b3b315
Relocating example code
robin-aws Feb 22, 2022
23395ac
Add Fold
robin-aws Feb 23, 2022
16142fb
Tons of documentation
robin-aws Feb 23, 2022
2b4b2f6
Bit more cleanup
robin-aws Feb 23, 2022
a090841
Add lit commands
robin-aws Feb 23, 2022
07132ad
Remove execution-created files
robin-aws Feb 23, 2022
ee50ca5
Pr feedback, fix build
robin-aws Feb 24, 2022
ef77a1c
Fix another lit test failure
robin-aws Feb 24, 2022
f172ef5
Trim redundant post-conditions
robin-aws Feb 24, 2022
08876c0
ACTUALLY fix lit test failure
robin-aws Feb 24, 2022
89984b4
Add trait specializations
robin-aws Feb 24, 2022
64f257f
Sigh
robin-aws Feb 24, 2022
a5c7863
Switching to Option<T> flavour
robin-aws Feb 25, 2022
e449f61
More verification progress and useful examples
robin-aws Feb 26, 2022
6a0c220
Fixed iterator adaptor, progress on other parts
robin-aws Mar 1, 2022
a811a1f
Added WithEnumeratedEnumerator
robin-aws Mar 1, 2022
a208cd4
Extra comment
robin-aws Mar 1, 2022
e68755c
Quick note
robin-aws Mar 2, 2022
4aca2f3
Merge branch 'master' of github.com:dafny-lang/libraries into enumera…
robin-aws May 3, 2023
256aabe
Update for Dafny 4, couple of notes
robin-aws May 3, 2023
2f2aa05
Trying out a pure-functional version
robin-aws May 8, 2023
5986181
Promising success with constant Next invariant
robin-aws May 10, 2023
47dcc9d
Partial attempt to use function instead of constant function value
robin-aws May 11, 2023
49de185
Generalizing to Actions
robin-aws May 14, 2023
a32f73b
Temporary Repr restriction
robin-aws May 15, 2023
377de50
Sweet solution for enumerator termination measure
robin-aws May 16, 2023
e38755c
SeqEnumerator
robin-aws May 17, 2023
5f6e167
Use height instead of Repr for termination, split up more files
robin-aws May 22, 2023
006a553
Progress
robin-aws May 23, 2023
f339ada
Remove unnecessary nested exists
robin-aws May 23, 2023
8c88b21
More progress
robin-aws May 23, 2023
4cae439
EnumeratedDistributesOverConcat!
robin-aws May 27, 2023
f321710
Partial work to fix definition of IsEnumerator()
robin-aws May 29, 2023
bd81878
Enumerators.dfy verifying again
robin-aws May 29, 2023
e066a2d
NoOp verifies!
robin-aws May 29, 2023
cf6ac72
Mapping enumerator verifies (avoiding Enumerated lemmas for now)
robin-aws May 29, 2023
300c012
Progress on composed action
robin-aws May 30, 2023
bad3054
Experimenting with no CanConsume
robin-aws Jun 7, 2023
bcb450d
Revert "Experimenting with no CanConsume"
robin-aws Jan 12, 2024
8139631
Complete EnumeratedDistributesOverConcat2
robin-aws Jan 12, 2024
8607ee7
More progress
robin-aws Jan 15, 2024
2efc280
Got ComposedAction to verify!
robin-aws Jan 15, 2024
259f899
Lots more Actions, partial refactoring for “history”
robin-aws Feb 17, 2024
b036c66
Got GenericAction to work as a base type
robin-aws Feb 21, 2024
627ec2a
Updated most cases to deal with history change
robin-aws Feb 22, 2024
abe9233
Improving combinator specs
robin-aws Feb 22, 2024
18a2ef4
Replace IsSmallerThan with DecreasesTo, motivating example
robin-aws Feb 22, 2024
fd231a1
Tweaks
robin-aws Feb 22, 2024
be37f10
added new type characteristic to get things to check
Mar 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
87 changes: 87 additions & 0 deletions examples/Enumerators/Datatypes.dfy
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// RUN: %dafny /compile:3 "%s" > "%t"
// RUN: %diff "%s.expect" "%t"

include "Enumerators.dfy"

// An example of an enumerator that traverses the sub-values in
// an algebraic datatype value.
module DatatypeEnumerator {

import opened Enumerators

// TODO: A TreeEnumerator would be much more interesting!
// Show how to implement multiple traversal options, probably
// using ConcatEnumerator for child paths.

// TODO: Could define a Enumerable<T> trait as well, although datatypes
// can't yet implement that anyway.
datatype List<T> = Cons(value: T, tail: List<T>) | Nil {
method Enumerator() returns (e: Enumerator<T>) {
robin-aws marked this conversation as resolved.
Show resolved Hide resolved
e := new ListEnumerator(this);
}

function Length(): nat {
match this
case Cons(_, tail) => 1 + tail.Length()
case Nil => 0
}
}

class ListEnumerator<T> extends Enumerator<T> {

var next: List<T>

constructor(next: List<T>)
ensures Valid()
ensures fresh(Repr - {this})
{
this.next := next;

enumerated := [];
Repr := {this};
}

predicate Valid()
reads this, Repr
ensures Valid() ==> this in Repr
decreases Repr, 0
{
&& this in Repr
}

function Decreases(): nat
reads Repr
requires Valid()
decreases Repr, 1
{
// TODO: This is where I wish I could just say "next" and
// rely on the well-founded ordering.
next.Length()
}

predicate method HasNext()
reads Repr
requires Valid()
decreases Repr, 2
ensures Decreases() == 0 ==> !HasNext()
{
next.Cons?
}

method Next() returns (element: T)
requires Valid()
requires HasNext()
modifies Repr
decreases Repr
ensures Valid()
ensures Repr <= old(Repr)
ensures Decreases() < old(Decreases())
ensures enumerated == old(enumerated) + [element]
{
element := next.value;
next := next.tail;

enumerated := enumerated + [element];
}
}
}
2 changes: 2 additions & 0 deletions examples/Enumerators/Datatypes.dfy.expect
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

Dafny program verifier finished with 12 verified, 0 errors
143 changes: 143 additions & 0 deletions examples/Enumerators/Enumerators.dfy
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// RUN: %dafny /compile:3 "%s" > "%t"
// RUN: %diff "%s.expect" "%t"

include "../../src/Enumerators/Enumerators.dfy"

module Demo {

import opened Enumerators

// Note the common template for using a while loop on
// an arbitrary Enumerator. This will likely be what we
// propose that a "foreach" loop in Dafny will desugar to.

method Example1() {
var numbers := [1, 2, 3, 4, 5];

var e: Enumerator<int> := new SeqEnumerator(numbers);
while (e.HasNext())
robin-aws marked this conversation as resolved.
Show resolved Hide resolved
invariant e.Valid() && fresh(e.Repr)
decreases e.Decreases()
{
var element := e.Next();

print element, "\n";
}
}

method Example2() {
var first := [1, 2, 3, 4, 5];
var second := [6, 7, 8];
var e1 := new SeqEnumerator(first);
var e2 := new SeqEnumerator(second);
var e := new ConcatEnumerator(e1, e2);

while (e.HasNext())
invariant e.Valid() && fresh(e.Repr)
decreases e.Decreases()
{
var element := e.Next();

print element, "\n";
}
}

method PrintWithCommas() {
var first := [1, 2, 3, 4, 5];
var e := new SeqEnumerator(first);

while (e.HasNext())
invariant e.Valid() && fresh(e.Repr)
decreases e.Decreases()
{
var element := e.Next();

print element;
if e.HasNext() {
print ", ";
}
}
print "\n";
}

method MappingExample() {
var first := [1, 2, 3, 4, 5];
var e1 := new SeqEnumerator(first);
var e := new MappingEnumerator(x => x + 2, e1);

var result: seq<int> := [];
while (e.HasNext())
invariant e.Valid() && fresh(e.Repr)
decreases e.Decreases()
{
var element := e.Next();

result := result + [element];
}
assert e.enumerated == Seq.Map(x => x + 2, first);
}

// Some examples showing the equivalence between enumerator
// operations and sequence operations.

method RoundTrip(s: seq<nat>) returns (result: seq<nat>)
ensures result == s
{
var e: Enumerator := new SeqEnumerator(s);
result := CollectToSeq(e);
}

method AddTwoToEach(s: seq<nat>) returns (result: seq<nat>)
ensures result == Seq.Map(x => x + 2, s)
{
var e := new SeqEnumerator(s);
var mapped := new MappingEnumerator(x => x + 2, e);
result := CollectToSeq(mapped);
}

method SetToSeq<T>(s: set<T>) returns (result: seq<T>)
ensures multiset(result) == multiset(s)
{
var e := new SetEnumerator(s);
result := CollectToSeq(e);
}

method Filter<T>(s: seq<T>, p: T -> bool) returns (result: seq<T>)
ensures result == Seq.Filter(p, s)
{
var e := new SeqEnumerator(s);
var filtered: FilteredEnumerator := new FilteredEnumerator(e, p);
result := CollectToSeq(filtered);
assert filtered.Valid();
}

method Concatenate<T>(first: seq<T>, second: seq<T>) returns (result: seq<T>)
ensures result == first + second
{
var e1 := new SeqEnumerator(first);
var e2 := new SeqEnumerator(second);
var concatenated: ConcatEnumerator := new ConcatEnumerator(e1, e2);
result := CollectToSeq(concatenated);
assert concatenated.Valid();
}

// TODO: The examples above work because Dafny is aware of the concrete
// types of the various enumerator values, and hence knows the additional post-conditions
Copy link
Collaborator

Choose a reason for hiding this comment

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

Where is this needed? For all new allocations in this Demo module, I gave the trait as an explicit type of the receiving variable and everything still verifies. For example, I changed the declarations of e1, e2, and e in Example2() to

    var e1: Enumerator := new SeqEnumerator(first);
    var e2: Enumerator := new SeqEnumerator(second);
    var e: Enumerator := new ConcatEnumerator(e1, e2);

Copy link
Member Author

Choose a reason for hiding this comment

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

Example2 is just fine with that because it doesn't assert anything about the behavior of the enumerators. But Concatenate has a post-condition that isn't verified if I remove the explicit type declaration on L119. That's because it needs the extra post-condition implied by !ConcatEnumerator.HasNext(), so Dafny doesn't have enough information if I don't guide it to recognize the concrete type of that enumerator. I'm trying to figure out if there's a better solution there.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah don't spend any further thought on this, I have a solution that seems to work nicely. Will push something tomorrow.

// of Valid() and !HasNext() necessary to support the more specific assertions.
// That's why we need to explicitly attach a more specific type than Enumerator
// to some variables, when type inference would otherwise choose Enumerator.
// This will be an issue when trying to add more higher-order operations on enumerators,
// or on linking to external implementations that don't have specific Dafny types to
// attach their invariants on.
//
// We'd like to have signatures like this, that ensures the Valid() and HasNext()
// implementations on the result have the desired properties, so we don't need the
// verifier to know the concrete type of the result:
//
// method MakeSeqEnumerator<T>(s: seq<T>) returns (result: Enumerator<T>)
// ensures forall e :: (result's HasNext applied to e) == false ==> e.enumerated == s)
//
// There isn't currently any way to refer to the HasNext function on the result that doesn't
// bind result as the function receiver, though, and my attempts to define ghost vars that
// hold onto such function references haven't worked out well so far.
}
2 changes: 2 additions & 0 deletions examples/Enumerators/Enumerators.dfy.expect
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

Dafny program verifier finished with 11 verified, 0 errors
104 changes: 104 additions & 0 deletions examples/Enumerators/IteratorAdaptor.dfy
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// RUN: %dafny /compile:3 "%s" > "%t"
// RUN: %diff "%s.expect" "%t"

include "../../src/Enumerators/Enumerators.dfy"
include "../../src/Wrappers.dfy"

// Dafny iterators do not share any common supertype, so there isn't
// currently any way to write a single adaptor from an arbitrary
// iterator to the Enumerator<T> trait. However, this example should
// serve as a template for adapting any specific iterator.
//
// In practice, a loop that uses a specific iterator type will need to
// declare a "decreases" termination metric, and hence the iterator
// in question will need at least one "yield ensures" clause to
// support such a metric. The same is true for adapting a specific iterator
// to the Enumerator<T> trait in order to implement the Decreases() function.
module IteratorAdaptorExample {

import opened Enumerators
import opened Wrappers

iterator RangeIterator(start: int, end: int) yields (element: int)
requires start <= end
// This is necessary in order to prove termination via Decreases()
yield ensures element - start + 1 == |elements|
// This is necessary to prove the Repr <= old(Repr) post-condition
// of Next(). Iterators that instantiate and "hand off" objects
// will need a weaker post-condition.
yield ensures _new == {};
robin-aws marked this conversation as resolved.
Show resolved Hide resolved
ensures |elements| == end - start
{
for i := start to end
invariant i - start == |elements|
invariant _new == {}
{
yield i;
}
}

class RangeEnumerator extends Enumerator<int> {

const iter: RangeIterator
var remaining: nat

constructor(start: int, end: int)
requires start <= end
ensures Valid()
ensures fresh(Repr)
{
iter := new RangeIterator(start, end);
remaining := end - start;
enumerated := [];

new;

Repr := {this, iter};
robin-aws marked this conversation as resolved.
Show resolved Hide resolved
}

predicate Valid()
reads this, Repr
ensures Valid() ==> this in Repr
decreases Repr, 0
{
&& this in Repr
&& iter in Repr
&& iter._modifies + iter._reads + iter._new == {}
robin-aws marked this conversation as resolved.
Show resolved Hide resolved
&& iter.Valid()
&& remaining == (iter.end - iter.start) - |iter.elements|
}

predicate method HasNext()
requires Valid()
reads this, Repr
decreases Repr, 2
ensures HasNext() ==> Decreases() > 0
{
remaining > 0
}

method Next() returns (element: int)
requires Valid()
requires HasNext()
modifies Repr
decreases Repr
ensures Valid()
ensures Repr <= old(Repr)
ensures Decreases() < old(Decreases())
ensures enumerated == old(enumerated) + [element]
{
var more := iter.MoveNext();
element := iter.element;
enumerated := enumerated + [element];
remaining := remaining - 1;
}

function Decreases(): nat
reads this, Repr
requires Valid()
decreases Repr, 1
{
remaining
}
}
}
2 changes: 2 additions & 0 deletions examples/Enumerators/IteratorAdaptor.dfy.expect
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

Dafny program verifier finished with 12 verified, 0 errors