Optimize proven integer arithmetic

This commit is contained in:
Chris Tate
2026-09-24 11:02:44 -05:00
parent fa64cd8ddf
commit 67e12aafd9
12 changed files with 369 additions and 9 deletions
@@ -43,6 +43,7 @@ import type {
import { ffiCallbackType, funcOf, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, mapOf, moduleEmbedsCompressedNpm, moduleUsesChildProcess, moduleUsesDgram, moduleUsesDynInvoke, moduleEmbedsBuiltin, moduleUsesFetch, moduleUsesFsWatch, moduleUsesHttp2, moduleUsesHttpServer, moduleUsesNet, moduleUsesNodeTest, moduleUsesProcessEvents, moduleUsesStream, moduleUsesTls, moduleUsesTlsCa, POINTER_KINDS, type PointerKind, RUNTIME_EMITTER_CLASS, STRING, VOID } from "../../ir/ir.js";
import { undefinedArmTag } from "../../ir/analysis.js";
import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
import type { IntegerRanges } from "../../ir/integer-ranges.js";
import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/constant-tables.js";
import { allocateFfiCallbackAdapters, hasForeignFfiCallback, hasRetainedFfiCallback, type FfiCallbackAdapter } from "../ffi-callbacks.js";
import {
@@ -202,6 +203,7 @@ export class CEmitter {
* unsigned integer shadow. Ordinary number reads widen the shadow back to
* f64; direct byte indices consume it without a conversion round trip. */
integerLoopBindings = new Map<string, string>();
integerRanges: IntegerRanges = new Map();
/** Declared functions referenced as values: each needs an env-signature
* wrapper + an interned immortal closure (so `f === f` holds). */
readonly fnValues = new Set<string>();
+27 -1
View File
@@ -838,6 +838,25 @@ function emitLiteralExpr(
}
}
/** Unsigned operations avoid signed overflow and implementation-defined
* right shifts/conversions. Reinterpret the final bits numerically. */
function emitIntegerBits(emitter: CEmitter, e: ExprOf<"bin" | "unary">, left: string, right?: string): Temp {
const a = `sc_bits_${emitter.tempCounter++}`;
const out = `sc_bits_${emitter.tempCounter++}`;
emitter.line(`uint32_t ${a} = (uint32_t)(int64_t)${left};`);
let operation: string;
if (e.kind === "unary") operation = `~${a}`;
else {
const b = `sc_bits_${emitter.tempCounter++}`;
const shift = e.op === "<<" || e.op === ">>" || e.op === ">>>";
emitter.line(`uint32_t ${b} = (uint32_t)(int64_t)${right}${shift ? " & 31u" : ""};`);
operation = e.op === ">>" ? `(${a} >> ${b}) | ((${a} & 0x80000000u) ? ~(UINT32_MAX >> ${b}) : 0u)`
: `${a} ${e.op === ">>>" ? ">>" : e.op} ${b}`;
}
emitter.line(`uint32_t ${out} = ${operation};`);
return emitter.newTemp(e.type, e.op === ">>>" ? `(double)${out}` : `(double)${out} - ((${out} & 0x80000000u) ? 4294967296.0 : 0.0)`);
}
function emitOperatorExpr(
emitter: CEmitter,
e: ExprOf<"bin" | "unary" | "incDec" | "fieldIncDec" | "assignExpr" | "seqExpr">,
@@ -846,6 +865,12 @@ function emitOperatorExpr(
case "bin": {
const l = emitter.emitExpr(e.left);
const r = emitter.emitExpr(e.right);
if ((e.op === "+" || e.op === "-") && emitter.integerRanges.get(e)) {
return emitter.newTemp(e.type, `(double)((int64_t)${l.name} ${e.op} (int64_t)${r.name})`);
}
if (["&", "|", "^", "<<", ">>", ">>>"].includes(e.op) && emitter.integerRanges.get(e.left) && emitter.integerRanges.get(e.right)) {
return emitIntegerBits(emitter, e, l.name, r.name);
}
switch (e.op) {
case "%":
return emitter.newTemp(e.type, `fmod(${l.name}, ${r.name})`);
@@ -875,7 +900,8 @@ function emitOperatorExpr(
}
case "unary": {
const v = emitter.emitExpr(e.operand);
if (e.op === "~") return emitter.newTemp(e.type, `scr_bit_not(${v.name})`);
if (e.op === "~") return emitter.integerRanges.get(e.operand)
? emitIntegerBits(emitter, e, v.name) : emitter.newTemp(e.type, `scr_bit_not(${v.name})`);
return emitter.newTemp(e.type, `${e.op}${v.name}`);
}
case "incDec": {
+2
View File
@@ -11,6 +11,7 @@ import { boxAccess, cDecl, cStringLiteral, elemAccess, vAdapters } from "./types
import { OVERFLOW_MEMBER } from "./shapes.js";
import { emitStableReceiver } from "./exprs.js";
import { matchIntegerBytesForLoop } from "../../ir/integer-loops.js";
import { analyzeIntegerRanges } from "../../ir/integer-ranges.js";
import { endsWithJump, matchStringSelfConcat } from "../../ir/analysis.js";
@@ -32,6 +33,7 @@ export function emitFunction(emitter: CEmitter, fn: IrFunction): void {
emitter.currentLocals = new Map(fn.locals.map((l) => [l.id, l]));
emitter.captureIds = new Set((fn.captures ?? []).map((c) => c.localId));
emitter.integerLoopBindings.clear();
emitter.integerRanges = analyzeIntegerRanges(fn);
emitter.line(`${emitter.signature(fn)} {${emitter.srcComment(fn.loc)}`);
emitter.indent++;
@@ -82,6 +82,7 @@ import type {
import { CAUGHT, ffiCallbackType, isDynTypedRefType, isFfiContextParam, isRefCounted, isUnitType, moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, moduleUsesChildProcess, moduleUsesDynInvoke, moduleUsesFetch, moduleUsesFsWatch, moduleUsesHttpServer, moduleUsesNet, moduleUsesNodeTest, moduleUsesProcessEvents, moduleUsesStream, moduleUsesTls, moduleUsesTlsCa, NPM_COMPRESS_MIN, POINTER_KINDS, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, typeKey, VOID } from "../../ir/ir.js";
import { matchIntegerBytesForLoop } from "../../ir/integer-loops.js";
import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
import { analyzeIntegerRanges, type IntegerRanges } from "../../ir/integer-ranges.js";
import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/constant-tables.js";
import { allocateFfiCallbackAdapters, hasForeignFfiCallback, hasRetainedFfiCallback, type FfiCallbackAdapter } from "../ffi-callbacks.js";
import { RUNTIME_ABI_MARKER } from "../runtime-abi.js";
@@ -336,6 +337,7 @@ class LlEmitter {
private captureIds = new Set<string>();
/** Active canonical byte-loop induction bindings: local id → size_t slot. */
private integerLoopBindings = new Map<string, string>();
private integerRanges: IntegerRanges = new Map();
/** Enclosing try-with-FINALLY regions, innermost last: a `return`
* inside one runs every crossed finally (innermost first) before the
* actual ret — the C emitter's pending-return path, with the finally
@@ -2972,6 +2974,7 @@ class LlEmitter {
this.currentLocals = new Map(fn.locals.map((l) => [l.id, l]));
this.captureIds = new Set((fn.captures ?? []).map((c) => c.localId));
this.integerLoopBindings.clear();
this.integerRanges = analyzeIntegerRanges(fn);
this.chainSlots.clear();
this.finallyStack = [];
this.tryStack = [];
@@ -4356,8 +4359,8 @@ class LlEmitter {
return emitBytesGet(this.expressionContext(), elem, receiver, index, integerIndex);
}
private emitToUint32(value: string): string {
return emitToUint32(this.expressionContext(), value);
private emitToUint32(value: string, expr?: IrExpr): string {
return emitToUint32(this.expressionContext(), value, expr);
}
private emitBytesSet(elem: IrBytesElem, receiver: string, index: string, value: string, integerIndex = false): void {
@@ -172,8 +172,15 @@ export function emitBytesGet(host: LlvmEmitterContext, elem: IrBytesElem, receiv
return { name: out, type: F64 };
}
export function emitToUint32(host: LlvmEmitterContext, value: string): string {
export function emitToUint32(host: LlvmEmitterContext, value: string, expr?: IrExpr): string {
const B = host.B;
if (expr && host.integerRanges.get(expr)) {
const integer = B.tmp();
const out = B.tmp();
B.line(`${integer} = fptosi double ${value} to i64`);
B.line(`${out} = trunc i64 ${integer} to i32`);
return out;
}
const aboveMin = B.tmp();
const belowMax = B.tmp();
const fast = B.tmp();
@@ -4,6 +4,7 @@
import type { IrBytesElem, IrExpr, IrFfiImport, IrFunction, IrLibFn, IrLocal, IrModule, IrRecordShape, IrStmt, IrType, IrUnionDef, SrcLoc } from "../../ir/ir.js";
import type { FfiCallbackAdapter } from "../ffi-callbacks.js";
import type { ConstantNumericTable } from "../../ir/constant-tables.js";
import type { IntegerRanges } from "../../ir/integer-ranges.js";
import type { BlockBuilder } from "./blocks.js";
import type { LlClassMeta } from "./classes.js";
import type { LlDyn } from "./dyn.js";
@@ -56,6 +57,7 @@ export interface LlvmEmitterContext extends ShapeHost {
cstr(text: string): string;
currentGenerator: { yieldT: IrType; nextT: IrType; } | null;
constantNumericTables: ReadonlyMap<string, ConstantNumericTable>;
integerRanges: IntegerRanges;
currentWasiCoro: { kind: "async" | "generator"; id: string; handle: string; self: string; finalLabel: string; cleanupLabel: string; suspendLabel: string; } | null;
declare(decl: string): void;
dyn: LlDyn;
@@ -74,7 +76,7 @@ export interface LlvmEmitterContext extends ShapeHost {
emitBytesIntrinsic(e: IrExpr & { kind: "bytesIntrinsic" }): LlValue;
emitBytesLength(elem: IrBytesElem, receiver: string, bytes: boolean): LlValue;
emitStableReceiver(receiver: IrExpr, following: IrExpr[]): LlValue;
emitToUint32(value: string): string;
emitToUint32(value: string, expr?: IrExpr): string;
emitCallExpr(e: ExprOf<"call" | "ffiCall" | "closure" | "callValue" | "selfRef" | "new" | "classRef" | "newValue" | "instanceOfValue" | "promiseVoidWiden" | "upcast" | "downcast" | "instanceOf" | "virtualCall">): LlValue;
emitChildProcessLibCall(e: LibCallExpr): LlValue;
emitContainerExpr(e: ExprOf<"arrayLit" | "arrayNewLen" | "arrayGet" | "arrayHas" | "arrayState" | "arrIntrinsic" | "bytesNew" | "bytesIntrinsic" | "mapNew" | "mapIntrinsic" | "setIntrinsic" | "setNew">): LlValue;
@@ -98,14 +98,25 @@ export function emitOperatorExpr(host: LlvmEmitterContext, e: ExprOf<"bin" | "un
B.line(`${t} = icmp ${e.op === "===" ? "eq" : "ne"} ptr ${l.name}, ${r.name}`);
} else if (arith[e.op] !== undefined || cmp[e.op] !== undefined) {
if (e.left.type.kind !== "f64") throw new LlvmUnsupportedError(`bin:${e.op}:${e.left.type.kind}`, e.loc);
if (arith[e.op] !== undefined) B.line(`${t} = ${arith[e.op]} double ${l.name}, ${r.name}`);
if ((e.op === "+" || e.op === "-") && host.integerRanges.get(e)) {
const left = B.tmp();
const right = B.tmp();
const result = B.tmp();
// Every signed i54 value is exactly representable as a double.
// Exposing that width lets LLVM cancel later number/integer
// round trips. The proven safe result cannot overflow i54.
B.line(`${left} = fptosi double ${l.name} to i54`);
B.line(`${right} = fptosi double ${r.name} to i54`);
B.line(`${result} = ${e.op === "+" ? "add" : "sub"} nsw i54 ${left}, ${right}`);
B.line(`${t} = sitofp i54 ${result} to double`);
} else if (arith[e.op] !== undefined) B.line(`${t} = ${arith[e.op]} double ${l.name}, ${r.name}`);
else B.line(`${t} = fcmp ${cmp[e.op]} double ${l.name}, ${r.name}`);
} else if (bit[e.op] !== undefined) {
if (e.left.type.kind !== "f64" || e.right.type.kind !== "f64") {
throw new LlvmUnsupportedError(`bin:${e.op}:${e.left.type.kind}:${e.right.type.kind}`, e.loc);
}
const left = host.emitToUint32(l.name);
let right = host.emitToUint32(r.name);
const left = host.emitToUint32(l.name, e.left);
let right = host.emitToUint32(r.name, e.right);
if (e.op === "<<" || e.op === ">>" || e.op === ">>>") {
const shift = B.tmp();
B.line(`${shift} = and i32 ${right}, 31`);
@@ -128,7 +139,7 @@ export function emitOperatorExpr(host: LlvmEmitterContext, e: ExprOf<"bin" | "un
if (e.op === "-") B.line(`${t} = fneg double ${v.name}`);
else if (e.op === "!") B.line(`${t} = xor i1 ${v.name}, true`);
else {
const value = host.emitToUint32(v.name);
const value = host.emitToUint32(v.name, e.operand);
const result = B.tmp();
B.line(`${result} = xor i32 ${value}, -1`);
B.line(`${t} = sitofp i32 ${result} to double`);
@@ -0,0 +1,68 @@
import { expect, test } from "vitest";
import { analyzeIntegerRanges } from "./integer-ranges.js";
import { F64, type IrExpr, type IrFunction, type IrStmt } from "./ir.js";
const loc = { file: "test.ts", start: 0, end: 0 };
const num = (value: number): IrExpr => ({ loc, kind: "numLit", value, type: F64 });
const ref = (localId = "x"): IrExpr => ({ loc, kind: "varRef", localId, type: F64 });
const bin = (op: Extract<IrExpr, { kind: "bin" }>["op"], left: IrExpr, right: IrExpr): IrExpr => ({ loc, kind: "bin", op, left, right, type: F64 });
const assign = (value: IrExpr): IrStmt => ({ loc, kind: "assign", localId: "x", value });
const fn = (body: IrStmt[], boxed = false): IrFunction => ({ loc, name: "f", params: [], returnType: F64, locals: [{ id: "x", name: "x", type: F64, mutable: true, ...(boxed ? { boxed: true as const } : {}) }], body });
test("tracks exact additions and bitwise conversions through assignments", () => {
const sum = bin("+", ref(), bin("<<", ref(), num(3)));
const unsigned = bin(">>>", ref(), num(0));
const ranges = analyzeIntegerRanges(fn([assign(bin("|", num(0), num(0))), assign(sum), { loc, kind: "return", value: unsigned }]));
expect(ranges.get(sum)).toEqual({ min: -4294967296, max: 4294967294 });
expect(ranges.get(unsigned)).toEqual({ min: 0, max: 4294967295 });
});
test("rejects negative zero, fractions, nonfinite values and imprecise sums", () => {
for (const value of [-0, 0.5, NaN, Infinity, -Infinity, 9007199254740992]) {
const sum = bin("+", num(value), num(0));
expect(analyzeIntegerRanges(fn([{ loc, kind: "return", value: sum }])).get(sum)).toBeNull();
}
const exact = bin("-", num(Number.MAX_SAFE_INTEGER), num(1));
const rounded = bin("+", num(Number.MAX_SAFE_INTEGER), num(2));
const ranges = analyzeIntegerRanges(fn([assign(exact), { loc, kind: "return", value: rounded }]));
expect(ranges.get(exact)).toEqual({ min: 9007199254740990, max: 9007199254740990 });
expect(ranges.get(rounded)).toBeNull();
});
test("forgets facts across opaque expressions, control flow and boxed locals", () => {
const sum = (): IrExpr => bin("+", ref(), num(1));
const call: IrExpr = { loc, kind: "call", callee: "unknown", args: [], type: F64 };
const afterCall = sum();
const afterBlock = sum();
const boxed = sum();
const ranges = analyzeIntegerRanges(fn([
assign(num(1)), { loc, kind: "exprStmt", expr: call }, assign(afterCall),
assign(num(1)), { loc, kind: "block", body: [assign(num(Infinity))] }, { loc, kind: "return", value: afterBlock },
]));
expect(ranges.get(afterCall)).toBeNull();
expect(ranges.get(afterBlock)).toBeNull();
expect(analyzeIntegerRanges(fn([assign(num(1)), { loc, kind: "return", value: boxed }], true)).get(boxed)).toBeNull();
});
test("analyzes nested bodies independently and snapshots operands before writes", () => {
const before = ref();
const write: IrExpr = { loc, kind: "assignExpr", localId: "x", value: num(Infinity), type: F64 };
const after = ref();
const nested = bin("+", ref(), num(1));
const ranges = analyzeIntegerRanges(fn([
assign(num(1)), { loc, kind: "exprStmt", expr: bin("|", before, write) }, { loc, kind: "exprStmt", expr: after },
{ loc, kind: "block", body: [assign(num(2)), { loc, kind: "return", value: nested }] },
]));
expect(ranges.get(before)).toEqual({ min: 1, max: 1 });
expect(ranges.get(after)).toBeNull();
expect(ranges.get(nested)).toEqual({ min: 3, max: 3 });
});
test("shared expression objects never inherit a proof from another occurrence", () => {
const shared = ref();
const ranges = analyzeIntegerRanges(fn([
assign(num(1)), { loc, kind: "exprStmt", expr: shared }, assign(num(Infinity)),
{ loc, kind: "return", value: { loc, kind: "call", callee: "unknown", args: [shared], type: F64 } },
]));
expect(ranges.get(shared)).toBeNull();
});
+102
View File
@@ -0,0 +1,102 @@
import type { IrExpr, IrFunction, IrStmt } from "./ir.js";
/** Exactly representable integers, excluding negative zero. These facts
* justify signed i64 arithmetic as well as unchecked ToUint32 conversion. */
export interface IntegerRange { min: number; max: number }
export type IntegerRanges = ReadonlyMap<IrExpr, IntegerRange | null>;
const SIGNED: IntegerRange = { min: -2147483648, max: 2147483647 };
const UNSIGNED: IntegerRange = { min: 0, max: 4294967295 };
/** Deliberately local range analysis. Facts flow only through straight-line
* statements and numeric expression evaluation. Unknown expressions and
* control-flow boundaries discard them; nested bodies start independently.
* No assumptions about parameters, captures, globals or loop iterations. */
export function analyzeIntegerRanges(fn: IrFunction): IntegerRanges {
const ranges = new Map<IrExpr, IntegerRange | null>();
if (fn.async || fn.generator) return ranges;
const captures = new Set((fn.captures ?? []).map((c) => c.localId));
const eligible = new Set(fn.locals.filter((l) => l.type.kind === "f64" && !l.boxed && !l.tdz && !captures.has(l.id)).map((l) => l.id));
type Facts = Map<string, IntegerRange>;
function remember(e: IrExpr, range: IntegerRange | null): IntegerRange | null {
// IR normally is a tree, but shared expression objects must be safe at
// every occurrence. An unknown occurrence invalidates any earlier fact.
const previous = ranges.get(e);
ranges.set(e, previous === undefined ? range : previous && range ? {
min: Math.min(previous.min, range.min), max: Math.max(previous.max, range.max),
} : null);
return range;
}
function opaque(value: unknown): void {
if (Array.isArray(value)) { value.forEach(opaque); return; }
if (value === null || typeof value !== "object") return;
const node = value as Record<string, unknown>;
if (typeof node["kind"] === "string") ranges.set(value as IrExpr, null);
for (const [key, child] of Object.entries(node)) if (key !== "type" && key !== "loc") opaque(child);
}
function expr(e: IrExpr, facts: Facts): IntegerRange | null {
let range: IntegerRange | null = null;
switch (e.kind) {
case "numLit":
if (Number.isSafeInteger(e.value) && !Object.is(e.value, -0)) range = { min: e.value, max: e.value };
break;
case "varRef": range = facts.get(e.localId) ?? null; break;
case "bin": {
// Evaluate in source order: an opaque right operand may invalidate
// locals, but cannot change the already-snapshotted left value.
const left = expr(e.left, facts);
const right = expr(e.right, facts);
if (e.type.kind !== "f64") break;
if (e.op === ">>>") range = UNSIGNED;
else if (["&", "|", "^", "<<", ">>"].includes(e.op)) range = SIGNED;
else if (left && right && (e.op === "+" || e.op === "-")) {
const min = e.op === "+" ? left.min + right.min : left.min - right.max;
const max = e.op === "+" ? left.max + right.max : left.max - right.min;
if (Number.isSafeInteger(min) && Number.isSafeInteger(max)) range = { min, max };
}
break;
}
case "unary":
expr(e.operand, facts);
if (e.op === "~") range = SIGNED;
break;
default:
facts.clear();
opaque(e);
return null;
}
return remember(e, range);
}
function body(stmts: IrStmt[]): void {
const facts: Facts = new Map();
for (const s of stmts) {
switch (s.kind) {
case "varDecl": case "assign": {
const value = s.kind === "varDecl" ? s.init : s.value;
const range = value ? expr(value, facts) : null;
facts.delete(s.localId);
if (range && eligible.has(s.localId)) facts.set(s.localId, range);
break;
}
case "return":
if (s.value) expr(s.value, facts);
facts.clear();
break;
case "exprStmt": expr(s.expr, facts); break;
default:
facts.clear();
// Only statement-list children are analyzed. Headers, conditions,
// case selectors and every other expression remain conservative.
for (const [key, value] of Object.entries(s)) {
if (key === "loc") continue;
if (["body", "then", "else_", "tryBody", "catchBody", "finallyBody"].includes(key) && Array.isArray(value)) body(value as IrStmt[]);
else if (s.kind === "switch" && key === "cases") {
for (const c of s.cases) { opaque(c.test); body(c.body); }
} else opaque(value);
}
}
}
}
body(fn.body);
return ranges;
}
@@ -0,0 +1,42 @@
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 { emitCModule } from "../src/backend/c/c-emitter.js";
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
test("bounded integer arithmetic emits integer operations and leaves uncertain numbers alone", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-integer-bits-"));
try {
const entry = join(dir, "main.ts");
const outPath = join(dir, "main.ir.json");
await writeFile(entry, `
function safe(input: number): number {
let h = input >>> 0;
h = h ^ (h << 13);
h = h + (h << 3);
return h >>> 0;
}
function unknown(input: number): number { return (input + 1) >>> 0; }
function negativeZero(): number { const x = -0; return x + x; }
console.log(safe(17), unknown(1.5), 1 / negativeZero());
`);
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 c = emitCModule(mod);
const ll = emitLlvmModule(mod);
const cBody = (name: string): string => c.match(new RegExp(`static [^\\n]+ sc_f_${name}\\([^\\n]*\\) \\{([\\s\\S]*?)\\n\\}`))![1]!;
const llBody = (name: string): string => ll.match(new RegExp(`define internal [^\\n]+ @sc_f_${name}\\([^\\n]*\\) #0 \\{([\\s\\S]*?)\\n\\}`))![1]!;
expect(cBody("safe").match(/scr_bit_/g)).toHaveLength(1);
expect(cBody("safe")).toContain("(double)((int64_t)");
expect(llBody("safe")).toContain(" = add nsw i54 ");
// Only the arbitrary input's slow ToUint32 path still adds doubles.
expect(llBody("safe").match(/ = fadd double /g)).toHaveLength(1);
expect(cBody("unknown")).toContain("scr_bit_ushr(");
expect(llBody("unknown")).toContain("uint32.coerce.slow");
expect(llBody("unknown")).toContain(" = fadd double ");
expect(llBody("negativeZero")).toContain(" = fadd double ");
} finally { await rm(dir, { recursive: true, force: true }); }
});
@@ -6691,6 +6691,12 @@
],
"diags": []
},
"<repo>/tests/corpus/2973-integer-bitwise-arithmetic.ts": {
"order": [
"<repo>/tests/corpus/2973-integer-bitwise-arithmetic.ts"
],
"diags": []
},
"<repo>/tests/corpus/300-if-else.ts": {
"order": [
"<repo>/tests/corpus/300-if-else.ts"
@@ -0,0 +1,89 @@
function mix(input: number): number {
let h = input >>> 0;
h = h ^ (h << 13);
h = h ^ (h >>> 17);
h = h ^ (h << 5);
h = h + (h << 3);
h = h ^ (h >>> 11);
h = h + (h << 15);
return h >>> 0;
}
function bits(input: number, shift: number): void {
const a = input | 0;
const b = shift >>> 0;
const sum = a + (a << 3);
const difference = sum - (a >>> 1);
const and = a & b;
const or = a | b;
const xor = a ^ b;
const left = a << b;
const signed = a >> b;
const unsigned = a >>> b;
const inverted = ~a;
console.log(sum, difference, and, or, xor, left, signed, unsigned, inverted);
}
const inputs = [0, -0, 1, -1, 0.5, -1.5, 2147483647, -2147483648, 4294967295, 4294967296, 9007199254740991, 9007199254740992, 1e30, NaN, Infinity, -Infinity];
for (const input of inputs) {
console.log("mix", input, mix(input));
for (const shift of [0, 1, 31, 32, 33, -1, 63, 1.5, NaN, Infinity]) bits(input, shift);
}
function boundaries(): void {
const max = 9007199254740991;
const exact = max - 1;
const rounded = max + 2;
const n = -0;
const neg = n + n;
const sub = n - 0;
const zero = (0 | 0) - (0 | 0);
console.log("bounds", exact, rounded, rounded | 0, 1 / neg, 1 / sub, 1 / zero);
}
boundaries();
function stale(flag: boolean): void {
let x = 1 | 0;
if (flag) x = Infinity;
const afterBranch = x + 2;
x = 3 | 0;
const snapshot = x | (x = 4294967297.75);
const afterWrite = x + 2;
let count = 0;
while (count < 3) {
const previous = x + 1;
console.log("loop", previous, previous | 0);
x = count === 0 ? NaN : 2.5;
count++;
}
console.log("stale", afterBranch, snapshot, afterWrite);
}
stale(false);
stale(true);
function captured(): void {
let x = 1 | 0;
function replace(): number { x = Infinity; return 1; }
const snapshot = x | replace();
const next = x + 1;
console.log("captured", snapshot, next);
}
captured();
function exceptional(): void {
let x = 1 | 0;
try {
x = Infinity;
throw new Error("stop");
} catch {
console.log("catch", x + 1, x | 0);
} finally {
x = -0;
}
const sum = x + x;
console.log("finally", 1 / sum);
}
exceptional();
function ordered(value: number): number { console.log("operand", value); return value; }
console.log("order", (ordered(4294967295) >>> 0) + (ordered(-2147483648) | 0));