task::cores(), Par::fold, Pieces of Bytes; Pieces Are Claimed While the Work Runs, and Order Is Kept Unless the Program Says order: par::Order::AnyStatus: Accepted; D9 (a fold’s step and merge return the accumulator or change it in place) added October 20, 2026
Date: October 19, 2026
Answers: #717, how a program that is not a grammar folds over all cores the way
par_fold does, and how the pieces of either are handed out
Refines: ADR-296 D10-D11 (par_fold: one piece per core,
cut before the work starts; the name cpu::count()), ADR-235
(Par[T] has Seq[T]’s surface)
Related: ADR-312 (user_parallelism), Part III 13.3b
(user-pool), ADR-366 (a grammar reads Bytes),
ADR-367 (a fold’s counters), Part I 5.1 (options after ;),
Part II 10.7, 12.6
let table = b.par_pieces(b'\n').fold(Table, fn(t, piece) { scan(t, piece) }, Table::merge;
order: par::Order::Any)
A fold that every core runs at once - a count, a sum, a table of totals - is
the most common shape of parallel work over one large input. Nikaia has it
only inside a grammar (par_fold, Part II 10.7). A program that scans bytes
itself cuts its own pieces, runs par_iter().map over them and merges a list
of results: one accumulator per piece, made and cleared for each, pieces of
equal size handed out before the work starts, and no way to ask how many
cores there are.
par_iter().fold(identity, step).reduce(identity, merge);
ranges are halved and stolen by idle threads, each stolen half gets a fresh
accumulator, results are joined in input order. with_min_len bounds a
piece. std::thread::available_parallelism() counts the cores.fetch_add, merged in whatever order the threads
end - correct because the merge commutes.stream.parallel().collect(...), with unordered() to drop the
order a merge would otherwise keep; Runtime.availableProcessors().reduction(+: x) with schedule(static | dynamic, n).runtime.NumCPU() and goroutines.task::cores() is the number of workers parallel work uses.task::cores() -> i64 is the size of the pool that runs user code: at
user_parallelism = yes, user-pool from the runtime configuration, where
0 means the threads the process may run on (its CPU affinity and quota, not
the machine’s total); at no, 1. It is at least 1. It answers how many
pieces run at once here, which is what a program sizes its work by; the
machine’s total is not that number where a container or an affinity mask
limits it. The name ADR-296 D11 wrote, cpu::count(), is this function.
Par::fold(init, step, merge) folds with nothing shared.let summary = rows.par_iter().fold(Summary, fn(acc, r) { acc.record(r) }, Summary::merge)
init makes an accumulator - a type, called with no arguments, as in
par_fold, or a function fn { … } - and is called once for each
accumulator, never copied from one value. step takes an accumulator and an
element and returns the accumulator, or changes it in place (D9); merge
takes two and returns one, or changes the first (D9). All
three are what a par_iter lambda is (Part II 12.6): they may not pause
(NK2209) and may not change a name outside them (NK2107). Each
accumulator is used by one worker at a time and shared with nothing, so
step changes it without a lock. The result is the merged accumulator; over
no elements it is one made by init.
Bytes ends at a byte the program names: b.par_pieces(sep).b.par_pieces(sep: u8) is a Par[Bytes] over b cut into pieces that each
end just after a sep, the last one at b’s end; every byte of b is in
exactly one piece, and no piece is empty. It is how a scan written by hand
gets what a grammar’s @frame gives par_fold: whole records per piece,
cut while the work runs. fold, map, for_each and the rest of Par
apply as to any other.
par_fold, Par::fold and par_pieces do not cut their input into one
piece per worker before the work starts. A worker that is done takes more;
a slow core holds back only the piece it is on. How large a piece is, the
runtime decides; the option piece: (D6) bounds it. This replaces ADR-296
D11’s blind split into n ranges; its seam repair stays: a piece of a
par_fold rule starts after the first boundary at or after its first byte.
order: par::Order::Any.par::Order is Kept | Any; a fold’s option order: takes it, Kept by
default.
Kept: each accumulator folds one run of the input that lies together,
in input order, and accumulators are merged in input order. The result is
the sequential fold’s whenever merge is associative and init is its
identity - the promise a written par_fold makes today. Pieces are still
claimed while the work runs (D4): a worker that takes a run not adjacent to
its last starts a new accumulator.Any: one accumulator per worker, kept for every piece that worker
claims, wherever in the input it lies; accumulators are merged in the order
workers finish. The program promises that the result does not depend on the
order in which elements reach step and accumulators reach merge: a
merge that commutes as well as associates, and a step that does not
care which element came first. The compiler does not check that, as it does
not check associativity. Where the promise is false, the result may differ
from run to run; nothing else breaks.Kept is the default because it asks of the program only what par_fold
already asks, gives the same answer on every run, and keeps collect-shaped
folds - one that gathers rows into a list - correct without a word. Any
is the opt-in for a fold whose accumulator is costly to make or to merge,
such as a hash table of totals: one per worker instead of one per run.
After ; (Part I 5.1), with defaults:
| Option | On | Default | Says |
|---|---|---|---|
order: par::Order |
Par::fold, par_fold |
Kept |
D5 |
workers: i64 |
Par::fold, par_fold |
task::cores() |
at most this many accumulators are folded at once |
piece: i64 |
par_iter, par_pieces, par_fold |
the runtime’s | the size a piece is cut near: elements for a list, bytes for Bytes and a grammar’s input |
workers: and piece: tune; on a program that keeps D5’s promise they
change no result. A value below 1 is a panic at the call that names the
option. workers: above task::cores() makes more accumulators than run at
once, nothing more.
user_parallelism = no each is its sequential fold.Par::fold, par_pieces(…).fold and par_fold run as one fold: one
accumulator, made by init, every element in order, merge never called.
Both orders give that answer. The options are read and do nothing.
A par_fold rule whose input fails reports the failure at the smallest
position, under Kept and under Any alike, as a sequential parse would:
pieces before a found failure are still parsed, pieces after it may be
dropped.
step and merge return the accumulator or change it in place; their result type says which.Changed by ADR-views-3 D3, D4: the in-place form is written fn(mut A, T), fn(mut A, A); ref mut stands only inside extern "C".
rows.par_iter().fold(Summary, fn(acc, r) { acc.with(r) }, Summary::joined) // returns
rows.par_iter().fold(Summary, fn(acc, r) { acc.record(r) }, Summary::merge) // in place
step and merge are each one of two forms, told apart by what they return:
step(acc, x) -> A and merge(a, b) -> A hand back the
accumulator, which replaces the one they were given.step of type fn(mut A, T) and merge of type
fn(mut A, A) return nothing and change their first argument; merge
takes the second lent or keep. A lambda whose body returns nothing
is of this form, its first parameter mut.The two choices are independent: a returning step may go with an in-place
merge. A step or merge that returns a type other than the accumulator, or
returns nothing without taking the accumulator as mut, is refused as any
call with the wrong function type is (NK1102). The same holds for the step
and merge of par_fold and fold in a grammar (Part II 10.7). The result is
the same in both forms; which form the lowering runs is not visible.
Why. Both forms are already written: par_fold in examples/1brc.nika
uses methods that change self (fn record(mut self, m: Reading)), and an
accumulator that is a number or a small value reads best as a function that
returns it (fn(n, _) { n + 1 }). Asking a program to write the one form as the
other costs a wrapper per fold and, for a large accumulator, a move per element
that the lowering must then prove away. The function’s type already says which
form it is, so no keyword or second method name is needed. Rejected: returning
only (as D2 first said), which rewrites every method that changes self into
one that consumes and returns it; in place only (Rust’s rayon with
fold_with and &mut), which makes the accumulator of a number a
ref mut i64 the program cannot write (NK1257); two methods
(fold/fold_mut), a second name for what the type already says.
unordered, after ;: an option is named and has a value
(Part I 5.1).ordered: false: a false at the call says nothing about what is given
up; an enum names it and leaves room..unordered() (Java): a grammar’s par_fold has no
chain, and the two would be written differently.Any by default: faster where it applies, but a fold that gathers into
a list would change its answer from run to run with no error.SharedMut on a number grows a fetch_add is a
question of Part II 12.2, asked when it is asked.task::cores() as the machine’s total: a container or an affinity mask
makes it larger than what runs at once, and every program that sizes work
by it would oversubscribe.char (s.par_pieces('\n')): the same shape over
String, asked with ADR-016’s cuts.piece: is not written: a
measurement’s, not a rule’s.</content> </invoke>