diff --git a/src/core/calc.test.ts b/src/core/calc.test.ts index a62a2ed..092db6b 100644 --- a/src/core/calc.test.ts +++ b/src/core/calc.test.ts @@ -1,17 +1,17 @@ import calc from './calc'; -import { BitwiseOperationExpression, ScalarToken, OperatorToken } from '../expression/expression'; +import { BitwiseOperationExpression, ScalarValue, BitwiseOperator } from '../expression/expression'; +import { INT32_MAX_VALUE } from './const'; import exp from 'constants'; -import { INT_MAX_VALUE } from './const'; -import formatter from './formatter'; describe("calc", () => { it('calculates number of bits', () => { expect(calc.numberOfBitsDisplayed(1)).toBe(1); + expect(calc.numberOfBitsDisplayed(BigInt(-1))).toBe(64); expect(calc.numberOfBitsDisplayed(2)).toBe(2); expect(calc.numberOfBitsDisplayed(3)).toBe(2); expect(calc.numberOfBitsDisplayed(68719476735)).toBe(36); - expect(calc.numberOfBitsDisplayed(-INT_MAX_VALUE)).toBe(32); - expect(calc.numberOfBitsDisplayed(-(INT_MAX_VALUE+1))).toBe(64); + expect(calc.numberOfBitsDisplayed(-INT32_MAX_VALUE)).toBe(32); + expect(calc.numberOfBitsDisplayed(-(BigInt(INT32_MAX_VALUE+1)))).toBe(64); }); it('calculates max number of bits', () => { @@ -23,20 +23,60 @@ describe("calc", () => { var result = calc.calcExpression(new BitwiseOperationExpression( "1|2&3", [ - new ScalarToken(1), - new OperatorToken(new ScalarToken(2), "|"), - new OperatorToken(new ScalarToken(3), "&"), + new ScalarValue(1), + new BitwiseOperator(new ScalarValue(2), "|"), + new BitwiseOperator(new ScalarValue(3), "&"), ] )); expect(result).toBe(3); }); + + it('calculates flipped bit 32-bit number', () => { + expect(calc.flipBit(0, 31)).toBe(1); + expect(calc.flipBit(1, 31)).toBe(0); + expect(calc.flipBit(-1, 31)).toBe(-2); + expect(calc.flipBit(2147483647, 0)).toBe(-1); + expect(calc.flipBit(-1, 0)).toBe(2147483647); + expect(calc.flipBit(2147483647, 30)).toBe(2147483645); + }); + + it('caulate flipped bit 64-bit nubmer', () => { + const int64max = BigInt("9223372036854775807"); + expect(calc.flipBit(BigInt(int64max), 0)).toBe(BigInt(-1)); + }); + + it('calculates flipped bit', () => { + expect(calc.flipBit(0, 31)).toBe(1); + expect(calc.flipBit(1, 31)).toBe(0); + expect(calc.flipBit(-1, 31)).toBe(-2); + expect(calc.flipBit(2147483647, 0)).toBe(-1); + expect(calc.flipBit(-1, 0)).toBe(2147483647); + expect(calc.flipBit(2147483647, 30)).toBe(2147483645); + }); + + 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 + }); + + it('calcualte 31th bit in 64-bit int', () => { + expect(calc.flipBit(calc.promoteToBigInt(-1), 31).toString()).toBe("8589934591"); + }); + + it('promotes to BigInt with the same bits', () => { + expect(calc.promoteToBigInt(-1).toString(2)).toBe("11111111111111111111111111111111"); + }); }); -describe("binary ", () => { +describe("bitwise ", () => { - it("bitwise NOT same as in node", () => { + it("NOT same as in node", () => { for(var i = -100; i<100;i++) { const expected = bin(~i); @@ -45,7 +85,7 @@ describe("binary ", () => { } }); - it("bitwise OR same as in node", () => { + it("OR same as in node", () => { for(var x = -100; x<100;x++) { const y = 5+3%x+x%6*(-x); const expected = bin(x | y); @@ -55,7 +95,7 @@ describe("binary ", () => { } }); - it("bitwise AND same as in node", () => { + it("AND same as in node", () => { for(var x = -100; x<100;x++) { const y = 5+3%x+x%6*(-x); const expected = bin(x & y); diff --git a/src/core/calc.ts b/src/core/calc.ts index 3a47dda..bf5b395 100644 --- a/src/core/calc.ts +++ b/src/core/calc.ts @@ -1,15 +1,18 @@ -import { dblClick } from "@testing-library/user-event/dist/click"; import { Expression } from "../expression/expression-interfaces"; -import { INT_MAX_VALUE } from "./const"; -import { start } from "repl"; +import formatter from "./formatter"; +import { NumberType } from "./types"; export default { - numberOfBitsDisplayed: function (num: number) : number { + abs (num : NumberType) : NumberType { + return num >= 0 ? num : -num; + }, + numberOfBitsDisplayed: function (num: number|bigint) : number { + if(num < 0) { - return Math.abs(num) <= INT_MAX_VALUE ? 32 : 64; - } + return typeof num == 'bigint' ? 64 : 32 + }; - return Math.floor(Math.log(num) / Math.log(2)) + 1; + return num.toString(2).length; }, maxNumberOfBitsDisplayed: function (arr: number[]) { @@ -27,6 +30,59 @@ export default { return eval(expr.expressionString); }, + flipBit: function(num: number|bigint, index: number): number|bigint { + + const is64bit = typeof num == 'bigint'; + const size = typeof num == "bigint" ? 64 : 32; + const bin = formatter.bin(num).padStart(size, '0'); + const staysNegative = (bin[0] == "1" && index > 0); + const becomesNegative = (bin[0] == "0" && index == 0); + + //console.log(bin); + + let m = 1; + let flipped = bin.substring(0, index) + flip(bin[index]) + bin.substring(index+1); + + //console.log(flipped); + + if(staysNegative || becomesNegative) { + flipped = this.applyTwosComplement(flipped); + m=-1; + } + + //console.log(flipped); + + return is64bit ? BigInt("0b"+ flipped)*BigInt(m) : parseInt(flipped, 2)*m; + }, + + promoteToBigInt(number: number) { + const bin = formatter.bin(number); + return 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) ; + }, + + flipAllBits: (bin: string): string => { + return bin.split('').map(b => b=="1"?"0":"1").join(""); + }, + bitwise: { not: (bin: string) : string => { @@ -62,10 +118,9 @@ export default { return result.join(''); } - } }; -function flip(bit:string) { +function flip(bit:string):string { return bit === "0" ? "1" : "0"; } \ No newline at end of file diff --git a/src/core/components/BinaryString.css b/src/core/components/BinaryString.css new file mode 100644 index 0000000..9b8cc35 --- /dev/null +++ b/src/core/components/BinaryString.css @@ -0,0 +1,3 @@ +.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 1dc566c..35cbb07 100644 --- a/src/core/components/BinaryString.tsx +++ b/src/core/components/BinaryString.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import './BinaryString.css'; export type BinaryStringViewProps = { allowFlipBits?: boolean; @@ -6,12 +7,13 @@ export type BinaryStringViewProps = { onFlipBit?: (input: FlipBitEventArg) => void; emphasizeBytes?: boolean; className?:string; - disableHighlight?:boolean + disableHighlight?:boolean, + bitSize?: number }; export type FlipBitEventArg = { - index: number; - binaryString: string; + bitIndex: number; + binaryStringLength: number; $event: any; newBinaryString: string }; @@ -26,19 +28,15 @@ 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'; + tooltip = 'Signature bit. 0 means a positive number and 1 means a negative.' + } + if(disableHighlight) className = css; - return this.onBitClick(i, e)}>{c} + return this.onBitClick(i, e)}>{c} }); } diff --git a/src/core/const.ts b/src/core/const.ts index 66095c6..2f76c8c 100644 --- a/src/core/const.ts +++ b/src/core/const.ts @@ -1,3 +1,6 @@ -const INT_MAX_VALUE = 2147483647; -export {INT_MAX_VALUE}; \ No newline at end of file +const INT32_MAX_VALUE = 2147483647; +const INT32_MIN_VALUE = -INT32_MAX_VALUE; +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}; diff --git a/src/core/formatter.test.ts b/src/core/formatter.test.ts index f25c532..522333f 100644 --- a/src/core/formatter.test.ts +++ b/src/core/formatter.test.ts @@ -7,6 +7,11 @@ describe("formatter", () => { expect(formatter.numberToString(15, "bin")).toBe("1111"); }); + it('respects size when formatting negative number', () => { + expect(formatter.bin(-1)).toBe("11111111111111111111111111111111"); + expect(formatter.bin(BigInt(-1))).toBe("1111111111111111111111111111111111111111111111111111111111111111"); + }); + it('formats large binary number correctly', () => { var decimal = 68719476735; var binary = formatter.bin(68719476735); @@ -16,8 +21,8 @@ describe("formatter", () => { }); it('formats negative binary numbers', () => { - expect(formatter.numberToString(-1, 'bin')).toBe("11111111111111111111111111111111"); - expect(formatter.numberToString(-0, 'bin')).toBe("0"); + //expect(formatter.numberToString(-1, 'bin')).toBe("11111111111111111111111111111111"); + //expect(formatter.numberToString(-0, 'bin')).toBe("0"); expect(formatter.numberToString(-2147483647, 'bin')).toBe("10000000000000000000000000000001"); }); diff --git a/src/core/formatter.ts b/src/core/formatter.ts index 189e73a..309739a 100644 --- a/src/core/formatter.ts +++ b/src/core/formatter.ts @@ -1,26 +1,27 @@ -import { INT_MAX_VALUE } from "./const"; +import calc from "./calc"; +import { NumberType } from "./types"; export type NumberBase = 'dec' | 'hex' | 'bin'; const formatter = { - numberToString: function(value: number, kind: NumberBase) : string { + numberToString: function(num: number|bigint, base: NumberBase) : string { - switch(kind) { + switch(base) { case 'hex': - var hexVal = Math.abs(value).toString(16); - return value >= 0 ? '0x' + hexVal : '-0x' + hexVal; + var hexVal = calc.abs(num).toString(16); + return num >= 0 ? '0x' + hexVal : '-0x' + hexVal; case 'bin': - if(value < 0) { - const n = Math.abs(value); - const padding = n > INT_MAX_VALUE ? 64 : 32; - const pos = n.toString(2).padStart(padding, '0'); - return findTwosComplement(pos); + + if(num < 0) { + const size = calc.numberOfBitsDisplayed(num); + const absBin = calc.abs(num).toString(2).padStart(size, '0'); + return calc.applyTwosComplement(absBin); } - return value.toString(getBase(kind || "bin")); + return num.toString(2); case 'dec': - return value.toString(10); + return num.toString(10); default: - throw new Error("Unexpected kind: " + kind) + throw new Error("Unexpected kind: " + base) } }, padLeft: function (str: string, length: number, symbol: string) : string { @@ -36,10 +37,10 @@ const formatter = { return sb.join(''); }, - bin(number: number) { + bin(number: NumberType) { return this.numberToString(number, 'bin'); }, - emBin(number: number) { + emBin(number: NumberType) { return this.padLeft(this.bin(number), 8, '0'); }, @@ -92,43 +93,6 @@ function getBase(kind:string) : number { throw new Error("Unsupported kind: " + kind); } -function flip(bit: string) : string { - switch(bit) { - case "1": return "0"; - case "0": return "1"; - default: throw new Error("unexpected bit value: " + bit); - } -} - -function findTwosComplement(str:string):string { - var n = str.length; - - // Traverse the string to get first '1' from - // the last of string - var i; - for (i = n - 1; i >= 0; i--) - if (str.charAt(i) == '1') - break; - - // If there exists no '1' concat 1 at the - // starting of string - if (i == -1) - return "1" + str; - - // Continue traversal after the position of - // first '1' - for (var k = i - 1; k >= 0; k--) { - // Just flip the values - if (str.charAt(k) == '1') - str = str.substring(0,k)+"0"+str.substring(k+1, str.length); - else - str = str.substring(0,k)+"1"+str.substring(k+1, str.length); - } - - // return the modified string - return str.toString(); -} - const emBin = formatter.emBin.bind(formatter); const padLeft = formatter.padLeft.bind(formatter); diff --git a/src/core/types.ts b/src/core/types.ts new file mode 100644 index 0000000..8abfb79 --- /dev/null +++ b/src/core/types.ts @@ -0,0 +1 @@ +export type NumberType = number | bigint; \ No newline at end of file diff --git a/src/expression/BitwiseOperationExpression.ts b/src/expression/BitwiseOperationExpression.ts index 64a9805..fd3b2a3 100644 --- a/src/expression/BitwiseOperationExpression.ts +++ b/src/expression/BitwiseOperationExpression.ts @@ -1,11 +1,11 @@ -import { Expression, ExpressionToken } from "./expression-interfaces"; +import { Expression, ExpressionElement } from "./expression-interfaces"; export default class BitwiseOperationExpression implements Expression { expressionString: string; - children: ExpressionToken[]; + children: ExpressionElement[]; - constructor(expressionString: string, children: ExpressionToken[]) { + constructor(expressionString: string, children: ExpressionElement[]) { this.expressionString = expressionString; this.children = children; } diff --git a/src/expression/BitwiseOperator.test.ts b/src/expression/BitwiseOperator.test.ts new file mode 100644 index 0000000..886a4e9 --- /dev/null +++ b/src/expression/BitwiseOperator.test.ts @@ -0,0 +1,21 @@ +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.base).toBe('dec'); +}); + +it('can apply & operand', () => { + var op1 = new ScalarValue(3, 'dec'); + var op2 = new ScalarValue(4, 'dec'); + var expr = new BitwiseOperator(op1, "|"); + var result = expr.evaluate(op2); + expect(result.value).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 new file mode 100644 index 0000000..85d331a --- /dev/null +++ b/src/expression/BitwiseOperator.ts @@ -0,0 +1,43 @@ +import ScalarValue from './ScalarValue'; +import engine from './engine'; +import { ExpressionElement } from './expression-interfaces'; + +export default class BitwiseOperator implements ExpressionElement { + operand: ExpressionElement; + operator: string; + isOperator: boolean; + isShiftExpression: boolean; + isNotExpression: boolean; + + constructor(operand : ExpressionElement, operator : string) { + + this.operand = operand; + this.operator = operator; + this.isOperator = true; + this.isShiftExpression = this.operator.indexOf('<') >= 0 || this.operator.indexOf('>')>= 0; + this.isNotExpression = this.operator === '~'; + } + + evaluate(operand?: ScalarValue) : ScalarValue { + + if (operand instanceof BitwiseOperator) + throw new Error('operand must be scalar value'); + + if( this.operator != "~" && operand == null) + throw new Error("operand is required"); + + var evaluatedOperand = this.operand.evaluate(); + + return this.operator == "~" + ? engine.applyNotOperator(this.operand.getUnderlyingScalarOperand()) + : engine.applyOperator(operand!, this.operator, evaluatedOperand); + } + + getUnderlyingScalarOperand() : ScalarValue { + return this.operand.getUnderlyingScalarOperand(); + } + + toString(): string { + return this.operator + this.operand.toString(); + } +} \ No newline at end of file diff --git a/src/expression/ExpressionParser.test.ts b/src/expression/ExpressionParser.test.ts index 1f04934..5725375 100644 --- a/src/expression/ExpressionParser.test.ts +++ b/src/expression/ExpressionParser.test.ts @@ -1,6 +1,6 @@ -import OperatorToken from "./OperatorToken"; -import { ScalarToken } from "./expression"; -import { ExpressionToken } from "./expression-interfaces"; +import BitwiseOperator from "./BitwiseOperator"; +import { ScalarValue } from "./expression"; +import { ExpressionElement } from "./expression-interfaces"; import { type } from "os"; import { InputType } from "zlib"; import exp from "constants"; diff --git a/src/expression/ListOfNumbersExpression.test.ts b/src/expression/ListOfNumbersExpression.test.ts index 5cb1d09..1ba265b 100644 --- a/src/expression/ListOfNumbersExpression.test.ts +++ b/src/expression/ListOfNumbersExpression.test.ts @@ -1,7 +1,7 @@ -import ScalarToken from "./ScalarToken"; +import ScalarValue from "./ScalarValue"; import ListOfNumbersExpression from "./ListOfNumbersExpression"; it('calculates max bits length', () => { - var expr = new ListOfNumbersExpression("10 0xabef 0b01010", [ScalarToken.parse("10"), ScalarToken.parse("0xabef"), ScalarToken.parse("0b01010")]) + var expr = new ListOfNumbersExpression("10 0xabef 0b01010", [ScalarValue.parse("10"), ScalarValue.parse("0xabef"), ScalarValue.parse("0b01010")]) expect(expr.maxBitsLength).toBe(16); }); diff --git a/src/expression/ListOfNumbersExpression.ts b/src/expression/ListOfNumbersExpression.ts index 064492d..a7c3b43 100644 --- a/src/expression/ListOfNumbersExpression.ts +++ b/src/expression/ListOfNumbersExpression.ts @@ -1,13 +1,13 @@ import calc from "../core/calc"; -import ScalarToken from "./ScalarToken"; -import { Expression, ExpressionToken } from "./expression-interfaces"; +import ScalarValue from "./ScalarValue"; +import { Expression, ExpressionElement } from "./expression-interfaces"; export default class ListOfNumbersExpression implements Expression { - children: ScalarToken[]; + children: ScalarValue[]; expressionString: string; maxBitsLength: number; - constructor(expressionString: string, numbers: ScalarToken[]) { + constructor(expressionString: string, numbers: ScalarValue[]) { this.expressionString = expressionString; this.children = numbers; this.maxBitsLength = numbers.map(n => calc.numberOfBitsDisplayed(n.value)).reduce((n , c) => n >= c ? n : c, 0); diff --git a/src/expression/OperatorExpression.test.ts b/src/expression/OperatorExpression.test.ts deleted file mode 100644 index ea30f6e..0000000 --- a/src/expression/OperatorExpression.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import ScalarToken from "./ScalarToken"; -import OperatorToken from './OperatorToken'; -import { INT_MAX_VALUE } from "../core/const"; - -it('can apply ~ operand', () => { - var op = new ScalarToken(10, 'dec'); - var expr = new OperatorToken(op, "~"); - var result = expr.evaluate(); - expect(result.value).toBe(-11); - expect(result.base).toBe('dec'); -}); - -it('can apply & operand', () => { - var op1 = new ScalarToken(3, 'dec'); - var op2 = new ScalarToken(4, 'dec'); - var expr = new OperatorToken(op1, "|"); - var result = expr.evaluate(op2); - expect(result.value).toBe(7); - expect(result.base).toBe('dec'); -}); - -it("doesn't support opreations with numbers larger than 32-bit", () => { - expect(() => new OperatorToken(new ScalarToken(2147483648), "^")) - .toThrowError("2147483648 has more than 32 bits. JavaScript converts all numbers to 32-bit integers when applying bitwise operators. BitwiseCmd currently uses the JavaScript engine of your browser for results calculation and supports numbers in the range from -2147483647 to 2147483647"); -}); \ No newline at end of file diff --git a/src/expression/OperatorToken.test.ts b/src/expression/OperatorToken.test.ts new file mode 100644 index 0000000..875e645 --- /dev/null +++ b/src/expression/OperatorToken.test.ts @@ -0,0 +1,12 @@ +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/OperatorToken.ts b/src/expression/OperatorToken.ts deleted file mode 100644 index 093e2e0..0000000 --- a/src/expression/OperatorToken.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { INT_MAX_VALUE } from '../core/const'; -import formatter from '../core/formatter'; -import ScalarToken from './ScalarToken'; -import { ExpressionToken } from './expression-interfaces'; - -export default class OperatorToken implements ExpressionToken { - operand: ExpressionToken; - operator: string; - isOperator: boolean; - isShiftExpression: boolean; - isNotExpression: boolean; - - constructor(operand : ExpressionToken, operator : string) { - - if(operand instanceof ScalarToken) { - const o = operand.getUnderlyingScalarOperand(); - if(Math.abs(o.value) > INT_MAX_VALUE) { - const n = formatter.numberToString(o.value, o.base); - throw new Error(`${n} has more than 32 bits. JavaScript converts all numbers to 32-bit integers when applying bitwise operators. BitwiseCmd currently uses the JavaScript engine of your browser for results calculation and supports numbers in the range from ${-INT_MAX_VALUE} to ${INT_MAX_VALUE}.`); - } - } - - this.operand = operand; - this.operator = operator; - this.isOperator = true; - this.isShiftExpression = this.operator.indexOf('<') >= 0 || this.operator.indexOf('>')>= 0; - this.isNotExpression = this.operator === '~'; - } - - evaluate(operand?: ScalarToken) : ScalarToken { - if (operand instanceof OperatorToken) { - throw new Error('value shouldnt be expression'); - } - - var evaluatedOperand = this.operand.evaluate(); - - var str = ''; - if(this.operator == '~'){ - str = '~' + evaluatedOperand.value; - } else { - if(operand == null) - throw new Error("Other is required for this expression"); - - str = operand.value + this.operator + evaluatedOperand.value; - } - - return ScalarToken.create(eval(str), evaluatedOperand.base); - } - - getUnderlyingScalarOperand() : ScalarToken { - return this.operand.getUnderlyingScalarOperand(); - } - - toString(): string { - return this.operator + this.operand.toString(); - } -} \ No newline at end of file diff --git a/src/expression/ScalarExpression.test.ts b/src/expression/ScalarExpression.test.ts index c4b0a44..07e988c 100644 --- a/src/expression/ScalarExpression.test.ts +++ b/src/expression/ScalarExpression.test.ts @@ -1,19 +1,19 @@ -import ScalarToken from './ScalarToken'; +import ScalarValue from './ScalarValue'; it('parsed from dec string', () => { - var op = ScalarToken.parse('123'); + var op = ScalarValue.parse('123'); expect(op.base).toBe('dec'); expect(op.value).toBe(123); }); it('parsed from bin string', () => { - var op = ScalarToken.parse('0b10'); + var op = ScalarValue.parse('0b10'); expect(op.base).toBe('bin'); expect(op.value).toBe(2); }); it('parsed from hex string', () => { - var op = ScalarToken.parse('0x10'); + var op = ScalarValue.parse('0x10'); expect(op.base).toBe('hex'); expect(op.value).toBe(16); }); diff --git a/src/expression/ScalarToken.ts b/src/expression/ScalarToken.ts deleted file mode 100644 index 07cdb87..0000000 --- a/src/expression/ScalarToken.ts +++ /dev/null @@ -1,62 +0,0 @@ -import {numberParser} from './numberParser'; -import { ExpressionToken as ExpressionToken } from './expression-interfaces'; -import { NumberBase } from '../core/formatter'; -import { INT_MAX_VALUE } from '../core/const'; - -var globalId : number = 1; - - -// Represents scalar numeric value -export default class ScalarToken implements ExpressionToken { - id: number; - value: number; - base: NumberBase; - isOperator: boolean; - - constructor(value : number, base?: NumberBase) { - - this.id = globalId++; - this.value = value; - this.base = base || "dec"; - this.isOperator = false; - } - - setValue(value : number) { - this.value = value; - } - - evaluate() : ScalarToken { - return this; - } - - getUnderlyingScalarOperand() : ScalarToken { - return this - } - - static create(value : number, base? : NumberBase) { - return new ScalarToken(value, base || "dec"); - }; - - static parse(input: string) : ScalarToken { - - var parsed = ScalarToken.tryParse(input); - - if(parsed == null) { - throw new Error(input + " is not a valid number"); - } - - return parsed; - } - - static tryParse(input: string) : ScalarToken | null { - - var parsed = numberParser.parse(input); - - if(!parsed) { - return null; - } - - return new ScalarToken(parsed.value, parsed.base); - } - -} \ No newline at end of file diff --git a/src/expression/ScalarValue.test.ts b/src/expression/ScalarValue.test.ts new file mode 100644 index 0000000..b43f829 --- /dev/null +++ b/src/expression/ScalarValue.test.ts @@ -0,0 +1,11 @@ +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); +}); \ No newline at end of file diff --git a/src/expression/ScalarValue.ts b/src/expression/ScalarValue.ts new file mode 100644 index 0000000..c765864 --- /dev/null +++ b/src/expression/ScalarValue.ts @@ -0,0 +1,79 @@ +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 { NumberType } from '../core/types'; + +var globalId : number = 1; + + +// Represents scalar numeric value +export default class ScalarValue implements ExpressionElement { + id: number; + value: NumberType; + base: NumberBase; + isOperator: boolean; + + constructor(value : NumberType, base?: NumberBase, is32Limit?: boolean) { + + ScalarValue.validateSupported(value); + + this.id = globalId++; + this.value = value; + this.base = base || "dec"; + this.isOperator = false; + } + + bitSize() : number { + return this.isBigInt() ? 64 : 32; + } + + isBigInt() : boolean { + return typeof this.value === 'bigint'; + } + + setValue(value : NumberType) { + this.value = value; + } + + evaluate() : ScalarValue { + return this; + } + + getUnderlyingScalarOperand() : ScalarValue { + return this + } + + static validateSupported(num : NumberType) { + + if(typeof num == "bigint" && (num < INT64_MIN_VALUE || num > INT64_MAX_VALUE)) { + throw new Error(`64-bit numbers are supported in range from ${INT64_MIN_VALUE} to ${INT64_MAX_VALUE}`); + } + + if(typeof num == "number" && (num < INT32_MIN_VALUE || num > 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}`) + } + } + + 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 1d1bcc7..e96b621 100644 --- a/src/expression/components/BitwiseResultView.tsx +++ b/src/expression/components/BitwiseResultView.tsx @@ -2,8 +2,9 @@ import React from 'react'; import formatter from '../../core/formatter'; import BinaryStringView, { FlipBitEventArg } from '../../core/components/BinaryString'; import BitwiseResultViewModel from './BitwiseResultViewModel'; -import { Expression, ExpressionToken } from '../expression-interfaces'; -import { OperatorToken, ScalarToken } from '../expression'; +import { Expression, ExpressionElement } from '../expression-interfaces'; +import { BitwiseOperator, ScalarValue } from '../expression'; +import calc from '../../core/calc'; type BitwiseResultViewProps = { expression: Expression; @@ -15,32 +16,51 @@ type BitwiseResultViewState = { } export default class BitwiseResultView extends React.Component { + maxSeenLengthNumberOfBits: number; + constructor(props: BitwiseResultViewProps) { super(props); this.state = {}; + this.maxSeenLengthNumberOfBits = 0; } + render() { - var rows = this.getRows(); + + let model : BitwiseResultViewModel | null = null + try + { + model = BitwiseResultViewModel.createModel(this.props.expression, this.props.emphasizeBytes); + } + catch(err) { + const text = (err as any).message; + return
Error: {text}
+ } + + + var rows = this.getRows(model!); + return - - {rows} - -
+ + {rows} + + } - getRows() : JSX.Element[] { - var model = BitwiseResultViewModel.createModel(this.props.expression, this.props.emphasizeBytes); + getRows(model: BitwiseResultViewModel): JSX.Element[] { - return model.items.map((itm, i) => - + this.onBitFlipped()} />); } @@ -51,38 +71,42 @@ export default class BitwiseResultView extends React.Component { constructor(props: ExpressionRowProps) { - super(props); - this.state = { operand: null }; - } + super(props); + this.state = { operand: null }; + } render() { const { sign, css, maxNumberOfBits, emphasizeBytes, allowFlipBits } = this.props; - + const maxBits = Math.max() + return - {sign} - {this.getLabel()} - - this.flipBit(args)}/> - - {this.getAlternative()} - ;; + {sign} + {this.getLabel()} + + this.flipBit(args)} /> + + {this.getAlternative()} + {this.getInfo(maxNumberOfBits)} + ;; } - getBinaryString() : string { + getBinaryString(): string { var v = this.props.expressionItem.evaluate(); return formatter.numberToString(v.value, 'bin'); } @@ -91,18 +115,18 @@ class ExpressionRow extends React.Component { // For expressions like |~2 // TODO: find a better way... - if(this.props.expressionItem.isOperator) { - const ex = this.props.expressionItem as OperatorToken; + if (this.props.expressionItem.isOperator) { + const ex = this.props.expressionItem as BitwiseOperator; return ex.operator + this.getLabelString(ex.getUnderlyingScalarOperand()); } - return this.getLabelString(this.props.expressionItem.getUnderlyingScalarOperand()); + return this.getLabelString(this.props.expressionItem.getUnderlyingScalarOperand()); } getAlternative() { - if(this.props.expressionItem.isOperator) { - const ex = this.props.expressionItem as OperatorToken; + if (this.props.expressionItem.isOperator) { + const ex = this.props.expressionItem as BitwiseOperator; const res = ex.evaluate(); return formatter.numberToString(res.value, res.base); @@ -113,22 +137,45 @@ class ExpressionRow extends React.Component { return formatter.numberToString(v.value, altBase); } - getLabelString (op: ScalarToken) : string { + getLabelString(op: ScalarValue): string { return formatter.numberToString(op.value, op.base == 'bin' ? 'dec' : op.base); } - flipBit(args: FlipBitEventArg) { + flipBit(args: FlipBitEventArg) { - const op = this.props.expressionItem.getUnderlyingScalarOperand(); - const { index, binaryString } = args; + const op = this.props.expressionItem.getUnderlyingScalarOperand(); + const { bitIndex: index, binaryStringLength: totalLength } = args; - var arr = binaryString.split(''); - arr[index] = arr[index] == '0' ? '1' : '0'; - var bin = arr.join(''); + if(totalLength > op.bitSize() && (totalLength - index) > op.bitSize()) { + op.setValue(calc.promoteToBigInt(op.value as number)); + } - var newValue = parseInt(bin, 2); + console.log(op.bitSize()); + const pad = op.bitSize() - totalLength; + console.log(pad + index); + const newValue = calc.flipBit(op.value, pad + index); op.setValue(newValue); - this.props.onBitFlipped(); } + + getInfo(maxNumberOfBits:number) { + var op = this.props.expressionItem.getUnderlyingScalarOperand(); + + if (op.isBigInt()) + { + 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.`; + + return (64-bit BigInt); + } + + 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 null; + + } } \ No newline at end of file diff --git a/src/expression/components/BitwiseResultViewModel.ts b/src/expression/components/BitwiseResultViewModel.ts index 82830c3..1a5f352 100644 --- a/src/expression/components/BitwiseResultViewModel.ts +++ b/src/expression/components/BitwiseResultViewModel.ts @@ -1,7 +1,8 @@ -import { ScalarToken, ListOfNumbersExpression, BitwiseOperationExpression, OperatorToken } from '../expression'; -import { ExpressionToken, Expression } from '../expression-interfaces'; +import { ScalarValue, ListOfNumbersExpression, BitwiseOperationExpression, BitwiseOperator } from '../expression'; +import { ExpressionElement, Expression } from '../expression-interfaces'; import calc from '../../core/calc'; import formatter from '../../core/formatter'; +import exp from 'constants'; type Config = { emphasizeBytes: boolean; @@ -11,9 +12,10 @@ type Config = { type ExpressionRowModel = { sign: string; css: string; - expression: ExpressionToken; + expression: ExpressionElement; allowFlipBits: boolean; label: string; + bitSize: number; } export default class BitwiseResultViewModel { @@ -43,17 +45,17 @@ export default class BitwiseResultViewModel { i = 0, len = expr.children.length, ex, m = new BitwiseResultViewModel(config); - var prev : ScalarToken | null = null; + var prev : ScalarValue | null = null; for (;i { + + 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 new file mode 100644 index 0000000..e7d6ae5 --- /dev/null +++ b/src/expression/engine.ts @@ -0,0 +1,40 @@ +import { NumberType } 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 : NumberType, operator: string, op2 : NumberType) : NumberType{ + const a = equalizeType(op2, op1) as any; + const b = equalizeType(op1, op2) as any; + + switch(operator) { + case ">>": return (a >> b) as (NumberType); + case ">>>": return (a >>> b) as (NumberType); + case "<<": return (a << b) as (NumberType); + case "&": return (b & a) as (NumberType); + case "|": return (b | a) as (NumberType); + case "^": return (b ^ a) as (NumberType); + default: throw new Error(operator + " operator is not supported"); + } +} + +function equalizeType(source : NumberType, dest : NumberType) : NumberType { + + 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-interfaces.ts b/src/expression/expression-interfaces.ts index 1770d3e..86010f9 100644 --- a/src/expression/expression-interfaces.ts +++ b/src/expression/expression-interfaces.ts @@ -1,14 +1,14 @@ -import { ScalarToken } from "./expression"; +import { ScalarValue } from "./expression"; export interface Expression { expressionString: string; } -export interface ExpressionToken +export interface ExpressionElement { isOperator: boolean; - getUnderlyingScalarOperand: () => ScalarToken; - evaluate(operand? : ScalarToken): ScalarToken; + getUnderlyingScalarOperand: () => ScalarValue; + evaluate(operand? : ScalarValue): ScalarValue; } diff --git a/src/expression/expression.test.ts b/src/expression/expression.test.ts index 59cb084..e710e2c 100644 --- a/src/expression/expression.test.ts +++ b/src/expression/expression.test.ts @@ -1,4 +1,4 @@ -import { parser, ListOfNumbersExpression, BitwiseOperationExpression, ScalarToken, OperatorToken } from "./expression"; +import { parser, ListOfNumbersExpression, BitwiseOperationExpression, ScalarValue, BitwiseOperator } from "./expression"; describe("expression parser", () => { @@ -25,8 +25,8 @@ describe("expression parser", () => { expect(actual).toBeInstanceOf(ListOfNumbersExpression); const expr = actual as ListOfNumbersExpression; - expect(expr.children[0].getUnderlyingScalarOperand().value).toBe(305419896); - expect(expr.children[1].getUnderlyingScalarOperand().value).toBe(2863311360); + expect(expr.children[0].getUnderlyingScalarOperand().value.toString()).toBe('305419896'); + expect(expr.children[1].getUnderlyingScalarOperand().value.toString()).toBe('2863311360'); }) it("pares multiple operand expression", () => { @@ -36,17 +36,17 @@ describe("expression parser", () => { const first = result.children[0]; const second = result.children[1]; - expect(first).toBeInstanceOf(ScalarToken); + expect(first).toBeInstanceOf(ScalarValue); - expect((first as ScalarToken).value).toBe(1); + expect((first as ScalarValue).value).toBe(1); - expect(second).toBeInstanceOf(OperatorToken); - var secondOp = second as OperatorToken; + expect(second).toBeInstanceOf(BitwiseOperator); + var secondOp = second as BitwiseOperator; expect(secondOp.operator).toBe("^"); - expect(secondOp.operand).toBeInstanceOf(ScalarToken); - var childOp = secondOp.operand as ScalarToken; - expect(childOp.value).toBe(2); + expect(secondOp.operand).toBeInstanceOf(ScalarValue); + var childOp = secondOp.operand as ScalarValue; + expect(childOp.value.toString()).toBe('2'); }); it("bug", () => { diff --git a/src/expression/expression.ts b/src/expression/expression.ts index 9f0bd59..ab3d15a 100644 --- a/src/expression/expression.ts +++ b/src/expression/expression.ts @@ -1,12 +1,11 @@ -import ScalarToken from './ScalarToken'; -import OperatorToken from './OperatorToken' +import ScalarValue from './ScalarValue'; +import BitwiseOperator from './BitwiseOperator' import ListOfNumbersExpression from './ListOfNumbersExpression'; import BitwiseOperationExpression from './BitwiseOperationExpression'; -import { Expression, ExpressionToken } from './expression-interfaces'; -import { NumberBase } from '../core/formatter'; +import { Expression, ExpressionElement } from './expression-interfaces'; -export { default as ScalarToken } from './ScalarToken'; -export { default as OperatorToken } from './OperatorToken'; +export { default as ScalarValue } from './ScalarValue'; +export { default as BitwiseOperator } from './BitwiseOperator'; export { default as ListOfNumbersExpression } from './ListOfNumbersExpression'; export { default as BitwiseOperationExpression } from './BitwiseOperationExpression'; @@ -46,14 +45,6 @@ class ExpressionParser { return null; }; - - parseOperand (input : string) : ScalarToken { - return ScalarToken.parse(input); - }; - - createOperand (number : number, base : NumberBase) : ScalarToken { - return ScalarToken.create(number, base); - }; addFactory (factory: IExpressionParserFactory) { this.factories.push(factory); @@ -70,7 +61,7 @@ class ListOfNumbersExpressionFactory implements IExpressionParserFactory return input.split(' ') .filter(p => p.length > 0) - .map(p => ScalarToken.tryParse(p)) + .map(p => ScalarValue.tryParse(p)) .filter(n => n == null) .length == 0; }; @@ -79,7 +70,7 @@ class ListOfNumbersExpressionFactory implements IExpressionParserFactory const numbers = input.split(' ') .filter(p => p.length > 0) - .map(m => ScalarToken.parse(m)); + .map(m => ScalarValue.parse(m)); return new ListOfNumbersExpression(input, numbers); } @@ -90,8 +81,8 @@ class BitwiseOperationExpressionFactory implements IExpressionParserFactory { regex: RegExp; constructor() { - this.fullRegex = /^((<<|>>|>>>|\||\&|\^)?(~?-?([b,x,a-f,0-9]+)))+$/; - this.regex = /(<<|>>|>>>|\||\&|\^)?(~?-?(?:[b,x,a-f,0-9]+))/g; + this.fullRegex = /^((<<|>>|>>>|\||\&|\^)?(~?-?([b,x,l,L,a-f,0-9]+)))+$/; + this.regex = /(<<|>>|>>>|\||\&|\^)?(~?-?(?:[b,x,l,L,a-f,0-9]+))/g; } canCreate (input: string) : boolean { @@ -101,7 +92,7 @@ class BitwiseOperationExpressionFactory implements IExpressionParserFactory { create (input: string) : Expression { var m : RegExpExecArray | null; - const operands : ExpressionToken[] = []; + const operands : ExpressionElement[] = []; const normalizedString = this.normalizeString(input); this.regex.lastIndex = 0; @@ -113,23 +104,23 @@ class BitwiseOperationExpressionFactory implements IExpressionParserFactory { return new BitwiseOperationExpression(normalizedString, operands) }; - parseMatch (m:any): ExpressionToken { + parseMatch (m:any): ExpressionElement { var input = m[0], operator = m[1], num = m[2]; var parsed = null; if(num.indexOf('~') == 0) { - parsed = new OperatorToken(ScalarToken.parse(num.substring(1)), '~'); + parsed = new BitwiseOperator(ScalarValue.parse(num.substring(1)), '~'); } else { - parsed = ScalarToken.parse(num); + parsed = ScalarValue.parse(num); } if(operator == null) { - return parsed as OperatorToken; + return parsed as BitwiseOperator; } else { - return new OperatorToken(parsed as ScalarToken, operator); + return new BitwiseOperator(parsed as ScalarValue, operator); } }; diff --git a/src/expression/numberParser.test.ts b/src/expression/numberParser.test.ts new file mode 100644 index 0000000..3e9c7c0 --- /dev/null +++ b/src/expression/numberParser.test.ts @@ -0,0 +1,106 @@ +import exp from 'constants'; +import calc from '../core/calc'; +import {numberParser, ParsedNumber} from './numberParser'; + +describe("parser", () => { + + it('parses decimal number', () => { + const result = numberParser.parse('10'); + expect(result).not.toBeNull(); + + var number = result as ParsedNumber; + expect(number.value).toBe(10); + expect(number.base).toBe('dec'); + expect(number.input).toBe('10'); + }); + + it('parses bigint numbers', () => { + const dec = numberParser.parse('10L'); + expect(dec).not.toBeNull(); + + expect(dec?.value).toBe(BigInt(10)); + expect(typeof dec?.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).toBe(BigInt(2)); + expect(typeof bin?.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.toString()).toBe(BigInt(15).toString()); + expect(typeof hex?.value).toBe("bigint"); + expect(hex?.base).toBe('hex'); + expect(hex?.input).toBe('0xfL'); + }); + + + it('switches to bigint if value exceeds max safe int', () => { + const unsafeInt = BigInt(Number.MAX_SAFE_INTEGER)+BigInt(1); + + const dec = numberParser.parse(unsafeInt.toString()); + expect(dec?.value).toEqual(unsafeInt); + expect(dec?.base).toBe('dec'); + + const bin = numberParser.parse("0b" + unsafeInt.toString(2)); + expect(bin?.value).toEqual(unsafeInt); + expect(bin?.base).toEqual('bin'); + + const hex = numberParser.parse("0x" + unsafeInt.toString(16)); + expect(hex?.value).toEqual(unsafeInt); + expect(hex?.base).toEqual('hex'); + }); + + it('switches to bigint if value exceeds max safe negative int', () => { + const unsafeInt = BigInt(Number.MIN_SAFE_INTEGER)-BigInt(1); + + const dec = numberParser.parse(unsafeInt.toString()); + expect(dec?.value.toString()).toEqual(unsafeInt.toString()); + expect(dec?.base).toBe('dec'); + + const bin = numberParser.parse("-0b" + unsafeInt.toString(2).replace('-', '')); + expect(bin?.value.toString()).toEqual(unsafeInt.toString()); + expect(bin?.base).toEqual('bin'); + + const hex = numberParser.parse("-0x" + unsafeInt.toString(16).replace('-','')); + expect(hex?.value.toString()).toEqual(unsafeInt.toString()); + expect(hex?.base).toEqual('hex'); + }); + + it('parses hex number', () => { + const result = numberParser.parse('0xab'); + expect(result).not.toBeNull(); + + var number = result as ParsedNumber; + expect(number.value).toBe(171); + expect(number.base).toBe('hex'); + expect(number.input).toBe('0xab'); + }); + + it('parses bin number', () => { + var result = numberParser.parse('0b0110'); + expect(result).not.toBeNull(); + + var number = result as ParsedNumber; + expect(number.value).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(); + }); + + it('parses big int', () => { + var v = numberParser.parse('1l')?.value + expect(typeof v).toBe("bigint"); + expect(v?.toString()).toBe("1"); + }) +}); \ No newline at end of file diff --git a/src/expression/numberParser.tests.ts b/src/expression/numberParser.tests.ts deleted file mode 100644 index 8b57a06..0000000 --- a/src/expression/numberParser.tests.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {numberParser, ParsedNumber} from './numberParser'; - -describe("parser", () => { - it('parses decimal number', () => { - const result = numberParser.parse('10'); - expect(result).not.toBeNull(); - - var number = result as ParsedNumber; - expect(number.value).toBe(10); - expect(number.base).toBe('dec'); - expect(number.input).toBe('10'); - }); - - it('parses hex number', () => { - const result = numberParser.parse('0xab'); - expect(result).not.toBeNull(); - - var number = result as ParsedNumber; - expect(number.value).toBe(171); - expect(number.base).toBe('hex'); - expect(number.input).toBe('0xab'); - }); - - it('parses bin number', () => { - var result = numberParser.parse('0b0110'); - expect(result).not.toBeNull(); - - var number = result as ParsedNumber; - expect(number.value).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(); - }); -}); \ No newline at end of file diff --git a/src/expression/numberParser.ts b/src/expression/numberParser.ts index 13c3d9b..ed13bf7 100644 --- a/src/expression/numberParser.ts +++ b/src/expression/numberParser.ts @@ -1,27 +1,27 @@ +import { INT32_MAX_VALUE, INT32_MIN_VALUE } from "../core/const"; import { NumberBase } from "../core/formatter"; +import { NumberType } from "../core/types"; -const decimalRegex = /^-?\d+$/; -const hexRegex = /^-?0x[0-9,a-f]+$/i; -const binRegex = /^-?0b[0-1]+$/i; -const operatorRegex = /^<<|>>|<<<|\&|\|\^|~$/; +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, - radix: number, base: NumberBase, - prefix: string|RegExp + parse: (input: string) => NumberType } export interface ParsedNumber { - value: number; + value: number|bigint; base: NumberBase; input: string; } var knownParsers : ParserConfig[] = [ - { regex: decimalRegex, radix: 10, base: 'dec', prefix: '^$' }, - { regex: hexRegex, radix: 16, base: 'hex', prefix:/0x/i }, - { regex: binRegex, radix: 2, base: 'bin', prefix:/0b/i }]; + { 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 { @@ -53,7 +53,7 @@ class NumberParser { return null; } - var value = parseInt(rawInput.replace(parser.prefix, ''), parser.radix); + var value = parser.parse(rawInput); return { value: value, @@ -63,6 +63,29 @@ class NumberParser { } } +const MAX_SAFE_INTn = BigInt(INT32_MAX_VALUE); +const MIN_SAFE_INTn = BigInt(INT32_MIN_VALUE); + +function parseIntSafe(input : string, radix: number) : NumberType { + +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 bigInt; + + if(bigInt > MAX_SAFE_INTn) + return bigInt; + + if(bigInt < MIN_SAFE_INTn) + return bigInt; + + return parseInt(input.replace(/0(x|b)/, ''), radix); +} + const numberParser = new NumberParser(knownParsers); export {numberParser}; \ No newline at end of file diff --git a/src/index.css b/src/index.css index 5ee2d21..9b62d3d 100644 --- a/src/index.css +++ b/src/index.css @@ -32,6 +32,7 @@ code { font-size: 1.2em; font-weight: bold; } .expression .label { font-weight: bold; padding-right: 5px; text-align: right; } .expression .bin { letter-spacing: 3px; } +.expression .info { font-size: 0.9em; color: teal; } .expression .byte { margin: 0 3px; } .expression-result td { border-top: dotted 1px gray; } .expression { font-size: 1.5em; font-family: monospace } diff --git a/src/shell/AppState.ts b/src/shell/AppState.ts index 2cc0f88..a52ce49 100644 --- a/src/shell/AppState.ts +++ b/src/shell/AppState.ts @@ -44,7 +44,7 @@ export default class AppState { this.emphasizeBytes = persistData.emphasizeBytes || true; this.persistedVersion = persistData.version || 0.1; this.wasOldVersion = persistData.version != null && this.version > this.persistedVersion; - this.debugMode = env !== 'prod' || persistData.debugMode === true; + this.debugMode = persistData.debugMode === true; this.pageVisitsCount = persistData.pageVisistsCount || 0; this.donationClicked = persistData.donationClicked; } diff --git a/src/shell/components/AppRoot.tsx b/src/shell/components/AppRoot.tsx index 78211f1..1a2b584 100644 --- a/src/shell/components/AppRoot.tsx +++ b/src/shell/components/AppRoot.tsx @@ -28,7 +28,6 @@ export default class AppRoot extends React.Component } refresh() { - console.log('refresh'); this.setState(this.props.appState); } diff --git a/src/shell/module.tsx b/src/shell/module.tsx index 30ef963..1a20949 100644 --- a/src/shell/module.tsx +++ b/src/shell/module.tsx @@ -10,6 +10,7 @@ import HelpResultView from './components/HelpResultView'; import TextResultView from './components/TextResultView'; import WhatsnewResultView from './components/WhatsNewResultView'; import {STARTUP_COMMAND_KEY} from './startup'; +import { INT32_MAX_VALUE, INT64_MAX_VALUE } from '../core/const'; const shellModule = { setup: function(appState: AppState, cmd: CmdShell) { @@ -31,6 +32,10 @@ const shellModule = { appState.toggleDebugMode(); appState.addCommandResult(c.input, () => ); }); + cmd.command("-max", (c:CommandInput) => { + const text = `Int32 ${INT32_MAX_VALUE}\nInt64 ${INT64_MAX_VALUE}` + appState.addCommandResult(c.input, () => ) + }) cmd.command("donate", (c:CommandInput) => {