Indexes¶
Indexes are finite label sets used for collections of values. They enable typed, dimension-safe operations over multiple related values.
Finite Label Indexes¶
Declare a finite index with named labels:
Labels conventionally use PascalCase and are namespaced by the index: Maneuver.Departure.
Named labels identify positions on an index axis. As an expression, a
qualified label is a self-typed constant index key of type
Key<Maneuver>. Like a constructor name, the same spelling also serves as a
syntactic selector in map/table keys, expected-fail keys, include index
bindings, and match patterns.
The label list is ordered: the sequence in which labels are declared is the
index order of the axis. Every index kind carries such an intrinsic order —
label declaration order here, coordinate order for
coordinate indexes, and ascending positions for
Fin(N). Order-sensitive operations, most
notably scan, follow this order, and it also
determines rendering order and the element order of
extern-function buffers.
Label order is semantic
{ Departure, Correction, Insertion } and
{ Insertion, Correction, Departure } declare different indexes:
scan accumulates in declared label order, so reordering the labels of an
index declaration changes downstream results. Reorder labels only when
the axis order itself should change.
No empty indexes
Every index has at least one element. A named index must declare a label,
Fin(0) is invalid, and coordinate constructors must produce at least one
coordinate. Completed indexed values are therefore always nonempty. If an
empty value reaches an aggregation internally, Graphcal reports X001 as a
compiler invariant violation; empty aggregation has no user-facing semantics.
Contextual syntax names
scan and unfold select recurrence syntax only as bare call heads followed
by (. range, linspace, step, and points are special only in
coordinate-index declarations; Fin is special only in Index positions.
Elsewhere these spellings remain ordinary identifiers.
Indexed Values¶
Annotate a type with [IndexName] to create an indexed value:
node delta_v: Velocity[Maneuver] = {
Maneuver.Departure: 2.46 km/s,
Maneuver.Correction: 0.12 km/s,
Maneuver.Insertion: 1.83 km/s,
};
Both param and node can be indexed.
An indexed value is a total map: every label of the index must appear
exactly once, and the compiler rejects missing or duplicate entries. The
written order of map entries is presentation only — the constructed value is
normalized to the index order, so listing Maneuver.Insertion first produces
exactly the same value. Only the index declaration determines the order; a
map or table literal cannot override it.
Element Access¶
Access a specific element with [Index.Label]:
Or with a loop variable, which is a key of the iterated axis:
For structural Fin indexes, constant-Int positions (@v[2]) are checked
statically, and a Fin loop variable supports additive
Fin-key arithmetic:
param v: Dimensionless[Fin(4)] = for i: Fin(4) { 1.0 };
node shifted: Dimensionless[Fin(3)] = for i: Fin(3) { @v[i + 1] };
Any Key-typed expression selects an element the same way; see
Key-Based Element Access.
for Comprehensions¶
Transform each element of an indexed value:
The result is a new indexed value with the same index.
For multi-axis comprehensions, the body may start with an explicit tuple-key
marker. The names are pure sugar and must exactly match the for bindings in
order; they do not introduce additional variables:
node v: Velocity[Maneuver, TimeStep] = for m: Maneuver, t: TimeStep {
(m, t) => @accel[m] * coord(t)
};
Aggregation Functions¶
Aggregation functions currently accept exactly one index axis. Numeric
aggregations require quantity elements. Most preserve the element dimension;
product raises it to the concrete axis cardinality. count accepts any
non-indexed element type and returns the exact cardinality as Int.
argmax and argmin return the location of the extremum as an
index key rather than its value.
| Function | Description | Result Type |
|---|---|---|
sum(values) |
Sum of all elements | Same dimension as elements |
product(values) |
Product of all elements | Element dimension raised to axis cardinality |
maximum(values) |
Maximum element | Same dimension as elements |
minimum(values) |
Minimum element | Same dimension as elements |
argmax(values) |
Key of the maximum element | Key<I> |
argmin(values) |
Key of the minimum element | Key<I> |
mean(values) |
Arithmetic mean | Same dimension as elements |
rss(values) |
Root sum square | Same dimension as elements |
count(values) |
Number of elements | Int |
On ties, argmax and argmin return the first extremum in index order, so
the identity @x[argmax(@x)] == maximum(@x) is guaranteed (axes are never
empty).
node total: Velocity = sum(for m: Maneuver { @delta_v[m] });
node largest: Velocity = maximum(for m: Maneuver { @delta_v[m] });
node combined_sigma: Velocity = rss(for m: Maneuver { @delta_v[m] });
node n: Int = count(for m: Maneuver { @delta_v[m] });
node normalized: Velocity = sum(@delta_v) / to_float(count(@delta_v));
node critical: Key<Maneuver> = argmax(@delta_v);
node critical_dv: Velocity = @delta_v[@critical];
Int and quantity arithmetic never mix implicitly. Use to_float(count(...))
when a scalar calculation needs the cardinality. A multi-axis value must first be
projected to a single axis with an explicit for comprehension (D021); total-
and partial-axis aggregation are not yet defined.
Index Keys¶
Every axis element is reflected at the term level as a key of type
Key<I>, where I is the axis — named, coordinate, Fin, or required.
This mirrors the Dim/Quantity(D) pattern: the axis stays a static,
type-level entity, while its elements become ordinary runtime values.
Key<I> is an ordinary value type. Keys can be stored in node and
const node declarations, passed to DAG params, carried in constructor
payload fields, and indexed themselves (Key<TimeStep>[Maneuver]). A key is
represented as its axis identity plus an element position — a coordinate key
carries the position, not the float coordinate, so re-indexing through a key
is exact and never a floating-point lookup.
index Maneuver = { Departure, Correction, Insertion };
param delta_v: Velocity[Maneuver] = {
Maneuver.Departure: 2.46 km/s,
Maneuver.Correction: 0.12 km/s,
Maneuver.Insertion: 1.83 km/s,
};
node critical: Key<Maneuver> = argmax(@delta_v);
node critical_dv: Velocity = @delta_v[@critical];
Labels Are Key Constants¶
A qualified label is a self-typed constant expression:
Maneuver.Departure : Key<Maneuver>. Exactly as a constructor name is both
an expression and a pattern, the label spelling keeps its selector role in
pattern-like positions — match arms, map/table keys, table headers and
slices — while everywhere else it is a Key-typed term:
Axis-less spellings are never keys implicitly: node k: Key<Fin(3)> = 1; is
rejected. Annotations check types, they do not convert integers into keys —
use the explicit formers below.
Loop Variables Are Keys¶
A loop variable has exactly one type: the key type of its axis.
| Binding | Loop variable type | Content extraction |
|---|---|---|
for m: Maneuver |
Key<Maneuver> |
opaque (equality and match only) |
for t: TimeStep |
Key<TimeStep> |
coordinate via coord(t) |
for i: Fin(3) |
Key<Fin(3)> |
integer via to_int(i) |
There is no dual use: coordinate arithmetic and comparison go through the
explicit extraction coord(t), and integer use of a Fin key through
to_int(i). In particular t == 0.5 s is a type error — write
coord(t) == 0.5 s. The unfold closure binders over coordinates
(prev_t, t) follow the same rule.
Equality and match¶
Keys of the same axis compare with == and !=. Keys of different axes
never compare, and keys have no ordering — extract the content first
(coord(t1) < coord(t2), to_int(i) < to_int(j)) when an order is needed.
node critical: Key<Maneuver> = argmax(@delta_v);
node is_critical: Bool[Maneuver] = for m: Maneuver { m == @critical };
A key of a concrete named axis drives exhaustive match, with the same
label patterns used for named-index loop variables:
node contingency: Dimensionless = match @critical {
Maneuver.Departure => 1.10,
Maneuver.Correction => 1.50,
Maneuver.Insertion => 1.25,
};
Keys of a required (pub(bind)) axis support indexing and equality only;
match needs the concrete label set.
Key-Based Element Access¶
Brackets accept any Key-typed term per axis position, mixing labels, loop
variables, and computed keys freely:
node peak_per_maneuver: Key<TimeStep>[Maneuver] = for m: Maneuver {
argmax(for t: TimeStep { @v[m, t] })
};
node v_at_peak: Velocity[Maneuver] = for m: Maneuver {
@v[m, @peak_per_maneuver[m]]
};
Access is governed by axis identity: Key<Maneuver> cannot index a Phase
axis. The one widening is structural: Key<Fin(N)> is accepted where
Key<Fin(M)> is expected when N <= M.
Evaluation granularity
Access with a compile-time-constant key (a label, a loop variable, or
key(...)) keeps fine-grained per-element DAG edges. Access with a
runtime key is a gather: the edge covers the whole indexed declaration.
Extractions: coord and to_int¶
The only key extractions are:
| Function | Signature | Applies to |
|---|---|---|
coord(k) |
Key<C> -> Quantity(D) |
coordinate axes over dimension D |
to_int(k) |
Key<Fin(N)> -> Int |
Fin axes only |
For a Fin key the position is the semantic content; for a coordinate key
only the coordinate is exposed, because exposing its position would couple
programs to the grid definition. A named key is opaque: it supports equality
and match, nothing else.
Introducing Keys¶
All introduction forms state their axis explicitly:
| Form | Checking | Fallible? |
|---|---|---|
Label expression Maneuver.Departure |
compile time | no |
| Loop variable | compile time | no |
argmax(v) / argmin(v) |
— | no (axes are non-empty) |
key(Fin(N), c) with static c |
compile-time range check | no |
fin_key(Fin(N), e) with runtime Int e |
runtime range check | yes |
floor_key(C, q) / ceil_key(C, q) |
runtime | yes (outside the axis range) |
nearest_key(C, q) |
runtime | no (total) |
key(Fin(N), c) takes a static constant and is fully discharged during
checking; an out-of-range position is a compile error. fin_key(Fin(N), e)
accepts a runtime Int and range-checks it during evaluation — a failure is
a per-node evaluation error, and downstream nodes receive
DependencyFailed. Named axes need no positional former (write the label),
and coordinate axes have no exact quantity-to-key cast: select a grid point
with an explicit search policy instead.
param readings: Pressure[Fin(8)] = for i: Fin(8) { 100.0 Pa };
node second: Key<Fin(8)> = key(Fin(8), 1);
param requested: Int = 5;
node channel: Key<Fin(8)> = fin_key(Fin(8), @requested);
node selected: Pressure = @readings[@channel];
The coordinate searches take a quantity of the axis dimension and return the
grid point below (floor_key), above (ceil_key), or closest to
(nearest_key) the query. floor_key and ceil_key fail when the query
falls outside the axis range; nearest_key is total, and a midpoint tie
resolves toward the axis start.
index TimeStep = range(0.0 s, 10.0 s, step: 0.1 s);
param event_time: Time = 3.47 s;
node before_event: Key<TimeStep> = floor_key(TimeStep, @event_time);
node near_event: Key<TimeStep> = nearest_key(TimeStep, @event_time);
No implicit runtime indexing
A runtime Int never indexes a Fin axis implicitly (@x[@n] is
rejected — write fin_key(Fin(N), @n)), and a quantity never looks up a
coordinate axis (@x[@t] is rejected — pick a policy with
nearest_key / floor_key / ceil_key). Statically discharged
constant positions such as @x[2] remain legal.
Fin-Key Arithmetic¶
A Fin key plus a static Nat constant is again a Fin key, with the bound
tracked exactly in the type:
The shift is exact and infallible — the bound grows in the type, so there is
no runtime check, wraparound, or clamp. It composes directly with indexing
and Fin widening:
param values: Velocity[Fin(5)] = for i: Fin(5) { 1.0 m/s };
node diffs: Velocity[Fin(4)] = for i: Fin(4) {
@values[i + 1] - @values[i] // i + 1 : Key<Fin(5)> — exact
};
node smoothed: Velocity[Fin(3)] = for i: Fin(3) {
(@values[i] + @values[i + 1] + @values[i + 2]) / 3.0
};
The fragment is deliberately exact: one key, +, static Nat constants.
Everything else is excluded for a stated reason:
i - 1— fallible at position 0, and Nat has no subtraction; restructure additively (takeT[Fin(N + 1)]input and produceT[Fin(N)]output).i * 2,i + j— only loose static bounds exist, which could not widen into the natural target axis.i % c,i / c— wraparound and truncation are data policies, not index arithmetic.i + @kwith runtime@k— a runtime offset escapes any static bound; writeto_int(i) + @k : Intand re-enter throughfin_key.
Named and coordinate keys have no arithmetic at all: for named keys it would
couple runtime behavior to label declaration order, and for coordinate keys
it is ambiguous (position or coordinate?) — use coord(t) and quantity
arithmetic instead.
Rendering and boundaries
Keys render at output boundaries as their label, position, or coordinate
(presentation only), and boundary inputs encode named keys by label,
Fin keys by position, and coordinate keys by coordinate. Keys do not
cross the experimental plugin ABI (same posture as Complex).
scan (Cumulative Fold)¶
scan computes a running accumulation across the index order:
Arguments:
- The indexed value to scan over
- The initial accumulator value
- A closure
|acc, item| exprthat combines the accumulator with each item
The result is an indexed value where each element is the accumulated result up to and including that element.
The source must have exactly one axis. scan never chooses an axis implicitly
from a multi-axis source; use an explicit for comprehension to select each
rank-one series first. The accumulator may itself be indexed. Its axes are
preserved after the source axis in the result:
index Element = { A, B };
node increments: Dimensionless[Maneuver] = for maneuver: Maneuver { 1.0 };
node initial: Dimensionless[Element] = for element: Element { 0.0 };
node state: Dimensionless[Maneuver, Element] = scan(
@increments,
@initial,
|previous, increment| for element: Element {
previous[element] + increment
}
);
The body must return exactly the accumulator type, including every state axis and its order. Each iteration reads the complete previous accumulator before constructing the next one.
The accumulation order is the index order, which is intrinsic to the index — never to how a particular value was written:
- Named index: label declaration order (
Departure, thenCorrection, thenInsertionfor theManeuveraxis above). - Coordinate index: coordinate order, from
starttowardend(descending when the step is negative). Fin(N): ascending positions#0through#(N-1).
Because indexed values are normalized to index order, scanning a map literal
that lists Maneuver.Insertion first still accumulates Departure first.
Multi-Indexed Values¶
Values can be indexed by multiple label indexes using tuple keys:
index Phase = { Launch, Cruise, Arrival };
node spacecraft_mass: Mass[Phase, Maneuver] = {
(Phase.Launch, Maneuver.Departure): 5000.0 kg,
(Phase.Launch, Maneuver.Correction): 0.0 kg,
(Phase.Launch, Maneuver.Insertion): 0.0 kg,
(Phase.Cruise, Maneuver.Departure): 0.0 kg,
(Phase.Cruise, Maneuver.Correction): 4500.0 kg,
(Phase.Cruise, Maneuver.Insertion): 0.0 kg,
(Phase.Arrival, Maneuver.Departure): 0.0 kg,
(Phase.Arrival, Maneuver.Correction): 0.0 kg,
(Phase.Arrival, Maneuver.Insertion): 4000.0 kg,
};
Access elements with multiple index arguments:
Mixed Label and Coordinate Indexes¶
Values can combine categorical label axes with coordinate axes such as time.
Construction via for Comprehension¶
The most common way to create a mixed-index value is with a multi-binding for comprehension:
index Maneuver = { Departure, Correction, Insertion };
index TimeStep = range(0.0 s, 1.0 s, step: 0.5 s);
node accel: Acceleration[Maneuver] = {
Maneuver.Departure: 10.0 m/s^2,
Maneuver.Correction: 5.0 m/s^2,
Maneuver.Insertion: -3.0 m/s^2,
};
node v: Velocity[Maneuver, TimeStep] = for m: Maneuver, t: TimeStep {
@accel[m] * coord(t)
};
The coordinate loop variable t is a key of the TimeStep axis; the
quantity coordinate it stands on is extracted explicitly with coord(t)
(see Index Keys).
Construction via Map Literal with for Values¶
You can also use a map literal where each named-label entry contains a comprehension over a coordinate index:
node v: Velocity[Maneuver, TimeStep] = {
Maneuver.Departure: for t: TimeStep { @accel[Maneuver.Departure] * coord(t) },
Maneuver.Correction: for t: TimeStep { @accel[Maneuver.Correction] * coord(t) },
Maneuver.Insertion: for t: TimeStep { @accel[Maneuver.Insertion] * coord(t) },
};
Mixed-Axis Element Access¶
Access elements by providing both a label and a range variable:
Aggregation¶
Aggregate over either axis independently:
// Sum over the label axis for each time step
node total_v: Velocity[TimeStep] = for t: TimeStep {
sum(for m: Maneuver { @v[m, t] })
};
// Max over the time axis for each maneuver
node max_v: Velocity[Maneuver] = for m: Maneuver {
maximum(for t: TimeStep { @v[m, t] })
};
Table Literals¶
For multi-indexed values, the table expression provides a spreadsheet-like layout that is easier to read:
1D Table¶
param delta_v: Velocity[Maneuver] = table[Maneuver] {
Departure: 2.46 km/s;
Correction: 0.12 km/s;
Insertion: 1.83 km/s;
};
Named axes in table[...] accept the same full paths as indexed types, including module-qualified paths such as mission.Maneuver. Labels in ordinary table bodies are bare (Departure, not Maneuver.Departure) because table[...] explicitly supplies exactly one owner for each row or column axis. Rows are terminated with ;.
2D Table¶
param m: Mass[Phase, Maneuver] = table[Phase, Maneuver] {
: Departure, Correction, Insertion;
Launch: 5000.0 kg, 0.0 kg, 0.0 kg;
Cruise: 0.0 kg, 4500.0 kg, 0.0 kg;
Arrival: 0.0 kg, 0.0 kg, 4000.0 kg;
};
The last index becomes columns, the second-to-last becomes rows. The header row starts with : and lists the column labels, followed by data rows with RowLabel: value, value, ...;. Commas separate cells; the semicolon is the explicit row (second-dimension) delimiter, so do not add a redundant trailing comma before it.
3D+ Table¶
For three or more indexes, use slice sections with qualified labels:
param m: Mass[Time, Phase, Maneuver] = table[Time, Phase, Maneuver] {
[Time.T1]
: Departure, Correction, Insertion;
Launch: 5000.0 kg, 0.0 kg, 0.0 kg;
Cruise: 0.0 kg, 4500.0 kg, 0.0 kg;
Arrival: 0.0 kg, 0.0 kg, 4000.0 kg;
[Time.T2]
: Departure, Correction, Insertion;
Launch: 4800.0 kg, 0.0 kg, 0.0 kg;
Cruise: 0.0 kg, 4300.0 kg, 0.0 kg;
Arrival: 0.0 kg, 0.0 kg, 3800.0 kg;
};
Each [SliceLabel] section contains its own header row and data rows. Named
slice labels use Index.Variant syntax (or module.Index.Variant when the
index is imported); Fin slice labels use #N.
Finite-Index Tables¶
Use explicit Fin(N) axes for positional vectors and matrices. Their labels are
#0, #1, ... and are omitted from ordinary rows and columns:
// 1D Fin axis
param v: Dimensionless[Fin(3)] = table[Fin(3)] {
1.0;
2.0;
3.0;
};
// 2D, both axes finite
param m: Dimensionless[Fin(2), Fin(3)] = table[Fin(2), Fin(3)] {
1.0, 2.0, 3.0;
4.0, 5.0, 6.0;
};
// 2D, mixed: named columns, finite rows
param mixed: Dimensionless[Fin(2), Maneuver] = table[Fin(2), Maneuver] {
: Departure, Correction;
1.0, 2.0;
3.0, 4.0;
};
// 3D with a finite slice axis
param m3d: Dimensionless[Fin(2), Phase, Maneuver] = table[Fin(2), Phase, Maneuver] {
[#0]
: Departure, Correction;
Launch: 1.0, 2.0;
Cruise: 3.0, 4.0;
[#1]
: Departure, Correction;
Launch: 5.0, 6.0;
Cruise: 7.0, 8.0;
};
Slice labels (all but the last two axes) always require an explicit marker:
[Index.Variant] for named axes or [#N] for Fin axes. The same conventions
apply to multi-declaration shared axes: a Fin row axis has unlabeled rows, and
a Fin slice axis uses [#N] sections.
The table expression is pure syntax sugar -- it desugars to a map literal at parse time.
Multi-declarations¶
A multi-declaration is a single surface form that introduces N parallel param / node / const node declarations sharing the same row axis. It aligns values that belong together on the same row:
pub index Component = { ComponentA, ComponentB };
param power_consumption: Power[Component],
param duty_cycle: Dimensionless[Component],
const node mass_per_unit: Mass[Component]
= table[Component, (_, _, _)] {
: _, _, _;
ComponentA: 10.0 W, 0.5, 2.5 kg;
ComponentB: 12.0 W, 1.0, 3.1 kg;
};
- Each slot on the left-hand side is a full declaration: kind (
param/node/const node), name, and type annotation. - The
table[SharedAxis, (…)]bracket declares the row axis followed by a parenthesized slot tuple. Each tuple entry is either_(1-D slot typedT[SharedAxis]) or a named axis, including module-qualified axes (2-D slot typedT[SharedAxis, ExtraAxis]). - The header row
: …;has exactly one cell per column. For 1-D slots the cell must be_. Every 2-D slot cell must use the qualifiedExtraAxis.Variantform (including the full module path when imported), because one heterogeneous header can concatenate columns owned by different axes. - Data-row labels remain bare because the shared row axis is explicit in the table prefix.
Mixed 1-D / 2-D slots:
pub index Component = { ComponentA, ComponentB };
pub index OperationMode = { Safe, Nominal };
param power_consumption: Power[Component],
param n_installed: Int[Component],
const node mass_per_unit: Mass[Component],
param power_mode_active: Bool[Component, OperationMode]
= table[Component, (_, _, _, OperationMode)] {
: _, _, _, OperationMode.Safe, OperationMode.Nominal;
ComponentA: 10.0 W, 1, 2.5 kg, true, true;
ComponentB: 12.0 W, 2, 3.1 kg, false, true;
};
Currently, at most one slot may carry an extra axis; multiple adjacent extra-axis slots are planned for a later extension.
N-D with slice sections¶
When the shared-axis prefix has more than one axis, the body uses slice sections. Each slice section begins with a [Axis.Variant, …] label covering every shared axis except the last (which becomes the row axis), followed by a header row and data rows as usual.
pub index Phase = { Launch, Cruise };
pub index Component = { ComponentA, ComponentB };
pub index OperationMode = { Safe, Nominal };
param power_consumption: Power[Phase, Component],
param power_mode_active: Bool[Phase, Component, OperationMode]
= table[Phase, Component, (_, OperationMode)] {
[Phase.Launch]
: _, OperationMode.Safe, OperationMode.Nominal;
ComponentA: 5.0 W, true, false;
ComponentB: 6.0 W, false, false;
[Phase.Cruise]
: _, OperationMode.Safe, OperationMode.Nominal;
ComponentA: 10.0 W, true, true;
ComponentB: 12.0 W, false, true;
};
Slice labels must qualify each shared axis in the declared order (Phase.Launch, not bare Launch), matching the convention used for single-decl 3D+ tables.
Editor integration¶
Each slot in a multi-declaration is its own declaration for the purposes of navigation: gotoDefinition, findReferences, rename, and hover all land on the slot header, and each slot receives its own inlay hint at its name. Axis and qualified header/slice references participate in navigation and rename as well. The formatter preserves the multi-decl surface form while canonicalizing alignment and retaining every required axis qualifier. Cell-level inlay hints (projecting slot names into the header row of the source) remain future work.
- Multi-declarations are pure syntactic sugar: each slot desugars to an ordinary declaration with its own
table[SharedAxis] { … }initializer. Cross-slot references work exactly as for any other declarations (@other_slot[Variant]). - Attributes (
#[…]) are not allowed on a multi-declaration. Visibility is written per slot and follows the ordinary declaration rules (pub nodeis valid;pub paramandpub(bind) nodeare not).
Coordinate Indexes¶
Coordinate indexes carry statically known quantity coordinates. Bounds and
spacing must be finite, compile-time quantities with the same dimension.
Runtime inputs and integer (Int) values are not accepted.
Exact-Step range¶
Use range when the increment is authoritative:
index TimeStep = range(0.0 s, 1.0 s, step: 0.25 s);
index Countdown = range(1.0 s, -1.0 s, step: -0.5 s);
The step must be nonzero and point from start toward end. It must land on
the endpoint exactly within floating-point validation tolerance; Graphcal does
not clip or overshoot. For example,
range(0.0 s, 1.0 s, step: 0.6 s) is rejected. Interior coordinates are derived
directly from start + position * step, avoiding cumulative drift, and the
validated final coordinate preserves the declared endpoint.
Count-Based linspace¶
Use linspace when the number of points is authoritative:
This produces exactly five coordinates and preserves both declared endpoints.
points must be a static positive Nat. A singleton is valid only when the
endpoints are identical:
Both constructors support ascending and descending coordinates. Concrete index cardinalities are limited to 1,000,000 elements to prevent accidental massive allocations.
Structural Finite Indexes¶
Fin(N) is an explicit structural index with integer positions 0 through
N - 1. It is distinct from the Nat value N: Graphcal never converts a
bare Nat into an Index implicitly.
// A 3-element vector
param v: Dimensionless[Fin(3)] = for i: Fin(3) { 1.0 };
// A 2-by-3 matrix
param m: Dimensionless[Fin(2), Fin(3)] =
for i: Fin(2), j: Fin(3) { 1.0 };
Write Fin(N) in every Index position, including indexed types, for
bindings, tables, Index-sorted generic arguments, and bindings for required
discrete index ports. Obsolete forms such as Dimensionless[3],
for i: range(3), and table[3] are rejected with a Fin(3) suggestion.
Fin(0) and cardinalities above the practical limit are invalid.
Generic Sorts Stay Distinct¶
A Nat argument remains a Nat, while Fin(...) is an Index argument:
type Vector<N: Nat, D: Dim> {
Vector(values: D[Fin(N)]),
}
type IndexedVector<I: Index, D: Dim> {
IndexedVector(values: D[I]),
}
param a: Vector<3, Dimensionless>;
param b: IndexedVector<Fin(3), Dimensionless>;
Consequently, Vector<Fin(3), Dimensionless> and
IndexedVector<3, Dimensionless> are sort errors rather than implicit
conversions.
Nat Arithmetic and Iteration¶
The cardinality inside Fin(...) may use Nat addition and multiplication, such
as Fin(N + 1) or Fin(Rows * Cols). Multiplication binds more tightly than
addition. Nat expressions are normalized to canonical polynomial form.
Subtraction is deliberately unsupported; express the larger side additively,
for example use D[Fin(N + 1)] for an input and D[Fin(N)] for a smaller
output.
A Fin(N) loop variable is a key of type Key<Fin(N)> (see
Index Keys). It can index the corresponding value, and its
integer position is extracted explicitly with to_int(i):
node doubled: Dimensionless[Fin(3)] =
for i: Fin(3) { @v[i] * 2.0 };
node positions: Dimensionless[Fin(3)] =
for i: Fin(3) { to_float(to_int(i)) };
Additive Fin-key arithmetic tracks the bound in the type, so shifted access is checked statically:
param values: Velocity[Fin(4)] = for i: Fin(4) { 1.0 m/s };
node diffs: Velocity[Fin(3)] =
for i: Fin(3) { @values[i + 1] - @values[i] };
Fin axes compose with named indexes:
index Phase = { Launch, Cruise };
node data: Dimensionless[Fin(3), Phase] =
for i: Fin(3), p: Phase { 1.0 };
Required Indexes¶
An index can be declared without specifying its labels or coordinates. These are required indexes — they must be bound via a parameterized include when the DAG is used as a library.
Required Discrete Index¶
This declares an unconstrained discrete index port Axis. A caller can bind it
to a concrete named index, forward another compatible required discrete index,
or supply a structural axis such as Fin(3) directly. Coordinate axes use the
dimension-constrained form below instead.
Required indexes form the library's bindable interface and must carry
pub(bind) (see Visibility, Bindability, and Input Ports).
Omitting the annotation — or writing plain pub — is error V002.
Required Coordinate Index¶
This declares a coordinate index Step constrained to dimension Time. The
including DAG must bind it to a range or linspace index with the same
dimension. A generic outer DAG may instead forward its own compatible required
coordinate index; that outer input must eventually be bound to a concrete axis.
Using Required Indexes¶
Required indexes are used exactly like concrete indexes — as axes in type annotations, for comprehensions, index access, match, and map/table literals:
pub(bind) index Phase;
param cost: Dimensionless[Phase];
pub node total: Dimensionless = sum(for p: Phase { @cost[p] });
The file cannot be evaluated standalone. It must be included with a binding that supplies a compatible index (see Index Bindings).
unfold (Recurrence Relations)¶
unfold computes values over an explicit coordinate index where each value
depends on the previous state:
node x: Dimensionless[TimeStep] = unfold(
TimeStep,
@x0,
|prev_x, prev_t, t| prev_x * (1.0 + @rate * (coord(t) - coord(prev_t)))
);
The first coordinate receives @x0. At every later coordinate, the body
receives the previous value together with the previous and current
coordinate keys (Key<TimeStep>); their quantity coordinates are extracted
with coord(...), exactly as for coordinate loop variables. The axis
belongs to the expression; the enclosing declaration's type is not consulted
to select it, so unfold can be nested or consumed immediately:
node total: Dimensionless = sum(
unfold(TimeStep, @x0, |prev_x, prev_t, t| prev_x + @rate * (coord(t) - coord(prev_t)))
);
Use the prev_x binding directly when computing the next step. A reference to
@x in its own initializer is an ordinary dependency cycle, including
@x[prev_t], @x[t], and future-coordinate forms. This is useful for
time-stepping simulations and discrete dynamic systems.
Indexed recurrence state¶
The state may be indexed over one or more fixed axes. unfold prepends its
coordinate axis to those state axes:
initial: T[Element]
trajectory: T[TimeStep, Element]
initial: T[Row, Column]
trajectory: T[TimeStep, Row, Column]
This supports coupled systems where every next component reads the complete
previous snapshot. For example, the matrix-vector recurrence
x(next) = coupling * x(previous) is:
pub index Element = { A, B };
pub index TimeStep = range(0.0 s, 2.0 s, step: 1.0 s);
param initial: Dimensionless[Element] = {
Element.A: 1.0,
Element.B: 2.0,
};
param coupling: Dimensionless[Element, Element] = {
(Element.A, Element.A): 1.0,
(Element.A, Element.B): 1.0,
(Element.B, Element.A): 1.0,
(Element.B, Element.B): 0.0,
};
node trajectory: Dimensionless[TimeStep, Element] = unfold(
TimeStep,
@initial,
|previous, previous_time, time| for i: Element {
sum(for j: Element { @coupling[i, j] * previous[j] })
}
);
All components for one coordinate are constructed from the same frozen
previous value, so loop order cannot turn a simultaneous update into an
in-place update. The body must return exactly the initial state's element type,
axes, and axis order. This is ordinary multi-axis syntax, not a nested type such
as T[Element][TimeStep].
See the complete runnable
indexed_state_recurrence.gcl
fixture for indexed unfold and scan examples.
Aggregation Over Any Index¶
Use aggregation functions directly on indexed values:
Built-in aggregation functions (sum, minimum, maximum, argmin,
argmax, mean, count) work with any index type.