Compute products of typed-array reads in 32 bits under bitwise operators (#2289)

This commit is contained in:
Chris Tate
2026-10-25 20:58:49 -05:00
committed by GitHub
7 changed files with 139 additions and 19 deletions
@@ -303,7 +303,7 @@ function emitScopedBytesGet(
const signed = elem === "i8" || elem === "i16" || elem === "i32";
B.line(`${number} = ${signed ? "sitofp" : "uitofp"} i32 ${u32} to double`);
B.line(`${out} = select i1 ${valid}, double ${number}, double ${f64Lit(NaN)}`);
return { name: out, type: F64, uint32: u32 };
return { name: out, type: F64, uint32: u32, ordered: valid };
}
function emitBytesLoad(
@@ -21,6 +21,10 @@ export interface LlValue {
* its absolute value is below `below` (a zero may be -0). Index consumers
* use it in place of a round trip (integer-values.ts `indexInteger`). */
wideInteger?: WideInteger;
/** An i1 SSA value (or `true`) that is true exactly when this number is
* not NaN: set by NaN-coded integer element reads and the wrapping sums
* of them, so ToInt32 consumers need no floating compare. */
ordered?: string;
slot?: boolean;
/** A sequence local held in a capture box; release the box itself. */
boxed?: boolean;
@@ -278,6 +278,18 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits"
returning("float_read_add", bin("|", bin("+", length, read("floats")), zero)),
returning("product", bin("|", bin("*", length, length), zero)),
returning("number_add", bin("+", length, read("bytes"))),
returning(
"read_products",
bin(
">>>",
bin(
"+",
bin("*", read("bytes"), { kind: "numLit", value: 77, type: F64, loc }),
bin("*", read("bytes"), { kind: "numLit", value: 150, type: F64, loc }),
),
zero,
),
),
];
const llvm = emitLlvmModule(mod);
const body = (name: string): string =>
@@ -292,8 +304,10 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits"
expect(body(name), name).not.toContain("uint32.coerce");
}
// A NaN-coded read converts to 0 through the sum: the result selects 0
// unless every NaN-coded operand is ordered.
expect(body("read_add")).toMatch(/ = fcmp ord double .*, 0\.0$/m);
// unless every NaN-coded operand is ordered. A read's own validity bit
// tells, so the number is not compared.
expect(body("read_add")).toMatch(/ = phi i1 \[ true, /);
expect(body("read_add")).not.toMatch(/fcmp ord/);
expect(body("read_add")).toMatch(/ = add i32 /);
expect(body("read_add")).toMatch(/ = select i1 .*, i32 .*, i32 0$/m);
expect(body("read_chain")).toMatch(/ = and i1 /);
@@ -309,6 +323,12 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits"
expect(body(name), name).not.toMatch(/ = (add|mul) i32 /);
}
expect(body("number_add")).not.toMatch(/ = add i32 |fcmp ord/);
// Products of NaN-coded reads and constants are exact integers or NaN, so
// their sum adds in 32 bits too, selected by the reads' validity bits.
expect(body("read_products")).toMatch(/ = mul i32 /);
expect(body("read_products")).toMatch(/ = add i32 /);
expect(body("read_products")).not.toContain("uint32.coerce");
expect(body("read_products")).not.toMatch(/fcmp ord/);
});
test.each([32, 64] as const)(
@@ -367,8 +367,8 @@ export function emitOperatorExpr(
`${result} = ${e.op === "+" ? "add" : e.op === "-" ? "sub" : "mul"} nsw i64 ${left}, ${right}`,
);
const number = integerNumber(host, result, range, e.type);
const uint32 = emitWrappingSum(host, e, l, r, li, ri);
return uint32 === undefined ? number : { ...number, uint32 };
const wrapped = emitWrappingSum(host, e, l, r, li, ri);
return wrapped === undefined ? number : { ...number, ...wrapped };
}
if (e.op === "%" && li.range.min >= 0 && ri.range.min > 0) {
const range = { min: 0, max: Math.min(li.range.max, ri.range.max - 1) };
@@ -411,12 +411,12 @@ export function emitOperatorExpr(
return { name: emitPowerOfTwoQuotient(host, l.name, r.name), type: e.type };
if (arith[e.op] !== undefined) {
B.line(`${t} = ${arith[e.op]} double ${l.name}, ${r.name}`);
const uint32 = emitWrappingSum(host, e, l, r, li, ri);
const wrapped = emitWrappingSum(host, e, l, r, li, ri);
// The exact integer beside the double, for index consumers.
const wideInteger = wideArithmetic(host, e.op, l, li, r, ri);
if (uint32 !== undefined || wideInteger) {
if (wrapped !== undefined || wideInteger) {
const value: LlValue = { name: t, type: e.type };
if (uint32 !== undefined) value.uint32 = uint32;
if (wrapped !== undefined) Object.assign(value, wrapped);
if (wideInteger) value.wideInteger = wideInteger;
return value;
}
@@ -22,8 +22,9 @@ export function markWrappingOperands(host: LlvmEmitterContext, ...operands: IrEx
/** The values an f64 expression can have when each one is an exact integer
* (never -0) or NaN: proven ranges, safe integer literals, NaN-coded reads
* of integer typed-array elements and of Int32[] elements, `charCodeAt`,
* and sums and differences of those whose every integer result is still
* exact in a double. */
* and sums, differences and products of those whose every integer result
* is a safe integer, which the double holds exactly. A product that can be
* -0 (a zero times a negative) has no range. */
export function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerRange | null {
if (e.type.kind !== "f64") return null;
const range = host.integerRanges.get(e);
@@ -51,7 +52,7 @@ export function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerR
// A UTF-16 code unit, or NaN past either end.
if (e.kind === "strIntrinsic" && e.method === "charCodeAt") return { min: 0, max: 65535 };
if (e.kind === "varRef") return host.integerOrNaNLocals.get(e.localId) ?? null;
if (e.kind === "bin" && (e.op === "+" || e.op === "-")) {
if (e.kind === "bin" && (e.op === "+" || e.op === "-" || e.op === "*")) {
const left = integerOrNaNRange(host, e.left);
const right = left && integerOrNaNRange(host, e.right);
return right ? integerArithmeticRange(e.op, left!, right) : null;
@@ -113,9 +114,14 @@ function wrappingOperand(
return { low, valid: null };
}
// The number is an exact integer or NaN, and its ToUint32 view is the
// integer's low 32 bits or 0. An ordered compare tells the two apart.
const valid = B.tmp();
B.line(`${valid} = fcmp ord double ${value.name}, 0.0`);
// integer's low 32 bits or 0. Its ordered bit, or an ordered compare,
// tells the two apart.
let valid: string | null = value.ordered ?? null;
if (valid === "true") valid = null;
else if (valid === null) {
valid = B.tmp();
B.line(`${valid} = fcmp ord double ${value.name}, 0.0`);
}
if (value.uint32 !== undefined) return { low: value.uint32, valid };
// Without a view, convert once. The conversion of NaN is poison, but the
// result only selects the sum when every operand is ordered.
@@ -137,8 +143,9 @@ function wrappingOperand(
* low halves is the converted exact result; a NaN operand makes the result
* NaN, which converts to 0. A product is exact when the operands' magnitude
* bounds multiply to at most 2^53; it may be -0 (`0 * -1`), which converts
* to 0 like the integer 0. Returns undefined, emitting nothing, for any
* other expression. */
* to 0 like the integer 0. Returns the converted result with its ordered
* bit (`true` when no operand can be NaN), or undefined, emitting nothing,
* for any other expression. */
export function emitWrappingSum(
host: LlvmEmitterContext,
e: IrExpr & { kind: "bin" },
@@ -146,7 +153,7 @@ export function emitWrappingSum(
r: LlValue,
li: LlInteger | null,
ri: LlInteger | null,
): string | undefined {
): { uint32: string; ordered: string } | undefined {
if ((e.op !== "+" && e.op !== "-" && e.op !== "*") || !host.wrappingSums.has(e)) return undefined;
const leftRange = li ? li.range : integerOrNaNRange(host, e.left);
const rightRange = leftRange && (ri ? ri.range : integerOrNaNRange(host, e.right));
@@ -165,14 +172,14 @@ export function emitWrappingSum(
`${sum} = ${e.op === "+" ? "add" : e.op === "-" ? "sub" : "mul"} i32 ${left.low}, ${right.low}`,
);
let valid = left.valid ?? right.valid;
if (valid === null) return sum;
if (valid === null) return { uint32: sum, ordered: "true" };
if (left.valid && right.valid) {
valid = B.tmp();
B.line(`${valid} = and i1 ${left.valid}, ${right.valid}`);
}
const out = B.tmp();
B.line(`${out} = select i1 ${valid}, i32 ${sum}, i32 0`);
return out;
return { uint32: out, ordered: valid };
}
/** Whether every product of a value in `left` and one in `right` is an
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6311-typed-array-integer-products.ts"
],
"diags": []
}