Fix operator precedence grouping for */ and +/- (#6993)

* Fix operator precedence grouping for */ and +/-

* Add release note for #6993

* Fix exponent associativity and add regression test

---------

Co-authored-by: Hadi Ayache <hadiayache@Hadis-Mac-mini.local>
This commit is contained in:
HadiAyache
2026-02-17 01:23:15 -08:00
committed by GitHub
parent 253530e239
commit f7227f4e62
3 changed files with 42 additions and 12 deletions

View File

@@ -39,6 +39,17 @@ describe('arithmetic', () => {
expect(evalArithmetic('20^3 - 5 * (10 / 2)')).toEqual(7975);
});
test('handles exponent as right-associative', () => {
expect(evalArithmetic('2^3^2')).toEqual(512);
});
test('handles same-precedence operators left-to-right', () => {
expect(evalArithmetic('24 / 3 * 2')).toEqual(16);
expect(evalArithmetic('24 * 3 / 2')).toEqual(36);
expect(evalArithmetic('10 - 2 + 1')).toEqual(9);
expect(evalArithmetic('10 + 2 - 1')).toEqual(11);
});
test('respects current number format', () => {
expect(evalArithmetic('1,222.45')).toEqual(1222.45);

View File

@@ -91,21 +91,34 @@ function parseParens(state: ParserState): AstNode {
return parsePrimary(state);
}
function makeOperatorParser<T extends readonly Operator[]>(...ops: T) {
type ParserFn = (state: ParserState) => AstNode;
return ops.reduce<ParserFn>((prevParser: ParserFn, op: Operator) => {
return (state: ParserState) => {
let node = prevParser(state);
while (nextOperator(state, op)) {
node = { op, left: node, right: prevParser(state) };
}
return node;
};
}, parseParens);
function parseExponent(state: ParserState): AstNode {
let node = parseParens(state);
if (nextOperator(state, '^')) {
node = { op: '^', left: node, right: parseExponent(state) };
}
return node;
}
function parseMultiplicative(state: ParserState): AstNode {
let node = parseExponent(state);
while (char(state) === '*' || char(state) === '/') {
const op = next(state) as '*' | '/';
node = { op, left: node, right: parseExponent(state) };
}
return node;
}
function parseAdditive(state: ParserState): AstNode {
let node = parseMultiplicative(state);
while (char(state) === '+' || char(state) === '-') {
const op = next(state) as '+' | '-';
node = { op, left: node, right: parseMultiplicative(state) };
}
return node;
}
// These operators go from high to low order of precedence
const parseOperator = makeOperatorParser('^', '/', '*', '-', '+');
const parseOperator = parseAdditive;
function parse(expression: string): AstNode {
const state = { str: expression.replace(/\s/g, ''), index: 0 };

View File

@@ -0,0 +1,6 @@
---
category: Bugfix
authors: [HadiAyache]
---
Fix arithmetic expression parsing so operators with the same precedence (`*`/`/`, `+`/`-`) are evaluated left-to-right.