mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Keep proven int32 arithmetic in 32-bit integers
Integer arithmetic with an int32 result range materialized its number with `sitofp i64`, which LLVM does not know to be exact, so it could not fold the conversion back for the next integer use or through the phis of loop-carried locals: a counter written `i = (i + 1) | 0` converted to a double and back on every iteration. With int32 operands and result, `+`, `-` and `*` now emit `add/sub/mul nsw i32` and convert with `sitofp i32`; other int32-range numbers convert from the low half of their i64, and an int32-range read of a number uses `fptosi ... to i32`. The nsw flag stays on the 32-bit operation (a truncated 64-bit result would lose it), so IndVars can still widen such counters into 64-bit address arithmetic.
This commit is contained in:
@@ -11,7 +11,13 @@ import { BYTES_ELEM_NUM, closureIdentityEqual, f64Lit } from "./common.js";
|
||||
import { emitStringInputs } from "./string-lifetimes.js";
|
||||
import { emitStringParts, numberPart, stringParts } from "./string-construction.js";
|
||||
import { emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
import { exactInteger, widenInteger, integerNumber } from "./integer-values.js";
|
||||
import {
|
||||
exactInteger,
|
||||
int32Number,
|
||||
integerNumber,
|
||||
narrowInteger,
|
||||
widenInteger,
|
||||
} from "./integer-values.js";
|
||||
import {
|
||||
bitwiseIdentityOperand,
|
||||
integerArithmeticRange,
|
||||
@@ -233,9 +239,20 @@ export function emitOperatorExpr(
|
||||
const li = exactInteger(host, l, e.left),
|
||||
ri = exactInteger(host, r, e.right);
|
||||
if (li && ri) {
|
||||
const range = e.op === "%" ? null : integerArithmeticRange(e.op, li.range, ri.range);
|
||||
if (range && withinInt32(range) && withinInt32(li.range) && withinInt32(ri.range)) {
|
||||
// Int32 operands and result: 32-bit arithmetic that cannot
|
||||
// overflow. The nsw flag survives where a truncated 64-bit
|
||||
// result would lose it, so LLVM can widen loop counters such
|
||||
// as `i = (i + 1) | 0` into 64-bit address arithmetic.
|
||||
const result = B.tmp();
|
||||
B.line(
|
||||
`${result} = ${e.op === "+" ? "add" : e.op === "-" ? "sub" : "mul"} nsw i32 ${narrowInteger(host, li)}, ${narrowInteger(host, ri)}`,
|
||||
);
|
||||
return int32Number(host, result, range, e.type);
|
||||
}
|
||||
const left = widenInteger(host, li),
|
||||
right = widenInteger(host, ri);
|
||||
const range = e.op === "%" ? null : integerArithmeticRange(e.op, li.range, ri.range);
|
||||
if (range) {
|
||||
const result = B.tmp();
|
||||
B.line(
|
||||
|
||||
@@ -40,6 +40,12 @@ export function exactInteger(
|
||||
if (value.uint32 !== undefined && range.min >= -2147483648 && range.max <= 2147483647) {
|
||||
return { name: value.uint32, type: "i32", signed: true, range };
|
||||
}
|
||||
if (range.min >= -2147483648 && range.max <= 2147483647) {
|
||||
// An int32 conversion lets LLVM fold it with an int32 source's sitofp.
|
||||
const name = host.B.tmp();
|
||||
host.B.line(`${name} = fptosi double ${value.name} to i32`);
|
||||
return { name, type: "i32", signed: true, range };
|
||||
}
|
||||
const name = host.B.tmp();
|
||||
host.B.line(`${name} = fptosi double ${value.name} to i64`);
|
||||
return { name, type: "i64", signed: true, range };
|
||||
@@ -52,6 +58,26 @@ export function widenInteger(host: LlvmEmitterContext, value: LlInteger): string
|
||||
return wide;
|
||||
}
|
||||
|
||||
/** The i32 of an integer whose range is within int32. */
|
||||
export function narrowInteger(host: LlvmEmitterContext, value: LlInteger): string {
|
||||
if (value.type === "i32") return value.name;
|
||||
const narrow = host.B.tmp();
|
||||
host.B.line(`${narrow} = trunc i64 ${value.name} to i32`);
|
||||
return narrow;
|
||||
}
|
||||
|
||||
/** `integerNumber` for an i32 whose range is within int32. */
|
||||
export function int32Number(
|
||||
host: LlvmEmitterContext,
|
||||
name: string,
|
||||
range: IntegerRange,
|
||||
type: IrType,
|
||||
): LlValue {
|
||||
const number = host.B.tmp();
|
||||
host.B.line(`${number} = sitofp i32 ${name} to double`);
|
||||
return { name: number, type, uint32: name, integer: { name, type: "i32", signed: true, range } };
|
||||
}
|
||||
|
||||
export function integerNumber(
|
||||
host: LlvmEmitterContext,
|
||||
name: string,
|
||||
@@ -60,7 +86,18 @@ export function integerNumber(
|
||||
): LlValue {
|
||||
const number = host.B.tmp();
|
||||
const uint32 = host.B.tmp();
|
||||
host.B.line(`${number} = sitofp i64 ${name} to double`);
|
||||
host.B.line(`${uint32} = trunc i64 ${name} to i32`);
|
||||
// A proven int32 converts from its low half. The value is the same, but
|
||||
// LLVM folds `fptosi (sitofp i32 x)` back to x, also through the phis of
|
||||
// loop-carried locals (whose other incoming values are int32 too, such as
|
||||
// `x | 0`), while a 64-bit source is not known to convert exactly. So a
|
||||
// counter written `i = (i + 1) | 0` stays an integer instead of taking a
|
||||
// double round trip on every iteration.
|
||||
const int32 = range.min >= -2147483648 && range.max <= 2147483647;
|
||||
host.B.line(
|
||||
int32
|
||||
? `${number} = sitofp i32 ${uint32} to double`
|
||||
: `${number} = sitofp i64 ${name} to double`,
|
||||
);
|
||||
return { name: number, type, uint32, integer: { name, type: "i64", signed: true, range } };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
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, deserializeModule } from "../src/index.js";
|
||||
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
||||
|
||||
test("int32 counters stay 32-bit integer arithmetic", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-integer-reads-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
await writeFile(
|
||||
entry,
|
||||
`
|
||||
class Text {
|
||||
pos: number;
|
||||
end: number;
|
||||
units: Uint16Array;
|
||||
values: Float64Array;
|
||||
constructor(n: number) {
|
||||
this.units = new Uint16Array(n);
|
||||
this.values = new Float64Array(n);
|
||||
this.pos = 0;
|
||||
this.end = n | 0;
|
||||
}
|
||||
skip(): number {
|
||||
const units = this.units;
|
||||
const end = this.end;
|
||||
let pos = this.pos;
|
||||
while (pos < end) {
|
||||
const c = units[pos] | 0;
|
||||
if (c === 42 || c > 127) break;
|
||||
pos = (pos + 1) | 0;
|
||||
}
|
||||
this.pos = pos;
|
||||
return pos;
|
||||
}
|
||||
sum(at: number): number {
|
||||
return this.values[at] + this.units[at];
|
||||
}
|
||||
}
|
||||
const text = new Text(8);
|
||||
console.log(text.skip(), text.sum(3), text.sum(9));
|
||||
`,
|
||||
);
|
||||
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 mod = deserializeModule(await readFile(outPath, "utf8"));
|
||||
const ll = emitLlvmModule(mod);
|
||||
const body = (name: string): string =>
|
||||
ll.match(new RegExp(`define internal [^\\n]+ @sc_bf_${name}\\([^\\n]*\\) #0 \\{([\\s\\S]*?)\\n\\}`))![1]!;
|
||||
const skip = body("_x25_Text_skip");
|
||||
// The counter's increment is 32-bit arithmetic that cannot overflow and
|
||||
// converts from i32, so LLVM folds the double round trip of the local.
|
||||
expect(skip).toMatch(/ = add nsw i32 /);
|
||||
expect(skip).not.toMatch(/ = sitofp i64 /);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user