Skip to content

Commit 7c1584a

Browse files
committed
py/compile: Fix scope of assignment expression target in comprehensions.
When := is used in a comprehension the target variable is bound to the parent scope, so it's either a global or a nonlocal. Prior to this commit that was handled by simply using the parent scope's id_info for the target variable. That's completely wrong because it uses the slot number for the parent's Python stack to store the variable, rather than the slot number for the comprehension. This will in most cases lead to incorrect behaviour or memory faults. This commit fixes the scoping of the target variable by explicitly declaring it a global or nonlocal, depending on whether the parent is the global scope or not. Then the id_info of the comprehension can be used to access the target variable. This fixes a lot of cases of using := in a comprehension. Code size change for this commit: bare-arm: +0 +0.000% minimal x86: +0 +0.000% unix x64: +152 +0.019% standard stm32: +96 +0.024% PYBV10 cc3200: +96 +0.052% esp8266: +196 +0.028% GENERIC esp32: +156 +0.010% GENERIC[incl +8(data)] mimxrt: +96 +0.027% TEENSY40 renesas-ra: +88 +0.014% RA6M2_EK nrf: +88 +0.048% pca10040 rp2: +104 +0.020% PICO samd: +88 +0.033% ADAFRUIT_ITSYBITSY_M4_EXPRESS Fixes issue micropython#10895. Signed-off-by: Damien George <[email protected]>
1 parent c80e7c1 commit 7c1584a

File tree

7 files changed

+144
-18
lines changed

7 files changed

+144
-18
lines changed

py/compile.c

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,14 @@ STATIC void compile_declare_nonlocal(compiler_t *comp, mp_parse_node_t pn, id_in
12741274
}
12751275
}
12761276

