Custom-built bitwise calculator, new number types, and improved UI (#46)

This commit is contained in:
Borys Levytskyi
2023-05-10 13:59:20 +03:00
committed by GitHub
parent 38af3721fd
commit 23748831ba
38 changed files with 1099 additions and 582 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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