From dc6fd6c48a7de3530d5309ba18291dfaef23dcbd Mon Sep 17 00:00:00 2001 From: Madan Kumar Date: Mon, 14 Sep 2026 00:37:42 +0530 Subject: [PATCH] gh-157451: Fix gettext.c2py precedence of unary `!` before a binary operator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In C the unary `!` operator binds tighter than every binary operator, so `!n + 1` means `(!n) + 1`. `c2py`'s parser emitted a bare `not ` prefix and then appended the binary operators to the whole string, so `!n + 1` compiled to `not n + 1` — i.e. `not (n + 1)` — because Python's `not` binds looser than arithmetic and comparison operators. Negate the operand as a self-contained parenthesised unit before the binary-operator loop, so `!n + 1` compiles to `(not n) + 1`. Double negation still normalises to 0/1 as in C (`!!n` -> `(not (not n))`). Add a regression test; the existing test only covered `!!!n` with no trailing binary operator, which is the one form that happened to work. --- Lib/gettext.py | 8 +++++++- Lib/test/test_gettext.py | 13 +++++++++++++ .../2026-09-14-00-30-00.gh-issue-157451.k7Qm2x.rst | 4 ++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-14-00-30-00.gh-issue-157451.k7Qm2x.rst diff --git a/Lib/gettext.py b/Lib/gettext.py index 2f77f0e849e9aeb..1fbdacc3eada7d7 100644 --- a/Lib/gettext.py +++ b/Lib/gettext.py @@ -119,8 +119,9 @@ def _error(value): def _parse(tokens, priority=-1): result = '' nexttok = next(tokens) + negations = 0 while nexttok == '!': - result += 'not ' + negations += 1 nexttok = next(tokens) if nexttok == '(': @@ -136,6 +137,11 @@ def _parse(tokens, priority=-1): except ValueError: raise _error(nexttok) from None result = '%s%d' % (result, value) + # In C the unary '!' binds tighter than any binary operator, but Python's + # 'not' binds looser, so negate the operand as a parenthesised unit before + # the binary-operator loop below ('!n + 1' means '(!n) + 1', not '!(n + 1)'). + for _ in range(negations): + result = '(not %s)' % result nexttok = next(tokens) j = 100 diff --git a/Lib/test/test_gettext.py b/Lib/test/test_gettext.py index 9ad37909a8ec4e0..6e09fe8f58a8cf6 100644 --- a/Lib/test/test_gettext.py +++ b/Lib/test/test_gettext.py @@ -613,6 +613,19 @@ def test_negation(self): self.assertEqual(f(1), 0) self.assertEqual(f(2), 0) + def test_negation_precedence(self): + # gh-157451: in C the unary '!' binds tighter than any binary + # operator, so '!n + 1' is '(!n) + 1', not '!(n + 1)'. + f = gettext.c2py('!n + 1') + self.assertEqual(f(0), 2) + self.assertEqual(f(1), 1) + self.assertEqual(gettext.c2py('!n < 3')(0), 1) + self.assertEqual(gettext.c2py('!n * 2')(0), 2) + self.assertEqual(gettext.c2py('!n * 2')(1), 0) + # Double negation still normalises to 0/1 (C semantics). + self.assertEqual(gettext.c2py('!!n')(5), 1) + self.assertEqual(gettext.c2py('!!n')(0), 0) + def test_nested_condition_operator(self): self.assertEqual(gettext.c2py('n?1?2:3:4')(0), 4) self.assertEqual(gettext.c2py('n?1?2:3:4')(1), 2) diff --git a/Misc/NEWS.d/next/Library/2026-09-14-00-30-00.gh-issue-157451.k7Qm2x.rst b/Misc/NEWS.d/next/Library/2026-09-14-00-30-00.gh-issue-157451.k7Qm2x.rst new file mode 100644 index 000000000000000..cbeb545a061b4c2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-14-00-30-00.gh-issue-157451.k7Qm2x.rst @@ -0,0 +1,4 @@ +Fix :func:`gettext.c2py` mistranslating the unary ``!`` operator when it is +followed by a binary operator (for example ``!n + 1``). ``!`` binds tighter +than any binary operator in C, so the operand is now negated as a +self-contained unit, matching the C semantics used by ``Plural-Forms`` rules.