Files
scriptc/tests/corpus/4140-array-callback-split-visit.ts
Malte Ubl 425386b877 Call array-method callbacks directly and pass plain elements
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).
2026-10-08 15:38:45 -05:00

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),
);