Read elements of native arrays passed to untyped code by index

A typed array such as a number[] reaches untyped code as a capsule of the
native array. A keyed read `xs[i]` there missed the inline cache, which
answers only dyn arrays, and went to the member helper, which converted
the index to a string; the capsule's key lookup then parsed the string
back into a number for every element.

Keyed sites now read an element of a native number[] capsule in place
when the index is an integer below the array's hole-free prefix, and
`length` sites read the native array's length. Other capsule elements
(holes, out-of-range and fractional indices, arrays of other element
types) are answered by the keyed probe through scr_dyn_index_get without
forming the key, or by the helper as before.
This commit is contained in:
Chris Tate
2026-10-24 21:59:28 -05:00
parent cf399bc1bf
commit 4241ca533f
9 changed files with 268 additions and 12 deletions
+117 -11
View File
@@ -384,6 +384,15 @@ function emitGet(host: Host, e: ExprOf<"call">, callee: IrFunction, key: string)
incoming.push(`[ ${numberResult(host, count, e)}, %${B.currentLabel()} ]`);
B.br(join);
B.startBlock(other);
// So is the length of a native array passed in from typed code.
const rest = B.newLabel("icg.rest");
const arr = nativeArray(host, receiver.name, rest);
const native = load(host, "i32", arr, 8);
const nativeCount = B.tmp();
B.line(`${nativeCount} = uitofp i32 ${native} to double`);
incoming.push(`[ ${numberResult(host, nativeCount, e)}, %${B.currentLabel()} ]`);
B.br(join);
B.startBlock(rest);
}
// Inline hits (borrowed values) and probe hits (owned ones) share one
// conversion to the result type.
@@ -623,6 +632,98 @@ function numberKey(host: Host, key: IrExpr): NumberKey | null {
};
}
/** Branches to `yes` when `d` is a capsule of a native array passed in
* from typed code (a TYPED_REF bound to live array ops, not a record or
* class instance), with the ScrArr in `yes`; else to `no`. */
function nativeArray(host: Host, d: string, no: string): string {
const B = host.B;
const capsule = B.newLabel("ica.cap");
const bound = B.newLabel("ica.ops");
const yes = B.newLabel("ica.arr");
kindIs(host, d, DYN_KIND.TYPED_REF, capsule, no);
B.startBlock(capsule);
// The flag word after `casts`: record, klass and bag_class are its top
// three bits; any of them means `array` holds other ops.
const flags = load(host, "i32", d, 88);
const kinds = B.tmp();
const plain = B.tmp();
B.line(`${kinds} = lshr i32 ${flags}, 29`);
B.line(`${plain} = icmp eq i32 ${kinds}, 0`);
B.condBr(plain, bound, no);
B.startBlock(bound);
const ops = load(host, "ptr", d, 64);
const live = B.tmp();
B.line(`${live} = icmp ne ptr ${ops}, null`);
B.condBr(live, yes, no);
B.startBlock(yes);
return load(host, "ptr", d, 16);
}
/** `xs[i]` on a native number[] passed in from typed code: an integral
* index below the array's hole-free prefix reads the element in place.
* The other cases branch to `miss`, whose probe and helper answer them as
* before. The result joins `incoming` at `join`. */
function emitNativeNumberElement(
host: Host,
receiver: string,
e: IrExpr,
indexIn: (next: string) => string,
miss: string,
join: string,
incoming: string[],
): void {
const B = host.B;
const arr = nativeArray(host, receiver, miss);
const numbers = B.newLabel("ica.f64");
const elem = load(host, "i8", arr, 16);
const f64 = B.tmp();
B.line(`${f64} = icmp eq i8 ${elem}, 0 ; SCR_ELEM_F64`);
B.condBr(f64, numbers, miss);
B.startBlock(numbers);
const index = indexIn(B.newLabel("ica.idx"));
const filled = load(host, "i32", arr, 20);
const filledD = B.tmp();
const nonNegative = B.tmp();
const below = B.tmp();
const ok = B.tmp();
B.line(`${filledD} = uitofp i32 ${filled} to double`);
B.line(`${nonNegative} = fcmp oge double ${index}, 0.0`);
B.line(`${below} = fcmp olt double ${index}, ${filledD}`);
B.line(`${ok} = and i1 ${nonNegative}, ${below}`);
const ranged = B.newLabel("ica.rng");
B.condBr(ok, ranged, miss);
B.startBlock(ranged);
const i = B.tmp();
const back = B.tmp();
const integral = B.tmp();
B.line(`${i} = fptoui double ${index} to i64`);
B.line(`${back} = uitofp i64 ${i} to double`);
B.line(`${integral} = fcmp oeq double ${back}, ${index}`);
const hit = B.newLabel("ica.hit");
B.condBr(integral, hit, miss);
B.startBlock(hit);
const data = load(host, "ptr", arr, 24);
const slot = B.tmp();
const value = B.tmp();
B.line(`${slot} = getelementptr inbounds double, ptr ${data}, i64 ${i}`);
B.line(`${value} = load double, ptr ${slot}`);
let out: string;
if (e.type.kind === "jsval") {
const raw = B.tmp();
const nan = B.tmp();
const bits = B.tmp();
out = B.tmp();
B.line(`${raw} = bitcast double ${value} to i64`);
B.line(`${nan} = fcmp uno double ${value}, ${value}`);
B.line(`${bits} = select i1 ${nan}, i64 ${JS_BITS.canonicalNan}, i64 ${raw}`);
B.line(`${out} = inttoptr i64 ${bits} to ptr`);
} else {
out = numberResult(host, value, e);
}
incoming.push(`[ ${out}, %${B.currentLabel()} ]`);
B.br(join);
}
function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue {
const B = host.B;
const receiver = emitBorrowedInput(host, e.args[0]!);
@@ -645,18 +746,23 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue {
B.line(`store ptr null, ptr ${keySlot}`);
host.ownSlot(keySlot, DYN);
}
kindIs(host, receiver.name, DYN_KIND.ARR, array, probe);
/** The key as a double in block `next`; other keys go to the probe. */
const indexIn = (next: string): string => {
if (numeric === null) {
kindIs(host, key!.name, DYN_KIND.NUM, next, probe);
B.startBlock(next);
return load(host, "double", key!.name, 16);
}
B.condBr(numeric.present, next, probe);
B.startBlock(next);
return numeric.value;
};
const other = B.newLabel("ick.other");
kindIs(host, receiver.name, DYN_KIND.ARR, array, other);
B.startBlock(other);
emitNativeNumberElement(host, receiver.name, e, indexIn, probe, join, incoming);
B.startBlock(array);
let index: string;
if (numeric === null) {
kindIs(host, key!.name, DYN_KIND.NUM, number, probe);
B.startBlock(number);
index = load(host, "double", key!.name, 16);
} else {
B.condBr(numeric.present, number, probe);
B.startBlock(number);
index = numeric.value;
}
const index = indexIn(number);
const len = load(host, "i64", receiver.name, 16);
const i = B.tmp();
const back = B.tmp();
@@ -134,3 +134,44 @@ console.log(len2({ x: 3, y: 4 }), typeof parsed);
await rm(dir, { recursive: true, force: true });
}
});
test("keyed and length sites read native number arrays in place", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-native-"));
try {
const entry = join(dir, "main.ts");
const outPath = join(dir, "main.ir.json");
await writeFile(join(dir, "package.json"), `{ "type": "module" }`);
await writeFile(
join(dir, "lib.cjs"),
`exports.total = function (xs) {
let s = 0;
for (let i = 0; i < xs.length; i++) s += xs[i];
return s;
};
`,
);
await writeFile(
entry,
`import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const lib: { total(xs: number[]): number } = require("./lib.cjs");
console.log(lib.total([1, 2, 3]));
`,
);
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
if (!result.ok)
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
const llvm = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8")));
const fn = llvm.split("\ndefine ").find((f) => f.includes("ica.f64")) ?? "";
// The keyed read tests the capsule's element kind and loads the double
// in place; other receivers keep the probe.
expect(fn).toMatch(/icmp eq i8 %t\d+, 0 ; SCR_ELEM_F64/);
expect(fn).toMatch(/load double, ptr %t\d+/);
expect(fn).toContain("@scr_js_keyed_probe(");
// The length read answers from the native array's length.
expect(fn).toContain("icg.rest");
expect(fn).toContain("@scr_js_load_probe(");
} finally {
await rm(dir, { recursive: true, force: true });
}
});
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/8300-native-array-capsule-reads/lib.cjs",
"<repo>/tests/corpus/8300-native-array-capsule-reads/main.ts"
],
"diags": []
}
+3 -1
View File
@@ -523,7 +523,9 @@ struct ScrDyn {
* identity while the source reference remains observable. */
ScrDyn *materialized;
ScrDynTypedCast *casts;
/* One word keeps this, the largest payload, at its previous size. */
/* One word keeps this, the largest payload, at its previous size.
* Dynamic-tier inline caches read record, klass and bag_class as
* the word's top three bits (backend/llvm/js-ic.ts nativeArray). */
uint32_t type_key_len : 27;
uint32_t traced : 1; /* native referent has a cycle header */
uint32_t observed : 1; /* native release reports this identity's disposal */
+3
View File
@@ -355,6 +355,9 @@ bool scr_js_store_probe(ScrDyn *receiver, const ScrStr *key, ScrDyn *value, ScrJ
/* ── keyed loads ──────────────────────────────────────────────────────── */
ScrDyn *scr_js_index_probe(ScrDyn *receiver, double index) {
/* A native array passed in from typed code answers its elements in
* place, without forming the index's key string. */
if (receiver->kind == SCR_DYN_TYPED_REF) return scr_dyn_index_get(receiver, index);
if (receiver->kind != SCR_DYN_ARR || !(index >= 0) || index >= (double)receiver->v.arr.len)
return NULL;
size_t i = (size_t)index;
@@ -0,0 +1,36 @@
"use strict";
// Reads and writes the arrays the typed program passes in.
exports.total = function (xs) {
let s = 0;
for (let i = 0; i < xs.length; i++) s += xs[i];
return s;
};
exports.at = function (xs, i) {
return xs[i];
};
exports.describe = function (xs, keys) {
const out = [];
for (const k of keys) {
const v = xs[k];
out.push(typeof v === "number" && Object.is(v, -0) ? "-0" : String(v));
}
return out.join(" ");
};
exports.has = function (xs, i) {
return i in xs;
};
exports.write = function (xs, i, v) {
xs[i] = v;
return xs[i];
};
exports.grow = function (xs, v) {
xs.push(v);
return xs.length;
};
exports.cut = function (xs, n) {
xs.length = n;
return xs.length;
};
exports.lengthOf = function (xs) {
return xs.length;
};
@@ -0,0 +1,59 @@
// A typed program passes its arrays to untyped CommonJS functions, which
// read their elements and lengths, write them and resize them. Element reads
// cover integral, negative, fractional, non-finite and out-of-range indexes,
// string keys, holes, frozen arrays and arrays of other element types.
import { createRequire } from "node:module";
interface Lib {
total(xs: readonly unknown[]): number;
at(xs: readonly unknown[], i: unknown): unknown;
describe(xs: readonly unknown[], keys: unknown[]): string;
has(xs: readonly unknown[], i: number): boolean;
write(xs: unknown[], i: number, v: unknown): unknown;
grow(xs: unknown[], v: unknown): number;
cut(xs: unknown[], n: number): number;
lengthOf(xs: readonly unknown[]): number;
}
const require = createRequire(import.meta.url);
const lib: Lib = require("./lib.cjs");
// Element reads of a number[] at integral, negative, fractional,
// non-finite and out-of-range indexes, and through string keys.
const nums: number[] = [1, 2.5, -0, NaN, Infinity, -7];
console.log(lib.total([1, 2, 3, 4]), lib.total(nums), lib.total([]));
console.log(lib.describe(nums, [0, 1, 2, 3, 4, 5, 6, -1, 1.5, -0, NaN, Infinity, 2 ** 32, "1", "01", "length"]));
console.log(lib.at(nums, 5), lib.at(nums, 6), typeof lib.at(nums, 2), Object.is(lib.at(nums, 2), -0));
// Holes: below and past the hole-free prefix, and an array cut longer.
const holey: number[] = [1, 2];
holey[4] = 5;
console.log(lib.describe(holey, [0, 1, 2, 3, 4, 5]), lib.has(holey, 3), lib.has(holey, 4), lib.lengthOf(holey));
const longer: number[] = [3, 4, 5];
longer.length = 6;
console.log(lib.describe(longer, [0, 2, 3, 5]), lib.lengthOf(longer));
// Writes from the package and from the program are seen by both.
const live: number[] = [10, 20, 30];
console.log(lib.write(live, 0, 11), lib.write(live, 3, 40), lib.grow(live, 50), live.join(","));
live.push(60);
live[1] = 21;
console.log(lib.total(live), lib.lengthOf(live), lib.at(live, 5));
console.log(lib.cut(live, 2), lib.total(live), live.length, lib.at(live, 2));
// Frozen and empty arrays.
const frozen: readonly number[] = Object.freeze([4, 5, 6]);
console.log(lib.total(frozen), lib.at(frozen, 1), lib.at([], 0), lib.lengthOf([]));
// Arrays of other element types read through the same sites.
const words: string[] = ["a", "b", "c"];
const flags: boolean[] = [true, false];
const points: { x: number }[] = [{ x: 1 }, { x: 2 }];
console.log(lib.describe(words, [0, 2, 3]), lib.describe(flags, [0, 1, 2]), (lib.at(points, 1) as { x: number }).x);
console.log(lib.at(words, 1.5), lib.has(words, 2), lib.lengthOf(points));
// A loop over a larger array.
const big: number[] = [];
for (let i = 0; i < 1000; i++) big.push(i % 7);
let sum = 0;
for (let round = 0; round < 50; round++) sum += lib.total(big);
console.log(sum);
@@ -0,0 +1 @@
{ "type": "module" }
@@ -0,0 +1 @@
{ "plain": 4860, "san": 7734 }