Fuse array callback chains into single loops (#919)

This commit is contained in:
Chris Tate
2026-10-13 08:18:50 -05:00
committed by GitHub
10 changed files with 2557 additions and 0 deletions
@@ -524,6 +524,7 @@ function arrayHofHelper(
if (existing) return existing;
const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(key, name);
lowerer.arrHofInfo.set(name, { method, elem, fnRet, arity, outElem, present, split: !!fastT });
const fn = buildArrayHofFn(
lowerer,
name,
@@ -1288,6 +1289,16 @@ function findIndexHelper(
if (existing) return existing;
const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(key, name);
if (!last)
lowerer.arrHofInfo.set(name, {
method: "findIndex",
elem,
fnRet,
arity,
outElem: elem,
present,
split: !!fastT,
});
const arrT = arrayOf(elem);
const fnT = funcOf([callbackValueType(lowerer, elem, present), F64, arrT].slice(0, arity), fnRet);
@@ -1362,6 +1373,15 @@ function someEveryHelper(
if (existing) return existing;
const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(key, name);
lowerer.arrHofInfo.set(name, {
method,
elem,
fnRet,
arity,
outElem: elem,
present,
split: !!fastT,
});
const arrT = arrayOf(elem);
const fnT = funcOf([callbackValueType(lowerer, elem, present), F64, arrT].slice(0, arity), fnRet);
const callF: IrExpr = predicateToBoolean(
@@ -1552,6 +1572,15 @@ function flatMapHelper(
if (existing) return existing;
const name = `%arr.flatMap.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(key, name);
lowerer.arrHofInfo.set(name, {
method: "flatMapArray",
elem,
fnRet,
arity,
outElem: fnRet.elem,
present,
split: !!fastT,
});
const arrT = arrayOf(elem);
const inner = fnRet.elem;
const fnT = funcOf([callbackValueType(lowerer, elem, present), F64, arrT].slice(0, arity), fnRet);
@@ -1785,6 +1814,17 @@ function reduceHelper(
if (existing) return existing;
const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(key, name);
if (method === "reduce")
lowerer.arrHofInfo.set(name, {
method,
elem,
fnRet,
arity,
outElem: accT,
present: false,
split: !!fastT,
hasInit,
});
const arrT = arrayOf(elem);
const fnT = funcOf([accT, arrayValueType(lowerer, elem), F64, arrT].slice(0, arity), fnRet);
File diff suppressed because it is too large Load Diff
@@ -344,6 +344,7 @@ import {
lowerFunction,
} from "./lower-functions.js";
import { lowerArrayMethodCall } from "./containers/array-methods.js";
import { type ArrayHofInfo, fuseArrayPipelines } from "./containers/array-pipelines.js";
import {
lowerMapMethodCall,
lowerMapForEachCall,
@@ -2111,6 +2112,9 @@ export class Lowerer {
/** Synthetic array-HOF loop functions (map/filter/forEach desugar),
* interned per method + element/callback-result type: key → fn name. */
readonly arrHofHelpers = new Map<string, string>();
/** What each array-HOF helper computes, keyed by helper name: the chain
* fusion pass reads it to recognize pipelines of helper calls. */
readonly arrHofInfo = new Map<string, ArrayHofInfo>();
/** Per-arm calls through union-typed class receivers, interned by the
* receiver union, member, completed argument ABI, and result type. */
readonly unionCallHelpers = new Map<string, string>();
@@ -5699,6 +5703,7 @@ export class Lowerer {
/** Final retention, pruning, and module assembly shared by ordinary emit
* and the retained reachability worklist. */
finishModule(functions: IrFunction[]): LowerResult {
if (this.diags.length === 0) fuseArrayPipelines(this, functions);
finalizeClassMethodValues(this, functions);
trackClassFieldCreation(this, functions);
@@ -0,0 +1,112 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile, deserializeModule } from "../src/index.js";
import type { IrFunction, IrModule } from "../src/ir/ir.js";
import { everyStmtList } from "../src/ir/traverse.js";
async function lowerSource(source: string): Promise<IrModule> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-array-fusion-"));
try {
const entry = join(dir, "main.ts");
await writeFile(entry, source);
const outPath = join(dir, "main.ir.json");
const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" });
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
return deserializeModule(await readFile(outPath, "utf8"));
} finally {
await rm(dir, { recursive: true, force: true });
}
}
/** Helper callees named in `fn`, by their `%arr.<method>` prefix. */
function helperCalls(fn: IrFunction): string[] {
const calls: string[] = [];
everyStmtList(fn.body, {
expr: (e) => {
if (e.kind === "call" && e.callee.startsWith("%arr."))
calls.push(e.callee.replace(/\.\d+$/, ""));
return true;
},
stmt: () => true,
});
return calls.sort();
}
/** The authored function `run` every fixture wraps its code in. */
function init(mod: IrModule): IrFunction {
const fn = mod.functions.find((f) => f.name === "run");
if (!fn) throw new Error("no run function");
return fn;
}
function count(fn: IrFunction, kind: string): number {
let n = 0;
everyStmtList(fn.body, {
expr: (e) => {
if (e.kind === kind) n++;
return true;
},
stmt: () => true,
});
return n;
}
const prelude = `
interface Row { id: number; group: string; value: number }
const rows: Row[] = [];
for (let i = 0; i < 10; i++) rows.push({ id: i, group: i % 2 === 0 ? "a" : "b", value: i * 3 });
function run(): void {
`;
const postlude = `
}
run();
`;
test("pure chains become one loop without intermediate arrays", async () => {
const mod = await lowerSource(`${prelude}
const total = rows
.filter((r) => r.group === "a")
.map((r) => ({ id: r.id, score: r.value * 2 }))
.filter((s) => s.score > 4)
.reduce((sum, s) => sum + s.score, 0);
const picked = rows.filter((r) => r.value > 6);
const n = picked.length;
console.log(total, n, rows.slice(2, 8).map((r) => r.id).some((id) => id === 5));
${postlude}`);
expect(helperCalls(init(mod))).toEqual(["%arr.fused", "%arr.fused", "%arr.fused"]);
expect(init(mod).locals.some((l) => l.name === "picked")).toBe(false);
// The per-element { id, score } record exists only as scalar locals.
const fused = mod.functions.filter((f) => f.name.startsWith("%arr.fused."));
expect(fused).toHaveLength(3);
for (const fn of fused) {
expect(count(fn, "recordLit")).toBe(0);
expect(count(fn, "arrayLit")).toBe(0);
}
});
test("observable callback order keeps the stage-by-stage helpers", async () => {
const mod = await lowerSource(`${prelude}
const log: string[] = [];
const a = rows.map((r) => { log.push("m" + r.id); return r.value; }).filter((v) => v > 3);
let seen = 0;
const b = rows.map((r) => { seen++; return r.id; }).filter((id) => id < seen);
let mapped = 0;
const c = rows.map((r) => { mapped++; return r.id; }).filter((id) => { if (id > 5) throw new Error("x"); return true; });
let found = 0;
const d = rows.map((r) => { found++; return r.id; }).some((id) => id === 1);
const e = rows.map((r) => r.id).filter((id, i, all) => all.length > i);
console.log(a.length, b.length, c.length, d, e.length, log.length, seen, mapped, found);
${postlude}`);
expect(helperCalls(init(mod))).not.toContain("%arr.fused");
});
test("a binding read twice stays an array", async () => {
const mod = await lowerSource(`${prelude}
const kept = rows.filter((r) => r.value > 3);
console.log(kept.map((r) => r.id).length, kept.length);
${postlude}`);
expect(init(mod).locals.some((l) => l.name === "kept")).toBe(true);
expect(helperCalls(init(mod))).toEqual(["%arr.filter", "%arr.fused"]);
});
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/4870-array-pipeline-fusion.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/4871-array-pipeline-analytics.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/4872-array-pipeline-temporaries.ts"
],
"diags": []
}
+215
View File
@@ -0,0 +1,215 @@
// Chains of array callbacks (filter/map/flatMap/slice feeding reduce, some,
// every, findIndex, forEach, length or a final array) keep JavaScript's
// stage-by-stage semantics: callback order, holes, sparse arrays,
// exceptions part-way through, early exits and slice bounds.
interface Item {
id: number;
group: string;
weight: number;
}
const items: Item[] = [];
for (let i = 0; i < 40; i++) {
items.push({ id: i, group: i % 3 === 0 ? "a" : i % 3 === 1 ? "b" : "c", weight: (i * 7) % 11 });
}
// Pure chains.
const heavy = items
.filter((it) => it.weight > 4)
.map((it) => ({ id: it.id, score: it.weight * 2 }))
.filter((s) => s.score % 3 !== 0);
console.log("heavy", heavy.length, heavy.reduce((sum, s) => sum + s.score, 0));
console.log(
"sum",
items.filter((it) => it.group === "b").map((it) => it.weight).reduce((a, b) => a + b, 0),
);
console.log("some", items.map((it) => it.weight).some((w) => w === 10));
console.log("every", items.filter((it) => it.group !== "a").every((it) => it.id % 3 !== 0));
console.log("findIndex", items.filter((it) => it.group === "c").findIndex((it) => it.weight === 0));
console.log("findIndex miss", items.filter((it) => it.group === "c").findIndex((it) => it.weight > 99));
console.log("length", items.filter((it) => it.weight < 3).length);
console.log("map length", items.map((it) => it.id * 2).length);
console.log(
"flatMap length",
items.slice(0, 12).flatMap((it) => (it.group === "a" ? [it.id, it.weight] : [])).length,
);
console.log(
"collect",
items
.filter((it) => it.weight === 3)
.map((it) => `${it.group}${it.id}`)
.join(","),
);
let forEachTotal = 0;
items.filter((it) => it.group === "a").forEach((it) => {
forEachTotal += it.weight;
});
console.log("forEach", forEachTotal);
const pairs = items.slice(5, 15).flatMap((it) => [it.id, -it.id]);
console.log("pairs", pairs.length, pairs.slice(0, 4).join(","));
const centroid = items
.slice(0, 20)
.map((it) => ({ x: it.id / 2, y: it.weight }))
.reduce((acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }), { x: 0, y: 0 });
console.log("centroid", centroid.x, centroid.y);
// A const read once by the next statement's consumer.
const picked = items.filter((it) => it.weight % 2 === 1);
const pickedSum = picked.reduce((acc, it) => acc + it.id, 0);
console.log("picked", pickedSum);
const firstMapped = items.map((it) => it.weight + 1);
console.log("firstMapped", firstMapped.filter((w) => w > 5).length);
// Read twice: it stays an array.
const twice = items.filter((it) => it.id < 5);
console.log("twice", twice.length, twice.map((it) => it.id).join(""));
// Slice bounds: negative, NaN, past the end, reversed.
const nums = [5, 1, 4, 2, 8, 7];
console.log("slice -3", nums.slice(-3).map((n) => n * 10).join(","));
console.log("slice NaN", nums.slice(NaN, 2).map((n) => n + 1).join(","));
console.log("slice past", nums.slice(4, 100).reduce((a, n) => a + n, 0));
console.log("slice reversed", nums.slice(4, 2).map((n) => n).length);
console.log("slice fraction", nums.slice(1.7, -1.2).filter((n) => n > 1).join(","));
console.log("slice -inf", nums.slice(-Infinity, Infinity).filter((n) => n % 2 === 0).length);
// Holes: map and slice keep them, filter and flatMap drop them; findIndex
// visits them as undefined.
const holes: number[] = [];
holes[0] = 1;
holes[2] = 3;
holes[3] = 4;
holes[5] = 6;
function keys(xs: number[]): string {
const out: number[] = [];
xs.forEach((_, i) => {
out.push(i);
});
return out.join(",");
}
console.log("holes map length", holes.map((n) => n * 2).length);
console.log("holes map reduce", holes.map((n) => n * 2).reduce((a, n) => a + n, 0));
console.log("holes map filter", holes.map((n) => n * 2).filter((n) => n > 2).join(","));
console.log("holes map keys", keys(holes.map((n) => n + 1).map((n) => n * 3)));
console.log("holes slice keys", keys(holes.slice(1).map((n) => n)));
console.log("holes map every", holes.map((n) => n > 0).every((b) => b));
const visited: string[] = [];
const hit = holes.map((n) => n).findIndex((n) => {
visited.push(String(n));
return n === undefined;
});
console.log("holes findIndex", hit, visited.join(","));
const sparse: number[] = [];
sparse[3] = 30;
sparse[10] = 100;
console.log("sparse", sparse.map((n) => n / 10).filter((n) => n > 1).join(","), sparse.map((n) => n).length);
// Explicit undefined elements reach the callbacks.
const maybe: (number | undefined)[] = [1, undefined, 3];
console.log(
"maybe",
maybe.map((n) => (n === undefined ? -1 : n * 2)).filter((n) => n !== 3).join(","),
);
console.log("maybe filter", maybe.filter((n) => n !== 1).map((n) => String(n)).join("/"));
// Callback order is observable: these keep stage-by-stage order.
const log: string[] = [];
const ordered = [1, 2, 3]
.map((n) => {
log.push(`m${n}`);
return n * 2;
})
.filter((n) => {
log.push(`f${n}`);
return n > 2;
});
console.log("ordered", ordered.join(","), log.join(" "));
let seen = 0;
const counted = [1, 2, 3, 4]
.map((n) => {
seen++;
return n;
})
.filter((n) => n <= seen);
console.log("counted", counted.join(","), seen);
// An exception part-way through a later stage: earlier stages completed.
let mapped = 0;
try {
[1, 2, 3, 4]
.map((n) => {
mapped++;
return n;
})
.filter((n) => {
if (n === 2) throw new Error("stop at " + n);
return true;
});
} catch (e) {
console.log("caught", (e as Error).message, "mapped", mapped);
}
// An exception in the first stage: the terminal never ran.
let consumed = 0;
try {
[1, 2, 3, 4]
.map((n) => {
if (n === 3) throw new Error("bad " + n);
return n;
})
.forEach((n) => {
consumed += n;
});
} catch (e) {
console.log("caught", (e as Error).message, "consumed", consumed);
}
// Early exit does not skip an exception the first stage would raise.
try {
const found = [1, 2, 3].map((n) => {
if (n === 3) throw new Error("late " + n);
return n;
}).some((n) => n === 1);
console.log("found", found);
} catch (e) {
console.log("caught", (e as Error).message);
}
// Index and array parameters.
console.log(
"indexed",
[10, 20, 30, 40].filter((n, i) => i !== 1).map((n) => n + 1).join(","),
[3, 1, 2].map((n) => n * 2).filter((n, i, arr) => arr.length === 3 && i > 0).join(","),
);
console.log(
"positions",
[5, 6, 7, 8, 9].filter((n) => n % 2 === 1).map((n, i) => n * 10 + i).join(","),
[5, 6, 7].map((n) => n + 1).map((n, i) => `${i}:${n}`).join(","),
[1, 2, 3, 4, 5, 6].slice(2).map((n, i) => n - i).join(","),
[1, 2, 3, 4, 5].filter((n) => n !== 3).reduce((acc, n, i) => acc + n * i, 0),
keys(holes.map((n) => n).map((n, i) => i)),
holes.map((n) => n * 2).filter((n, i) => i > 2).join(","),
[9, 8, 7, 6].map((n, i, arr) => n + i + arr.length).filter((n) => n > 12).length,
);
const positions: string[] = [];
[4, 5, 6, 7].filter((n) => n > 4).forEach((n, i) => {
positions.push(`${i}=${n}`);
});
console.log("forEach positions", positions.join(" "));
// Callbacks that mutate the source keep the unfused semantics.
const grow = [1, 2, 3];
const grown = grow
.map((n) => {
if (n === 1) grow.push(9);
return n;
})
.filter((n) => n > 0);
console.log("grow", grown.join(","), grow.length);
// Boolean filters and string elements.
const words = ["alpha", "", "beta", "", "gamma"];
console.log("words", words.filter(Boolean).map((w) => w.toUpperCase()).join("-"));
console.log("chars", words.map((w) => w.length).filter((n) => n > 0).reduce((a, n) => a * n, 1));
@@ -0,0 +1,107 @@
// Order and request-log analytics written the idiomatic way: one
// filter/map/reduce chain per question.
interface Order {
id: number;
customer: number;
region: string;
status: string;
qty: number;
price: number;
discount: number;
day: number;
}
interface Request {
route: string;
status: number;
ms: number;
bytes: number;
}
let seed = 77;
function random(): number {
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
return seed / 4294967296;
}
const regions = ["north", "south", "east", "west", "central"];
const statuses = ["done", "done", "done", "pending", "cancelled", "refunded"];
const routes = ["/", "/api/items", "/api/cart", "/api/user", "/static/app.js", "/health"];
function makeOrders(count: number): Order[] {
const out: Order[] = [];
for (let i = 0; i < count; i++) {
out.push({
id: i,
customer: Math.floor(random() * 20000),
region: regions[Math.floor(random() * regions.length)]!,
status: statuses[Math.floor(random() * statuses.length)]!,
qty: 1 + Math.floor(random() * 9),
price: Math.floor(random() * 50000) / 100,
discount: random() < 0.3 ? Math.floor(random() * 30) / 100 : 0,
day: Math.floor(random() * 365),
});
}
return out;
}
function makeRequests(count: number): Request[] {
const out: Request[] = [];
for (let i = 0; i < count; i++) {
const r = random();
out.push({
route: routes[Math.floor(random() * routes.length)]!,
status: r < 0.9 ? 200 : r < 0.95 ? 404 : 500,
ms: Math.floor(random() * random() * 2000),
bytes: Math.floor(random() * 100000),
});
}
return out;
}
const scale = 0.02;
const orders = makeOrders(Math.floor(150000 * scale));
const requests = makeRequests(Math.floor(150000 * scale));
const lines: string[] = [];
let check = 0;
for (let pass = 0; pass < 4; pass++) {
const minDay = pass * 30;
for (const region of regions) {
const revenue = orders
.filter((o) => o.status === "done" && o.region === region && o.day >= minDay)
.map((o) => ({ gross: o.qty * o.price, net: o.qty * o.price * (1 - o.discount) }))
.reduce((sum, line) => sum + line.net, 0);
const big = orders.filter((o) => o.region === region && o.qty * o.price > 2000).length;
if (pass < 2) lines.push(`${region} revenue=${revenue.toFixed(2)} big=${big}`);
check = (check + Math.round(revenue) + big) % 1000000007;
}
const discounted = orders
.filter((o) => o.discount > 0)
.map((o) => o.qty * o.price * o.discount)
.reduce((a, b) => a + b, 0);
const refunds = orders.filter((o) => o.status === "refunded").map((o) => o.qty).reduce((a, b) => a + b, 0);
const anyHuge = orders.some((o) => o.qty * o.price > 4490 + pass);
const allPositive = orders.map((o) => o.price).every((p) => p >= 0);
const firstLate = orders.filter((o) => o.status === "pending").findIndex((o) => o.day > 360 - pass);
check = (check + Math.round(discounted) + refunds + (anyHuge ? 1 : 0) + (allPositive ? 1 : 0) + firstLate) % 1000000007;
for (const route of routes) {
const hits = requests.filter((q) => q.route === route);
const slow = hits.filter((q) => q.ms > 1000 + pass).length;
const errors = hits.filter((q) => q.status >= 500).length;
const traffic = hits.map((q) => q.bytes / 1024).reduce((a, kb) => a + kb, 0);
const latency = requests
.filter((q) => q.route === route && q.status === 200)
.map((q) => ({ ms: q.ms, weight: q.bytes > 50000 ? 2 : 1 }))
.reduce((acc, s) => acc + s.ms * s.weight, 0);
if (pass < 2)
lines.push(`${route} hits=${hits.length} slow=${slow} errors=${errors} kb=${Math.round(traffic)} lat=${latency}`);
check = (check + slow + errors + Math.round(traffic) + latency) % 1000000007;
}
const recent = orders.slice(orders.length - 20000).filter((o) => o.status === "done").map((o) => o.customer);
const vip = new Set(recent.filter((c) => c % 97 === pass));
check = (check + vip.size + recent.length) % 1000000007;
const tags = orders.slice(0, 30000).flatMap((o) => (o.discount > 0 ? [o.region, o.status] : [o.region])).length;
check = (check + tags) % 1000000007;
}
for (const line of lines) console.log(line);
console.log("check", check);
@@ -0,0 +1,123 @@
// Per-element temporaries inside array callback chains: records built by
// one stage and read by the next, reduce accumulators, captured
// configuration, and consumers that write shared state.
interface Point {
x: number;
y: number;
}
class Sample {
readonly label: string;
readonly value: number;
constructor(label: string, value: number) {
this.label = label;
this.value = value;
}
}
const points: Point[] = [];
for (let i = 0; i < 25; i++) points.push({ x: i, y: (i * 13) % 7 });
function makeScale(factor: number, offset: number): (p: Point) => number {
return (p) => p.x * factor + p.y + offset;
}
for (let round = 0; round < 3; round++) {
const scale = makeScale(round + 1, round * 2);
const scored = points
.filter((p) => p.x % 3 !== round)
.map((p) => ({ id: p.x, score: scale(p), flag: p.y > 3 }))
.filter((s) => s.flag || s.score > 20);
console.log(
"round",
round,
scored.reduce((sum, s) => sum + s.score, 0),
points.map((p) => ({ x: p.x * 2, y: p.y })).reduce((acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }), { x: 0, y: 0 }).x,
);
}
// The initial accumulator object comes back unchanged for an empty chain.
const seedPoint: Point = { x: -1, y: -1 };
const none = points.filter((p) => p.x > 100).map((p) => ({ x: p.x, y: p.y })).reduce((acc, p) => (p.x > acc.x ? p : acc), seedPoint);
console.log("seed identity", none === seedPoint, none.x);
const best = points.filter((p) => p.y === 6).map((p) => ({ x: p.x, y: p.y })).reduce((acc, p) => (p.x > acc.x ? p : acc), seedPoint);
console.log("best", best === seedPoint, best.x, best.y);
// Records with string fields, and records that escape into the result.
const labels = points
.filter((p) => p.y % 2 === 0)
.map((p) => ({ name: `p${p.x}`, size: p.y }))
.filter((r) => r.size > 0)
.map((r) => r.name)
.join(" ");
console.log("labels", labels);
const kept = points.filter((p) => p.x < 4).map((p) => ({ x: p.y, y: p.x }));
console.log("kept", kept.map((p) => `${p.x}/${p.y}`).join(","));
// Class instances.
const samples = points.map((p) => new Sample(p.x % 2 === 0 ? "even" : "odd", p.y));
console.log(
"samples",
samples.filter((s) => s.label === "even").map((s) => s.value).reduce((a, b) => a + b, 0),
samples.filter((s) => s.value > 4).every((s) => s.value <= 6),
);
// Consumers that write shared state the stages never read.
let evens = 0;
let largest = -Infinity;
points
.filter((p) => p.x % 2 === 0)
.map((p) => p.y * 10)
.forEach((v) => {
evens++;
if (v > largest) largest = v;
});
console.log("forEach state", evens, largest);
// A consumer that throws after earlier elements were consumed.
let partial = 0;
try {
points
.filter((p) => p.x > 2)
.map((p) => p.x)
.forEach((x) => {
if (x === 7) throw new RangeError("seven");
partial += x;
});
} catch (e) {
console.log("forEach threw", (e as Error).message, partial);
}
// A stage that reads state the consumer writes keeps unfused order.
let budget = 3;
const spent = points
.filter((p) => budget > 0)
.map((p) => p.x)
.reduce((acc, x) => {
budget--;
return acc + x;
}, 0);
console.log("budget", spent, budget);
// flatMap with holes and explicit undefined in the returned arrays.
const inner: (number | undefined)[] = [1, undefined, 3];
const sparseInner: number[] = [];
sparseInner[2] = 5;
console.log(
"flatMap",
[0, 1].flatMap(() => inner).length,
[0, 1].flatMap(() => sparseInner).length,
[0, 1, 2].flatMap((n) => [n, n * 10]).filter((n) => n > 0).map((n) => n + 1).join(","),
[0, 1].flatMap(() => inner).map((v) => (v === undefined ? "u" : String(v))).join(""),
);
// Chains inside callbacks of other chains.
const grid = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
console.log(
"nested",
grid.map((row) => row.filter((v) => v % 2 === 1).map((v) => v * v).reduce((a, b) => a + b, 0)).filter((s) => s > 10).join(","),
);