mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
Fuse array callback chains into single loops (#919)
This commit is contained in:
@@ -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"]);
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4870-array-pipeline-fusion.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4871-array-pipeline-analytics.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4872-array-pipeline-temporaries.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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(","),
|
||||
);
|
||||
Reference in New Issue
Block a user