diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 3050601c2..eaeecd100 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -97,7 +97,7 @@ import { emitJsMarshal, emitJsOp, emitJsExit, islandAdapter, islandTypedAdapter import { dynKind, raceAdapterFor, genResultThunkFor, childExitThunkFor, childExitSignalThunkFor, childDataThunkFor, execFileThunkFor, emitterFixedAdapter, wrapEmitterListener, unwrapNullableClosure, closeBindThunkFor, closeOverrideWrapFor } from "./expr-callbacks.js"; import { streamDataAdapter, streamDoneFnFor, cryptoBytesThunkFor, fsRenameThunkFor, streamCbThunkFor, zlibBytesThunkFor } from "./expr-stream-callbacks.js"; import { resolveThunkFor, tagInSet, arrPush, emitArrayCopyLoop, emitStrIntrinsic, emitArrIntrinsic, wrapNullable, emitMapNew, mapSet, emitMapLikeIntrinsic, emitSetNew } from "./expr-containers.js"; -import { emitBytesReceiver, emitIntegerLoopIndex, emitBytesIndex, emitBytesData, emitBytesLength, emitBytesGet, emitBytesU32, emitBytesSet, emitBytesIntrinsic } from "./expr-bytes.js"; +import { emitBytesReceiver, emitIntegerLoopIndex, emitBytesIndex, emitBytesData, emitBytesLength, emitBytesGet, emitToUint32, emitBytesSet, emitBytesIntrinsic } from "./expr-bytes.js"; import { emitRegexIntrinsic, emitRecordKeyGet, keyedRecordReadInto } from "./expr-records.js"; import { dynPromiseAdapter, streamTypedRefCommitAdapter, liveDynUnionRefAdapter, streamTypedRefBoxValue, streamTypedRefMaterializeAdapter, streamFromArrayAdapter } from "./expr-stream-bridges.js"; import { emitWebLibCall, emitDynamicLibCall, emitFilesystemLibCall, emitPathUrlLibCall, emitPrimitiveLibCall } from "./lib-filesystem.js"; @@ -4313,8 +4313,8 @@ class LlEmitter { return emitBytesGet(this.expressionContext(), elem, receiver, index, integerIndex); } - private emitBytesU32(value: string): string { - return emitBytesU32(this.expressionContext(), value); + private emitToUint32(value: string): string { + return emitToUint32(this.expressionContext(), value); } private emitBytesSet(elem: IrBytesElem, receiver: string, index: string, value: string, integerIndex = false): void { diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index a6b7d88a7..249e2ad61 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -182,7 +182,7 @@ export function emitBytesGet(host: LlvmEmitterContext, elem: IrBytesElem, receiv return { name: out, type: F64 }; } -export function emitBytesU32(host: LlvmEmitterContext, value: string): string { +export function emitToUint32(host: LlvmEmitterContext, value: string): string { const B = host.B; const aboveMin = B.tmp(); const belowMax = B.tmp(); @@ -191,9 +191,9 @@ export function emitBytesU32(host: LlvmEmitterContext, value: string): string { B.line(`${belowMax} = fcmp ole double ${value}, ${f64Lit(9007199254740992)}`); B.line(`${fast} = and i1 ${aboveMin}, ${belowMax}`); - const fastLabel = B.newLabel("bytes.coerce.fast"); - const slowLabel = B.newLabel("bytes.coerce.slow"); - const done = B.newLabel("bytes.coerce.done"); + const fastLabel = B.newLabel("uint32.coerce.fast"); + const slowLabel = B.newLabel("uint32.coerce.slow"); + const done = B.newLabel("uint32.coerce.done"); B.condBr(fast, fastLabel, slowLabel); B.startBlock(fastLabel); @@ -214,9 +214,9 @@ export function emitBytesU32(host: LlvmEmitterContext, value: string): string { B.line(`${aboveNegInf} = fcmp ogt double ${value}, ${f64Lit(-Infinity)}`); B.line(`${finiteRange} = and i1 ${belowInf}, ${aboveNegInf}`); B.line(`${finite} = and i1 ${ordered}, ${finiteRange}`); - const finiteLabel = B.newLabel("bytes.coerce.finite"); - const nonfiniteLabel = B.newLabel("bytes.coerce.nonfinite"); - const slowDone = B.newLabel("bytes.coerce.slow.done"); + const finiteLabel = B.newLabel("uint32.coerce.finite"); + const nonfiniteLabel = B.newLabel("uint32.coerce.nonfinite"); + const slowDone = B.newLabel("uint32.coerce.slow.done"); B.condBr(finite, finiteLabel, nonfiniteLabel); B.startBlock(finiteLabel); @@ -252,7 +252,7 @@ export function emitBytesU32(host: LlvmEmitterContext, value: string): string { export function emitBytesSet(host: LlvmEmitterContext, elem: IrBytesElem, receiver: string, index: string, value: string, integerIndex = false): void { const B = host.B; const idx = host.emitBytesIndex(receiver, index, integerIndex); - const stored = elem === "f32" ? null : host.emitBytesU32(value); + const stored = elem === "f32" ? null : host.emitToUint32(value); const data = host.emitBytesData(receiver); const p = B.tmp(); if (elem === "u8") { diff --git a/packages/compiler/src/backend/llvm/expr-context.ts b/packages/compiler/src/backend/llvm/expr-context.ts index 9ecbaba54..33da4f761 100644 --- a/packages/compiler/src/backend/llvm/expr-context.ts +++ b/packages/compiler/src/backend/llvm/expr-context.ts @@ -70,7 +70,7 @@ export interface LlvmEmitterContext extends ShapeHost { emitBytesIntrinsic(e: IrExpr & { kind: "bytesIntrinsic" }): LlValue; emitBytesLength(elem: IrBytesElem, receiver: string, bytes: boolean): LlValue; emitBytesReceiver(receiver: IrExpr, following: IrExpr[]): LlValue; - emitBytesU32(value: string): string; + emitToUint32(value: string): 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; diff --git a/packages/compiler/src/backend/llvm/expr-primitives.test.ts b/packages/compiler/src/backend/llvm/expr-primitives.test.ts new file mode 100644 index 000000000..c990628a7 --- /dev/null +++ b/packages/compiler/src/backend/llvm/expr-primitives.test.ts @@ -0,0 +1,40 @@ +import { expect, test } from "vitest"; +import { F64, VOID, type IrExpr, type IrModule, type IrStmt } from "../../ir/ir.js"; +import { emitLlvmModule } from "./emitter.js"; + +const loc = { file: "bitwise-emission.ts", start: 0, end: 0 }; + +function fixture(): IrModule { + const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); + const body: IrStmt[] = (["&", "|", "^", "<<", ">>", ">>>"] as const).map((op) => ({ + kind: "exprStmt", + expr: { kind: "bin", op, left: num(5), right: num(3), type: F64, loc }, + loc, + })); + body.push({ + kind: "exprStmt", + expr: { kind: "unary", op: "~", operand: num(5), type: F64, loc }, + loc, + }); + return { + irVersion: 11, + sourceFile: loc.file, + entry: "__main", + functions: [{ name: "__main", params: [], returnType: VOID, locals: [], body, loc }], + }; +} + +test("LLVM emits bitwise number operators as native i32 instructions", () => { + const llvm = emitLlvmModule(fixture()); + expect(llvm).not.toContain(["@", "scr", "_bit_"].join("")); + expect(llvm).toMatch(/ = and i32 .*?, .*?$/m); + expect(llvm).toMatch(/ = or i32 .*?, .*?$/m); + expect(llvm).toMatch(/ = xor i32 .*?, .*?$/m); + expect(llvm).toMatch(/ = shl i32 .*?, .*?$/m); + expect(llvm).toMatch(/ = ashr i32 .*?, .*?$/m); + expect(llvm).toMatch(/ = lshr i32 .*?, .*?$/m); + expect(llvm).toMatch(/ = and i32 .*?, 31$/m); + expect(llvm).toMatch(/ = xor i32 .*?, -1$/m); + expect(llvm).toMatch(/ = uitofp i32 .*? to double$/m); + expect(llvm).toMatch(/ = sitofp i32 .*? to double$/m); +}); diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index a7e55e83c..e255ce6ff 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -81,13 +81,13 @@ export function emitOperatorExpr(host: LlvmEmitterContext, e: ExprOf<"bin" | "un const arith: Record = { "+": "fadd", "-": "fsub", "*": "fmul", "/": "fdiv" }; 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", + const bit: Record = { + "&": "and", + "|": "or", + "^": "xor", + "<<": "shl", + ">>": "ashr", + ">>>": "lshr", }; if ((e.op === "===" || e.op === "!==") && e.left.type.kind === "bool") { B.line(`${t} = icmp ${e.op === "===" ? "eq" : "ne"} i1 ${l.name}, ${r.name}`); @@ -99,8 +99,22 @@ export function emitOperatorExpr(host: LlvmEmitterContext, e: ExprOf<"bin" | "un 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}`); 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); + if (e.op === "<<" || e.op === ">>" || e.op === ">>>") { + const shift = B.tmp(); + B.line(`${shift} = and i32 ${right}, 31`); + right = shift; + } + const result = B.tmp(); + B.line(`${result} = ${bit[e.op]} i32 ${left}, ${right}`); + B.line(`${t} = ${e.op === ">>>" ? "uitofp" : "sitofp"} i32 ${result} to double`); } 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})`); @@ -113,8 +127,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 value = host.emitToUint32(v.name); + const result = B.tmp(); + B.line(`${result} = xor i32 ${value}, -1`); + B.line(`${t} = sitofp i32 ${result} to double`); } return { name: t, type: e.type }; } diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index fa4f80cdd..ad178232a 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -6432,6 +6432,12 @@ ], "diags": [] }, + "/tests/corpus/2937-bitwise-hot-loop.ts": { + "order": [ + "/tests/corpus/2937-bitwise-hot-loop.ts" + ], + "diags": [] + }, "/tests/corpus/300-if-else.ts": { "order": [ "/tests/corpus/300-if-else.ts" diff --git a/tests/corpus/2937-bitwise-hot-loop.ts b/tests/corpus/2937-bitwise-hot-loop.ts new file mode 100644 index 000000000..488e94d2c --- /dev/null +++ b/tests/corpus/2937-bitwise-hot-loop.ts @@ -0,0 +1,9 @@ +// Dependent bitwise chains keep JavaScript's 32-bit coercion and unsigned +// result semantics when every intermediate value feeds the next operation. +let value = 0x12345678; +for (let i = 0; i < 1_000_000; i++) { + value = (value ^ (value << 13)) >>> 0; + value = (value ^ (value >>> 17)) >>> 0; + value = (value ^ (value << 5)) >>> 0; +} +console.log(value);