1277+
STATIC void compile_declare_global_or_nonlocal(compiler_t *comp, mp_parse_node_t pn, id_info_t *id_info, bool is_global) {
1278+
if (is_global) {
1279+
compile_declare_global(comp, pn, id_info);
1280+
} else {
1281+
compile_declare_nonlocal(comp, pn, id_info);
1282+
}
1283+
}
1284+
12771285
STATIC void compile_global_nonlocal_stmt(compiler_t *comp, mp_parse_node_struct_t *pns) {
12781286
if (comp->pass == MP_PASS_SCOPE) {
12791287
bool is_global = MP_PARSE_NODE_STRUCT_KIND(pns) == PN_global_stmt;
@@ -1288,11 +1296,7 @@ STATIC void compile_global_nonlocal_stmt(compiler_t *comp, mp_parse_node_struct_
12881296
for (size_t i = 0; i < n; i++) {
12891297
qstr qst = MP_PARSE_NODE_LEAF_ARG(nodes[i]);
12901298
id_info_t *id_info = scope_find_or_add_id(comp->scope_cur, qst, ID_INFO_KIND_UNDECIDED);
1291-
if (is_global) {
1292-
compile_declare_global(comp, (mp_parse_node_t)pns, id_info);
1293-
} else {
1294-
compile_declare_nonlocal(comp, (mp_parse_node_t)pns, id_info);
1295-
}
1299+
compile_declare_global_or_nonlocal(comp, (mp_parse_node_t)pns, id_info, is_global);
12961300
}
12971301
}
12981302
}
@@ -2133,13 +2137,30 @@ STATIC void compile_namedexpr_helper(compiler_t *comp, mp_parse_node_t pn_name,
21332137
}
21342138
compile_node(comp, pn_expr);
21352139
EMIT(dup_top);
2136-
scope_t *old_scope = comp->scope_cur;
2137-
if (SCOPE_IS_COMP_LIKE(comp->scope_cur->kind)) {
2138-
// Use parent's scope for assigned value so it can "escape"
2139-
comp->scope_cur = comp->scope_cur->parent;
2140+
2141+
qstr target = MP_PARSE_NODE_LEAF_ARG(pn_name);
2142+
2143+
// When a variable is assigned via := in a comprehension then that variable is bound to
2144+
// the parent scope. Any global or nonlocal declarations in the parent scope are honoured.
2145+
// For details see: https://peps.python.org/pep-0572/#scope-of-the-target
2146+
if (comp->pass == MP_PASS_SCOPE && SCOPE_IS_COMP_LIKE(comp->scope_cur->kind)) {
2147+
id_info_t *id_info_parent = mp_emit_common_get_id_for_modification(comp->scope_cur->parent, target);
2148+
if (id_info_parent->kind == ID_INFO_KIND_GLOBAL_EXPLICIT) {
2149+
scope_find_or_add_id(comp->scope_cur, target, ID_INFO_KIND_GLOBAL_EXPLICIT);
2150+
} else {
2151+
id_info_t *id_info = scope_find_or_add_id(comp->scope_cur, target, ID_INFO_KIND_UNDECIDED);
2152+
bool is_global = comp->scope_cur->parent->parent == NULL; // comprehension is defined in outer scope
2153+
if (!is_global && id_info->kind == ID_INFO_KIND_GLOBAL_IMPLICIT) {
2154+
// Variable was already referenced but now needs to be closed over, so reset the kind
2155+
// such that scope_check_to_close_over() is called in compile_declare_nonlocal().
2156+
id_info->kind = ID_INFO_KIND_UNDECIDED;
2157+
}
2158+
compile_declare_global_or_nonlocal(comp, pn_name, id_info, is_global);
2159+
}
21402160
}
2141-
compile_store_id(comp, MP_PARSE_NODE_LEAF_ARG(pn_name));
2142-
comp->scope_cur = old_scope;
2161+
2162+
// Do the store to the target variable.
2163+
compile_store_id(comp, target);
21432164
}
21442165

21452166
STATIC void compile_namedexpr(compiler_t *comp, mp_parse_node_struct_t *pns) {

py/emit.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ static inline void mp_emit_common_get_id_for_load(scope_t *scope, qstr qst) {
191191
scope_find_or_add_id(scope, qst, ID_INFO_KIND_GLOBAL_IMPLICIT);
192192
}
193193

194-
void mp_emit_common_get_id_for_modification(scope_t *scope, qstr qst);
194+
id_info_t *mp_emit_common_get_id_for_modification(scope_t *scope, qstr qst);
195195
void mp_emit_common_id_op(emit_t *emit, const mp_emit_method_table_id_ops_t *emit_method_table, scope_t *scope, qstr qst);
196196

197197
extern const emit_method_table_t emit_bc_method_table;

py/emitcommon.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ size_t mp_emit_common_use_const_obj(mp_emit_common_t *emit, mp_obj_t const_obj)
8686
return emit->const_obj_list.len - 1;
8787
}
8888

89-
void mp_emit_common_get_id_for_modification(scope_t *scope, qstr qst) {
89+
id_info_t *mp_emit_common_get_id_for_modification(scope_t *scope, qstr qst) {
9090
// name adding/lookup
9191
id_info_t *id = scope_find_or_add_id(scope, qst, ID_INFO_KIND_GLOBAL_IMPLICIT);
9292
if (id->kind == ID_INFO_KIND_GLOBAL_IMPLICIT) {
@@ -98,6 +98,7 @@ void mp_emit_common_get_id_for_modification(scope_t *scope, qstr qst) {
9898
id->kind = ID_INFO_KIND_GLOBAL_IMPLICIT_ASSIGNED;
9999
}
100100
}
101+
return id;
101102
}
102103

103104
void mp_emit_common_id_op(emit_t *emit, const mp_emit_method_table_id_ops_t *emit_method_table, scope_t *scope, qstr qst) {

tests/basics/assign_expr_scope.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Test scoping rules for assignment expression :=.
2+
3+
# Test that var is closed over (assigned to in the scope of scope0).
4+
def scope0():
5+
any((var0 := i) for i in range(2))
6+
return var0
7+
8+
9+
print("scope0")
10+
print(scope0())
11+
print(globals().get("var0", None))
12+
13+
# Test that var1 gets closed over correctly in the list comprehension.
14+
def scope1():
15+
var1 = 0
16+
dummy1 = 1
17+
dummy2 = 1
18+
print([var1 := i for i in [0, 1] if i > var1])
19+
print(var1)
20+
21+
22+
print("scope1")
23+
scope1()
24+
print(globals().get("var1", None))
25+
26+
# Test that var2 in the comprehension honours the global declaration.
27+
def scope2():
28+
global var2
29+
print([var2 := i for i in range(2)])
30+
print(globals().get("var2", None))
31+
32+
33+
print("scope2")
34+
scope2()
35+
print(globals().get("var2", None))
36+
37+
# Test that var1 in the comprehension remains local to inner1.
38+
def scope3():
39+
global var3
40+
41+
def inner3():
42+
print([var3 := i for i in range(2)])
43+
44+
inner3()
45+
print(globals().get("var3", None))
46+
47+
48+
print("scope3")
49+
scope3()
50+
print(globals().get("var3", None))
51+
52+
# Test that var4 in the comprehension honours the global declarations.
53+
def scope4():
54+
global var4
55+
56+
def inner4():
57+
global var4
58+
print([var4 := i for i in range(2)])
59+
60+
inner4()
61+
print(var4)
62+
63+
64+
print("scope4")
65+
scope4()
66+
print(globals().get("var4", None))
67+
68+
# Test that var5 in the comprehension honours the nonlocal declaration.
69+
def scope5():
70+
def inner5():
71+
nonlocal var5
72+
print([var5 := i for i in range(2)])
73+
74+
inner5()
75+
print(var5)
76+
var5 = 0 # force var5 to be a local to scope5
77+
78+
79+
print("scope5")
80+
scope5()
81+
print(globals().get("var5", None))

tests/basics/assign_expr_scope.py.exp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
scope0
2+
1
3+
None
4+
scope1
5+
[1]
6+
1
7+
None
8+
scope2
9+
[0, 1]
10+
1
11+
1
12+
scope3
13+
[0, 1]
14+
None
15+
None
16+
scope4
17+
[0, 1]
18+
1
19+
1
20+
scope5
21+
[0, 1]
22+
1
23+
None

tests/basics/assign_expr_syntaxerror.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ def test(code):
88

99
test("x := 1")
1010
test("((x, y) := 1)")
11-
12-
# these are currently all allowed in MicroPython, but not in CPython
1311
test("([i := i + 1 for i in range(4)])")
1412
test("([i := -1 for i, j in [(1, 2)]])")
1513
test("([[(i := j) for i in range(2)] for j in range(2)])")
14+
15+
# this is currently allowed in MicroPython, but not in CPython
1616
test("([[(j := i) for i in range(2)] for j in range(2)])")
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
SyntaxError
22
SyntaxError
3-
[1, 2, 3, 4]
4-
[-1]
5-
[[0, 0], [1, 1]]
3+
SyntaxError
4+
SyntaxError
5+
SyntaxError
66
[[0, 1], [0, 1]]

0 commit comments

Comments
 (0)