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
6 changes: 3 additions & 3 deletions packages/compiler/src/backend/llvm/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 {
Expand Down
16 changes: 8 additions & 8 deletions packages/compiler/src/backend/llvm/expr-bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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") {
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/src/backend/llvm/expr-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
40 changes: 40 additions & 0 deletions packages/compiler/src/backend/llvm/expr-primitives.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
36 changes: 26 additions & 10 deletions packages/compiler/src/backend/llvm/expr-primitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,13 @@ export function emitOperatorExpr(host: LlvmEmitterContext, e: ExprOf<"bin" | "un
const arith: Record<string, string> = { "+": "fadd", "-": "fsub", "*": "fmul", "/": "fdiv" };
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",
const bit: Record<string, "and" | "or" | "xor" | "shl" | "ashr" | "lshr"> = {
"&": "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}`);
Expand All @@ -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})`);
Expand All @@ -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 };
}
Expand Down
6 changes: 6 additions & 0 deletions packages/compiler/test/ts7/baselines/order-parity.json
Original file line number Diff line number Diff line change
Expand Up @@ -6432,6 +6432,12 @@
],
"diags": []
},
"<repo>/tests/corpus/2937-bitwise-hot-loop.ts": {
"order": [
"<repo>/tests/corpus/2937-bitwise-hot-loop.ts"
],
"diags": []
},
"<repo>/tests/corpus/300-if-else.ts": {
"order": [
"<repo>/tests/corpus/300-if-else.ts"
Expand Down
9 changes: 9 additions & 0 deletions tests/corpus/2937-bitwise-hot-loop.ts
Original file line number Diff line number Diff line change
@@ -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);
Loading