Skip to main content

Evaluation

A program's top-level statements are evaluated sequentially, and the program's value is the last statement's value:

let x = 5
x = x + 3
x
// ➔ 8

No scope is pushed around the whole program: declarations persist across statements, and across cells in a notebook or inputs in a REPL that share one session. Blocks and function bodies still push their own lexical scopes (see Control Flow).

Symbolic by default

Values stay exact unless you ask otherwise. A transcendental of an exact argument stays symbolic —

Ln(2)

evaluates to the symbolic Ln(2) (ln(2)), not a decimal approximation.

Numeric approximation is explicit, via N(expr) — it is a function call, not a language mode:

N(Ln(2))

evaluates to 0.6931471805599453….

Collections: literals are values, pipelines are generators

A collection literal — a list […], set {…}, tuple (…), or dictionary — evaluates its elements when the statement executes. Assigning one to a variable stores a snapshot of the element values:

let xs = []
for k in 1..3 { xs = Join(xs, [k]) }
xs
// ➔ [1, 2, 3]

Lazy collection operatorsRange, Map, Filter, Take, Join — are generators: their operands (bounds, sources, functions) are evaluated when the expression is, but enumeration is deferred until the collection is materialized (displayed, indexed, aggregated, or iterated). A deferred mapping function reads program state at materialization time, like a generator in Python — if it captures a variable that later changes, the materialized elements reflect the later value. To snapshot, force the work to happen where you stand: accumulate through a loop, or apply an eager operation (an aggregate, an index) at the point of definition.

Errors are values

Per Principles, "errors are values": a runtime problem — a type error, an out-of-domain argument, reassigning a const — becomes an Error value embedded in the result, not a thrown exception. A program never throws to its host for a runtime problem.

Parse-time problems are different: a malformed program surfaces as a diagnostic, not as a value. So do the few execution-time problems that are really about the source, not the computation — a gated host pragma, or an #error directive (see below).

Because only the last statement's value is the program's result, an error value produced by an earlier statement would otherwise vanish silently. Each non-final statement that evaluates to an error value therefore also emits a runtime-error diagnostic — for example an indexed assignment (xs[2] = 9, which is rejected: element assignment is not supported), or reassigning a const in the middle of a program.

Pragma security

#env(...) and #navigator(...) read state from the host process (or the browser) at parse time. Because a notebook document can be shared or opened in an unfamiliar environment, both are gated off by default:

#env("HOME")

by default produces a host-pragma-disabled diagnostic and no host read — the pragma evaluates to Nothing. A host can opt back in and let #env/ #navigator read as documented in Pragmas.

The benign pragmas — #line, #column, #url, #filename, #date, #time — always work; they don't read anything sensitive from the host.

#error(...) never crashes the host embedding the program: it becomes an error-directive diagnostic, so a single bad cell is contained.

Interruptibility

A host can give an evaluation an explicit time budget, and independent count-based bounds on iteration and recursion depth. A breached limit becomes an error value (or an evaluation-canceled diagnostic when it happens in a non-final statement) — see Execution for how a host sets one.

These limits are cooperative. A browser that evaluates untrusted or potentially unbounded programs should run Epsil in a Web Worker it can terminate from the outside. See Execution Constraints for the complete cancellation model.