From b2f2c255016f9add47af16a94dacc895012988c5 Mon Sep 17 00:00:00 2001 From: Twix1288 Date: Tue, 30 Jun 2026 14:45:06 -0700 Subject: [PATCH 1/3] Fix AST generation asymmetry for unary positive in match statements --- Grammar/python.gram | 6 +++--- Parser/parser.c | 42 ++++++++++++++++++++++++++++++++++++++--- Python/ast.c | 12 ++++++------ Python/ast_preprocess.c | 17 ++++++++++++----- 4 files changed, 60 insertions(+), 17 deletions(-) diff --git a/Grammar/python.gram b/Grammar/python.gram index a8adeb566aaf5d1..60f2cd30b29f974 100644 --- a/Grammar/python.gram +++ b/Grammar/python.gram @@ -554,12 +554,12 @@ complex_number[expr_ty]: signed_number[expr_ty]: | NUMBER - | '+' number=NUMBER { number } + | '+' number=NUMBER { _PyAST_UnaryOp(UAdd, number, EXTRA) } | '-' number=NUMBER { _PyAST_UnaryOp(USub, number, EXTRA) } signed_real_number[expr_ty]: | real_number - | '+' real=real_number { real } + | '+' real=real_number { _PyAST_UnaryOp(UAdd, real, EXTRA) } | '-' real=real_number { _PyAST_UnaryOp(USub, real, EXTRA) } real_number[expr_ty]: @@ -567,7 +567,7 @@ real_number[expr_ty]: imaginary_number[expr_ty]: | imag=NUMBER { _PyPegen_ensure_imaginary(p, imag) } - | '+' imag=NUMBER { _PyPegen_ensure_imaginary(p, imag) } + | '+' imag=NUMBER { _PyAST_UnaryOp(UAdd, _PyPegen_ensure_imaginary(p, imag), EXTRA) } capture_pattern[pattern_ty]: | target=pattern_capture_target { _PyAST_MatchAs(NULL, target->v.Name.id, EXTRA) } diff --git a/Parser/parser.c b/Parser/parser.c index 58b6dd77a38b26d..39bc96f8de98595 100644 --- a/Parser/parser.c +++ b/Parser/parser.c @@ -9126,7 +9126,16 @@ signed_number_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ signed_number[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'+' NUMBER")); - _res = number; + Token *_token = _PyPegen_get_last_nonnwhitespace_token(p); + if (_token == NULL) { + p->level--; + return NULL; + } + int _end_lineno = _token->end_lineno; + UNUSED(_end_lineno); // Only used by EXTRA macro + int _end_col_offset = _token->end_col_offset; + UNUSED(_end_col_offset); // Only used by EXTRA macro + _res = _PyAST_UnaryOp ( UAdd , number , EXTRA ); if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) { p->error_indicator = 1; p->level--; @@ -9236,7 +9245,16 @@ signed_real_number_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ signed_real_number[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'+' real_number")); - _res = real; + Token *_token = _PyPegen_get_last_nonnwhitespace_token(p); + if (_token == NULL) { + p->level--; + return NULL; + } + int _end_lineno = _token->end_lineno; + UNUSED(_end_lineno); // Only used by EXTRA macro + int _end_col_offset = _token->end_col_offset; + UNUSED(_end_col_offset); // Only used by EXTRA macro + _res = _PyAST_UnaryOp ( UAdd , real , EXTRA ); if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) { p->error_indicator = 1; p->level--; @@ -9346,6 +9364,15 @@ imaginary_number_rule(Parser *p) } expr_ty _res = NULL; int _mark = p->mark; + if (p->mark == p->fill && _PyPegen_fill_token(p) < 0) { + p->error_indicator = 1; + p->level--; + return NULL; + } + int _start_lineno = p->tokens[_mark]->lineno; + UNUSED(_start_lineno); // Only used by EXTRA macro + int _start_col_offset = p->tokens[_mark]->col_offset; + UNUSED(_start_col_offset); // Only used by EXTRA macro { // NUMBER if (p->error_indicator) { p->level--; @@ -9385,7 +9412,16 @@ imaginary_number_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ imaginary_number[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'+' NUMBER")); - _res = _PyPegen_ensure_imaginary ( p , imag ); + Token *_token = _PyPegen_get_last_nonnwhitespace_token(p); + if (_token == NULL) { + p->level--; + return NULL; + } + int _end_lineno = _token->end_lineno; + UNUSED(_end_lineno); // Only used by EXTRA macro + int _end_col_offset = _token->end_col_offset; + UNUSED(_end_col_offset); // Only used by EXTRA macro + _res = _PyAST_UnaryOp ( UAdd , _PyPegen_ensure_imaginary ( p , imag ) , EXTRA ); if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) { p->error_indicator = 1; p->level--; diff --git a/Python/ast.c b/Python/ast.c index 4cfa2ff559a5f7d..7ee01362594a4fc 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -426,11 +426,11 @@ ensure_literal_number(expr_ty exp, bool allow_real, bool allow_imaginary) } static int -ensure_literal_negative(expr_ty exp, bool allow_real, bool allow_imaginary) +ensure_literal_signed(expr_ty exp, bool allow_real, bool allow_imaginary) { assert(exp->kind == UnaryOp_kind); - // Must be negation ... - if (exp->v.UnaryOp.op != USub) { + // Must be negation or positive ... + if (exp->v.UnaryOp.op != USub && exp->v.UnaryOp.op != UAdd) { return 0; } // ... of a constant ... @@ -461,7 +461,7 @@ ensure_literal_complex(expr_ty exp) } break; case UnaryOp_kind: - if (!ensure_literal_negative(left, /*real=*/true, /*imaginary=*/false)) { + if (!ensure_literal_signed(left, /*real=*/true, /*imaginary=*/false)) { return 0; } break; @@ -512,9 +512,9 @@ validate_pattern_match_value(expr_ty exp) // Constants and attribute lookups are always permitted return 1; case UnaryOp_kind: - // Negated numbers are permitted (whether real or imaginary) + // Signed numbers are permitted (whether real or imaginary) // Compiler will complain if AST folding doesn't create a constant - if (ensure_literal_negative(exp, /*real=*/true, /*imaginary=*/true)) { + if (ensure_literal_signed(exp, /*real=*/true, /*imaginary=*/true)) { return 1; } break; diff --git a/Python/ast_preprocess.c b/Python/ast_preprocess.c index 54dec3dfe042686..493d34531ea8605 100644 --- a/Python/ast_preprocess.c +++ b/Python/ast_preprocess.c @@ -867,11 +867,14 @@ fold_const_match_patterns(expr_ty node, PyArena *ctx_, _PyASTPreprocessState *st { case UnaryOp_kind: { - if (node->v.UnaryOp.op == USub && + if ((node->v.UnaryOp.op == USub || node->v.UnaryOp.op == UAdd) && node->v.UnaryOp.operand->kind == Constant_kind) { PyObject *operand = node->v.UnaryOp.operand->v.Constant.value; - PyObject *folded = PyNumber_Negative(operand); + PyObject *folded = node->v.UnaryOp.op == USub ? PyNumber_Negative(operand) : PyNumber_Positive(operand); + if (folded == NULL) { + return 0; + } return make_const(node, folded, ctx_); } break; @@ -879,14 +882,18 @@ fold_const_match_patterns(expr_ty node, PyArena *ctx_, _PyASTPreprocessState *st case BinOp_kind: { operator_ty op = node->v.BinOp.op; - if ((op == Add || op == Sub) && - node->v.BinOp.right->kind == Constant_kind) + if (op == Add || op == Sub) { CALL(fold_const_match_patterns, expr_ty, node->v.BinOp.left); - if (node->v.BinOp.left->kind == Constant_kind) { + CALL(fold_const_match_patterns, expr_ty, node->v.BinOp.right); + if (node->v.BinOp.left->kind == Constant_kind && + node->v.BinOp.right->kind == Constant_kind) { PyObject *left = node->v.BinOp.left->v.Constant.value; PyObject *right = node->v.BinOp.right->v.Constant.value; PyObject *folded = op == Add ? PyNumber_Add(left, right) : PyNumber_Subtract(left, right); + if (folded == NULL) { + return 0; + } return make_const(node, folded, ctx_); } } From 6a7d2f21cd7b3228309be1e2c345b92be98e97d3 Mon Sep 17 00:00:00 2001 From: Twix1288 Date: Tue, 30 Jun 2026 14:50:28 -0700 Subject: [PATCH 2/3] Add NEWS entry for gh-152708 --- .../2026-06-30-14-50-00.gh-issue-152708.aBcDeF.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-30-14-50-00.gh-issue-152708.aBcDeF.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-30-14-50-00.gh-issue-152708.aBcDeF.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-30-14-50-00.gh-issue-152708.aBcDeF.rst new file mode 100644 index 000000000000000..edcf99f56ff9071 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-30-14-50-00.gh-issue-152708.aBcDeF.rst @@ -0,0 +1 @@ +Fix AST generation asymmetry where unary positive ``+`` was implicitly dropped in pattern match expressions, restoring parsing parity with unary negative ``-``. From 9d7e6730e95a41de4005a839aafedf5dbf9ca8c5 Mon Sep 17 00:00:00 2001 From: Twix1288 Date: Tue, 30 Jun 2026 15:58:10 -0700 Subject: [PATCH 3/3] Fix ast.parse validation error for UnaryOp in complex match patterns --- Python/ast.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Python/ast.c b/Python/ast.c index 7ee01362594a4fc..23c59f6211bb2ad 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -476,6 +476,11 @@ ensure_literal_complex(expr_ty exp) return 0; } break; + case UnaryOp_kind: + if (!ensure_literal_signed(right, /*real=*/false, /*imaginary=*/true)) { + return 0; + } + break; default: return 0; }