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,WhichorMatch, where expressions are evaluated depending on the value of a condition or the structure of a value - Iterative
LooporFixedPoint, 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.
["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 ofexpr, 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 ofBreak, orNothingif 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.