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
["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, otherwise Nothing.

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.

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.

\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,
"Nothing"
]
]
]

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 Undefined when condition evaluates to False. When condition cannot be determined, the expression holds unevaluated.

["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, Undefined 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 to JavaScript or GLSL, When(e, cond) emits a ternary (cond ? e : NaN). 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

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"

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 Cortex, ["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 Undefined. 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.