Skip to content

gh-157451: Fix gettext.c2py precedence of unary ! before a binary operator - #157453

Merged
serhiy-storchaka merged 4 commits into
python:mainfrom
winklemad:gh-157451-gettext-c2py-negation-precedence
Sep 17, 2026
Merged

serhiy-storchaka merged 4 commits into
python:mainfrom
winklemad:gh-157451-gettext-c2py-negation-precedence

Conversation

@winklemad

@winklemad winklemad commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

In C the unary ! operator binds tighter than every binary operator, so !n + 1 means (!n) + 1. gettext.c2py's parser (_parse in Lib/gettext.py) 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.

>>> import gettext
>>> gettext.c2py('!n + 1')(0)   # was 0; C: (!0)+1 == 2
2
>>> gettext.c2py('!n < 3')(0)   # was 0; C: (!0)<3 == 1
1

This selects the wrong plural form for any .mo catalog whose Plural-Forms rule applies ! before a binary operator.

The fix negates 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))).

The existing test_negation only covered !!!n with no trailing binary operator — the one form that happened to work — so the precedence bug wasn't pinned. Added test_negation_precedence covering ! before +/</* and double negation.

AI-tools disclosure (per the devguide policy): I used an AI assistant to help locate the bug and draft the patch and test. I verified the behaviour and the fix against a locally built interpreter, understand the change, and stand by it.

…nary 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.
@winklemad
winklemad requested a review from tomasr8 as a code owner September 13, 2026 19:08
@python-cla-bot

python-cla-bot Bot commented Sep 13, 2026

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

c2py is not a documented function, so the :func: cross-reference failed the
docs nit check. Use an inline literal instead.
@serhiy-storchaka

Copy link
Copy Markdown
Member

Thanks for the catch, this has been there since the parser was written.

The fix can be simpler. Nothing but the ! loop can put not at the start of result, so instead of counting negations it is enough to parenthesize the operand once, just before the binary-operator loop:

    if result.startswith('not '):
        result = '(%s)' % result

Please also add a test with ! as a right operand, e.g. c2py('2 * !n + 1')(0) == 3: currently it raises SyntaxError from the generated code rather than returning a wrong value.

@serhiy-storchaka serhiy-storchaka added needs backport to 3.13 bugs and security fixes needs backport to 3.14 bugs and security fixes needs backport to 3.15 pre-release feature fixes, bugs and security fixes labels Sep 16, 2026
Since only the '!' loop can prefix `not ` onto result, wrap the operand
once before the binary-operator loop instead of counting negations. Also
add a test with '!' as a right operand ('2 * !n + 1'), which previously
generated a SyntaxError.
@winklemad

Copy link
Copy Markdown
Contributor Author

Thanks — that's much cleaner. I've switched to the single-wrap approach (if result.startswith('not '): result = '(%s)' % result) and added the right-operand test: c2py('2 * !n + 1')(0) == 3 (and == 1 for n=1), which raised a SyntaxError before the fix. Full test_gettext passes.

Comment thread Lib/gettext.py Outdated
Comment thread Misc/NEWS.d/next/Library/2026-09-14-00-30-00.gh-issue-157451.k7Qm2x.rst Outdated

@serhiy-storchaka serhiy-storchaka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. 👍 I tweaked a comment and the NEWS entry.

@serhiy-storchaka
serhiy-storchaka enabled auto-merge (squash) September 17, 2026 08:16
@serhiy-storchaka
serhiy-storchaka merged commit 01192a8 into python:main Sep 17, 2026
55 checks passed
@miss-islington-app

Copy link
Copy Markdown

Thanks @winklemad for the PR, and @serhiy-storchaka for merging it 🌮🎉.. I'm working now to backport this PR to: 3.13, 3.14, 3.15.
🐍🍒⛏🤖

@bedevere-app

bedevere-app Bot commented Sep 17, 2026

Copy link
Copy Markdown

GH-157663 is a backport of this pull request to the 3.15 branch.

@bedevere-app bedevere-app Bot removed the needs backport to 3.15 pre-release feature fixes, bugs and security fixes label Sep 17, 2026
@bedevere-app

bedevere-app Bot commented Sep 17, 2026

Copy link
Copy Markdown

GH-157664 is a backport of this pull request to the 3.14 branch.

@bedevere-app bedevere-app Bot removed the needs backport to 3.14 bugs and security fixes label Sep 17, 2026
@bedevere-app

bedevere-app Bot commented Sep 17, 2026

Copy link
Copy Markdown

GH-157665 is a backport of this pull request to the 3.13 branch.

@bedevere-app bedevere-app Bot removed the needs backport to 3.13 bugs and security fixes label Sep 17, 2026
serhiy-storchaka added a commit that referenced this pull request Sep 17, 2026
…nary operator (GH-157453) (GH-157665)

In C the unary "!" operator binds tighter than binary operators, but "not"
binds looser in Python, so "!n + 1" was translated to "not n + 1".  "!" as
the right operand of a binary operator, for example "2 * !n", produced
invalid Python code and raised SyntaxError.
(cherry picked from commit 01192a8)

Co-authored-by: Madan Kumar <[email protected]>
Co-authored-by: Serhiy Storchaka <[email protected]>
serhiy-storchaka added a commit that referenced this pull request Sep 17, 2026
…nary operator (GH-157453) (GH-157664)

In C the unary "!" operator binds tighter than binary operators, but "not"
binds looser in Python, so "!n + 1" was translated to "not n + 1".  "!" as
the right operand of a binary operator, for example "2 * !n", produced
invalid Python code and raised SyntaxError.
(cherry picked from commit 01192a8)

Co-authored-by: Madan Kumar <[email protected]>
Co-authored-by: Serhiy Storchaka <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gettext.c2py mistranslates unary ! before a binary operator (wrong operator precedence)

2 participants