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
ifwith anelsecan 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, anda ==> bevaluatesbonly whenaholds; -
ifevaluates 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.