Ask three libraries for the sum of the same float array and you can get three answers — not wrong ones, just different fold orders rounding differently. Mica refuses the roulette: every reduction walks one documented tree, and that tree is part of the language’s contract.
The example is
examples/Reductions.
Build and run it:
make -C examples/Reductions runThe tree
A span is cut into blocks of 128 consecutive elements (ReductionBlock,
an exported constant); a block folds left to right; block results combine
pairwise, the split handing the left side half the blocks rounded down.
Two consequences fall out by construction:
- serial = parallel, bit for bit — carriers compute whole subtrees at fixed boundaries, and the join replays the same combines;
- a float sum’s rounding error grows with the logarithm of the length, where a plain left fold grows it linearly.
numpy and Julia both ship pairwise summation; Mica’s difference is that the tree is documented and promised, not an implementation detail free to drift.
One verb family, every one-dimensional shape
Sum(series) { a fixed array }
Sum(ramp) { a Vector }
Sum(grades) { a dynamic array }
Sum(Span(series, 0, 4)) { any sub-range, through composition }
Sum(Span(m, 0, 5)) { a Matrix: its elements as one row-major span }The verbs take the language’s own borrowed view, so anything that admits
into a span reduces — and the Span composition you
write is literally the recursion spine the tree itself runs on. A matrix
has no second verb family: its elements admit as one contiguous span,
numpy’s semantics on Mica’s admission machinery.
The comparison verbs ride the same tree
Minimum, Maximum, ArgMin, ArgMax carry (value, index) pairs up the
same tree, and ties answer the first winning index — the only
order-free deterministic answer:
marks[1] := 9; marks[3] := 9;
at := ArgMax(marks) on fail use -1; { 1 — the first 9 wins }The empty view splits by the error charter
The sum of nothing is the additive zero, because mathematics says so. A
mean, an extreme, or a winning index over nothing does not exist — so
those verbs carry fails MathError and the call site answers the absence:
WriteLn(" Sum(empty) = %lf", Sum(empty)); { 0.000000 }
total := Mean(empty) on fail use -1.0; { the absence, answered }A fixed array or a tensor is never empty — a dimension counts at least one element — so the empty case arrives exactly where it can: dynamic data and narrowed windows.
What this does not do
- No axis parameter. A runtime axis cannot select a compile-time
shape. Per-axis reductions are spelled as views —
Sum(RowView(address m, r))— which is Eigen’s design, one verb over one span. - No integral
Mean.Meandemands a fractional element (5210at the call otherwise); an integral mean spells its conversion, keeping float32 fidelity intact. - No tolerance anywhere. The tree is exact IEEE at every node; comparing against an expectation uses the same arithmetic the runtime uses.
Try it
Replace Sum(series) with a hand-written left fold and compare: the two
answers differ in the last bits, and the tree’s is the one the parallel
run reproduces. The multicore proof is the harness’s
ReductionMulticore case — four tasks computing the tree’s own quarters,
joined bit-exactly at every carrier count.
Next
The verbs read views; the slicing page is where views and copies get their names.