Learn · from docs/tutorial.md in the eLucid8 repository

Contents

eLucid8 tutorial

Also published as a shared document: eLucid8 Tutorial. The language as it stands on 2026-10-03 (syntax v0.1 with streams, functions, clocks, effects, the T4 rule, and aggregates over ranges). Every example below is checked by a test (packages/evaluator/test/tutorial.test.ts) and runs as written in the playground.

eLucid8 is a Lucid-family language. A program does not run from top to bottom: it defines values over contexts, and you ask for one value at one context. The runtime then computes only what that answer depends on: this is demand-driven evaluation, or eduction.

How to read the examples

Each example is a complete program. Its comment lines (--) also say what data it uses and what it gives:

To try one in the playground: paste the program, set each source to "formula" with the given text, choose the value and coordinates, and press Run query. Tick trace to see why each value was computed.

1. Values over contexts

A dimension is a named logical axis. A source is input data over some dimensions. A value is defined by an equation over its dimensions.

-- A first program: one dimension, one source, one value.
dimension t : int
source x @ (t)
double @ (t) = 2 * x
-- data x = #t + 10
-- expect x {t = 0} = 10
-- expect double {t = 3} = 26

Read double @ (t) = 2 * x as "at every t, double is twice x at the same t". Inside an equation, a name such as x means that value at the current context.

#t is the current coordinate on dimension t:

-- The current coordinate, #t.
dimension t : int
square @ (t) = #t * #t
-- expect square {t = 7} = 49

A dimension may declare a range. Boundary rules (section 6) and tiling use it. (Today it does not stop you from querying outside it; whether it should is an open question.)

-- Two dimensions, with ranges.
dimension x : int range 0..9
dimension y : int range 0..9
distance @ (x, y) = #x + #y
-- expect distance {x = 3, y = 4} = 7

2. Looking elsewhere: @

v @ {d = e} is v at another context: the current one with coordinate d replaced by e.

