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 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-20 12:58:11 -07:00
co-authored by Claude Opus 5.5
parent 73a595692f
commit 8b4dc59819
6 changed files with 113 additions and 4 deletions
@@ -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:
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6550-npm-boolean-bitwise/main.ts"
],
"diags": []
}
@@ -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([]));
@@ -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;
+30
View File
@@ -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 };
@@ -0,0 +1 @@
{ "name": "token-sema", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }