Skip to content

Commit c3f2373

Browse files
authored
gh-153568: Share expression parsing with assignment targets (#157450)
* gh-153568: Share expression parsing with assignment targets * Clarify target parsing contracts and AST expectations
1 parent 62b7f28 commit c3f2373

7 files changed

Lines changed: 1897 additions & 3425 deletions

File tree

Grammar/python.gram

Lines changed: 35 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ assignment[stmt_ty]:
157157
_PyAST_AnnAssign(CHECK(expr_ty, _PyPegen_set_expr_context(p, a, Store)), b, c, 1, EXTRA)
158158
) }
159159
| a=('(' b=single_target ')' { b }
160-
| single_subscript_attribute_target) ':' b=expression c=['=' d=annotated_rhs { d }] {
160+
| attribute_or_subscript_target) ':' b=expression c=['=' d=annotated_rhs { d }] {
161161
CHECK_VERSION(stmt_ty, 6, "Variable annotations syntax is", _PyAST_AnnAssign(a, b, c, 0, EXTRA)) }
162162
| a[asdl_expr_seq*]=(z=star_targets '=' { z })+ b=annotated_rhs !'=' tc=[TYPE_COMMENT] {
163163
_PyAST_Assign(a, b, NEW_TYPE_COMMENT(p, tc), EXTRA) }
@@ -740,16 +740,10 @@ star_expression[expr_ty] (memo):
740740

741741
star_named_expressions[asdl_expr_seq*]: a[asdl_expr_seq*]=','.star_named_expression+ [','] { a }
742742

743-
star_named_expressions_sequence[asdl_expr_seq*]: a[asdl_expr_seq*]=','.star_named_expression_sequence+ [','] { a }
744-
745743
star_named_expression[expr_ty]:
746744
| '*' a=bitwise_or { _PyAST_Starred(a, Load, EXTRA) }
747745
| named_expression
748746

749-
star_named_expression_sequence[expr_ty]:
750-
| invalid_starred_expression_unpacking_sequence
751-
| star_named_expression
752-
753747
assignment_expression[expr_ty]:
754748
| a=NAME ':=' ~ b=expression {
755749
CHECK_VERSION(expr_ty, 8, "Assignment expressions are",
@@ -791,28 +785,17 @@ comparison[expr_ty]:
791785
| bitwise_or
792786

793787
compare_op_bitwise_or_pair[CmpopExprPair*]:
794-
| eq_bitwise_or
795-
| noteq_bitwise_or
796-
| lte_bitwise_or
797-
| lt_bitwise_or
798-
| gte_bitwise_or
799-
| gt_bitwise_or
800-
| notin_bitwise_or
801-
| in_bitwise_or
802-
| isnot_bitwise_or
803-
| is_bitwise_or
804-
805-
eq_bitwise_or[CmpopExprPair*]: '==' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, Eq, a) }
806-
noteq_bitwise_or[CmpopExprPair*]:
807-
| (tok='!=' { _PyPegen_check_barry_as_flufl(p, tok) ? NULL : tok}) a=bitwise_or {_PyPegen_cmpop_expr_pair(p, NotEq, a) }
808-
lte_bitwise_or[CmpopExprPair*]: '<=' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, LtE, a) }
809-
lt_bitwise_or[CmpopExprPair*]: '<' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, Lt, a) }
810-
gte_bitwise_or[CmpopExprPair*]: '>=' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, GtE, a) }
811-
gt_bitwise_or[CmpopExprPair*]: '>' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, Gt, a) }
812-
notin_bitwise_or[CmpopExprPair*]: 'not' 'in' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, NotIn, a) }
813-
in_bitwise_or[CmpopExprPair*]: 'in' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, In, a) }
814-
isnot_bitwise_or[CmpopExprPair*]: 'is' 'not' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, IsNot, a) }
815-
is_bitwise_or[CmpopExprPair*]: 'is' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, Is, a) }
788+
| '==' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, Eq, a) }
789+
| (tok='!=' { _PyPegen_check_barry_as_flufl(p, tok) ? NULL : tok }) a=bitwise_or {
790+
_PyPegen_cmpop_expr_pair(p, NotEq, a) }
791+
| '<=' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, LtE, a) }
792+
| '<' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, Lt, a) }
793+
| '>=' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, GtE, a) }
794+
| '>' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, Gt, a) }
795+
| 'not' 'in' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, NotIn, a) }
796+
| 'in' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, In, a) }
797+
| 'is' 'not' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, IsNot, a) }
798+
| 'is' a=bitwise_or { _PyPegen_cmpop_expr_pair(p, Is, a) }
816799

