mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Reference-write analysis merged every reference-array element into one location, so the map, filter and fused-pipeline helpers, which fill a fresh result array, counted as writing all array elements. A borrow of an input element across their callbacks (and of any element across a call into them) was refused, and each element was retained and released per call. An array local declared once from an array literal or `new Array(n)`, never rebound, unboxed, and otherwise only the receiver of element stores, length updates, `length`, `push` and presence tests, or the value of a `return`, is private to its activation: no code reads its elements or reaches its storage another way before the function returns it. Its stores cannot release a value a borrow depends on, so they no longer add the array-element location, and they stay out of the function's summary (a caller's borrows predate the array). Executed RC operations: ast-interp 4.49M -> 2.31M, optional-fields 18.76M -> 10.72M, template-render 0.40M -> 0.36M, the same as an unsound oracle that drops every array-element write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
125 lines
3.5 KiB
TypeScript
125 lines
3.5 KiB
TypeScript
// Element stores into a function's own fresh array (the map, filter and
|
|
// fused helpers' results, and hand-written builders) cannot release an
|
|
// element of another array, so elements read from the input stay borrowed
|
|
// across callbacks. Arrays that escape, are read back, or are the input
|
|
// itself keep the conservative ownership; each "must own" case below makes
|
|
// the overwritten slot the last owner, churns the allocator and reads the
|
|
// value again.
|
|
|
|
class Item {
|
|
name: string;
|
|
weight: number;
|
|
constructor(name: string, weight: number) {
|
|
this.name = name;
|
|
this.weight = weight;
|
|
}
|
|
label(): string {
|
|
return `${this.name}:${this.weight}`;
|
|
}
|
|
}
|
|
|
|
function churn(): number {
|
|
let total = 0;
|
|
for (let i = 0; i < 64; i++) total += new Item(`tmp${i}`, i).weight;
|
|
return total;
|
|
}
|
|
|
|
const log: string[] = [];
|
|
const scratch: Item[] = [];
|
|
|
|
// map/filter callbacks that write unrelated arrays: borrowed elements stay valid.
|
|
function mapped(items: Item[]): string[] {
|
|
return items.map((item) => {
|
|
scratch.push(new Item("s", scratch.length));
|
|
if (scratch.length > 3) scratch.length = 0;
|
|
log.push(item.name);
|
|
churn();
|
|
return item.label();
|
|
});
|
|
}
|
|
|
|
function heavy(items: Item[]): Item[] {
|
|
return items.filter((item) => {
|
|
scratch[0] = new Item("replaced", item.weight);
|
|
churn();
|
|
return item.weight > 1;
|
|
});
|
|
}
|
|
|
|
// A hand-written builder: a fresh local array that is only stored into and returned.
|
|
function build(items: Item[], extra: (item: Item) => Item): Item[] {
|
|
const out: Item[] = [];
|
|
for (let i = 0; i < items.length; i++) {
|
|
const item = items[i]!;
|
|
out.push(extra(item));
|
|
out[out.length - 1] = extra(item);
|
|
churn();
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Must own: the callback overwrites the very array being filtered.
|
|
function selfOverwrite(items: Item[]): string[] {
|
|
const kept = items.filter((item, i) => {
|
|
items[i] = new Item(`new${i}`, -1);
|
|
churn();
|
|
return item.weight >= 0 && item.name.length > 0;
|
|
});
|
|
return kept.map((item) => item.label());
|
|
}
|
|
|
|
// Must own: a fresh array that escapes into a holder before it is written.
|
|
const holder: Item[][] = [];
|
|
function escaped(): string {
|
|
const out: Item[] = [new Item("first", 1)];
|
|
holder.push(out);
|
|
const first = holder[holder.length - 1]![0]!;
|
|
out[0] = new Item("second", 2);
|
|
churn();
|
|
return first.label();
|
|
}
|
|
|
|
// Must own: a fresh array whose element is read back before it is overwritten.
|
|
function readBack(): string {
|
|
const out: Item[] = [];
|
|
out.push(new Item("kept", 3));
|
|
const kept = out[0]!;
|
|
out[0] = new Item("other", 4);
|
|
out.length = 0;
|
|
churn();
|
|
return kept.label();
|
|
}
|
|
|
|
// Must own: the fresh array is passed to a callee that clears it.
|
|
function clear(list: Item[]): void {
|
|
list.length = 0;
|
|
}
|
|
function passed(): string {
|
|
const out: Item[] = [new Item("passed", 5)];
|
|
const head = out[0]!;
|
|
clear(out);
|
|
churn();
|
|
return head.label();
|
|
}
|
|
|
|
const base = [new Item("a", 1), new Item("b", 2), new Item("c", 3)];
|
|
console.log(mapped(base).join(","));
|
|
console.log(log.join(""));
|
|
console.log(
|
|
heavy(base)
|
|
.map((item) => item.label())
|
|
.join(","),
|
|
);
|
|
console.log(
|
|
build(base, (item) => new Item(item.name.toUpperCase(), item.weight * 10))
|
|
.map((item) => item.label())
|
|
.join(","),
|
|
);
|
|
console.log(selfOverwrite([new Item("x", 1), new Item("y", 2)]).join(","));
|
|
console.log(escaped(), holder.length, holder[0]![0]!.label());
|
|
console.log(readBack());
|
|
console.log(passed());
|
|
const numbers = base.map((item) => item.weight * 2);
|
|
numbers[0] = 100;
|
|
console.log(numbers.join(","), base.map((item) => item.label()).join(","));
|