diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 66fc8fc65..f795fe237 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -78,14 +78,27 @@ export function emitOperatorExpr(host: LlvmEmitterContext, e: ExprOf<"bin" | "un const cmp: Record = { "<": "olt", "<=": "ole", ">": "ogt", ">=": "oge", "===": "oeq", "!==": "une" }; const libm: Record = { "%": "fmod", "**": "pow" }; const bit: Record = { - "&": "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, @@ -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})`); @@ -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 }; } diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 9affd84aa..8bc16f5a6 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -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 = | "+" | "-" | "*" | "/" | "%" | "**" | "&" | "|" | "^" | "<<" | ">>" | ">>>" diff --git a/packages/compiler/test/bitwise-emission.test.ts b/packages/compiler/test/bitwise-emission.test.ts new file mode 100644 index 000000000..e4fc165b1 --- /dev/null +++ b/packages/compiler/test/bitwise-emission.test.ts @@ -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"); +}); diff --git a/tests/corpus/141-bitwise-inline-coercion.ts b/tests/corpus/141-bitwise-inline-coercion.ts new file mode 100644 index 000000000..853bab7db --- /dev/null +++ b/tests/corpus/141-bitwise-inline-coercion.ts @@ -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));