Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/compiler/src/backend/c/c-emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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>();
Expand Down
28 changes: 27 additions & 1 deletion packages/compiler/src/backend/c/exprs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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">,
Expand All @@ -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})`);
Expand Down Expand Up @@ -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": {
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/backend/c/stmts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";


Expand All @@ -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++;
Expand Down
7 changes: 5 additions & 2 deletions packages/compiler/src/backend/llvm/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 8 additions & 1 deletion packages/compiler/src/backend/llvm/expr-bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 3 additions & 1 deletion packages/compiler/src/backend/llvm/expr-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
19 changes: 15 additions & 4 deletions packages/compiler/src/backend/llvm/expr-primitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand All @@ -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`);
Expand Down
68 changes: 68 additions & 0 deletions packages/compiler/src/ir/integer-ranges.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
Loading
Loading