Load array lengths with the range of a JS array length (#1121)

This commit is contained in:
Chris Tate
2026-10-16 07:55:40 -05:00
committed by GitHub
6 changed files with 76 additions and 6 deletions
@@ -63,7 +63,7 @@ export function emitArraySearchHelpers(
`define internal double @${arraySearchHelperName(kind)}(ptr %a, ptr %v) ${FN_ATTRS} {`,
`entry:`,
` %lenp = getelementptr inbounds %ScrArr, ptr %a, i32 0, i32 1`,
` %len = load ${sz}, ptr %lenp`,
` %len = load ${sz}, ptr %lenp${sz === "i64" ? ", !range !{i64 0, i64 4294967296}" : ""}`,
` %fp = getelementptr inbounds %ScrArr, ptr %a, i32 0, i32 6`,
` %f32 = load i32, ptr %fp`,
widen,
@@ -97,7 +97,9 @@ function emitDenseGuard(
const loadChecked = (): void => {
B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 1`);
host.markMemoryPointer(lenPtr, "array:header");
B.line(`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
B.line(
`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}${host.arrayLengthRange}`,
);
B.line(`${capPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 2`);
host.markMemoryPointer(capPtr, "array:header");
B.line(`${cap} = load ${size}, ptr ${capPtr}${host.fieldAliasAttachment(capPtr)}`);
+11 -2
View File
@@ -565,6 +565,13 @@ export class LlEmitter {
return `, !tbaa !${tag}`;
}
/** The `!range` attachment of an array length load: a length is at most
* 2^32 - 1 (the runtime's SCR_ARR_MAX_LENGTH), so conversions to double
* and back fold. 32-bit targets load the full range anyway. */
get arrayLengthRange(): string {
return this.sizeType === "i64" ? ", !range !{i64 0, i64 4294967296}" : "";
}
fieldAliasAttachment(ptr: string): string {
const tag = this.fieldPointerTags.get(ptr);
return tag === undefined ? "" : `, !tbaa !${tag}`;
@@ -7145,7 +7152,9 @@ export class LlEmitter {
done = B.newLabel("arr.len.done");
B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 1`);
this.markMemoryPointer(lenPtr, "array:header");
B.line(`${len} = load ${this.sizeType}, ptr ${lenPtr}${this.fieldAliasAttachment(lenPtr)}`);
B.line(
`${len} = load ${this.sizeType}, ptr ${lenPtr}${this.fieldAliasAttachment(lenPtr)}${this.arrayLengthRange}`,
);
B.line(`${lenNumber} = ${sizeToDouble(this.sizeType)} ${this.sizeType} ${len} to double`);
B.line(`${same} = fcmp oeq double ${length.name}, ${lenNumber}`);
B.condBr(same, done, resize);
@@ -7819,7 +7828,7 @@ export class LlEmitter {
B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${arr!.name}, i32 0, i32 1`);
this.markMemoryPointer(lenPtr, "array:header");
B.line(
`${rawLen} = load ${this.sizeType}, ptr ${lenPtr}${this.fieldAliasAttachment(lenPtr)}`,
`${rawLen} = load ${this.sizeType}, ptr ${lenPtr}${this.fieldAliasAttachment(lenPtr)}${this.arrayLengthRange}`,
);
B.line(`${len} = ${sizeToDouble(this.sizeType)} ${this.sizeType} ${rawLen} to double`);
B.line(`${inBounds} = fcmp olt double ${i}, ${len}`);
@@ -557,7 +557,9 @@ export function emitArrIntrinsic(
t = B.tmp();
B.line(`${p} = getelementptr inbounds %ScrArr, ptr ${r.name}, i32 0, i32 1`);
host.markMemoryPointer(p, "array:header");
B.line(`${len} = load ${host.sizeType}, ptr ${p}${host.fieldAliasAttachment(p)}`);
B.line(
`${len} = load ${host.sizeType}, ptr ${p}${host.fieldAliasAttachment(p)}${host.arrayLengthRange}`,
);
B.line(`${t} = ${sizeToDouble(host.sizeType)} ${host.sizeType} ${len} to double`);
return { name: t, type: e.type };
}
@@ -523,7 +523,9 @@ function emitDenseReferenceArrayRead(
belowLen = B.tmp();
B.line(`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 1`);
host.markMemoryPointer(lenPtr, "array:header");
B.line(`${len} = load ${host.sizeType}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
B.line(
`${len} = load ${host.sizeType}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}${host.arrayLengthRange}`,
);
B.line(`${belowLen} = icmp ult ${host.sizeType} ${offset}, ${len}`);
const stateLabel = B.newLabel("local.array.state");
B.condBr(belowLen, stateLabel, no);
@@ -0,0 +1,55 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile } from "../src/index.js";
async function emit(source: string, outputKind: "llvm" | "asm"): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-array-length-range-"));
try {
const entry = join(dir, "main.ts");
const outPath = join(dir, outputKind === "llvm" ? "main.ll" : "main.s");
await writeFile(entry, source);
const result = await compile(entry, { outDir: dir, outPath, outputKind });
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
return await readFile(outPath, "utf8");
} finally {
await rm(dir, { recursive: true, force: true });
}
}
const source = `
class Signature {
parameters: string[];
rest: boolean;
constructor(parameters: string[], rest: boolean) {
this.parameters = parameters;
this.rest = rest;
}
}
function count(signature: Signature): number {
const length = signature.parameters.length;
if (signature.rest) return length + signature.parameters[length - 1]!.length;
return length;
}
const signature = new Signature(["a", "bb"], process.argv.length > 1);
console.log(count(signature), count(new Signature(["x"], false)));
`;
test.runIf(process.arch === "x64" || process.arch === "arm64")(
"array length loads carry the range of a JS array length",
async () => {
const llvm = await emit(source, "llvm");
const body = /^define internal double @sc_bf_count\([^]*?^}/m.exec(llvm)?.[0] ?? "";
expect(body).toMatch(/= load i64, ptr %\S+(, !tbaa !\d+)?, !range !\{i64 0, i64 4294967296\}/);
},
);
test.runIf(process.arch === "x64")(
"a length used as an index is not converted to double and back",
async () => {
const asm = await emit(source, "asm");
const body = /^sc_bf_count:[^]*?\.cfi_endproc/m.exec(asm)?.[0] ?? "";
expect(body).not.toMatch(/cvtsi2sd|cvttsd2si/);
},
);