817800
# Bitwise operators
818801
# -----------------
@@ -1014,14 +997,20 @@ strings[expr_ty] (memo):
1014997
| a[asdl_expr_seq*]=(fstring|string)+ { _PyPegen_concatenate_strings(p, a, EXTRA) }
1015998
| a[asdl_expr_seq*]=tstring+ { _PyPegen_concatenate_tstrings(p, a, EXTRA) }
1016999

1000+
display_items[asdl_expr_seq*]: a[asdl_expr_seq*]=','.display_item+ [','] { a }
1001+
1002+
display_item[expr_ty]:
1003+
| invalid_starred_expression_unpacking_sequence
1004+
| star_named_expression
1005+
10171006
list[expr_ty]:
1018-
| '[' a=[star_named_expressions_sequence] ']' { _PyAST_List(a, Load, EXTRA) }
1007+
| '[' a=[display_items] ']' { _PyAST_List(a, Load, EXTRA) }
10191008

10201009
tuple[expr_ty]:
1021-
| '(' a=[y=star_named_expression_sequence ',' z=[star_named_expressions_sequence] { _PyPegen_seq_insert_in_front(p, y, z) } ] ')' {
1010+
| '(' a=[y=display_item ',' z=[display_items] { _PyPegen_seq_insert_in_front(p, y, z) } ] ')' {
10221011
_PyAST_Tuple(a, Load, EXTRA) }
10231012

1024-
set[expr_ty]: '{' a=star_named_expressions_sequence '}' { _PyAST_Set(a, EXTRA) }
1013+
set[expr_ty]: '{' a=display_items '}' { _PyAST_Set(a, EXTRA) }
10251014

10261015
# Dicts
10271016
# -----
@@ -1088,8 +1077,8 @@ args[expr_ty]:
10881077
EXTRA) }
10891078

10901079
kwargs[asdl_seq*]:
1091-
| a=','.kwarg_or_starred+ ',' b=','.kwarg_or_double_starred+ { _PyPegen_join_sequences(p, a, b) }
1092-
| ','.kwarg_or_starred+
1080+
| a=','.kwarg_or_starred+ b=[',' c=','.kwarg_or_double_starred+ { c }] {
1081+
b ? _PyPegen_join_sequences(p, a, b) : a }
10931082
| ','.kwarg_or_double_starred+
10941083

10951084
starred_expression[expr_ty]:
@@ -1115,72 +1104,35 @@ kwarg_or_double_starred[KeywordOrStarred*]:
11151104
# Generic targets
11161105
# ---------------
11171106

1118-
# NOTE: star_targets may contain *bitwise_or, targets may not.
1107+
# Targets reuse primary. Copying the context leaves its cached values in Load.
11191108
star_targets[expr_ty]:
11201109
| a=star_target !',' { a }
11211110
| a=star_target b=(',' c=star_target { c })* [','] {
11221111
_PyAST_Tuple(CHECK(asdl_expr_seq*, _PyPegen_seq_insert_in_front(p, a, b)), Store, EXTRA) }
11231112

1124-
star_targets_list_seq[asdl_expr_seq*]: a[asdl_expr_seq*]=','.star_target+ [','] { a }
1125-
1126-
star_targets_tuple_seq[asdl_expr_seq*]:
1127-
| a=star_target b=(',' c=star_target { c })+ [','] { (asdl_expr_seq*) _PyPegen_seq_insert_in_front(p, a, b) }
1128-
| a=star_target ',' { (asdl_expr_seq*) _PyPegen_singleton_seq(p, a) }
1129-
11301113
star_target[expr_ty] (memo):
1131-
| '*' a=(!'*' star_target) {
1132-
_PyAST_Starred(CHECK(expr_ty, _PyPegen_set_expr_context(p, a, Store)), Store, EXTRA) }
1133-
| target_with_star_atom
1114+
| '*' a=(!'*' star_target) { _PyAST_Starred(a, Store, EXTRA) }
1115+
| target
11341116

