Skip to content

Commit dc6fd6c

Browse files
committed
gh-157451: Fix gettext.c2py precedence of unary ! before a binary operator
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.
1 parent fb46c67 commit dc6fd6c

3 files changed

Lines changed: 24 additions & 1 deletion

File tree

Lib/gettext.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,9 @@ def _error(value):
119119
def _parse(tokens, priority=-1):
120120
result = ''
121121
nexttok = next(tokens)
122+
negations = 0
122123
while nexttok == '!':
123-
result += 'not '
124+
negations += 1
124125
nexttok = next(tokens)
125126

126127
if nexttok == '(':
@@ -136,6 +137,11 @@ def _parse(tokens, priority=-1):
136137
except ValueError:
137138
raise _error(nexttok) from None
138139
result = '%s%d' % (result, value)
140+
# In C the unary '!' binds tighter than any binary operator, but Python's
141+
# 'not' binds looser, so negate the operand as a parenthesised unit before
142+
# the binary-operator loop below ('!n + 1' means '(!n) + 1', not '!(n + 1)').
143+
for _ in range(negations):
144+
result = '(not %s)' % result
139145
nexttok = next(tokens)
140146

141147
j = 100

Lib/test/test_gettext.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,19 @@ def test_negation(self):
613613
self.assertEqual(f(1), 0)
614614
self.assertEqual(f(2), 0)
615615

616+
def test_negation_precedence(self):
617+
# gh-157451: in C the unary '!' binds tighter than any binary
618+
# operator, so '!n + 1' is '(!n) + 1', not '!(n + 1)'.
619+
f = gettext.c2py('!n + 1')
620+
self.assertEqual(f(0), 2)
621+
self.assertEqual(f(1), 1)
622+
self.assertEqual(gettext.c2py('!n < 3')(0), 1)
623+
self.assertEqual(gettext.c2py('!n * 2')(0), 2)
624+
self.assertEqual(gettext.c2py('!n * 2')(1), 0)
625+
# Double negation still normalises to 0/1 (C semantics).
626+
self.assertEqual(gettext.c2py('!!n')(5), 1)
627+
self.assertEqual(gettext.c2py('!!n')(0), 0)
628+
616629
def test_nested_condition_operator(self):
617630
self.assertEqual(gettext.c2py('n?1?2:3:4')(0), 4)
618631
self.assertEqual(gettext.c2py('n?1?2:3:4')(1), 2)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :func:`gettext.c2py` mistranslating the unary ``!`` operator when it is
2+
followed by a binary operator (for example ``!n + 1``). ``!`` binds tighter
3+
than any binary operator in C, so the operand is now negated as a
4+
self-contained unit, matching the C semantics used by ``Plural-Forms`` rules.

0 commit comments

Comments
 (0)