diff --git a/Grammar/python.gram b/Grammar/python.gram index a8adeb566aaf5d..60f2cd30b29f97 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/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 00000000000000..edcf99f56ff907 --- /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 ``-``. diff --git a/Parser/parser.c b/Parser/parser.c index 58b6dd77a38b26..39bc96f8de9859 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 4cfa2ff559a5f7..23c59f6211bb2a 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; @@ -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; } @@ -512,9 +517,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 54dec3dfe04268..493d34531ea860 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_); } }