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

RFC: for loops #4

Closed
wants to merge 2 commits into from
Closed
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
207 changes: 207 additions & 0 deletions for-loops.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
- Feature Name: for-loops
- Start Date: 2021-01-20

# Summary
[summary]: #summary

This RFC gives a concrete proposal for adding `for` loops to Dafny.

# Motivation
[motivation]: #motivation

Dafny has only one kind of loop, `while` loops (with either one guard or with a set of guards). Some tasks
where other languages require a loop can be done in Dafny with the aggregate `forall` statement, avoiding
iteration altogether. Still, some situations call for a loop with a loop index and iteration pattern just
like what a typical `for` loop provides. This RFC
gives a concrete proposal, so that design aspects of `for` loops in the context of Dafny can be discussed.

Two central design points are:
- the half-open interval of values for which the loop body is performed (e.g., `0 <= i < a.Length`) and
the closed interval that appears in the loop invariant (e.g., `0 <= i <= a.Length`)
- what should be the possible increments of the loop index (e.g., `+1`, `-1`, `+2`)

Choose a reason for hiding this comment

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

In my opinion, increments other than +1 and -1 are a non-issue. It happens rarely and when it does, we simply use a while loop.


Requests for `for` loops in Dafny come up from time to time. Here is a record of
[one such request](https://github.com/dafny-lang/dafny/issues/7).

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

The `for` loop has the following syntax:

```
ForLoop ::=
"for" Ident [":" Type] ":=" Expression ("to" | "downto") Expression
{ InvariantDecl | ModifiesDecl }
[ Body ]
```

The scope of the loop-index variable introduced by `Ident` is the `for` loop itself. Its name must not be
the same as any local variable introduced in the same block scope as the `for` loop itself.
The loop index is immutable in the `Body`.

The type of loop index is `Type`, if present; otherwise, it is inferred from the types of the
two expressions, which must be assignable to the loop index. In either case, the type must be an
integer-based type (`int`, some subset type based on an integer-based type, or a `newtype` based
on an integer-based type; note that bitvector types, `ORDINAL`, and `real`-based types are not
allowed).

The loop index is automatically updated by the `for` loop. This guarantees that the loop will always
terminate. Therefore, the `for` loop does not allow user-specified `decreases` clauses.

The meaning of the loop is defined in terms of existing language features. Ignoring the scope of the
loop index (which was explained above), the "up" loop

```
for i: I := lo to hi
invariant J
modifies W
{
Body
}
```

has the meaning

``` dafny
var _lo := lo;
var _hi := hi;
assert _lo <= _hi; // this is Dafny-like

Choose a reason for hiding this comment

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

What does // this is Dafny-like mean?

Do we really need to assert _lo <= _hi? This could be needlessly restrictive: I think there are some situations where lo might already be bigger than hi, and the intended behavior is simply to do no loop iterations at all if that's the case.

Choose a reason for hiding this comment

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

I definitely agree. It is really useful if the loop simply does nothing when lo > hi. Note: I understand it will do nothing when lo == hi, the interval being half-open.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I thought this would be "Dafny-like" because Dafny already imposes the restriction lo <= hi in an expression s[lo..hi].

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What is an example when allowing lo > hi is useful? Is it a frequent occurrence?

Choose a reason for hiding this comment

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

I think lo == hi (which should result in 0 iterations) is very common, and lo > hi is less common, but here's an example: Say I write a method to resize an array from oldsize to newsize (and I don't know which one is bigger), and if newsize is bigger, the new elements should be initialized to some default value, so after copying the elements, maybe I want to be "smart" and write a loop like for i in [oldsize..newsize] { newarray[i] = default } and run that loop no matter whether oldsize or newsize is bigger, relying on the fact that if oldsize is bigger, 0 iterations will be performed. But I guess examples like this one can be easily rewritten so that they also work if lo <= hi is required by Dafny, I can't think of an example right now where such rewriting would be too cumbersome.

Copy link
Collaborator Author

@RustanLeino RustanLeino Apr 12, 2021

Choose a reason for hiding this comment

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

Thanks for the thoughts. For this particular example, one can also use a forall statement:

forall i | 0 <= i < newsize {
  newarray[i] := if i < oldsize then oldarray[i] else default;
}

assert forall _i :: _lo <= _i <= _hi ==> /* _i is a value of the type of I */;
var i := _lo;
while i < _hi
invariant _lo <= i <= _hi // automatically generated
invariant J
modifies W
{
Body
i := i + 1;
}
```

The "down" loop

```
for i: I := hi downto lo
invariant J
modifies W
{
Body
}
```

has the meaning

``` dafny
var _lo := lo;
var _hi := hi;
assert _lo <= _hi;
assert forall _i :: _lo <= _i <= _hi ==> /* _i is a value of the type of I */;
var i := _hi; // this is different from the "up" loop
while _lo < i // this is different from the "up" loop
invariant _lo <= i <= _hi
invariant J
modifies W
{
i := i - 1; // this is different from the "up" loop
Body
}
```

Note that the "up" loop performs a post-increment of `i`, whereas the "down" loop
performs a pre-decrement of `i`. This is common in computer science.

Note that in both cases, `Body` is never executed with `i` having the value `hi`.
This is likely to lead to confusion for users who are used to adding those
ugly `- 1`'s. Programs in the proposed design will look prettier, and also offer
a more direct translation to pretty intervals in invariants.

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

The proposal introduces 3 new keywords: `for`, `to`, `downto`. Alternatives are:

- Make `to` and `downto` context-sensitive keywords. That is, reserve them only in
`for` loops as shown by the grammar above.

- Don't support "down" loops. This would mean `downto` does not need to be reserved.

- Use `..` instead of `to` for "up" loops. Without the invention of new syntax
for intervals written in the "opposite order", this excludes "down" loops.
If `..` is used in the syntax, then it would also seem natural to write `in`
instead of `:=`, like this:

```
for i in lo .. hi
```

# Prior art
[prior-art]: #prior-art

Most popular languages have some form of `for` loop. In C-like languages, the first
line of the proposed `for` loops would look like:

``` C
for (int i = lo; i < hi; i++)
```

ignoring issues of types, issues of scopes, the issue of re-evaluating `hi`, and
the issue of prohibiting other updates of `i`.

"Down" loops written in C look like:

``` C
for (int i = hi; lo <= --i; )
```

The Eiffel programming language supports loop invariants. It has an `until`
condition, which says when to terminate, rather than the negated termination
condition of standard `while` loops and C's `for` loop. The update of the
loop index is provided in the program text, as in C and as for common `while` loops.

Why3 supports the OCaml-like `for` loop

```
for i = lo to hi do invariant { J } Body done
```

It also has a `downto` variant. In this form, the upper bound `hi` is still
inclusive; that is, the automatically generated loop invariant is

```
lo <= i <= hi + 1
```

Another unique and verification-friendly feature of Why3's `for` loops
is that if the values of `lo` and `hi` are such that the loop body will not
iterate at all, then the entire loop is skipped, including the checking of
the invariant on entry to the loop.

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

- Are "down" loops important to include in the language?
- Is it okay to exclude loops there the loop index changes by something other than `+1` or `-1`?
- Is the `..` token to be preferred over the `to` keyword?
- If `..` is used and "down" loops are important, should there be an additional keyword like `reverse`
added to the syntax somewhere?
- `while` loops are nice in teaching. They make it clear where the loop index is declared and what
values it may have. You can also see the update of the loop index, which is useful if you want
to use weakest preconditions to compute the needed values of variables near the end of the loop
body. Would the availability of `for` loops lead teachers and students astray by luring them
into using `for` loops before loop invariants are clearly understood?

# Future possibilities
[future-possibilities]: #future-possibilities

Another kind of loop that is missing from Dafny are `foreach` loops, which loop over all
values of some collection or `iterator`. If such were ever introduced, a syntax like

```
foreach x in S
invariant J
{
Body
}
```

is still available.
Copy link

@backtracking backtracking Jan 21, 2021

Choose a reason for hiding this comment

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

FYI, Why3 has a for each loop with syntax

for <pat> in <expr> with <Module> do invariant {...} variant {...} <body> done

It roughly desugars to

let it = <Module>.create <expr> in
try while true do invariant {...} variant {...} let <pat> = <Module>.next it in <body> done
with <Module>.Done -> () end

which means that <Module> is providing

  • a function create (to create the iterator from <expr>)
  • a function next (to get the next value)
  • an exception Done (to signal termination of the iteration)

Note: break and continue are honored.

Choose a reason for hiding this comment

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

Regarding the way to specify create and next, you might be interested in this NFM 2016 paper:
https://hal.inria.fr/hal-01281759v2/document

Copy link
Member

Choose a reason for hiding this comment

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

What is the reason not to use just foreach loops (using the for keyword seems fine)?

[lo .. hi] in for i in [lo .. hi] is already in Dafny. That'd allow for arbitrary stepping using sequence comprehensions such as in for x in seq(5, i => i*i).

The versions with just -1 and +1 updates seem restrictive to me.