Other ops (#68)

This commit is contained in:
Borys Levytskyi
2025-11-08 14:09:08 -05:00
committed by GitHub
parent c4877ac376
commit f0fefd6546
14 changed files with 511 additions and 74 deletions

30
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"@fortawesome/fontawesome-free": "^6.7.2",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@testing-library/jest-dom": "^6.4.5",
@@ -2442,6 +2443,18 @@
"node": ">=6"
}
},
"node_modules/@fortawesome/free-regular-svg-icons": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
"integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
@@ -4440,7 +4453,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/regexpp": "^4.4.0",
"@typescript-eslint/scope-manager": "5.62.0",
@@ -17968,20 +17980,6 @@
}
}
},
"node_modules/tailwindcss/node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
@@ -18345,6 +18343,7 @@
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"license": "(MIT OR CC0-1.0)",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -18869,6 +18868,7 @@
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz",
"integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/bonjour": "^3.5.9",
"@types/connect-history-api-fallback": "^1.3.5",

View File

@@ -6,6 +6,7 @@
"@fortawesome/fontawesome-free": "^6.7.2",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@testing-library/jest-dom": "^6.4.5",

View File

@@ -1,6 +1,6 @@
import calc from './calc';
import { Integer, asInteger } from './Integer';
import { INT32_MIN_VALUE, INT64_MAX_VALUE, UINT64_MAX_VALUE } from './const';
import { INT32_MIN_VALUE, INT32_MAX_VALUE, INT64_MAX_VALUE, INT64_MIN_VALUE, UINT64_MAX_VALUE } from './const';
describe('calc.flipBit', () => {
it('calculates flipped bit 32-bit number', () => {
@@ -86,7 +86,98 @@ describe('calc.xor', () => {
it('positive and negative nubmer', () => {
expect(calc.xor(Integer.int(-1), Integer.int(10)).num()).toBe(-11);
});
})
});
describe('calc.add', () => {
it('adds positives', () => {
expect(calc.add(Integer.int(2), Integer.int(3)).num()).toBe(5);
});
it('adds negative and positive', () => {
expect(calc.add(Integer.int(-2), Integer.int(3)).num()).toBe(1);
});
it('adds negatives', () => {
expect(calc.add(Integer.int(-2), Integer.int(-3)).num()).toBe(-5);
});
it('wraps on 32-bit overflow', () => {
expect(calc.add(Integer.int(INT32_MAX_VALUE), Integer.int(1)).num()).toBe(INT32_MIN_VALUE);
expect(calc.add(Integer.int(INT32_MAX_VALUE), Integer.int(2)).num()).toBe(-2147483647);
});
it('wraps on 64-bit overflow', () => {
const r = calc.add(new Integer(INT64_MAX_VALUE, 64), Integer.long(1));
expect(r.value.toString()).toBe(INT64_MIN_VALUE.toString());
});
it('promotes to larger operand size', () => {
const r = calc.add(Integer.int(-1), Integer.long(2));
expect(r.maxBitSize).toBe(64);
expect(r.num()).toBe(1);
});
});
describe('calc.sub', () => {
it('subtracts positives', () => {
expect(calc.sub(Integer.int(7), Integer.int(2)).num()).toBe(5);
});
it('subtracts negative and positive', () => {
expect(calc.sub(Integer.int(-2), Integer.int(3)).num()).toBe(-5);
});
it('subtracts negatives', () => {
expect(calc.sub(Integer.int(-2), Integer.int(-3)).num()).toBe(1);
});
it('wraps on 32-bit overflow', () => {
// 2147483647 - (-1) = 2147483648 -> -2147483648
expect(calc.sub(Integer.int(2147483647), Integer.int(-1)).num()).toBe(-2147483648);
// -2147483648 - 1 = -2147483649 -> 2147483647
expect(calc.sub(Integer.int(-2147483648), Integer.int(1)).num()).toBe(2147483647);
});
it('promotes to larger operand size', () => {
const r = calc.sub(Integer.int(-1), Integer.long(2));
expect(r.maxBitSize).toBe(64);
expect(r.num()).toBe(-3);
});
});
describe('calc.div', () => {
it('divides positives with truncation', () => {
expect(calc.div(Integer.int(7), Integer.int(2)).num()).toBe(3);
});
it('divides negative and positive with truncation', () => {
expect(calc.div(Integer.int(-7), Integer.int(2)).num()).toBe(-3);
expect(calc.div(Integer.int(7), Integer.int(-2)).num()).toBe(-3);
});
it('divides negatives', () => {
expect(calc.div(Integer.int(-8), Integer.int(-2)).num()).toBe(4);
});
it('wraps INT_MIN / -1 to INT_MIN for 32-bit', () => {
expect(calc.div(Integer.int(-2147483648), Integer.int(-1)).num()).toBe(-2147483648);
});
it('64-bit division', () => {
const r = calc.div(new Integer(INT64_MAX_VALUE, 64), Integer.long(2));
expect(r.value.toString()).toBe('4611686018427387903');
});
it('promotes to larger operand size', () => {
const r = calc.div(Integer.int(-8), Integer.long(2));
expect(r.maxBitSize).toBe(64);
expect(r.num()).toBe(-4);
});
it('division by zero throws', () => {
expect(() => calc.div(Integer.int(1), Integer.int(0))).toThrow();
});
});
describe('calc.lshift', () => {
@@ -200,6 +291,7 @@ describe("calc misc", () => {
})
});
describe("calc.engine.", () => {
it("not", () => {
expect(calc.engine.not("0101")).toBe("1010");
@@ -227,6 +319,82 @@ describe("calc.engine.", () => {
expect(calc.engine.xor("10101", "11011")).toBe("01110");
});
it("add", () => {
// 1-bit
expect(calc.engine.add("1", "1")).toBe("0"); // 1 + 1 = 2 -> 0 (wrap)
expect(calc.engine.add("1", "0")).toBe("1");
expect(calc.engine.add("0", "0")).toBe("0");
// 4-bit
expect(calc.engine.add("0001", "0001")).toBe("0010"); // 1+1=2
expect(calc.engine.add("0111", "0001")).toBe("1000"); // 7+1=8
expect(calc.engine.add("1111", "0001")).toBe("0000"); // 15+1=16 -> 0 (wrap)
expect(calc.engine.add("1111", "1111")).toBe("1110"); // 15+15=30 -> 14
});
it("mul", () => {
// 4-bit unsigned-style values
expect(calc.engine.mul("0001", "0011")).toBe("0011"); // 1*3=3
expect(calc.engine.mul("0010", "0011")).toBe("0110"); // 2*3=6
expect(calc.engine.mul("1111", "0010")).toBe("1110"); // 15*2=30 -> 14
// two's complement semantics for negatives
expect(calc.engine.mul("1111", "0011")).toBe("1101"); // (-1)*3 = -3
expect(calc.engine.mul("1111", "1111")).toBe("0001"); // (-1)*(-1) = 1
expect(calc.engine.mul("1000", "0010")).toBe("0000"); // (-8)*2 = -16 -> 0
// 8-bit example
expect(calc.engine.mul("11111111", "00000010")).toBe("11111110"); // (-1)*2 = -2
});
it("sub", () => {
// 4-bit examples
expect(calc.engine.sub("0011", "0001")).toBe("0010"); // 3-1=2
expect(calc.engine.sub("0000", "0001")).toBe("1111"); // 0-1 -> -1
expect(calc.engine.sub("1000", "0001")).toBe("0111"); // -8-1 -> 7 (wrap)
});
it("div", () => {
// 4-bit unsigned-style values
expect(calc.engine.div("0110", "0011")).toBe("0010"); // 6/3=2
expect(calc.engine.div("0111", "0010")).toBe("0011"); // 7/2=3
// two's complement negatives
expect(calc.engine.div("1111", "0010")).toBe("0000"); // (-1)/2 = 0 (trunc toward zero)
expect(calc.engine.div("1000", "0010")).toBe("1100"); // (-8)/2 = -4 -> 1100
// division by zero
expect(() => calc.engine.div("0001", "0000")).toThrow();
});
it("add", () => {
// 1-bit
expect(calc.engine.add("1", "1")).toBe("0"); // 1 + 1 = 2 -> 0 (wrap)
expect(calc.engine.add("1", "0")).toBe("1");
expect(calc.engine.add("0", "0")).toBe("0");
// 4-bit
expect(calc.engine.add("0001", "0001")).toBe("0010"); // 1+1=2
expect(calc.engine.add("0111", "0001")).toBe("1000"); // 7+1=8
expect(calc.engine.add("1111", "0001")).toBe("0000"); // 15+1=16 -> 0 (wrap)
expect(calc.engine.add("1111", "1111")).toBe("1110"); // 15+15=30 -> 14
});
it("mul", () => {
// 4-bit unsigned-style values
expect(calc.engine.mul("0001", "0011")).toBe("0011"); // 1*3=3
expect(calc.engine.mul("0010", "0011")).toBe("0110"); // 2*3=6
expect(calc.engine.mul("1111", "0010")).toBe("1110"); // 15*2=30 -> 14
// two's complement semantics for negatives
expect(calc.engine.mul("1111", "0011")).toBe("1101"); // (-1)*3 = -3
expect(calc.engine.mul("1111", "1111")).toBe("0001"); // (-1)*(-1) = 1
expect(calc.engine.mul("1000", "0010")).toBe("0000"); // (-8)*2 = -16 -> 0
// 8-bit example
expect(calc.engine.mul("11111111", "00000010")).toBe("11111110"); // (-1)*2 = -2
});
it("lshift", () => {
expect(calc.engine.lshift("1", 1)).toBe("0");
expect(calc.engine.lshift("01", 1)).toBe("10");
@@ -263,4 +431,4 @@ describe("calc.engine.", () => {
expect(calc.engine.applyTwosComplement("10101100")).toBe("01010100");
expect(calc.engine.applyTwosComplement("01010100")).toBe("10101100"); // reverse
});
});
});

View File

@@ -10,7 +10,7 @@ const calc = {
},
flipBit: function(num: Integer | JsNumber, bitIndex: number): Integer {
return this._applySingle(asInteger(num), (bin) => this.engine.flipBit(bin, bitIndex));
return this._executeForSingleOperand(asInteger(num), (bin) => this.engine.flipBit(bin, bitIndex));
},
promoteTo64Bit(number: Integer) : Integer {
@@ -33,9 +33,13 @@ const calc = {
case ">>": return this.rshift(op1, op2.value);
case ">>>": return this.urshift(op1, op2.value);
case "<<": return this.lshift(op1, op2.value);
case "&": return this.and(op1,op2);
case "|": return this.or(op1,op2);
case "^": return this.xor(op1,op2);
case "&": return this.and(op1, op2);
case "|": return this.or(op1, op2);
case "^": return this.xor(op1, op2);
case "+": return this.add(op1, op2);
case "-": return this.sub(op1, op2);
case "*": return this.mul(op1, op2);
case "/": return this.div(op1, op2);
default: throw new Error(operator + " operator is not supported");
}
},
@@ -63,7 +67,7 @@ const calc = {
while(bytes > num.maxBitSize) bytes -= num.maxBitSize;
return this._applySingle(num, bin => this.engine.lshift(bin, bytes));
return this._executeForSingleOperand(num, bin => this.engine.lshift(bin, bytes));
},
rshift (num : Integer, numBytes : JsNumber) : Integer {
@@ -75,7 +79,7 @@ const calc = {
while(bytes > num.maxBitSize) bytes -= num.maxBitSize;
return this._applySingle(num, bin => this.engine.rshift(bin, bytes));
return this._executeForSingleOperand(num, bin => this.engine.rshift(bin, bytes));
},
urshift (num : Integer, numBytes : JsNumber) : Integer {
@@ -87,26 +91,42 @@ const calc = {
while(bytes > num.maxBitSize) bytes -= num.maxBitSize;
return this._applySingle(num, bin => this.engine.urshift(bin, bytes));
return this._executeForSingleOperand(num, bin => this.engine.urshift(bin, bytes));
},
not(num:Integer) : Integer {
return this._applySingle(num, this.engine.not);
return this._executeForSingleOperand(num, this.engine.not);
},
and (num1 : Integer, num2 : Integer) : Integer {
return this._applyTwo(num1, num2, this.engine.and);
return this._executeForTwoOperands(num1, num2, this.engine.and);
},
or (num1 : Integer, num2 : Integer) : Integer {
return this._applyTwo(num1, num2, this.engine.or);
return this._executeForTwoOperands(num1, num2, this.engine.or);
},
xor (num1 : Integer, num2 : Integer) : Integer {
return this._applyTwo(num1, num2, this.engine.xor);
return this._executeForTwoOperands(num1, num2, this.engine.xor);
},
mul (num1: Integer, num2: Integer) : Integer {
return this._executeForTwoOperands(num1, num2, this.engine.mul);
},
sub (num1: Integer, num2: Integer) : Integer {
return this._executeForTwoOperands(num1, num2, this.engine.sub);
},
div (num1: Integer, num2: Integer) : Integer {
return this._executeForTwoOperands(num1, num2, this.engine.div);
},
_applySingle(num: Integer, operation: (bin:string) => string) : Integer {
add(num1: Integer, num2: Integer) : Integer {
return this._executeForTwoOperands(num1, num2, this.engine.add);
},
_executeForSingleOperand(num: Integer, operation: (bin:string) => string) : Integer {
let bin = this.toBinaryString(num).padStart(num.maxBitSize, num.value < 0 ? '1' : '0');
@@ -123,7 +143,7 @@ const calc = {
return new Integer(typeof num.value === "bigint" ? result : asIntN(result), num.maxBitSize, num.signed);
},
_applyTwo(op1: Integer, op2: Integer, operation: (bin1:string, bin2:string) => string) : Integer {
_executeForTwoOperands(op1: Integer, op2: Integer, operation: (bin1:string, bin2:string) => string) : Integer {
if(op1.maxBitSize === op2.maxBitSize && op1.signed !== op2.signed)
throw new Error("This operation cannot be applied to signed and unsigned operands of the same size");
@@ -209,6 +229,72 @@ const calc = {
return result.join('');
},
add (bin1: string, bin2:string) : string {
checkSameLength(bin1, bin2);
const len = bin1.length;
let carry = 0;
const out: string[] = new Array(len);
for (let i = len - 1; i >= 0; i--) {
const b1 = bin1[i] === '1' ? 1 : 0;
const b2 = bin2[i] === '1' ? 1 : 0;
const sum = b1 + b2 + carry;
out[i] = (sum % 2) === 1 ? '1' : '0';
carry = sum >= 2 ? 1 : 0;
}
// Overflow carry is discarded to keep fixed width
return out.join('');
},
mul (bin1: string, bin2: string) : string {
checkSameLength(bin1, bin2);
const len = bin1.length;
const toSignedBigInt = (bin: string) => {
if (bin[0] === '1') {
const mag = BigInt('0b' + calc.engine.applyTwosComplement(bin));
return -mag;
}
return BigInt('0b' + bin);
};
const a = toSignedBigInt(bin1);
const b = toSignedBigInt(bin2);
const product = a * b;
const modulo = (BigInt(1) << BigInt(len));
const wrapped = ((product % modulo) + modulo) % modulo;
return wrapped.toString(2).padStart(len, '0');
},
sub (bin1: string, bin2: string) : string {
checkSameLength(bin1, bin2);
// Two's complement subtraction: a - b == a + (-b)
const negB = calc.engine.applyTwosComplement(bin2);
return calc.engine.add(bin1, negB);
},
div (bin1: string, bin2: string) : string {
checkSameLength(bin1, bin2);
const len = bin1.length;
const toSignedBigInt = (bin: string) => {
if (bin[0] === '1') {
const mag = BigInt('0b' + calc.engine.applyTwosComplement(bin));
return -mag;
}
return BigInt('0b' + bin);
};
const a = toSignedBigInt(bin1);
const b = toSignedBigInt(bin2);
if (b === BigInt(0))
throw new Error('Division by zero');
const quotient = a / b; // BigInt division truncates toward zero
const modulo = (BigInt(1) << BigInt(len));
const wrapped = (((quotient % modulo) + modulo) % modulo);
return wrapped.toString(2).padStart(len, '0');
},
flipBit(bin: string, bitIndex : number) : string {
return bin.substring(0, bitIndex) + flip(bin[bitIndex]) + bin.substring(bitIndex+1)
},
@@ -252,12 +338,13 @@ function nextPowOfTwo(num: number) : number {
return p;
}
// Promotes both numbers to the same size using the size of the bigger one
function equalizeSize(n1: Integer, n2: Integer) : [Integer, Integer] {
if(n1.maxBitSize === n2.maxBitSize)
{
if(n1.signed === n2.signed) return [n1,n2];
// Example int and usinged int. Poromoted both yo 64 bit
// Example int and usinged int. Poromoted both to 64 bit
return [n1.resize(n1.maxBitSize*2).toSigned(), n2.resize(n2.maxBitSize*2).toSigned()];
}
@@ -274,4 +361,4 @@ function equalizeSize(n1: Integer, n2: Integer) : [Integer, Integer] {
Console.WriteLine(Convert.ToString(r,2).PadLeft(32, '0'));
Console.WriteLine(Convert.ToString(r));
Console.WriteLine(r.GetType().Name);
*/
*/

View File

@@ -7,7 +7,8 @@ import { Operator, Operand, ListOfNumbers } from '../expression';
import calc from '../../core/calc';
import { Integer } from '../../core/Integer';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faInfoCircle, faUndo } from '@fortawesome/free-solid-svg-icons';
import { faArrowRotateLeft as iconUndo } from '@fortawesome/free-solid-svg-icons';
import { faCircleQuestion as iconTip } from '@fortawesome/free-regular-svg-icons';
import loglevel from 'loglevel';
import IconWithToolTip from '../../shell/components/IconWithTooltip';
@@ -150,7 +151,7 @@ class ExpressionElementTableRow extends React.Component<ExpressionElementRowProp
const buttons = [];
if (this.scalar.value.value < 0)
buttons.push(<IconWithToolTip icon={faInfoCircle}>
buttons.push(<IconWithToolTip icon={iconTip}>
<div className='accent1 tooltip-header'>Two's Complement</div>
<p>
This is a negative number. It's binary representation is <u>inverted</u> using <strong>Two's Complement</strong> operation.
@@ -159,7 +160,7 @@ class ExpressionElementTableRow extends React.Component<ExpressionElementRowProp
</IconWithToolTip>)
if (!this.originalValue.isTheSame(this.scalar.value))
buttons.push(<button title='Undo all changes' className='undo' data-control="undo" onClick={() => this.undo()}><FontAwesomeIcon icon={faUndo} /></button>);
buttons.push(<button title='Undo all changes' className='undo' data-control="undo" onClick={() => this.undo()}><FontAwesomeIcon icon={iconUndo} /></button>);
return <React.Fragment>{buttons}</React.Fragment>
}
@@ -263,4 +264,4 @@ class ExpressionElementTableRow extends React.Component<ExpressionElementRowProp
return <React.Fragment>{children}</React.Fragment>
}
}
}

View File

@@ -18,6 +18,9 @@ describe("expression parser", () => {
expect(parser.parse("~1")).toBeInstanceOf(BitwiseOperation);
expect(parser.parse("1^2")).toBeInstanceOf(BitwiseOperation);
expect(parser.parse("1|2")).toBeInstanceOf(BitwiseOperation);
expect(parser.parse("1*2")).toBeInstanceOf(BitwiseOperation);
expect(parser.parse("1-2")).toBeInstanceOf(BitwiseOperation);
expect(parser.parse("1/2")).toBeInstanceOf(BitwiseOperation);
});
it("parses big binary bitwise expression", () => {
@@ -30,6 +33,46 @@ describe("expression parser", () => {
expect(expr.children[1].getUnderlyingOperand().value.toString()).toBe('2863311360');
})
it("parses addition operation", () => {
const expr = parser.parse("23 + 34") as BitwiseOperation;
expect(expr.children.length).toBe(2);
expect(expr.children[1]).toBeInstanceOf(Operator);
expect((expr.children[1] as Operator).operator).toBe("+");
expect(expr.children[0].getUnderlyingOperand().value.toString()).toBe('23');
expect(expr.children[1].getUnderlyingOperand().value.toString()).toBe('34')
});
it("parses multiplication operation", () => {
const expr = parser.parse("23 * 34") as BitwiseOperation;
expect(expr.children.length).toBe(2);
expect(expr.children[1]).toBeInstanceOf(Operator);
expect((expr.children[1] as Operator).operator).toBe("*");
expect(expr.children[0].getUnderlyingOperand().value.toString()).toBe('23');
expect(expr.children[1].getUnderlyingOperand().value.toString()).toBe('34')
});
it("parses division operation", () => {
const expr = parser.parse("23 / 34") as BitwiseOperation;
expect(expr.children.length).toBe(2);
expect(expr.children[1]).toBeInstanceOf(Operator);
expect((expr.children[1] as Operator).operator).toBe("/");
expect(expr.children[0].getUnderlyingOperand().value.toString()).toBe('23');
expect(expr.children[1].getUnderlyingOperand().value.toString()).toBe('34')
});
it("parses subtraction operation", () => {
const expr = parser.parse("23 - 34") as BitwiseOperation;
expect(expr.children.length).toBe(2);
expect(expr.children[1]).toBeInstanceOf(Operator);
expect((expr.children[1] as Operator).operator).toBe("-");
expect(expr.children[0].getUnderlyingOperand().value.toString()).toBe('23');
expect(expr.children[1].getUnderlyingOperand().value.toString()).toBe('34')
});
it("pares multiple operand expression", () => {
const result = parser.parse("1^2") as BitwiseOperation;
expect(result.children.length).toBe(2);
@@ -61,6 +104,60 @@ describe("expression parser", () => {
})
});
describe("multiplication", () => {
it("evaluates simple products", () => {
const expr = parser.parse("2*3") as BitwiseOperation;
const res = (expr.children[1] as Operator).evaluate(expr.children[0] as Operand);
expect(res.value.toString()).toBe('6');
const expr2 = parser.parse("-2*3") as BitwiseOperation;
const res2 = (expr2.children[1] as Operator).evaluate(expr2.children[0] as Operand);
expect(res2.value.toString()).toBe('-6');
});
it("wraps on 32-bit overflow", () => {
const expr = parser.parse("2147483647*2") as BitwiseOperation;
const res = (expr.children[1] as Operator).evaluate(expr.children[0] as Operand);
expect(res.value.toString()).toBe('-2');
});
it("wraps on 64-bit overflow", () => {
const expr = parser.parse("9223372036854775807l*2l") as BitwiseOperation;
const res = (expr.children[1] as Operator).evaluate(expr.children[0] as Operand);
expect(res.value.toString()).toBe('-2');
});
});
describe("division", () => {
it("evaluates simple quotients with truncation", () => {
const expr1 = parser.parse("7/2") as BitwiseOperation; // -> 3
const res1 = (expr1.children[1] as Operator).evaluate(expr1.children[0] as Operand);
expect(res1.value.toString()).toBe('3');
const expr2 = parser.parse("-7/2") as BitwiseOperation; // -> -3
const res2 = (expr2.children[1] as Operator).evaluate(expr2.children[0] as Operand);
expect(res2.value.toString()).toBe('-3');
const expr3 = parser.parse("1/2") as BitwiseOperation; // -> 0
const res3 = (expr3.children[1] as Operator).evaluate(expr3.children[0] as Operand);
expect(res3.value.toString()).toBe('0');
});
it("handles INT_MIN/-1 based on parsed width", () => {
// Parser promotes -2147483648 to 64-bit (abs value exceeds INT32_MAX),
// so the result is a positive 64-bit 2147483648.
const expr = parser.parse("-2147483648/-1") as BitwiseOperation;
const res = (expr.children[1] as Operator).evaluate(expr.children[0] as Operand);
expect(res.value.toString()).toBe('2147483648');
});
it("64-bit division", () => {
const expr = parser.parse("9223372036854775807l/2l") as BitwiseOperation;
const res = (expr.children[1] as Operator).evaluate(expr.children[0] as Operand);
expect(res.value.toString()).toBe('4611686018427387903');
});
});
describe("comparison with nodejs engine", () => {
it('set 32-bit', () => {
@@ -81,7 +178,7 @@ describe("comparison with nodejs engine", () => {
it('random: two inbary strings 64-bit', () => {
const signs = ["|", "&", "^", "<<", ">>", ">>>"]
const signs = ["|", "&", "^", "<<", ">>", ">>>", "*", "/", "-"]
for(var i =0; i<1000; i++){
@@ -92,7 +189,34 @@ describe("comparison with nodejs engine", () => {
const input = op1.toString() + sign + op2.toString();
testBinary(input, input);
if (sign === "*") {
// Use BigInt to avoid precision issues and wrap to 32-bit two's complement
const modulo = (BigInt(1) << BigInt(32));
const expectedNum = ((BigInt(op1) * BigInt(op2)) % modulo + modulo) % modulo;
const expectedSigned = expectedNum >= (BigInt(1) << BigInt(31)) ? expectedNum - modulo : expectedNum;
const expr = parser.parse(input) as BitwiseOperation;
const res = (expr.children[1] as Operator).evaluate(expr.children[0] as Operand).value.toString();
expect(res).toBe(expectedSigned.toString());
} else if (sign === "/") {
const denom = op2 === 0 ? 1 : op2;
const q = BigInt(op1) / BigInt(denom); // trunc toward zero
const modulo = (BigInt(1) << BigInt(32));
const expectedNum = ((q % modulo) + modulo) % modulo;
const expectedSigned = expectedNum >= (BigInt(1) << BigInt(31)) ? expectedNum - modulo : expectedNum;
const expr = parser.parse(op1.toString() + '/' + denom.toString()) as BitwiseOperation;
const res = (expr.children[1] as Operator).evaluate(expr.children[0] as Operand).value.toString();
expect(res).toBe(expectedSigned.toString());
} else if (sign === "-") {
const diff = BigInt(op1) - BigInt(op2);
const modulo = (BigInt(1) << BigInt(32));
const expectedNum = ((diff % modulo) + modulo) % modulo;
const expectedSigned = expectedNum >= (BigInt(1) << BigInt(31)) ? expectedNum - modulo : expectedNum;
const expr = parser.parse(input) as BitwiseOperation;
const res = (expr.children[1] as Operator).evaluate(expr.children[0] as Operand).value.toString();
expect(res).toBe(expectedSigned.toString());
} else {
testBinary(input, input);
}
}
});
@@ -177,4 +301,4 @@ describe("comparison with nodejs engine", () => {
expect(actual).toBe(expected);
}
});
});

View File

@@ -81,8 +81,8 @@ class BitwiseOperationExpressionFactory implements IExpressionParserFactory {
regex: RegExp;
constructor() {
this.fullRegex = /^[-,~,<,>,&,^|,b,x,l,s,u,a-f,0-9,\s]+$/i;
this.regex = /(<<|>>|>>>|\||&|\^)?(~?-?(?:[b,x,l,s,u,,a-f,0-9]+))/gi;
this.fullRegex = /^[-,~,<,>,&,^|,*,/,-,b,x,l,s,u,a-f,0-9,+\s]+$/i;
this.regex = /(<<|>>|>>>|\||&|\^|\+|\*|\/|(?<!^)-)?(~?-?(?:[b,x,l,s,u,,a-f,0-9]+))/gi;
}
canCreate (input: string) : boolean {

View File

@@ -32,6 +32,10 @@ html { height: 100% }
/* Theme-scoped code styling to avoid cross-theme overrides */
.light code, .dark code, .midnight code, .bladerunner code { font-size: 1.2em; font-weight: bold; }
h1 { font-family: Impact, Verdana;
text-transform: uppercase;
font-weight: lighter;
}
.icon { margin-right: 5px; vertical-align: middle; }
.header-cmd { color: #c5c5c5 }
@@ -217,13 +221,14 @@ button.link-button {text-decoration: underline;}
.bladerunner .solid-background { background: #0b0f14; }
.bladerunner .header-cmd { color: #ff7fb0a8 !important; }
.bladerunner .lights-on .header-cmd { color: #ff7fb0 !important; }
/* Rule for element when it has both .header and .on */
.bladerunner .header.lights-on { text-shadow: 0 0 4px rgba(255, 127, 176, 0.35), 0 0 9px rgba(255, 127, 176, 0.22); }
.bladerunner .header h1 { text-transform: uppercase; color: #66d9e8a8; font-family: Impact, Verdana, sans-serif; cursor: pointer; position: relative; z-index: 1; letter-spacing: 0.02em; }
.bladerunner .header h1.lights-on { color: #66d9e8; text-shadow: 0 0 4px rgba(102, 217, 232, 0.35), 0 0 9px rgba(102, 217, 232, 0.22); }
.bladerunner .header h1 { text-transform: uppercase; color: #66d9e8a8; cursor: pointer; position: relative; z-index: 1; font-weight: bold; user-select: none;}
.bladerunner .header h1.lights-on { color: #66d9e8; text-shadow: 0 0 4px rgba(102, 217, 232, 0.35), 0 0 9px rgba(102, 217, 232, 0.22); }
.bladerunner .header h1.lights-on::after {
/* Neon-style halo: layered cyan + magenta radial glows */
content: "";
animation: flicker 1s forwards;
position: absolute;
left: 0;
top: 50%;
@@ -243,6 +248,21 @@ button.link-button {text-decoration: underline;}
rgba(0, 0, 0, 0) 72%);
filter: blur(35px);
}
@keyframes flicker {
20%, 24%, 55% {
opacity: 0;
}
40% {
opacity: 1;
}
80% {
opacity: 1;
}
100% {
opacity: 1;
}
}
.bladerunner {color: #e6f0ff;}
.bladerunner .expression { color: white; }
.bladerunner .expressionInput { color: white; }
@@ -260,17 +280,16 @@ button.link-button {text-decoration: underline;}
.bladerunner button.btn { color: #00eaff }
.bladerunner button.btn:hover { background: #11222c }
.bladerunner button.btn:disabled { color: #0e6e7e; background-color: inherit; }
.bladerunner .accent1 { color: mediumseagreen }
.bladerunner .accent1-border { border-color: mediumseagreen }
.bladerunner .accent1 { color: #e3a600 }
.bladerunner .accent1 button:hover { text-shadow: 0 0 2px #e3a600 }
.bladerunner .accent1-border { border-color: #e3a600 }
.bladerunner .button { border-color: #00eaff }
.bladerunner code { color: #e3a600 }
.bladerunner code a, .bladerunner code a:visited { color: #e3a600 }
.bladerunner .command-link { color: #5fc2e9 }
.bladerunner .command-link:hover,
.bladerunner .command-link:focus {
/* very dim magenta glow */
text-shadow: 0 0 2px rgba(255, 127, 176, 0.5);
}
.bladerunner .command-link:focus { text-shadow: 0 0 2px rgba(69, 243, 255, 0.949); }
.bladerunner .soft { color: #aebed0 }
.bladerunner .solid-border { background-color: #0e161d; border-color: #1f3b4a;}
.bladerunner .expressionInput {border-color: #1f3b4a;}
@@ -279,18 +298,20 @@ button.link-button {text-decoration: underline;}
.bladerunner .expression {color: #e6f0ff;}
.bladerunner .settings-button button {color: #b9cfe2;}
.bladerunner button.hashLink, .bladerunner a.hashLink, .bladerunner a.hashLink:visited { color: #52687b; }
.bladerunner button.hashLink:hover, .bladerunner a.hashLink:hover {
.bladerunner button.hashLink:hover, .bladerunner .undo button:hover, .bladerunner a.hashLink:hover {
/* subtle glow derived from current hover color */
--text-shadow: 0 0 2px currentColor;
text-shadow:0 0 2px rgb(229, 33, 0.5);
color: #5fc2e9;
}
.bladerunner .undo button:hover { opacity: 1; }
.bladerunner .cur { color: #6c8497; }
/* Blade Runner: subtle glow for SVG icons inside hash links on hover/focus */
.bladerunner .hashLink:hover .icon,
.bladerunner .hashLink:focus .icon,
.bladerunner .hashLink:hover svg,
.bladerunner .undo:hover svg,
.bladerunner .hashLink:focus svg {
filter: drop-shadow(0 0 2px currentColor);
}
@@ -304,9 +325,31 @@ button.link-button {text-decoration: underline;}
.bladerunner .top-links button:focus {
/* very dim magenta glow */
text-shadow:0 0 2px rgb(229, 33, 0.5);
color: #FFBF00;
color: hsl(45, 100%, 50%);
}
/*
.bladerunner .top-links li {position: relative;}
.bladerunner .top-links button:hover::after, .bladerunner .top-links a:hover::after {
content: "";
position: absolute;
right: 50%;
top: 50%;
transform: translate(50%, -50%);
white-space: nowrap;
width: 200%;
height: 120%;
pointer-events: none;
z-index: 0;
background:
radial-gradient(ellipse at center,
rgba(227, 166, 0, 0.85) 0%,
rgba(227, 166, 0, 0.55) 22%,
rgba(0, 0, 0, 0) 60%);
filter: blur(20px);
}*/
/* Blade Runner: add matching glow to SVG icons inside top-links on hover/focus */
.bladerunner .top-links a:hover .icon,
.bladerunner .top-links a:focus .icon,

View File

@@ -1,6 +1,6 @@
import log from 'loglevel';
export const APP_VERSION = 9;
export const APP_VERSION = 10;
export type PersistedAppData = {
emphasizeBytes: boolean;

View File

@@ -19,29 +19,18 @@ const toggleLights = (): void => {
if (lightIsOn)
turnLightsOff();
else
turnLightsOnWithFlickering();
turnLightsOn();
};
const turnLightsOnWithFlickering = (): void => {
const turnLightsOn = (): void => {
const header = getHeader();
const flickers = [true, false, true, false, true, false, true];
log.info("Turning lights on with flickering");
flickers.forEach((flicker, index) => {
setTimeout(() => {
if (flicker)
header.classList.add(LIGHTS_ON_CLASS);
else
header.classList.remove(LIGHTS_ON_CLASS);
}, index * 100);
});
};
header.classList.add(LIGHTS_ON_CLASS);
}
const startLightsIfWereOnAfterDelay = (): void => {
if (getLightIsOn())
setTimeout(turnLightsOnWithFlickering, 500);
setTimeout(turnLightsOn, 500);
else
log.info("Lights are off by user preference");
};

View File

@@ -48,7 +48,7 @@ function HelpResultView() {
</div>
<div className="right-panel">
<div className="section">
<div className="section-title soft">Supported Bitwise Operations</div>
<div className="section-title soft">Supported Operations</div>
<ul>
<li><code>&amp;</code> bitwise AND</li>
<li><code>|</code> bitwise inclusive OR</li>
@@ -57,10 +57,23 @@ function HelpResultView() {
<li><code>&lt;&lt;</code> left shift</li>
<li><code>&gt;&gt;</code> sign propagating right shift</li>
<li><code>&gt;&gt;&gt;</code> zero-fill right shift</li>
</ul>
<ul>
<li><code>+</code> addition</li>
<li><code>-</code> subtraction</li>
<li><code>*</code> multiplication</li>
<li><code>/</code> division (truncates toward zero*)</li>
</ul>
<div className='important-note'>
<FontAwesomeIcon icon={faCircleExclamation} size='lg'/> <a target='_blank' href='https://en.cppreference.com/w/c/language/operator_precedence' rel="noreferrer">Operator precedence</a> is IGNORED. Operators are executed <strong>left-to-right</strong>.
</div>
<p className='description'>
* Truncates toward zero means the fractional part is discarded and the quotient is rounded toward 0.
For example: <code>7/2 = 3</code>, <code>-7/2 = -3</code>.
Learn more:
<a target='_blank' rel='noreferrer' href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Division#bigint_division'> JS BigInt division</a>,
<a target='_blank' rel='noreferrer' href='https://en.cppreference.com/w/c/language/operator_arithmetic#Integer_arithmetic'> C integer division</a>.
</p>
</div>
<div className="section soft-border">
<div className="section-title soft">Supported Number Types</div>
@@ -86,3 +99,4 @@ function HelpResultView() {
}
export default HelpResultView;

View File

@@ -26,12 +26,12 @@ function SettingsPane(props : SettingsPaneProps) {
</div>
<div className='setting'>
<button type="button" onClick={() => appState.toggleDimExtrBits()}>
<FontAwesomeIcon size='xl' icon={appState.dimExtraBits ? faToggleOn : faToggleOff} /> Dim Extra Bits
<FontAwesomeIcon size='xl' icon={appState.dimExtraBits ? faToggleOn : faToggleOff} /> Dim Padding Bits
</button>
<p className='description'>
{appState.dimExtraBits
? "Extra bits used for padding are now dimmed."
: "No bits are dimmed."}
? "Extra bits, used for padding to align numbers of different sizes, are dimmed."
: "Extra bits used for padding are not dimmed."}
</p>
</div>
<div className='setting'>

View File

@@ -7,6 +7,15 @@ function WhatsNewResultView() {
return <div className="changelog">
<h3>Changelog</h3>
<div className='item item-new'>
<p>
<span className="soft date">Nov 8th, 2025</span> <br />
</p>
<ul>
<li>Expressions now support arithmetic operators alongside bitwise ones: <code>+</code>, <code>-</code>, <code>*</code>, and <code>/</code>.</li>
<li>Try examples: <code><CommandLink text="15 + 3" /></code>, <code><CommandLink text="10 - 4" /></code>, <code><CommandLink text="3 * -5" /></code>, <code><CommandLink text="7 / 2 + 3" /></code>.</li>
</ul>
</div>
<div className='item'>
<p>
<span className="soft date">Nov 6th, 2025</span> <br />
</p>

1
src/types/fontawesome-regular.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module '@fortawesome/free-regular-svg-icons';