Laurel User Guide

5.1. Modifies clauses🔗

As previously mentioned, Laurel procedures have a transparent body by default, so callers can reason about the callee's body. This works also when the callee mutates the heap in its body. However, when we make a heap-mutating procedure opaque, and the body is no longer available for reasoning, then the caller must accept the possibility that the entire heap was mutated, meaning that nothing can be proven about the heap any more. This is sound but imprecise, and it makes reasoning about callers of such procedures difficult. To enable heap reasoning after calling opaque heap-mutating procedures, Laurel has modifies clauses.

A modifies clause specifies the heap references that may have been modified by the procedure. Example:

composite Container {
  var value: int
}

procedure bump(c: Container)
  opaque
  modifies c
{
  c#value := c#value + 1
};

procedure caller()
  opaque
{
  var a: Container := new Container;
  var b: Container := new Container;
  var x: int := a#value;
  var y: int := b#value;
  bump(b);
  assert x == a#value;  // holds: only b is in bump's modifies clause
  assert y == b#value   // fails: b is in bump's modifies clause
};

An opaque procedure that writes to an object it has not listed in the modifies clause is rejected, also when this write is done through another procedure call. You can list several references by repeating the clause (modifies c; modifies d), and the wildcard modifies * permits modifying any object — at the cost of telling callers that nothing on the heap is preserved.

Objects allocated with new inside the procedure body are exempt: a freshly allocated object may be modified freely without appearing in the modifies clause, because no caller could hold any prior knowledge about it.

composite Container { var value: int }

procedure makeOne()
  opaque
{
  var c: Container := new Container;
  c#value := 1   // allowed: c is freshly allocated here
};

A modifies clause frames the normal return only. A procedure that can also finish by throwing frames that exit separately, with a throwsOn case's modifies — see the Exceptions section.