Files
scriptc/tests/corpus/4598-live-array-ref-element-access.ts
T
Malte Ubl 25e3ece9d5 Access live array references one element at a time
A static array passed where a dynamically typed array is expected (for
example a `number[]` given to an `(object | undefined)[]` or `any[]`
parameter) travels as a live typed reference. Every operation on that
reference refreshed its dyn view by rebuilding the entire array, so
`xs[i]`, `xs.length`, `xs[i] = v`, `i in xs`, `Object.hasOwn`, each
`for...of` step, each `for...in` key probe, and `push`/`pop`/`at` all
cost O(n), and any loop over the array was quadratic: 10k elements took
12 s and 20k took 53 s on Linux. Writes also re-converted and swapped the
whole native storage on every store.

The native array is already the source of truth: each refresh rebuilds
the view from it and keeps only the prototype and symbol properties. Live
array capsules now carry per-element hooks (ScrDynTypedArrayOps), emitted
per array type next to the existing snapshot adapter and bound when the
capsule is built. The runtime answers length and presence from the native
array and converts exactly one element per read, writes an in-range or
appended element in place, and steps array iterators without a refresh.
Holes and out-of-range reads, which can reach the prototype chain, keep
using the view. A value the element type rejects, a write that would
grow the array by more than one element, a multi-value push, and `at`
with a coercible argument also keep the snapshot path, so their
diagnostics and atomicity are unchanged.

The capsule payload stays 80 bytes: the key length and the two flags now
share one 32-bit word, which frees the slot for the hooks pointer on both
64- and 32-bit targets.

Linux, 100k elements: the index loop went from beyond a 120 s timeout to
7 ms; Node takes 0.5 ms.

Test: test_json.c live_array_ref_tests. Corpus:
4598-live-array-ref-element-access.
2026-10-10 14:24:13 -07:00

153 lines
6.3 KiB
TypeScript

