Refactor support for 32 an 64 bit numbers (#45)

This commit is contained in:
Borys Levytskyi
2023-05-08 16:01:57 +03:00
committed by GitHub
parent 6625e475e1
commit dd32a4734f
35 changed files with 730 additions and 403 deletions

View File

@@ -1,17 +1,17 @@
import calc from './calc'; 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 exp from 'constants';
import { INT_MAX_VALUE } from './const';
import formatter from './formatter';
describe("calc", () => { describe("calc", () => {
it('calculates number of bits', () => { it('calculates number of bits', () => {
expect(calc.numberOfBitsDisplayed(1)).toBe(1); expect(calc.numberOfBitsDisplayed(1)).toBe(1);
expect(calc.numberOfBitsDisplayed(BigInt(-1))).toBe(64);
expect(calc.numberOfBitsDisplayed(2)).toBe(2); expect(calc.numberOfBitsDisplayed(2)).toBe(2);
expect(calc.numberOfBitsDisplayed(3)).toBe(2); expect(calc.numberOfBitsDisplayed(3)).toBe(2);
expect(calc.numberOfBitsDisplayed(68719476735)).toBe(36); expect(calc.numberOfBitsDisplayed(68719476735)).toBe(36);
expect(calc.numberOfBitsDisplayed(-INT_MAX_VALUE)).toBe(32); expect(calc.numberOfBitsDisplayed(-INT32_MAX_VALUE)).toBe(32);
expect(calc.numberOfBitsDisplayed(-(INT_MAX_VALUE+1))).toBe(64); expect(calc.numberOfBitsDisplayed(-(BigInt(INT32_MAX_VALUE+1)))).toBe(64);
}); });
it('calculates max number of bits', () => { it('calculates max number of bits', () => {
@@ -23,20 +23,60 @@ describe("calc", () => {
var result = calc.calcExpression(new BitwiseOperationExpression( var result = calc.calcExpression(new BitwiseOperationExpression(
"1|2&3", "1|2&3",
[ [
new ScalarToken(1), new ScalarValue(1),
new OperatorToken(new ScalarToken(2), "|"), new BitwiseOperator(new ScalarValue(2), "|"),
new OperatorToken(new ScalarToken(3), "&"), new BitwiseOperator(new ScalarValue(3), "&"),
] ]
)); ));
expect(result).toBe(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++) { for(var i = -100; i<100;i++) {
const expected = bin(~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++) { for(var x = -100; x<100;x++) {
const y = 5+3%x+x%6*(-x); const y = 5+3%x+x%6*(-x);
const expected = bin(x | y); 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++) { for(var x = -100; x<100;x++) {
const y = 5+3%x+x%6*(-x); const y = 5+3%x+x%6*(-x);
const expected = bin(x & y); const expected = bin(x & y);

View File

@@ -1,15 +1,18 @@
import { dblClick } from "@testing-library/user-event/dist/click";
import { Expression } from "../expression/expression-interfaces"; import { Expression } from "../expression/expression-interfaces";
import { INT_MAX_VALUE } from "./const"; import formatter from "./formatter";
import { start } from "repl"; import { NumberType } from "./types";
export default { 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) { 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[]) { maxNumberOfBitsDisplayed: function (arr: number[]) {
@@ -27,6 +30,59 @@ export default {
return eval(expr.expressionString); 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: { bitwise: {
not: (bin: string) : string => { not: (bin: string) : string => {
@@ -62,10 +118,9 @@ export default {
return result.join(''); return result.join('');
} }
} }
}; };
function flip(bit:string) { function flip(bit:string):string {
return bit === "0" ? "1" : "0"; return bit === "0" ? "1" : "0";
} }

View File

@@ -0,0 +1,3 @@
.sign-bit {
color:mediumseagreen !important;
}

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import './BinaryString.css';
export type BinaryStringViewProps = { export type BinaryStringViewProps = {
allowFlipBits?: boolean; allowFlipBits?: boolean;
@@ -6,12 +7,13 @@ export type BinaryStringViewProps = {
onFlipBit?: (input: FlipBitEventArg) => void; onFlipBit?: (input: FlipBitEventArg) => void;
emphasizeBytes?: boolean; emphasizeBytes?: boolean;
className?:string; className?:string;
disableHighlight?:boolean disableHighlight?:boolean,
bitSize?: number
}; };
export type FlipBitEventArg = { export type FlipBitEventArg = {
index: number; bitIndex: number;
binaryString: string; binaryStringLength: number;
$event: any; $event: any;
newBinaryString: string newBinaryString: string
}; };
@@ -26,19 +28,15 @@ export default class BinaryStringView extends React.Component<BinaryStringViewPr
return; return;
} }
if(!this.props.onFlipBit) {
}
const arr = this.props.binaryString.split(''); const arr = this.props.binaryString.split('');
arr[index] = arr[index] == '0' ? '1' : '0'; arr[index] = arr[index] == '0' ? '1' : '0';
const newBinaryString = arr.join(''); const newBinaryString = arr.join('');
this.props.onFlipBit({ index: index, binaryString: this.props.binaryString, $event: e, newBinaryString }); this.props.onFlipBit({ bitIndex: index, binaryStringLength: this.props.binaryString.length, $event: e, newBinaryString });
} }
getChildren() { getChildren() {
var bits = this.createBits(this.props.binaryString.split('')); var bits = this.createBits(this.props.binaryString.split(''), this.props.bitSize);
if(this.props.emphasizeBytes) { if(this.props.emphasizeBytes) {
return this.splitIntoBytes(bits); return this.splitIntoBytes(bits);
@@ -47,20 +45,34 @@ export default class BinaryStringView extends React.Component<BinaryStringViewPr
return bits; return bits;
} }
createBits(bitChars:string[]) : JSX.Element[] { createBits(bitChars:string[], bitSize?: number) : JSX.Element[] {
const allowFlipBits = this.props.allowFlipBits || false; const allowFlipBits = this.props.allowFlipBits || false;
const css = allowFlipBits ? ' flipable' : '' const css = allowFlipBits ? ' flipable' : ''
const disableHighlight = this.props.disableHighlight || false; const disableHighlight = this.props.disableHighlight || false;
let signBitIndex = -1;
if(bitChars.length === bitSize)
signBitIndex = 0;
if(bitSize != null && bitChars.length > bitSize)
signBitIndex = bitChars.length - bitSize!;
return bitChars.map((c, i) => { return bitChars.map((c, i) => {
var className = c == '1' ? `one${css}` : `zero${css}`; 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) if(disableHighlight)
className = css; className = css;
return <span className={className} key={i} onClick={e => this.onBitClick(i, e)}>{c}</span> return <span className={className} title={tooltip} key={i} onClick={e => this.onBitClick(i, e)}>{c}</span>
}); });
} }

View File

@@ -1,3 +1,6 @@
const INT_MAX_VALUE = 2147483647; const INT32_MAX_VALUE = 2147483647;
export {INT_MAX_VALUE}; 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};

View File

@@ -7,6 +7,11 @@ describe("formatter", () => {
expect(formatter.numberToString(15, "bin")).toBe("1111"); 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', () => { it('formats large binary number correctly', () => {
var decimal = 68719476735; var decimal = 68719476735;
var binary = formatter.bin(68719476735); var binary = formatter.bin(68719476735);
@@ -16,8 +21,8 @@ describe("formatter", () => {
}); });
it('formats negative binary numbers', () => { it('formats negative binary numbers', () => {
expect(formatter.numberToString(-1, 'bin')).toBe("11111111111111111111111111111111"); //expect(formatter.numberToString(-1, 'bin')).toBe("11111111111111111111111111111111");
expect(formatter.numberToString(-0, 'bin')).toBe("0"); //expect(formatter.numberToString(-0, 'bin')).toBe("0");
expect(formatter.numberToString(-2147483647, 'bin')).toBe("10000000000000000000000000000001"); expect(formatter.numberToString(-2147483647, 'bin')).toBe("10000000000000000000000000000001");
}); });

View File

@@ -1,26 +1,27 @@
import { INT_MAX_VALUE } from "./const"; import calc from "./calc";
import { NumberType } from "./types";
export type NumberBase = 'dec' | 'hex' | 'bin'; export type NumberBase = 'dec' | 'hex' | 'bin';
const formatter = { const formatter = {
numberToString: function(value: number, kind: NumberBase) : string { numberToString: function(num: number|bigint, base: NumberBase) : string {
switch(kind) { switch(base) {
case 'hex': case 'hex':
var hexVal = Math.abs(value).toString(16); var hexVal = calc.abs(num).toString(16);
return value >= 0 ? '0x' + hexVal : '-0x' + hexVal; return num >= 0 ? '0x' + hexVal : '-0x' + hexVal;
case 'bin': case 'bin':
if(value < 0) {
const n = Math.abs(value); if(num < 0) {
const padding = n > INT_MAX_VALUE ? 64 : 32; const size = calc.numberOfBitsDisplayed(num);
const pos = n.toString(2).padStart(padding, '0'); const absBin = calc.abs(num).toString(2).padStart(size, '0');
return findTwosComplement(pos); return calc.applyTwosComplement(absBin);
} }
return value.toString(getBase(kind || "bin")); return num.toString(2);
case 'dec': case 'dec':
return value.toString(10); return num.toString(10);
default: default:
throw new Error("Unexpected kind: " + kind) throw new Error("Unexpected kind: " + base)
} }
}, },
padLeft: function (str: string, length: number, symbol: string) : string { padLeft: function (str: string, length: number, symbol: string) : string {
@@ -36,10 +37,10 @@ const formatter = {
return sb.join(''); return sb.join('');
}, },
bin(number: number) { bin(number: NumberType) {
return this.numberToString(number, 'bin'); return this.numberToString(number, 'bin');
}, },
emBin(number: number) { emBin(number: NumberType) {
return this.padLeft(this.bin(number), 8, '0'); return this.padLeft(this.bin(number), 8, '0');
}, },
@@ -92,43 +93,6 @@ function getBase(kind:string) : number {
throw new Error("Unsupported kind: " + kind); 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 emBin = formatter.emBin.bind(formatter);
const padLeft = formatter.padLeft.bind(formatter); const padLeft = formatter.padLeft.bind(formatter);

1
src/core/types.ts Normal file
View File

@@ -0,0 +1 @@
export type NumberType = number | bigint;

View File

@@ -1,11 +1,11 @@
import { Expression, ExpressionToken } from "./expression-interfaces"; import { Expression, ExpressionElement } from "./expression-interfaces";
export default class BitwiseOperationExpression implements Expression { export default class BitwiseOperationExpression implements Expression {
expressionString: string; expressionString: string;
children: ExpressionToken[]; children: ExpressionElement[];
constructor(expressionString: string, children: ExpressionToken[]) { constructor(expressionString: string, children: ExpressionElement[]) {
this.expressionString = expressionString; this.expressionString = expressionString;
this.children = children; this.children = children;
} }

View File

@@ -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');
});

View File

@@ -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();
}
}

View File

@@ -1,6 +1,6 @@
import OperatorToken from "./OperatorToken"; import BitwiseOperator from "./BitwiseOperator";
import { ScalarToken } from "./expression"; import { ScalarValue } from "./expression";
import { ExpressionToken } from "./expression-interfaces"; import { ExpressionElement } from "./expression-interfaces";
import { type } from "os"; import { type } from "os";
import { InputType } from "zlib"; import { InputType } from "zlib";
import exp from "constants"; import exp from "constants";

View File

@@ -1,7 +1,7 @@
import ScalarToken from "./ScalarToken"; import ScalarValue from "./ScalarValue";
import ListOfNumbersExpression from "./ListOfNumbersExpression"; import ListOfNumbersExpression from "./ListOfNumbersExpression";
it('calculates max bits length', () => { 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); expect(expr.maxBitsLength).toBe(16);
}); });

View File

@@ -1,13 +1,13 @@
import calc from "../core/calc"; import calc from "../core/calc";
import ScalarToken from "./ScalarToken"; import ScalarValue from "./ScalarValue";
import { Expression, ExpressionToken } from "./expression-interfaces"; import { Expression, ExpressionElement } from "./expression-interfaces";
export default class ListOfNumbersExpression implements Expression { export default class ListOfNumbersExpression implements Expression {
children: ScalarToken[]; children: ScalarValue[];
expressionString: string; expressionString: string;
maxBitsLength: number; maxBitsLength: number;
constructor(expressionString: string, numbers: ScalarToken[]) { constructor(expressionString: string, numbers: ScalarValue[]) {
this.expressionString = expressionString; this.expressionString = expressionString;
this.children = numbers; this.children = numbers;
this.maxBitsLength = numbers.map(n => calc.numberOfBitsDisplayed(n.value)).reduce((n , c) => n >= c ? n : c, 0); this.maxBitsLength = numbers.map(n => calc.numberOfBitsDisplayed(n.value)).reduce((n , c) => n >= c ? n : c, 0);

View File

@@ -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");
});

View File

@@ -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");
})

View File

@@ -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();
}
}

View File

@@ -1,19 +1,19 @@
import ScalarToken from './ScalarToken'; import ScalarValue from './ScalarValue';
it('parsed from dec string', () => { it('parsed from dec string', () => {
var op = ScalarToken.parse('123'); var op = ScalarValue.parse('123');
expect(op.base).toBe('dec'); expect(op.base).toBe('dec');
expect(op.value).toBe(123); expect(op.value).toBe(123);
}); });
it('parsed from bin string', () => { it('parsed from bin string', () => {
var op = ScalarToken.parse('0b10'); var op = ScalarValue.parse('0b10');
expect(op.base).toBe('bin'); expect(op.base).toBe('bin');
expect(op.value).toBe(2); expect(op.value).toBe(2);
}); });
it('parsed from hex string', () => { it('parsed from hex string', () => {
var op = ScalarToken.parse('0x10'); var op = ScalarValue.parse('0x10');
expect(op.base).toBe('hex'); expect(op.base).toBe('hex');
expect(op.value).toBe(16); expect(op.value).toBe(16);
}); });

View File

@@ -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);
}
}

View File

@@ -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);
});

View File

@@ -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);
}
}

View File

@@ -2,8 +2,9 @@ import React from 'react';
import formatter from '../../core/formatter'; import formatter from '../../core/formatter';
import BinaryStringView, { FlipBitEventArg } from '../../core/components/BinaryString'; import BinaryStringView, { FlipBitEventArg } from '../../core/components/BinaryString';
import BitwiseResultViewModel from './BitwiseResultViewModel'; import BitwiseResultViewModel from './BitwiseResultViewModel';
import { Expression, ExpressionToken } from '../expression-interfaces'; import { Expression, ExpressionElement } from '../expression-interfaces';
import { OperatorToken, ScalarToken } from '../expression'; import { BitwiseOperator, ScalarValue } from '../expression';
import calc from '../../core/calc';
type BitwiseResultViewProps = { type BitwiseResultViewProps = {
expression: Expression; expression: Expression;
@@ -15,32 +16,51 @@ type BitwiseResultViewState = {
} }
export default class BitwiseResultView extends React.Component<BitwiseResultViewProps, BitwiseResultViewState> { export default class BitwiseResultView extends React.Component<BitwiseResultViewProps, BitwiseResultViewState> {
maxSeenLengthNumberOfBits: number;
constructor(props: BitwiseResultViewProps) { constructor(props: BitwiseResultViewProps) {
super(props); super(props);
this.state = {}; this.state = {};
this.maxSeenLengthNumberOfBits = 0;
} }
render() { 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 <div className='error'>Error: {text}</div>
}
var rows = this.getRows(model!);
return <table className="expression"> return <table className="expression">
<tbody> <tbody>
{rows} {rows}
</tbody> </tbody>
</table> </table>
} }
getRows() : JSX.Element[] { getRows(model: BitwiseResultViewModel): JSX.Element[] {
var model = BitwiseResultViewModel.createModel(this.props.expression, this.props.emphasizeBytes);
return model.items.map((itm, i) => this.maxSeenLengthNumberOfBits = Math.max(model.maxNumberOfBits, this.maxSeenLengthNumberOfBits);
<ExpressionRow
key={i} return model.items.map((itm, i) =>
<ExpressionRow
key={i}
sign={itm.sign} sign={itm.sign}
css={itm.css} css={itm.css}
bitSize={itm.bitSize}
allowFlipBits={itm.allowFlipBits} allowFlipBits={itm.allowFlipBits}
expressionItem={itm.expression} expressionItem={itm.expression}
emphasizeBytes={this.props.emphasizeBytes} emphasizeBytes={this.props.emphasizeBytes}
maxNumberOfBits={model.maxNumberOfBits} maxNumberOfBits={this.maxSeenLengthNumberOfBits}
onBitFlipped={() => this.onBitFlipped()} />); onBitFlipped={() => this.onBitFlipped()} />);
} }
@@ -51,38 +71,42 @@ export default class BitwiseResultView extends React.Component<BitwiseResultView
} }
type ExpressionRowProps = { type ExpressionRowProps = {
sign: string, sign: string,
css: string, css: string,
maxNumberOfBits: number, bitSize: number,
emphasizeBytes: boolean, maxNumberOfBits: number,
allowFlipBits: boolean, emphasizeBytes: boolean,
expressionItem: ExpressionToken, allowFlipBits: boolean,
expressionItem: ExpressionElement,
onBitFlipped: any onBitFlipped: any
} }
class ExpressionRow extends React.Component<ExpressionRowProps> { class ExpressionRow extends React.Component<ExpressionRowProps> {
constructor(props: ExpressionRowProps) { constructor(props: ExpressionRowProps) {
super(props); super(props);
this.state = { operand: null }; this.state = { operand: null };
} }
render() { render() {
const { sign, css, maxNumberOfBits, emphasizeBytes, allowFlipBits } = this.props; const { sign, css, maxNumberOfBits, emphasizeBytes, allowFlipBits } = this.props;
const maxBits = Math.max()
return <tr className={"row-with-bits " + css}> return <tr className={"row-with-bits " + css}>
<td className="sign">{sign}</td> <td className="sign">{sign}</td>
<td className="label">{this.getLabel()}</td> <td className="label">{this.getLabel()}</td>
<td className="bin"> <td className="bin">
<BinaryStringView <BinaryStringView
emphasizeBytes={emphasizeBytes} emphasizeBytes={emphasizeBytes}
binaryString={formatter.padLeft(this.getBinaryString(), maxNumberOfBits, '0')} binaryString={formatter.padLeft(this.getBinaryString(), maxNumberOfBits, '0')}
allowFlipBits={allowFlipBits} allowFlipBits={allowFlipBits}
onFlipBit={args => this.flipBit(args)}/> bitSize={this.props.bitSize}
</td> onFlipBit={args => this.flipBit(args)} />
<td className="other">{this.getAlternative()}</td> </td>
</tr>;; <td className="other">{this.getAlternative()}</td>
<td className="info" data-test-name='ignore'>{this.getInfo(maxNumberOfBits)}</td>
</tr>;;
} }
getBinaryString() : string { getBinaryString(): string {
var v = this.props.expressionItem.evaluate(); var v = this.props.expressionItem.evaluate();
return formatter.numberToString(v.value, 'bin'); return formatter.numberToString(v.value, 'bin');
} }
@@ -91,18 +115,18 @@ class ExpressionRow extends React.Component<ExpressionRowProps> {
// For expressions like |~2 // For expressions like |~2
// TODO: find a better way... // TODO: find a better way...
if(this.props.expressionItem.isOperator) { if (this.props.expressionItem.isOperator) {
const ex = this.props.expressionItem as OperatorToken; const ex = this.props.expressionItem as BitwiseOperator;
return ex.operator + this.getLabelString(ex.getUnderlyingScalarOperand()); return ex.operator + this.getLabelString(ex.getUnderlyingScalarOperand());
} }
return this.getLabelString(this.props.expressionItem.getUnderlyingScalarOperand()); return this.getLabelString(this.props.expressionItem.getUnderlyingScalarOperand());
} }
getAlternative() { getAlternative() {
if(this.props.expressionItem.isOperator) { if (this.props.expressionItem.isOperator) {
const ex = this.props.expressionItem as OperatorToken; const ex = this.props.expressionItem as BitwiseOperator;
const res = ex.evaluate(); const res = ex.evaluate();
return formatter.numberToString(res.value, res.base); return formatter.numberToString(res.value, res.base);
@@ -113,22 +137,45 @@ class ExpressionRow extends React.Component<ExpressionRowProps> {
return formatter.numberToString(v.value, altBase); 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); return formatter.numberToString(op.value, op.base == 'bin' ? 'dec' : op.base);
} }
flipBit(args: FlipBitEventArg) { flipBit(args: FlipBitEventArg) {
const op = this.props.expressionItem.getUnderlyingScalarOperand(); const op = this.props.expressionItem.getUnderlyingScalarOperand();
const { index, binaryString } = args; const { bitIndex: index, binaryStringLength: totalLength } = args;
var arr = binaryString.split(''); if(totalLength > op.bitSize() && (totalLength - index) > op.bitSize()) {
arr[index] = arr[index] == '0' ? '1' : '0'; op.setValue(calc.promoteToBigInt(op.value as number));
var bin = arr.join(''); }
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); op.setValue(newValue);
this.props.onBitFlipped(); 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 <span title={title} style={{cursor:"help"}}>(64-bit BigInt)</span>;
}
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 <span title={title} style={{cursor:"help"}}>(32-bit Number)</span>;
}
return null;
}
} }

View File

@@ -1,7 +1,8 @@
import { ScalarToken, ListOfNumbersExpression, BitwiseOperationExpression, OperatorToken } from '../expression'; import { ScalarValue, ListOfNumbersExpression, BitwiseOperationExpression, BitwiseOperator } from '../expression';
import { ExpressionToken, Expression } from '../expression-interfaces'; import { ExpressionElement, Expression } from '../expression-interfaces';
import calc from '../../core/calc'; import calc from '../../core/calc';
import formatter from '../../core/formatter'; import formatter from '../../core/formatter';
import exp from 'constants';
type Config = { type Config = {
emphasizeBytes: boolean; emphasizeBytes: boolean;
@@ -11,9 +12,10 @@ type Config = {
type ExpressionRowModel = { type ExpressionRowModel = {
sign: string; sign: string;
css: string; css: string;
expression: ExpressionToken; expression: ExpressionElement;
allowFlipBits: boolean; allowFlipBits: boolean;
label: string; label: string;
bitSize: number;
} }
export default class BitwiseResultViewModel { export default class BitwiseResultViewModel {
@@ -43,17 +45,17 @@ export default class BitwiseResultViewModel {
i = 0, len = expr.children.length, i = 0, len = expr.children.length,
ex, m = new BitwiseResultViewModel(config); ex, m = new BitwiseResultViewModel(config);
var prev : ScalarToken | null = null; var prev : ScalarValue | null = null;
for (;i<len;i++) { for (;i<len;i++) {
ex = expr.children[i]; ex = expr.children[i];
if(ex instanceof ScalarToken) { if(ex instanceof ScalarValue) {
m.addScalarRow(ex); m.addScalarRow(ex);
prev = ex; prev = ex;
continue; continue;
} }
var eo = ex as OperatorToken; var eo = ex as BitwiseOperator;
// If it a single NOT expression // If it a single NOT expression
if(eo.isNotExpression) { if(eo.isNotExpression) {
@@ -63,11 +65,11 @@ export default class BitwiseResultViewModel {
prev = notResult; prev = notResult;
} }
else if(eo.isShiftExpression){ else if(eo.isShiftExpression){
prev = eo.evaluate(prev as ScalarToken); prev = eo.evaluate(prev as ScalarValue);
m.addShiftExpressionResultRow(eo, prev); m.addShiftExpressionResultRow(eo, prev);
} else { } else {
prev = eo.evaluate(prev as ScalarToken); prev = eo.evaluate(prev as ScalarValue);
m.addOperatorRow(eo); m.addOperatorRow(eo);
m.addExpressionResultRow(prev); m.addExpressionResultRow(prev);
} }
@@ -77,7 +79,7 @@ export default class BitwiseResultViewModel {
return m; return m;
}; };
addScalarRow(expr: ScalarToken) { addScalarRow(expr: ScalarValue) {
const bits = calc.numberOfBitsDisplayed(expr.value); const bits = calc.numberOfBitsDisplayed(expr.value);
this.maxNumberOfBits = Math.max(bits, this.maxNumberOfBits); this.maxNumberOfBits = Math.max(bits, this.maxNumberOfBits);
this.items.push({ this.items.push({
@@ -85,14 +87,16 @@ export default class BitwiseResultViewModel {
css: '', css: '',
expression: expr, expression: expr,
allowFlipBits: this.allowFlipBits, allowFlipBits: this.allowFlipBits,
label: '' label: '',
bitSize: expr.bitSize(),
}); });
}; };
addOperatorRow(expr: OperatorToken) { addOperatorRow(expr: BitwiseOperator) {
const resultNumber = expr.isNotExpression ? expr.evaluate() : expr.getUnderlyingScalarOperand(); const resultNumber = expr.isNotExpression ? expr.evaluate() : expr.getUnderlyingScalarOperand();
const bits = calc.numberOfBitsDisplayed(resultNumber.value); const bits = calc.numberOfBitsDisplayed(resultNumber.value);
this.maxNumberOfBits = Math.max(bits, this.maxNumberOfBits); this.maxNumberOfBits = Math.max(bits, this.maxNumberOfBits);
this.items.push({ this.items.push({
@@ -100,11 +104,12 @@ export default class BitwiseResultViewModel {
css: '', css: '',
label: this.getLabel(resultNumber), label: this.getLabel(resultNumber),
expression: expr.operand, expression: expr.operand,
allowFlipBits: this.allowFlipBits allowFlipBits: this.allowFlipBits,
bitSize: resultNumber.bitSize()
}); });
}; };
addShiftExpressionResultRow(expr : OperatorToken, resultExpr : ScalarToken) { addShiftExpressionResultRow(expr : BitwiseOperator, resultExpr : ScalarValue) {
const bits = calc.numberOfBitsDisplayed(resultExpr.value); const bits = calc.numberOfBitsDisplayed(resultExpr.value);
this.maxNumberOfBits = Math.max(bits, this.maxNumberOfBits); this.maxNumberOfBits = Math.max(bits, this.maxNumberOfBits);
const child = expr.operand.getUnderlyingScalarOperand(); const child = expr.operand.getUnderlyingScalarOperand();
@@ -113,11 +118,12 @@ export default class BitwiseResultViewModel {
css: 'expression-result', css: 'expression-result',
expression: resultExpr, expression: resultExpr,
allowFlipBits: false, allowFlipBits: false,
label: '' label: '',
bitSize: resultExpr.bitSize()
}); });
}; };
addExpressionResultRow(expr : ScalarToken) { addExpressionResultRow(expr : ScalarValue) {
const bits = calc.numberOfBitsDisplayed(expr.value); const bits = calc.numberOfBitsDisplayed(expr.value);
this.maxNumberOfBits = Math.max(bits, this.maxNumberOfBits); this.maxNumberOfBits = Math.max(bits, this.maxNumberOfBits);
this.items.push({ this.items.push({
@@ -126,10 +132,11 @@ export default class BitwiseResultViewModel {
expression: expr, expression: expr,
allowFlipBits: false, allowFlipBits: false,
label: '', label: '',
bitSize: expr.bitSize()
}); });
}; };
getLabel (op: ScalarToken) : string { getLabel (op: ScalarValue) : string {
return formatter.numberToString(op.value, op.base === 'bin' ? 'dec' : op.base) return formatter.numberToString(op.value, op.base === 'bin' ? 'dec' : op.base)
} }

View File

@@ -0,0 +1,41 @@
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');
});
});

40
src/expression/engine.ts Normal file
View File

@@ -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;

View File

@@ -1,14 +1,14 @@
import { ScalarToken } from "./expression"; import { ScalarValue } from "./expression";
export interface Expression export interface Expression
{ {
expressionString: string; expressionString: string;
} }
export interface ExpressionToken export interface ExpressionElement
{ {
isOperator: boolean; isOperator: boolean;
getUnderlyingScalarOperand: () => ScalarToken; getUnderlyingScalarOperand: () => ScalarValue;
evaluate(operand? : ScalarToken): ScalarToken; evaluate(operand? : ScalarValue): ScalarValue;
} }

View File

@@ -1,4 +1,4 @@
import { parser, ListOfNumbersExpression, BitwiseOperationExpression, ScalarToken, OperatorToken } from "./expression"; import { parser, ListOfNumbersExpression, BitwiseOperationExpression, ScalarValue, BitwiseOperator } from "./expression";
describe("expression parser", () => { describe("expression parser", () => {
@@ -25,8 +25,8 @@ describe("expression parser", () => {
expect(actual).toBeInstanceOf(ListOfNumbersExpression); expect(actual).toBeInstanceOf(ListOfNumbersExpression);
const expr = actual as ListOfNumbersExpression; const expr = actual as ListOfNumbersExpression;
expect(expr.children[0].getUnderlyingScalarOperand().value).toBe(305419896); expect(expr.children[0].getUnderlyingScalarOperand().value.toString()).toBe('305419896');
expect(expr.children[1].getUnderlyingScalarOperand().value).toBe(2863311360); expect(expr.children[1].getUnderlyingScalarOperand().value.toString()).toBe('2863311360');
}) })
it("pares multiple operand expression", () => { it("pares multiple operand expression", () => {
@@ -36,17 +36,17 @@ describe("expression parser", () => {
const first = result.children[0]; const first = result.children[0];
const second = result.children[1]; 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); expect(second).toBeInstanceOf(BitwiseOperator);
var secondOp = second as OperatorToken; var secondOp = second as BitwiseOperator;
expect(secondOp.operator).toBe("^"); expect(secondOp.operator).toBe("^");
expect(secondOp.operand).toBeInstanceOf(ScalarToken); expect(secondOp.operand).toBeInstanceOf(ScalarValue);
var childOp = secondOp.operand as ScalarToken; var childOp = secondOp.operand as ScalarValue;
expect(childOp.value).toBe(2); expect(childOp.value.toString()).toBe('2');
}); });
it("bug", () => { it("bug", () => {

View File

@@ -1,12 +1,11 @@
import ScalarToken from './ScalarToken'; import ScalarValue from './ScalarValue';
import OperatorToken from './OperatorToken' import BitwiseOperator from './BitwiseOperator'
import ListOfNumbersExpression from './ListOfNumbersExpression'; import ListOfNumbersExpression from './ListOfNumbersExpression';
import BitwiseOperationExpression from './BitwiseOperationExpression'; import BitwiseOperationExpression from './BitwiseOperationExpression';
import { Expression, ExpressionToken } from './expression-interfaces'; import { Expression, ExpressionElement } from './expression-interfaces';
import { NumberBase } from '../core/formatter';
export { default as ScalarToken } from './ScalarToken'; export { default as ScalarValue } from './ScalarValue';
export { default as OperatorToken } from './OperatorToken'; export { default as BitwiseOperator } from './BitwiseOperator';
export { default as ListOfNumbersExpression } from './ListOfNumbersExpression'; export { default as ListOfNumbersExpression } from './ListOfNumbersExpression';
export { default as BitwiseOperationExpression } from './BitwiseOperationExpression'; export { default as BitwiseOperationExpression } from './BitwiseOperationExpression';
@@ -46,14 +45,6 @@ class ExpressionParser {
return null; return null;
}; };
parseOperand (input : string) : ScalarToken {
return ScalarToken.parse(input);
};
createOperand (number : number, base : NumberBase) : ScalarToken {
return ScalarToken.create(number, base);
};
addFactory (factory: IExpressionParserFactory) { addFactory (factory: IExpressionParserFactory) {
this.factories.push(factory); this.factories.push(factory);
@@ -70,7 +61,7 @@ class ListOfNumbersExpressionFactory implements IExpressionParserFactory
return input.split(' ') return input.split(' ')
.filter(p => p.length > 0) .filter(p => p.length > 0)
.map(p => ScalarToken.tryParse(p)) .map(p => ScalarValue.tryParse(p))
.filter(n => n == null) .filter(n => n == null)
.length == 0; .length == 0;
}; };
@@ -79,7 +70,7 @@ class ListOfNumbersExpressionFactory implements IExpressionParserFactory
const numbers = input.split(' ') const numbers = input.split(' ')
.filter(p => p.length > 0) .filter(p => p.length > 0)
.map(m => ScalarToken.parse(m)); .map(m => ScalarValue.parse(m));
return new ListOfNumbersExpression(input, numbers); return new ListOfNumbersExpression(input, numbers);
} }
@@ -90,8 +81,8 @@ class BitwiseOperationExpressionFactory implements IExpressionParserFactory {
regex: RegExp; regex: RegExp;
constructor() { constructor() {
this.fullRegex = /^((<<|>>|>>>|\||\&|\^)?(~?-?([b,x,a-f,0-9]+)))+$/; this.fullRegex = /^((<<|>>|>>>|\||\&|\^)?(~?-?([b,x,l,L,a-f,0-9]+)))+$/;
this.regex = /(<<|>>|>>>|\||\&|\^)?(~?-?(?:[b,x,a-f,0-9]+))/g; this.regex = /(<<|>>|>>>|\||\&|\^)?(~?-?(?:[b,x,l,L,a-f,0-9]+))/g;
} }
canCreate (input: string) : boolean { canCreate (input: string) : boolean {
@@ -101,7 +92,7 @@ class BitwiseOperationExpressionFactory implements IExpressionParserFactory {
create (input: string) : Expression { create (input: string) : Expression {
var m : RegExpExecArray | null; var m : RegExpExecArray | null;
const operands : ExpressionToken[] = []; const operands : ExpressionElement[] = [];
const normalizedString = this.normalizeString(input); const normalizedString = this.normalizeString(input);
this.regex.lastIndex = 0; this.regex.lastIndex = 0;
@@ -113,23 +104,23 @@ class BitwiseOperationExpressionFactory implements IExpressionParserFactory {
return new BitwiseOperationExpression(normalizedString, operands) return new BitwiseOperationExpression(normalizedString, operands)
}; };
parseMatch (m:any): ExpressionToken { parseMatch (m:any): ExpressionElement {
var input = m[0], var input = m[0],
operator = m[1], operator = m[1],
num = m[2]; num = m[2];
var parsed = null; var parsed = null;
if(num.indexOf('~') == 0) { if(num.indexOf('~') == 0) {
parsed = new OperatorToken(ScalarToken.parse(num.substring(1)), '~'); parsed = new BitwiseOperator(ScalarValue.parse(num.substring(1)), '~');
} }
else { else {
parsed = ScalarToken.parse(num); parsed = ScalarValue.parse(num);
} }
if(operator == null) { if(operator == null) {
return parsed as OperatorToken; return parsed as BitwiseOperator;
} else { } else {
return new OperatorToken(parsed as ScalarToken, operator); return new BitwiseOperator(parsed as ScalarValue, operator);
} }
}; };

View File

@@ -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");
})
});

View File

@@ -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();
});
});

View File

@@ -1,27 +1,27 @@
import { INT32_MAX_VALUE, INT32_MIN_VALUE } from "../core/const";
import { NumberBase } from "../core/formatter"; import { NumberBase } from "../core/formatter";
import { NumberType } from "../core/types";
const decimalRegex = /^-?\d+$/; const decimalRegex = /^-?\d+[l,L]?$/;
const hexRegex = /^-?0x[0-9,a-f]+$/i; const hexRegex = /^-?0x[0-9,a-f]+[l,L]?$/i;
const binRegex = /^-?0b[0-1]+$/i; const binRegex = /^-?0b[0-1]+[l,L]?$/i;
const operatorRegex = /^<<|>>|<<<|\&|\|\^|~$/;
interface ParserConfig { interface ParserConfig {
regex: RegExp, regex: RegExp,
radix: number,
base: NumberBase, base: NumberBase,
prefix: string|RegExp parse: (input: string) => NumberType
} }
export interface ParsedNumber { export interface ParsedNumber {
value: number; value: number|bigint;
base: NumberBase; base: NumberBase;
input: string; input: string;
} }
var knownParsers : ParserConfig[] = [ var knownParsers : ParserConfig[] = [
{ regex: decimalRegex, radix: 10, base: 'dec', prefix: '^$' }, { regex: decimalRegex, base: 'dec', parse:(s) => parseIntSafe(s, 10) },
{ regex: hexRegex, radix: 16, base: 'hex', prefix:/0x/i }, { regex: hexRegex, base: 'hex', parse:(s) => parseIntSafe(s, 16)},
{ regex: binRegex, radix: 2, base: 'bin', prefix:/0b/i }]; { regex: binRegex, base: 'bin', parse:(s) => parseIntSafe(s, 2) }];
class NumberParser { class NumberParser {
@@ -53,7 +53,7 @@ class NumberParser {
return null; return null;
} }
var value = parseInt(rawInput.replace(parser.prefix, ''), parser.radix); var value = parser.parse(rawInput);
return { return {
value: value, 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); const numberParser = new NumberParser(knownParsers);
export {numberParser}; export {numberParser};

View File

@@ -32,6 +32,7 @@ code { font-size: 1.2em; font-weight: bold; }
.expression .label { font-weight: bold; padding-right: 5px; text-align: right; } .expression .label { font-weight: bold; padding-right: 5px; text-align: right; }
.expression .bin { letter-spacing: 3px; } .expression .bin { letter-spacing: 3px; }
.expression .info { font-size: 0.9em; color: teal; }
.expression .byte { margin: 0 3px; } .expression .byte { margin: 0 3px; }
.expression-result td { border-top: dotted 1px gray; } .expression-result td { border-top: dotted 1px gray; }
.expression { font-size: 1.5em; font-family: monospace } .expression { font-size: 1.5em; font-family: monospace }

View File

@@ -44,7 +44,7 @@ export default class AppState {
this.emphasizeBytes = persistData.emphasizeBytes || true; this.emphasizeBytes = persistData.emphasizeBytes || true;
this.persistedVersion = persistData.version || 0.1; this.persistedVersion = persistData.version || 0.1;
this.wasOldVersion = persistData.version != null && this.version > this.persistedVersion; 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.pageVisitsCount = persistData.pageVisistsCount || 0;
this.donationClicked = persistData.donationClicked; this.donationClicked = persistData.donationClicked;
} }

View File

@@ -28,7 +28,6 @@ export default class AppRoot extends React.Component<AppRootProps, AppRootState>
} }
refresh() { refresh() {
console.log('refresh');
this.setState(this.props.appState); this.setState(this.props.appState);
} }

View File

@@ -10,6 +10,7 @@ import HelpResultView from './components/HelpResultView';
import TextResultView from './components/TextResultView'; import TextResultView from './components/TextResultView';
import WhatsnewResultView from './components/WhatsNewResultView'; import WhatsnewResultView from './components/WhatsNewResultView';
import {STARTUP_COMMAND_KEY} from './startup'; import {STARTUP_COMMAND_KEY} from './startup';
import { INT32_MAX_VALUE, INT64_MAX_VALUE } from '../core/const';
const shellModule = { const shellModule = {
setup: function(appState: AppState, cmd: CmdShell) { setup: function(appState: AppState, cmd: CmdShell) {
@@ -31,6 +32,10 @@ const shellModule = {
appState.toggleDebugMode(); appState.toggleDebugMode();
appState.addCommandResult(c.input, () => <TextResultView text={`Debug Mode: ${appState.debugMode}`}/>); appState.addCommandResult(c.input, () => <TextResultView text={`Debug Mode: ${appState.debugMode}`}/>);
}); });
cmd.command("-max", (c:CommandInput) => {
const text = `Int32 ${INT32_MAX_VALUE}\nInt64 ${INT64_MAX_VALUE}`
appState.addCommandResult(c.input, () => <TextResultView text={text} />)
})
cmd.command("donate", (c:CommandInput) => { cmd.command("donate", (c:CommandInput) => {