1135-
target_with_star_atom[expr_ty] (memo):
1136-
| a=t_primary '.' b=NAME !t_lookahead { _PyAST_Attribute(a, b->v.Name.id, Store, EXTRA) }
1137-
| a=t_primary '[' b=slices ']' !t_lookahead { _PyAST_Subscript(a, b, Store, EXTRA) }
1138-
| star_atom
1139-
1140-
star_atom[expr_ty]:
1141-
| a=NAME { _PyPegen_set_expr_context(p, a, Store) }
1142-
| '(' a=target_with_star_atom ')' { _PyPegen_set_expr_context(p, a, Store) }
1143-
| '(' a=[star_targets_tuple_seq] ')' { _PyAST_Tuple(a, Store, EXTRA) }
1144-
| '[' a=[star_targets_list_seq] ']' { _PyAST_List(a, Store, EXTRA) }
1117+
target[expr_ty] (memo):
1118+
| a=target_value { _PyPegen_make_target(p, a, STAR_TARGETS) }
11451119

11461120
single_target[expr_ty]:
1147-
| single_subscript_attribute_target
1148-
| a=NAME { _PyPegen_set_expr_context(p, a, Store) }
1149-
| '(' a=single_target ')' { a }
1150-
1151-
single_subscript_attribute_target[expr_ty]:
1152-
| a=t_primary '.' b=NAME !t_lookahead { _PyAST_Attribute(a, b->v.Name.id, Store, EXTRA) }
1153-
| a=t_primary '[' b=slices ']' !t_lookahead { _PyAST_Subscript(a, b, Store, EXTRA) }
1154-
1155-
t_primary[expr_ty]:
1156-
| a=t_primary '.' b=NAME &t_lookahead { _PyAST_Attribute(a, b->v.Name.id, Load, EXTRA) }
1157-
| a=t_primary '[' b=slices ']' &t_lookahead { _PyAST_Subscript(a, b, Load, EXTRA) }
1158-
| a=t_primary b=genexp &t_lookahead {
1159-
_PyAST_Call(a, CHECK(asdl_expr_seq*, (asdl_expr_seq*)_PyPegen_singleton_seq(p, b)), NULL, EXTRA) }
1160-
| a=t_primary '(' b=[arguments] ')' &t_lookahead {
1161-
_PyAST_Call(a,
1162-
(b) ? ((expr_ty) b)->v.Call.args : NULL,
1163-
(b) ? ((expr_ty) b)->v.Call.keywords : NULL,
1164-
EXTRA) }
1165-
| a=atom &t_lookahead { a }
1121+
| a=target_value { _PyPegen_make_target(p, a, SINGLE_TARGETS) }
1122+
1123+
attribute_or_subscript_target[expr_ty]:
1124+
| a=target_value { _PyPegen_make_target(p, a, ATTRIBUTE_OR_SUBSCRIPT_TARGETS) }
11661125

1167-
t_lookahead: '(' | '[' | '.'
1126+
target_value[expr_ty]:
1127+
| a=primary !('(' | '[' | '.') { a }
11681128

11691129
# Targets for del statements
11701130
# --------------------------
11711131

11721132
del_targets[asdl_expr_seq*]: a[asdl_expr_seq*]=','.del_target+ [','] { a }
11731133

11741134
del_target[expr_ty] (memo):
1175-
| a=t_primary '.' b=NAME !t_lookahead { _PyAST_Attribute(a, b->v.Name.id, Del, EXTRA) }
1176-
| a=t_primary '[' b=slices ']' !t_lookahead { _PyAST_Subscript(a, b, Del, EXTRA) }
1177-
| del_t_atom
1178-
1179-
del_t_atom[expr_ty]:
1180-
| a=NAME { _PyPegen_set_expr_context(p, a, Del) }
1181-
| '(' a=del_target ')' { _PyPegen_set_expr_context(p, a, Del) }
1182-
| '(' a=[del_targets] ')' { _PyAST_Tuple(a, Del, EXTRA) }
1183-
| '[' a=[del_targets] ']' { _PyAST_List(a, Del, EXTRA) }
1135+
| a=target_value { _PyPegen_make_target(p, a, DEL_TARGETS) }
11841136

11851137
# TYPING ELEMENTS
11861138
# ---------------

Lib/test/test_ast/test_ast.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,51 @@ def test_issue18374_binop_col_offset(self):
696696
self.assertEqual(grandchild_binop.end_col_offset, 3)
697697
self.assertEqual(grandchild_binop.end_lineno, 1)
698698

