From 23748831ba1c8b863a16e8cfe3199103fe399b68 Mon Sep 17 00:00:00 2001 From: Borys Levytskyi Date: Wed, 10 May 2023 13:59:20 +0300 Subject: [PATCH] Custom-built bitwise calculator, new number types, and improved UI (#46) --- src/core/Integer.test.ts | 34 +++ src/core/Integer.ts | 118 ++++++++ src/core/calc.test.ts | 269 ++++++++++++----- src/core/calc.ts | 273 +++++++++++++----- src/core/components/BinaryString.css | 3 - src/core/components/BinaryString.tsx | 14 +- src/core/const.ts | 8 +- src/core/formatter.test.ts | 8 +- src/core/formatter.ts | 18 +- src/core/types.ts | 27 -- src/core/utils.tsx | 17 +- src/expression/BitwiseOperator.test.ts | 5 +- src/expression/BitwiseOperator.ts | 45 ++- .../ListOfNumbersExpression.test.ts | 7 +- src/expression/OperatorToken.test.ts | 12 - src/expression/ScalarExpression.test.ts | 19 -- src/expression/ScalarValue.test.ts | 14 +- src/expression/ScalarValue.ts | 76 ++--- .../components/BitwiseResultView.tsx | 48 +-- .../components/BitwiseResultViewModel.ts | 13 +- src/expression/engine.test.ts | 41 --- src/expression/engine.ts | 44 --- src/expression/expression.test.ts | 127 +++++++- src/expression/expression.ts | 29 +- src/expression/numberParser.test.ts | 117 +++++--- src/expression/numberParser.ts | 127 ++++---- src/index.css | 46 ++- src/index.tsx | 2 - src/networking/components/VpcView.css | 2 +- src/shell/components/AboutResultView.tsx | 2 +- src/shell/components/AppRoot.tsx | 6 +- src/shell/components/HelpResultView.css | 9 +- src/shell/components/HelpResultView.tsx | 32 +- src/shell/components/InputBox.tsx | 7 +- src/shell/components/TopLinks.tsx | 5 +- src/shell/components/WhatsNewResultView.css | 3 +- src/shell/components/WhatsNewResultView.tsx | 51 +++- src/shell/startup.ts | 3 +- 38 files changed, 1099 insertions(+), 582 deletions(-) create mode 100644 src/core/Integer.test.ts create mode 100644 src/core/Integer.ts delete mode 100644 src/core/types.ts delete mode 100644 src/expression/OperatorToken.test.ts delete mode 100644 src/expression/ScalarExpression.test.ts delete mode 100644 src/expression/engine.test.ts delete mode 100644 src/expression/engine.ts diff --git a/src/core/Integer.test.ts b/src/core/Integer.test.ts new file mode 100644 index 0000000..a59d86e --- /dev/null +++ b/src/core/Integer.test.ts @@ -0,0 +1,34 @@ +import { Integer } from "./Integer" +import { UINT32_MAX_VALUE } from "./const"; + +it('converts signed to unsigned and vice versa', () => { + const n1 = new Integer(-1, 8); + const n2 = n1.toUnsigned(); + const n3 = n2.toSigned(); + + expect(n2.signed).toBe(false); + expect(n2.num()).toBe(255); + + expect(n3.signed).toBe(true); + expect(n3.num()).toBe(-1); + + expect(new Integer(1, 32, false).resize(64).toSigned().maxBitSize).toBe(64); +}); + +it('convers to different type', () => { + const src = new Integer(-1); + const dest = new Integer(1, 64, false); + + const n = src.convertTo(dest); + expect(n.num()).toBe(UINT32_MAX_VALUE); +}) + +it('converts to largest size', () => { + const n8 = new Integer(-1, 8); + const n16 = n8.resize(16); + const n32 = n16.resize(32); + + expect(n8.num()).toBe(-1); + expect(n16.num()).toBe(-1); + expect(n32.num()).toBe(-1); +}); \ No newline at end of file diff --git a/src/core/Integer.ts b/src/core/Integer.ts new file mode 100644 index 0000000..020709a --- /dev/null +++ b/src/core/Integer.ts @@ -0,0 +1,118 @@ +import { type } from "os"; +import { INT32_MAX_VALUE, INT32_MIN_VALUE } from "./const"; +import { asIntN } from "./utils"; +import formatter from "./formatter"; +import calc from "./calc"; + +export type JsNumber = number | bigint; + + +export class Integer { + + readonly value: bigint; + readonly maxBitSize: number; + readonly signed: boolean; + + constructor(value: JsNumber, maxBitSize?: number, signed? : boolean) { + this.value = typeof value == "bigint" ? value : BigInt(value); + this.maxBitSize = maxBitSize != null ? maxBitSize : (value >= INT32_MIN_VALUE && value <= INT32_MAX_VALUE) ? 32 : 64; + this.signed = signed == null ? true : signed == true; + } + + static unsigned(value : JsNumber, maxBitSize?: number) { + return new Integer(value, maxBitSize, false); + } + + static long(value: JsNumber) : Integer { + return new Integer(value, 64); + } + + static int(value: JsNumber) : Integer { + return new Integer(value, 32); + } + + static short(value: JsNumber) : Integer { + return new Integer(value, 16); + } + + static byte(value: JsNumber) : Integer { + return new Integer(value, 8); + } + + toUnsigned() { + return this.signed + ? new Integer(BigInt("0b" + this.toString(2)), this.maxBitSize, false) + : new Integer(this.value, this.maxBitSize, this.signed); + + } + + toSigned() { + + if(this.signed) + return new Integer(this.value, this.maxBitSize, this.signed); + + const bin = calc.engine.applyTwosComplement(this.toString(2)); + const n = BigInt("0b"+bin); + + return new Integer(bin[0] == '1' ? n : -n, this.maxBitSize, true) + } + + resize(newSize: number) { + + if(newSize < this.maxBitSize) + throw new Error("Size cannot be reduced"); + + if(newSize > 64) + throw new Error(`Bit size of ${newSize} is not supported`) + + return new Integer(this.value, newSize, this.signed); + } + + convertTo(other: Integer) { + + let newValue = this.value; + + if(this.signed && !other.signed) + newValue = this.toUnsigned().value; + + return new Integer(newValue, other.maxBitSize, other.signed); + } + + valueOf() { + return this.value.toString(); + } + + toString(base?:number) { + return formatter.numberToString(this, base || 10); + } + + num() { + return asIntN(this.value); + } + + bigint() { + return this.value; + } +} + +export function asInteger(num: JsNumber | Integer | string): Integer { + + if(typeof num == "string") + return asInteger(BigInt(num)); + + if(isInteger(num)) + return num; + + if(typeof num == "number" && isNaN(num)) { + throw new Error("Cannot create BoundedNumber from NaN"); + } + + const size = num > INT32_MAX_VALUE || num < INT32_MIN_VALUE ? 64 : 32; + + const n = typeof num == "bigint" ? num : BigInt(num); + return new Integer(n, size); +} + +export function isInteger(num: JsNumber | Integer): num is Integer { + return (num).maxBitSize !== undefined; + } \ No newline at end of file diff --git a/src/core/calc.test.ts b/src/core/calc.test.ts index fdc4ea3..f87a8dc 100644 --- a/src/core/calc.test.ts +++ b/src/core/calc.test.ts @@ -1,126 +1,251 @@ import calc from './calc'; -import { ScalarValue } from '../expression/expression'; -import { INT32_MAX_VALUE } from './const'; -import { asBoundedNumber } from './types'; +import { Integer, asInteger } from './Integer'; +import { INT32_MIN_VALUE, INT64_MAX_VALUE, UINT64_MAX_VALUE } from './const'; describe('calc.flipBit', () => { it('calculates flipped bit 32-bit number', () => { - expect(calc.flipBit(0, 31).value).toBe(1); - expect(calc.flipBit(1, 31).value).toBe(0); - expect(calc.flipBit(-1, 31).value).toBe(-2); - expect(calc.flipBit(2147483647, 0).value).toBe(-1); - expect(calc.flipBit(-1, 0).value).toBe(2147483647); - expect(calc.flipBit(2147483647, 30).value).toBe(2147483645); + expect(calc.flipBit(0, 31).num()).toBe(1); + expect(calc.flipBit(1, 31).num()).toBe(0); + expect(calc.flipBit(-1, 31).num()).toBe(-2); + expect(calc.flipBit(2147483647, 0).num()).toBe(-1); + expect(calc.flipBit(-1, 0).num()).toBe(2147483647); + expect(calc.flipBit(2147483647, 30).num()).toBe(2147483645); + }); + + it('sing-bit in 8-bit number', () => { + const result = calc.flipBit(new Integer(-1, 8), 0); + expect(result.maxBitSize).toBe(8); }); it('caulate flipped bit 64-bit nubmer', () => { - const int64max = BigInt("9223372036854775807"); - expect(calc.flipBit(BigInt(int64max), 0).value.toString()).toBe("-1"); + const int64max = asInteger("9223372036854775807"); + expect(calc.flipBit(int64max, 0).num()).toBe(-1); + }); + + it('treats usingned type differently', () => { + + const v = BigInt("0b01111111"); + const r = calc.flipBit(new Integer(v, 8), 0); + + expect(r.num()).toBe(-1); + expect(r.signed).toBe(true); + + const ur = calc.flipBit(Integer.unsigned(v, 8), 0) + expect(ur.num()).toBe(255); + expect(ur.signed).toBe(false); }); it('calculates flipped bit', () => { - expect(calc.flipBit(0, 31).value).toBe(1); - expect(calc.flipBit(1, 31).value).toBe(0); - expect(calc.flipBit(-1, 31).value).toBe(-2); - expect(calc.flipBit(2147483647, 0).value).toBe(-1); - expect(calc.flipBit(-1, 0).value).toBe(2147483647); - expect(calc.flipBit(2147483647, 30).value).toBe(2147483645); + expect(calc.flipBit(0, 31).num()).toBe(1); + expect(calc.flipBit(1, 31).num()).toBe(0); + expect(calc.flipBit(-1, 31).num()).toBe(-2); + expect(calc.flipBit(2147483647, 0).num()).toBe(-1); + expect(calc.flipBit(-1, 0).num()).toBe(2147483647); + expect(calc.flipBit(2147483647, 30).num()).toBe(2147483645); }); - - it('calcualte 31th bit in 64-bit int', () => { - expect(calc.flipBit(calc.promoteTo64Bit(-1), 31).value.toString()).toBe("8589934591"); - }); + it('supports ulong', () => { + const ulong = calc.flipBit(new Integer(INT64_MAX_VALUE, 64, false), 0); + expect(ulong.toString()).toBe(UINT64_MAX_VALUE.toString()); + }) }); +describe('calc.addSpace', () => { + it('resizes number based on the space required', () => { + const n8 = new Integer(1, 8); + const n16 = new Integer(1, 16); + + expect(calc.addSpace(n8, 0).maxBitSize).toBe(8); + expect(calc.addSpace(n8, 1).maxBitSize).toBe(16); + expect(calc.addSpace(n8, 9).maxBitSize).toBe(32); + expect(calc.addSpace(n16, 1).maxBitSize).toBe(32); + expect(calc.addSpace(n16, 32).maxBitSize).toBe(64); + }); +}); + describe('calc.numberOfBitsDisplayed', () => { it('calculates number of bits', () => { expect(calc.numberOfBitsDisplayed(1)).toBe(1); - expect(calc.numberOfBitsDisplayed(BigInt(-1))).toBe(64); + expect(calc.numberOfBitsDisplayed(BigInt(-1))).toBe(32); expect(calc.numberOfBitsDisplayed(2)).toBe(2); expect(calc.numberOfBitsDisplayed(3)).toBe(2); expect(calc.numberOfBitsDisplayed(68719476735)).toBe(36); - expect(calc.numberOfBitsDisplayed(-INT32_MAX_VALUE)).toBe(32); - expect(calc.numberOfBitsDisplayed(-(BigInt(INT32_MAX_VALUE+1)))).toBe(64); + expect(calc.numberOfBitsDisplayed(INT32_MIN_VALUE-1)).toBe(64); }); }); -describe('calc.applyTwosComplement', () => { - it('applies twos complement', () => { - expect(calc.applyTwosComplement("010")).toBe("110"); - expect(calc.applyTwosComplement("110")).toBe("010"); // reverse - expect(calc.applyTwosComplement("110")).toBe("010"); - expect(calc.applyTwosComplement("0")).toBe("10"); - expect(calc.applyTwosComplement("10101100")).toBe("01010100"); - expect(calc.applyTwosComplement("01010100")).toBe("10101100"); // reverse - }); -}); - -describe('calc.rshift', () => { - - it('produces number when given number and vice vers', () => { - const number = calc.rshift({value:1, maxBitSize:32}, 1).value; - const bigInt = calc.rshift({value:BigInt(1), maxBitSize:32}, 1).value; - - expect(typeof number).toBe('number'); - expect(number).toBe(2); - - expect(typeof bigInt).toBe('bigint'); - expect(bigInt.toString()).toBe('2'); - }); - - it('supports scalar values', () => { - const operand = new ScalarValue(1); - expect(calc.rshift(operand, 1).value).toBe(2); - }); +describe('calc.lshift', () => { it("respects bit size", () => { - expect(calc.rshift({value: BigInt("0b0100"), maxBitSize: 4}, 2).value.toString()).toBe("0"); + expect(calc.lshift(new Integer(BigInt("0b0100"), 4), 2).num()).toBe(0); }); it('transitions number to negative', ()=> { // 4-bit space - expect(calc.rshift({value: BigInt("0b0100"), maxBitSize: 4}, 1).value.toString()).toBe("-8"); + expect(calc.lshift(new Integer(BigInt("0b0100"), 4), 1).num()).toBe(-8); // 5-bit space - expect(calc.rshift({value: BigInt("0b00100"), maxBitSize: 5}, 1).value.toString()).toBe("8"); - expect(calc.rshift({value: BigInt("0b01000"), maxBitSize: 5}, 1).value.toString()).toBe("-16"); + expect(calc.lshift(new Integer(BigInt("0b00100"), 5), 1).num()).toBe(8); + expect(calc.lshift(new Integer(BigInt("0b01000"), 5), 1).num()).toBe(-16); // 32-bit - expect(calc.rshift({value: BigInt("2147483647"), maxBitSize: 32}, 1).value.toString()).toBe("-2"); - expect(calc.rshift({value: BigInt("2147483647"), maxBitSize: 32}, 2).value.toString()).toBe("-4"); + expect(calc.lshift(new Integer(BigInt("2147483647"), 32), 1).num()).toBe(-2); + expect(calc.lshift(new Integer(BigInt("2147483647"), 32), 2).num()).toBe(-4); // 64-bit - expect(calc.rshift({value: BigInt("9223372036854775807"), maxBitSize: 64}, 1).value.toString()).toBe("-2"); - expect(calc.rshift({value: BigInt("9223372036854775807"), maxBitSize: 64}, 2).value.toString()).toBe("-4"); - expect(calc.rshift({value: BigInt("2147483647"), maxBitSize: 64}, 1).value.toString()).toBe("4294967294"); - expect(calc.rshift({value: BigInt("2147483647"), maxBitSize: 64}, 2).value.toString()).toBe("8589934588"); + expect(calc.lshift(new Integer(BigInt("9223372036854775807"), 64), 1).num()).toBe(-2); + expect(calc.lshift(new Integer(BigInt("9223372036854775807"), 64), 2).num()).toBe(-4); + expect(calc.lshift(new Integer(BigInt("2147483647"), 64), 1).value.toString()).toBe("4294967294"); + expect(calc.lshift(new Integer(BigInt("2147483647"), 64), 2).value.toString()).toBe("8589934588"); + }); + it('test', () => { + const actual = calc.lshift(asInteger(100081515), 31).num(); + expect(actual).toBe(-2147483648); + }); + + it('1 to sign bit', () => { + const actual = calc.lshift(asInteger(1), 31).num(); + expect(actual).toBe(-2147483648); + }); + + it('resizes items if operands have different sizes', () => { + // -1L|-1 - 64 bit + // 1123123u|-1 - 64 bit + const r = calc.or(new Integer(-1, 64), new Integer(-1)); + expect(r.maxBitSize).toBe(64); + expect(r.num()).toBe(-1); + + const r2 = calc.or(Integer.unsigned(1123123, 32), new Integer(-1)); + expect(r2.maxBitSize).toBe(64); + }); +}); + +describe('preserves C compiler behvaior', () => { + it('lshift same size bytes', () => { + const int = Integer.int(-1); + const long = Integer.long(-1); + + expect(calc.lshift(int, 32).num()).toBe(int.num()); + expect(calc.lshift(long, 32).num()).toBe(-4294967296); + expect(calc.lshift(long, 64).num()).toBe(long.num()); + + expect(calc.rshift(int, 32).num()).toBe(int.num()); + expect(calc.rshift(long, 32).num()).toBe(-1); + expect(calc.rshift(long, 64).num()).toBe(long.num()); + + expect(calc.urshift(int, 32).num()).toBe(int.num()); + expect(calc.urshift(long, 32).num()).toBe(4294967295); + expect(calc.urshift(long, 64).num()).toBe(long.num()); + }); + + it('shift by bigger numbers of bytes', () => { + const byte = Integer.byte(-1); + expect(calc.urshift(byte, 9).num()).toBe(calc.urshift(byte, 1).num()); + expect(calc.urshift(byte, 17).num()).toBe(calc.urshift(byte, 1).num()); + expect(calc.urshift(byte, 18).num()).toBe(calc.urshift(byte, 2).num()); + + const int = Integer.int(-1); + expect(calc.lshift(int, 33).num()).toBe(-2); + expect(calc.rshift(int, 33).num()).toBe(-1); + + expect(calc.rshift(Integer.int(1), 33).num()).toBe(0); + expect(calc.rshift(Integer.int(1), 32).num()).toBe(1); + + expect(calc.lshift(Integer.byte(1), 20).num()).toBe(16); }); }); describe("calc misc", () => { - it('promoteTo64Bit', () => { - expect(calc.promoteTo64Bit(-1).value.toString(2)).toBe("11111111111111111111111111111111"); + const n = asInteger(-1); + expect(calc.toBinaryString(calc.promoteTo64Bit(n))).toBe("11111111111111111111111111111111"); }); it('binaryRepresentation', () => { - expect(calc.toBinaryString(asBoundedNumber(2147483647))).toBe("1111111111111111111111111111111"); - expect(calc.toBinaryString(new ScalarValue(2147483647))).toBe("1111111111111111111111111111111"); - }) + expect(calc.toBinaryString(asInteger(2147483647))).toBe("1111111111111111111111111111111"); + }); - + it('not 64bit', () => { + const actual = calc.not(asInteger("8920390230576132")).toString(); + expect(actual).toBe("-8920390230576133"); + }); }); -describe("bitwise ", () => { +describe("calc.engine.", () => { + it("not", () => { + expect(calc.engine.not("0101")).toBe("1010"); + expect(calc.engine.not("11111")).toBe("00000") + }); + + it("or", () => { + expect(calc.engine.or("1", "1")).toBe("1"); + expect(calc.engine.or("1", "0")).toBe("1"); + expect(calc.engine.or("0", "0")).toBe("0"); + expect(calc.engine.or("10101", "01111")).toBe("11111"); + }); + + it("and", () => { + expect(calc.engine.and("1", "1")).toBe("1"); + expect(calc.engine.and("1", "0")).toBe("0"); + expect(calc.engine.and("0", "0")).toBe("0"); + expect(calc.engine.and("10101", "11011")).toBe("10001"); + }); + + it("xor", () => { + expect(calc.engine.xor("1", "1")).toBe("0"); + expect(calc.engine.xor("1", "0")).toBe("1"); + expect(calc.engine.xor("0", "0")).toBe("0"); + expect(calc.engine.xor("10101", "11011")).toBe("01110"); + }); + + it("lshift", () => { + expect(calc.engine.lshift("1", 1)).toBe("0"); + expect(calc.engine.lshift("01", 1)).toBe("10"); + expect(calc.engine.lshift("01101", 4)).toBe("10000"); + expect(calc.engine.lshift("000001", 4)).toBe("010000"); + }); + + it("rshift", () => { + expect(calc.engine.rshift("1", 1)).toBe("1"); + expect(calc.engine.rshift("01", 1)).toBe("00"); + expect(calc.engine.rshift("0101", 2)).toBe("0001"); + expect(calc.engine.rshift("1000", 3)).toBe("1111"); + expect(calc.engine.rshift("1101", 1)).toBe("1110"); + }); + + it("urshift", () => { + expect(calc.engine.urshift("1", 1)).toBe("0"); + expect(calc.engine.urshift("01", 1)).toBe("00"); + expect(calc.engine.urshift("0101", 2)).toBe("0001"); + expect(calc.engine.urshift("1000", 3)).toBe("0001"); + expect(calc.engine.urshift("1101", 1)).toBe("0110"); + }); + + it('flipbit', () => { + expect(calc.engine.flipBit("1", 0)).toBe("0"); + expect(calc.engine.flipBit("101", 1)).toBe("111"); + }); + + it('applyTwosComplement', () => { + expect(calc.engine.applyTwosComplement("010")).toBe("110"); + expect(calc.engine.applyTwosComplement("110")).toBe("010"); // reverse + expect(calc.engine.applyTwosComplement("110")).toBe("010"); + expect(calc.engine.applyTwosComplement("0")).toBe("10"); + expect(calc.engine.applyTwosComplement("10101100")).toBe("01010100"); + expect(calc.engine.applyTwosComplement("01010100")).toBe("10101100"); // reverse + }); +}) + +describe("engine comparison", () => { + it("NOT same as in node", () => { for(var i = -100; i<100;i++) { const expected = bin(~i); - const actual = calc.bitwise.not(bin(i)); + const actual = calc.engine.not(bin(i)); expect(`${i} is ${actual}`).toBe(`${i} is ${(expected)}`); } }); @@ -129,7 +254,7 @@ describe("bitwise ", () => { for(var x = -100; x<100;x++) { const y = 5+3%x+x%6*(-x); const expected = bin(x | y); - const actual = calc.bitwise.or(bin(x), bin(y)); + const actual = calc.engine.or(bin(x), bin(y)); expect(`${x} is ${actual}`).toBe(`${x} is ${(expected)}`); } @@ -139,7 +264,7 @@ describe("bitwise ", () => { for(var x = -100; x<100;x++) { const y = 5+3%x+x%6*(-x); const expected = bin(x & y); - const actual = calc.bitwise.and(bin(x), bin(y)); + const actual = calc.engine.and(bin(x), bin(y)); expect(`${x} is ${actual}`).toBe(`${x} is ${(expected)}`); } diff --git a/src/core/calc.ts b/src/core/calc.ts index 0656ac1..f32a302 100644 --- a/src/core/calc.ts +++ b/src/core/calc.ts @@ -1,72 +1,43 @@ -import formatter from "./formatter"; -import { BoundedNumber, JsNumber, maxBitSize, asBoundedNumber } from "./types"; +import { Integer, JsNumber, asInteger } from "./Integer"; import { asIntN } from "./utils"; export default { - abs (num : BoundedNumber) : BoundedNumber { - return asBoundedNumber(num.value >= 0 ? num.value : -num.value); + abs (num : Integer) : Integer { + return asInteger(num.value >= 0 ? num.value : -num.value); }, - maxBitSize(num : JsNumber) : number { - return maxBitSize(num); + numberOfBitsDisplayed: function (num: Integer | JsNumber) : number { + return this.toBinaryString(asInteger(num)).length; }, - numberOfBitsDisplayed: function (num: JsNumber) : number { - - if(num < 0) { - return typeof num == 'bigint' ? 64 : 32 - }; - - return num.toString(2).length; + flipBit: function(num: Integer | JsNumber, bitIndex: number): Integer { + return this._applySingle(asInteger(num), (bin) => this.engine.flipBit(bin, bitIndex)); }, - flipBit: function(num: BoundedNumber | JsNumber, index: number): BoundedNumber { + promoteTo64Bit(number: Integer) : Integer { + const bin = this.toBinaryString(number); + return new Integer(BigInt("0b" + bin), 64); + }, - num = asBoundedNumber(num); - const is64bit = num.maxBitSize == 64; - const size = num.maxBitSize; - const bin = formatter.bin(num.value).padStart(size, '0'); - const staysNegative = (bin[0] == "1" && index > 0); - const becomesNegative = (bin[0] == "0" && index == 0); - - let m = 1; - let flipped = bin.substring(0, index) + flip(bin[index]) + bin.substring(index+1); + addSpace(number: Integer, requiredSpace: number) : Integer { + const bin = this.toBinaryString(number); + const totalSpaceRequired = number.maxBitSize + requiredSpace; + return new Integer(BigInt("0b" + bin), nextPowOfTwo(totalSpaceRequired)); + }, - if(staysNegative || becomesNegative) { - flipped = this.applyTwosComplement(flipped); - m=-1; + operation (op1: Integer, operator: string, op2 : Integer) : Integer { + switch(operator) { + 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); + default: throw new Error(operator + " operator is not supported"); } - - const n : JsNumber = is64bit ? BigInt("0b"+ flipped)*BigInt(m) : parseInt(flipped, 2)*m; - return asBoundedNumber(n); }, - promoteTo64Bit(number: number) : BoundedNumber { - const bin = this.toBinaryString(asBoundedNumber(number)); - return asBoundedNumber(BigInt("0b" + bin)); - }, - - applyTwosComplement: (bin:string):string => { - var lastIndex = bin.lastIndexOf('1'); - - // If there exists no '1' concat 1 at the - // starting of string - if (lastIndex == -1) - return "1" + bin; - - // Continue traversal backward after the position of - // first '1' - var flipped =[]; - for (var i = lastIndex - 1; i >= 0; i--) { - // Just flip the values - flipped.unshift(bin.charAt(i) == "1" ? "0" : "1"); - } - - return flipped.join('') + bin.substring(lastIndex) ; - }, - - toBinaryString(num: BoundedNumber) : string { - + toBinaryString(num: Integer) : string { const bitSize = num.maxBitSize; const bin = this.abs(num).value.toString(2); @@ -74,41 +45,125 @@ export default { throw new Error(`Binary represenation '${bin}' is bigger than the given bit size ${bitSize}`) const r = num.value < 0 - ? this.applyTwosComplement(bin.padStart(bitSize, '0')) + ? this.engine.applyTwosComplement(bin.padStart(bitSize, '0')) : bin; return r; }, - rshift (num: BoundedNumber, numBytes : JsNumber) : BoundedNumber { + lshift (num: Integer, numBytes : JsNumber) : Integer { + + let bytes = asIntN(numBytes); + + if(num.maxBitSize == numBytes) + return num; // Preserve C undefined behavior - const bytes = asIntN(numBytes); + while(bytes > num.maxBitSize) bytes -= num.maxBitSize; + + return this._applySingle(num, bin => this.engine.lshift(bin, bytes)); + }, + + rshift (num : Integer, numBytes : JsNumber) : Integer { + let bytes = asIntN(numBytes); + + if(num.maxBitSize == numBytes) + return num; // Preserve C undefined behavior + + while(bytes > num.maxBitSize) bytes -= num.maxBitSize; + + return this._applySingle(num, bin => this.engine.rshift(bin, bytes)); + }, + + urshift (num : Integer, numBytes : JsNumber) : Integer { + + let bytes = asIntN(numBytes); + + if(num.maxBitSize == numBytes) + return num; // Preserve C undefined behavior + + while(bytes > num.maxBitSize) bytes -= num.maxBitSize; + + return this._applySingle(num, bin => this.engine.urshift(bin, bytes)); + }, + + not(num:Integer) : Integer { + return this._applySingle(num, this.engine.not); + }, + + and (num1 : Integer, num2 : Integer) : Integer { + return this._applyTwo(num1, num2, this.engine.and); + }, + + or (num1 : Integer, num2 : Integer) : Integer { + return this._applyTwo(num1, num2, this.engine.or); + }, + + xor (num1 : Integer, num2 : Integer) : Integer { + return this._applyTwo(num1, num2, this.engine.xor); + }, + + _applySingle(num: Integer, operation: (bin:string) => string) : Integer { + let bin = this.toBinaryString(num).padStart(num.maxBitSize, '0'); - bin = bin.substring(bytes) + "0".repeat(bytes); + bin = operation(bin); + + let negative = false; + + if(num.signed && bin['0'] == '1') { + bin = this.engine.applyTwosComplement(bin); + negative = true; + } + + const result = BigInt("0b" + bin) * BigInt(negative ? -1 : 1); + 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 { + + if(op1.maxBitSize == op2.maxBitSize && op1.signed != op2.signed) + throw new Error("Operator `" + operation + "` cannot be applied to signed and unsigned operands of the same size"); + + const [num1, num2] = equalizeSize(op1, op2); + + let bin1 = this.toBinaryString(num1).padStart(num1.maxBitSize, '0'); + let bin2 = this.toBinaryString(num2).padStart(num2.maxBitSize, '0'); + + let resultBin = operation(bin1, bin2); let m = BigInt(1); - if(bin['0'] == '1') { - bin = this.applyTwosComplement(bin); + if(resultBin['0'] == '1') { + resultBin = this.engine.applyTwosComplement(resultBin); m = BigInt(-1); } - const result = BigInt("0b" + bin) * m; - return asBoundedNumber(typeof num.value == "bigint" ? result : asIntN(result)); + const result = BigInt("0b" + resultBin) * m; + return new Integer(result, num1.maxBitSize); }, - bitwise: { - not: (bin: string) : string => { + engine: { + lshift (bin: string, bytes: number):string { + return bin.substring(bytes) + "0".repeat(bytes); + }, + rshift (bin: string, bytes: number):string { + const pad = bin[0].repeat(bytes); + return pad + bin.substring(0, bin.length - bytes); + }, + urshift (bin: string, bytes: number):string { + const pad = '0'.repeat(bytes); + return pad + bin.substring(0, bin.length - bytes); + }, + not (bin: string) : string { - var padded = bin + return bin .split('').map(c => flip(c)) .join(""); - - return padded; }, - or: (bin1: string, bin2 : string) : string => { + or (bin1: string, bin2 : string) : string { + + checkSameLength(bin1, bin2); const result = []; for(var i=0; i { + and (bin1: string, bin2 : string) : string { + + checkSameLength(bin1, bin2); const result = []; for(var i=0; i { + var lastIndex = bin.lastIndexOf('1'); + + // If there exists no '1' concat 1 at the + // starting of string + if (lastIndex == -1) + return "1" + bin; + + // Continue traversal backward after the position of + // first '1' + var flipped =[]; + for (var i = lastIndex - 1; i >= 0; i--) { + // Just flip the values + flipped.unshift(bin.charAt(i) == "1" ? "0" : "1"); + } + + return flipped.join('') + bin.substring(lastIndex) ; + }, } }; +function checkSameLength(bin1: string, bin2: string) { + if (bin1.length != bin2.length) + throw new Error("Binary strings must have the same length"); +} + function flip(bit:string):string { return bit === "0" ? "1" : "0"; -} \ No newline at end of file +} + +function nextPowOfTwo(num: number) : number { + let p = 2; + while(p < num) p = p*2; + return p; +} + +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 + return [n1.resize(n1.maxBitSize*2).toSigned(), n2.resize(n2.maxBitSize*2).toSigned()]; + } + + return n1.maxBitSize > n2.maxBitSize + ? [n1, n2.convertTo(n1)] + : [n1.convertTo(n2), n2]; +} + +/* + // c# + var op = -1; + var r = op>>>33; + Console.WriteLine(Convert.ToString(op, 2).PadLeft(32, '0')); + 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/core/components/BinaryString.css b/src/core/components/BinaryString.css index 9b8cc35..e69de29 100644 --- a/src/core/components/BinaryString.css +++ b/src/core/components/BinaryString.css @@ -1,3 +0,0 @@ -.sign-bit { - color:mediumseagreen !important; -} \ No newline at end of file diff --git a/src/core/components/BinaryString.tsx b/src/core/components/BinaryString.tsx index 35cbb07..30fd124 100644 --- a/src/core/components/BinaryString.tsx +++ b/src/core/components/BinaryString.tsx @@ -8,7 +8,7 @@ export type BinaryStringViewProps = { emphasizeBytes?: boolean; className?:string; disableHighlight?:boolean, - bitSize?: number + signBitIndex?: number, }; export type FlipBitEventArg = { @@ -36,7 +36,7 @@ export default class BinaryStringView extends React.Component bitSize) - signBitIndex = bitChars.length - bitSize!; - return bitChars.map((c, i) => { var className = c == '1' ? `one${css}` : `zero${css}`; var tooltip = ''; - if(i === signBitIndex) { - className += ' sign-bit'; + if(i === this.props.signBitIndex) { + className += ' accent1'; tooltip = 'Signature bit. 0 means a positive number and 1 means a negative.' } diff --git a/src/core/const.ts b/src/core/const.ts index 2f76c8c..19a4b96 100644 --- a/src/core/const.ts +++ b/src/core/const.ts @@ -1,6 +1,8 @@ const INT32_MAX_VALUE = 2147483647; -const INT32_MIN_VALUE = -INT32_MAX_VALUE; +const INT32_MIN_VALUE = -2147483648; +const UINT32_MAX_VALUE = 4294967295; const INT64_MAX_VALUE = BigInt("9223372036854775807"); -const INT64_MIN_VALUE = BigInt("-9223372036854775807"); -export {INT32_MAX_VALUE, INT32_MIN_VALUE, INT64_MAX_VALUE, INT64_MIN_VALUE}; +const INT64_MIN_VALUE = BigInt("-9223372036854775808"); +const UINT64_MAX_VALUE = BigInt("18446744073709551615"); +export {INT32_MAX_VALUE, INT32_MIN_VALUE, INT64_MAX_VALUE, INT64_MIN_VALUE, UINT32_MAX_VALUE, UINT64_MAX_VALUE}; diff --git a/src/core/formatter.test.ts b/src/core/formatter.test.ts index fadcd5c..d7e411d 100644 --- a/src/core/formatter.test.ts +++ b/src/core/formatter.test.ts @@ -1,4 +1,5 @@ import formatter from './formatter'; +import { Integer } from './Integer'; describe("formatter", () => { it('formats string', () => { @@ -8,8 +9,11 @@ describe("formatter", () => { }); it('respects size when formatting negative number', () => { - expect(formatter.bin(-1)).toBe("11111111111111111111111111111111"); - expect(formatter.bin(BigInt(-1))).toBe("1111111111111111111111111111111111111111111111111111111111111111"); + const minusOne = BigInt(-1); + const n32 = new Integer(minusOne, 32); + const n64 = new Integer(minusOne, 64); + expect(formatter.bin(n32)).toBe("11111111111111111111111111111111"); + expect(formatter.bin(n64)).toBe("1111111111111111111111111111111111111111111111111111111111111111"); }); it('formats large binary number correctly', () => { diff --git a/src/core/formatter.ts b/src/core/formatter.ts index aacd82b..bd92001 100644 --- a/src/core/formatter.ts +++ b/src/core/formatter.ts @@ -1,20 +1,20 @@ -import { BoundFunction } from "@testing-library/react"; import calc from "./calc"; -import { BoundedNumber, JsNumber, isBoundedNumber, asBoundedNumber } from "./types"; +import { Integer, JsNumber, asInteger } from "./Integer"; export type NumberBase = 'dec' | 'hex' | 'bin'; const formatter = { - numberToString: function(num: BoundedNumber | JsNumber, base: NumberBase) : string { + numberToString: function(num: Integer | JsNumber, base: NumberBase | number) : string { - num = asBoundedNumber(num); + num = asInteger(num); + base = typeof base == "string" ? getBase(base) : base; switch(base) { - case 'hex': + case 16: var hexVal = calc.abs(num).value.toString(16); return num.value >= 0 ? '0x' + hexVal : '-0x' + hexVal; - case 'bin': + case 2: return calc.toBinaryString(num); - case 'dec': + case 10: return num.value.toString(10); default: throw new Error("Unexpected kind: " + base) @@ -33,10 +33,10 @@ const formatter = { return sb.join(''); }, - bin(number: JsNumber) { + bin(number: Integer | JsNumber) { return this.numberToString(number, 'bin'); }, - emBin(number: JsNumber) { + emBin(number: Integer | JsNumber) { return this.padLeft(this.bin(number), 8, '0'); }, diff --git a/src/core/types.ts b/src/core/types.ts deleted file mode 100644 index 6914049..0000000 --- a/src/core/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { type } from "os"; - -export type JsNumber = number | bigint; -export type BoundedNumber = { - value: JsNumber, - maxBitSize: number -} - -export function asBoundedNumber(num: JsNumber | BoundedNumber): BoundedNumber { - - if(isBoundedNumber(num)) - return num; - - if(typeof num == "number" && isNaN(num)) { - throw new Error("Cannot create BoundedNumber from NaN"); - } - - return {value:num, maxBitSize: maxBitSize(num)}; -} - -export function isBoundedNumber(num: JsNumber | BoundedNumber): num is BoundedNumber { - return (num).maxBitSize !== undefined; - } - -export function maxBitSize(num : JsNumber) : number { - return typeof num == "bigint" ? 64 : 32; -}; \ No newline at end of file diff --git a/src/core/utils.tsx b/src/core/utils.tsx index ab58064..2744641 100644 --- a/src/core/utils.tsx +++ b/src/core/utils.tsx @@ -1,4 +1,4 @@ -import { JsNumber } from "./types"; +import { Integer, JsNumber, isInteger } from "./Integer"; function chunkifyString(input: string, chunkSize: number) : string[] { @@ -11,8 +11,19 @@ function chunkifyString(input: string, chunkSize: number) : string[] { return result; } -function asIntN(num: JsNumber) : number { +function asIntN(num: JsNumber | Integer) : number { + if(isInteger(num)) + return asIntN(num.value); + return typeof num == "bigint" ? parseInt(num.toString()): num as number; } -export {chunkifyString, asIntN}; \ No newline at end of file +function random(from: number, to: number) { + return Math.floor(Math.random() * (to+1)); +} + +function randomBool() { + return random(1, 10000) % 2 == 0; +} + +export {chunkifyString, asIntN, random, randomBool}; \ No newline at end of file diff --git a/src/expression/BitwiseOperator.test.ts b/src/expression/BitwiseOperator.test.ts index 886a4e9..74d343e 100644 --- a/src/expression/BitwiseOperator.test.ts +++ b/src/expression/BitwiseOperator.test.ts @@ -1,13 +1,12 @@ import ScalarValue from "./ScalarValue"; import BitwiseOperator from './BitwiseOperator'; -import { INT32_MAX_VALUE } from "../core/const"; it('can apply ~ operand', () => { var op = new ScalarValue(10, 'dec'); var expr = new BitwiseOperator(op, "~"); var result = expr.evaluate(); - expect(result.value).toBe(-11); + expect(result.value.num()).toBe(-11); expect(result.base).toBe('dec'); }); @@ -16,6 +15,6 @@ it('can apply & operand', () => { var op2 = new ScalarValue(4, 'dec'); var expr = new BitwiseOperator(op1, "|"); var result = expr.evaluate(op2); - expect(result.value).toBe(7); + expect(result.value.num()).toBe(7); expect(result.base).toBe('dec'); }); \ No newline at end of file diff --git a/src/expression/BitwiseOperator.ts b/src/expression/BitwiseOperator.ts index 85d331a..47a3f39 100644 --- a/src/expression/BitwiseOperator.ts +++ b/src/expression/BitwiseOperator.ts @@ -1,5 +1,5 @@ +import calc from '../core/calc'; import ScalarValue from './ScalarValue'; -import engine from './engine'; import { ExpressionElement } from './expression-interfaces'; export default class BitwiseOperator implements ExpressionElement { @@ -29,8 +29,8 @@ export default class BitwiseOperator implements ExpressionElement { var evaluatedOperand = this.operand.evaluate(); return this.operator == "~" - ? engine.applyNotOperator(this.operand.getUnderlyingScalarOperand()) - : engine.applyOperator(operand!, this.operator, evaluatedOperand); + ? applyNotOperator(this.operand.getUnderlyingScalarOperand()) + : applyOperator(operand!, this.operator, evaluatedOperand); } getUnderlyingScalarOperand() : ScalarValue { @@ -40,4 +40,43 @@ export default class BitwiseOperator implements ExpressionElement { toString(): string { return this.operator + this.operand.toString(); } +} + +function applyNotOperator(operand: ScalarValue) : ScalarValue { + return new ScalarValue(calc.not(operand.value), operand.base); +} + +function applyOperator(op1 : ScalarValue, operator: string, op2 : ScalarValue) : ScalarValue { + + const isShift = /<|>/.test(operator); + if(!isShift) + { + if(op1.value.maxBitSize == op2.value.maxBitSize && op1.value.signed != op2.value.signed) + throw new Error("Operator `" + operator + "` cannot be applied to signed and unsigned operands of the same " + op2.value.maxBitSize + " -bit size"); + + equalizeSize(op1, op2); + } + + const result = calc.operation(op1.value, operator, op2.value); + return new ScalarValue(result, op2.base); +} + +function equalizeSize(op1: ScalarValue, op2: ScalarValue) { + + const n1 = op1.value; + const n2 = op2.value; + + if(n1.maxBitSize === n2.maxBitSize) + { + if(n1.signed === n2.signed) return; + + // Example int and usinged int. Poromoted both to 64 bit + op1.setValue(n1.resize(n1.maxBitSize*2).toSigned()).setLabel("converted"); + op2.setValue(n2.resize(n2.maxBitSize*2).toSigned()).setLabel("converted"); + } + + if(n1.maxBitSize > n2.maxBitSize) + op2.setValue(n2.convertTo(n1)).setLabel("converted"); + else + op1.setValue(n1.convertTo(n2)).setLabel("converted"); } \ No newline at end of file diff --git a/src/expression/ListOfNumbersExpression.test.ts b/src/expression/ListOfNumbersExpression.test.ts index 1ba265b..0f09931 100644 --- a/src/expression/ListOfNumbersExpression.test.ts +++ b/src/expression/ListOfNumbersExpression.test.ts @@ -1,7 +1,12 @@ import ScalarValue from "./ScalarValue"; import ListOfNumbersExpression from "./ListOfNumbersExpression"; +import { numberParser } from "./numberParser"; it('calculates max bits length', () => { - var expr = new ListOfNumbersExpression("10 0xabef 0b01010", [ScalarValue.parse("10"), ScalarValue.parse("0xabef"), ScalarValue.parse("0b01010")]) + const v1 = new ScalarValue(numberParser.parse("10").value); + const v2 = new ScalarValue(numberParser.parse("0xabef").value); + const v3 = new ScalarValue(numberParser.parse("0b01010").value); + + var expr = new ListOfNumbersExpression("10 0xabef 0b01010", [v1, v2, v3]) expect(expr.maxBitsLength).toBe(16); }); diff --git a/src/expression/OperatorToken.test.ts b/src/expression/OperatorToken.test.ts deleted file mode 100644 index 875e645..0000000 --- a/src/expression/OperatorToken.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import exp from "constants"; -import BitwiseOperator from "./BitwiseOperator"; -import ScalarValue from "./ScalarValue" - -it('supports evaluation of big int', () => { - const v = new ScalarValue(BigInt(1)); - const op = new BitwiseOperator(new ScalarValue(1), "<<"); - - //const r = op.evaluate(v); - //expect(r.isBigInt()).toBe(true); - //expect(r.value.toString()).toBe("2"); -}) \ No newline at end of file diff --git a/src/expression/ScalarExpression.test.ts b/src/expression/ScalarExpression.test.ts deleted file mode 100644 index 07e988c..0000000 --- a/src/expression/ScalarExpression.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import ScalarValue from './ScalarValue'; - -it('parsed from dec string', () => { - var op = ScalarValue.parse('123'); - expect(op.base).toBe('dec'); - expect(op.value).toBe(123); -}); - -it('parsed from bin string', () => { - var op = ScalarValue.parse('0b10'); - expect(op.base).toBe('bin'); - expect(op.value).toBe(2); -}); - -it('parsed from hex string', () => { - var op = ScalarValue.parse('0x10'); - expect(op.base).toBe('hex'); - expect(op.value).toBe(16); -}); diff --git a/src/expression/ScalarValue.test.ts b/src/expression/ScalarValue.test.ts index b43f829..d681cc6 100644 --- a/src/expression/ScalarValue.test.ts +++ b/src/expression/ScalarValue.test.ts @@ -1,11 +1,11 @@ +import { INT32_MAX_VALUE } from "../core/const"; import ScalarValue from "./ScalarValue"; -it('supports bigint', () => { - const int = new ScalarValue(1); - const bigint = new ScalarValue(BigInt(1)); - expect(int.isBigInt()).toBe(false); - expect(bigint.isBigInt()).toBe(true); - expect(int.bitSize()).toBe(32); - expect(bigint.bitSize()).toBe(64); +it('converts numbers to bigint', () => { + const int32 = new ScalarValue(INT32_MAX_VALUE); + const int64 = new ScalarValue(BigInt(INT32_MAX_VALUE+1)); + + expect(int32.value.maxBitSize).toBe(32); + expect(int64.value.maxBitSize).toBe(64); }); \ No newline at end of file diff --git a/src/expression/ScalarValue.ts b/src/expression/ScalarValue.ts index 016cf5c..74004e7 100644 --- a/src/expression/ScalarValue.ts +++ b/src/expression/ScalarValue.ts @@ -1,9 +1,8 @@ import {numberParser} from './numberParser'; import { ExpressionElement as ExpressionElement } from './expression-interfaces'; import { NumberBase } from '../core/formatter'; -import { INT32_MAX_VALUE, INT32_MIN_VALUE, INT64_MAX_VALUE, INT64_MIN_VALUE } from '../core/const'; -import { BoundedNumber, JsNumber, isBoundedNumber, asBoundedNumber } from '../core/types'; -import calc from '../core/calc'; +import { INT32_MAX_VALUE, INT32_MIN_VALUE, INT64_MAX_VALUE, INT64_MIN_VALUE, UINT64_MAX_VALUE } from '../core/const'; +import { Integer, JsNumber, isInteger, asInteger } from '../core/Integer'; var globalId : number = 1; @@ -11,38 +10,33 @@ var globalId : number = 1; // Represents scalar numeric value export default class ScalarValue implements ExpressionElement { id: number; - value: JsNumber; + value: Integer; base: NumberBase; isOperator: boolean; - maxBitSize: number; + label: string; - constructor(value : BoundedNumber | JsNumber, base?: NumberBase) { + constructor(value : Integer | JsNumber, base?: NumberBase) { - if(!isBoundedNumber(value)) - value = asBoundedNumber(value); + if(!isInteger(value)) + value = asInteger(value); ScalarValue.validateSupported(value); this.id = globalId++; - this.value = 0; - this.maxBitSize = 0; + this.value = value as Integer; this.base = base || "dec"; - this.isOperator = false; - - this.setValue(value); + this.isOperator = false; + this.label = ''; + } + + setValue(value : Integer) : ScalarValue { + this.value = value; + return this; } - bitSize() : number { - return this.isBigInt() ? 64 : 32; - } - - isBigInt() : boolean { - return typeof this.value === 'bigint'; - } - - setValue(value : BoundedNumber) { - this.value = value.value; - this.maxBitSize = value.maxBitSize; + setLabel(label : string) : ScalarValue { + this.label = label; + return this; } evaluate() : ScalarValue { @@ -50,39 +44,17 @@ export default class ScalarValue implements ExpressionElement { } getUnderlyingScalarOperand() : ScalarValue { - return this + return this; } - static validateSupported(num : BoundedNumber) { + static validateSupported(num : Integer) { - if(typeof num.value == "bigint" && (num.value < INT64_MIN_VALUE || num.value > INT64_MAX_VALUE)) { - throw new Error(`64-bit numbers are supported in range from ${INT64_MIN_VALUE} to ${INT64_MAX_VALUE}`); + if(num.signed && (num.value < INT64_MIN_VALUE || num.value > INT64_MAX_VALUE)) { + throw new Error(`Signed 64-bit numbers are supported in range from ${INT64_MIN_VALUE} to ${INT64_MAX_VALUE}. Given number was ${num}`); } - if(typeof num.value == "number" && (num.value < INT32_MIN_VALUE || num.value > INT32_MAX_VALUE)) { - throw new Error(`Numer JavaScript type can only by used for numbers in range from ${INT32_MIN_VALUE} to ${INT32_MAX_VALUE}`) + if(!num.signed && (num.value > UINT64_MAX_VALUE)) { + throw new Error(`Unisgned 64-bit numbers larger than ${UINT64_MAX_VALUE} are not supported. Given number was ${num}`); } } - - static parse(input: string) : ScalarValue { - - var parsed = ScalarValue.tryParse(input); - - if(parsed == null) { - throw new Error(input + " is not a valid number"); - } - - return parsed; - } - - static tryParse(input: string) : ScalarValue | null { - - var parsed = numberParser.parse(input); - - if(!parsed) { - return null; - } - - return new ScalarValue(parsed.value, parsed.base); - } } \ No newline at end of file diff --git a/src/expression/components/BitwiseResultView.tsx b/src/expression/components/BitwiseResultView.tsx index 944c219..e5eafcb 100644 --- a/src/expression/components/BitwiseResultView.tsx +++ b/src/expression/components/BitwiseResultView.tsx @@ -56,7 +56,7 @@ export default class BitwiseResultView extends React.Component { } render() { const { sign, css, maxNumberOfBits, emphasizeBytes, allowFlipBits } = this.props; - const maxBits = Math.max() + const scalar = this.props.expressionItem.evaluate(); + const bin = formatter.numberToString(scalar.value, 'bin').padStart(maxNumberOfBits, '0'); + const signBitIndex = scalar.value.signed && bin.length >= scalar.value.maxBitSize ? bin.length - scalar.value.maxBitSize : -1; return {sign} @@ -96,21 +98,16 @@ class ExpressionRow extends React.Component { this.flipBit(args)} /> {this.getAlternative()} - {this.getInfo(maxNumberOfBits)} + {this.getInfo(maxNumberOfBits)} ;; } - getBinaryString(): string { - var v = this.props.expressionItem.evaluate(); - return formatter.numberToString(v.value, 'bin'); - } - getLabel(): string { // For expressions like |~2 @@ -146,12 +143,14 @@ class ExpressionRow extends React.Component { const op = this.props.expressionItem.getUnderlyingScalarOperand(); const { bitIndex: index, binaryStringLength: totalLength } = args; - if(totalLength > op.bitSize() && (totalLength - index) > op.bitSize()) { - op.setValue(calc.promoteTo64Bit(op.value as number)); + const maxBitSize = op.value.maxBitSize; + const space = (totalLength - index - maxBitSize); + if(totalLength > op.value.maxBitSize && space > 0) { + op.setValue(calc.addSpace(op.value, space)); } - const pad = op.bitSize() - totalLength; - const newValue = calc.flipBit(op, pad + index); + const pad = op.value.maxBitSize - totalLength; + const newValue = calc.flipBit(op.value, pad + index); op.setValue(newValue); this.props.onBitFlipped(); } @@ -159,18 +158,21 @@ class ExpressionRow extends React.Component { getInfo(maxNumberOfBits:number) { var op = this.props.expressionItem.getUnderlyingScalarOperand(); - if (op.isBigInt()) + if((op.value.maxBitSize != 32 || op.value.maxBitSize <= maxNumberOfBits) || op.label.length > 0) { - const title = `BigInt JavaScript type is used to reprsent this number. All bitwise operations that involve this number have their operands converted to BigInt. BitwiseCmd treats this number as 64-bit number.`; + let title = `BitwiseCmd treats this number as ${op.value.maxBitSize}-bit integer`; + let text = `${op.value.maxBitSize}-bit `; + + if(!op.value.signed) + text += " unsigned "; - return (64-bit BigInt); - } + if(op.label.length > 0) + { + text += " (converted)"; + title += ". This number was converted to facilitate bitwise operation with an operand of a different type"; + } - if(op.bitSize() == 32 && maxNumberOfBits >= 32) - { - const title = "BitwiseCmd treats this number as 32-bit integer. First bit is a sign bit. Try clicking on the first bit and see what will happen."; - - return (32-bit Number); + return {text}; } return null; diff --git a/src/expression/components/BitwiseResultViewModel.ts b/src/expression/components/BitwiseResultViewModel.ts index 1a5f352..aa9950d 100644 --- a/src/expression/components/BitwiseResultViewModel.ts +++ b/src/expression/components/BitwiseResultViewModel.ts @@ -2,7 +2,6 @@ import { ScalarValue, ListOfNumbersExpression, BitwiseOperationExpression, Bitwi import { ExpressionElement, Expression } from '../expression-interfaces'; import calc from '../../core/calc'; import formatter from '../../core/formatter'; -import exp from 'constants'; type Config = { emphasizeBytes: boolean; @@ -15,7 +14,7 @@ type ExpressionRowModel = { expression: ExpressionElement; allowFlipBits: boolean; label: string; - bitSize: number; + maxBitSize: number; } export default class BitwiseResultViewModel { @@ -40,7 +39,7 @@ export default class BitwiseResultViewModel { } static buildBitwiseOperation (expr : BitwiseOperationExpression, config : Config) { - + var op = expr.children[0], i = 0, len = expr.children.length, ex, m = new BitwiseResultViewModel(config); @@ -88,7 +87,7 @@ export default class BitwiseResultViewModel { expression: expr, allowFlipBits: this.allowFlipBits, label: '', - bitSize: expr.bitSize(), + maxBitSize: expr.value.maxBitSize, }); }; @@ -105,7 +104,7 @@ export default class BitwiseResultViewModel { label: this.getLabel(resultNumber), expression: expr.operand, allowFlipBits: this.allowFlipBits, - bitSize: resultNumber.bitSize() + maxBitSize: resultNumber.value.maxBitSize }); }; @@ -119,7 +118,7 @@ export default class BitwiseResultViewModel { expression: resultExpr, allowFlipBits: false, label: '', - bitSize: resultExpr.bitSize() + maxBitSize: resultExpr.value.maxBitSize }); }; @@ -132,7 +131,7 @@ export default class BitwiseResultViewModel { expression: expr, allowFlipBits: false, label: '', - bitSize: expr.bitSize() + maxBitSize: expr.value.maxBitSize }); }; diff --git a/src/expression/engine.test.ts b/src/expression/engine.test.ts deleted file mode 100644 index 3c2c234..0000000 --- a/src/expression/engine.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import ScalarValue from "./ScalarValue"; -import engine from "./engine"; - -describe('evaluate', () => { - - it('treats differently big ints and regular numbers', () => { - const bigResult = engine.applyOperator(new ScalarValue(BigInt(2147483647)), "<<", new ScalarValue(BigInt(1))); - const result = engine.applyOperator(new ScalarValue(2147483647), "<<", new ScalarValue(1)); - - expect(bigResult.value.toString()).toBe("4294967294"); - expect(result.value.toString()).toBe("-2"); - }); - - it("can execute all operators without errors", () => { - - const operators = [">>", "<<", "|", "&", "^"]; - - // >>> not supported by BigInt - expect(() => engine.applyOperator(new ScalarValue(1), ">>>", new ScalarValue(2))).not.toThrow(); - - operators.forEach(o => { - expect(() => engine.applyOperator(new ScalarValue(BigInt(1)), o, new ScalarValue(BigInt(2)))).not.toThrow(); - expect(() => engine.applyOperator(new ScalarValue(1), o, new ScalarValue(2))).not.toThrow(); - }); - - }); - - it('promotes either of operands to BigInt if the other one is', () => { - const bint = new ScalarValue(BigInt(1)); - const int = new ScalarValue(1); - - const rshift = engine.applyOperator(bint, ">>", int); - expect(rshift.isBigInt()).toBe(true); - expect(rshift.value.toString()).toBe('0'); - - const lshift = engine.applyOperator(int, "<<", bint); - expect(lshift.isBigInt()).toBe(true); - expect(lshift.value.toString()).toBe('2'); - }); - -}); \ No newline at end of file diff --git a/src/expression/engine.ts b/src/expression/engine.ts deleted file mode 100644 index 0f24bf7..0000000 --- a/src/expression/engine.ts +++ /dev/null @@ -1,44 +0,0 @@ -import calc from "../core/calc"; -import { JsNumber, asBoundedNumber } from "../core/types"; -import ScalarValue from "./ScalarValue"; - -const engine = { - applyNotOperator(operand: ScalarValue) : ScalarValue { - return new ScalarValue(~operand.value, operand.base); - }, - applyOperator(op1 : ScalarValue, operator: string, op2 : ScalarValue) : ScalarValue { - - const result = evalute(op1.value, operator, op2.value); - - return new ScalarValue(result, op2.base); - } -}; - -function evalute(op1 : JsNumber, operator: string, op2 : JsNumber) : JsNumber{ - const o1 = equalizeType(op2, op1); - const o2 = equalizeType(op1, op2); - - const a = o1 as any; - const b = o2 as any; - - switch(operator) { - case ">>": return (a >> b) as (JsNumber); - case ">>>": return (a >>> b) as (JsNumber); - case "<<": return calc.rshift(asBoundedNumber(o1), o2).value; - case "&": return (b & a) as (JsNumber); - case "|": return (b | a) as (JsNumber); - case "^": return (b ^ a) as (JsNumber); - default: throw new Error(operator + " operator is not supported"); - } -} - -function equalizeType(source : JsNumber, dest : JsNumber) : JsNumber { - - return typeof source == 'bigint' && typeof dest != 'bigint' - ? BigInt(dest) - : dest; -} - - - -export default engine; \ No newline at end of file diff --git a/src/expression/expression.test.ts b/src/expression/expression.test.ts index e710e2c..a59495d 100644 --- a/src/expression/expression.test.ts +++ b/src/expression/expression.test.ts @@ -1,15 +1,16 @@ import { parser, ListOfNumbersExpression, BitwiseOperationExpression, ScalarValue, BitwiseOperator } from "./expression"; +import { random } from "../core/utils"; +import { INT32_MAX_VALUE } from "../core/const"; describe("expression parser", () => { - it("pares list of number expression", () => { + it("parses list of number expression", () => { var result = parser.parse("1 2 3"); expect(result).toBeInstanceOf(ListOfNumbersExpression); }); it("doesn't list of numbers in case of bad numbers", () => { expect(parser.parse("1 2 z")).toBeNull(); - //expect(parser.parse("-")).toBeNull(); expect(parser.parse("")).toBeNull(); }); @@ -38,7 +39,7 @@ describe("expression parser", () => { expect(first).toBeInstanceOf(ScalarValue); - expect((first as ScalarValue).value).toBe(1); + expect((first as ScalarValue).value.toString()).toBe("1"); expect(second).toBeInstanceOf(BitwiseOperator); var secondOp = second as BitwiseOperator; @@ -53,4 +54,122 @@ describe("expression parser", () => { var result = parser.parse("1|~2") as BitwiseOperationExpression; expect(result.children.length).toBe(2); }); -}) \ No newline at end of file +}); + +describe("comparison with nodejs engine", () => { + + it('set 32-bit', () => { + + const inputs = [ + "1485578196>>14", + "921979543<<31", + "1123|324", + "213&9531", + "120^442161", + "1<<7", + "2>>>8", + "2<<7" + ]; + + inputs.forEach(i => testBinary(i, i)); + }); + + it('random: two inbary strings 64-bit', () => { + + const signs = ["|", "&", "^", "<<", ">>", ">>>"] + + for(var i =0; i<1000; i++){ + + const sign = signs[random(0, signs.length-1)]; + const isShift = sign.length > 1; + const op1 = random(-INT32_MAX_VALUE, INT32_MAX_VALUE); + const op2 = isShift ? random(0, 31) : random(-INT32_MAX_VALUE, INT32_MAX_VALUE); + + const input = op1.toString() + sign + op2.toString(); + + testBinary(input, input); + } + }); + + it('random: 64 and 32-bit', () => { + + for(var i =0; i<1000; i++){ + + const num = random(-Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); + const actualInput = "~" + num.toString(); + const expectedInput = num > INT32_MAX_VALUE ? `~BigInt("${num}")` : actualInput; + const expected = eval(expectedInput).toString(); + + let actual = ""; + + try + { + const expr = parser.parse(actualInput) as BitwiseOperationExpression; + const bo = (expr.children[0] as BitwiseOperator); + const res = bo.evaluate(); + actual = res.value.toString(); + + if(actual != expected) { + const uop = bo.getUnderlyingScalarOperand(); + console.log(`Expected:${expectedInput}\nActual:${actualInput}\n${uop.value} ${uop.value.maxBitSize}\n${res.value} ${typeof res.value} ${res.value.maxBitSize}`) + } + } + catch(err) + { + + console.log(`Error:\nExpected:${expectedInput}\nActual:${actualInput}\n${typeof actualInput}`); + + throw err; + } + + expect(actual).toBe(expected); + } + }); + + it('random: two inbary strings 64-bit', () => { + + const signs = ["|", "&", "^"] + + for(var i =0; i<1000; i++){ + const sign = signs[random(0, signs.length-1)]; + const isShift = sign.length > 1; + const op1 = random(-Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); + const op2 = isShift ? random(0, 63) : Number.MAX_SAFE_INTEGER; + + const actualInput = `${op1}l${sign}${op2}l`; + const expectedInput = `BigInt("${op1}")${sign}BigInt("${op2}")`; + + testBinary(expectedInput, actualInput); + } + }); + + function testBinary(expectedInput:string, actualInput: string) { + + const expected = eval(expectedInput).toString(); + + let actual = ""; + + try + { + var expr = parser.parse(actualInput) as BitwiseOperationExpression; + + var op1 = expr.children[0] as ScalarValue; + var op2 = expr.children[1] as BitwiseOperator; + + actual = op2.evaluate(op1).value.toString(); + const equals = actual === expected; + + if(!equals) + { + console.log(`Expected:${expectedInput}\n$Actual:${actualInput}\nop1:${typeof op1.value}\nop2:${typeof op2.getUnderlyingScalarOperand().value}`); + } + } + catch(err) + { + console.log(`Error:\nExpected:${expectedInput}\nActual:${actualInput}\n${typeof actualInput}`); + throw err; + } + + expect(actual).toBe(expected); + } +}); \ No newline at end of file diff --git a/src/expression/expression.ts b/src/expression/expression.ts index ab3d15a..dcfe3d1 100644 --- a/src/expression/expression.ts +++ b/src/expression/expression.ts @@ -3,6 +3,8 @@ import BitwiseOperator from './BitwiseOperator' import ListOfNumbersExpression from './ListOfNumbersExpression'; import BitwiseOperationExpression from './BitwiseOperationExpression'; import { Expression, ExpressionElement } from './expression-interfaces'; +import { numberParser, numberRegexString } from './numberParser'; +import { parse } from 'path'; export { default as ScalarValue } from './ScalarValue'; export { default as BitwiseOperator } from './BitwiseOperator'; @@ -16,6 +18,7 @@ interface IExpressionParserFactory { class ExpressionParser { factories: IExpressionParserFactory[]; + constructor() { this.factories = []; }; @@ -36,6 +39,7 @@ class ExpressionParser { var i = 0, l = this.factories.length, factory; for(;i p.length > 0) - .map(p => ScalarValue.tryParse(p)) - .filter(n => n == null) + .map(p => numberParser.caseParse(p)) + .filter(n => n == false) .length == 0; }; @@ -70,7 +74,7 @@ class ListOfNumbersExpressionFactory implements IExpressionParserFactory const numbers = input.split(' ') .filter(p => p.length > 0) - .map(m => ScalarValue.parse(m)); + .map(m => parseScalarValue(m)); return new ListOfNumbersExpression(input, numbers); } @@ -81,8 +85,8 @@ class BitwiseOperationExpressionFactory implements IExpressionParserFactory { regex: RegExp; constructor() { - this.fullRegex = /^((<<|>>|>>>|\||\&|\^)?(~?-?([b,x,l,L,a-f,0-9]+)))+$/; - this.regex = /(<<|>>|>>>|\||\&|\^)?(~?-?(?:[b,x,l,L,a-f,0-9]+))/g; + this.fullRegex = /^((<<|>>|>>>|\||\&|\^)?(~?-?([b,x,l,s,u,a-f,0-9]+)))+$/i; + this.regex = /(<<|>>|>>>|\||\&|\^)?(~?-?(?:[b,x,l,s,u,,a-f,0-9]+))/gi; } canCreate (input: string) : boolean { @@ -104,17 +108,19 @@ class BitwiseOperationExpressionFactory implements IExpressionParserFactory { return new BitwiseOperationExpression(normalizedString, operands) }; - parseMatch (m:any): ExpressionElement { + parseMatch (m:RegExpExecArray): ExpressionElement { + var input = m[0], operator = m[1], num = m[2]; var parsed = null; + if(num.indexOf('~') == 0) { - parsed = new BitwiseOperator(ScalarValue.parse(num.substring(1)), '~'); + parsed = new BitwiseOperator(parseScalarValue(num.substring(1)), '~'); } else { - parsed = ScalarValue.parse(num); + parsed = parseScalarValue(num); } if(operator == null) { @@ -129,6 +135,13 @@ class BitwiseOperationExpressionFactory implements IExpressionParserFactory { }; } +function parseScalarValue(input : string) : ScalarValue { + const n = numberParser.parse(input); + var sv = new ScalarValue(n.value, n.base); + if(sv.value.maxBitSize != n.value.maxBitSize) throw new Error("Gotcha!"); + return sv; +} + var parser = new ExpressionParser(); parser.addFactory(new ListOfNumbersExpressionFactory()); parser.addFactory(new BitwiseOperationExpressionFactory()); diff --git a/src/expression/numberParser.test.ts b/src/expression/numberParser.test.ts index 87cc3e4..5ccb9e6 100644 --- a/src/expression/numberParser.test.ts +++ b/src/expression/numberParser.test.ts @@ -1,3 +1,5 @@ +import exp from 'constants'; +import { asIntN } from '../core/utils'; import {numberParser, ParsedNumber} from './numberParser'; describe("parser", () => { @@ -7,12 +9,25 @@ describe("parser", () => { expect(result).not.toBeNull(); var number = result as ParsedNumber; - expect(number.value.value).toBe(10); + expect(number.value.maxBitSize).toBe(32); + expect(asIntN(number.value.num())).toBe(10); expect(number.base).toBe('dec'); expect(number.input).toBe('10'); }); - it('parses bigint numbers', () => { + it('parses negative numbers', () => { + expect(numberParser.parse('-1')?.value.num()).toBe(-1); + expect(numberParser.parse('-0b10')?.value.num()).toBe(-2); + expect(numberParser.parse('-0x10')?.value.num()).toBe(-16); + }); + + it('parses 64-bit numbers by size', () => { + const dec = numberParser.parse('3433374389036042'); + expect(dec?.value.toString()).toBe('3433374389036042'); + expect(dec?.value.maxBitSize).toBe(64); + }); + + it('parses 64-bit numbers with L notation', () => { const dec = numberParser.parse('10L'); expect(dec).not.toBeNull(); @@ -20,25 +35,9 @@ describe("parser", () => { expect(typeof dec?.value.value).toBe("bigint"); expect(dec?.base).toBe('dec'); expect(dec?.input).toBe('10L'); - - const bin = numberParser.parse('0b10l'); - expect(bin).not.toBeNull(); - - expect(bin?.value.value).toBe(BigInt(2)); - expect(typeof bin?.value.value).toBe("bigint"); - expect(bin?.base).toBe('bin'); - expect(bin?.input).toBe('0b10l'); - - const hex = numberParser.parse('0xfL'); - expect(hex).not.toBeNull(); - - expect(hex?.value.value.toString()).toBe(BigInt(15).toString()); - expect(typeof hex?.value.value).toBe("bigint"); - expect(hex?.base).toBe('hex'); - expect(hex?.input).toBe('0xfL'); + expect(dec?.value.maxBitSize).toBe(64); }); - it('switches to bigint if value exceeds max safe int', () => { const unsafeInt = BigInt(Number.MAX_SAFE_INTEGER)+BigInt(1); @@ -76,7 +75,8 @@ describe("parser", () => { expect(result).not.toBeNull(); var number = result as ParsedNumber; - expect(number.value.value).toBe(171); + expect(number.value.maxBitSize).toBe(32); + expect(number.value.num()).toBe(171); expect(number.base).toBe('hex'); expect(number.input).toBe('0xab'); }); @@ -86,39 +86,82 @@ describe("parser", () => { expect(result).not.toBeNull(); var number = result as ParsedNumber; - expect(number.value.value).toBe(6); + expect(number.value.num()).toBe(6); expect(number.base).toBe('bin'); expect(number.input).toBe('0b0110'); }); it('returns null on bad inputs', () => { - expect(numberParser.parse('abc')).toBeNull(); - expect(numberParser.parse('')).toBeNull(); + expect(numberParser.caseParse('abc')).toBe(false); + expect(numberParser.caseParse('')).toBe(false); + expect(numberParser.caseParse('-1u')).toBe(true); + expect(() => numberParser.parse('abc')).toThrowError("abc is not a number"); + expect(() => (numberParser.parse(''))).toThrowError('input is null or empty'); }); it('parses big int', () => { var v = numberParser.parse('1l')?.value - expect(typeof v?.value).toBe("bigint"); - expect(v?.value.toString()).toBe("1"); + expect(v?.num()).toBe(1); }); - xit('parses single', () => { + it('fits usigned int32 max value into 32-bit data type', () => { + const n1 = numberParser.parse("4294967295u"); + const n2 = numberParser.parse("4294967296u"); + + expect(n1?.value.maxBitSize).toBe(32); + expect(n2?.value.maxBitSize).toBe(64); + expect(n1?.value.signed).toBe(false); + expect(n2?.value.signed).toBe(false); + }) + + it('parses single', () => { var v = numberParser.parse('1s')?.value - expect(typeof v?.value).toBe("number"); expect(v?.maxBitSize).toBe(16); - expect(v?.value.toString()).toBe("1"); + expect(v?.num()).toBe(1); + expect(v?.signed).toBe(true); - var v2 = numberParser.parse('1i8')?.value - expect(v2).toEqual(v); + //var v2 = numberParser.parse('1i8')?.value + //expect(v2).toEqual(v); }); - xit('parses byte', () => { - var v = numberParser.parse('1b')?.value - expect(typeof v?.value).toBe("number"); - expect(v?.maxBitSize).toBe(16); - expect(v?.value.toString()).toBe("1"); + it('cannot parse negative usigned', () => { + expect(() => numberParser.parse('-1u')).toThrowError("-1u unsigned integer cannot be negative"); + }); - var v2 = numberParser.parse('1i16')?.value - expect(v2).toEqual(v); + it('parses usigned single', () => { + var v = numberParser.parse('1us')?.value + expect(v?.maxBitSize).toBe(16); + expect(v?.num()).toBe(1); + expect(v?.signed).toBe(false); + }); + + it('parses usigned int32', () => { + var v = numberParser.parse('1u')?.value + expect(v?.maxBitSize).toBe(32); + expect(v?.num()).toBe(1); + expect(v?.signed).toBe(false); + }); + + it('parses usigned byte', () => { + var v = numberParser.parse('1ub')?.value + expect(v?.maxBitSize).toBe(8); + expect(v?.num()).toBe(1); + expect(v?.signed).toBe(false); + }); + + it('parses usigned long', () => { + var v = numberParser.parse('1ul')?.value + expect(v?.maxBitSize).toBe(64); + expect(v?.num()).toBe(1); + expect(v?.signed).toBe(false); + }); + + it('parses byte', () => { + var v = numberParser.parse('1b')?.value + expect(v?.maxBitSize).toBe(8); + expect(v?.num()).toBe(1); + + //var v2 = numberParser.parse('1i16')?.value + //expect(v2?.num()).toEqual(v?.num()); }); }); \ No newline at end of file diff --git a/src/expression/numberParser.ts b/src/expression/numberParser.ts index ed16998..10053e9 100644 --- a/src/expression/numberParser.ts +++ b/src/expression/numberParser.ts @@ -1,94 +1,83 @@ -import { INT32_MAX_VALUE, INT32_MIN_VALUE } from "../core/const"; +import { INT32_MAX_VALUE, UINT32_MAX_VALUE } from "../core/const"; import { NumberBase } from "../core/formatter"; -import { BoundedNumber, asBoundedNumber } from "../core/types"; +import { Integer } from "../core/Integer"; -// byte -i8 or b -// single - i16 or s - -const decimalRegex = /^-?\d+[l,L]?$/; -const hexRegex = /^-?0x[0-9,a-f]+[l,L]?$/i; -const binRegex = /^-?0b[0-1]+[l,L]?$/i; - -interface ParserConfig { - regex: RegExp, - base: NumberBase, - parse: (input: string) => BoundedNumber -} +const numberRegexString = "-?([0-9]+|0b[0-1]+|0x[0-9,a-f]+)(l|s|b|ul|us|ub|u)?"; +const numberRegexFullString = "^"+numberRegexString+"$" export interface ParsedNumber { - value: BoundedNumber; + value: Integer; base: NumberBase; input: string; } -var knownParsers : ParserConfig[] = [ - { regex: decimalRegex, base: 'dec', parse:(s) => parseIntSafe(s, 10) }, - { regex: hexRegex, base: 'hex', parse:(s) => parseIntSafe(s, 16)}, - { regex: binRegex, base: 'bin', parse:(s) => parseIntSafe(s, 2) }]; - - class NumberParser { + + numberRegexString: string; - parsers: ParserConfig[]; - - constructor(parsers: ParserConfig[]) + constructor() { - this.parsers = parsers; + this.numberRegexString = numberRegexString; } - parse (input : string) : ParsedNumber | null { - return this.parsers.map(p => this.applyParser(p, input)).reduce((c, n) => c || n); - }; + caseParse(input : string) { + const regex = new RegExp(numberRegexFullString); + return regex.test(input); + } - parseOperator (input: string) : string | null { - var m = input.match(input); + parse (input : string) : ParsedNumber { + + if(input.length == 0) throw new Error("input is null or empty"); + + const regex = new RegExp(numberRegexFullString, "i"); - if(m == null || m.length == 0) { - return null; - } + const m = regex.exec(input); - return m[0]; - }; + if(m == null || m.length == 0) + throw new Error(input + " is not a number"); - applyParser(parser : ParserConfig, rawInput: string) : ParsedNumber | null { - - if(!parser.regex.test(rawInput)) { - return null; - } - - var value = parser.parse(rawInput); - - return { + const value = parseInteger(m[0], m[1], m[2] || ''); + + return { value: value, - base: parser.base, - input: rawInput - } + base: getBase(input), + input: input + } + }; +} + +function parseInteger(input : string, numberPart: string, suffix: string) : Integer { + + const isNegative = input.startsWith('-'); + let num = BigInt(numberPart); + const signed = !suffix.startsWith('u'); + + if(!signed && isNegative) + throw new Error(input + " unsigned integer cannot be negative"); + + const size = getSizeBySuffix(suffix, num, signed); + return new Integer(isNegative ? -num : num, size, signed); +} + +function getSizeBySuffix(suffix: string, value : bigint, signed: boolean) { + + const max32 = signed ? INT32_MAX_VALUE : UINT32_MAX_VALUE; + + switch(suffix.replace('u', '').toLowerCase()) { + case 'l': return 64; + case 's': return 16; + case 'b': return 8; + default: return value > max32 ? 64 : 32; } } -const MAX_SAFE_INTn = BigInt(INT32_MAX_VALUE); -const MIN_SAFE_INTn = BigInt(INT32_MIN_VALUE); +function getBase(input: string): NumberBase { -function parseIntSafe(input : string, radix: number) : BoundedNumber { - - const bigIntStr = input.replace('-', '').replace('l', '').replace('L', ''); - let bigInt = BigInt(bigIntStr); - const isNegative = input.startsWith('-'); - const isBigInt = input.toLowerCase().endsWith('l'); - - if(isNegative) bigInt *= BigInt(-1); - - if(isBigInt) return asBoundedNumber(bigInt); - - if(bigInt > MAX_SAFE_INTn) - return asBoundedNumber(bigInt); - - if(bigInt < MIN_SAFE_INTn) - return asBoundedNumber(bigInt); - - return asBoundedNumber(parseInt(input.replace(/0(x|b)/, ''), radix)); + if(input.indexOf('0b') > -1) return 'bin'; + if(input.indexOf('0x') > -1) return 'hex'; + return 'dec'; } -const numberParser = new NumberParser(knownParsers); +const numberParser = new NumberParser(); -export {numberParser}; \ No newline at end of file +export {numberParser, numberRegexString}; diff --git a/src/index.css b/src/index.css index 9b62d3d..ec7032b 100644 --- a/src/index.css +++ b/src/index.css @@ -5,7 +5,7 @@ html { height: 100% } font-family: Verdana; font-size: 0.8em; margin: 0; - padding: 20px 100px 0 100px; + padding: 0px 30px; height: 100%; overflow: auto; position: relative; @@ -17,7 +17,17 @@ code { font-size: 1.2em; font-weight: bold; } .header-cmd { color: #c5c5c5 } .mono { font-family: monospace; font-size: 1.3em } -.expressionInput { width: 500px; padding: 3px; border: none; outline: none; } +.expressionInput { + padding: 3px; + outline: none; + border: none; + border-bottom: solid 1px rgba(255, 255, 255, 0.5); + z-index: 100; + padding-left: 15px; + padding-bottom: 5px; + width: 600px; + background: ragba(0, 0, 0, 0); +} .hidden { display: none;} @@ -46,14 +56,11 @@ code { font-size: 1.2em; font-weight: bold; } .hex .prefix { display: inline; } -.help { padding: 10px; } -.help ul { list-style-type: none; margin: 0; padding: 0; } -.help p { margin-top: 0.5em } - .indicator { padding: 0px 5px; background: transparent; border: none; cursor: pointer; vertical-align: middle; } .error { color: maroon; } .soft { opacity: 0.7 } + .small-text { font-size: 0.8em;} #view { padding: 10px} @@ -70,13 +77,15 @@ code { font-size: 1.2em; font-weight: bold; } .light .on { color: #121212; } .light .prefix { color: #888} .light .other { color: #bbb } -.light .hashLink, .light .hashLink:visited { color: #ddd; } +.light .hashLink, .light .hashLink:visited { color: #aaa; } .light .hashLink:hover { color: #888 } .light ul.top-links li:hover { background: #ddd } .light .error { color: #da586d } .light button.btn { color: black} .light button.btn:hover { background: #ddd} .light button.btn:disabled { color: #888; background-color: inherit; } +.light .accent1 { color:rgb(56, 161, 103)} +.light .expressionInput { border-bottom: solid 1px rgba(0, 0, 0, 0.5);} /* Dark */ .dark { background: #121212; color: white;} @@ -87,7 +96,7 @@ code { font-size: 1.2em; font-weight: bold; } .dark .on { color: white; } .dark .zero { color: #999;} .dark .prefix { color: #999} -.dark .other { color: #444;} +.dark .other { color: #777;} .dark .hashLink, .dark .hashLink:visited { color: #333 } .dark .hashLink:hover { color: #999 } .dark ul.top-links li:hover { background: #333 } @@ -95,6 +104,7 @@ code { font-size: 1.2em; font-weight: bold; } .dark button.btn { color: white} .dark button.btn:hover { background: #333} .dark button.btn:disabled { color: #999; background-color: inherit; } +.dark .accent1 { color:mediumseagreen}ч /* Midnight Theme @@ -118,6 +128,7 @@ code { font-size: 1.2em; font-weight: bold; } .midnight button.btn { color: white} .midnight button.btn:hover { background: #132537} .midnight button.btn:disabled { color: #85a0ad; background-color: inherit; } +.midnight .accent1 { color:mediumseagreen} button { border: none; @@ -150,4 +161,23 @@ button:focus {outline:0;} @media (max-width: 450px) { .expressionInput { width: 350px; } +} + + +#output { + padding: 30px 30px; +} + +.input-p { + display:inline; + z-index: 101; + position: absolute; + margin-right: -10px; + margin-top: 3px; + opacity: 0.5; +} + +.debug-indicators { + left: 1px; + opacity: 0.5; } \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 6e63049..177dfd5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,5 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import './index.css'; -import reportWebVitals from './reportWebVitals'; import cmd, { CommandInput } from './shell/cmd'; import AppRoot from './shell/components/AppRoot'; import log from 'loglevel'; diff --git a/src/networking/components/VpcView.css b/src/networking/components/VpcView.css index a7d1e34..1d878c2 100644 --- a/src/networking/components/VpcView.css +++ b/src/networking/components/VpcView.css @@ -12,7 +12,7 @@ } .vpc-view .host-part { - color: teal; + color: darkcyan; } .vpc-view .subnet-part { diff --git a/src/shell/components/AboutResultView.tsx b/src/shell/components/AboutResultView.tsx index 7bde7b8..6059abc 100644 --- a/src/shell/components/AboutResultView.tsx +++ b/src/shell/components/AboutResultView.tsx @@ -4,7 +4,7 @@ function AboutResultView() { return

Created by Borys Levytskyi. Please give it a like if BitwiseCmd has helped you in your work.

-

If you have an idea, suggestion or you've spotted a bug here, please send it to bitwisecmd@gmail.com or tweet on @BitwiseCmd. Your feedback is greatly appreciated.

+

If you have an idea or, suggestion or you've spotted a bug here, please send it to bitwisecmd@gmail.com. Your feedback is greatly appreciated.

Project on GitHub

; }; diff --git a/src/shell/components/AppRoot.tsx b/src/shell/components/AppRoot.tsx index 1a2b584..692e8c0 100644 --- a/src/shell/components/AppRoot.tsx +++ b/src/shell/components/AppRoot.tsx @@ -49,7 +49,11 @@ export default class AppRoot extends React.Component } render() { - return
+ + const enableNewUi = this.props.appState.env != 'prod' || true; + const newUi = enableNewUi ? 'new-ui' : ''; + + return

BitwiseCmd diff --git a/src/shell/components/HelpResultView.css b/src/shell/components/HelpResultView.css index 94f770a..08188a8 100644 --- a/src/shell/components/HelpResultView.css +++ b/src/shell/components/HelpResultView.css @@ -1,4 +1,7 @@ -.help .section {margin-bottom:10px;} +.help ul { list-style-type: none; margin: 0; margin-top: 0.2em; padding: 0; } +.help p { margin-top: 0.5em } +.help .section {padding: 1em;} .help .panel-container {overflow: hidden;} -.help .left-panel {float:left; margin-right: 20px;} -.help .right-panel {float:left;} \ No newline at end of file +.help .left-panel {float:left; margin-right: 1em;} +.help .right-panel {float:left; } +.help .section-title {font-weight: bold;} diff --git a/src/shell/components/HelpResultView.tsx b/src/shell/components/HelpResultView.tsx index 1ce38a6..5bd681e 100644 --- a/src/shell/components/HelpResultView.tsx +++ b/src/shell/components/HelpResultView.tsx @@ -1,6 +1,7 @@ import React from 'react'; import CommandLink from '../../core/components/CommandLink'; import './HelpResultView.css'; +import { INT32_MAX_VALUE, INT32_MIN_VALUE } from '../../core/const'; function HelpResultView() { @@ -8,23 +9,23 @@ function HelpResultView() {
- Bitwise Calculation Commands +
Bitwise Calculation Commands
  • — type bitwise expression to see the result in binary
  • — type one or more numbers to see their binary representations
- IP Address & Networking Commands +
IP Address & Networking Commands
    -
  • — enter a single or multiple IP addresses (separated by space) to see their binary representation
  • +
  • — enter single or multiple IP addresses (separated by space) to see their binary representation
  • — subnet mask notations are supported as well
  • — display information about a subnet (network address, broadcast address, etc.)
  • — see how VPC network address bits are divided between VPC address, Subnets, and Hosts
- Color Theme Commands +
Color Theme Commands
  • — set the Light color theme
  • — set the Dark color theme
  • @@ -32,7 +33,7 @@ function HelpResultView() {
- Other Commands +
Other Commands
  • — clear output pane
  • — display this help
  • @@ -45,12 +46,7 @@ function HelpResultView() {
- Supported Bitwise Operations
- - - as implemented in the JavaScript engine of your browser. If it cannot do it, BitwiseCmd cannot do it. - - +
Supported Bitwise Operations
  • & — bitwise AND
  • | — bitwise inclusive OR
  • @@ -61,10 +57,22 @@ function HelpResultView() {
  • >>> — zero-fill right shift
+
+
Supported Number Types NEW
+

+ BitiwseCmd no longer uses the browser's JavaScript engine for the execution of bitwise operations. It has its own calculator implementation which brings supports bitwise operations on the following signed and unsigned data types: +

+
    +
  • 8-bit integer - numbers entered with b or ub suffixes for signed and unsigned versions respectively (e.g. ).
  • +
  • 64-bit integer - numbers entered with s or us suffixes for signed and unsigned versions respectively (e.g. ).
  • +
  • 32-bit integer - numbers entered without suffixes that fall in range of {INT32_MIN_VALUE} and {INT32_MAX_VALUE}. Use u suffix to denote an unsigned version of 32-bit integer. This is a default number type.
  • +
  • 64-bit integer - numbers entered without suffixes and exceed the 32-bit range or entered with and and ul suffixes for signed and unsigned versions respectively (e.g. ).
  • +
+
Tip

- You can click on bits to flip them in number inputs (e.g. ) or IP addresses (e.g. ) + You can click on bits to flip them in number inputs (e.g. ) or IP addresses (e.g. ).

diff --git a/src/shell/components/InputBox.tsx b/src/shell/components/InputBox.tsx index 3c39185..ea61f11 100644 --- a/src/shell/components/InputBox.tsx +++ b/src/shell/components/InputBox.tsx @@ -24,13 +24,16 @@ export default class InputBox extends React.Component { } render() { - return + > + { this.nameInput = input; }} onKeyUp={e => this.onKeyUp(e)} onKeyDown={e => this.onKeyDown(e)} className="expressionInput mono" placeholder="type an expression like '1>>2' or 'help' " - autoComplete="off"/>; + autoComplete="off"/> + } onKeyUp(e: any) { diff --git a/src/shell/components/TopLinks.tsx b/src/shell/components/TopLinks.tsx index 6b44694..74bde46 100644 --- a/src/shell/components/TopLinks.tsx +++ b/src/shell/components/TopLinks.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import { faEnvelope, faDonate } from "@fortawesome/free-solid-svg-icons"; -import { faTwitter, faGithub} from "@fortawesome/free-brands-svg-icons"; +import { faGithub} from "@fortawesome/free-brands-svg-icons"; import './TopLinks.css'; import cmd from '../cmd'; @@ -16,9 +16,6 @@ function TopLinks() {
  • github
  • -
  • - twitter -
  • idea or feedback
  • diff --git a/src/shell/components/WhatsNewResultView.css b/src/shell/components/WhatsNewResultView.css index 3697646..2501bae 100644 --- a/src/shell/components/WhatsNewResultView.css +++ b/src/shell/components/WhatsNewResultView.css @@ -1,2 +1,3 @@ .changelog .item { margin-top: 2em; } -.changelog .item-new .date { font-weight: bold; text-decoration: underline;} \ No newline at end of file +.changelog .item-new .date { font-weight: bold; text-decoration: underline;} +.changelog li { padding: 5px 0} \ No newline at end of file diff --git a/src/shell/components/WhatsNewResultView.tsx b/src/shell/components/WhatsNewResultView.tsx index ddd85d8..57e31ff 100644 --- a/src/shell/components/WhatsNewResultView.tsx +++ b/src/shell/components/WhatsNewResultView.tsx @@ -7,49 +7,72 @@ function WhatsnewResultView() { return

    Changelog

    +

    + May 10th, 2023
    +

    + Behold! After a long time of inactivity, BitwiseCmd is getting an update. Here is what changed: +

    +
      +
    • Browser's JavaScript engine is no longer used for the execution of bitwise operations. + BitwiseCmd has its own shiny custom-built bitwise calculator that supports operations integer of different sizes (8,16,32, and 64 bits) as well as their signed and unsigned versions. . + This calculator tries to follow the same behavior of bitwise operations as implemented in C. + This includes shifting an integer by the number of bytes equal to its size (spoiler: you get the same number, this is undefined behavior in C. Don't believe me? Check this link).
    • +
    • A slightly improved UI
    • +
    +

    +
    +
    + May 5th, 2023

    - May 5th, 2023
    - Fixed bug with incorrect binary representation of 64 bit numbers. + Fixed bug with incorrect binary representation of 64-bit numbers.

    Jul 24th, 2021

      -
    • Added support of vpc command to see hpw VPC network address is divided bettwen VPC, Subnets and Hosts. Try it out:
    • +
    • Added support of vpc command to see how VPC network address is divided between VPC, Subnets, and Hosts. Try it out:
    • Added ability to remove individual results

    -

    Jun 16th, 2021
    - Added support of subnet command to display information about subnet ip adress such. Try it out: + Jun 16th, 2021 +

    + Added support of subnet command to display information about subnet IP address such. Try it out:

    -

    Jun 14th, 2021
    - Added support of ip addresses and subnet masks notatioans. Try them out: + Jun 14th, 2021 +

    + Added support of IP addresses and subnet mask notations. Try them out:

      -
    • Single IP address
    • +
    • A single IP address
    • Multiple IP addresses and subnet mask notations
    -

    Jun 6th, 2017
    + Jun 6th, 2017 +

    Added command. Use it for generating v4 GUIDs

    -

    May 27th, 2017
    + May 27th, 2017 +

    Added support of binary number notation (e.g. ).

    -

    May 20th, 2017
    - New theme added.

    + May 20th, 2017 +

    + A new theme was added. +

    -

    May 16th, 2017
    - Complete rewrite using React. Old implementation is available at http://bitwisecmd.com/old. Please let me know if you have problems with this release by creating issue in Github Repo.

    + May 16th, 2017 +

    + Complete rewrite using React. Please let me know if you have problems with this release by creating an issue in Github Repo. +

    ; } diff --git a/src/shell/startup.ts b/src/shell/startup.ts index ac359cf..ebb3f4c 100644 --- a/src/shell/startup.ts +++ b/src/shell/startup.ts @@ -3,7 +3,6 @@ import hash from '../core/hash'; import AppState from './AppState'; import { Env } from './interfaces'; import appStateStore from './appStateStore'; -import CommandLink from '../core/components/CommandLink'; export type StartupAppData = { appState: AppState, @@ -11,7 +10,7 @@ export type StartupAppData = { } const STARTUP_COMMAND_KEY = 'StartupCommand'; -const DEFAULT_COMMANDS = ['help', '127.0.0.1 192.168.0.0/8', '-1 -1L', '1|2&6', '4 0b1000000 0x80']; +const DEFAULT_COMMANDS = ['help', '127.0.0.1 192.168.0.1/8', '-1b -1s -1 -1l', '1|2&6', '4 0b1000000 0x80']; function bootstrapAppData() : StartupAppData { const env = window.location.host === "bitwisecmd.com" ? 'prod' : 'stage';