Skip to content

Commit 0c6594c

Browse files
committed
Merge branch 'master' of https://github.com/brownplt/lambda-py
2 parents 130bb37 + f44594f commit 0c6594c

File tree

4 files changed

+323
-4
lines changed

4 files changed

+323
-4
lines changed

‎base/python-lexical-printer.rkt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@
331331
(display starting-tab)
332332
(lexexpr-print-helper left "")
333333
(display "")
334-
(display op)
334+
(pretty-print-op op)
335335
(display "")
336336
(lexexpr-print-helper right "")
337337
this-expr

‎doc/scope.md‎

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
While λπ handles much of Python, some of its most immediate uses involve
2+
wrangling Pythonic scope.
3+
4+
The first two phases of desugaring are related to scope, and for many
5+
applications may produce a more usable AST structure. The goal of these two
6+
phases is to determine the relationship between the various definition forms
7+
and their use sites. The definition forms and related modifiers we're
8+
concerned with are:
9+
10+
x = e
11+
12+
class C(superclass):
13+
...
14+
15+
def f(arg, ...):
16+
...
17+
18+
global x
19+
20+
nonlocal x
21+
22+
23+
The scope desugaring takes these forms and elaborates them into a simpler AST
24+
datatype, where each variable is bound with an explicit, lexical, let-binding
25+
form, and each use of a variable is marked with whether it is global or local.
26+
Further, not all assignment statements are actually assignments to variables
27+
directly; some turn into assignments to class members when in the body of a
28+
class. These assignments are also transformed.
29+
30+
This is accomplished in two phases:
31+
32+
1. Mark each assignment statement and variable use as either global, local,
33+
or a class instance variable. The distinction between nonlocal and local is
34+
removed at this point, as all variables have an obvious, unambiguous binding
35+
position inserted as a "let"-like form wrapping their use at the appropriate
36+
level. Variables that are declared in classes and act as both variables and
37+
fields of a class are marked as "instance" variables.
38+
2. Transform instance variable assignments to class field assignments, and do
39+
appropriate lifting of definitions out of classes.
40+
41+
There is a printer for this simplified language and some scripts for
42+
experimenting with it. Here is an example (run from the base/ directory of a
43+
lambda-py checkout).
44+
45+
Our sample program is:
46+
47+
$ cat ../examples/scope/nonlocal-function-vardef.py
48+
def f():
49+
x = 0
50+
def inc():
51+
nonlocal x
52+
x += 1
53+
return x
54+
def dec():
55+
nonlocal x
56+
x -= 1
57+
return x
58+
return inc, dec
59+
60+
inc, dec = f()
61+
___assertEqual(inc(), 1)
62+
___assertEqual(inc(), 2)
63+
___assertEqual(dec(), 1)
64+
___assertEqual(dec(), 0)
65+
66+
To see the phase-1 desugaring, we can use the script `show-scope.sh`:
67+
68+
$ ./show-scope.sh < ../examples/scope/nonlocal-function-vardef.py
69+
# assigned to (global) f
70+
def f( ):
71+
{
72+
defvar (local) x = UNDEF in{
73+
defvar (local) inc = UNDEF in{
74+
defvar (local) dec = UNDEF in{
75+
(local) x = 0
76+
# assigned to (local) inc
77+
def inc( ):
78+
{
79+
nonlocal x
80+
(local) x+=1
81+
return (local) x
82+
}
83+
# assigned to (local) dec
84+
def dec( ):
85+
{
86+
nonlocal x
87+
(local) x-=1
88+
return (local) x
89+
}
90+
return [(local) inc, (local) dec]
91+
}
92+
}
93+
}
94+
}
95+
[(global) inc, (global) dec] = (global) f()
96+
(global) ___assertEqual((global) inc(), 1)
97+
(global) ___assertEqual((global) inc(), 2)
98+
(global) ___assertEqual((global) dec(), 1)
99+
(global) ___assertEqual((global) dec(), 0)
100+
101+
Notice a few things here:
102+
103+
1. The local definitions for `x`, `inc`, and `dec` have been explicitly lifted
104+
to the top of the block of `f`.
105+
2. The definition of `f` is annotated with a note that it is a global definition
106+
3. The various *uses* of `x` inside `inc` and `dec` are labelled `local`
107+
(though, as a reminder, there is a note that there was a nonlocal declaration
108+
for them).
109+
4. Both `inc` and `dec` are `global` variables in the global block because of
110+
the later assignment, but both names are also noted as `local` in the body
111+
of `f`.
112+
113+
What's printing here is actually a prettier representation of a modified AST
114+
that contains scope-type information explicitly on each use of an identifier.
115+
116+
If we change the program slightly, we can see some differences. For example,
117+
we could change `dec` to take an `x` parameter, and drop the `nonlocal x` at
118+
the top of `dec`:
119+
120+
$ cat ../examples/scope/nonlocal-function-vardef-shadow.py
121+
def f():
122+
x = 0
123+
def inc():
124+
nonlocal x
125+
x += 1
126+
return x
127+
def dec():
128+
x = 1 # <--- this line changed
129+
x -= 1
130+
return x
131+
return inc, dec
132+
133+
inc, dec = f()
134+
___assertEqual(inc(), 1)
135+
___assertEqual(inc(), 2)
136+
___assertEqual(dec(1), 0) # <--- these calls each have a fresh `x`
137+
___assertEqual(dec(1), 0) # <--/
138+
___assertEqual(inc(), 3)
139+
140+
141+
This desugars differently:
142+
143+
144+
$ ./show-scope.sh < ../examples/scope/nonlocal-function-vardef-shadow.py
145+
# assigned to (global) f
146+
def f( ):
147+
{
148+
defvar (local) x = UNDEF in{
149+
defvar (local) inc = UNDEF in{
150+
defvar (local) dec = UNDEF in{
151+
(local) x = 0
152+
# assigned to (local) inc
153+
def inc( ):
154+
{
155+
nonlocal x
156+
(local) x+=1
157+
return (local) x
158+
}
159+
# assigned to (local) dec
160+
def dec( ):
161+
{
162+
defvar (local) x = UNDEF in{# <-- new local binding here, shadowing the outer `x`
163+
(local) x = 1
164+
(local) x-=1
165+
return (local) x
166+
}
167+
}
168+
return [(local) inc, (local) dec]
169+
}
170+
}
171+
}
172+
}
173+
[(global) inc, (global) dec] = (global) f()
174+
(global) ___assertEqual((global) inc(), 1)
175+
(global) ___assertEqual((global) inc(), 2)
176+
(global) ___assertEqual((global) dec(), 0)
177+
(global) ___assertEqual((global) dec(), 0)
178+
(global) ___assertEqual((global) inc(), 3)
179+
180+
181+
Here we see that in the body of `dec`, there is an additional local binding for
182+
`x` added, and that is the binding used and updated in the body of `dec` (the
183+
other `x` is unaffected).
184+
185+
There is a second step for handling classes, which come with their own
186+
complications. This is a little more verbose, but after, all variables are
187+
either global or local, and instance variables are turned into object access
188+
and update. After the first step, they are merely labelled as "instance"
189+
variables, which is still useful for distinguishing their role. For example,
190+
in this simple class, the binding site of `x` in C is labelled as `instance`.
191+
The `x` in the body of the method `f` is correctly labelled as a reference to
192+
the global `x`, and the final `x` is labelled as local, meaning it will act as
193+
a reference to the nearest (non-global) definition.
194+
195+
196+
$ cat ../examples/scope/simple-class.py
197+
x = 5
198+
class C(object):
199+
x = 10
200+
def f(self):
201+
return x
202+
y = x + 10
203+
204+
c = C()
205+
___assertEqual(c.y, 20)
206+
___assertEqual(c.f(), 5)
207+
208+
209+
$ ./show-scope.sh < ../examples/scope/simple-class.py
210+
(global) x = 5
211+
class C((global) object):
212+
{
213+
(instance) x = 10
214+
# assigned to (instance) f
215+
def f((local) self ):
216+
{
217+
nonlocal self
218+
return (global) x
219+
}
220+
(instance) y = (local) x + 10
221+
}
222+
(global) c = (global) C()
223+
(global) ___assertEqual((global) c.y, 20)
224+
(global) ___assertEqual((global) c.f(), 5)
225+
226+
227+
The full desugaring of the class is more verbose, and arguably harder to relate
228+
to surface Python, but also makes the binding structure totally clear:
229+
230+
231+
$ ./show-postclass-scope.sh < ../examples/scope/simple-class.py
232+
module:
233+
pass
234+
defvars x, C, (global) c = UNDEF in{
235+
(global) x = 5
236+
# assigned to (global) C
237+
class C((global) object):
238+
{
239+
defvar (local) class-replacement4308 = UNDEF in{
240+
# assigned to (local) class-replacement4308
241+
# Here we make a *function* that refers to the correct x in the
242+
# scope outside this class body. This function will be
243+
# substituted for uses of "x" inside the methods of the class.
244+
# This works in general for nested combinations of global and
245+
# nonlocal
246+
def class-replacement4308((local) self ):
247+
{
248+
# active locals: self,
249+
nonlocal self
250+
return (global) x
251+
}
252+
{
253+
## Here, the "x" inside the class body is bound to a normal,
254+
## let-bound identifier
255+
defvar (local) x = UNDEF in{
256+
defvar (local) f = UNDEF in{
257+
defvar (local) y = UNDEF in{
258+
# active locals: x, f, y,
259+
## x is updated, and C's x field is immediately updated to
260+
## the same value: this assignment statement has a dual
261+
## purpose
262+
(local) x = 10
263+
(global) C.x = (local) x
264+
# assigned to (local) f
265+
def f((local) self ):
266+
{
267+
# This is the use of the function that refers to x
268+
return (local) class-replacement4308((local) self)
269+
}
270+
(global) C.f = (local) f
271+
## Here, the reference to x is a simple, local reference
272+
## to the let-bound x in the class body
273+
(local) y = (local) x + 10
274+
(global) C.y = (local) y
275+
}
276+
}
277+
}
278+
}
279+
}
280+
}
281+
(global) c = (global) C()
282+
(global) ___assertEqual((global) c.y, 20)
283+
(global) ___assertEqual((global) c.f(), 5)
284+
}
285+
286+
Here, we have re-organized things so that all scope is explicit. At this
287+
point, the semantics of scope works with simple lexical rules of let-binding
288+
and function parameter passing, and it's explicit which assignments cause
289+
updates to the fields of C, and which nested uses refer to outside the
290+
function.
291+
292+
From here, tools that would be potentially easier to build than on surface
293+
Python's AST are:
294+
295+
1. A way to get this information into a Python TreeVisitor, so the visitor can
296+
know the true binding position of each used variable as it encounters them.
297+
2. A proof-of-concept variable rename refactoring or similar tool, either
298+
standalone or on top of (1)
299+
3. A simple type-checker that knows about class fields as both variables and
300+
members of the class object
301+
302+
One thing the current system lacks is a good cross-module scope story, but this
303+
can be built up from the knowledge of globals in each module and more attention
304+
to import statements. In easy cases, this is merely a matter of engineering,
305+
and is a direction we'd like to move in.
306+
307+

‎examples/scope/simple-class.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
x=5
2+
classC(object):
3+
x=10
4+
deff(self):
5+
returnx
6+
y=x+10
7+
8+
c=C()
9+
___assertEqual(c.y, 20)
10+
___assertEqual(c.f(), 5)

‎tests/python-reference/scope/nonlocal-function-vardef-shadow.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ def inc():
44
nonlocalx
55
x+=1
66
returnx
7-
defdec(x):
7+
defdec():
8+
x=1
89
x-=1
910
returnx
1011
returninc, dec
1112

1213
inc, dec=f()
1314
___assertEqual(inc(), 1)
1415
___assertEqual(inc(), 2)
15-
___assertEqual(dec(1), 0)
16-
___assertEqual(dec(1), 0)
16+
___assertEqual(dec(), 0)
17+
___assertEqual(dec(), 0)
18+
___assertEqual(inc(), 3)

0 commit comments

Comments
(0)