Skip to content

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

Description

@winklemad

Bug report

gettext.c2py() compiles a C plural-form expression into an equivalent Python function. In C the unary ! operator binds tighter than every binary operator, so !n + 1 means (!n) + 1. But c2py emits a bare not prefix (Lib/gettext.py, in _parse) and then appends the binary operators onto the whole string. Because Python's not binds looser than arithmetic/comparison operators, !n + 1 is compiled to not n + 1, i.e. not (n + 1) — a different expression.

Reproduction

import gettext

gettext.c2py('!n + 1')(0)   # -> 0   ; C says (!0)+1 == 2
gettext.c2py('!n + 1')(1)   # -> 0   ; C says (!1)+1 == 1
gettext.c2py('!n < 3')(0)   # -> 0   ; C: (!0) < 3 == 1  (!n is 0/1, always < 3)
gettext.c2py('!n*2')(0)     # -> 1   ; C: (!0)*2 == 2

The generated source for '!n + 1' is int(not n + 1) (i.e. int(not (n + 1))) instead of int((not n) + 1).

Only two forms happen to work today: a bare !x with nothing after it (which is what test_negation covers, e.g. !!!n), and !(...) with no trailing binary operator.

Impact

c2py is public API and is also used by GNUTranslations._parse to compile the plural= rule from a catalog's Plural-Forms header, so a valid .mo whose plural rule applies ! before a binary operator selects the wrong plural form from ngettext().

Why it's not caught

test_gettext.test_negation only exercises !!!n (no following binary operator), which is exactly the case that works, so the precedence bug is not pinned by any test. The neighbouring cases (chained comparisons, ///) are handled deliberately; unary ! was overlooked.

Fix

Negate the operand as a self-contained parenthesised unit ((not <operand>)) before the binary-operator loop, so !n + 1 compiles to (not n) + 1. Double negation still normalises to 0/1 (!!n(not (not n))). I have a small patch + tests and will open a PR referencing this issue.

Reproduced on main (3.16.0a0). Found with AI assistance; I've verified and understand the behaviour and the fix.

Linked PRs

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions