Skip to content

Py_MIN(), Py_MAX() and Py_ABS() cause C compatibility regressions #158942

Description

@lpyu001

Bug report

Summary

On GCC and clang, when compiling C , Py_MIN(), Py_MAX() and Py_ABS() are now GNU statement expressions that copy each argument into local variables named _x and _y. Code that used these public macros in an integer constant expression no longer compiles: file-scope array sizes, struct member array sizes, enum values and case labels all fail. Code that passes a bit-field also no longer compiles. When the caller passes a variable named _x or _y, the code compiles and the result is silently wrong: Py_MAX(a, _x) returns a, and Py_MIN(b, _y) and Py_ABS(_x) read an uninitialized local. Nested calls such as Py_MAX(Py_MIN(a, b), c) now emit -Wshadow warnings, and mixed-sign arguments such as Py_MIN(size_t_var, INT_MAX) emit -Wsign-compare warnings under GCC.

Reproduction Code

Requires GCC or clang compiling C. MSVC and C++ use the previous ternary definitions. The commands below use GCC 13.3.0 and clang 18.1.3 on x86-64 Linux, with -I pointing at a CPython source tree that includes the new Include/pymacro.h.

Wrong results:

/* wrong_result.c */
#include <Python.h>
#include <stdio.h>

int main(void)
{
    int a = 1, _x = 5;
    printf("Py_MAX(a, _x) = %d (expected 5)\n", Py_MAX(a, _x));

    int b = 9, _y = 2;
    printf("Py_MIN(b, _y) = %d (expected 2)\n", Py_MIN(b, _y));

    int c = -7;
    {
        int _x = c;
        printf("Py_ABS(_x) = %d (expected 7)\n", Py_ABS(_x));
    }
    return 0;
}
gcc -std=c11 -O0 -I Include -I . wrong_result.c -o t && ./t
gcc -std=c11 -O2 -I Include -I . wrong_result.c -o t && ./t
clang -std=c11 -O0 -I Include -I . wrong_result.c -o t && ./t

Compile failures and new warnings (each block is a separate file):

/* file_scope.c: compile with -c */
#include <Python.h>

static char buf[Py_MAX(sizeof(long), 16)];
struct S { char data[Py_MIN(8, 32)]; };
enum { E = Py_MAX(3, 7) };
/* bitfield.c: compile with -c */
#include <Python.h>

struct S { unsigned bf : 4; };
int f(struct S *s, int k) { return Py_MAX(s->bf, k); }
/* case_label.c: compile with -c */
#include <Python.h>

int f(int v)
{
    switch (v) {
    case Py_MAX(1, 2): return 1;
    default: return 0;
    }
}
/* shadow.c: compile with -c -Wall -Wextra -Wshadow */
#include <Python.h>

int f(int a, int b, int c) { return Py_MAX(Py_MIN(a, b), c); }
/* signcmp.c: compile with -c -Wall -Wextra */
#include <Python.h>
#include <limits.h>

int f(size_t n) { return (int)Py_MIN(n, INT_MAX); }

Actual Behavior

wrong_result.c output:

--- gcc -O0
Py_MAX(a, _x) = 1 (expected 5)
Py_MIN(b, _y) = -219883104 (expected 2)
Py_ABS(_x) = 32766 (expected 7)
--- gcc -O2
Py_MAX(a, _x) = 1 (expected 5)
Py_MIN(b, _y) = 0 (expected 2)
Py_ABS(_x) = 0 (expected 7)
--- clang -O0
Py_MAX(a, _x) = 1 (expected 5)
Py_MIN(b, _y) = 0 (expected 2)
Py_ABS(_x) = 0 (expected 7)

GCC compiles wrong_result.c without any warning at the default warning level. Under -Wall, clang warns variable '_y' is uninitialized when used within its own initialization [-Wuninitialized] and the same for _x. GCC gives no such warning.

Compile failures with GCC:

=== gcc -std=c11 -c file_scope.c
Include/pymacro.h:127:8: error: braced-group within expression allowed only inside a function
Include/pymacro.h:121:8: error: braced-group within expression allowed only inside a function
Include/pymacro.h:127:8: error: braced-group within expression allowed only inside a function
=== gcc -std=c11 -c bitfield.c
bitfield.c:4:43: error: ‘typeof’ applied to a bit-field
=== gcc -std=c11 -c case_label.c
case_label.c:6:5: error: case label does not reduce to an integer constant

Compile failures with clang:

=== clang -std=c11 -c file_scope.c
file_scope.c:3:17: error: statement expression not allowed at file scope
file_scope.c:4:22: error: statement expression not allowed at file scope
file_scope.c:5:12: error: statement expression not allowed at file scope
=== clang -std=c11 -c bitfield.c
bitfield.c:4:36: error: invalid application of 'typeof' to bit-field
=== clang -std=c11 -c case_label.c
case_label.c:6:10: error: expression is not an integer constant expression

New warnings:

=== gcc -std=c11 -Wall -Wextra -Wshadow -c shadow.c
Include/pymacro.h:121:26: warning: declaration of ‘_x’ shadows a previous local [-Wshadow]
=== gcc -std=c11 -Wall -Wextra -Wshadow -c signcmp.c
Include/pymacro.h:123:14: warning: comparison of integer expressions of different signedness: ‘size_t’ {aka ‘long unsigned int’} and ‘int’ [-Wsign-compare]
Include/pymacro.h:123:26: warning: operand of ‘?:’ changes signedness from ‘int’ to ‘size_t’ {aka ‘long unsigned int’} due to unsignedness of other operand [-Wsign-compare]
=== clang -std=c11 -Wall -Wextra -Wshadow -c shadow.c
shadow.c:3:44: warning: declaration shadows a local variable [-Wshadow]

clang emits no -Wsign-compare warning for signcmp.c.

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

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

    interpreter-core(Objects, Python, Grammar, and Parser dirs)topic-C-APItype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions