Skip to main content

Control Structures

Control Structures define how a sequence of expressions is evaluated.

Overview​

The flow of a program is controlled by control structures. Control structures are expressions that define how a sequence of expressions is evaluated.

There are three kind of control structures:

  • Sequential: Block, the most common where expressions are evaluated one after the other
  • Conditional If, Which or Match, where expressions are evaluated depending on the value of a condition or the structure of a value
  • Iterative Loop or FixedPoint, where expressions are evaluated repeatedly

Sequential Control Structure​

Block(expr-1, ...expr-n)

A ["Block"] expression is a sequence of expressions that are evaluated sequentially.

A new scope is created for the ["Block"] expression. The scope is destroyed when the ["Block"] expression is finished evaluating. This means that variables defined in the ["Block"] expression are not accessible outside of the ["Block"] expression.

The value of the ["Block"] expression is the value of the last expression expr-n.

If one of the expression in the block is a ["Return"] expression, a ["Break"] expression or a ["Continue"] expression, no more expressions are evaluated and the value of the ["Block"] is this expression.

["Block"] expressions can be nested as necessary. Scoping is lexical: a nested scope — an inner ["Block"], an ["If"] branch, or a ["Loop"] body — can read the enclosing block's variables, and assigning to a variable declared in an enclosing block updates that binding. A ["Declare"] in the inner block instead introduces a new variable that shadows the outer one for the duration of the inner block.

["Block", ["Assign", "c", 5], ["Multiply", "c", 2]]


// ➔ 10

Translating simultaneous assignments​

Block is sequential: a later assignment observes changes made by every earlier expression. A source language with simultaneous action tuples must therefore snapshot all right-hand sides before committing any left-hand side.

For example, if (a → 1, b → a + 1) means that b reads the value of a from before the tuple, translate it as:

["Block",
["Assign", "_next_a", 1],
["Assign", "_next_b", ["Add", "a", 1]],
["Assign", "a", "_next_a"],
["Assign", "b", "_next_b"]]

The temporary names must be fresh. The first pass evaluates every right-hand side against the old state; the second pass commits the results. Commit order then does not matter because no temporary depends on a newly assigned value.

["Block",
["Declare", "counter", "integer"],
["Assign", "counter", 0],
["Loop",
["Block",
["If", ["Not", ["Less", "counter", 5]], ["Break"]],
["Assign", "counter", ["Add", "counter", 1]]
]
],
"counter"
]


// ➔ 5

Conditional Control Structure​

If(condition, expr-1)

If the value of condition is the symbol True, the value of the ["If"] expression is expr-1. Without an else-branch there is nothing to select when condition is False, and the value is the Missing marker.

["If", "False", 5]
// ➔ Missing

If(condition, expr-1, expr-2)

If the value of condition is the symbol True, the value of the ["If"] expression is expr-1, otherwise expr-2.

Only the selected branch is evaluated. The branch that is not selected is dead code: if it contains an error, that error never reaches the value.

["If", "True", 5, ["Divide", "x"]]
// ➔ 5 — the malformed branch is never evaluated

Only evaluation skips the branch. The boxed expression still carries the diagnostic — the JSON above boxes as ["If", "True", 5, ["Divide", "x", ["Error", "'missing'"]]] — so the error is still reportable to the user.

Here's an example of a function that returns the absolute value of a number:

["Function", ["If", ["Greater", "n", 0], "n", ["Negate", "n"]], "n"]

["If"] expressions can be nested as necessary.

Which(condition-1, expr-1, ...condition-n, expr-n)

The value of the ["Which"] expression is the value of the first expression expr-n for which the corresponding condition condition-n is True. When no condition is True — including the no-operand case ["Which"] — there is nothing to select, and the value is the Missing marker.

Only the selected expression is evaluated. The expressions that are not selected are dead code: an error in one of them never reaches the value. ["Which", "False", ["Divide", "x"], "True", 7] evaluates to 7. As with ["If"], only evaluation skips them; the boxed expression still carries the diagnostic.

\begin{cases} x & \text{if } x > 0 \\ -x & \text{if } x < 0 \\ 0 & \text{otherwise} \end{cases}
$$$\begin{cases} x & \text{if } x > 0 \\ -x & \text{if } x < 0 \\ 0 & \text{otherwise} \end{cases}$$
["Block",
["Assign", "n", -10]
["Which", ["Greater", "n", 0], "n", ["Negate", "n"], "n"]
]
// ➔ 10

A ["Which"] expression is equivalent to the following ["If"] expression:

["If", ["Equal", condition-1, "True"], expr-1,
["If", ["Equal", condition-2, "True"], _expr-2,
... ["If", ["Equal", condition-n, "True"],
expr-n,
"Missing"
]
]
]

A ["Which"] expression is equivalent to a switch statement in JavaScript or the Which[] function in Mathematica.

When(expr, condition)

Returns the value of expr when condition evaluates to True, and Missing — the absent-value marker, the same value a Which with no matching clause and an If with no else-branch give — when condition evaluates to False. When condition cannot be determined, the expression holds unevaluated.

The type of a When expression admits the absent case: When(5, x > 0) has type integer | missing, and a list that holds a restricted element has type list<integer | missing>. Only a restriction whose condition is the literal True keeps the bare type of expr. A consumer that tests type.matches("number") must set the missing member aside first, as it must for a Which with no default clause.

["When"] is the AST head produced by restriction-brace syntax: expr\{cond\} parses to ["When", expr, cond]. It is also useful directly for masking values where a predicate does not hold. The braces may be separated from the base expression by spacing commands ((1-t)^2(1+2t)\ \{t\ge0\}\{t\le1\} attaches both restrictions).

["When", ["Square", "x"], ["Greater", "x", 0]]
// Evaluates to x^2 when x > 0, Missing otherwise.

Stacked restrictions canonicalize to a single When with an And predicate:

["When", ["When", "x", ["Greater", "x", 0]], ["Less", "x", 10]]
// Canonicalizes to:
["When", "x", ["And", ["Greater", "x", 0], ["Less", "x", 10]]]

Downstream simplification, interval intersection, and compilation operate on the canonical form, so source variants (stacked braces or a single brace with \wedge) are interchangeable.

When compiled, When(e, cond) emits a ternary whose masked branch is the absence marker of the value's domain: (cond ? e : NaN) for a number, on every target, and on JavaScript (cond ? e : undefined) for a value that is provably not a number, such as a string, so that a compiled IsMissing agrees with the interpreter. This makes When suitable for plot-domain masking.

Match(subject, case-1, ...case-n)

Structural pattern matching: the value of the ["Match"] expression is the value of the first case whose pattern matches the structure of subject (and whose guard, if any, is True).

Each case is a ["MatchCase"] expression:

  • ["MatchCase", pattern, body]
  • ["MatchCase", pattern, guard, body]

The pattern uses the same wildcards as patterns and rules: "_n" captures one value, "__n" a sequence, "___n" an optional sequence, and "_" matches anything without capturing. The guard and body reference captures by their bare name ("n" for "_n").

["Match", ["List", 3, 4],
["MatchCase", ["List", "_a", "_b"], ["Add", "a", "b"]]
]
// ➔ 7

Unlike ["Which"], which evaluates boolean conditions and stays unevaluated while a condition cannot be decided, ["Match"] inspects the structure of the subject and always selects a case: a symbolic subject that is not structurally equal to a pattern falls through to the next case.

["Match", "x",
["MatchCase", 0, "'zero'"],
["MatchCase", "_", "'other'"]
]
// ➔ "other" — x could be 0 semantically, but is not structurally 0

In Epsil, the wildcard case may also be spelled otherwise: match x { 0 => "zero"; otherwise => "other" }. It is a contextual synonym for a bare _ pattern — it lowers to the same "_" node, may take a guard (otherwise if c => …), and binds nothing. Only the whole-pattern spelling is special: inside a structured pattern, otherwise is an ordinary identifier (and therefore a capture).

A guard must evaluate to True for the case to be selected; False or an undecidable guard falls through to the next case:

["Match", 5,
["MatchCase", "_n", ["Greater", "n", 0], "'positive'"],
["MatchCase", "_", "'other'"]
]
// ➔ "positive"

Two auxiliary heads may appear inside a pattern:

  • ["Pin", expr] matches the value of expr, evaluated when the match is performed, rather than its structure. Use it to match against a constant (["Pin", "Pi"]) or the current value of a variable.
  • ["Alternatives", p-1, ..., p-n], as a case's pattern, matches if any alternative matches. The alternatives share the case's guard and body and must not contain named captures.
["Match", 2,
["MatchCase", ["Alternatives", 1, 2, 3], "'small'"],
["MatchCase", ["Pin", "Pi"], "'pi'"],
["MatchCase", "_", "'big'"]
]
// ➔ "small"

Range patterns. A two-operand ["Range", lo, hi] at the top level of a case's pattern — or of one of its ["Alternatives"] — is an inclusive numeric membership test rather than a structure to match: the case is selected if the subject is a real number between lo and hi.