699+
def test_primary_target_context(self):
700+
for expression in ('factory().field[index]', 'factory()[index].field'):
701+
for context in (ast.Load, ast.Store, ast.Del):
702+
with self.subTest(expression=expression, context=context):
703+
if context is ast.Load:
704+
node = ast.parse(expression).body[0].value
705+
elif context is ast.Store:
706+
node = ast.parse(f'{expression} = value').body[0].targets[0]
707+
else:
708+
node = ast.parse(f'del {expression}').body[0].targets[0]
709+
self.assertIsInstance(node.ctx, context)
710+
for child in ast.walk(node):
711+
if child is not node and hasattr(child, 'ctx'):
712+
self.assertIsInstance(child.ctx, ast.Load)
713+
714+
def test_container_target_context(self):
715+
cases = (
716+
('(a, [b, c])', '(a, [b, c])'),
717+
('[a, (b, c)]', '[a, (b, c)]'),
718+
('((a))', 'a'),
719+
('()', '()'),
720+
('[]', '[]'),
721+
)
722+
for expression, expected_segment in cases:
723+
for context in (ast.Load, ast.Store, ast.Del):
724+
with self.subTest(expression=expression, context=context):
725+
if context is ast.Load:
726+
source = expression
727+
elif context is ast.Store:
728+
source = f'{expression} = value'
729+
else:
730+
source = f'del {expression}'
731+
statement = ast.parse(source).body[0]
732+
node = statement.value if context is ast.Load else statement.targets[0]
733+
for child in ast.walk(node):
734+
if hasattr(child, 'ctx'):
735+
self.assertIsInstance(child.ctx, context)
736+
self.assertEqual(ast.get_source_segment(source, node),
737+
expected_segment)
738+
739+
node = ast.parse('[a, *[b, *c]] = value').body[0].targets[0]
740+
for child in ast.walk(node):
741+
if hasattr(child, 'ctx'):
742+
self.assertIsInstance(child.ctx, ast.Store)
743+
699744
def test_issue39579_dotted_name_end_col_offset(self):
700745
tree = ast.parse('@a.b.c\ndef f(): pass')
701746
attr_b = tree.body[0].decorator_list[0].value

Lib/test/test_syntax.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3400,7 +3400,7 @@ def test_invalid_line_continuation_error_position(self):
34003400

34013401
def test_invalid_line_continuation_left_recursive(self):
34023402
# Check bpo-42218: SyntaxErrors following left-recursive rules
3403-
# (t_primary_raw in this case) need to be tested explicitly
3403+
# (primary_raw in this case) need to be tested explicitly
34043404
self._check_error("A.\u018a\\ ",
34053405
"unexpected character after line continuation character")
34063406
self._check_error("A.\u03bc\\\n",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Speed up parsing by sharing expression rules with assignment and deletion
2+
targets, and simplify comparison and keyword argument rules.

Parser/action_helpers.c

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,20 @@ _PyPegen_set_expr_context(Parser *p, expr_ty expr, expr_context_ty ctx)
346346
return new;
347347
}
348348

349+
/* Invalid targets return NULL without raising, allowing parsing to backtrack.
350+
Copy valid targets to preserve the memoized expression's context. */
351+
expr_ty
352+
_PyPegen_make_target(Parser *p, expr_ty expr, TARGETS_TYPE targets_type)
353+
{
354+
assert(expr != NULL);
355+
assert(targets_type != FOR_TARGETS);
356+
if (_PyPegen_get_invalid_target(expr, targets_type) != NULL) {
357+
return NULL;
358+
}
359+
return _PyPegen_set_expr_context(
360+
p, expr, targets_type == DEL_TARGETS ? Del : Store);
361+
}
362+
349363
/* Constructs a KeyValuePair that is used when parsing a dict's key value pairs */
350364
KeyValuePair *
351365
_PyPegen_key_value_pair(Parser *p, expr_ty key, expr_ty value)
@@ -1236,6 +1250,14 @@ _PyPegen_get_invalid_target(expr_ty e, TARGETS_TYPE targets_type)
12361250
return NULL;
12371251
}
12381252

1253+
if (targets_type == SINGLE_TARGETS ||
1254+
targets_type == ATTRIBUTE_OR_SUBSCRIPT_TARGETS) {
1255+
if (targets_type == SINGLE_TARGETS && e->kind == Name_kind) {
1256+
return NULL;
1257+
}
1258+
return e->kind == Attribute_kind || e->kind == Subscript_kind ? NULL : e;
1259+
}
1260+
12391261
#define VISIT_CONTAINER(CONTAINER, TYPE) do { \
12401262
Py_ssize_t len = asdl_seq_LEN((CONTAINER)->v.TYPE.elts);\
12411263
for (Py_ssize_t i = 0; i < len; i++) {\

0 commit comments

Comments
 (0)