Skip to content
Open
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
35 changes: 25 additions & 10 deletions packages/compiler/src/backend/llvm/expr-primitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,27 @@ export function emitOperatorExpr(host: LlvmEmitterContext, e: ExprOf<"bin" | "un
const cmp: Record<string, string> = { "<": "olt", "<=": "ole", ">": "ogt", ">=": "oge", "===": "oeq", "!==": "une" };
const libm: Record<string, string> = { "%": "fmod", "**": "pow" };
const bit: Record<string, string> = {
"&": "scr_bit_and",
"|": "scr_bit_or",
"^": "scr_bit_xor",
"<<": "scr_bit_shl",
">>": "scr_bit_shr",
">>>": "scr_bit_ushr",
"&": "and",
"|": "or",
"^": "xor",
"<<": "shl",
">>": "ashr",
">>>": "lshr",
};
if ((e.op === "===" || e.op === "!==") && e.left.type.kind === "bool") {
if (bit[e.op] !== undefined) {
// Reuse the JS-exact ToUint32 conversion used by typed-array
// stores. Inlining the integer operations lets LLVM eliminate
// repeated coercions in nested expressions and integer hot loops.
const left = host.emitBytesU32(l.name);
const right = host.emitBytesU32(r.name);
const shift = e.op === "<<" || e.op === ">>" || e.op === ">>>";
const rhs = shift ? B.tmp() : right;
// LLVM shifts by >= 32 are poison; JS masks the count to 5 bits.
if (shift) B.line(`${rhs} = and i32 ${right}, 31`);
const integer = B.tmp();
B.line(`${integer} = ${bit[e.op]} i32 ${left}, ${rhs}`);
B.line(`${t} = ${e.op === ">>>" ? "uitofp" : "sitofp"} i32 ${integer} to double`);
} else if ((e.op === "===" || e.op === "!==") && e.left.type.kind === "bool") {
B.line(`${t} = icmp ${e.op === "===" ? "eq" : "ne"} i1 ${l.name}, ${r.name}`);
} else if ((e.op === "===" || e.op === "!==") && host.llType(e.left.type) === "ptr") {
// Reference identity (JS object equality) — closures, arrays,
Expand All @@ -96,7 +109,7 @@ export function emitOperatorExpr(host: LlvmEmitterContext, e: ExprOf<"bin" | "un
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 {
const fn = libm[e.op] ?? bit[e.op];
const fn = libm[e.op];
if (fn === undefined) throw new LlvmUnsupportedError(`bin:${e.op}`, e.loc);
host.declare(`declare double @${fn}(double, double)`);
B.line(`${t} = call double @${fn}(double ${l.name}, double ${r.name})`);
Expand All @@ -109,8 +122,10 @@ 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 {
host.declare(`declare double @scr_bit_not(double)`);
B.line(`${t} = call double @scr_bit_not(double ${v.name})`);
const operand = host.emitBytesU32(v.name);
const integer = B.tmp();
B.line(`${integer} = xor i32 ${operand}, -1`);
B.line(`${t} = sitofp i32 ${integer} to double`);
}
return { name: t, type: e.type };
}
Expand Down
3 changes: 2 additions & 1 deletion packages/compiler/src/ir/ir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4410,7 +4410,8 @@ export type IrLibFn =
* JS ToInt32/ToUint32 semantics — operands convert (NaN/±Infinity → 0,
* truncate, wrap mod 2^32), the operation runs in 32-bit space (shift
* counts mask to 5 bits), and the result returns to f64 (`>>>` as Uint32,
* the rest as Int32) — backends emit the scr_bit_* runtime helpers. */
* the rest as Int32). The C backend uses scr_bit_* helpers; LLVM emits
* JS-exact coercions followed by native i32 operations. */
export type IrNumBinOp =
| "+" | "-" | "*" | "/" | "%" | "**"
| "&" | "|" | "^" | "<<" | ">>" | ">>>"
Expand Down
62 changes: 62 additions & 0 deletions packages/compiler/test/bitwise-emission.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { expect, test } from "vitest";
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
import { F64, VOID, type IrExpr, type IrModule, type IrNumBinOp } from "../src/ir/ir.js";
import { validateModule } from "../src/ir/validate.js";

const loc = { file: "bitwise.ts", start: 0, end: 0 };
const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc });

function fixture(expr: IrExpr): IrModule {
return {
irVersion: 10,
sourceFile: loc.file,
entry: "__main",
functions: [
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
{
name: "bits",
params: [
{ localId: "a", name: "a", type: F64 },
{ localId: "b", name: "b", type: F64 },
],
returnType: F64,
locals: [
{ id: "a", name: "a", type: F64, mutable: false },
{ id: "b", name: "b", type: F64, mutable: false },
],
body: [{ kind: "return", value: expr, loc }],
loc,
},
],
};
}

test.each([
["&", "and"], ["|", "or"], ["^", "xor"],
["<<", "shl"], [">>", "ashr"], [">>>", "lshr"],
] as const)("emits integer %s without out-of-line bitwise helpers", (op, instruction) => {
const mod = fixture({ kind: "bin", op: op as IrNumBinOp, left: ref("a"), right: ref("b"), type: F64, loc });
validateModule(mod);
const llvm = emitLlvmModule(mod);
expect(llvm).not.toContain("@scr_bit_");
expect(llvm).toMatch(new RegExp(`= ${instruction} i32 `));
expect(llvm).toContain(op === ">>>" ? "uitofp i32" : "sitofp i32");
if (op === "<<" || op === ">>" || op === ">>>") {
expect(llvm).toMatch(/= and i32 %\w+, 31/);
}
// Conversion stays guarded and keeps the modular/nonfinite slow path;
// an unconditional fptosi would turn valid JS inputs into LLVM poison.
expect(llvm).toContain("fcmp oge double");
expect(llvm).toContain("fcmp ole double");
expect(llvm).toContain("frem double");
expect(llvm).toContain("bytes.coerce.nonfinite");
});

test("emits bitwise not as an integer xor with signed numeric result", () => {
const mod = fixture({ kind: "unary", op: "~", operand: ref("a"), type: F64, loc });
validateModule(mod);
const llvm = emitLlvmModule(mod);
expect(llvm).not.toContain("@scr_bit_");
expect(llvm).toMatch(/= xor i32 %\w+, -1/);
expect(llvm).toContain("sitofp i32");
});
39 changes: 39 additions & 0 deletions tests/corpus/141-bitwise-inline-coercion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Pin ToUint32 fast/slow boundaries and expression evaluation order.
// 140-bitwise-operators.ts covers every operator's JS edge semantics.
const values = [
-9007199254740994, -9007199254740992, -9007199254740991,
-4294967297, -4294967296, -2147483649, -2147483648.5,
-1.9, -Number.MIN_VALUE, -0, 0, Number.MIN_VALUE, 1.9,
2147483647.5, 2147483648, 4294967295.5, 4294967296, 4294967297,
9007199254740991, 9007199254740992, 9007199254740994,
Number.MAX_VALUE, -Number.MAX_VALUE, NaN, Infinity, -Infinity,
];
for (const a of values) {
for (const b of values) {
console.log(a & b, a | b, a ^ b, a << b, a >> b, a >>> b, ~a);
console.log(((a ^ b) << 5) >>> 0, (a >>> b) >> 1, ~~a);
}
}

let calls = 0;
function next(value: number): number {
calls++;
console.log("operand", calls);
return value;
}
console.log((next(-3.75) ^ next(2147483648)) >>> next(33), calls);
console.log(~next(-4294967297), calls);
let state = 3;
console.log(state++ ^ (state = 7), state);
console.log((state = -1) >>> state++, state);

function xorshift(seed: number, count: number): number {
let value = seed >>> 0;
for (let i = 0; i < count; i++) {
value = (value ^ (value << 13)) >>> 0;
value = (value ^ (value >>> 17)) >>> 0;
value = (value ^ (value << 5)) >>> 0;
}
return value;
}
for (const seed of values) console.log(xorshift(seed, 1000));