Laurel User Guide

4.3. Statements are expressions🔗

Laurel has one syntactic category for statements and expressions. In practice:

  • a block evaluates its items left to right;

  • every item but the last is evaluated for its effect and its value discarded;

  • the block's value is the value of its last item;

  • an assignment evaluates to the assigned value;

  • an if with an else can produce a value; without one, the missing branch produces nothing, so the form is only usable where its value is discarded.

procedure example(flag: bool): int
{
  var x: int := 0;
  return {
    x := 10;
    if flag
      then { x := x + 1; x }
      else { x := x + 2; x }
  }
};

The nested block evaluates to 11 or 12.

A local declaration is lexically scoped to its block and does not escape it. A declaration without an initializer introduces an arbitrary value of its type — not a null, and not an "unbound" marker, so a front end for a language with unbound locals must encode that state separately.

procedure declarations()
  opaque
{
  var x: int;        // arbitrary int
  var y: int := 4;   // exactly 4
  assert y == 4
};

One current restriction is worth knowing: a block used as a value should not contain a non-final loop, return, or exit. Keep control-flow constructs in blocks that are used in statement position.

4.3.1. Evaluation order🔗

Because an operand may itself assign or call, the order operands run in is observable, so it is fixed: evaluation is left to right everywhere. That covers the items of a block and the arguments of a call — if one argument mutates state that a later one reads, the later one sees the mutation, and an earlier one does not.

Three constructs deliberately do not evaluate all of their operands:

  • && and || evaluate their right operand only when the left one does not already decide the result, and a ==> b evaluates b only when a holds;

  • if evaluates only the branch it takes.

That is the whole of it — there is no other laziness, and nothing is reordered or evaluated more than once, with one exception: an update operator applied to a field evaluates the receiver twice, so keep the receiver side-effect-free (see Assignment and update operators).

The eager & and | exist precisely so that the choice is yours: they evaluate both operands whatever the left one says. Where the operands are pure the two spellings agree, and the short-circuiting pair is the one to reach for when the right operand may fail an obligation — x != 0 && 10 / x > 1 is well-formed, while x != 0 & 10 / x > 1 is not.