From 8b4dc5981994e0ee66af76b994d2df804518dcea Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 20 Oct 2026 12:58:11 -0700 Subject: [PATCH] Convert booleans with ToNumber in bitwise operators &, |, ^, <<, >> and >>> accept boolean operands, converted as ToNumber does (true 1, false 0), as JavaScript dependencies write them (async-sema's `isFn(pauseFn) ^ isFn(resumeFn)`). Hoisted JavaScript vars that may also hold undefined or null convert those to 0, which is what ToInt32 makes of NaN and 0. Number operands lower exactly as before. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/expressions/binary.ts | 59 +++++++++++++++++-- .../6550-npm-boolean-bitwise/main.ts.json | 6 ++ tests/corpus/6550-npm-boolean-bitwise/main.ts | 15 +++++ .../node_modules/token-sema/index.d.ts | 6 ++ .../node_modules/token-sema/index.js | 30 ++++++++++ .../node_modules/token-sema/package.json | 1 + 6 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6550-npm-boolean-bitwise/main.ts.json create mode 100644 tests/corpus/6550-npm-boolean-bitwise/main.ts create mode 100644 tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.d.ts create mode 100644 tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.js create mode 100644 tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/package.json diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 8ef631f60..ead8f3598 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -1981,8 +1981,56 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr case ts.SyntaxKind.LessThanLessThanToken: case ts.SyntaxKind.GreaterThanGreaterThanToken: case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: { - if (!bothNum) + // Booleans take part as ToNumber converts them, true to 1 and false + // to 0: JavaScript code writes `isFn(a) ^ isFn(b)` and `flags | ok`. + // A hoisted JavaScript `var` can also hold undefined, and ToInt32 + // maps ToNumber(undefined) (NaN) and ToNumber(null) to 0. + const optionalBoolTag = (e: IrExpr): number => { + if (e.type.kind !== "union") return -1; + const arms = lowerer.unions.get(e.type.unionId)?.arms ?? []; + const ok = arms.every( + (a) => a.kind === "bool" || a.kind === "undefinedT" || a.kind === "nullT", + ); + return ok ? arms.findIndex((a) => a.kind === "bool") : -1; + }; + const numeric = (e: IrExpr): boolean => + e.type.kind === "f64" || e.type.kind === "bool" || optionalBoolTag(e) >= 0; + const boolBits = !bothNum && numeric(arithmeticLeft) && numeric(arithmeticRight); + if (!bothNum && !boolBits) operatorUnsupported(lowerer, expr, ts.tokenToString(op) ?? "?", [left.type, right.type]); + const bitPrefix: IrStmt[] = []; + const oneOrZero = (cond: IrExpr): IrExpr => ({ + kind: "ternary", + cond, + then: numLit(1, loc), + else_: numLit(0, loc), + type: F64, + loc, + }); + // The tag test reads an operand twice: when either side needs it, + // both sides evaluate once into temporaries, in source order. + const stash = optionalBoolTag(arithmeticLeft) >= 0 || optionalBoolTag(arithmeticRight) >= 0; + const stable = (e: IrExpr): IrExpr => { + if (!stash || isSafeToRepeat(e)) return e; + const tmp = lowerer.declareHiddenLocal("%bits", e.type); + bitPrefix.push({ kind: "varDecl", localId: tmp.id, init: e, loc }); + return varRef(tmp.id, e.type, loc); + }; + const bitOperand = (operand: IrExpr): IrExpr => { + const e = stable(operand); + if (e.type.kind === "bool") return oneOrZero(e); + const tag = optionalBoolTag(e); + if (tag < 0 || e.type.kind !== "union") return e; + const unionId = e.type.unionId; + return { + kind: "ternary", + cond: { kind: "unionIsTag", unionId, tag, negated: false, value: e, type: BOOL, loc }, + then: oneOrZero({ kind: "unionNarrow", unionId, tag, value: e, type: BOOL, loc }), + else_: numLit(0, loc), + type: F64, + loc, + }; + }; const bitOp = op === ts.SyntaxKind.AmpersandToken ? "&" @@ -1995,14 +2043,17 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr : op === ts.SyntaxKind.GreaterThanGreaterThanToken ? ">>" : ">>>"; - return { + const bits: IrExpr = { kind: "bin", op: bitOp, - left: arithmeticLeft, - right: arithmeticRight, + left: bitOperand(arithmeticLeft), + right: bitOperand(arithmeticRight), type: F64, loc, }; + return bitPrefix.length === 0 + ? bits + : { kind: "seqExpr", stmts: bitPrefix, result: bits, type: F64, loc }; } case ts.SyntaxKind.EqualsEqualsEqualsToken: case ts.SyntaxKind.ExclamationEqualsEqualsToken: diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6550-npm-boolean-bitwise/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6550-npm-boolean-bitwise/main.ts.json new file mode 100644 index 000000000..a34eeedc0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6550-npm-boolean-bitwise/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6550-npm-boolean-bitwise/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6550-npm-boolean-bitwise/main.ts b/tests/corpus/6550-npm-boolean-bitwise/main.ts new file mode 100644 index 000000000..a08502343 --- /dev/null +++ b/tests/corpus/6550-npm-boolean-bitwise/main.ts @@ -0,0 +1,15 @@ +// Bitwise operators over booleans in a JavaScript dependency convert them +// with ToNumber, true to 1 and false to 0, as Node does (async-sema's +// `isFn(pauseFn) ^ isFn(resumeFn)` option check). +import { Sema, bits } from 'token-sema'; + +const plain = new Sema(3); +const paired = new Sema(2, { pauseFn: () => {}, resumeFn: () => {} }); +console.log(plain.capacity, plain.pausable, paired.capacity, paired.pausable); +try { + new Sema(1, { pauseFn: () => {} }); +} catch (error) { + console.log((error as Error).message); +} +console.log(bits([1, 0, 3])); +console.log(bits([])); diff --git a/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.d.ts b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.d.ts new file mode 100644 index 000000000..de028a027 --- /dev/null +++ b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.d.ts @@ -0,0 +1,6 @@ +export declare class Sema { + constructor(capacity: number, options?: { pauseFn?: () => void; resumeFn?: () => void }); + readonly capacity: number; + readonly pausable: number; +} +export declare function bits(values: number[]): string; diff --git a/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.js b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.js new file mode 100644 index 000000000..fda2e0b8e --- /dev/null +++ b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.js @@ -0,0 +1,30 @@ +// The option check from async-sema (MIT): pauseFn and resumeFn must be given +// together, tested with `^` over two booleans. Bitwise operators convert +// booleans with ToNumber. +'use strict'; +function isFn(x) { + return typeof x == 'function'; +} +class Sema { + constructor(capacity, { pauseFn, resumeFn } = {}) { + if (isFn(pauseFn) ^ isFn(resumeFn)) throw new Error('pauseFn and resumeFn must be both set for pausing'); + this.capacity = capacity; + this.pausable = isFn(pauseFn) & isFn(resumeFn); + } +} +function bits(values) { + var yes = values.length > 0; + var no = values.length < 0; + var flags = 0; + var count = 0; + for (var v of values) { + flags = flags | (v > 0); + count = count + ((v > 1) & 1); + } + return [ + yes ^ no, yes ^ yes, yes & no, yes | no, no | no, yes & 3, 2 | yes, + yes << 3, yes >> 0, no >>> 0, -1 >>> yes, 5 ^ yes, no ^ 7, + typeof (yes ^ no), flags, count, + ].join(' '); +} +module.exports = { Sema: Sema, bits: bits }; diff --git a/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/package.json b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/package.json new file mode 100644 index 000000000..7d74609a7 --- /dev/null +++ b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/package.json @@ -0,0 +1 @@ +{ "name": "token-sema", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }