mirror of
https://github.com/BorysLevytskyi/BitwiseCmd.git
synced 2026-01-26 05:34:13 +01:00
Custom-built bitwise calculator, new number types, and improved UI (#46)
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
import ScalarValue from "./ScalarValue";
|
||||
import BitwiseOperator from './BitwiseOperator';
|
||||
import { INT32_MAX_VALUE } from "../core/const";
|
||||
|
||||
it('can apply ~ operand', () => {
|
||||
var op = new ScalarValue(10, 'dec');
|
||||
var expr = new BitwiseOperator(op, "~");
|
||||
|
||||
var result = expr.evaluate();
|
||||
expect(result.value).toBe(-11);
|
||||
expect(result.value.num()).toBe(-11);
|
||||
expect(result.base).toBe('dec');
|
||||
});
|
||||
|
||||
@@ -16,6 +15,6 @@ it('can apply & operand', () => {
|
||||
var op2 = new ScalarValue(4, 'dec');
|
||||
var expr = new BitwiseOperator(op1, "|");
|
||||
var result = expr.evaluate(op2);
|
||||
expect(result.value).toBe(7);
|
||||
expect(result.value.num()).toBe(7);
|
||||
expect(result.base).toBe('dec');
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import calc from '../core/calc';
|
||||
import ScalarValue from './ScalarValue';
|
||||
import engine from './engine';
|
||||
import { ExpressionElement } from './expression-interfaces';
|
||||
|
||||
export default class BitwiseOperator implements ExpressionElement {
|
||||
@@ -29,8 +29,8 @@ export default class BitwiseOperator implements ExpressionElement {
|
||||
var evaluatedOperand = this.operand.evaluate();
|
||||
|
||||
return this.operator == "~"
|
||||
? engine.applyNotOperator(this.operand.getUnderlyingScalarOperand())
|
||||
: engine.applyOperator(operand!, this.operator, evaluatedOperand);
|
||||
? applyNotOperator(this.operand.getUnderlyingScalarOperand())
|
||||
: applyOperator(operand!, this.operator, evaluatedOperand);
|
||||
}
|
||||
|
||||
getUnderlyingScalarOperand() : ScalarValue {
|
||||
@@ -40,4 +40,43 @@ export default class BitwiseOperator implements ExpressionElement {
|
||||
toString(): string {
|
||||
return this.operator + this.operand.toString();
|
||||
}
|
||||
}
|
||||
|
||||
function applyNotOperator(operand: ScalarValue) : ScalarValue {
|
||||
return new ScalarValue(calc.not(operand.value), operand.base);
|
||||
}
|
||||
|
||||
function applyOperator(op1 : ScalarValue, operator: string, op2 : ScalarValue) : ScalarValue {
|
||||
|
||||
const isShift = /<|>/.test(operator);
|
||||
if(!isShift)
|
||||
{
|
||||
if(op1.value.maxBitSize == op2.value.maxBitSize && op1.value.signed != op2.value.signed)
|
||||
throw new Error("Operator `" + operator + "` cannot be applied to signed and unsigned operands of the same " + op2.value.maxBitSize + " -bit size");
|
||||
|
||||
equalizeSize(op1, op2);
|
||||
}
|
||||
|
||||
const result = calc.operation(op1.value, operator, op2.value);
|
||||
return new ScalarValue(result, op2.base);
|
||||
}
|
||||
|
||||
function equalizeSize(op1: ScalarValue, op2: ScalarValue) {
|
||||
|
||||
const n1 = op1.value;
|
||||
const n2 = op2.value;
|
||||
|
||||
if(n1.maxBitSize === n2.maxBitSize)
|
||||
{
|
||||
if(n1.signed === n2.signed) return;
|
||||
|
||||
// Example int and usinged int. Poromoted both to 64 bit
|
||||
op1.setValue(n1.resize(n1.maxBitSize*2).toSigned()).setLabel("converted");
|
||||
op2.setValue(n2.resize(n2.maxBitSize*2).toSigned()).setLabel("converted");
|
||||
}
|
||||
|
||||
if(n1.maxBitSize > n2.maxBitSize)
|
||||
op2.setValue(n2.convertTo(n1)).setLabel("converted");
|
||||
else
|
||||
op1.setValue(n1.convertTo(n2)).setLabel("converted");
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
import ScalarValue from "./ScalarValue";
|
||||
import ListOfNumbersExpression from "./ListOfNumbersExpression";
|
||||
import { numberParser } from "./numberParser";
|
||||
|
||||
it('calculates max bits length', () => {
|
||||
var expr = new ListOfNumbersExpression("10 0xabef 0b01010", [ScalarValue.parse("10"), ScalarValue.parse("0xabef"), ScalarValue.parse("0b01010")])
|
||||
const v1 = new ScalarValue(numberParser.parse("10").value);
|
||||
const v2 = new ScalarValue(numberParser.parse("0xabef").value);
|
||||
const v3 = new ScalarValue(numberParser.parse("0b01010").value);
|
||||
|
||||
var expr = new ListOfNumbersExpression("10 0xabef 0b01010", [v1, v2, v3])
|
||||
expect(expr.maxBitsLength).toBe(16);
|
||||
});
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import exp from "constants";
|
||||
import BitwiseOperator from "./BitwiseOperator";
|
||||
import ScalarValue from "./ScalarValue"
|
||||
|
||||
it('supports evaluation of big int', () => {
|
||||
const v = new ScalarValue(BigInt(1));
|
||||
const op = new BitwiseOperator(new ScalarValue(1), "<<");
|
||||
|
||||
//const r = op.evaluate(v);
|
||||
//expect(r.isBigInt()).toBe(true);
|
||||
//expect(r.value.toString()).toBe("2");
|
||||
})
|
||||
@@ -1,19 +0,0 @@
|
||||
import ScalarValue from './ScalarValue';
|
||||
|
||||
it('parsed from dec string', () => {
|
||||
var op = ScalarValue.parse('123');
|
||||
expect(op.base).toBe('dec');
|
||||
expect(op.value).toBe(123);
|
||||
});
|
||||
|
||||
it('parsed from bin string', () => {
|
||||
var op = ScalarValue.parse('0b10');
|
||||
expect(op.base).toBe('bin');
|
||||
expect(op.value).toBe(2);
|
||||
});
|
||||
|
||||
it('parsed from hex string', () => {
|
||||
var op = ScalarValue.parse('0x10');
|
||||
expect(op.base).toBe('hex');
|
||||
expect(op.value).toBe(16);
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
import { INT32_MAX_VALUE } from "../core/const";
|
||||
import ScalarValue from "./ScalarValue";
|
||||
|
||||
|
||||
it('supports bigint', () => {
|
||||
const int = new ScalarValue(1);
|
||||
const bigint = new ScalarValue(BigInt(1));
|
||||
expect(int.isBigInt()).toBe(false);
|
||||
expect(bigint.isBigInt()).toBe(true);
|
||||
expect(int.bitSize()).toBe(32);
|
||||
expect(bigint.bitSize()).toBe(64);
|
||||
it('converts numbers to bigint', () => {
|
||||
const int32 = new ScalarValue(INT32_MAX_VALUE);
|
||||
const int64 = new ScalarValue(BigInt(INT32_MAX_VALUE+1));
|
||||
|
||||
expect(int32.value.maxBitSize).toBe(32);
|
||||
expect(int64.value.maxBitSize).toBe(64);
|
||||
});
|
||||
@@ -1,9 +1,8 @@
|
||||
import {numberParser} from './numberParser';
|
||||
import { ExpressionElement as ExpressionElement } from './expression-interfaces';
|
||||
import { NumberBase } from '../core/formatter';
|
||||
import { INT32_MAX_VALUE, INT32_MIN_VALUE, INT64_MAX_VALUE, INT64_MIN_VALUE } from '../core/const';
|
||||
import { BoundedNumber, JsNumber, isBoundedNumber, asBoundedNumber } from '../core/types';
|
||||
import calc from '../core/calc';
|
||||
import { INT32_MAX_VALUE, INT32_MIN_VALUE, INT64_MAX_VALUE, INT64_MIN_VALUE, UINT64_MAX_VALUE } from '../core/const';
|
||||
import { Integer, JsNumber, isInteger, asInteger } from '../core/Integer';
|
||||
|
||||
var globalId : number = 1;
|
||||
|
||||
@@ -11,38 +10,33 @@ var globalId : number = 1;
|
||||
// Represents scalar numeric value
|
||||
export default class ScalarValue implements ExpressionElement {
|
||||
id: number;
|
||||
value: JsNumber;
|
||||
value: Integer;
|
||||
base: NumberBase;
|
||||
isOperator: boolean;
|
||||
maxBitSize: number;
|
||||
label: string;
|
||||
|
||||
constructor(value : BoundedNumber | JsNumber, base?: NumberBase) {
|
||||
constructor(value : Integer | JsNumber, base?: NumberBase) {
|
||||
|
||||
if(!isBoundedNumber(value))
|
||||
value = asBoundedNumber(value);
|
||||
if(!isInteger(value))
|
||||
value = asInteger(value);
|
||||
|
||||
ScalarValue.validateSupported(value);
|
||||
|
||||
this.id = globalId++;
|
||||
this.value = 0;
|
||||
this.maxBitSize = 0;
|
||||
this.value = value as Integer;
|
||||
this.base = base || "dec";
|
||||
this.isOperator = false;
|
||||
|
||||
this.setValue(value);
|
||||
this.isOperator = false;
|
||||
this.label = '';
|
||||
}
|
||||
|
||||
setValue(value : Integer) : ScalarValue {
|
||||
this.value = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
bitSize() : number {
|
||||
return this.isBigInt() ? 64 : 32;
|
||||
}
|
||||
|
||||
isBigInt() : boolean {
|
||||
return typeof this.value === 'bigint';
|
||||
}
|
||||
|
||||
setValue(value : BoundedNumber) {
|
||||
this.value = value.value;
|
||||
this.maxBitSize = value.maxBitSize;
|
||||
setLabel(label : string) : ScalarValue {
|
||||
this.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
evaluate() : ScalarValue {
|
||||
@@ -50,39 +44,17 @@ export default class ScalarValue implements ExpressionElement {
|
||||
}
|
||||
|
||||
getUnderlyingScalarOperand() : ScalarValue {
|
||||
return this
|
||||
return this;
|
||||
}
|
||||
|
||||
static validateSupported(num : BoundedNumber) {
|
||||
static validateSupported(num : Integer) {
|
||||
|
||||
if(typeof num.value == "bigint" && (num.value < INT64_MIN_VALUE || num.value > INT64_MAX_VALUE)) {
|
||||
throw new Error(`64-bit numbers are supported in range from ${INT64_MIN_VALUE} to ${INT64_MAX_VALUE}`);
|
||||
if(num.signed && (num.value < INT64_MIN_VALUE || num.value > INT64_MAX_VALUE)) {
|
||||
throw new Error(`Signed 64-bit numbers are supported in range from ${INT64_MIN_VALUE} to ${INT64_MAX_VALUE}. Given number was ${num}`);
|
||||
}
|
||||
|
||||
if(typeof num.value == "number" && (num.value < INT32_MIN_VALUE || num.value > INT32_MAX_VALUE)) {
|
||||
throw new Error(`Numer JavaScript type can only by used for numbers in range from ${INT32_MIN_VALUE} to ${INT32_MAX_VALUE}`)
|
||||
if(!num.signed && (num.value > UINT64_MAX_VALUE)) {
|
||||
throw new Error(`Unisgned 64-bit numbers larger than ${UINT64_MAX_VALUE} are not supported. Given number was ${num}`);
|
||||
}
|
||||
}
|
||||
|
||||
static parse(input: string) : ScalarValue {
|
||||
|
||||
var parsed = ScalarValue.tryParse(input);
|
||||
|
||||
if(parsed == null) {
|
||||
throw new Error(input + " is not a valid number");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
static tryParse(input: string) : ScalarValue | null {
|
||||
|
||||
var parsed = numberParser.parse(input);
|
||||
|
||||
if(!parsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ScalarValue(parsed.value, parsed.base);
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export default class BitwiseResultView extends React.Component<BitwiseResultView
|
||||
key={i}
|
||||
sign={itm.sign}
|
||||
css={itm.css}
|
||||
bitSize={itm.bitSize}
|
||||
bitSize={itm.maxBitSize}
|
||||
allowFlipBits={itm.allowFlipBits}
|
||||
expressionItem={itm.expression}
|
||||
emphasizeBytes={this.props.emphasizeBytes}
|
||||
@@ -88,7 +88,9 @@ class ExpressionRow extends React.Component<ExpressionRowProps> {
|
||||
}
|
||||
render() {
|
||||
const { sign, css, maxNumberOfBits, emphasizeBytes, allowFlipBits } = this.props;
|
||||
const maxBits = Math.max()
|
||||
const scalar = this.props.expressionItem.evaluate();
|
||||
const bin = formatter.numberToString(scalar.value, 'bin').padStart(maxNumberOfBits, '0');
|
||||
const signBitIndex = scalar.value.signed && bin.length >= scalar.value.maxBitSize ? bin.length - scalar.value.maxBitSize : -1;
|
||||
|
||||
return <tr className={"row-with-bits " + css}>
|
||||
<td className="sign">{sign}</td>
|
||||
@@ -96,21 +98,16 @@ class ExpressionRow extends React.Component<ExpressionRowProps> {
|
||||
<td className="bin">
|
||||
<BinaryStringView
|
||||
emphasizeBytes={emphasizeBytes}
|
||||
binaryString={formatter.padLeft(this.getBinaryString(), maxNumberOfBits, '0')}
|
||||
binaryString={bin}
|
||||
allowFlipBits={allowFlipBits}
|
||||
bitSize={this.props.bitSize}
|
||||
signBitIndex={signBitIndex}
|
||||
onFlipBit={args => this.flipBit(args)} />
|
||||
</td>
|
||||
<td className="other">{this.getAlternative()}</td>
|
||||
<td className="info" data-test-name='ignore'>{this.getInfo(maxNumberOfBits)}</td>
|
||||
<td className="info accent1" data-test-name='ignore'>{this.getInfo(maxNumberOfBits)}</td>
|
||||
</tr>;;
|
||||
}
|
||||
|
||||
getBinaryString(): string {
|
||||
var v = this.props.expressionItem.evaluate();
|
||||
return formatter.numberToString(v.value, 'bin');
|
||||
}
|
||||
|
||||
getLabel(): string {
|
||||
|
||||
// For expressions like |~2
|
||||
@@ -146,12 +143,14 @@ class ExpressionRow extends React.Component<ExpressionRowProps> {
|
||||
const op = this.props.expressionItem.getUnderlyingScalarOperand();
|
||||
const { bitIndex: index, binaryStringLength: totalLength } = args;
|
||||
|
||||
if(totalLength > op.bitSize() && (totalLength - index) > op.bitSize()) {
|
||||
op.setValue(calc.promoteTo64Bit(op.value as number));
|
||||
const maxBitSize = op.value.maxBitSize;
|
||||
const space = (totalLength - index - maxBitSize);
|
||||
if(totalLength > op.value.maxBitSize && space > 0) {
|
||||
op.setValue(calc.addSpace(op.value, space));
|
||||
}
|
||||
|
||||
const pad = op.bitSize() - totalLength;
|
||||
const newValue = calc.flipBit(op, pad + index);
|
||||
const pad = op.value.maxBitSize - totalLength;
|
||||
const newValue = calc.flipBit(op.value, pad + index);
|
||||
op.setValue(newValue);
|
||||
this.props.onBitFlipped();
|
||||
}
|
||||
@@ -159,18 +158,21 @@ class ExpressionRow extends React.Component<ExpressionRowProps> {
|
||||
getInfo(maxNumberOfBits:number) {
|
||||
var op = this.props.expressionItem.getUnderlyingScalarOperand();
|
||||
|
||||
if (op.isBigInt())
|
||||
if((op.value.maxBitSize != 32 || op.value.maxBitSize <= maxNumberOfBits) || op.label.length > 0)
|
||||
{
|
||||
const title = `BigInt JavaScript type is used to reprsent this number. All bitwise operations that involve this number have their operands converted to BigInt. BitwiseCmd treats this number as 64-bit number.`;
|
||||
let title = `BitwiseCmd treats this number as ${op.value.maxBitSize}-bit integer`;
|
||||
let text = `${op.value.maxBitSize}-bit `;
|
||||
|
||||
if(!op.value.signed)
|
||||
text += " unsigned ";
|
||||
|
||||
return <span title={title} style={{cursor:"help"}}>(64-bit BigInt)</span>;
|
||||
}
|
||||
if(op.label.length > 0)
|
||||
{
|
||||
text += " (converted)";
|
||||
title += ". This number was converted to facilitate bitwise operation with an operand of a different type";
|
||||
}
|
||||
|
||||
if(op.bitSize() == 32 && maxNumberOfBits >= 32)
|
||||
{
|
||||
const title = "BitwiseCmd treats this number as 32-bit integer. First bit is a sign bit. Try clicking on the first bit and see what will happen.";
|
||||
|
||||
return <span title={title} style={{cursor:"help"}}>(32-bit Number)</span>;
|
||||
return <span title={title} style={{cursor:"help"}}>{text}</span>;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ScalarValue, ListOfNumbersExpression, BitwiseOperationExpression, Bitwi
|
||||
import { ExpressionElement, Expression } from '../expression-interfaces';
|
||||
import calc from '../../core/calc';
|
||||
import formatter from '../../core/formatter';
|
||||
import exp from 'constants';
|
||||
|
||||
type Config = {
|
||||
emphasizeBytes: boolean;
|
||||
@@ -15,7 +14,7 @@ type ExpressionRowModel = {
|
||||
expression: ExpressionElement;
|
||||
allowFlipBits: boolean;
|
||||
label: string;
|
||||
bitSize: number;
|
||||
maxBitSize: number;
|
||||
}
|
||||
|
||||
export default class BitwiseResultViewModel {
|
||||
@@ -40,7 +39,7 @@ export default class BitwiseResultViewModel {
|
||||
}
|
||||
|
||||
static buildBitwiseOperation (expr : BitwiseOperationExpression, config : Config) {
|
||||
|
||||
|
||||
var op = expr.children[0],
|
||||
i = 0, len = expr.children.length,
|
||||
ex, m = new BitwiseResultViewModel(config);
|
||||
@@ -88,7 +87,7 @@ export default class BitwiseResultViewModel {
|
||||
expression: expr,
|
||||
allowFlipBits: this.allowFlipBits,
|
||||
label: '',
|
||||
bitSize: expr.bitSize(),
|
||||
maxBitSize: expr.value.maxBitSize,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -105,7 +104,7 @@ export default class BitwiseResultViewModel {
|
||||
label: this.getLabel(resultNumber),
|
||||
expression: expr.operand,
|
||||
allowFlipBits: this.allowFlipBits,
|
||||
bitSize: resultNumber.bitSize()
|
||||
maxBitSize: resultNumber.value.maxBitSize
|
||||
});
|
||||
};
|
||||
|
||||
@@ -119,7 +118,7 @@ export default class BitwiseResultViewModel {
|
||||
expression: resultExpr,
|
||||
allowFlipBits: false,
|
||||
label: '',
|
||||
bitSize: resultExpr.bitSize()
|
||||
maxBitSize: resultExpr.value.maxBitSize
|
||||
});
|
||||
};
|
||||
|
||||
@@ -132,7 +131,7 @@ export default class BitwiseResultViewModel {
|
||||
expression: expr,
|
||||
allowFlipBits: false,
|
||||
label: '',
|
||||
bitSize: expr.bitSize()
|
||||
maxBitSize: expr.value.maxBitSize
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import ScalarValue from "./ScalarValue";
|
||||
import engine from "./engine";
|
||||
|
||||
describe('evaluate', () => {
|
||||
|
||||
it('treats differently big ints and regular numbers', () => {
|
||||
const bigResult = engine.applyOperator(new ScalarValue(BigInt(2147483647)), "<<", new ScalarValue(BigInt(1)));
|
||||
const result = engine.applyOperator(new ScalarValue(2147483647), "<<", new ScalarValue(1));
|
||||
|
||||
expect(bigResult.value.toString()).toBe("4294967294");
|
||||
expect(result.value.toString()).toBe("-2");
|
||||
});
|
||||
|
||||
it("can execute all operators without errors", () => {
|
||||
|
||||
const operators = [">>", "<<", "|", "&", "^"];
|
||||
|
||||
// >>> not supported by BigInt
|
||||
expect(() => engine.applyOperator(new ScalarValue(1), ">>>", new ScalarValue(2))).not.toThrow();
|
||||
|
||||
operators.forEach(o => {
|
||||
expect(() => engine.applyOperator(new ScalarValue(BigInt(1)), o, new ScalarValue(BigInt(2)))).not.toThrow();
|
||||
expect(() => engine.applyOperator(new ScalarValue(1), o, new ScalarValue(2))).not.toThrow();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('promotes either of operands to BigInt if the other one is', () => {
|
||||
const bint = new ScalarValue(BigInt(1));
|
||||
const int = new ScalarValue(1);
|
||||
|
||||
const rshift = engine.applyOperator(bint, ">>", int);
|
||||
expect(rshift.isBigInt()).toBe(true);
|
||||
expect(rshift.value.toString()).toBe('0');
|
||||
|
||||
const lshift = engine.applyOperator(int, "<<", bint);
|
||||
expect(lshift.isBigInt()).toBe(true);
|
||||
expect(lshift.value.toString()).toBe('2');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
import calc from "../core/calc";
|
||||
import { JsNumber, asBoundedNumber } from "../core/types";
|
||||
import ScalarValue from "./ScalarValue";
|
||||
|
||||
const engine = {
|
||||
applyNotOperator(operand: ScalarValue) : ScalarValue {
|
||||
return new ScalarValue(~operand.value, operand.base);
|
||||
},
|
||||
applyOperator(op1 : ScalarValue, operator: string, op2 : ScalarValue) : ScalarValue {
|
||||
|
||||
const result = evalute(op1.value, operator, op2.value);
|
||||
|
||||
return new ScalarValue(result, op2.base);
|
||||
}
|
||||
};
|
||||
|
||||
function evalute(op1 : JsNumber, operator: string, op2 : JsNumber) : JsNumber{
|
||||
const o1 = equalizeType(op2, op1);
|
||||
const o2 = equalizeType(op1, op2);
|
||||
|
||||
const a = o1 as any;
|
||||
const b = o2 as any;
|
||||
|
||||
switch(operator) {
|
||||
case ">>": return (a >> b) as (JsNumber);
|
||||
case ">>>": return (a >>> b) as (JsNumber);
|
||||
case "<<": return calc.rshift(asBoundedNumber(o1), o2).value;
|
||||
case "&": return (b & a) as (JsNumber);
|
||||
case "|": return (b | a) as (JsNumber);
|
||||
case "^": return (b ^ a) as (JsNumber);
|
||||
default: throw new Error(operator + " operator is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
function equalizeType(source : JsNumber, dest : JsNumber) : JsNumber {
|
||||
|
||||
return typeof source == 'bigint' && typeof dest != 'bigint'
|
||||
? BigInt(dest)
|
||||
: dest;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default engine;
|
||||
@@ -1,15 +1,16 @@
|
||||
import { parser, ListOfNumbersExpression, BitwiseOperationExpression, ScalarValue, BitwiseOperator } from "./expression";
|
||||
import { random } from "../core/utils";
|
||||
import { INT32_MAX_VALUE } from "../core/const";
|
||||
|
||||
describe("expression parser", () => {
|
||||
|
||||
it("pares list of number expression", () => {
|
||||
it("parses list of number expression", () => {
|
||||
var result = parser.parse("1 2 3");
|
||||
expect(result).toBeInstanceOf(ListOfNumbersExpression);
|
||||
});
|
||||
|
||||
it("doesn't list of numbers in case of bad numbers", () => {
|
||||
expect(parser.parse("1 2 z")).toBeNull();
|
||||
//expect(parser.parse("-")).toBeNull();
|
||||
expect(parser.parse("")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -38,7 +39,7 @@ describe("expression parser", () => {
|
||||
|
||||
expect(first).toBeInstanceOf(ScalarValue);
|
||||
|
||||
expect((first as ScalarValue).value).toBe(1);
|
||||
expect((first as ScalarValue).value.toString()).toBe("1");
|
||||
|
||||
expect(second).toBeInstanceOf(BitwiseOperator);
|
||||
var secondOp = second as BitwiseOperator;
|
||||
@@ -53,4 +54,122 @@ describe("expression parser", () => {
|
||||
var result = parser.parse("1|~2") as BitwiseOperationExpression;
|
||||
expect(result.children.length).toBe(2);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe("comparison with nodejs engine", () => {
|
||||
|
||||
it('set 32-bit', () => {
|
||||
|
||||
const inputs = [
|
||||
"1485578196>>14",
|
||||
"921979543<<31",
|
||||
"1123|324",
|
||||
"213&9531",
|
||||
"120^442161",
|
||||
"1<<7",
|
||||
"2>>>8",
|
||||
"2<<7"
|
||||
];
|
||||
|
||||
inputs.forEach(i => testBinary(i, i));
|
||||
});
|
||||
|
||||
it('random: two inbary strings 64-bit', () => {
|
||||
|
||||
const signs = ["|", "&", "^", "<<", ">>", ">>>"]
|
||||
|
||||
for(var i =0; i<1000; i++){
|
||||
|
||||
const sign = signs[random(0, signs.length-1)];
|
||||
const isShift = sign.length > 1;
|
||||
const op1 = random(-INT32_MAX_VALUE, INT32_MAX_VALUE);
|
||||
const op2 = isShift ? random(0, 31) : random(-INT32_MAX_VALUE, INT32_MAX_VALUE);
|
||||
|
||||
const input = op1.toString() + sign + op2.toString();
|
||||
|
||||
testBinary(input, input);
|
||||
}
|
||||
});
|
||||
|
||||
it('random: 64 and 32-bit', () => {
|
||||
|
||||
for(var i =0; i<1000; i++){
|
||||
|
||||
const num = random(-Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
|
||||
const actualInput = "~" + num.toString();
|
||||
const expectedInput = num > INT32_MAX_VALUE ? `~BigInt("${num}")` : actualInput;
|
||||
const expected = eval(expectedInput).toString();
|
||||
|
||||
let actual = "";
|
||||
|
||||
try
|
||||
{
|
||||
const expr = parser.parse(actualInput) as BitwiseOperationExpression;
|
||||
const bo = (expr.children[0] as BitwiseOperator);
|
||||
const res = bo.evaluate();
|
||||
actual = res.value.toString();
|
||||
|
||||
if(actual != expected) {
|
||||
const uop = bo.getUnderlyingScalarOperand();
|
||||
console.log(`Expected:${expectedInput}\nActual:${actualInput}\n${uop.value} ${uop.value.maxBitSize}\n${res.value} ${typeof res.value} ${res.value.maxBitSize}`)
|
||||
}
|
||||
}
|
||||
catch(err)
|
||||
{
|
||||
|
||||
console.log(`Error:\nExpected:${expectedInput}\nActual:${actualInput}\n${typeof actualInput}`);
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('random: two inbary strings 64-bit', () => {
|
||||
|
||||
const signs = ["|", "&", "^"]
|
||||
|
||||
for(var i =0; i<1000; i++){
|
||||
const sign = signs[random(0, signs.length-1)];
|
||||
const isShift = sign.length > 1;
|
||||
const op1 = random(-Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
|
||||
const op2 = isShift ? random(0, 63) : Number.MAX_SAFE_INTEGER;
|
||||
|
||||
const actualInput = `${op1}l${sign}${op2}l`;
|
||||
const expectedInput = `BigInt("${op1}")${sign}BigInt("${op2}")`;
|
||||
|
||||
testBinary(expectedInput, actualInput);
|
||||
}
|
||||
});
|
||||
|
||||
function testBinary(expectedInput:string, actualInput: string) {
|
||||
|
||||
const expected = eval(expectedInput).toString();
|
||||
|
||||
let actual = "";
|
||||
|
||||
try
|
||||
{
|
||||
var expr = parser.parse(actualInput) as BitwiseOperationExpression;
|
||||
|
||||
var op1 = expr.children[0] as ScalarValue;
|
||||
var op2 = expr.children[1] as BitwiseOperator;
|
||||
|
||||
actual = op2.evaluate(op1).value.toString();
|
||||
const equals = actual === expected;
|
||||
|
||||
if(!equals)
|
||||
{
|
||||
console.log(`Expected:${expectedInput}\n$Actual:${actualInput}\nop1:${typeof op1.value}\nop2:${typeof op2.getUnderlyingScalarOperand().value}`);
|
||||
}
|
||||
}
|
||||
catch(err)
|
||||
{
|
||||
console.log(`Error:\nExpected:${expectedInput}\nActual:${actualInput}\n${typeof actualInput}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
});
|
||||
@@ -3,6 +3,8 @@ import BitwiseOperator from './BitwiseOperator'
|
||||
import ListOfNumbersExpression from './ListOfNumbersExpression';
|
||||
import BitwiseOperationExpression from './BitwiseOperationExpression';
|
||||
import { Expression, ExpressionElement } from './expression-interfaces';
|
||||
import { numberParser, numberRegexString } from './numberParser';
|
||||
import { parse } from 'path';
|
||||
|
||||
export { default as ScalarValue } from './ScalarValue';
|
||||
export { default as BitwiseOperator } from './BitwiseOperator';
|
||||
@@ -16,6 +18,7 @@ interface IExpressionParserFactory {
|
||||
|
||||
class ExpressionParser {
|
||||
factories: IExpressionParserFactory[];
|
||||
|
||||
constructor() {
|
||||
this.factories = [];
|
||||
};
|
||||
@@ -36,6 +39,7 @@ class ExpressionParser {
|
||||
var i = 0, l = this.factories.length, factory;
|
||||
|
||||
for(;i<l;i++) {
|
||||
|
||||
factory = this.factories[i];
|
||||
|
||||
if(factory.canCreate(trimmed) == true){
|
||||
@@ -61,8 +65,8 @@ class ListOfNumbersExpressionFactory implements IExpressionParserFactory
|
||||
|
||||
return input.split(' ')
|
||||
.filter(p => p.length > 0)
|
||||
.map(p => ScalarValue.tryParse(p))
|
||||
.filter(n => n == null)
|
||||
.map(p => numberParser.caseParse(p))
|
||||
.filter(n => n == false)
|
||||
.length == 0;
|
||||
};
|
||||
|
||||
@@ -70,7 +74,7 @@ class ListOfNumbersExpressionFactory implements IExpressionParserFactory
|
||||
|
||||
const numbers = input.split(' ')
|
||||
.filter(p => p.length > 0)
|
||||
.map(m => ScalarValue.parse(m));
|
||||
.map(m => parseScalarValue(m));
|
||||
|
||||
return new ListOfNumbersExpression(input, numbers);
|
||||
}
|
||||
@@ -81,8 +85,8 @@ class BitwiseOperationExpressionFactory implements IExpressionParserFactory {
|
||||
regex: RegExp;
|
||||
|
||||
constructor() {
|
||||
this.fullRegex = /^((<<|>>|>>>|\||\&|\^)?(~?-?([b,x,l,L,a-f,0-9]+)))+$/;
|
||||
this.regex = /(<<|>>|>>>|\||\&|\^)?(~?-?(?:[b,x,l,L,a-f,0-9]+))/g;
|
||||
this.fullRegex = /^((<<|>>|>>>|\||\&|\^)?(~?-?([b,x,l,s,u,a-f,0-9]+)))+$/i;
|
||||
this.regex = /(<<|>>|>>>|\||\&|\^)?(~?-?(?:[b,x,l,s,u,,a-f,0-9]+))/gi;
|
||||
}
|
||||
|
||||
canCreate (input: string) : boolean {
|
||||
@@ -104,17 +108,19 @@ class BitwiseOperationExpressionFactory implements IExpressionParserFactory {
|
||||
return new BitwiseOperationExpression(normalizedString, operands)
|
||||
};
|
||||
|
||||
parseMatch (m:any): ExpressionElement {
|
||||
parseMatch (m:RegExpExecArray): ExpressionElement {
|
||||
|
||||
var input = m[0],
|
||||
operator = m[1],
|
||||
num = m[2];
|
||||
|
||||
var parsed = null;
|
||||
|
||||
if(num.indexOf('~') == 0) {
|
||||
parsed = new BitwiseOperator(ScalarValue.parse(num.substring(1)), '~');
|
||||
parsed = new BitwiseOperator(parseScalarValue(num.substring(1)), '~');
|
||||
}
|
||||
else {
|
||||
parsed = ScalarValue.parse(num);
|
||||
parsed = parseScalarValue(num);
|
||||
}
|
||||
|
||||
if(operator == null) {
|
||||
@@ -129,6 +135,13 @@ class BitwiseOperationExpressionFactory implements IExpressionParserFactory {
|
||||
};
|
||||
}
|
||||
|
||||
function parseScalarValue(input : string) : ScalarValue {
|
||||
const n = numberParser.parse(input);
|
||||
var sv = new ScalarValue(n.value, n.base);
|
||||
if(sv.value.maxBitSize != n.value.maxBitSize) throw new Error("Gotcha!");
|
||||
return sv;
|
||||
}
|
||||
|
||||
var parser = new ExpressionParser();
|
||||
parser.addFactory(new ListOfNumbersExpressionFactory());
|
||||
parser.addFactory(new BitwiseOperationExpressionFactory());
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import exp from 'constants';
|
||||
import { asIntN } from '../core/utils';
|
||||
import {numberParser, ParsedNumber} from './numberParser';
|
||||
|
||||
describe("parser", () => {
|
||||
@@ -7,12 +9,25 @@ describe("parser", () => {
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
var number = result as ParsedNumber;
|
||||
expect(number.value.value).toBe(10);
|
||||
expect(number.value.maxBitSize).toBe(32);
|
||||
expect(asIntN(number.value.num())).toBe(10);
|
||||
expect(number.base).toBe('dec');
|
||||
expect(number.input).toBe('10');
|
||||
});
|
||||
|
||||
it('parses bigint numbers', () => {
|
||||
it('parses negative numbers', () => {
|
||||
expect(numberParser.parse('-1')?.value.num()).toBe(-1);
|
||||
expect(numberParser.parse('-0b10')?.value.num()).toBe(-2);
|
||||
expect(numberParser.parse('-0x10')?.value.num()).toBe(-16);
|
||||
});
|
||||
|
||||
it('parses 64-bit numbers by size', () => {
|
||||
const dec = numberParser.parse('3433374389036042');
|
||||
expect(dec?.value.toString()).toBe('3433374389036042');
|
||||
expect(dec?.value.maxBitSize).toBe(64);
|
||||
});
|
||||
|
||||
it('parses 64-bit numbers with L notation', () => {
|
||||
const dec = numberParser.parse('10L');
|
||||
expect(dec).not.toBeNull();
|
||||
|
||||
@@ -20,25 +35,9 @@ describe("parser", () => {
|
||||
expect(typeof dec?.value.value).toBe("bigint");
|
||||
expect(dec?.base).toBe('dec');
|
||||
expect(dec?.input).toBe('10L');
|
||||
|
||||
const bin = numberParser.parse('0b10l');
|
||||
expect(bin).not.toBeNull();
|
||||
|
||||
expect(bin?.value.value).toBe(BigInt(2));
|
||||
expect(typeof bin?.value.value).toBe("bigint");
|
||||
expect(bin?.base).toBe('bin');
|
||||
expect(bin?.input).toBe('0b10l');
|
||||
|
||||
const hex = numberParser.parse('0xfL');
|
||||
expect(hex).not.toBeNull();
|
||||
|
||||
expect(hex?.value.value.toString()).toBe(BigInt(15).toString());
|
||||
expect(typeof hex?.value.value).toBe("bigint");
|
||||
expect(hex?.base).toBe('hex');
|
||||
expect(hex?.input).toBe('0xfL');
|
||||
expect(dec?.value.maxBitSize).toBe(64);
|
||||
});
|
||||
|
||||
|
||||
it('switches to bigint if value exceeds max safe int', () => {
|
||||
const unsafeInt = BigInt(Number.MAX_SAFE_INTEGER)+BigInt(1);
|
||||
|
||||
@@ -76,7 +75,8 @@ describe("parser", () => {
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
var number = result as ParsedNumber;
|
||||
expect(number.value.value).toBe(171);
|
||||
expect(number.value.maxBitSize).toBe(32);
|
||||
expect(number.value.num()).toBe(171);
|
||||
expect(number.base).toBe('hex');
|
||||
expect(number.input).toBe('0xab');
|
||||
});
|
||||
@@ -86,39 +86,82 @@ describe("parser", () => {
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
var number = result as ParsedNumber;
|
||||
expect(number.value.value).toBe(6);
|
||||
expect(number.value.num()).toBe(6);
|
||||
expect(number.base).toBe('bin');
|
||||
expect(number.input).toBe('0b0110');
|
||||
});
|
||||
|
||||
it('returns null on bad inputs', () => {
|
||||
expect(numberParser.parse('abc')).toBeNull();
|
||||
expect(numberParser.parse('')).toBeNull();
|
||||
expect(numberParser.caseParse('abc')).toBe(false);
|
||||
expect(numberParser.caseParse('')).toBe(false);
|
||||
expect(numberParser.caseParse('-1u')).toBe(true);
|
||||
expect(() => numberParser.parse('abc')).toThrowError("abc is not a number");
|
||||
expect(() => (numberParser.parse(''))).toThrowError('input is null or empty');
|
||||
});
|
||||
|
||||
it('parses big int', () => {
|
||||
var v = numberParser.parse('1l')?.value
|
||||
expect(typeof v?.value).toBe("bigint");
|
||||
expect(v?.value.toString()).toBe("1");
|
||||
expect(v?.num()).toBe(1);
|
||||
});
|
||||
|
||||
xit('parses single', () => {
|
||||
it('fits usigned int32 max value into 32-bit data type', () => {
|
||||
const n1 = numberParser.parse("4294967295u");
|
||||
const n2 = numberParser.parse("4294967296u");
|
||||
|
||||
expect(n1?.value.maxBitSize).toBe(32);
|
||||
expect(n2?.value.maxBitSize).toBe(64);
|
||||
expect(n1?.value.signed).toBe(false);
|
||||
expect(n2?.value.signed).toBe(false);
|
||||
})
|
||||
|
||||
it('parses single', () => {
|
||||
var v = numberParser.parse('1s')?.value
|
||||
expect(typeof v?.value).toBe("number");
|
||||
expect(v?.maxBitSize).toBe(16);
|
||||
expect(v?.value.toString()).toBe("1");
|
||||
expect(v?.num()).toBe(1);
|
||||
expect(v?.signed).toBe(true);
|
||||
|
||||
var v2 = numberParser.parse('1i8')?.value
|
||||
expect(v2).toEqual(v);
|
||||
//var v2 = numberParser.parse('1i8')?.value
|
||||
//expect(v2).toEqual(v);
|
||||
});
|
||||
|
||||
xit('parses byte', () => {
|
||||
var v = numberParser.parse('1b')?.value
|
||||
expect(typeof v?.value).toBe("number");
|
||||
expect(v?.maxBitSize).toBe(16);
|
||||
expect(v?.value.toString()).toBe("1");
|
||||
it('cannot parse negative usigned', () => {
|
||||
expect(() => numberParser.parse('-1u')).toThrowError("-1u unsigned integer cannot be negative");
|
||||
});
|
||||
|
||||
var v2 = numberParser.parse('1i16')?.value
|
||||
expect(v2).toEqual(v);
|
||||
it('parses usigned single', () => {
|
||||
var v = numberParser.parse('1us')?.value
|
||||
expect(v?.maxBitSize).toBe(16);
|
||||
expect(v?.num()).toBe(1);
|
||||
expect(v?.signed).toBe(false);
|
||||
});
|
||||
|
||||
it('parses usigned int32', () => {
|
||||
var v = numberParser.parse('1u')?.value
|
||||
expect(v?.maxBitSize).toBe(32);
|
||||
expect(v?.num()).toBe(1);
|
||||
expect(v?.signed).toBe(false);
|
||||
});
|
||||
|
||||
it('parses usigned byte', () => {
|
||||
var v = numberParser.parse('1ub')?.value
|
||||
expect(v?.maxBitSize).toBe(8);
|
||||
expect(v?.num()).toBe(1);
|
||||
expect(v?.signed).toBe(false);
|
||||
});
|
||||
|
||||
it('parses usigned long', () => {
|
||||
var v = numberParser.parse('1ul')?.value
|
||||
expect(v?.maxBitSize).toBe(64);
|
||||
expect(v?.num()).toBe(1);
|
||||
expect(v?.signed).toBe(false);
|
||||
});
|
||||
|
||||
it('parses byte', () => {
|
||||
var v = numberParser.parse('1b')?.value
|
||||
expect(v?.maxBitSize).toBe(8);
|
||||
expect(v?.num()).toBe(1);
|
||||
|
||||
//var v2 = numberParser.parse('1i16')?.value
|
||||
//expect(v2?.num()).toEqual(v?.num());
|
||||
});
|
||||
});
|
||||
@@ -1,94 +1,83 @@
|
||||
import { INT32_MAX_VALUE, INT32_MIN_VALUE } from "../core/const";
|
||||
import { INT32_MAX_VALUE, UINT32_MAX_VALUE } from "../core/const";
|
||||
import { NumberBase } from "../core/formatter";
|
||||
import { BoundedNumber, asBoundedNumber } from "../core/types";
|
||||
import { Integer } from "../core/Integer";
|
||||
|
||||
// byte -i8 or b
|
||||
// single - i16 or s
|
||||
|
||||
const decimalRegex = /^-?\d+[l,L]?$/;
|
||||
const hexRegex = /^-?0x[0-9,a-f]+[l,L]?$/i;
|
||||
const binRegex = /^-?0b[0-1]+[l,L]?$/i;
|
||||
|
||||
interface ParserConfig {
|
||||
regex: RegExp,
|
||||
base: NumberBase,
|
||||
parse: (input: string) => BoundedNumber
|
||||
}
|
||||
const numberRegexString = "-?([0-9]+|0b[0-1]+|0x[0-9,a-f]+)(l|s|b|ul|us|ub|u)?";
|
||||
const numberRegexFullString = "^"+numberRegexString+"$"
|
||||
|
||||
export interface ParsedNumber {
|
||||
value: BoundedNumber;
|
||||
value: Integer;
|
||||
base: NumberBase;
|
||||
input: string;
|
||||
}
|
||||
|
||||
var knownParsers : ParserConfig[] = [
|
||||
{ regex: decimalRegex, base: 'dec', parse:(s) => parseIntSafe(s, 10) },
|
||||
{ regex: hexRegex, base: 'hex', parse:(s) => parseIntSafe(s, 16)},
|
||||
{ regex: binRegex, base: 'bin', parse:(s) => parseIntSafe(s, 2) }];
|
||||
|
||||
|
||||
class NumberParser {
|
||||
|
||||
numberRegexString: string;
|
||||
|
||||
parsers: ParserConfig[];
|
||||
|
||||
constructor(parsers: ParserConfig[])
|
||||
constructor()
|
||||
{
|
||||
this.parsers = parsers;
|
||||
this.numberRegexString = numberRegexString;
|
||||
}
|
||||
|
||||
parse (input : string) : ParsedNumber | null {
|
||||
return this.parsers.map(p => this.applyParser(p, input)).reduce((c, n) => c || n);
|
||||
};
|
||||
caseParse(input : string) {
|
||||
const regex = new RegExp(numberRegexFullString);
|
||||
return regex.test(input);
|
||||
}
|
||||
|
||||
parseOperator (input: string) : string | null {
|
||||
var m = input.match(input);
|
||||
parse (input : string) : ParsedNumber {
|
||||
|
||||
if(input.length == 0) throw new Error("input is null or empty");
|
||||
|
||||
const regex = new RegExp(numberRegexFullString, "i");
|
||||
|
||||
if(m == null || m.length == 0) {
|
||||
return null;
|
||||
}
|
||||
const m = regex.exec(input);
|
||||
|
||||
return m[0];
|
||||
};
|
||||
if(m == null || m.length == 0)
|
||||
throw new Error(input + " is not a number");
|
||||
|
||||
applyParser(parser : ParserConfig, rawInput: string) : ParsedNumber | null {
|
||||
|
||||
if(!parser.regex.test(rawInput)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var value = parser.parse(rawInput);
|
||||
|
||||
return {
|
||||
const value = parseInteger(m[0], m[1], m[2] || '');
|
||||
|
||||
return {
|
||||
value: value,
|
||||
base: parser.base,
|
||||
input: rawInput
|
||||
}
|
||||
base: getBase(input),
|
||||
input: input
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function parseInteger(input : string, numberPart: string, suffix: string) : Integer {
|
||||
|
||||
const isNegative = input.startsWith('-');
|
||||
let num = BigInt(numberPart);
|
||||
const signed = !suffix.startsWith('u');
|
||||
|
||||
if(!signed && isNegative)
|
||||
throw new Error(input + " unsigned integer cannot be negative");
|
||||
|
||||
const size = getSizeBySuffix(suffix, num, signed);
|
||||
return new Integer(isNegative ? -num : num, size, signed);
|
||||
}
|
||||
|
||||
function getSizeBySuffix(suffix: string, value : bigint, signed: boolean) {
|
||||
|
||||
const max32 = signed ? INT32_MAX_VALUE : UINT32_MAX_VALUE;
|
||||
|
||||
switch(suffix.replace('u', '').toLowerCase()) {
|
||||
case 'l': return 64;
|
||||
case 's': return 16;
|
||||
case 'b': return 8;
|
||||
default: return value > max32 ? 64 : 32;
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_SAFE_INTn = BigInt(INT32_MAX_VALUE);
|
||||
const MIN_SAFE_INTn = BigInt(INT32_MIN_VALUE);
|
||||
function getBase(input: string): NumberBase {
|
||||
|
||||
function parseIntSafe(input : string, radix: number) : BoundedNumber {
|
||||
|
||||
const bigIntStr = input.replace('-', '').replace('l', '').replace('L', '');
|
||||
let bigInt = BigInt(bigIntStr);
|
||||
const isNegative = input.startsWith('-');
|
||||
const isBigInt = input.toLowerCase().endsWith('l');
|
||||
|
||||
if(isNegative) bigInt *= BigInt(-1);
|
||||
|
||||
if(isBigInt) return asBoundedNumber(bigInt);
|
||||
|
||||
if(bigInt > MAX_SAFE_INTn)
|
||||
return asBoundedNumber(bigInt);
|
||||
|
||||
if(bigInt < MIN_SAFE_INTn)
|
||||
return asBoundedNumber(bigInt);
|
||||
|
||||
return asBoundedNumber(parseInt(input.replace(/0(x|b)/, ''), radix));
|
||||
if(input.indexOf('0b') > -1) return 'bin';
|
||||
if(input.indexOf('0x') > -1) return 'hex';
|
||||
return 'dec';
|
||||
}
|
||||
|
||||
const numberParser = new NumberParser(knownParsers);
|
||||
const numberParser = new NumberParser();
|
||||
|
||||
export {numberParser};
|
||||
export {numberParser, numberRegexString};
|
||||
|
||||
Reference in New Issue
Block a user