Skip to content

Commit

Permalink
feat: add right-associative support for binary operators
Browse files Browse the repository at this point in the history
fixes #195
  • Loading branch information
6utt3rfly committed Oct 29, 2021
1 parent fa67850 commit 2da8834
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 14 deletions.
8 changes: 8 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@
"browser": true,
"node": true
},
"overrides": [
{
"files": ["test/**/*.js", "*.test.js"],
"parserOptions": {
"ecmaVersion": 7
}
}
],
"rules": {
"semi": 1,
"no-dupe-args": 1,
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,12 @@ const parse_tree = Jsep.parse('1 + 1');

```javascript
// Add a custom ^ binary operator with precedence 10
// (Note that higher number = higher precedence)
jsep.addBinaryOp("^", 10);

// Add exponentiation operator (right-to-left)
jsep.addBinaryOp('**', 11, true);

// Add a custom @ unary operator
jsep.addUnaryOp('@');

Expand Down
22 changes: 18 additions & 4 deletions src/jsep.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,18 @@ export class Jsep {
* @method jsep.addBinaryOp
* @param {string} op_name The name of the binary op to add
* @param {number} precedence The precedence of the binary op (can be a float). Higher number = higher precedence
* @param {boolean} [isRightAssociative=false] whether operator is right-associative
* @returns {Jsep}
*/
static addBinaryOp(op_name, precedence) {
static addBinaryOp(op_name, precedence, isRightAssociative) {
Jsep.max_binop_len = Math.max(op_name.length, Jsep.max_binop_len);
Jsep.binary_ops[op_name] = precedence;
if (isRightAssociative) {
Jsep.right_associative.add(op_name);
}
else {
Jsep.right_associative.delete(op_name);
}
return Jsep;
}

Expand Down Expand Up @@ -110,6 +117,7 @@ export class Jsep {
if (op_name.length === Jsep.max_binop_len) {
Jsep.max_binop_len = Jsep.getMaxKeyLen(Jsep.binary_ops);
}
Jsep.right_associative.delete(op_name);

return Jsep;
}
Expand Down Expand Up @@ -402,7 +410,7 @@ export class Jsep {

// Otherwise, we need to start a stack to properly place the binary operations in their
// precedence structure
biop_info = { value: biop, prec: Jsep.binaryPrecedence(biop)};
biop_info = { value: biop, prec: Jsep.binaryPrecedence(biop), right_a: Jsep.right_associative.has(biop) };

right = this.gobbleToken();

Expand All @@ -421,12 +429,15 @@ export class Jsep {
break;
}

biop_info = { value: biop, prec };
biop_info = { value: biop, prec, right_a: Jsep.right_associative.has(biop) };

cur_biop = biop;

// Reduce: make a binary expression from the three topmost entries.
while ((stack.length > 2) && (prec <= stack[stack.length - 2].prec)) {
const comparePrev = prev => biop_info.right_a && prev.right_a
? prec > prev.prec
: prec <= prev.prec;
while ((stack.length > 2) && comparePrev(stack[stack.length - 2])) {
right = stack.pop();
biop = stack.pop().value;
left = stack.pop();
Expand Down Expand Up @@ -929,6 +940,9 @@ Object.assign(Jsep, {
'*': 10, '/': 10, '%': 10
},

// sets specific binary_ops as right-associative
right_associative: new Set(),

// Additional valid identifier chars, apart from a-z, A-Z and 0-9 (except on the starting char)
additional_identifier_chars: new Set(['$', '_']),

Expand Down
30 changes: 21 additions & 9 deletions test/jsep.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,27 @@ import {testParser, testOpExpression, esprimaComparisonTest, resetJsepDefaults}
}, assert);
});

QUnit.test('Ops', function (assert) {
testOpExpression('1', assert);
testOpExpression('1+2', assert);
testOpExpression('1*2', assert);
testOpExpression('1*(2+3)', assert);
testOpExpression('(1+2)*3', assert);
testOpExpression('(1+2)*3+4-2-5+2/2*3', assert);
testOpExpression('1 + 2- 3* 4 /8', assert);
testOpExpression('\n1\r\n+\n2\n', assert);
QUnit.module('Ops', function (qunit) {
qunit.before(() => {
jsep.addBinaryOp('**', 11, true); // ES2016, right-associative
});
qunit.after(resetJsepDefaults);

[
'1',
'1+2',
'1*2',
'1*(2+3)',
'(1+2)*3',
'(1+2)*3+4-2-5+2/2*3',
'1 + 2- 3* 4 /8',
'\n1\r\n+\n2\n',
'1 + -2',
'-1 + -2 * -3 * 2',
'2 ** 3 ** 4',
'2 ** 3 ** 4 * 5 ** 6 ** 7 * (8 + 9)',
'(2 ** 3) ** 4 * (5 ** 6 ** 7) * (8 + 9)',
].forEach(expr => QUnit.test(`Expr: ${expr}`, assert => testOpExpression(expr, assert)));
});

QUnit.test('Custom operators', function (assert) {
Expand Down
1 change: 1 addition & 0 deletions test/test_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const binOps = {
'*': (a, b) => a * b,
'/': (a, b) => a / b,
'%': (a, b) => a % b,
'**': (a, b) => a ** b, // ES2016
};

export const unOps = {
Expand Down
3 changes: 2 additions & 1 deletion typings/tsd.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,12 @@ declare module 'jsep' {

let unary_ops: { [op: string]: any };
let binary_ops: { [op: string]: number };
let right_associative: Set<string>;
let additional_identifier_chars: Set<string>;
let literals: { [literal: string]: any };
let this_str: string;

function addBinaryOp(operatorName: string, precedence: number): void;
function addBinaryOp(operatorName: string, precedence: number, rightToLeft?: boolean): void;

function addUnaryOp(operatorName: string): void;

Expand Down

0 comments on commit 2da8834

Please sign in to comment.