From f0fefd65468605d6eaa836faf8d3c450a15cd8b1 Mon Sep 17 00:00:00 2001 From: Borys Levytskyi Date: Sat, 8 Nov 2025 14:09:08 -0500 Subject: [PATCH] Other ops (#68) --- package-lock.json | 30 +-- package.json | 1 + src/core/calc.test.ts | 174 +++++++++++++++++- src/core/calc.ts | 117 ++++++++++-- .../components/BitwiseResultView.tsx | 9 +- src/expression/expression.test.ts | 130 ++++++++++++- src/expression/expression.ts | 4 +- src/index.css | 65 +++++-- src/shell/AppState.ts | 2 +- src/shell/Bladerunner.ts | 21 +-- src/shell/components/HelpResultView.tsx | 16 +- src/shell/components/SettingsPane.tsx | 6 +- src/shell/components/WhatsNewResultView.tsx | 9 + src/types/fontawesome-regular.d.ts | 1 + 14 files changed, 511 insertions(+), 74 deletions(-) create mode 100644 src/types/fontawesome-regular.d.ts diff --git a/package-lock.json b/package-lock.json index 197f328..a3ba4e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index d8b6b4e..81b08cd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/core/calc.test.ts b/src/core/calc.test.ts index 19140e4..9bd4236 100644 --- a/src/core/calc.test.ts +++ b/src/core/calc.test.ts @@ -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 }); -}); \ No newline at end of file +}); diff --git a/src/core/calc.ts b/src/core/calc.ts index f37578e..2be6a97 100644 --- a/src/core/calc.ts +++ b/src/core/calc.ts @@ -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); -*/ \ No newline at end of file +*/ diff --git a/src/expression/components/BitwiseResultView.tsx b/src/expression/components/BitwiseResultView.tsx index 0d3e39d..0fae563 100644 --- a/src/expression/components/BitwiseResultView.tsx +++ b/src/expression/components/BitwiseResultView.tsx @@ -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 + buttons.push(
Two's Complement

This is a negative number. It's binary representation is inverted using Two's Complement operation. @@ -159,7 +160,7 @@ class ExpressionElementTableRow extends React.Component) if (!this.originalValue.isTheSame(this.scalar.value)) - buttons.push(); + buttons.push(); return {buttons} } @@ -263,4 +264,4 @@ class ExpressionElementTableRow extends React.Component{children} } -} \ No newline at end of file +} diff --git a/src/expression/expression.test.ts b/src/expression/expression.test.ts index eb75086..2b2925c 100644 --- a/src/expression/expression.test.ts +++ b/src/expression/expression.test.ts @@ -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); } -}); \ No newline at end of file +}); diff --git a/src/expression/expression.ts b/src/expression/expression.ts index 2b8f5ba..4f3454b 100644 --- a/src/expression/expression.ts +++ b/src/expression/expression.ts @@ -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 = /(<<|>>|>>>|\||&|\^|\+|\*|\/|(? { 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"); }; diff --git a/src/shell/components/HelpResultView.tsx b/src/shell/components/HelpResultView.tsx index 839e5dc..81fbd41 100644 --- a/src/shell/components/HelpResultView.tsx +++ b/src/shell/components/HelpResultView.tsx @@ -48,7 +48,7 @@ function HelpResultView() {

-
Supported Bitwise Operations
+
Supported Operations
  • & — bitwise AND
  • | — bitwise inclusive OR
  • @@ -57,10 +57,23 @@ function HelpResultView() {
  • << — left shift
  • >> — sign propagating right shift
  • >>> — zero-fill right shift
  • +
+
    +
  • + — addition
  • +
  • - — subtraction
  • +
  • * — multiplication
  • +
  • / — division (truncates toward zero*)
Operator precedence is IGNORED. Operators are executed left-to-right.
+

+ * “Truncates toward zero” means the fractional part is discarded and the quotient is rounded toward 0. + For example: 7/2 = 3, -7/2 = -3. + Learn more: + JS BigInt division, + C integer division. +

Supported Number Types
@@ -86,3 +99,4 @@ function HelpResultView() { } export default HelpResultView; + diff --git a/src/shell/components/SettingsPane.tsx b/src/shell/components/SettingsPane.tsx index 6a7d3d9..5e89942 100644 --- a/src/shell/components/SettingsPane.tsx +++ b/src/shell/components/SettingsPane.tsx @@ -26,12 +26,12 @@ function SettingsPane(props : SettingsPaneProps) {

{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."}

diff --git a/src/shell/components/WhatsNewResultView.tsx b/src/shell/components/WhatsNewResultView.tsx index f4f6b6d..90526f8 100644 --- a/src/shell/components/WhatsNewResultView.tsx +++ b/src/shell/components/WhatsNewResultView.tsx @@ -7,6 +7,15 @@ function WhatsNewResultView() { return

Changelog

+

+ Nov 8th, 2025
+

+
    +
  • Expressions now support arithmetic operators alongside bitwise ones: +, -, *, and /.
  • +
  • Try examples: , , , .
  • +
+
+

Nov 6th, 2025

diff --git a/src/types/fontawesome-regular.d.ts b/src/types/fontawesome-regular.d.ts new file mode 100644 index 0000000..b00c62e --- /dev/null +++ b/src/types/fontawesome-regular.d.ts @@ -0,0 +1 @@ +declare module '@fortawesome/free-regular-svg-icons';