["Match", 5,
["MatchCase", ["Range", 1, 10], "'in range'"],
["MatchCase", "_", "'out of range'"]
]
// ➔ "in range"

Both endpoints are included, and the endpoint comparisons use the engine's tolerance. The bounds must be numeric literals; Infinity and -Infinity are allowed, so ["Range", 0, {"num": "+Infinity"}] means "any non-negative number". A Range that is not a well-formed range pattern — a symbolic bound, or a third step operand — is not a membership test and keeps its ordinary structural meaning. (In Epsil, those spellings are reported as parse diagnostics instead.)

A subject that is not a number falls through: a symbol (including a constant such as "Pi", which is not a literal), an operator expression, a string, a collection, a complex number, and NaN all fail a range case.

["Match", "Pi",
["MatchCase", ["Range", 1, 10], "'in range'"],
["MatchCase", "_", "'not a number literal'"]
]
// ➔ "not a number literal"

The consequence is that a literal Range value can no longer be matched structurally at the top level of a pattern. To compare a subject against a Range value, use a pin — ["Pin", ["Range", 1, 10]], which compares values and is not re-read as a membership test. A Range nested inside a List, Tuple or Dictionary pattern also keeps its ordinary structural meaning.

In Epsil, a range pattern is written lo..hi: match n { 0..9 => "digit"; _ => "more" }.

Error subjects. ["Match"] also decides when the subject is an error — it is the construct for rescuing a failed computation. Matching stays total, but an error is never handed to a pattern that would pretend the failure has a shape: literal, range, pin, and structural cases all fail against an error subject, and it falls through to "_" or to a capture.

["Match", ["Ln", "'a'"],
["MatchCase", 0, "'zero'"],
["MatchCase", ["List", "_a"], "'a list'"],
["MatchCase", "_", "'rescued'"]
]
// ➔ "rescued"

An explicit ["Error", ...] pattern is the exception: it destructures the error, binding its payload. This is the idiomatic rescue:

["Match", ["Ln", "'a'"],
["MatchCase", ["Error", "_code"], "code"],
["MatchCase", "_", "'no error'"]
]
// ➔ ["ErrorCode", "'incompatible-type'", "'number'", "'string'"]

A guard can inspect the subject with IsError or Type:

["Match", ["Ln", "'a'"],
["MatchCase", "_v", ["IsError", "v"], "'failed'"],
["MatchCase", "_", "'ok'"]
]
// ➔ "failed"

The ["ErrorTrace"] breadcrumb a bubbled error carries is stripped before matching, so a pattern only ever sees the error's code and context.

If no case matches, the value of the expression is ["Error", "'match-no-case'", subject] — an ordinary error value.

Matches over constant cases are dispatched in constant time, and fixed-shape List/Tuple/Dictionary patterns are matched without invoking the general pattern matcher. When compiled to JavaScript, constant cases emit a comparison chain or a switch statement, and fixed-shape destructuring is supported; symbolic patterns (such as ["Add", "_a", 1]) cannot be compiled and fail with an error rather than producing incorrect code.

In Epsil, ["Match"] is written with the match keyword: match x { 0 => "zero"; 1 | 2 | == Pi => "small or pi"; _ => "other" }.

Loops​

Loop(body)

Repeatedly evaluate body until the value of body is a ["Break"] expression, or a ["Return"] expression.

  • ["Break"] exits the loop immediately. The value of the ["Loop"] expression is the value of the ["Break"] expression.
  • ["Return"] exits the loop and returns the value of the ["Return"] expression.

To exit the loop, a ["Break"] or ["Return"] expression must be evaluated.

Loop with only a body argument is equivalent to a while(true) in JavaScript or a While[True, ...] in Mathematica.

Loop(body, element-1, element-2, ...)

Iterates over one or more ["Element", _name_, _collection_] clauses, evaluating body once per combination, for effect. The value of the ["Loop"] expression is Nothing — unlike Comprehension (see below), Loop does not accumulate or return a list of results.

Bindings are evaluated as nested loops, outermost = first Element clause. Later clauses see earlier bindings in scope, so a clause's collection can depend on a name bound by an earlier clause (dependent collections work).

["Loop", ["Print", ["Square", "_"]], ["Element", "_", ["Range", 5]]]
// ➔ 1 4 9 16 25 (printed); the Loop expression evaluates to Nothing

Loop with a body and a single Element clause is equivalent to a forEach() in JavaScript. It is somewhat similar to a Do[...] in Mathematica.

