Laurel User Guide

6.6. Exceptional frames🔗

modifies at the top level frames the normal return. A modifies inside a case frames that case's throwing path: it names the locations that may change when the procedure throws for that reason.

composite Cell {
  value: int
}
composite Err {}

procedure doWork(c: Cell, logCell: Cell, fail: bool) returns (r: int)
  throws (e: Err)
  opaque
  modifies c
  throwsOn fail {
    modifies logCell
  }
{
  if fail then {
    logCell#value := 1;
    var e: Err := new Err;
    throw e
  };
  c#value := 42;
  r := 0
};

Only c may change on the normal path, and only logCell when it throws because fail. All frames are checked against the body and assumed at call sites, a case's modifies accepts the same targets the top-level one does including field-granular ones (modifies logCell#value), and an object allocated inside the body is exempt from all of them.

Because each case carries its own frame, a procedure that writes different locations for different reasons can say so — one case each — rather than declaring only their union. A caller that rules out one case can then conclude the others' targets are unchanged.

Two things to keep in mind. Guards are not checked for overlap: if two can hold at once both frames apply, and a caller gets only their intersection, which is rarely what was meant. And framing a throwing path means naming its condition, so a procedure whose throwing condition cannot be stated — one that propagates a callee's exception, say — has to leave that path unframed by stating no case.