From 2bb7e7ec7a5ec8b63c87a3ab377891536df39e1f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 16 Oct 2026 07:33:26 -0500 Subject: [PATCH] Load array lengths with the range of a JS array length An array's length is at most 2^32 - 1 (the runtime's SCR_ARR_MAX_LENGTH). On 64-bit targets the length loads now carry that range (`!range`), so LLVM folds a length converted to double and back to an integer, and simplifies the comparisons that use it. A length stored into an int32 local or used as an index no longer goes through double: tsc-ts has 18% fewer int/double conversion instructions. --- .../compiler/src/backend/llvm/array-search.ts | 2 +- .../src/backend/llvm/dense-array-access.ts | 4 +- packages/compiler/src/backend/llvm/emitter.ts | 13 ++++- .../src/backend/llvm/expr-containers.ts | 4 +- .../src/backend/llvm/local-array-reads.ts | 4 +- .../test/array-length-range-emission.test.ts | 55 +++++++++++++++++++ 6 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/array-length-range-emission.test.ts diff --git a/packages/compiler/src/backend/llvm/array-search.ts b/packages/compiler/src/backend/llvm/array-search.ts index 92a4870ec..8c9590bee 100644 --- a/packages/compiler/src/backend/llvm/array-search.ts +++ b/packages/compiler/src/backend/llvm/array-search.ts @@ -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, diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index 9dd010431..9f3535156 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -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)}`); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 89f723e1a..4ae8865b0 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -554,6 +554,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}`; @@ -7111,7 +7118,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); @@ -7785,7 +7794,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}`); diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index cd05d6993..04ae64d21 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -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 }; } diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts index 5646de371..b31c02ad0 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -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); diff --git a/packages/compiler/test/array-length-range-emission.test.ts b/packages/compiler/test/array-length-range-emission.test.ts new file mode 100644 index 000000000..4628298dd --- /dev/null +++ b/packages/compiler/test/array-length-range-emission.test.ts @@ -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 { + 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/); + }, +);