mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
Array HOF helpers (%arr.map/filter/forEach/flatMap/find*/some/every/ reduce*) called every callback through its closure, bracketed by the ambient JS receiver stack, with each element boxed as T | undefined. - Intern a helper per literal callback and admit %arr.* helpers as constant-callback consumers: the callback becomes a guarded direct call that LLVM inlines into the loop. - Replace the scalar-only receiver check with an exact module fixpoint (AmbientReceiverReaders) over direct calls, constructors and virtual dispatch; known callbacks that cannot reach a receiver read skip the push/pop. - For inline arrows over plain element types, lower the callback a second time with the element type and split each visit on the array state: present values take the fast closure with no union box; holes and present undefined keep the original closure and undefined arm. - Resolve factory-made callbacks (every return yields one function) and immutable captured callbacks at their closure creation sites. - Read and write scalar capture boxes in place instead of calling scr_box_get_f64/scr_box_set_*. Corpus 4140-4142 pin the split visit, receiver brackets on the direct path, and factory/captured callbacks. Linux x64 (sandbox, --layouts=4 --runs=16, baseline stack 06 tip): | workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node | | --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: | | json-records | 192.5 | 193.8 | +0.7% | -2.1% .. +2.4% | neutral | +0.0% | 436.3 | 0.44x | | regex-logs | 166.4 | 165.5 | -0.5% | -1.8% .. +0.9% | neutral | +0.0% | 243.9 | 0.68x | | ast-interp | 264.9 | 243.0 | -8.3% | -9.5% .. -6.9% | faster | +0.0% | 232.2 | 1.05x | | records-sort | 593.2 | 576.4 | -2.8% | -6.6% .. +0.3% | neutral | +3.0% | 210.3 | 2.74x | | template-render | 69.3 | 68.6 | -1.0% | -3.2% .. +2.6% | neutral | +0.0% | 189.0 | 0.36x | | functional-pipeline | 215.8 | 111.0 | -48.6% | -49.6% .. -46.3% | faster | +3.8% | 255.8 | 0.43x | | async-pipeline | 194.8 | 196.8 | +1.0% | -1.1% .. +12.0% | neutral | +0.0% | 85.9 | 2.29x | | word-graph | 271.8 | 270.1 | -0.6% | -7.0% .. +5.5% | neutral | +3.0% | 517.1 | 0.52x | | alloc-trees | 314.4 | 316.6 | +0.7% | -0.4% .. +1.9% | neutral | +0.0% | 124.9 | 2.53x | | numeric-kernels | 46.2 | 45.7 | -1.1% | -2.5% .. +1.5% | neutral | +0.0% | 128.2 | 0.36x | | validate-errors | 179.7 | 174.5 | -2.9% | -5.4% .. +0.7% | neutral | +0.0% | 711.1 | 0.24x | | csv-numbers | 343.0 | 338.9 | -1.2% | -7.3% .. +0.7% | neutral | +0.0% | 519.6 | 0.65x | | log-summary | 166.1 | 168.3 | +1.3% | -4.4% .. +9.1% | neutral | +0.0% | 300.0 | 0.56x | | inventory-report | 298.1 | 290.1 | -2.7% | -14.6% .. +5.4% | neutral | +1.3% | 421.4 | 0.69x | Geomean -5.9%; candidate/node 0.715x. macOS arm64 (shared host): geomean -4.7%, functional-pipeline -44.1%, ast-interp -11.0%, inventory-report -8.0%; none slower (records-sort -0.5% on a calmer rerun).
135 lines
5.4 KiB
TypeScript
135 lines
5.4 KiB
TypeScript
// Inline array callbacks over plain element types: a present value reaches
|
|
// the callback as the element itself, while holes and present undefined
|
|
// still reach it as undefined. Pins arguments, mutation during iteration,
|
|
// captured bindings, exceptions, and nested callbacks on both paths.
|
|
interface Item {
|
|
id: number;
|
|
tag: string;
|
|
}
|
|
class Box {
|
|
readonly n: number;
|
|
constructor(n: number) {
|
|
this.n = n;
|
|
}
|
|
twice(): number {
|
|
return this.n * 2;
|
|
}
|
|
}
|
|
|
|
const items: Item[] = [
|
|
{ id: 1, tag: "a" },
|
|
{ id: 2, tag: "b" },
|
|
{ id: 3, tag: "a" },
|
|
];
|
|
// Spreading a holey array materializes present undefined values.
|
|
const holey: Item[] = [];
|
|
holey[1] = { id: 7, tag: "z" };
|
|
holey.length = 3;
|
|
const padded: Item[] = [...holey];
|
|
|
|
function describe(label: string, run: () => unknown): void {
|
|
try {
|
|
console.log(label, JSON.stringify(run()));
|
|
} catch (error) {
|
|
if (error instanceof Error) console.log(label, "threw", error.name, error.message);
|
|
}
|
|
}
|
|
|
|
describe("map", () => items.map((item) => item.id * 10));
|
|
describe("map-index-array", () => items.map((item, index, array) => `${item.tag}${index}/${array.length}`));
|
|
describe("filter", () => items.filter((item) => item.tag === "a").map((item) => item.id));
|
|
describe("filter-index", () => items.filter((_item, index) => index !== 1).length);
|
|
describe("some", () => items.some((item) => item.id === 3));
|
|
describe("every", () => items.every((item) => item.id > 0));
|
|
describe("findIndex", () => items.findIndex((item) => item.tag === "b"));
|
|
describe("findLastIndex", () => items.findLastIndex((item) => item.tag === "a"));
|
|
describe("flatMap", () => items.flatMap((item) => (item.tag === "a" ? [item.id, -item.id] : [])));
|
|
describe("flatMap-scalar", () => items.flatMap((item) => item.id + 0.5));
|
|
describe("reduce", () => items.reduce((sum, item) => sum + item.id, 100));
|
|
describe("reduce-seedless", () => items.map((item) => item.id).reduce((a, b) => a * b));
|
|
describe("reduceRight", () => items.reduceRight((acc, item) => acc + item.tag, ">"));
|
|
|
|
// Holes are skipped by map/filter/some/every/forEach/reduce/flatMap and
|
|
// visited as undefined by findIndex; present undefined is always visited.
|
|
describe("holey-map", () => holey.map((item) => item.id).length);
|
|
describe("holey-filter", () => holey.filter((item) => item.id > 0).length);
|
|
describe("holey-findIndex", () => holey.findIndex((item) => item === undefined));
|
|
describe("padded-findIndex", () => padded.findIndex((item) => item === undefined));
|
|
describe("padded-some-throws", () => padded.some((item) => item.id === 7));
|
|
describe("padded-every-guarded", () => padded.every((item) => item === undefined || item.id === 7));
|
|
describe("padded-filter-guarded", () => padded.filter((item) => item === undefined || item.id > 0).length);
|
|
describe("padded-map-throws", () => padded.map((item) => item.tag));
|
|
describe("padded-reduce-guarded", () =>
|
|
padded.reduce((acc, item) => acc + (item === undefined ? "u" : item.tag), ""),
|
|
);
|
|
describe("padded-flatMap-guarded", () => padded.flatMap((item) => (item === undefined ? [0] : [item.id])));
|
|
let seen = "";
|
|
padded.forEach((item, index) => {
|
|
seen += item === undefined ? `u${index}` : `${item.tag}${index}`;
|
|
});
|
|
console.log("padded-forEach", seen);
|
|
const keptUndefined = padded.filter((item): boolean => item === undefined);
|
|
console.log("kept-undefined", keptUndefined.length, keptUndefined.findIndex((item) => item === undefined));
|
|
|
|
// Class elements and method calls inside the callback.
|
|
const boxes = [new Box(1), new Box(2), new Box(3)];
|
|
describe("class-map", () => boxes.map((box) => box.twice()));
|
|
describe("class-filter", () => boxes.filter((box) => box.twice() > 2).length);
|
|
|
|
// Mutation during iteration: the length is read once; elements are read fresh.
|
|
const growing: Item[] = [{ id: 1, tag: "g" }, { id: 2, tag: "g" }];
|
|
describe("push-during-map", () =>
|
|
growing.map((item) => {
|
|
growing.push({ id: item.id + 10, tag: "h" });
|
|
return item.id;
|
|
}),
|
|
);
|
|
console.log("grown", growing.length);
|
|
const shrinking: Item[] = [{ id: 1, tag: "s" }, { id: 2, tag: "s" }, { id: 3, tag: "s" }];
|
|
describe("shrink-during-filter", () =>
|
|
shrinking.filter((item, index) => {
|
|
if (index === 0) shrinking.length = 2;
|
|
return item.id > 0;
|
|
}).length,
|
|
);
|
|
const replacing: Item[] = [{ id: 1, tag: "r" }, { id: 2, tag: "r" }];
|
|
describe("replace-during-some", () =>
|
|
replacing.some((item, index) => {
|
|
if (index === 0) replacing[1] = { id: 99, tag: "x" };
|
|
return item.id === 99;
|
|
}),
|
|
);
|
|
|
|
// Captured bindings: per-iteration lets, mutated counters, escaping closures.
|
|
const readers: (() => number)[] = [];
|
|
let calls = 0;
|
|
for (let round = 0; round < 3; round++) {
|
|
const picked = items.filter((item) => {
|
|
calls++;
|
|
return item.id > round;
|
|
});
|
|
readers.push(() => round * 100 + picked.length);
|
|
const scale = round + 1;
|
|
console.log("round", round, picked.map((item) => item.id * scale).join(","), calls);
|
|
}
|
|
console.log("readers", readers.map((read) => read()).join(","));
|
|
|
|
// Exceptions leave the loop at the throwing element.
|
|
let visited = 0;
|
|
describe("throw-in-map", () =>
|
|
items.map((item) => {
|
|
visited++;
|
|
if (item.id === 2) throw new RangeError(`bad ${item.tag}`);
|
|
return item.id;
|
|
}),
|
|
);
|
|
console.log("visited", visited);
|
|
|
|
// Nested callbacks over the same and other arrays.
|
|
describe("nested", () =>
|
|
items.map((outer) => items.filter((inner) => inner.tag === outer.tag).map((inner) => inner.id).join("+")),
|
|
);
|
|
describe("nested-reduce", () =>
|
|
items.reduce((acc, outer) => acc + items.filter((inner) => inner.id <= outer.id).length, 0),
|
|
);
|