Inside body:

  • ["Break"], optionally with a value, exits the loop immediately. The value of the ["Loop"] expression is the value of Break, or Nothing if none was provided.
  • ["Continue"] skips to the next iteration.
  • ["Return"] exits the loop and propagates out of the enclosing ["Function"] expression.

Bound names do not leak into the enclosing scope.

FixedPoint(body, initial-value)

FixedPoint(body, initial-value, max-iterations)

Assumes body is an expression using an implicit argument _.

Apply body to initial-value, then apply body to the result until the result no longer changes.

To determine if a fixed point has been reached and the loop should terminate, the previous and current values are compared with Equal.

Inside body, use a ["Break"] expression to exit the loop immediately or Return to exit the enclosing ["Function"] expression.

LaTeX Syntax for Control Structures​

Control structures are expressed in LaTeX using keywords. Each keyword can be written three equivalent ways, which may be mixed freely:

  • \keyword{if} — the preferred form: stays in math mode and renders with symmetric keyword spacing.
  • \text{if} — the conventional form.
  • \operatorname{if} (or \mathrm{if}) — operator-name spelling.

Multi-word keywords are a single token, e.g. \keyword{for all}. (\keyword{...} requires the rendering environment to define the \keyword command; \text{...} and \operatorname{...} render everywhere.)

Inline If​

\keyword{if} x > 0 \keyword{then} x \keyword{else} -x

Parses to ["If", ["Greater", "x", 0], "x", ["Negate", "x"]].

The else branch is optional.

where Bindings​

x^2 + y^2 \keyword{where} x \coloneq 3,\; y \coloneq 4

Parses to a ["Block"] with variable declarations, assignments, and the body expression as the return value.

for Loops​

\keyword{for} i \keyword{from} 1 \keyword{to} 10 \keyword{do} i^2

Parses to ["Loop", ["Power", "i", 2], ["Element", "i", ["Range", 1, 10]]].

This is an imperative loop, evaluated for effect: its value is Nothing.

for Comprehensions​

A trailing for clause produces a list comprehension:

(x, y) \keyword{for} x = [1...3], y = [1...x]

Parses to:

["Comprehension",
["Tuple", "x", "y"],
["Element", "x", ["Range", 1, 3]],
["Element", "y", ["Range", 1, "x"]]]

The trailing for keyword binds looser than , and =, so the body expression ((x, y) above) is parsed before the keyword fires, and the bindings are parsed as comma-separated name = expr pairs after it.

Multiple bindings produce a Cartesian product (or a dependency-shaped iteration when later bindings reference earlier ones). See the Comprehension definition in the Collections reference for full semantics.

Restriction Braces​

A trailing \{cond\} after an expression masks the value by a predicate:

f(x)\left\{0 < x < 2\right\}

Parses to ["When", ["f", "x"], ["Less", 0, "x", 2]].

When the condition is True, the expression evaluates to its left operand; when False, it evaluates to Missing. This is distinct from a set literal (standalone \{1, 2, 3\} continues to parse as a Set); the disambiguation is positional — trailing braces after a complete expression attach as a When restriction.

Stacked restrictions chain and canonicalize:

x\left\{x > 0\right\}\left\{x < 10\right\}

Parses to ["When", "x", ["And", ["Greater", "x", 0], ["Less", "x", 10]]]. The serializer round-trips this canonical form back to stacked braces.

Semicolon Blocks​

Semicolons (; or \;) act as statement separators to build Block expressions:

x \coloneq 3;\; x^2 + 1

Parses to ["Block", ["Declare", "x"], ["Assign", "x", 3], ["Add", ["Power", "x", 2], 1]].

Controlling the Flow of Execution​

To exit a function, use Return.

To control the flow of a loop expression, use Break and Continue.

Return(value)

Interrupts the evaluation of a ["Function"] expression. The value of the ["Function"] expression is value.

The ["Return"] expression is useful when used with functions that have multiple exit points, conditional logic, loops, etc...

Here's a contrived example of a function that returns the sign of a number:

[
"Function",
[
"Block",
["If", ["Greater", "x", 0], ["Return", 1]],
["If", ["Less", "x", 0], ["Return", -1]],
0
],
"x"
]

Break()

Break(value)

Break is a registered operator. When in a loop, exit the loop immediately. The value of the enclosing ["Loop"] expression becomes value, or Nothing if not provided.

Outside a loop, Break is inert.

Continue()

Continue is a registered operator. When in a loop, skip to the next iteration of the loop. Continue takes no argument.

Outside a loop, Continue is inert.