// A static array passed where a dynamically typed array is expected travels
// as a live reference: element reads, length, writes, presence, iteration,
// and push/pop/at act on the original array, and changes made through
// either view are visible through the other. The 100k-element loops pin
// that each access costs one element rather than a copy of the array.
type Slot = object | undefined;
type Loose = any[];
const N = 100_000;
function sumByIndex(xs: Slot[]): number {
let total = 0;
for (let i = 0; i < xs.length; i++) total += xs[i] as unknown as number;
return total;
}
function scale(xs: Loose, factor: number): void {
for (let i = 0; i < xs.length; i++) xs[i] = (xs[i] as number) * factor;
}
function countPresent(xs: Slot[]): number {
let present = 0;
for (let i = 0; i < xs.length; i++) if (i in xs && Object.hasOwn(xs, i)) present++;
return present;
}
function sumByIteration(xs: Slot[]): number {
let total = 0;
for (const value of xs) total += value as unknown as number;
return total;
}
function churn(xs: Loose, count: number): number {
for (let i = 0; i < count; i++) xs.push(i);
let total = 0;
for (let i = 0; i < count; i++) total += (xs.at(-1 - i) as number) - (xs.at(i) as number);
for (let i = 0; i < count; i++) total += xs.pop() as number;
return total;
}
function writeThrough(xs: Slot[], index: number, value: unknown): void {
(xs as unknown[])[index] = value;
}
// Large live arrays: every loop would take minutes if an access rebuilt
// the whole array.
const big: number[] = [];
for (let i = 0; i < N; i++) big.push(i);
const bigView = big as unknown as Slot[];
console.log("sum", sumByIndex(bigView), sumByIteration(bigView));
scale(big, 2);
console.log("scaled", big[0], big[1], big[N - 1], sumByIndex(bigView));
console.log("present", countPresent(bigView));
console.log("churn", churn(big, N), big.length, big[N - 1]);
big.length = 10;
console.log("truncated", bigView.length, bigView[9], bigView[10]);
const floats = new Float64Array(N);
for (let i = 0; i < N; i++) floats[i] = i / 2;
console.log("typed array", sumByIndex(floats as unknown as Slot[]), sumByIteration(floats as unknown as Slot[]));
interface Point { x: number; y: number }
const many: Point[] = [];
for (let i = 0; i < N; i++) many.push({ x: i, y: -i });
const manyView = many as unknown as Slot[];
let nested = 0;
for (let i = 0; i < manyView.length; i++) {
const point = manyView[i] as Point;
point.y = point.x * 2;
nested += point.x;
}
console.log("nested", nested, many[N - 1]!.y, (manyView[N - 1] as Point).y);
// Reads, length, holes, and out-of-range indexes through the view.
const nums: number[] = [1, 2, 3, 4];
const view = nums as unknown as Slot[];
console.log("len", view.length, "first", view[0], "last", view[view.length - 1], "oob", view[view.length], view[-1 as number]);
console.log("in", 0 in view, view.length in view, "length" in view, Object.hasOwn(view, 3), Object.hasOwn(view, 4));
// Writes are visible both ways.
writeThrough(view, 1, 20);
console.log("native sees", nums[1], nums.length);
nums[2] = 30;
console.log("view sees", view[2], view.length);
nums.push(5);
console.log("view after native push", view.length, view[4]);
writeThrough(view, 5, 6);
console.log("append", nums.length, nums[5]);
writeThrough(view, 8, 9);
console.log("gap", nums.length, JSON.stringify(nums), 6 in nums, 7 in view, Object.hasOwn(view, 7));
console.log("hole reads", view[6], view[7]);
nums.length = 3;
console.log("native truncate", view.length, view[3], JSON.stringify(view));
// Holes in the original array.
const holey: number[] = [1, , 3] as number[];
const holeyView = holey as unknown as Slot[];
console.log("holey", holeyView.length, holeyView[1], 1 in holeyView, holeyView[2]);
writeThrough(holeyView, 1, 2);
console.log("filled", JSON.stringify(holey), 1 in holey);
let holeySum = 0;
const sparse: number[] = [1, , 3] as number[];
for (const value of sparse as unknown as Slot[]) holeySum += value === undefined ? 100 : (value as unknown as number);
console.log("holey iteration", holeySum);
// Strings and booleans.
const words: string[] = ["a", "b"];
const wordsView = words as unknown as Slot[];
writeThrough(wordsView, 0, "z");
(wordsView as unknown as Loose).push("c");
console.log("words", words.join(""), wordsView[1], wordsView.length, (wordsView as unknown as Loose).at(-1));
const flags: boolean[] = [true, false];
const flagsView = flags as unknown as Slot[];
writeThrough(flagsView, 1, true);
console.log("flags", flags[1], flagsView[0], (flagsView as unknown as Loose).pop(), flags.length);
// Nested objects keep identity and liveness both ways.
const points: Point[] = [{ x: 1, y: 2 }, { x: 3, y: 4 }];
const pointsView = points as unknown as Slot[];
const p0 = pointsView[0] as Point;
console.log("nested", p0.x, pointsView[0] === pointsView[0], p0 === pointsView[0], (pointsView[1] as Point).y);
p0.x = 10;
console.log("native nested", points[0]!.x);
points[1]!.y = 40;
console.log("view nested", (pointsView[1] as Point).y);
writeThrough(pointsView, 0, { x: 7, y: 8 });
console.log("replaced", points[0]!.x, (pointsView[0] as Point).y, p0.x);
points[1] = { x: 5, y: 6 };
console.log("native replaced", (pointsView[1] as Point).x);
const maybe: (Point | undefined)[] = [{ x: 1, y: 1 }, undefined];
const maybeView = maybe as unknown as Slot[];
writeThrough(maybeView, 1, { x: 2, y: 2 });
writeThrough(maybeView, 0, undefined);
console.log("optional", maybe[0] === undefined, maybe[1]!.x, maybeView[0], (maybeView[1] as Point).y);
// Nested arrays.
const grid: number[][] = [[1, 2], [3, 4]];
const gridView = grid as unknown as Slot[];
const row = gridView[1] as number[];
row[0] = 30;
console.log("grid", grid[1]![0], (gridView[0] as number[]).length, gridView.length);
grid[0]!.push(99);
console.log("grid push", (gridView[0] as number[])[2], (gridView[0] as number[]).length);
// Iteration sees elements appended during the loop.
const growing: number[] = [1, 2, 3];
let total = 0;
for (const value of growing as unknown as Slot[]) {
total += value as unknown as number;
if (total === 1) growing.push(4);
}
console.log("for-of", total);
const keys: string[] = [];
for (const key in growing as unknown as Slot[]) keys.push(key);
console.log("for-in", keys.join(","), JSON.stringify(Object.keys(growing as unknown as Slot[])));
console.log("isArray", Array.isArray(growing as unknown as Slot[]), typeof (growing as unknown as Slot[]));