mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
Compute products of typed-array reads in 32 bits under bitwise operators (#2289)
This commit is contained in:
@@ -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
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6311-typed-array-integer-products.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Reference in New Issue
Block a user