#eLucid8 semantics sketch (v0.1)
This note proposes the smallest semantic core worth prototyping. It is a design draft, not a settled language specification.
Provenance labels. Each section is tagged with where its rules come from, following cos/LUCID-BACKGROUND.md §5:
- [Lucid]: documented Lucid-family behavior, with a primary source cited in §9;
- [eLucid8]: an eLucid8 language design choice;
- [Runtime]: an implementation policy, which must not change logical results.
Rules marked "owner decision" have been decided. Anything else tagged [eLucid8] or [Runtime] remains a proposal.
#1. Context-indexed values
Provenance: the context-indexed value model is [Lucid] (§9: L1, L2). Typed coordinates and the outcome set are [eLucid8].
A value is a partial function from a logical context to a value:
Value : Context -> T
Context = DimensionName -> Coordinate
Each dimension has a declared coordinate domain and identity. Examples include integer step, event-time time, integer x/y, and categorical model_version. A context contains coordinates only for the dimensions relevant to the value.
(Owner decision, 2026-10-02.) Coordinates are typed. In v0.1 a coordinate is either an integer or a text label, and equality compares type and value: integer 1 and text label "1" are different coordinates. Decimal (floating-point) coordinates are not supported in v0.1. Event times are integers in a declared unit.
Evaluating a value at a context has exactly one outcome:
Outcome<T> = Value(T) | Undefined | Error(E)
Undefined means the context is outside the value's domain (the value is a partial function). Error means evaluation failed. The two are always distinct.
A stream is simply a value whose context has one dimension:
x@{time = t}
A multidimensional value has several coordinates:
y@{time = t, x = i, y = j}
Dimensions are logical axes. They do not inherently mean wall-clock time, physical location, tensor layout, or device placement; those meanings are declared or mapped separately.
#2. Equations and demand
Provenance: demand-driven evaluation is [Lucid] (§9: L3). Rejecting unguarded cycles, projection, propagation, boundary policies and snapshots are [eLucid8]. §9 notes where these agree with Lucid and where they differ.
An equation defines how to evaluate a named value at a context. Evaluating a requested point recursively requests the points named by its dependencies. Only demanded values need to be evaluated.
z@{time = t, x = i} = f(a@{time = t, x = i}, b@{time = t, x = i})
The evaluator records dependencies for each computed point. A demand trace should identify the requested coordinate, evaluated dependencies, cache hits, and any effects encountered.
Recursive definitions require a well-founded demand rule or an explicit recurrence operator. The initial prototype should reject unguarded cycles rather than guessing an evaluation order. (This departs from Lucid, where an unguarded definition such as X = X + 1 denotes ⊥ and a demand-driven interpreter loops forever; §9: L4.)
The following rules are owner decisions (2026-10-02).
Projection. When a value is referenced in a context with more dimensions than it declares, the context is projected to the referenced value's dimensions. For example, bias@(time) used at (time, x, y) is looked up at time alone and does not depend on x or y. If a referenced value needs a dimension that the context does not supply, for example alarm@(time) using field@(time, x, y), the program is rejected before evaluation unless the missing coordinates are given explicitly.
Propagation. If an outcome depends on a dependency whose outcome is Error, it is an Error that identifies the upstream error. It is never silently replaced by an ordinary value: if score is late or fails, alarm is an upstream error, not false. A dependency outcome of Undefined propagates as Undefined. If the dependencies include both Error and Undefined, the result is Error. Strictness (owner decision, 2026-10-02; supersedes the earlier "every operator is strict"):
- Conditionals (
if c then a else b) demand the condition, then only the branch taken. [Lucid] (MDP §5.3, p. 90). and/orshort-circuit left to right:false and X = falseandtrue or X = true, without demandingX. [eLucid8], following pLucid (§9 L7). Commutative (parallel) short-circuiting is out of scope.- All other operators are strict and pointwise. Their operands are demanded together, so an
Errorin any operand wins overUndefined. [Lucid] (MDP pp. 90, 96). - These choices preserve the least-fixed-point meaning: each operator is monotone and continuous, and demand-driven evaluation computes it. A short-circuited operand is never demanded, so no error is hidden.
Lists are not comparable (owner decision, 2026-10-04). [eLucid8] A list is what a neighbourhood or a window alignment gives. == and != with a list operand (on either side) give an Error ("== needs values, not lists"). Strictness is unchanged: both operands are demanded together, so an Error operand wins, then an Undefined one, before the list is examined. (Before this decision the reference compared lists by identity, so the result could depend on whether memory management had recomputed a list.)
Non-finite numbers (owner decision T4, 2026-10-03). [eLucid8] If an arithmetic operator or an aggregate produces a non-finite number (Infinity, -Infinity or NaN), the outcome is an Error ("non-finite"), not a value, consistent with division by zero. It propagates like any other Error, so the trace names where a computation first overflowed. External source data is not affected by this rule; a source defined by an eLucid8 formula (as in the playground) is an expression, so the rule applies to it (owner, 2026-10-03). Likewise, a service's answer is external: a non-finite number a service answers is a value, like source data, until an operator produces a non-finite result from it (owner decision, 2026-10-04; §5).
Explicit error recovery may be considered later.
Neighborhood boundaries. A neighborhood operation must declare a boundary policy explicitly. The spatial demo uses truncation: only neighbors inside the declared coordinate domain are included, and the aggregate (for example, an average) is computed over the neighbors that are available. Truncation drops only out-of-bounds neighbors. An in-bounds neighbor that is Undefined or Error propagates as usual.
Non-numbers in neighbourhood aggregates (owner decision, 2026-10-04; closes Q9). [eLucid8] sum, mean, min and max of a list (a neighbourhood or a window alignment) whose elements include a non-number (text, a boolean or a list) give an Error ("sum needs numbers"), as for aggregates over declared ranges and as + does; count counts any values. An Error or Undefined neighbour still decides the outcome first, as above. (Before this decision the reference converted non-numbers as JavaScript does.)
Maths functions (owner decision N1, 2026-10-03; proposals/milestone-5-network-v0.1.md). [eLucid8] exp(e), log(e) (natural logarithm), sqrt(e), tanh(e), abs(e), and the two-argument min(a, b) and max(a, b).
- They are strict and pointwise, like arithmetic operators: an Error argument wins over an Undefined one, and a non-number argument is an Error.
- Results outside the real numbers are Errors under the non-finite rule, naming the function:
log(0)(−∞),log(-1)andsqrt(-1)(NaN), andexpof a large argument (+∞). - Defined by one library (owner decision A5, 2026-10-04). [eLucid8]
exp,logandtanhare defined by fdlibm 5.3 (Sun's freely distributable maths library:e_exp.c,e_log.c, ands_tanh.cwiths_expm1.c), computed in its operation order with each operation a separate IEEE double operation (never fused into a multiply-add), plus two additions:exp(1)isMath.E(2.718281828459045; fdlibm's algorithm gives one ulp more), andtanh(x)isxfor |x| < 2⁻²⁸ (FreeBSD msun's later branch, correctly rounded there; fdlibm 5.3 computes it throughexpm1above 2⁻⁵⁵; owner decision Q11). The library is identified asfdlibm-5.3+exp1+msun-tanh.sqrt(correctly rounded),abs,minandmaxare exact and are ECMAScript's. So every backend (the TypeScript reference on any JavaScript engine, its fused kernels, the Rust runtime) gives the same bits.- Recorded semantic change (last bits only). Until A5, these functions were whatever the host's
Math.exp,Math.logandMath.tanhgave. V8 derives them from fdlibm too, but compiles them per machine: on Apple silicon with fused multiply-add, with a different branch fortanhof |x| < 2⁻²⁸, and withexp(1) = Math.E. Its results therefore differ between machines, and from fdlibm by up to 3 ulp on a small share of inputs (0.03–2.7% in the samples measured; 12.7% fortanhwith 2⁻⁶⁰ ≤ |x| < 2⁻²⁰). Fixing the definition changes the last bits of some results on such machines: the network example's parameters by at most 5e-12 relative after 200 steps. It makes them identical everywhere. Evidence:cos/ARCHITECTURE.mditem 58.
- Recorded semantic change (last bits only). Until A5, these functions were whatever the host's
- The two-argument
minandmaxare distinct from the aggregates of the same names: a second argument of the formd in a..bmakes an aggregate (above); any other second expression makes the two-argument function. - Further rules (owner decision, 2026-10-03): a
funmay not be named after a built-in maths function or aggregate (exp,log,sqrt,tanh,abs,min,max,sum,mean,count); it is a compile error. Nor may a source, value, effect, function or parameter be namedtrue,falseorundefined, which are always literals. A source'sInfinityorNaNis a value until a function's result is non-finite, as for operators (sotanh(Infinity)is 1, andabs(-Infinity)is an Error).minandmaxfollow IEEE 754 for signed zero (min(0, -0)is −0), which no comparison distinguishes.
Aggregates over declared ranges (owner decision A1–A4, 2026-10-03; proposals/aggregates-v0.1.md). [eLucid8], after MDP's sum_z(product_z) (§5.3, p. 91) [Lucid].
sum,mean,min,maxandcountmay take any expression and one or more bindingsd in a..b. Each binding rangesdover the integersatob; the bounds are integer expressions evaluated in the current context.- The expression is evaluated at the current context, extended or overridden by the bindings. This is the explicit way to supply coordinates the context lacks.
- Order: elements combine in a fixed order: dimensions as written, the last varying fastest; each range ascending; left to right. The result is therefore identical under every runtime policy.
- Outcomes: strict, as for neighbourhood aggregates (an
Errorelement wins, else anUndefinedelement makes the resultUndefined). An empty range gives 0 forsumandcount, andUndefinedformean,minandmax. Non-finite results areErrors. - Values with no dimensions (
c @ () = …) are allowed; each is evaluated once per query and cached like any point. - Further rules (owner decision, 2026-10-03):
sum,mean,minandmaxover a non-number element give anError, as+does;countcounts any values. All bounds are evaluated in the outer context, so a binding cannot depend on another binding in the same aggregate (nest aggregates for triangular ranges). Folding starts from the first element, as the order above says. A dimension may be bound only once per aggregate.alignis not allowed inside an aggregate. Only values may have no dimensions; sources and effects need at least one. Local definitions do not see an aggregate's bound dimensions. An aggregate over more elements than a configurable limit stops with a program error, as the depth limit does.
Stream operators (owner decision, 2026-10-02; proposals/streams-v0.1.md). Along an integer dimension d with origin o (the range minimum, else 0):
next.d eiseatd = #d + 1;first.d eiseatd = o;a fby.d bisaat#d = o,batd = #d − 1when#d > o, andUndefinedwhen#d < o.
These follow Wadge & Ashcroft (pp. 47, 61) [Lucid]; the dimension suffix and the rule before the origin are [eLucid8]. fby demands only the operand it selects. Recursion that is not well founded (for example x = next.t x) stops at a demand-depth limit on logical depth (default 1,000,000) and is reported as a ProgramError, never as a value. (Owner decision, 2026-10-04.) [Runtime] The depth limit is a resource limit, like memory: whether a query stops with the depth-limit ProgramError may depend on runtime choices (evaluating a recurrence by levels or per element, retention, suspension), so a deep, well-founded history may reach the limit under one runtime policy and not under another. Outcomes (Value, Undefined, Error) never depend on runtime choices.
Functions, where and local dimensions (owner decision, 2026-10-02; proposals/functions-v0.1.md).
- Functions are first-order and apply to whole histories, with arguments substituted by name. [Lucid] (Wadge & Ashcroft p. 61). They are inlined; recursion through functions is rejected in v0.1. [eLucid8]
- Local values in
where … endinherit the enclosing dimensions. [eLucid8] - A local dimension declared in
wherestarts at 0 when the block is entered (MDP p. 91). [Lucid] It is supplied implicitly for references from outside the block. This is the only implicit coordinate; missing global dimensions remain errors.
Source snapshots. Each query pins an immutable version of every source it reads, and no source may advance during evaluation. Together these version pins form the query's snapshot.
Retaining source versions. (Owner decision, 2026-10-02, following eduction's usage counts; §9: L12. MDP p. 100 notes that recomputing a discarded value assumes "the input values to the program are available", and this rule guarantees that for pinned versions.) Each source version has a usage count: the number of running queries that pin it plus the number of usable cache entries that read it. While the count is above zero, the version must stay readable. When it reaches zero, the version may be removed. If a demand needs a version that has been removed, the outcome is an Error (source version unavailable), never a value computed from a different version.
#3. Logical time and physical clocks
Provenance: logical time not counting a global clock is [Lucid] (§9: L5). Clock mappings, alignment policies and deadline outcomes are [eLucid8].
Logical coordinates are not durations. A logical step = 12 identifies a position in a history; by itself it does not assert that twelve milliseconds or twelve seconds have elapsed.
A clock mapping associates logical ticks with physical events or deadlines:
clock sensor_clock:
source = sensor.timestamps
deadline = 20ms
Clocked dimensions can have different rates or be event-driven. Combining values from distinct clocks requires an explicit alignment policy (for example, latest-at-or-before, exact timestamp, window, or hold-last-value). There is no implicit global order across independent clocks.
Clocks and alignment (owner decision, 2026-10-02; rules C1–C6 in proposals/clocks-alignment.md). [eLucid8]
- C1. Clocked dimensions. An integer dimension may be declared clocked. Its coordinates are tick indices of one named clock, and each clock has its own dimension.
- C2. Clocks are timestamp tables. A clock maps each tick to a timestamp in integer milliseconds, strictly increasing. The table is versioned and pinned per query as part of the snapshot. Alignment and deadlines are therefore deterministic and independent of runtime policy. A tick with no timestamp makes any alignment through it
Undefined. - C3. No implicit cross-clock reference. A value on another clock is reachable only through an alignment operator. Projection never crosses clocks.
- C4. Alignment operators, explicit at the reference, select ticks on the other clock by timestamp:
exact: equal timestamp;asof: the latest tick at or before;hold: likeasof, but steps back overUndefined(never overError) to the latest defined value, with an optionalmaxAgeMs;window(widthMs): the list of defined values with timestamps in (T − width, T], omittingUndefined; anyErrormakes the result anError.- When nothing qualifies, the result is
Undefined, or the empty list forwindow.
- C5. Deadlines on clocks. A service call on a clocked dimension is issued at its tick's timestamp, and its deadline is relative to that timestamp.
- C6. Aligned results depend on both clock tables' versions.
exact,asofandwindoware predictable for batching, and so ishold's first step. Footprints declare alignments as entries{ name, align }, implemented 2026-10-02.
Lustre's current (holding the last value of a slower clock) is a related precedent for hold. Lustre clocks, however, derive from one base clock, whereas eLucid8 clocks are independent and aligned by timestamp. Wadge & Ashcroft's hiatons (p. 111) are not adopted.
The runtime records actual start and completion times. A deadline miss is an observable result, not a silent semantic substitution. Hard real-time guarantees require bounded execution and bounded-latency storage; a prototype can report timing but should not claim hard real-time certification.
#4. Point and cluster storage
Provenance: [Runtime], except that typed key equality and the validity of cache entries with respect to snapshots follow from [eLucid8] rules in §1–§2. Caching values by variable and context has a precedent in Lucid eduction implementations (the "warehouse"; §9: L6). That precedent concerns implementations, not language semantics. Cluster entries are new in eLucid8: Lucid implementations stored and computed only individual values, never tiles or blocks (§9: L11). GLU's "granularity" concerned coarse-grained computation nodes, not blocks of stored values (§9: L8).
The semantic object is the value at a logical context. Storage granularity is an implementation decision:
- A point entry stores one context and its value.
- A cluster entry stores a region of contexts and a corresponding block of values.
For example, a cluster may cover a fixed time and an x/y tile. A point demand can be satisfied from a containing cluster. A cluster request can be assembled from point entries if its coverage is complete.
The initial prototype should require exact, typed coordinate keys and explicit cluster coverage. Hashing may index entries, but a hash collision must never imply coordinate equality. Cluster overlap, precedence, and invalidation rules must be deterministic.
Cached pure values are reusable only while their dependencies and definition versions remain valid. Memory limits and eviction may affect performance but must not change results.
The cache rules below were accepted by the owner on 2026-10-02. Worked examples are in proposals/point-cluster-rules.md.
- Identity (R0). A point entry is identified by its value name, definition version and exact typed coordinate key. A cluster entry is identified by its value name, definition version and coverage. A key has exactly the dimensions the value declares.
- Coverage (R1). A cluster covers a finite region: for each dimension, either a single coordinate or a finite set of coordinates. Integer dimensions may use inclusive ranges. Text-label dimensions must list an explicit finite set. A cluster holds one outcome for every context in its coverage, with no holes;
Value,UndefinedandErrorall count as outcomes. A partially computed region is stored only as point entries or as smaller complete clusters. - Selection (R2). A point demand uses an exact point entry if a usable one exists. Otherwise it uses the usable covering cluster with the fewest contexts, and remaining ties are broken by canonical coverage order: dimensions are sorted by name, and for each dimension the lowest coordinate is compared, then the highest (text-label sets compare their sorted members). Insertion order is never used. If overlapping entries for the same value, definition version, coordinates and snapshot disagree, the runtime reports a cache-integrity error. A check mode may look for such conflicts proactively.
- Tile demand (R5). A demand for a tile means exactly the set of point demands for its elements, and each element's outcome is what point eduction would produce. v0.1 propagates tile demand per element. Batched region demands are a later optimisation and must give identical outcomes; they may over-request pure values only, never effectful ones. Examples are in
proposals/point-cluster-rules.md. - Validity (R3). An entry is usable only for the definition version and the dependency and source versions it actually read. If any of those changes, the entry is unusable; for a cluster, the whole cluster becomes unusable. Eviction is always permitted. A pure
Erroroutcome is deterministic for its definition and source versions and may be cached like any other outcome, as eduction did witherrorandeod(§9: L9). Unguarded cycles never reach the cache, because they are rejected before evaluation.
#5. Effects and AI operations
Provenance: [eLucid8] for the observable rules: outcomes, propagation, no speculation and memoization within a query. [Runtime] for reuse across queries, the retry mechanism and adapters. No Lucid precedent for calls to external services (such as model calls) or effect caching was found in the sources checked so far.
Pure equations are deterministic within a program version. An effect calls an external service: a program outside eLucid8 that answers on request, such as an AI model or a web API (after first use, "service"). A source is external data you read; an effect calls an external service that computes an answer. (Naming: owner decision, 2026-10-04; the rules below are unchanged.) External operations are explicit effects and return structured outcomes:
Result<T, E> = Ok(T) | Error(E)
A service call records its input identity, the service's identifier and version (for an AI model: the model identifier/version and the prompt or operation version), request options, start/completion time, and outcome. Its cache and retry policy must be specified. Replaying a recorded response is distinct from making a fresh request to the service.
The first adapter should expose service calls as coarse operations over ordinary values or clusters. Mixing simple operators with coarse-grained nodes that run chunks of conventional code has a precedent in GLU (§9: L8). Token-by-token streaming is outside the first milestone.
The following rules are owner decisions (2026-10-02, R4).
- Outcomes. A call's
Ok(v)becomes the outcomeValue(v), and itsError(e), including a late result, becomesError(e). Errors then propagate as described in §2. - Non-demanded points. An outcome at a context nobody demanded, such as an extra point computed while filling a cluster, must never fail the query.
- Policy at the call site. Each service call declares its speculation, reuse and retry policy where it is written. The adapter declares its capabilities, such as whether calls are idempotent and whether they can be cancelled.
- Late errors (owner decision, 2026-10-02): a call that returns an error after its deadline is reported as
late; the adapter's error code is kept in the call record. - Defaults:
- no speculation: a service call runs only for demanded contexts;
- memoization within a query: repeated demands for the same call in one query see the same outcome, including a failure;
- no caching of failures;
- retry only errors configured as transient, with a bounded number of attempts and a deadline.
- At most 10 attempts (owner decision, 2026-10-04):
retry <codes> up to Ncounts every attempt, the first included, and N must be from 1 to 10. Any other N is a compile error. - Service faults are ordinary failures (owner decision, 2026-10-04). When a service run as a separate process cannot be started, crashes, does not answer in time or answers malformed, the call fails with a defined code (
service_unavailable,service_crashed,service_timeout,malformed_response;service-protocol.md§4). These are effect failures like any other: anError(orlateafter the deadline), never a stopped query, and aretryclause may name them. The process is restarted for the next call. A service that does not report when it completed is timed by the runtime, so whether its call islatedepends on real time and is not reproducible; services whose timing must be reproducible report it. - Answers are values. A service's answer becomes a value as it is, including a non-finite number (§2: the non-finite rule covers operators, not external data). A service decides what inputs it accepts; one may fail a call whose inputs it cannot use (for example the playground's
anomaly_detectorfails withinvalid_input,services.md). - Reuse across queries. A successful result is reused in another query only under an explicit cache policy. Its cache key must include the service (for an AI model, the model) and its configuration, the definition version, the coordinates and the source snapshot. Here, snapshot means the pinned versions of the sources that result actually read. Serving a stored result is a replay and is recorded as one in the trace.
- First prototype. No live service calls in the evaluator until pure demand and cache behavior are established. The evaluator represents effect outcomes now, using a deterministic adapter.
#6. Logical axes and physical placement
Provenance: [eLucid8] proposal.
Logical dimensions can describe batch, sequence, layer, tensor coordinates, or space. A separate placement plan maps regions or clusters to devices and specifies any required communication. Placement must preserve the logical value semantics.
The first prototype will not compile training graphs or choose optimal sharding. It should provide a trace format that could later record logical coordinates, storage clusters, physical device, communication, and timing.
#7. Prototype acceptance questions
The prototype should answer these design questions with executable examples:
- Can one-dimensional streams and multidimensional values share the same context model?
- Can a point demand evaluate only the dependency points it needs?
- Can point and cluster caching return equivalent results under overlapping demands?
- Can independent clock domains be combined only through visible alignment rules?
- Can a late or failed external service call be represented without pretending it produced an ordinary value?
- Does the demand trace make it clear why a value was computed, reused, or marked late?
#8. Small demonstration workload
Use a finite time-varying spatial field. The source provides sensor samples at (time, x, y). A pure neighborhood operator with a truncation boundary policy gathers local values; an effectful model adapter (deterministic in the first prototype) scores a region; a deterministic rule emits alarms. Query a few alarm coordinates and compare point caching with tile caching. Include timestamps and a simulated deadline so clock semantics can be explored without requiring real-time hardware.
This workload exercises multidimensional contexts, demand, clusters, effects, and real-time mapping while remaining independent of distributed training infrastructure.
#9. Lucid lineage and sources
The following was checked on 2026-10-02 against the primary sources listed in references.md. W&A is Wadge and Ashcroft, Lucid, the Dataflow Programming Language (1985); page numbers are the book's own. Wadge 2022 is "We Demand Data — the story of Lucid and Eduction". Ashcroft, Faustini, Jagannathan and Wadge, Multidimensional Programming, (cited as MDP) has been checked for §5.3 (pp. 89–91), §6.2.4–6.3 (pp. 110–112) and §8.4 (pp. 146–148), from OCR excerpts supplied by the owner, a co-author. Quotations come from those excerpts; where the OCR is garbled, the text is paraphrased rather than quoted. The sections most relevant to eLucid8 are §2.2 (the intensional language Lucid), §2.4 (space and time), §3.2 (denotational semantics), ch. 5 (eduction, especially §5.3–5.4), §6.2.4 (fault-tolerant eductive evaluation) and §8.4 (multiple dimensions).
L1. Histories. A Lucid variable denotes a history indexed by logical time, defined with operators such as
fbyandnext(W&A ch. 3).L2. Multidimensional contexts. Lucid was extended beyond a single time dimension (W&A §7.4, "ILucid, Lucid with Multidimensional Time"). In eduction, a variable may need only some of the available dimensions: "dimensions s and t are enough to get a value for X, but … Y may need dimension h as well" (Wadge 2022). MDP's Laplacian relaxation example uses contexts of the form
[time=T, [x=X, y=Y]](§6.2.4, pp. 110–111). eLucid8's projection rule (§2) is consistent with this, but the exact rule is an eLucid8 choice. Owner statement, 2026-10-02: eduction computed which dimensions a variable needs as demand proceeded, rather than declaring them in advance. A demand always carried a context (time, and possibly space), but not a coordinate for every dimension, because the set of dimensions could be infinite (s0, s1, …). eLucid8 instead declares each value's dimensions and checks references before evaluation (§2). In v0.1 a program has finitely many declared dimensions; infinite dimension families are out of scope. Because eduction computed only the contexts actually needed, a demand never reached a variable that needed a coordinate it lacked (owner statement; see Q2). MDP §8.4 (pp. 146–147): under "first-principle semantics … every dimension in a program is a parameter", so every demand carries a coordinate for every dimension. The cost is that a value which depends only ontgets computed and stored at many space points. Dimensionality analysis fixes this. It computes, by successive approximation starting from{}for every variable, "an upper bound on the dimensions to which the declaration of the expression or variable is sensitive". The bound is then used "to limit the size of tags and avoid some unnecessary duplication of values". eLucid8 declares each value's dimensions instead of inferring them. Each declaration plays the role of that upper bound, and because an equation sees only its projected context, it cannot depend on an undeclared dimension. Inferring dimensions with MDP's algorithm is a possible later addition. Infinite dimensions: MDP describes "lazy tags", where a demand engages in "a finite dialogue" about the context instead of carrying a full tag. It notes the open problem of which tag to store the result under, and the "functionally sequential" approach as not yet implemented (pp. 147–148). This is out of scope for v0.1. Other dimensions: MDP lists space, "alpha" (arrays), branching time (functions), tree and name dimensions. It also notes that intensional systems "can be adapted to handle version control by adding a version dimension" (p. 146). eLucid8 currently keeps definition and source versions outside the logical context, as runtime metadata (§4). Treating versions as a dimension is a recorded alternative, not a proposal. Declared, scoped dimensions: MDP's matrix-multiplication program (§5.3, p. 91) declares dimensions locally (dimension z;inside awhereclause). It queries the current coordinate with#and shifts context with@(the@operator is confirmed on p. 96). The owner's transcription of this program came through OCR and is partly garbled, so it is cited for these constructs only. eLucid8'scontext.xandget(name, { x: … })play the roles of#xand@. Local dimensions start at 0: in the same example, a demand forCat[x=i, y=j]"is simply the value of sum_z(product_z) at context [z=0, [x=i,y=j]]". The local dimensionz, which is orthogonal toxandy, enters with its coordinate set to 0 (MDP §5.3, p. 91). From there, demand moves alongzonly as needed: demands atz=0give rise to demands atz=1, and so on (p. 91; the OCR is garbled, so this is paraphrased). So Lucid does supply a coordinate for a dimension the outer demand lacks, but only for a locally declared dimension, and only at its origin. eLucid8 has no local dimensions yet. If they are added, initialising at the origin is the Lucid precedent. Its rule of rejecting references to undeclared or unsupplied global dimensions (§2) is unaffected.L3. Demand-driven evaluation. "Demands for the values of parts of the program … generate demands for values of subexpressions" (W&A §2.7, p. 37). Eduction is "tagged demand-driven dataflow" (Wadge 2022). MDP restates eduction as a demand rule: "If a value e at context [c] is demanded at some stage, then and only then are values demanded that are known to directly determine value e at context [c]" (§5.3, p. 90). For
x = (if p then a else b fi) * y, a demand forx[i]demandsp[i]andy[i]; then, ifp[i]is true, onlya[i], and "eduction avoids the computation of value b[i]" (p. 90). A conditional being non-strict in the branch not taken is therefore documented [Lucid] behaviour. MDP Figures 5.1–5.3 (ch. 5) trace eductive evaluation of matrix multiplication stage by stage. They mark a demand as?and a computed value as*, each at its context[c], and distinguish demand propagation from value propagation, and single demands from multiple demands for the same value. This is a precedent for eLucid8's demand trace (§2). Source: the owner's text description of the figures; page numbers not yet recorded. In the walkthrough on p. 95, a demand forproductat[z=3, [x=i,y=j]]"results in demands for A at context [x=i,y=3] and B at context [x=3,y=j]". This is a concrete case of a context shift that remaps dimensions. The binary summation is evaluated through nested local dimensions ([t=1, [u=0, [x=i,y=j]]]). MDP then states that "a purely demand-driven realization of eduction … is generally necessary to deal with the call-by-need semantics needed to evaluate nonstrict and nonpointwise Lucid operators" (p. 95), before contrasting it with data-driven execution. Its example is a functionstagger, which advances one argument or the other depending on comparing their values. Evaluating it at timetis impossible "without examining the condition", so "a call-by-need semantics is necessary, which demand-driven execution implements" (MDP pp. 95–96). Data-driven execution would need all three arguments ofif p then a else b fito be available, "even though one is superfluous". For the non-pointwisex @ y, a data-driven implementation is "impossible to envision", because "the value of x that is needed is determined by the value of y" (p. 96). Rule from MDP: data-driven execution is usable only for "'predictable' operators—the strict and pointwise operators such as +, <, and next", for which it is possible "to predict, without semantic examination of the operator, which argument values … are necessary". It is used "locally, when possible and, when useful, with demand-driven execution being used everywhere else by default" (p. 96). For eLucid8 this is the criterion for batched tile demand (R5): a region may be batched only when the dependencies are predictable without evaluating values. Otherwise the evaluator must over-approximate (pure values only) or fall back to per-element demand, which is the default. This is why eLucid8 allows eager or speculative evaluation (cluster filling, batched tile demand in R5) only for pure values, with failures isolated, and never lets it change outcomes.L4. Unguarded cycles.
X = X + 1has least solution ⊥; "a demand-driven interpreter will cycle endlessly" (W&A §2.7, p. 38). eLucid8 instead rejects unguarded cycles. This is a deliberate departure. W&A describe a static precedent for such checks: many programs "pass a simple syntactic test [the 'cyclesum test' of Wadge (1981)] that ensures their robustness" against deadlock, while programs that fail it may still be deadlock-free for data-dependent reasons (p. 91). The test is also used to show that a transformed definition keeps a unique fixed point (pp. 141, 143). eLucid8's rejection of unguarded cycles is a test of this kind.L5. Logical time. "The Lucid time parameter cannot be understood as counting the ticks of some global clock" (W&A §5.10, p. 110). Time "is just a formal parameter, it has no necessary connection to wall clock time" (Wadge 2022). W&A judge Lucid "inherently unsuitable" for real-time problems and sketch hiatons as a possible extension (p. 111). eLucid8's clock mapping is a new design, not inherited.
L6. Warehouse. "The warehouse is an associative store indexed by the pair (V,t)". It was managed by a "retirement plan" that "discards values that haven't been used recently" (Wadge 2022). This is a precedent for point caching and eviction at the implementation level. MDP describes the same reuse in its Laplacian example: a value needed by two neighbours "can be computed only if it has not been computed before, and retained for future use until it is no longer needed in computing other values" (§5.3, p. 90). Abstract eduction engine (MDP §5.4, pp. 96–98):
- Suspended operation component (SOC): propagates demand. It holds each operation with placeholders for its arguments, demanded "at the appropriate context as determined by the operation itself". Pointwise operators use the result's context, while non-pointwise ones modify it.
- Variable value component (VVC): "responsible for retaining values of variables that have been already computed". A demand for a value already being computed is recorded, and "all pending demands for that variable value" are satisfied when it arrives.
- Execution component (EC): applies operations.
- eLucid8 correspondence: the evaluator's demand loop corresponds to the SOC,
PointCacheto the VVC, and equation execution to the EC. The VVC's coalescing of pending demands is the precedent for memoizing a service call within a query (§5), including calls still in flight once evaluation becomes concurrent.
L7. Error values. pLucid has a special
errorobject. "If the argument to an operator is the object eod (or the object error) then, except for certain operators, the object yielded by that operator will be the object eod (or the object error)". The exceptions areiserror,iseod, and the non-strictand/or(for example,false and error = false) (W&A Appendix C.3, pp. 194–195). eLucid8's error propagation (§2) agrees with pLucid's default. It has not yet decided whether any operators are non-strict. pLucid'seod(end of data) is not eLucid8'sUndefined(outside the domain), and neither is the same as ⊥ (non-termination).L8. GLU (Granular Lucid). Owner statement, 2026-10-02 (the owner was involved in GLU's development; no page citation yet, published account in Multidimensional Programming ch. 7). GLU programs could mix simple operators with coarse-grained nodes that ran chunks of C code; the coarse-grained nodes are what "granular" refers to. The GLU the owner worked on was not multidimensional: it had no space dimension. Later versions may have had several (see below). GLU is therefore a precedent for coarse-grained operations such as eLucid8's model-call adapter. It is not a precedent for multidimensional contexts or for cluster entries, which group stored values over a coordinate region. eLucid8 must keep the two kinds of granularity distinct: compute granularity (how much work one node does) and storage granularity (point vs. cluster entries). Reconciled, 2026-10-02: MDP §8.4 (p. 147) says "a GLU program can have finitely many dimensions which can be handled by renaming". The owner confirms that some versions of GLU may have had multiple dimensions. So the statement that GLU was not multidimensional applies to the GLU the owner worked on, not to every version. Either way, GLU's "granularity" refers to coarse-grained code nodes, not stored blocks of values, so the conclusions above are unchanged.
L9. Errors in the warehouse. Owner statement, 2026-10-02 (no page citation yet). Eduction stored
errorandeodin the warehouse like any other value: they were first-class data objects, just as integers and floats were. This supports eLucid8's rule that a pureErroroutcome may be cached (§4). eLucid8's refusal to cache model-call failures is a new rule for effects, because such failures can be transient. Lucid's errors were deterministic. One difference remains open: in pLucid,erroris a value that programs can test withiserror. In eLucid8,Erroris currently an outcome that programs cannot inspect. Whether to make it first-class belongs with the deferred work on explicit error recovery.L10. Fault tolerance by recomputation. Owner statement, 2026-10-02, confirmed by MDP §6.2.4–6.3, pp. 110–112. Because a value is defined by equations, it can always be recomputed from scratch. If data in the warehouse went missing or a computation failed, the value was simply demanded and computed again. The warehouse was a cache, never the source of truth. This underlies eLucid8's rules that eviction is always permitted (§4) and that cache policy never changes results. Where eLucid8 differs: recomputation is guaranteed only for pure values. Source data and model-call outcomes are not defined by equations, so they cannot be recomputed this way. That is why eLucid8 pins source versions and keeps them while they are in use (§2), and records effect outcomes (§5).
- Omission faults (a lost demand, or a value lost before it satisfied its demand). These were tolerated by re-demand. After a suitable time with no evidence that an outstanding demand was still being processed, it was treated as lost and reissued (MDP §6.2.4, p. 111). This is temporal redundancy.
- Corruption faults. These were tolerated by issuing redundant identical demands, marked as redundant, and choosing the result by majority voting (MDP §6.2.4, p. 111). This is spatial redundancy.
- MDP attributes both to Lucid's "referential transparency and intensional nature" (§6.3, p. 112).
- Implications for eLucid8:
- Re-demand is always safe for pure values. For service calls it is safe only when the adapter declares the call idempotent, which is why retries in §5 are bounded and depend on the adapter's capabilities.
- Redundant demands with voting resemble running several service calls (for example, several models) and voting on the result. That is a possible later effect policy, outside v0.1.
- Lineage of the demo: MDP's worked example, Laplacian relaxation, defines
sat[time=T, [x=X, y=Y]]from the average of its spatial neighbours attime=T−1(p. 110–111). This is a direct Lucid precedent for eLucid8's time-varying spatial neighbourhood workload (§8) and for contexts with both time and space dimensions (L2).
L11. No tiles in Lucid. Owner statement, 2026-10-02. Lucid implementations never had blocks or tiles as data objects. Every value was stored and computed individually. The point/cluster distinction in §4 was formulated by the owner for eLucid8 in 2026.
L12. Usage counts. Owner statement, 2026-10-02, confirmed by MDP §5.3–5.4, pp. 90 and 98–99. Eduction kept a usage count for each data object, and an object could be collected once its count reached zero. This applied to all data, both internal and external. The warehouse's retirement plan was ideally based on usage counts, but in practice it often used other heuristics. For example, datons (warehouse values) older than a given age could be purged, because everything except external inputs could be recomputed (L10). eLucid8 adopts usage counts for source versions (§2). For pure cache entries the count is only an optimisation, because L10 means they can be evicted and recomputed at any time. For source versions it is required for correctness, because they cannot be recomputed. Heuristics such as age-based purging are therefore allowed for pure cache entries but never for source versions that are still in use. MDP's mechanism (pp. 98–99): each value in the VVC carries a usage count, the number of times it will be used, taken from the program. In MDP's example, each value of
xis used twice and each value ofyonce, except thatxattime=0is used only once. The count "is decremented whenever a value is used (more precisely, retrieved from the VVC). When the usage count drops to 0, the value can be discarded." Difference in eLucid8: MDP's count is the number of expected future retrievals, derived from the program text. eLucid8's count for a source version is the number of current holders: running queries that pin it, plus usable cache entries that read it. The Lucid count suits pure values whose future uses are predictable from the program. The holder count suits external data, whose future uses cannot be predicted. Bounded storage: in MDP's matrix-transposition example (Atrans,C,Bdefined by chainedrealigns), "each value of A, B, and C is used only once", so each is discarded as soon as it is retrieved. As a result, "at most twice the storage would be required at any time … precisely the same as would be required by a parallel in-place matrix transposition scheme" (p. 99). Limit: "So far we have assumed that the usage count of each variable value can be determined at compile time. However, generally this is not the case" (pp. 99–100). MDP's example is a conditional,z = if p then x else y fi: whether a given value ofxoryis used at all depends on the run-time value ofp(p. 100). One approach starts every count at 1 and issues a "phantom" demand to the branch not taken, whose "only purpose … is to decrement usage counts". However, some programs make even phantom demand propagation need "possibly unbounded superfluous computation" (p. 100). Practical Lucid implementations therefore age values in the VVC and discard the oldest "when storage in the VVC is at a premium". A discarded value that is needed again "can be computed again, assuming the input values to the program are available". This aging can be combined with "a deterministic yet incomplete scheme": values with a precise usage count are discarded at 0, and the rest by a retention scheme (p. 100). This is the published form of the owner's account of the retirement plan. This is where heuristics such as the retirement plan take over. W&A pp. 67–68: the pLucid interpreter, "extended by A. Faustini at the University of Warwick", used a "retirement-age" garbage-collection scheme: a value is retired after surviving a dynamically adjusted number of collections without being referred to. "The storage-management strategy affects performance but not correctness. Owner statement, 2026-10-04: pLucid was written in C; eLucid8's production runtime follows it in a systems language, Rust (architecture v1, A2)." Owner statement, 2026-10-03: usage counts were determined statically from the equations, before eduction. Each occurrence of a variable in an equation adds one to its count: iny = 1, nothing usesy, so its count is 0;x = 1 fby x+1givesxa count of 1 (each value is used once, by the next);x = 1 fby x+1 fby x+2gives 2; addingz = 1 + xraises it to 3. The owner relates this to the cycle sum test. Owner statement, 2026-10-03 (continued): usage counts must be augmented with a retirement plan based on the age of the daton, because forif … then … else … fiit is not known in advance which branch will be taken, so counts there cannot be exact (as MDP p. 100 also says). W&A mention the cycle-sum test only for deadlock and unique fixed points (pp. 91, 141, 143). eLucid8 implication: the point cache currently keeps entries until they are evicted. Discarding pure entries by usage count is a later memory optimisation, and it can never change results (L10).
#10. Open questions
- Q2. Missing dimensions compared with eduction. Closed, 2026-10-02. In Lucid the case did not arise, because eduction only ever computed the contexts that were actually needed (L2). eLucid8 declares dimensions, so it guarantees the same property by rejecting, before evaluation, any reference that would need a coordinate the demand does not carry (§2). This is consistent with Lucid, not a departure. MDP adds that under first-principle semantics every dimension is a parameter of every demand, and dimensionality analysis then drops the ones a variable is insensitive to (§8.4, pp. 146–147; L2). A locally declared dimension enters at coordinate 0 (MDP §5.3, p. 91; L2).
- Q3. Tile demand propagation. Closed, 2026-10-02: the owner accepted rule R5 (see §4).
- Q4. Infrastructure faults are not outcomes. Closed: owner decision, 2026-10-02. For pure values, a runtime failure such as a lost worker, a crashed process or an evicted entry is never reported as an
Erroroutcome; the runtime re-demands instead, as Lucid did (MDP §6.2.4).Erroroutcomes are reserved for failures that the program's meaning produces (§2) and for effect outcomes (§5). This takes effect once evaluation is concurrent or distributed; the v0.1 evaluator runs in one process. - Q5. Should preparation latency affect deadlines? Closed: owner decision, 2026-10-02, option (a). Timing and runtime policy stay independent. Estimated or measured preparation latency never feeds the clock that decides deadline outcomes, so the guiding invariant (no runtime policy changes any outcome) holds for all outcomes in v0.1, including deadline outcomes.
- Q6. Which latency criterion for deadlines? Closed: owner decision, 2026-10-02. A bounded deadline-miss rate: a policy is acceptable if at most a configured share of queries exceeds the latency budget. The bound is a parameter; the demo uses 1% of queries over 25 ms. p95 was rejected because it can hide a very bad worst case.
- Q11.
tanhof small arguments: fdlibm 5.3 or msun's later branch. Closed: owner decision, 2026-10-04, option (b); found while checking the A5 port against the fdlibm 5.3 source text. [eLucid8] For 2⁻⁵⁵ ≤ |x| < 2⁻²⁸, fdlibm 5.3'ss_tanh.ccomputestanhthroughexpm1, which is within 1 ulp of x but not always x; FreeBSD msun (and V8, which derives from it) returns x there, which is the correctly rounded result. With the 5.3 branch,tanhdiffers from V8 on 12.7% of inputs with 2⁻⁶⁰ ≤ |x| < 2⁻²⁰ (1 ulp), on every machine. Options: (a) keep fdlibm 5.3 as written (the A5 decision's text; implemented); (b) take msun's branch (return xfor |x| < 2⁻²⁸), a one-line change in each port, and name it in the library's identifier. Decided: (b), since it is correctly rounded and matches V8 there; the identifier isfdlibm-5.3+exp1+msun-tanh. - Q10. Conditionals in clocked coordinates. Open, 2026-10-03; found while writing the Alps moving-window example. [eLucid8] C3's check accepts only the same clock's
#-coordinates (plus constants) when setting a clocked dimension, in a shift or an aggregate bound, sotime in (if #time < 23 then 0 else #time - 23)..#timeis rejected although it never crosses clocks. Proposal: accept any expression built from the same clock's coordinates and constants, including conditionals. Not decided. - Q9. Non-numbers in neighbourhood aggregates. Closed: owner decision, 2026-10-04. [eLucid8] The aggregates over declared ranges gave an
Errorfor a non-number element, but the older neighbourhood aggregates silently converted non-numbers. Nowsum,mean,minandmaxof a list with a non-number element are anErrortoo, andcountcounts any values (§2, "Non-numbers in neighbourhood aggregates"). No example, tutorial example or demo relied on the conversion. - Q8. Coordinates outside a declared range. Open, 2026-10-03; found while writing the tutorial. [eLucid8] A dimension's
rangeis used by neighbourhood truncation and tiling, but a query or shift outside it is evaluated anyway: withdimension x : int range 0..9,#x + #yatx = 20, y = 4gives 24. Options: (a) keep this, since a range is a hint for boundaries, not a domain; (b) make an out-of-range contextUndefined(outside the value's domain, §1); (c) reject out-of-range literal coordinates at compile time and giveUndefinedat run time. Proposal: (b), consistent withUndefinedmeaning "outside the domain". Not decided. - Q7. Placement-invariance and floating-point reductions. Open, 2026-10-03; raised by the training direction. [eLucid8]
- The rule today: every runtime policy (caching, tiling, vectorising, and placement, §6) must give identical outcomes. Vectorised kernels keep this exactly by mirroring the scalar operation order (V2).
- The concrete case:
sumover a neighbourhood or a batch, split across two devices, adds two partial sums. Floating-point addition is not associative, so(a + b) + (c + d)can differ from((a + b) + c) + din the last bits. A placement plan that splits a reduction can therefore change a value, and the difference can grow through a training recurrence. - Options:
- (a) Deterministic reductions: reductions always combine in canonical coordinate order, whatever the placement. Outcomes stay identical, at a cost in parallel speed.
- (b) A declared tolerance: placement may reassociate reductions, and results agree within a stated bound. This weakens the rule for all programs.
- (c) Exact by default, opt-in reassociation: a program marks the reductions that may be reassociated. Check mode compares those within a declared tolerance and everything else exactly.
- Proposal: (c), because it keeps today's guarantee for every existing program and makes any loss of exactness visible in the source. Not decided; it matters only once placement splits reductions (training stage 2).
- Update, 2026-10-03: for aggregates over declared ranges, the owner chose a fixed order of combination (A2), which is option (a) for those aggregates. The general question remains open.