mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
Optimize proven integer arithmetic
This commit is contained in:
@@ -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>();
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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));
|
||||
Reference in New Issue
Block a user