From ae43e3c731bf02c032ba29b6c1affb01e1c6c811 Mon Sep 17 00:00:00 2001 From: Gunther Cox Date: Mon, 10 Aug 2026 07:45:36 -0400 Subject: [PATCH 1/3] Add test case for exponent precedence --- mathparse/mathparse.py | 10 +++++----- tests/test_unary_operations.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/mathparse/mathparse.py b/mathparse/mathparse.py index f029dd9..83b6ed5 100644 --- a/mathparse/mathparse.py +++ b/mathparse/mathparse.py @@ -466,11 +466,11 @@ def to_postfix(tokens: list) -> list: """ precedence = { '.': 5, - '/': 4, - '*': 4, - '+': 3, - '-': 3, - '^': 2, + '^': 4, + '/': 3, + '*': 3, + '+': 2, + '-': 2, '(': 1 } diff --git a/tests/test_unary_operations.py b/tests/test_unary_operations.py index 3ee3071..0b65da8 100644 --- a/tests/test_unary_operations.py +++ b/tests/test_unary_operations.py @@ -10,6 +10,24 @@ def test_exponent(self): self.assertEqual(result, 256) + def test_exponent_precedence_over_addition_and_multiplication(self): + """ + Exponentiation must bind tighter than + and *. + 2 ^ 3 + 4 * 5 = (2^3) + (4*5) = 8 + 20 = 28 + """ + result = mathparse.parse('2 ^ 3 + 4 * 5') + + self.assertEqual(result, 28) + + def test_exponent_precedence_mixed_expression(self): + """ + Exponentiation must bind tighter than * and +. + 2 + 3 * 4 ^ 5 = 2 + 3 * (4^5) = 2 + 3*1024 = 3074 + """ + result = mathparse.parse('2 + 3 * 4 ^ 5') + + self.assertEqual(result, 3074) + def test_without_unary_operator_fre(self): result = mathparse.parse('50 * (85 / 100)', language='FRE') self.assertEqual(result, 42.5) From 632eb0a2aed988363b18bf569125d1ce8a99d6c6 Mon Sep 17 00:00:00 2001 From: Gunther Cox Date: Mon, 10 Aug 2026 07:51:52 -0400 Subject: [PATCH 2/3] Fix exponent value assignment --- mathparse/mathparse.py | 24 +++++++++++++++++++----- tests/test_unary_operations.py | 9 +++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/mathparse/mathparse.py b/mathparse/mathparse.py index 83b6ed5..c4d9ef5 100644 --- a/mathparse/mathparse.py +++ b/mathparse/mathparse.py @@ -477,6 +477,10 @@ def to_postfix(tokens: list) -> list: # Unary functions have a higher precedence than binary operators unary_precedence = max(precedence.values()) + 1 + # Right-associative operators use strict inequality when popping, + # so that e.g. 2^3^2 evaluates as 2^(3^2) = 512, not (2^3)^2 = 64 + right_associative = {'^'} + postfix = [] opstack = [] @@ -497,13 +501,23 @@ def to_postfix(tokens: list) -> list: postfix.append(top_token) top_token = opstack.pop() elif is_binary(token): - # For binary operators, pop operators with higher or - # equal precedence + # Right-associative operators only pop operators of strictly + # higher precedence; left-associative operators also pop equal + # precedence (ensuring left-to-right evaluation order). + if token in right_associative: + should_pop_op = lambda top: ( # noqa: E731 + top in precedence and + precedence[top] > precedence[token] + ) + else: + should_pop_op = lambda top: ( # noqa: E731 + top in precedence and + precedence[top] >= precedence[token] + ) while (opstack != []) and ( ( - opstack[-1] in precedence and token in precedence and ( - precedence[opstack[-1]] >= precedence[token] - ) + opstack[-1] in precedence and token in precedence and + should_pop_op(opstack[-1]) ) or ( is_unary( diff --git a/tests/test_unary_operations.py b/tests/test_unary_operations.py index 0b65da8..3da2210 100644 --- a/tests/test_unary_operations.py +++ b/tests/test_unary_operations.py @@ -28,6 +28,15 @@ def test_exponent_precedence_mixed_expression(self): self.assertEqual(result, 3074) + def test_exponent_right_associative(self): + """ + Exponentiation is right-associative: 2^3^2 = 2^(3^2) = 512, + not (2^3)^2 = 64. + """ + result = mathparse.parse('2 ^ 3 ^ 2') + + self.assertEqual(result, 512) + def test_without_unary_operator_fre(self): result = mathparse.parse('50 * (85 / 100)', language='FRE') self.assertEqual(result, 42.5) From e35e05437a388bb69ef64f88e93359a04fb51bd5 Mon Sep 17 00:00:00 2001 From: Gunther Cox Date: Mon, 10 Aug 2026 07:55:22 -0400 Subject: [PATCH 3/3] Simplify code --- mathparse/mathparse.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/mathparse/mathparse.py b/mathparse/mathparse.py index c4d9ef5..8aaab68 100644 --- a/mathparse/mathparse.py +++ b/mathparse/mathparse.py @@ -501,23 +501,19 @@ def to_postfix(tokens: list) -> list: postfix.append(top_token) top_token = opstack.pop() elif is_binary(token): - # Right-associative operators only pop operators of strictly - # higher precedence; left-associative operators also pop equal - # precedence (ensuring left-to-right evaluation order). - if token in right_associative: - should_pop_op = lambda top: ( # noqa: E731 - top in precedence and - precedence[top] > precedence[token] - ) - else: - should_pop_op = lambda top: ( # noqa: E731 - top in precedence and - precedence[top] >= precedence[token] - ) + # Pop operators with higher precedence, or equal precedence when + # the current token is left-associative (right-associative + # operators like ^ only yield to strictly higher precedence so + # that 2^3^2 = 2^(3^2) = 512 rather than (2^3)^2 = 64). + is_left_assoc = token not in right_associative while (opstack != []) and ( ( - opstack[-1] in precedence and token in precedence and - should_pop_op(opstack[-1]) + opstack[-1] in precedence and token in precedence and ( + precedence[opstack[-1]] > precedence[token] or ( + is_left_assoc and + precedence[opstack[-1]] == precedence[token] + ) + ) ) or ( is_unary(