HW 5: Environment Walkthroughs¶
Step-by-step traces of the variable environment as the compiler works through a piq program: what the
environment is before each statement, and what it is after. These are the environments that compileStmt /
compileStmts / compileLoop (see HW 5) pass from one statement to the next.
One notation note before you start: these traces write the environment as { x ↦ 10 }. HW 5's own ghci checks
show the exact same environment as fromList [("x",10.0)] — that's just how Data.Map prints an Env. Same
value, two notations.
How to read a trace¶
env {} ← the environment at this point
x = 10 ← a statement (concrete piq syntax)
env { x ↦ 10 } ← the environment after it (= before the next statement)
y = x * 2 -- a note: what gets evaluated, what gets drawn
» save i: none ← a step the compiler takes that isn't a statement
{ x ↦ 10, y ↦ 20 }is an environment wherexis 10 andyis 20.{}is the empty environment. The order inside the braces doesn't matter (GHCI prints them in alphabetical order, asfromList [("x",10.0),("y",20.0)]).- Every value is a
Double; whole numbers are written without the.0. - Only the variable environment is shown. The procedure environment (
box ↦ its definition, …) is built once from thedefines before anything runs, and never changes. - Drawing statements leave the environment alone, so their env line is just repeated. Where nothing interesting happens, a few env lines are skipped.
Part 4: assignment and sequences¶
Example 1: assignment, drawing, reassignment¶
env {}
x = 10 -- 10
env { x ↦ 10 }
y = x * 2 -- x * 2 = 10 * 2 = 20
env { x ↦ 10, y ↦ 20 }
square y -- y = 20: draws a 20 mm square
env { x ↦ 10, y ↦ 20 }
x = x + y -- x + y = 10 + 20 = 30
env { x ↦ 30, y ↦ 20 }
- An assignment evaluates its right-hand side in the environment before it, then binds the name.
- A drawing statement uses the environment but doesn't change it.
- Assigning to a name that already has a value replaces the value. The env after one statement is exactly the
env before the next: that's what
compileStmtsthreads along.
Example 2: doubling (part4Doubling)¶
env {}
x = 10
env { x ↦ 10 }
square x -- 10 mm square
x = x * 2 -- uses the OLD x: 10 * 2 = 20
env { x ↦ 20 }
square x -- 20 mm square
x = x * 2 -- 20 * 2 = 40
env { x ↦ 40 }
square x -- 40 mm square
env { x ↦ 40 }
x = x * 2is not an equation. Thexon the right is looked up in the env before the statement.
Example 3: using a variable before it has a value¶
env {}
x = 10
env { x ↦ 10 }
square y -- look up y in { x ↦ 10 }: not there
-- STOPS: undefined variable: y
y = 20 -- never reached
- Each statement only sees what the statements before it produced. It doesn't matter that
ygets a value later.
Part 5: loops¶
Example 4: an accumulator¶
env {}
total = 0
env { total ↦ 0 }
for i from 1 to 4 {
» bounds, evaluated once in { total ↦ 0 }: 1 and 4, so i takes 1, 2, 3, 4
» save i: none (i has no value yet)
── i = 1 ── -- start from the env before the loop, set i ↦ 1
env { total ↦ 0, i ↦ 1 }
total = total + i -- 0 + 1 = 1
env { total ↦ 1, i ↦ 1 }
── i = 2 ── -- start from the env iteration 1 left, set i ↦ 2
env { total ↦ 1, i ↦ 2 }
total = total + i -- 1 + 2 = 3
env { total ↦ 3, i ↦ 2 }
── i = 3 ──
env { total ↦ 3, i ↦ 3 }
total = total + i -- 3 + 3 = 6
env { total ↦ 6, i ↦ 3 }
── i = 4 ──
env { total ↦ 6, i ↦ 4 }
total = total + i -- 6 + 4 = 10
env { total ↦ 10, i ↦ 4 }
» restore i: it had no value before, so remove it
}
env { total ↦ 10 }
- Each iteration starts from the environment the previous iteration left, with the loop variable set to the
next value. That's how
totalaccumulates (compileLoopthreads the env just likecompileStmts). - Assignments to other variables (
total) survive the loop. - The loop variable doesn't: it was saved as "none", so afterwards it's removed.
Example 5: a loop variable that already had a value¶
env {}
i = 100
env { i ↦ 100 }
for i from 1 to 3 {
» bounds: 1 and 3
» save i: 100
── i = 1 ──
env { i ↦ 1 }
dot
env { i ↦ 1 }
── i = 2 ──
env { i ↦ 2 }
dot
env { i ↦ 2 }
── i = 3 ──
env { i ↦ 3 }
dot
env { i ↦ 3 }
» restore i: put back 100
}
env { i ↦ 100 }
square i -- still a 100 mm square
- Inside the loop,
iis the loop'si. Afterwards, the oldiis back, as if the loop had never touched it.
Example 6: the bounds are evaluated once¶
env {}
n = 3
env { n ↦ 3 }
for i from 1 to n {
» bounds, evaluated once in { n ↦ 3 }: 1 and 3, so i takes 1, 2, 3 (fixed from now on)
» save i: none
── i = 1 ──
env { n ↦ 3, i ↦ 1 }
n = n + 1 -- 3 + 1 = 4
env { n ↦ 4, i ↦ 1 }
── i = 2 ──
env { n ↦ 4, i ↦ 2 }
n = n + 1 -- 5. n is now bigger than 3, but the loop still stops after i = 3
env { n ↦ 5, i ↦ 2 }
── i = 3 ──
env { n ↦ 5, i ↦ 3 }
n = n + 1 -- 6
env { n ↦ 6, i ↦ 3 }
» restore i: remove it
}
env { n ↦ 6 }
- Changing
ninside the loop doesn't change how many times it runs: the list of values[1 .. 3]was made before the first iteration.
Example 7: zero iterations, and a bad bound¶
env {}
x = 1
env { x ↦ 1 }
for i from 5 to 1 {
» bounds: 5 and 1, so i takes no values at all ([5 .. 1] is [])
» save i: none
x = x * 10 -- never runs
» restore i: remove it (it isn't there anyway)
}
env { x ↦ 1 } -- unchanged, and no code was produced
for i from 1 to x / 2 {
» bounds, in { x ↦ 1 }: 1 and 0.5
-- STOPS: non-integral loop bound: 0.5
dot
}
Example 8: nested loops¶
for row from 1 to 2 {
for col from 1 to 2 {
dot
move right 10
}
move left 20
move down 10
}
env {}
for row from 1 to 2 {
» bounds: 1 and 2; save row: none
── row = 1 ──
env { row ↦ 1 }
for col from 1 to 2 {
» bounds: 1 and 2; save col: none
── col = 1 ──
env { row ↦ 1, col ↦ 1 }
dot
move right 10
env { row ↦ 1, col ↦ 1 }
── col = 2 ──
env { row ↦ 1, col ↦ 2 }
dot
move right 10
env { row ↦ 1, col ↦ 2 }
» restore col: remove it
}
env { row ↦ 1 }
move left 20
move down 10
env { row ↦ 1 }
── row = 2 ── -- start from the env row 1 left: { row ↦ 1 }, set row ↦ 2
env { row ↦ 2 }
for col from 1 to 2 {
» bounds: 1 and 2; save col: none (removed at the end of row 1)
── col = 1 ──
env { row ↦ 2, col ↦ 1 }
dot
move right 10
── col = 2 ──
env { row ↦ 2, col ↦ 2 }
dot
move right 10
env { row ↦ 2, col ↦ 2 }
» restore col: remove it
}
env { row ↦ 2 }
move left 20
move down 10
env { row ↦ 2 }
» restore row: remove it
}
env {}
- The inner loop is just a statement in the outer loop's body, so it runs (save, iterate, restore) once per outer
iteration.
colappears and disappears each time;rowstays put for a whole row.
Part 6: procedures¶
Example 9: a simple call¶
define box(size, gap) {
half = size / 2
move right half
square size
move right half + gap
}
(A variant of box from part6Boxes, with a local variable half.)
env {}
w = 5
env { w ↦ 5 }
box(w * 2, 3)
» look up box: parameters size, gap
» 2 arguments for 2 parameters: OK
» evaluate the arguments in the CALLER's env { w ↦ 5 }: w * 2 = 10, and 3
» local env = caller's env + size ↦ 10, gap ↦ 3
env { w ↦ 5, size ↦ 10, gap ↦ 3 }
half = size / 2 -- 10 / 2 = 5
env { w ↦ 5, size ↦ 10, gap ↦ 3, half ↦ 5 }
move right half -- 5
square size -- 10 mm square
move right half + gap -- 5 + 3 = 8
env { w ↦ 5, size ↦ 10, gap ↦ 3, half ↦ 5 }
» keep the body's code; throw its env away
env { w ↦ 5 } -- the caller's env, exactly as it was before the call
- The arguments are evaluated where the call is, before the body starts.
- The body runs in a new local environment: the caller's, plus the parameters. (So the body could also read
w.) - Nothing the body does to the environment (
half,size,gap) escapes. The call returns the caller's env unchanged, plus the body's code.
Example 10: a parameter with the same name as a caller variable (part6Shadowing)¶
define foo(x) {
x = x + 100
square x
}
env {}
x = 10
env { x ↦ 10 }
foo(20)
» evaluate the argument in { x ↦ 10 }: 20
» local env = { x ↦ 10 } + x ↦ 20 (the parameter hides the caller's x)
env { x ↦ 20 }
x = x + 100 -- 20 + 100 = 120
env { x ↦ 120 }
square x -- 120 mm square
env { x ↦ 120 }
» throw the local env away
env { x ↦ 10 }
square x -- 10 mm square
- The body's
xis the parameter. Changing it changes only the local environment, which disappears after the call.
Example 11: a call inside a loop (part6Boxes)¶
define box(size, gap) {
move right size / 2
square size
move right size / 2 + gap
}
move left 80
for i from 1 to 5 {
box(i * 8, 10)
}
env {}
move left 80
env {}
for i from 1 to 5 {
» bounds: 1 and 5; save i: none
── i = 1 ──
env { i ↦ 1 }
box(i * 8, 10)
» arguments, in { i ↦ 1 }: 1 * 8 = 8, and 10
» local env:
env { i ↦ 1, size ↦ 8, gap ↦ 10 }
move right size / 2 -- 4
square size -- 8 mm square
move right size / 2 + gap -- 4 + 10 = 14
» throw the local env away
env { i ↦ 1 }
── i = 2 ──
env { i ↦ 2 }
box(i * 8, 10)
» arguments, in { i ↦ 2 }: 16, and 10
» local env:
env { i ↦ 2, size ↦ 16, gap ↦ 10 }
move right size / 2 -- 8
square size -- 16 mm square
move right size / 2 + gap -- 18
» throw the local env away
env { i ↦ 2 }
── i = 3, 4, 5: the same, with size 24, 32, 40 ──
env { i ↦ 5 }
» restore i: remove it
}
env {}
- Each call gets a fresh local environment built from the env at that moment (here, with the current
i). sizeandgapnever show up in the loop's own environment.
Example 12: a procedure that calls a procedure that has a loop¶
define target(r) {
for k from 1 to 3 {
circle k * r / 3
}
dot
}
define flower(r) {
d = 2 * r
target(r)
move up d
target(r / 2)
move down d
}
move left 80
flower(15)
(A cut-down version of part6Flowers.)
env {}
move left 80
env {}
flower(15)
» argument, in {}: 15
» flower's local env = {} + r ↦ 15
env { r ↦ 15 }
d = 2 * r -- 30
env { r ↦ 15, d ↦ 30 }
target(r)
» argument, in flower's env { r ↦ 15, d ↦ 30 }: 15
» target's local env = flower's env + r ↦ 15
env { r ↦ 15, d ↦ 30 }
for k from 1 to 3 {
» bounds: 1 and 3; save k: none
── k = 1 ──
env { r ↦ 15, d ↦ 30, k ↦ 1 }
circle k * r / 3 -- radius 5
── k = 2 ──
env { r ↦ 15, d ↦ 30, k ↦ 2 }
circle k * r / 3 -- radius 10
── k = 3 ──
env { r ↦ 15, d ↦ 30, k ↦ 3 }
circle k * r / 3 -- radius 15
» restore k: remove it
}
env { r ↦ 15, d ↦ 30 }
dot
» throw target's local env away
env { r ↦ 15, d ↦ 30 } -- back in flower, with flower's env
move up d -- 30
target(r / 2)
» argument, in flower's env: 15 / 2 = 7.5
» target's local env = flower's env + r ↦ 7.5 (target's r hides flower's r)
env { r ↦ 7.5, d ↦ 30 }
for k from 1 to 3 {
» bounds: 1 and 3; save k: none
── k = 1, 2, 3 ── -- radius 2.5, 5, 7.5
env { r ↦ 7.5, d ↦ 30, k ↦ 3 } (after the last iteration)
» restore k: remove it
}
env { r ↦ 7.5, d ↦ 30 }
dot
» throw target's local env away
env { r ↦ 15, d ↦ 30 } -- flower's r is 15 again
move down d -- 30
» throw flower's local env away
env {}
- Calls nest: each call builds its local env from the env of whoever called it, and each one's env is thrown away when it finishes, returning to its caller's env.
- Inside
target,dis visible (it came from flower's env), even thoughtargetdoesn't use it.