1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# Multi-level (transitive / pass-through) closure capture: a nested function
# capturing a variable from a scope more than one level up. Each intermediate
# scope threads the captured cell through, matching CPython.
# === Two-level read ===
def outer_read(a):
def mid():
def inner():
return a # captured from `outer_read`, through `mid`
return inner()
return mid()
assert outer_read(10) == 10
# === Four-level read ===
def a4(x):
def b():
def c():
def d():
return x * 2
return d()
return c()
return b()
assert a4(21) == 42
# === Two-level nonlocal write ===
def writer():
a = 0
def mid():
def inner():
nonlocal a
a += 5
inner()
mid()
return a
assert writer() == 5
# === Intermediate scope rebinds the name (shadowing) ===
def shadow(x):
def mid():
x = 99 # mid's own local shadows the outer `x`
def inner():
return x # captures mid's x, NOT outer's
return inner()
return (mid(), x)
assert shadow(1) == (99, 1)
# === Owner reads its own variable before the capturing def appears ===
# This pins the fix: the variable must be recognised as a cell up front, even
# though it is only captured by a grand-nested function further down.
def early_use(n):
total = n # read/assigned here, before `mid`/`inner` are defined
total += 1
def mid():
def inner():
return total # captures `total` two levels up
return inner()
return (mid(), total)
assert early_use(10) == (11, 11)
# === Sibling closures two levels down share the same cell ===
def shared():
v = 0
def mid():
def setter(x):
nonlocal v
v = x
def getter():
return v
return setter, getter
return mid()
s, g = shared()
s(42)
assert g() == 42
# === Each instantiation captures its own cell ===
def make_adder(n):
def mid():
def add(x):
return x + n
return add
return mid()
add3 = make_adder(3)
add5 = make_adder(5)
assert add3(10) == 13
assert add5(10) == 15
assert add3(10) == 13
# === Comprehension nested two levels deep captures an enclosing variable ===
def comp(n):
def mid():
return [n + i for i in range(3)]
return mid()
assert comp(10) == [10, 11, 12]
# === Mixed capture from two different levels at once ===
def two_levels(a):
def mid(b):
def inner():
return a + b # `a` from outer (2 levels), `b` from mid (1 level)
return inner()
return mid(5)
assert two_levels(10) == 15
# === Lambda capturing two levels up ===
def lam(a):
def mid():
return (lambda: a)()
return mid()
assert lam(7) == 7
# === Three-level nonlocal write ===
def writer3():
a = 0
def m1():
def m2():
def inner():
nonlocal a
a += 7
inner()
m2()
m1()
return a
assert writer3() == 7
# === Chained nonlocal: each intermediate scope also declares it ===
def chained():
a = 1
def m1():
nonlocal a
def inner():
nonlocal a
a = 99
inner()
m1()
return a
assert chained() == 99
# === Capturing function defined inside control-flow blocks ===
# Exercises the transitive cell-var pre-pass recursing through if/for bodies.
def control_flow(a, flag):
if flag:
def mid():
for _ in range(1):
def inner():
return a
return inner()
return mid()
return -1
assert control_flow(5, True) == 5
# === Owner mutates the captured variable after building the closure ===
# The closure must observe the later value (a shared cell, not a snapshot).
def late_mutation():
x = 1
def mid():
def inner():
return x
return inner
f = mid()
x = 42 # mutated after the closure was created
return f()
assert late_mutation() == 42
# === Default argument in a nested function references a transitive capture ===
def with_default(a):
def mid():
def inner(y=a): # default evaluated in mid; `a` captured from outer
return y
return inner()
return mid()
assert with_default(11) == 11
# === Owner reads the var BEFORE a nested default captures it ===
# Pins the pre-pass bug: a transitive capture hidden in a nested function's
# default expression must promote the variable to a cell up front, otherwise the
# earlier `a + 1` read resolves it as a plain local and diverges at runtime.
def default_after_read(a):
y = a + 1 # owner reads `a` before `mid`/`inner` are defined
def mid():
def inner(b=a): # default evaluated in mid; `a` captured from outer
return b
return inner()
return y, mid()
assert default_after_read(10) == (11, 10)
# === Single-level default capture with an earlier owner read ===
def default_one_level(a):
y = a + 1
def mid(x=a): # default evaluated in outer; `a` captured one level up
return x
return y, mid()
assert default_one_level(10) == (11, 10)
# === `def f(a=a)` gotcha: default RHS captures the enclosing name ===
# The right-hand `a` is evaluated in the enclosing scope (capturing the param),
# even though the nested function also has a parameter named `a`.
def same_name_default(a):
def inner(a=a): # RHS `a` is outer's param (10); param `a` shadows inside
return a
return inner()
assert same_name_default(10) == 10
# === Intermediate rebind shadows a default capture ===
# `mid` rebinds `a`, so `inner`'s default captures mid's `a`, not outer's.
def default_shadowed(a):
def mid():
a = 99
def inner(b=a): # captures mid's a (99), not outer's
return b
return inner()
return (mid(), a)
assert default_shadowed(1) == (99, 1)
# === `global` in an intermediate scope is not a capture candidate ===
# A name declared `global` is not a local binding, so a nested function reading
# it resolves to the module global rather than capturing a (non-existent) cell.
g_counter = 100
def uses_global():
global g_counter
g_counter = 20
def inner():
return g_counter # the module global, not a captured cell
return inner()
assert uses_global() == 20
assert g_counter == 20
# The intermediate `global` even overrides an enclosing local of the same name.
g_shadowed = 100
def outer_with_global_mid():
g_shadowed = 1 # outer local, shadowed for mid's chain by mid's `global`
def mid():
global g_shadowed
def inner():
return g_shadowed # module global (100), not outer's local (1)
return inner()
return mid()
assert outer_with_global_mid() == 100
# === Lambda whose default is a capturing lambda ===
# The default `(lambda: x)` is evaluated in the enclosing scope, so its `x`
# captures the enclosing `x` — NOT the outer lambda's same-named param. The
# cell-var pre-pass must therefore scan lambda defaults without filtering by the
# lambda's own params, else the enclosing cell is missed (the closure build then
# fails). `y` reads `x` first to pin that the owner stays consistent with the
# late-promoted cell.
def lambda_default_capture():
x = 10
y = x + 1
g = lambda x=(lambda: x): x()
return y, g()
assert lambda_default_capture() == (11, 10)
# The same capture works two levels up and survives owner mutation after the
# inner closure is built (a shared cell, not a snapshot).
def lambda_default_two_level():
v = 1
def mid():
g = lambda x=(lambda: v): x
return g()
inner = mid()
v = 42 # mutated after the default closure was created
return inner()
assert lambda_default_two_level() == 42