mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
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:
@@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
+7
@@ -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": []
|
||||
}
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user