Fusion
pipe and flow run each step in order. Nothing fuses at runtime by
itself.
Fusion is a separate, optional build step: @stopcock/fp-compiler, a
Vite/Rollup/esbuild plugin. It recognizes a pipe/flow/compile call
built from supported array operators and replaces it with one
inlined loop before your code ships.
What the plugin does
Section titled “What the plugin does”Without the plugin, or for anything it doesn’t recognize, this call runs
three full passes over data:
pipe( data, A.filter((x: number) => x > 0), A.map((x: number) => x * 2), A.take(10),)With the plugin, the same call compiles to roughly:
const output: number[] = []for (let i = 0; i < data.length; i++) { const x = data[i] if (!(x > 0)) continue output.push(x * 2) if (output.length === 10) break}One pass, no intermediate arrays, and take(10) stops the loop as soon as
it has 10 results. pipe, A.filter, A.map, and A.take never get
called at runtime for a fully-lowered site like this one.
What compiles
Section titled “What compiles”The compiler recognizes chains built only from its supported operators:
element steps (map, filter, reject, filterMap, mapWhile,
flatMap, take, takeUntil, drop, takeWhile, dropWhile), full-array
boundaries (sort, sortBy, reverse, uniq, tail, init, flatten,
scan, without), and terminals (sum, count, reduce, forEach,
find, findIndex, findMap, every, some, none, head, last,
length, isEmpty, join, min, max).
A materializing operator like sort still needs the whole array before it
can produce anything, so a chain that includes one compiles to two loops:
whatever comes before it fused into the first, then the sort, then whatever
comes after:
pipe( data, A.filter((x: number) => x > 3), // fused into loop 1 A.map((x: number) => x * 2), A.sortBy((a: number, b: number) => a - b), // runs on the materialized result A.take(3), // exact semantics: runs after the complete sort)compilePure(A.sortBy(compare), A.take(k)) may replace that exact adjacent
pair with a bounded top-k rewrite, but only when you explicitly promise the
comparator is pure. Ordinary pipe and compile never do this on their
own.
When a chain doesn’t compile
Section titled “When a chain doesn’t compile”An unsupported operator, a dynamic step, a lexically shadowed import, or
anything the compiler can’t resolve statically leaves the site untouched:
it’s an ordinary runtime call to pipe, exactly as if the plugin weren’t
installed. Nothing breaks, it’s just not fused. Run stopcock check --strict
to see which call sites compiled, which bailed, and why.
Compiling never changes results, but it can change how many times a
callback runs and in what order if your callbacks have side effects. See
the @stopcock/fp-compiler README for the exact
interleaving contract, and Benchmarks for how
compiled chains compare to lodash, ramda, remeda, rambda, and ts-belt.