Keep the helper for callbacks that read a local class's captures

A lone array callback in a local class's method could read a binding the
method reaches through its class's captures. Those bindings are shared by
every method of the class, so another method can assign one while the
loop runs, but the copied loop read the value once when the call started.
stableTarget now counts a method's class captures as inherited, so such
a callback keeps its helper and reads the current value on every call.
This commit is contained in:
Chris Tate
2026-10-25 04:54:28 -05:00
parent 493785a121
commit 256fbdba20
2 changed files with 22 additions and 2 deletions
@@ -950,11 +950,14 @@ class PipelineFusion {
* nothing assigns after their declaration: `fn`'s own locals and
* parameters, written neither by `fn` nor by any closure that shares
* them, and not by the callback body. A binding a local class captures
* counts as written, since its methods may assign it. The callback must
* counts as written, since its methods may assign it; so does a binding
* a method reads through its class's captures. The callback must
* not observe the ambient receiver (AmbientReceiverReaders). */
private stableTarget(fn: IrFunction): InlineTarget {
const locals = new Map(fn.locals.map((l) => [l.id, l]));
const inherited = new Set((fn.captures ?? []).map((c) => c.localId));
const inherited = new Set(
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
);
const written = this.written.get(fn.name);
const classCaptured = this.classCaptured(fn);
return (arg) => {
@@ -249,3 +249,20 @@ console.log(out.join(","), hit);
${postlude}`);
expect(helperCalls(init(mod))).toEqual(["%arr.loop", "%arr.loop"]);
});
test("a lone call in a local class's method keeps its helper for the class's captures", async () => {
const mod = await lowerSource(`${prelude}
let k = 1;
class Box {
set(v: number): void { k = v; }
go(): number[] {
const b = new Box();
return rows.map((r) => { b.set(r.id); return r.value + k; });
}
}
console.log(new Box().go().join(","));
${postlude}`);
const go = mod.functions.find((f) => f.name.endsWith(".Box.go"));
expect(go).toBeDefined();
expect(helperCalls(go!)).toEqual(["%arr.map"]);
});