Kinds — the dimensional type system
Every value in Flux carries a kind: a statement of what the value means, not merely how
it is stored. close is not “a float” — it is a price, a point on the price axis.
close − open is not “another float” — it is a level, a displacement along that axis.
rsi(close, 14) is a bounded oscillator, osc(0,100). Because meaning is tracked, the
compiler rejects meaningless arithmetic at compile time, reconciles branches and co-plotted
series soundly, and infers presentation — pane, scale, guides — from the kind alone. Kinds
are the keystone of the language: causality, totality, presentation inference and the
optimizer’s guarantees all lean on them.
The mechanism is general. The same machinery kinds calendar arithmetic, exact decimal
amounts, physical units (meas[u]), user-declared records and variants, and the APP plane’s
messages and views; market-data kinds are the flagship instantiation and serve as the running
illustrations here. This page is the normative reference for the kind system itself — its
two cooperating systems, its sorts and partial order, join and meet, its orthogonal tags, and
named declarations. Per-operator rules live in Operators; the inference
algorithm, the error policy in full, and the complete kind → presentation table live in
Inference.
New here? Start with Guide §5 — Kinds: types that carry meaning →
Two systems, one error channel
Shared, not drift — this section and The affine substrate below are the canonical statement of the two-systems / affine split; Operators carries the one-paragraph echo and links back here.
“Join and meet of binary operations” is a category error, and the kind system is built on
refusing it. price − price = level, yet price ⊔ price = price — subtraction and
unification answer different questions. Flux therefore separates two systems cleanly:
- (A) The coercion lattice
(K, ≤, ⊔, ⊓, ⊤, ⊥).a ≤ bmeans “acoerces tobwithout lying about itself”. The join⊔unifies — the branches of anif, the two arguments ofnz, series co-plotted in one pane. The meet⊓constrains — the kind a parameter demands, the intersection of oscillator bounds. The lattice answers “is there one kind that covers both?” — nothing more. - (B) The dimensional operator algebra for
+ − × ÷ < > ==— a rule table that computes the result kind of each operation from the kinds of its operands, in the style of classical dimensional analysis. This is not the join: it can move across the lattice (price − price → level) or refuse outright (price + osc → ⊤). - A single error channel:
⊤. Every undefined rule in (B) and every incompatibility in (A) produces the same top element⊤. A⊤is a hard error in a demanding position — an operand, aplottarget, an argument — and an operand is itself a demanding position, which is whyclose + rsi(…)fails at the+, whether or not anything downstream reads it. A⊤that reaches no demanding position at all is a warning ([WarnTop]), not a failure.
Read the two rules above together and one thing follows that is easy to miss, and worth stating
outright, because it is the difference between a type system that is strict and one that is
merely loud: a mixture of dimensions never reaches ⊤. Add a price to an oscillator and the
algebra has no rule, so you get ⊤ and a hard [ErrDim]. But reconcile a price and an
oscillator — take one branch of an if or the other — and the join does not fail. It erases the
dimension and lands on quantity: a real kind, a number that remembers it is a number and has
forgotten what of. That is a [WarnBranchDim], and the value flows.
The hard channel is for nonsense that is certain; the soft one for a mixture that is merely
suspicious. What separates them is not consumption — it is where the ⊤ was born.
⊤ still appears in a reconciliation, and is still hard when it does: an if whose branches
cross sorts (a price and a colour) has no common kind at all, and no erasure can save it.
Figure — the coercion lattice (A) and the operator algebra (B) meet only in the single error
channel ⊤;
price − price → level flows through B, never through A.
Why this rule exists. If
−were modelled as a join,close − openwould have kindprice ⊔ price = price— a displacement mislabelled as a point, and every downstream rule (price + level → price, overlay placement, axis choice) would silently go wrong. If joins were modelled as algebra,if c then ema12 else ema26would need an “operator rule” for a question that is really “do these two branches share a kind?”. Keeping the two systems separate — and letting them fail into one shared⊤— reconciles a total lattice (every pair of kinds has a join) with a system that still rejects nonsense (that join is⊤exactly where no honest common kind exists).
fast = volume > sma(volume, 20)
spread = close - open // price − price → level (algebra, system B)
line = if fast then ema(close, 12) else ema(close, 26)
// price ⊔ price → price (join, system A)
plot spread, line
// ✗ plot close + rsi(close, 14) — [ErrDim]: point + dimensionless has no affine meaningThree failures, and they are not the same failure:
fast = volume > sma(volume, 20)
mix = if fast then close else rsi(close, 14) // ⚠ [WarnBranchDim] — no common dimension, so
plot mix // the join ERASES it: mix : quantity. It flows.
// ✗ plot close + rsi(close, 14) — [ErrDim]: the ALGEBRA has no rule for point + dimensionless.
// Hard, at the `+`, read or not: an operand is a demanding
// position, and there is nothing suspicious about this — a
// point plus a bare number is not a thing.
// ✗ if fast then close else up — [ErrDim]: price ⊔ color = ⊤. Crossing SORTS is not a mixture
// the lattice can erase — there is no common kind to land on.[WarnTop] is the lint that sits on top of the first case: a binding whose dimension was erased
and which nobody reads is almost always a mistake in the making, so leave mix unplotted and you
get a second warning saying exactly that. Consume it and the [WarnTop] goes away — the
[WarnBranchDim] stays, because the erasure is still there.
A ⊤ also propagates as a kind poison without re-diagnosing: one root fault produces one
message, and every node it contaminates stays silent (the anti-cascade rule; see
Inference for the typable cone that keeps live preview working around it).
The affine substrate
The asymmetry of the algebra is not a convention — it is geometry. The price axis is a one-dimensional affine space:
priceis a point on that axis,levelis a vector — a displacement between points,ratiois a dimensionless scalar.
Affine geometry then hands the core rules over for free: point − point = vector
(price − price → level), point ± vector = point (close + atr(14) → price), point ÷ point
= scalar (price ÷ price → ratio), and “point + point” does not exist at all — which is
exactly why close + rsi(close, 14) is refused rather than computed.
Above the affine pairs, a dimension is an element of an abelian group of integer
exponents over the five generators {P, V, T, I, rad} — price, volume, time, ordinal bar
index, angle. Multiplication adds exponent vectors, division subtracts them, so × and ÷
are total on dimensions (price × volume → pv is P·V; pv ÷ volume → price is
P¹V¹−V¹). Only +, − and the order comparisons demand equal dimension, because adding
metres to kilograms — or points to displacements of a different axis — has no meaning.
gap = close - close[1] // level pt(P) − pt(P) = vec(P)
band = close + 2 * atr(14) // price pt(P) + lit·vec(P) = pt(P)
rel = close / open // ratio P⁰ — dimensionless, centered on 1
flow = close * volume // pv P·V by the group law
speed = change(close, 5) / (5 bars) // slope vec(P) ÷ vec(I) = P·I⁻¹ — price per barThree affine pairs live in the antichain, one per axis: price/level on P, time/
duration on T, and barindex/barspan on the ordinal axis I (rule A1 — the x axis
is affine too, so barindex − barindex → barspan, and a regression slope is
level ÷ barspan → slope = P·I⁻¹, price per bar, deliberately distinct from P·T⁻¹).
The sorts
The lattice is stratified into sorts: fine structure inside each sort, and across sorts
the join is ⊤ and the meet is ⊥. The extrema are shared by all sorts: ⊤ (any — the
error channel, hard when demanded) and ⊥ (never — the kind of na, which inhabits every
kind: na : ∀κ.κ enters the lattice at ⊥ and subsumes upward to whatever is expected).
The scalar sort
Everything in the scalar sort sits below quantity, which sits below ⊤.
The dimensionless spine — lit < {ratio, osc(lo,hi), signal, dir, depth} < num:
lit— a const-folded literal, polymorphic in dimension:lit ≤safeevery scalar, dimensioned or not. This is what makesclose + 10,close > 30000andnz(x, 0)legal without ceremony: the literal adopts the kind its context gives it.ratio— dimensionless, centered on 1, multiplicative (price ÷ price, band width).osc(lo,hi)— dimensionless and bounded, an interval-parameterized family (rsi → osc(0,100),cmo → osc(-100,100),cmf → osc(-1,1)). See the interval sub-lattice below.signal— a boolean event,{0,1}(comparisons,cross_up,rising,in_session).dir— a direction,{-1, 0, +1}(rule A6): categorical-discrete, the flat sibling ofsignal(dir < num). It types the direction field of trend structures (superTrend(…).dir), is presented as bar coloring or marks, and is discriminated by comparison (st.dir == 1), never bymatchand never by<. (There is no unary+in the grammar: the value+1is written1.)depth— a dimensionless z-intention tagged@z, produced by analysis and consumed by the presentation z channel (projected in 3-D, flattened in 2-D).num— dimensionless of unknown role: the least upper bound of the spine.kdj(…).jand calendar accessors land here.
The dimensioned antichain — each element ≤ quantity, each ≥ lit, and all pairwise
incomparable: price = pt(P) · level = vec(P) · volume = V (signed values allowed:
obv) · pv = P·V (money-flow) · time = pt(T) · duration = vec(T) · barindex = pt(I)
· barspan = vec(I) · slope = P·I⁻¹ · angle = rad — plus every composed dimension
the group law can produce (P², P²·V⁻¹, …), which are full plottable kinds presented in an
auto pane labelled by their exponents, without warning (rule A3).
quantity — the erased dimension, least upper bound of the whole scalar sort. It is
what a dimensionally incompatible join produces (see L2 below): still plottable, but
suspect, and always flagged. quantity is reserved for genuinely erased or mixed
dimensions; a precise composed dimension like P² never degrades to it.
The categorical sorts — color, clock, string
Categorical kinds are flat (each is one element, joining only with itself) and live outside arithmetic; each is consumed by a dedicated eliminator rather than computed with:
color— an RGBA style value, consumed by fills, bar coloring and style channels (Color).clock— a resampling schedule: an ordinal index with constructorstf("1d"),renko(box),pnf(box, rev),range(r). Aclockis a first-class value (you can select one with anif, take one as aninput) but it is eliminated only by@(Time and state); it is never plotted and never compared.string— bounded, immutable UTF-8 text (rule A12): labels, prompts, alert messages.stringis flat likecolorand outside numeric arithmetic, with exactly one operator overload:string + string → stringis concatenation — the single categorical line of the+rule table. There is nostring < string(locale-dependent collation is excluded by determinism), equality is bit-equality, and astringis consumed by text-expecting channels — it is never plotted as a series (Text).
The structural sorts — vec and record
Structural kinds compose componentwise; mismatched shapes are nonsense and resolve to the extrema.
vec<κ>[n]— a vector ofκwith const-folded capacityn(n ≤ N_max = 10 000). Capacity, not exact count: see below. Introduced bywindow(e, n), list literals,vec.fill(N, x)andvec.range(N); covariant inκ.Notation.
vec<κ>[n]is the metalanguage of this documentation. In source, the same kind is writtenvec(κ, N)— with parentheses, because<and>are exclusively the comparison operators and never type delimiters.record{f₁:κ₁, …}— a named product. This sort is load-bearing for the catalogue: a large share of the built-in kernels return records —bollinger → record{upper, middle, lower: price},macd → record{macd, signal, hist: level},superTrend → record{st: price, dir: dir},adx → record{adx, plusDi, minusDi: osc(0,100)}. Projection ise.f; functional update ise with { f: v }(shape-preserving — unlisted fields carry over, unknown fields are[ErrField]).
The APP-plane sorts — variant and ui
The APP plane adds two sorts by the same construction, directly under ⊤, without
reopening the sealed core lattice:
variant{Tag₁:κ₁ | …}— the tagged sum, categorical dual ofrecord(the product). Its introduction is the constructor injectionTag(e), its eliminator ismatch(symmetric to projection). Order, join, meet and⊤-propagation are derived identically torecord: label sets are invariant (same labels → refine per payload, covariantly; different label sets → incomparable, join⊤, meet⊥). Messages, commands and subscriptions of an app arevariants (The APP plane).ui— the view-primitive sort: a closed, vetted set of primitives, flat likecolor, consumed by the host renderer, and never coercible to a scalar (ui ⊔ κ = ⊤,ui ⊓ κ = ⊥across sorts — a primitive in scalar position is caught immediately).
No function sort
There is deliberately no arrow kind in the lattice. Lambdas are second-class: a
(p⃗) -> body is admitted only where a higher-order kernel expects a function
(window/fold/map/scan/loop, vec.where, sortBy, …) and is inlined at that site.
A lambda can therefore never be a record field, a vec element, a variant payload or a
let-bound value — which is what keeps every value representable as plain data, every buffer
bounded, and compiled artifacts free of closures and function pointers. Recursion goes
through def (whose call graph must be acyclic — [ErrTotalRec]) or through bounded
scan/loop, never through a self-referencing lambda.
The kind catalogue
One line per kind. The full presentation table (pane/overlay/scale/guides/css class per kind) is normative in Inference; the last column here is the one-line summary.
| kind | meaning | dimension | typical producers | presents as |
|---|---|---|---|---|
lit |
const-folded literal, dimension-polymorphic | adopts context | numeric literals, const params | adopts its consumer |
ratio |
dimensionless, centered 1, multiplicative | P⁰ | close/open, bbw, vortex |
pane around 1, guide 1 |
osc(lo,hi) |
bounded oscillator (interval family) | dimensionless | rsi, stochastic, mfi (0,100) · cmf (−1,1) |
pane, fixed [lo,hi], midline + refined guides |
osc(-∞,∞) |
unbounded centered-0 oscillator (A4) | dimensionless | roc, cci, trix, coppock, fisher, kst |
pane, auto scale, guide 0 |
signal |
boolean event {0,1} |
dimensionless | comparisons, cross_up, in_session |
marks / fills / bar color — never a line |
dir |
direction {-1,0,+1} (A6) |
dimensionless | superTrend(…).dir, SAR side |
bar coloring / marks — never a line |
depth |
normalized z-intention (@z) |
dimensionless | (analysis-produced; consumed by z) | z axis in 3-D, flattened in 2-D |
num |
dimensionless, role unknown (spine LUB) | dimensionless | kdj(…).j, calendar accessors |
pane fallback, auto scale |
price |
point on the price axis | pt(P) | close, ema, vwap, bollinger fields |
overlay on the shared price axis |
level |
price displacement | vec(P) | close−open, atr, stdev, macd fields |
pane centered on 0 |
volume |
base-asset count (signed allowed) | V | volume, obv, ad |
pane, signed auto scale, guide 0 |
pv |
money-flow | P·V | eldersForceIndex |
pane, auto, guide 0 |
time |
instant on the timeline | pt(T) | time |
x axis / annotation — never a series |
duration |
exact elapsed time (machine) |
vec(T) | time − time[n] |
x axis / annotation |
barindex |
ordinal bar position | pt(I) | rollingExtremaIndex |
x axis / anchoring — never a series |
barspan |
bar count | vec(I) | barssince |
pane counter [0,max] |
slope |
price per bar | P·I⁻¹ | lrSlope (= level ÷ barspan) |
pane centered on 0 |
angle |
angle with unit @deg|@rad |
rad | chopZone (angle@deg) |
style channel / pane [-π,π] |
| composed dims | any other exponent vector (A3) | ℤ-vector over {P,V,T,I,rad} | price×price (P²), eom (P²·V⁻¹) |
auto pane labelled by exponents |
quantity |
erased / mixed dimension (scalar LUB) | erased | lossy joins, non-normalized affine sums | pane fallback — the erasure stays visible |
color |
RGBA style value | — | color constructors, gradients | consumed by fill / bar color |
clock |
resampling schedule | — | tf, renko, pnf, range |
never plotted — eliminated by @ |
string |
bounded immutable UTF-8 text | — | literals, interpolation, fmt.* |
consumed by labels/alerts — never a series |
vec<κ>[n] |
fixed-capacity vector | element κ | window, list literals, vec.fill |
reduced / indexed, or a declared representation |
record{…} |
named product | per field | bollinger, macd, adx, superTrend |
exploded per field |
variant{…} |
tagged sum (APP plane) | per payload | Msg/Cmd/Sub, labelled inputs |
consumed by match — never plotted |
ui |
view primitive (APP plane) | — | button, panel, scene{…} |
consumed by the host renderer |
⊤ |
any — the error channel | — | failed joins, missing algebra rules | not presentable — [ErrPlot] |
⊥ |
never — the kind of na |
— | na |
not presentable |
decimal(scale), period, the asset tag (B,Q[,@v]), meas[u] and metric[id] are
orthogonal tags on these kinds, not kinds of their own — see Orthogonal
tags below.
The partial order
Figure — the kind lattice: every sort under one ⊤ and over one ⊥; inside the scalar sort,
the dimensionless spine rises to
num while the dimensioned antichain rises only to
quantity, and lit sits safely below every scalar.
The single most consequential decision in the order: dimensions form an antichain, and
num is incomparable to every dimension — price ⊀ num. Dimensioned kinds rise to
quantity, never to num.
Why this rule exists. If
price ≤ numheld, thensignal ⊔ price = num— a boolean event and a price would silently reconcile into an ordinary plottable number, and the error detection that motivates the whole system would evaporate at exactly the moment it matters (a co-plot or anifmixing incompatible things). By routing all dimensioned kinds toquantityinstead, a mixture is never silent: in arithmetic it is refused ([ErrDim]), and in branch reconciliation it compiles toquantityand is flagged ([WarnBranchDim]) — visible, plottable, suspect.
Descending the order gains information, and presentation always reads the lowest kind known — which is why inference synthesizes the minimal (principal) kind and coerces only at consumption sites (see Inference).
Two tiers of coercion edges
Every edge of the order is strictly rising, and each edge belongs to exactly one of two disjoint tiers:
≤safe— silent.⊥ ≤ κ ≤ ⊤for every κ; wideningoscbounds (osc(0,100) ≤ osc(-100,100)); wideningveccapacity (vec<κ>[k] ≤safe vec<κ>[N]fork ≤ N); the spine edges{ratio, osc, signal, dir, depth} ≤ numandnum ≤ quantity; andlit ≤every scalar.≤lossy— compiles, warns, offers a quick-fix. Erasing a dimension (D ≤ quantityfor any dimensionedD), and accepting a non-normalized affine combination. Loss of information is legal but never silent.
Antisymmetry survives because both tiers rise strictly and never overlap.
Literal polymorphism
lit is the ergonomic floor of the scalar sort. A const-folded literal coerces safely into
any scalar, so the natural things are legal without ceremony:
hot = close > 30000 // price vs lit → the lit reads as price → signal
lift = close + 10 // lit adopts price → price
safe = nz(change(close, 1), 0) // level ⊔ lit = level
shift = rsi(close, 14) - 50 // osc(0,100) − lit → osc(-50,50): interval propagatedThe compiler still lints impossible literals against claimed bounds (rsi(close,14) > 150 is
[WarnLit]), because a lit adopting a kind does not suspend arithmetic sanity.
Capacity, not length: vec<κ>[n]
The n in vec<κ>[n] is a const-folded capacity — an upper bound, not an exact count.
A shorter vector inhabits a longer one through the ≤safe widening
vec<κ>[k] ≤safe vec<κ>[N] (k ≤ N), its tail [k, N) reading na. Memory stays bounded
by the declared cap, so totality holds; bounded iteration (map/fold) is na-aware on the
capacity, so a widened tail contributes nothing. [ErrLen] fires only when two declared
capacities are genuinely incompatible — otherwise the shorter operand widens to the longer.
This is also why list literals of different lengths join cleanly:
vec<κ>[k] ⊔ vec<κ>[m] = vec<κ>[max(k,m)].
w = window(close, 20) // vec<price>[20] — a specific produced length
wide = nz(w, window(close, 50)) // vec<price>[50] — 20 widens into 50, tail naNon-const or over-cap lengths are [ErrTotal] (n ≤ N_max = 10 000) — the price of “total,
bounded memory” is that a capacity is always a compile-time fact.
Join and meet
The join catalogue
κ ⊔ κ = κ ⊥ ⊔ κ = κ ⊤ ⊔ κ = ⊤ lit ⊔ κ = κ
osc(L,H) ⊔ osc(L',H') = osc(min L L', max H H') — the envelope
spine siblings → num (osc ⊔ ratio = num · dir ⊔ signal = num)
D ⊔ D' (D ≠ D') = quantity D ⊔ num = quantity signal ⊔ price = quantity
vec<S>[k] ⊔ vec<T>[m] = vec<S ⊔ T>[max(k,m)]
record{f:A} ⊔ record{f:B} = record{f: A ⊔ B} — differing field sets → ⊤
variant: same label set → per-payload join — differing label sets → ⊤
across sorts → ⊤Two placements deserve their reasons:
signal ⊔ price = quantity, not⊤(correction L2).quantityis a common upper bound of both (signal ≤ num ≤ quantity,price ≤ quantity), so⊤would not be the least one — and the error policy wants exactly this outcome: mixed-dimension branch reconciliation is a warning ([WarnBranchDim]on a plottablequantity), while mixed dimension in an arithmetic position stays a hard[ErrDim]. The boundary between hard and soft runs between algebra and reconciliation, not through the lattice.dir ⊔ signal = num, notquantity. Both are dimensionless spine leaves belownum, sonumis their least upper bound — exactly asosc ⊔ ratio = num. Promoting toquantitywould put a larger element above an existing upper bound and destroy uniqueness of the LUB.
Joins land at ⊤ exactly where genuine nonsense lives: across sorts (price ⊔ color,
num ⊔ record{…}), on mismatched structural shapes (field or label sets that differ),
and — the one enumerated in-sort exception — on equal dimension with differing
representation tags ([ErrRepr], below). Mere dimension mixtures never reach ⊤; they
rise to quantity and stay visible.
c = volume > sma(volume, 20)
a = if c then rsi(close, 14) else cmo(close, 14)
// osc(0,100) ⊔ osc(-100,100) = osc(-100,100) — envelope
b = if c then close else rsi(close, 14)
// price ⊔ osc = quantity — compiles, [WarnBranchDim], suggest two panesThe meet catalogue and the lit correction
⊤ ⊓ κ = κ ⊥ ⊓ κ = ⊥
quantity ⊓ D = D num ⊓ ratio = ratio — a constraint refines
osc ⊓ osc = interval intersection (disjoint → lit, by L1)
incomparable scalars ⊓ = lit — correction L1
across sorts → ⊥ (lit does not cross sorts)
variant/record: same shape → per-component meet — differing shapes → ⊥Correction L1 is load-bearing: the meet of two incomparable scalars — say
price ⊓ volume, or dir ⊓ signal — is lit, not ⊥. lit ≤safe both operands, so
lit is a common lower bound, and the greatest lower bound must sit at or above every common
lower bound; forcing ⊥ would contradict lit ≤ price ∧ lit ≤ volume ⇒ lit ≤ price ⊓ volume
and break the absorption law (price ⊓ (price ⊔ level) = price ⊓ quantity = price holds
precisely because meets refine rather than annihilate). Within the oscillator family the same
correction resolves the empty intersection: two osc kinds with disjoint intervals are
incomparable scalars, so their meet is lit — the empty interval ∅ is the bottom only of
the interval sub-lattice viewed in isolation, never the global GLB.
The osc interval lattice
osc(lo,hi) is a family ordered by interval inclusion:
osc(L,H) ≤ osc(L',H') ⟺ [L,H] ⊆ [L',H']. Join is the envelope, meet is the
intersection, and bounds may be infinite constants: osc(-∞,∞) (rule A4) is the
kind of the entire percent-change family (roc, cci, trix, coppock, fisher, kst,
chaikinVolatility, volumeOscillator) — dimensionless, centered on 0, additive, which
distinguishes it cleanly from ratio (centered on 1, multiplicative) and from num (role
unknown).
Bounds are presentation claims, not runtime invariants. The kind osc(0,100) asserts
“this is presented on a fixed 0–100 scale with a midline”; only an explicit clamp makes a
bound real at runtime, and arithmetic honestly propagates claimed intervals
(rsi − 50 → osc(-50,50)). Rule A5 splits the catalogue accordingly: a bound genuinely
enforced by the computation (rsi, stochastic, mfi, cmf) yields a fixed [lo,hi]
scale; a merely conventional bound (adx, correl, balanceOfPower) is a claim —
auto-scale plus indicative guides. A bound that failed to const-fold would fall back to
num; no kernel in the catalogue produces one.
Deep ⊤ propagation
vec<⊤>[n], record{f: ⊤, …} and variant{T: ⊤ | …} all reduce to ⊤. Without this,
a buried mismatch — vec<price> ⊔ vec<color> producing vec<⊤> — would slip beneath every
κ ≠ ⊤ guard and surface as a runtime mystery instead of a compile-time diagnosis. An error
at any depth is an error of the whole value.
Closure, verified by enumeration
With L1 and L2 in place, every pair of kinds has a unique LUB and a unique GLB. The kind
set is finite by structural family: the dimensional core is finite, and every
parameterized axis — osc bounds, vec capacity, decimal scale and precision, the
representation tags, the asset-tag components and the fx pair annotation — is a
const-folded tag whose keys are enumerable at the point of verification. The lattice laws
(antisymmetry, absorption, uniqueness of LUB/GLB) are therefore machine-verified by
exhaustive enumeration, family by family — not merely argued
(Guarantees). Named record/variant declarations preserve this:
their reference graph must be acyclic ([ErrTotalType]), so every named kind flattens to a
finite-height structure.
The operator algebra in brief
The full, normative per-operator tables are in Operators. The shape of
system B, in one screen — a missing rule means ⊤, hence [ErrDim] when consumed:
±(equal dimension; affine):pt(d) − pt(d) → vec(d)·pt(d) ± vec(d) → pt(d)·vec ± vec → vec·ratio ± ratio → ratio·osc ± osc →propagated interval ·lit ± x → x· differing dimensions →⊤. One categorical overload:string + string → string(concatenation), and nothing else touchesstring.- Affine combinations (
(high + low) / 2): the expression is normalized toΣ λᵢ·ptᵢwith const-folded coefficients;Σλ = 1 → price(a weighted point),Σλ = 0 → level(a pure displacement), anything else →quantity+[WarnAffine]. Multiplying by alitpreserves the role, sosma(close,20) + 2 * stdev(close,20)types asprice + level → price. Rule A7 applies this after inlining, so recursive composites type correctly:dema = 2·ema − ema(ema)hasΣλ = 1 → price. ×and÷(the group law, rule A2): exponent vectors add and subtract, so both operators are total on dimensions —D × ratio → D,price × volume → pv,price × price → P²(plottable, A3),pv ÷ volume → price,pv ÷ price → volume,level ÷ barspan → slope.- Order comparisons (
< > <= >= cross_*): admitted only on the ordered scalar sort with compatible dimensions (κₐ ⊔ κ_b ∉ {quantity, ⊤}) →signal.price < oscis[ErrDim];color,clock,string,record,vec,variant,uiare never ordered;diris categorical-discrete and outside order. - Equality (
== !=): decidable equality →signal— bit-equality for scalars,string,dir,color; deep,na-aware equality forrecord/vec/variant(mismatched shapes →[ErrDim]);clockanduiare consumed, never compared ([ErrArg]). - Logic (
and or not):signal × signal → signal— there is no arithmetic onsignal.
trend = ema(close, 50) // price ([CallPoly]: α = price)
hits = count(close cross_up trend, 20) // osc(0,20) — const-folded n
since = barssince(close cross_up trend) // barspan — a bar count, not a num
bad = close < rsi(close, 14) // ✗ [ErrDim] — price ⊔ osc = quantityOrthogonal tags
Beyond its dimension, a kind can carry up to three orthogonal tags, plus one axis that
lives only on ratio:
- a numeric representation tag —
f64(default) ordecimal(scale)(rule A11); - a time representation tag on the
Tdimension —machine(default; exact elapsed time —duration) orcalendar(period, rule A10); - an asset tag — a fixed-arity string-keyed n-uple
(B, Q [, @v])on price-dimension kinds (rule A9);
plus (4) the currency-pair annotation of fx — an axis carried only by ratio,
whose top is the bare ratio itself. Since ratio never carries an asset tag, no kind ever
carries four tags: a decimal price[BTC,USD] holds three (representation + asset +
dimension), and that is the ceiling.
Figure — the tag axes around one kind, and the two regimes: representation tags refuse to
join (
[ErrRepr]), asset components widen while ± demands identity ([ErrDim]).
The two axes families obey two different regimes — and the difference is the point:
- Representation tags (1, 2) never coerce silently. On two kinds of equal dimension but
differing representation tags, join and meet produce
⊤— surfacing as[ErrRepr]in a demanding position. The only bridge is an explicit conversion (toDecimal/toFloat; theperiodconstructors for calendar time). This mirrors the arithmetic refusal: you do not add anf64 priceto adecimal price, or a machinedurationto a calendarperiod, without saying so. - The asset tag (3) widens per component. Each component has its own top
(
⊤base,⊤quote,⊤venue); a join widens the component that differs and preserves the one that matches —price[B1,Q] ⊔ price[B2,Q] = price[⊤base, Q]— and never raises[ErrRepr]. Safety comes not from the join but from the tagged±algebra:+,−, the comparisons and the affine rule all demand component-wise identical tags, and refuse with[ErrDim]otherwise. Widening keeps composition possible (two venues’price[BTC,USD]can form an index); the identity gate still catches every unit bug.
For both regimes, identical tags fall back to the plain dimension rules with the tag
preserved through ± × ÷: decimal price − decimal price = decimal level,
period + period = period, price[BTC,USD] − price[BTC,USD] = level[BTC,USD].
Numeric representation — f64 and decimal(scale)
decimal(scale) is exact fixed-point, orthogonal to dimension: a decimal always has
a dimension, and the group law is unchanged (money ÷ qty → price with money ≡ pv,
qty ≡ volume). The scale rides the kind and the system computes it: ± takes the max of
the two scales, × sums them, and ÷ is the one non-closed operation — it requires an
explicit target scale and rounding mode. Declared precision picks the narrowest backing
(i64/i128/i256); exceeding the declared precision yields a deterministic na plus a
diagnostic, never a silent wraparound. Literals are suffixed: 1.50d is decimal(2), 1.5
is f64. Decimal’s domain is settled money — order amounts, fills, balances; the analysis
kernels remain f64.
exact = toDecimal(close, 2) // decimal(2) price — explicit entry, rounds
bad = close + exact // ✗ [ErrRepr] — f64 price + decimal price
worse = if c then close else exact // ✗ [ErrRepr] — the join refuses the same mixture
fine = toDecimal(close * volume, 2) + toDecimal(close * volume, 2) // decimal(2) pv — a money amount adds; same representation, scales combineTime representation — machine and calendar
time = pt(T) is an instant (an int64 epoch under the hood); duration = vec(T) is
exact elapsed machine time. period is the same vec(T) tagged calendar — a
months-and-days quantity that is aware of zones and daylight-saving transitions. Both
t + duration and t + period are dimensionally pt(T) + vec(T) → pt(T); the tag is what
separates “exactly 24 hours later” from “the same wall-clock time tomorrow”, and the two
never mix silently:
age = time - time[20] // duration — machine-exact
renew = time + time.months(1) // time — calendar arithmetic, DST-aware
// ✗ (time - time[20]) + time.months(1) — [ErrRepr]: a duration and a period do not addperiod values are produced only by the constructors time.years/months/weeks/days(n)
(composable: time.months(1) + time.days(10)); calendar accessors (year, dayOfWeek, …) project a
time into a declared zone as num, and out-of-range calendar arithmetic yields a
deterministic na, never a wraparound. The pinned time-zone database and the shared
conversion routine that make this reproducible are specified with the compute pillar
(Compute).
The asset tag — (B, Q [, @v])
A price is never a bare number: it is a rate, quote-per-base. The asset tag makes that identity dimensional, per kind:
| kind | tag | reading |
|---|---|---|
price[B,Q], level[B,Q] |
base + quote | a rate and its displacement — both components ride |
volume[B] |
base only | a count of the base asset — no currency |
pv[Q] |
quote only | a money amount — the base cancels in price × volume and is deliberately dropped |
The quote rides every dimension containing the P factor (including composed P², slope);
dimensionless kinds carry no asset tag at all. The default is mono-asset
price[primary, baseccy], byte-identical to a single-series script that never mentions the
axis; tags become concrete only where series keys are static literals, and widen to
component tops otherwise. The venue component @v is opt-in (default off; absence is the
concrete composite level, not ⊤venue).
c = volume > sma(volume, 20)
btcUsd = series("BTC-USD").close // price[BTC,USD]
btcEur = series("BTC-EUR").close // price[BTC,EUR]
ethUsd = series("ETH-USD").close // price[ETH,USD]
diff = btcUsd - btcUsd[1] // level[BTC,USD]
mixA = btcUsd + ethUsd // ✗ [ErrDim] — mixing assets
mixQ = btcUsd + btcEur // ✗ [ErrDim] — mixing currencies
cmpQ = btcUsd < btcEur // ✗ [ErrDim] — comparisons gate on identity too
rel = btcUsd / ethUsd // ratio — cross-base relative strength, tag dropped
either = if c then btcUsd else btcEur // price[BTC,⊤quote] — the join widens, never errorsfx and money — existing kinds, tagged
Currency conversion needs no new sort. fx[Q1/Q2] is the kind ratio carrying an
optional, ordered currency-pair annotation — the fourth tag axis, living only on ratio,
top = the bare ratio. money[Q] is an alias for decimal pv[Q]. Zero new sorts, zero
new lattice height. An fx arises from a division of same-base, different-quote prices, or
from a feed declared as an fx series; conversion is a type-checked unit cancellation:
c = volume > sma(volume, 20)
btcUsd = series("BTC-USD").close
btcEur = series("BTC-EUR").close
eurGbp = series("EUR-GBP").close // an fx series, declared as one
usdPerEur = btcUsd / btcEur // fx[USD/EUR] — same base, quotes differ
inUsd = btcEur * usdPerEur // price[BTC,USD] — the shared quote cancels
flipped = 1 / usdPerEur // fx[EUR/USD] — the reciprocal edge
avg = if c then usdPerEur else eurGbp // ratio — differing pairs join to bare ratioA pair mismatch in ×/÷ widens to ⊤quote rather than erroring (the × regime never
hard-fails); the pair annotation is excluded from the comparison identity gate, so two fx
rates always compare as the ratios they are. The complete edge catalogue — derivation,
reciprocal, triangular chaining, conversion in both operand orders — is in
Asset & currency.
Unit and metric annotations
Two further tag axes extend the same construction beyond finance, both carried by num
alone, both with the bare num as top:
meas[u](rule A14) — generalist quantities: a unit annotation whoseuis a product of catalogue symbols with integer exponents (meas[m],meas[m·s⁻¹],meas[kg·m·s⁻²]), augmented by an affinepoint|deltabit for zero-arbitrary scales (aunit.tempC(20)point versus aunit.tempCDelta(5)displacement — the same point/vector discipline asprice/level, applied to temperature).±and comparisons gate on identical units;×/÷compose exponent maps with exact pinned conversion factors and cancel fully toratio; mixed products with the financial dimensioned kinds are an explicit wall ([ErrDim]). This is the canonical algebra; the unit catalogue, the conversion verb and the entrance maps are its author-facing surface, specified in units.
warm = unit.tempC(20) + 5 // a point ± lit → point — the lit reads as a delta
span = unit.tempC(25) - unit.tempC(20) // point − point → a 5-degree deltametric[id](rule A15) — identity annotations for non-price series (macro indices, on-chain metrics, analytics). Same-id±preserves, differing ids refuse ([ErrDim]-class),metric[A] ÷ metric[B] → ratio, and kind-preserving families carry the annotation through. The axis is deliberately held inert — no non-price series enters the ANALYSIS plane until the amendment is armed; its design is specified with Asset & currency.
Named records and variants
Structural kinds can be declared and named at program level:
record Band { upper: price, middle: price, lower: price }
variant Tool { Select | Draw | Erase }
variant Save { Saved | Failed(code: num) }
t = Tool.Select // qualified constructor — resolves homonyms
s = Save.Saved // across variants that share a labelA nullary constructor is read as a value of its variant; a payload-carrying constructor is
applied like a function, its payload checked against the declared field kinds. The
qualified form T.C selects the constructor of the named variant T — resolved at
name resolution, before field projection ever applies, so label collisions across variants
are a non-problem.
Two disciplines keep named declarations inside the sealed lattice guarantees:
- Records are monomorphic in v1. A
defthat projects a field requires a concrete record kind known at inference. An open-row extension (row polymorphism) is a named additive extension that lands later — never a v1 prerequisite. - The reference graph must be acyclic. A declared
record/variantmay reference other named declarations, but any cycle — direct or transitive — is rejected at name resolution with[ErrTotalType], the kind-level twin of the acyclicdefcall graph ([ErrTotalRec]). Every named kind therefore flattens to a finite-height structure, and the finite-height property that closure and machine verification rest on extends to user declarations unchanged.
record Node { next: Node } // ✗ [ErrTotalType] — cyclic reference, unbounded heightA worked example: the lattice refuses wrong physics
Point & Figure charting is a stress test the kind system passes without a single new kind —
and it shows the dimensional lens doing real work. Every piece kinds naturally:
pnf(box, rev) is a clock; the column state is a scan over a plain record; and the box
size must be a level, because the geometry says so:
box = atr(14) // level — a displacement, by construction
anchor = close // price — a point
edge = anchor + 3 * box // price ✓ — pt(P) + lit·vec(P) = pt(P)
// the column state the frontier belongs to — a plain bounded scan over a record
state = scan({ dir: 1, extreme: close, count: 0 },
(p) -> if close > p.extreme + box
then { dir: 1, extreme: close, count: p.count + 1 }
else p)Suppose you had declared the box a price. Then anchor + box would be price + price —
point + point, no affine meaning — and the compiler answers [ErrDim] on the spot. The
lattice does not merely permit the correct model; it refuses the incorrect one. The same
lens types the rest of the state: count is a dimensionless box count (num), so
count * box is num · vec(P) → level and extreme + count * box lands back on price;
and count is deliberately not a barspan — boxes are not bars, and the kinds keep the
two countings from ever being confused.
Errors and na at the kind level
The error policy in full — including causality, totality and firewall diagnostics — is specified in Inference; the kind-level skeleton is:
- Hard (
[ErrDim],[ErrRepr],[ErrLen],[ErrField],[ErrArg],[ErrPlot],[ErrTotalType],[ErrTotalMatch]): a load-bearing guarantee is violated or the nonsense is certain — a⊤/⊥in a demanding position, a violated side condition, a shape mismatch. - Warning (
[WarnTop],[WarnAffine],[WarnBranchDim],[WarnBoundsØ],[WarnLit]): suspect but possibly intended, or a recoverable information loss — exactly the≤lossytier and the dead-⊤case.
na deserves its kind-level statement: na synthesizes ⊥ and subsumes into every expected
kind, so absence is a value of every kind, never a separate shape. Runtime na propagates
through arithmetic with the kind preserved, and every comparison touching na is itself
na — never true or false. You test absence explicitly:
m = sma(close, 200) // price — na during warm-up
odd = m == na // legal, but always na — never true
hit = is_na(m) // signal — the way to ask
has = is_some(m) // signal — its dualA match on a possibly-na scrutinee must cover it (an na or _ arm), and destructuring
an na record yields na in every field — absence composes structurally, with no special
cases to memorize.
See also
- Operators — the full dimensional algebra per operator, UFCS,
with. - Inference — bidirectional kind inference, presentation inference, the
error policy,
na. - Time and state — streams, delay, clocks and
@, causality. - units — the
meas[u]surface: the unit catalogue, conversions, entrance maps. - Asset & currency — the asset tag,
fxandmoney, venues. - The seven design pillars — how dimensional joins the other pillars.