-- Reading a value at another context.
dimension t : int
source x @ (t)
previous @ (t) = x @ {t = #t - 1}
change @ (t) = x - x @ {t = #t - 1}
first_one @ (t) = x @ {t = 0}
-- data x = #t * #t
-- expect previous {t = 5} = 16
-- expect change {t = 5} = 9
-- expect first_one {t = 5} = 0

Projection. A value can be used in a context with more dimensions than it declares; the extra coordinates are simply ignored. Here offset depends on t only, but is used inside a value over (t, x):

-- Projection: a value over (t) used inside a value over (t, x).
dimension t : int
dimension x : int range 0..9
source reading @ (t, x)
source offset @ (t)
corrected @ (t, x) = reading - offset
-- data reading = 100 * #t + #x
-- data offset = #t
-- expect corrected {t = 2, x = 5} = 203

The opposite is an error: a value cannot use another that needs a coordinate it does not have, unless the equation supplies it.

-- A missing dimension is rejected before anything runs.
dimension t : int
dimension x : int range 0..9
source reading @ (t, x)
total @ (t) = reading
-- data reading = #x
-- expect compile error: x

Supplying the coordinate explicitly fixes it:

-- Supplying the missing coordinate.
dimension t : int
dimension x : int range 0..9
source reading @ (t, x)
at_origin @ (t) = reading @ {x = 0}
-- data reading = 100 * #t + #x
-- expect at_origin {t = 3} = 300

3. Outcomes: values, Undefined and Error

Every value at every context has exactly one outcome: an ordinary value, Undefined (there is no value there), or an Error (something failed, and the error names where).

-- Undefined: a source with no value at some contexts.
dimension t : int
source x @ (t)
plus_one @ (t) = x + 1
-- data x = if #t == 2 then undefined else #t
-- expect plus_one {t = 1} = 2
-- expect plus_one {t = 2} = Undefined

Operators are strict: an Undefined or Error operand makes the result Undefined or an Error. Division by zero is an Error, and so is any result that is infinite or not a number:

-- Errors propagate, and name their origin and its context.
dimension t : int
source x @ (t)
ratio @ (t) = 10 / x
scaled @ (t) = ratio * 2
-- data x = #t
-- expect ratio {t = 5} = 2
-- expect ratio {t = 0} = Error: division by zero
-- expect scaled {t = 0} = Error via ratio (t = 0): division by zero

When an operator meets both, Error wins over Undefined, so a failure is never hidden behind a missing value.

-- Error over Undefined.
dimension t : int
source a @ (t)
source b @ (t)
both @ (t) = a + b
-- data a = undefined
-- data b = 1 / 0
-- expect both {t = 0} = Error via b (t = 0): division by zero

4. Conditionals and logic

if c then a else b evaluates the condition, then only the branch taken. The other branch is never demanded, so an Error there does no harm:

-- Only the branch taken is evaluated.
dimension t : int
source x @ (t)
safe @ (t) = if x == 0 then 0 else 10 / x
-- data x = #t
-- expect safe {t = 0} = 0
-- expect safe {t = 5} = 2

and and or short-circuit from left to right: false and X is false without evaluating X.

-- Short-circuit and/or.
dimension t : int
source x @ (t)
in_band @ (t) = x > 0 and 100 / x > 10
small @ (t) = x == 0 or 1 / x < 0.5
-- data x = #t
-- expect in_band {t = 0} = false
-- expect in_band {t = 5} = true
-- expect small {t = 0} = true
-- expect small {t = 4} = true
-- expect small {t = 1} = false

Comparisons give true or false; not negates. Values can also be text, in double quotes:

-- Booleans and text.
dimension t : int
size @ (t) = if #t > 3 then "big" else "small"
small @ (t) = not (#t > 3)
-- expect size {t = 5} = "big"
-- expect size {t = 1} = "small"
-- expect small {t = 3} = true
-- expect small {t = 5} = false

5. Streams: first, next and fby

Along an integer dimension, Lucid's stream operators relate positions to each other. With origin 0 (or the start of the dimension's range):

-- first, next and fby.
dimension t : int
source x @ (t)
start @ (t) = first.t x
ahead @ (t) = next.t x
delayed @ (t) = 0 fby.t x
-- data x = 10 * #t
-- expect start {t = 4} = 0
-- expect ahead {t = 4} = 50
-- expect delayed {t = 0} = 0
-- expect delayed {t = 4} = 30

fby makes recursive definitions well founded: a value can refer to itself, one step back.

-- A counter and a running sum.
dimension t : int
source x @ (t)
n @ (t) = 0 fby.t n + 1
running @ (t) = x fby.t running + next.t x
-- data x = 2 * #t + 1
-- expect n {t = 6} = 6
-- expect running {t = 0} = 1
-- expect running {t = 4} = 25

fby has the lowest precedence: x fby.t running + next.t x means x fby.t (running + next.t x). Read running as: at the origin, x; afterwards, the previous running plus the current x. (The odd numbers sum to squares.)

A recursion that never reaches the origin is not well founded. A value that refers to itself at the same context is rejected before evaluation:

-- An unguarded cycle is rejected.
dimension t : int
loop @ (t) = loop + 1
-- expect compile error: cycle

A recursion that runs the wrong way, such as x @ (t) = next.t x, compiles but never reaches a value; it stops at the demand-depth limit with a program error, never with a value.

6. Neighbourhoods and aggregates

neighbourhood(v, x in -1..1, …, boundary truncate) is the list of v's values around the current point. Truncation drops neighbours outside the declared range (it never drops an in-range neighbour that is Undefined or an Error). The aggregates mean, sum, min, max and count reduce a list. (They also reduce any expression over declared ranges: section 13.)

-- A 3×3 neighbourhood, truncated at the edges.
dimension x : int range 0..9
dimension y : int range 0..9
source h @ (x, y)
smooth @ (x, y) = mean(neighbourhood(h, x in -1..1, y in -1..1, boundary truncate))
cells @ (x, y) = count(neighbourhood(h, x in -1..1, y in -1..1, boundary truncate))
highest @ (x, y) = max(neighbourhood(h, x in -1..1, y in -1..1, boundary truncate))
-- data h = 10 * #x + #y
-- expect smooth {x = 5, y = 5} = 55
-- expect cells {x = 5, y = 5} = 9
-- expect cells {x = 0, y = 0} = 4
-- expect highest {x = 0, y = 0} = 11

7. Time and space together

Neighbourhoods and fby combine into recurrences over time and space. Heat diffusion: each step, every cell becomes the mean of its neighbourhood at the previous step.

-- Heat diffusion: a recurrence over time and a 2D grid.
dimension time : int
dimension x : int range 0..15
dimension y : int range 0..15
source initial @ (x, y)
heat @ (time, x, y) =
  initial fby.time mean(neighbourhood(heat, x in -1..1, y in -1..1, boundary truncate))
-- data initial = if #x == 8 and #y == 8 then 100 else 0
-- expect heat {time = 0, x = 8, y = 8} = 100
-- expect heat {time = 1, x = 8, y = 8} = 11.111
-- expect heat {time = 5, x = 8, y = 8} = 4.405

A query for heat at step 5 evaluates only the 286 points that can affect it (its "light cone"), not the whole grid at every step.

Conway's Game of Life has the same shape:

-- The Game of Life, from a glider.
dimension time : int
dimension x : int range 0..9
dimension y : int range 0..9
source seed @ (x, y)
alive @ (time, x, y) = seed fby.time (if n == 3 or (alive == 1 and n == 2) then 1 else 0)
n @ (time, x, y) = sum(neighbourhood(alive, x in -1..1, y in -1..1, boundary truncate)) - alive
-- data seed = if (#x == 1 and #y == 0) or (#x == 2 and #y == 1) or (#y == 2 and #x <= 2) then 1 else 0
-- expect alive {time = 0, x = 1, y = 0} = 1
-- expect alive {time = 4, x = 1, y = 0} = 0
-- expect alive {time = 4, x = 2, y = 1} = 1
-- expect alive {time = 4, x = 3, y = 3} = 1

After four generations the glider has moved one cell diagonally.

8. Functions, where and local dimensions

A function applies to whole histories: its arguments are values, not single numbers.

-- A function over histories: the previous value.
dimension t : int
source x @ (t)
fun prev(v) = v fby.t v
moving3 @ (t) = (x + prev(x) + prev(prev(x))) / 3
-- data x = #t * #t
-- expect moving3 {t = 4} = 9.667

(prev(prev(x)) is two steps back; (x fby.t x) fby.t x would not be, because fby's right operand is read one step back, so it is x one step back again.)

where … end adds local definitions:

-- A function with local definitions: an exponential moving average.
dimension t : int
source x @ (t)
fun ema(v, a) = s
  where
    s = v fby.t a * next.t v + (1 - a) * s
  end
smoothed @ (t) = ema(x, 0.5)
-- data x = if #t == 0 then 0 else 8
-- expect smoothed {t = 0} = 0
-- expect smoothed {t = 1} = 4
-- expect smoothed {t = 3} = 7

A local dimension is declared inside where and starts at 0 when the block is entered, as in Multidimensional Programming. Here it walks a running total:

-- A local dimension: the prefix sum of x up to t.
dimension t : int
source x @ (t)
prefix @ (t) = acc @ {z = #t}
  where
    dimension z
    acc = x @ {t = 0} fby.z acc + x @ {t = #z + 1}
  end
-- data x = #t + 1
-- expect prefix {t = 0} = 1
-- expect prefix {t = 4} = 15

9. Clocks and alignment

A dimension can be clocked: its coordinates are ticks of a named clock, each with a timestamp in milliseconds. Values on different clocks never mix implicitly. You combine them through align, with an explicit policy:

-- Two clocks, combined only through alignment.
dimension ts : int clock sensor
dimension tc : int clock calib
source reading @ (ts)
source factor @ (tc)
asof_value @ (ts) = reading * align(factor, asof)
held_value @ (ts) = reading * align(factor, hold max 100ms)
windowed @ (ts) = mean(align(factor, window 250ms))
-- clock sensor = 0, 100, 200, 300
-- clock calib = 0, 150, 280
-- data reading = 10
-- data factor = if #tc == 2 then undefined else #tc + 1
-- expect asof_value {ts = 2} = 20
-- expect asof_value {ts = 3} = Undefined
-- expect held_value {ts = 3} = Undefined
-- expect windowed {ts = 3} = 2

At tick ts = 3 (300 ms) the latest calibration tick is 2 (280 ms), whose value is Undefined, so asof gives Undefined. hold would step back to tick 1 (150 ms), but that is 150 ms old, more than its 100 ms limit. window averages the defined values in (50, 300] ms: only tick 1's 2.

10. Effects: calling an external service

An external service is a program outside eLucid8 that answers on request: an AI model, a web API, a simulator. A source is external data you read; an effect calls an external service that computes an answer.

A service call is an effect: never computed speculatively, made once per query and context, and recorded in the trace. Its policy is written at the call site: a deadline, which errors to retry, and whether successful results may be reused by later queries.

The playground supplies a few services, listed in its Program pane and in the Services reference. The examples here call sum_service, a stand-in that answers with the sum of its numeric inputs after the latency set in the playground.

-- A service call with a deadline.
dimension t : int
source x @ (t)
effect score @ (t) = call sum_service(x) deadline 10ms reuse successes
alarm @ (t) = score > 20
-- latency 5
-- data x = 7 * #t
-- expect score {t = 4} = 28
-- expect alarm {t = 4} = true
-- expect alarm {t = 2} = false

If a call finishes after its deadline, the outcome is a late Error, never an ordinary value, and it propagates like any other error:

-- A late service call.
dimension t : int
source x @ (t)
effect score @ (t) = call sum_service(x) deadline 3ms
alarm @ (t) = score > 20
-- latency 5
-- data x = 7 * #t
-- expect alarm {t = 4} = Error via score (t = 4): late (5 ms > deadline 3 ms)

The playground rejects a call to a service it does not supply, so a misspelt name never returns a plausible number:

-- A misspelt service name.
dimension t : int
source x @ (t)
effect score @ (t) = call sum_servce(x) deadline 10ms
-- data x = #t
-- expect compile error: unknown service: sum_servce; available: sum_service

11. What the compiler checks

Before anything runs, the compiler rejects:

-- Coordinates never cross clocks.
dimension ts : int clock sensor
dimension tc : int clock calib
source factor @ (tc)
wrong @ (ts) = factor @ {tc = #ts}
-- clock sensor = 0, 100
-- clock calib = 0, 150
-- data factor = 1
-- expect compile error: clock

Reserved names

Some words have a fixed meaning, so they cannot name your own definitions:

wordsrule
dimension, source, fun, effect, if, notkeywords: never usable as a name
true, false, undefinedliterals: naming a source, value, effect, function or parameter after one is a compile error
sum, mean, count, min, max, exp, log, sqrt, tanh, absbuilt-in aggregates and maths functions (section 15): a fun with one of these names is a compile error

Other words of the language, such as then, else, where, end, fby, first, next, in, range, align and window, are recognised by where they appear, so they are not reserved; a value may have such a name, but a clearer name avoids confusion. A value may also be called log or max, since only log(…) is the function.

-- A built-in cannot be redefined.
dimension t : int range 0..9
source x @ (t)
fun sum(v) = v + 1
total @ (t) = sum(x, t in 0..9)
-- data x = #t
-- expect compile error: sum is a built-in aggregate and cannot be redefined

12. Running programs

13. Aggregates over ranges, and values with no dimensions

sum, mean, min, max and count also take any expression and one or more bindings d in a..b. The expression is evaluated at the current context with d set to each integer from a to b, and the results are combined.

A total over a fixed range. Here total has no dimensions of its own (see below); the binding supplies every t:

-- The sum of a series over a fixed range.
dimension t : int range 0..9
source x @ (t)
total @ () = sum(x, t in 0..9)
squares @ () = sum(x * x, t in 0..9)
-- data x = #t + 1
-- expect total {} = 55
-- expect squares {} = 385

The argument can be any expression (x * x), not only a named value.

A moving window. The bounds are integer expressions of the current context, as in @, and binding a dimension the context already has overrides it inside the aggregate. Near the start, the window reaches before the data: an Undefined element makes the mean Undefined, so clamp the bound if you want a shorter window there.

-- A moving mean over the last three ticks, raw and clamped at the start.
dimension t : int
source x @ (t)
raw @ (t) = mean(x, t in #t - 2..#t)
clamped @ (t) = mean(x, t in (if #t < 2 then 0 else #t - 2)..#t)
-- data x = if #t < 0 then undefined else 3 * #t
-- expect raw {t = 5} = 12
-- expect raw {t = 1} = Undefined
-- expect clamped {t = 1} = 1.5
-- expect clamped {t = 5} = 12

Supplying a coordinate the context lacks. row_total is indexed by i only, but m needs i and j: the binding supplies j, as @ would. Without it, the compiler rejects the reference (section 11).

-- A per-row total over a two-dimensional table.
dimension i : int range 0..2
dimension j : int range 0..3
source m @ (i, j)
row_total @ (i) = sum(m, j in 0..3)
row_max @ (i) = max(m, j in 0..3)
grand @ () = sum(m, i in 0..2, j in 0..3)
-- data m = 10 * #i + #j
-- expect row_total {i = 2} = 86
-- expect row_max {i = 1} = 13
-- expect grand {} = 138

Matrix multiplication is one line: the sum over k of A's row times B's column. It agrees with the version that walks a local dimension with a running sum (section 8), Multidimensional Programming's own example:

-- Matrix multiplication, with an aggregate and with a local dimension.
dimension i : int range 0..3
dimension j : int range 0..3
dimension k : int range 0..3
source A @ (i, j)
source B @ (i, j)
C @ (i, j) = sum(A @ {j = #k} * B @ {i = #k}, k in 0..3)
C_walk @ (i, j) = acc @ {z = 3}
  where
    dimension z
    acc = A @ {j = 0} * B @ {i = 0} fby.z acc + A @ {j = #z + 1} * B @ {i = #z + 1}
  end
-- data A = #i + #j
-- data B = #i - #j
-- expect C {i = 2, j = 1} = 12
-- expect C_walk {i = 2, j = 1} = 12
-- expect C {i = 3, j = 0} = 32
-- expect C_walk {i = 3, j = 0} = 32

A value with no dimensions is written name @ (). It is computed once per query and can be used from any context: here, a data mean used to centre every point.

-- A value with no dimensions: computed once per query, used anywhere.
dimension t : int range 0..9
source x @ (t)
average @ () = mean(x, t in 0..9)
centred @ (t) = x - average
-- data x = 2 * #t
-- expect average {} = 9
-- expect centred {t = 0} = -9
-- expect centred {t = 9} = 9

Outcomes are those of the neighbourhood aggregates (section 6). An empty range gives 0 for sum and count, and Undefined for mean, min and max. Every element is demanded: an Error element makes the result an Error, which wins over an Undefined element; otherwise an Undefined element makes it Undefined. An aggregate over more than a million elements (for example i in 0..1000000000000) stops the query with a program error before computing any element; the limit can be changed with the query option maxAggregate.

-- Empty ranges, Undefined elements and Error elements.
dimension t : int
source x @ (t)
none @ (t) = sum(x, t in 1..0)
nothing @ (t) = mean(x, t in 1..0)
upto @ (t) = sum(x, t in 0..#t)
-- data x = if #t == 3 then undefined else if #t == 6 then 1 / 0 else #t
-- expect none {t = 0} = 0
-- expect nothing {t = 0} = Undefined
-- expect upto {t = 2} = 3
-- expect upto {t = 5} = Undefined
-- expect upto {t = 7} = Error via x (t = 6): division by zero

The order is fixed: the dimensions as written, the last varying fastest; each range ascending; left to right, starting from the first element. Floating-point addition is not associative, so the order can matter; fixing it makes every result the same under every cache, tile and vectorised kernel. Here the order of the bindings changes the answer:

-- The order of the bindings is the order of the sum.
dimension x : int range 0..1
dimension y : int range 0..1
source g @ (x, y)
xy @ () = sum(g, x in 0..1, y in 0..1)
yx @ () = sum(g, y in 0..1, x in 0..1)
-- data g = if #x == 1 and #y == 0 then -10000000000000000 else if #y == 0 then 10000000000000000 else 1
-- expect xy {} = 1
-- expect yx {} = 2

(xy adds g at (0,0), (0,1), (1,0), (1,1): 10¹⁶ + 1 rounds to 10¹⁶, so that 1 is lost. yx adds (0,0), (1,0), (0,1), (1,1): the large terms cancel first.)

An aggregate does not guard a reference to the value being defined, since its range may include the current point:

-- An aggregate is not a guard.
dimension t : int
source x @ (t)
total @ (t) = x + sum(total, t in 0..#t - 1)
-- data x = 1
-- expect compile error: unguarded cycle

(Write a running total with fby, as in section 5, or sum the source directly: sum(x, t in 0..#t).)

14. Real time: what it means in eLucid8, and its limits

eLucid8 is a real-time multidimensional language in a specific sense: it keeps logical time and physical time apart, and connects them only explicitly.

So time, space and any other axis are all dimensions of one context, and the passage of real time is a separate, declared mapping.

-- A sensor on a real clock, a slower calibration feed, and service calls with deadlines.
dimension ts : int clock sensor
dimension tc : int clock calib
source reading @ (ts)
source gain @ (tc)
calibrated @ (ts) = reading * align(gain, asof)
effect score @ (ts) = call sum_service(calibrated) deadline 20ms
effect hurried @ (ts) = call sum_service(calibrated) deadline 2ms
alert @ (ts) = score > 50
hurried_alert @ (ts) = hurried > 50
-- clock sensor = 1000, 1100, 1200, 1300
-- clock calib = 950, 1250
-- latency 5
-- data reading = 10 + 10 * #ts
-- data gain = #tc + 2
-- expect calibrated {ts = 1} = 40
-- expect calibrated {ts = 3} = 120
-- expect alert {ts = 3} = true
-- expect alert {ts = 1} = false
-- expect hurried {ts = 3} = Error: late (1305 ms > deadline 1302 ms)
-- expect hurried_alert {ts = 3} = Error via hurried (ts = 3): late (1305 ms > deadline 1302 ms)

How to read it:

Limitations. eLucid8 states timing; it does not guarantee it.

These limits are deliberate for a prototype. Wadge and Ashcroft judged Lucid unsuitable for real-time work (p. 111). eLucid8's clocks, alignment and deadlines are a new, experimental answer, designed so that timing is explicit and every miss is visible.

15. Maths functions

exp, log (the natural logarithm), sqrt, tanh and abs take one number; min(a, b) and max(a, b) take two. Like + and *, they are strict and pointwise: every argument is demanded, an Error argument wins over an Undefined one, and an argument that is not a number is an Error.

Activations. A sigmoid is built from exp, and a ReLU from the two-argument max:

-- A sigmoid from exp, a ReLU from max, and tanh.
dimension t : int range -5..5
source x @ (t)
sigmoid @ (t) = 1 / (1 + exp(-x))
relu @ (t) = max(x, 0)
squash @ (t) = tanh(x)
-- data x = #t
-- expect sigmoid {t = 0} = 0.5
-- expect sigmoid {t = 2} = 0.881
-- expect relu {t = -3} = 0
-- expect relu {t = 4} = 4
-- expect squash {t = 1} = 0.762

min and max: function or aggregate? A second argument of the form d in a..b makes the aggregate of section 13; any other second argument makes the two-argument function; and a single argument (a neighbourhood) is the list aggregate of section 6. They nest freely:

-- A root-mean-square with sqrt and an aggregate; the largest absolute value, two ways.
dimension t : int range 0..3
source x @ (t)
rms @ () = sqrt(mean(x * x, t in 0..3))
peak @ () = max(abs(x), t in 0..3)
peak_of_ends @ () = max(max(abs(x @ {t = 0}), abs(x @ {t = 3})), 0)
-- data x = 2 * #t - 3
-- expect rms {} = 2.236
-- expect peak {} = 3
-- expect peak_of_ends {} = 3

Here x is −3, −1, 1 and 3, so the mean square is 5 and rms is √5.

Results outside the real numbers are Errors that name the function, under the same rule as overflowing arithmetic (section 3): log(0) is −∞, log(-1) and sqrt(-1) are not numbers, and exp of a large argument overflows. An underflow to 0, as in exp(-1000), is an ordinary 0.

-- log, sqrt and exp outside the real numbers, and how the Errors travel.
dimension t : int range 0..3
source x @ (t)
lg @ (t) = log(x)
root @ (t) = sqrt(x - 1)
grow @ (t) = exp(1000 * x)
later @ (t) = lg + 1
either @ (t) = max(undefined, log(x))
-- data x = #t
-- expect lg {t = 1} = 0
-- expect lg {t = 0} = Error: log gave -Infinity (non-finite)
-- expect root {t = 0} = Error: sqrt gave NaN (non-finite)
-- expect grow {t = 0} = 1
-- expect grow {t = 1} = Error: exp gave Infinity (non-finite)
-- expect later {t = 0} = Error via lg (t = 0): log gave -Infinity (non-finite)
-- expect either {t = 0} = Error: log gave -Infinity (non-finite)
-- expect either {t = 2} = Undefined

either shows Error over Undefined: at t = 0 the log argument is an Error, so max is an Error even though its other argument is Undefined.

These names are reserved for the functions: fun abs(v) = … is a compile error. A value may still be called log; only log(…) is the function.

Quick reference

ConstructExample
dimensiondimension x : int range 0..15, dimension s : label, dimension t : int clock sensor
sourcesource field @ (time, x, y)
valuesmooth @ (time, x, y) = …
current coordinate#x
another contextv @ {x = #x + 1, time = 0}
operators+ - * /, < <= == != > >=, and or not
conditionalif c then a else b
missing valueundefined
streamsfirst.t e, next.t e, a fby.t b
neighbourhoodneighbourhood(f, x in -1..1, y in -1..1, boundary truncate)
aggregatesmean, sum, min, max, count of a neighbourhood, or sum(e, h in 0..46, x in 0..15)
maths functionsexp(e), log(e), sqrt(e), tanh(e), abs(e), min(a, b), max(a, b)
no dimensionsaverage @ () = mean(x, t in 0..9)
functionfun f(v, a) = e where … end
local dimensionwhere dimension z … end
alignmentalign(v, exact), asof, hold max 100ms, window 200ms
effecteffect v @ (t) = call service(args) deadline 20ms retry overloaded up to 2 reuse successes (up to n: 1 to 10 attempts in total)
comment-- …
reserved namesdimension source fun effect if not, true false undefined; built-ins can't be redefined by fun (section 11)