CS374: Principles of Programming Languages - Lab: Environments and Scope (15 Points)
Contents
Purpose, Task, and Criteria
Purpose: To build the Interpreter assignment's Environment class, individually or with a partner, covering nested scopes, define versus assign, and shadowing, and to verify it against the exact behaviors the Interpreter's evaluator depends on.
Task: Individually or with a partner, implement an Environment class with parent chaining, distinguish define from assign, and verify shadowing, scope restoration, and name-error behavior against a provided test script.
Criteria: I grade a correct Environment class that passes all provided behavior tests, and a short trace exercise that predicts scope behavior on paper. The full breakdown is in the rubric below.
Assignment Goals
The goals of this assignment are:
- To implement an Environment class with parent chaining supporting nested scopes
- To distinguish definition (creating a name in the current scope) from assignment (updating the nearest enclosing binding)
- To verify shadowing, scope restoration, and name-error behavior with tests and paper traces
Background Reading and References
Please refer to the following readings and examples offering templates to help get you started:
The Assignment
In this lab you build Environment, the class that makes scope real in your interpreter. An environment is the data structure that maps variable names to their values. Each block of code gets its own environment, and each environment points to the enclosing one, so a lookup can walk outward until it finds the name. You leave with a tested environment.py that the Interpreter assignment’s Step 2c imports unchanged, plus a paper trace that predicts what your class will do before you run it. You may do this lab with a partner or on your own (see the pair policy below).
Pair policy. You may do this lab in pairs. You each submit the same files, name each other in them, and earn the same grade. You may also work alone. The Interpreter assignment remains individual work: you may both carry this shared Environment into it, but the evaluator around it must be your own.
Before You Start
You need:
- Python 3.10 or newer. The class uses only the standard library.
- Any editor that saves plain
.pyfiles, and a terminal you can runpython3from. If the terminal is new to you, read the Dev Environment tutorial and the Shell for Language Development primer first; both are short. - The provided behavior script test_environment.py.
- The reading listed above (the Binding, Scope, and Environments activity). Part 0 uses its vocabulary.
Make a folder for this lab and confirm your Python version from inside it (any 3.10 or higher is fine; on Windows, if python3 is not found, use python wherever this page says python3):
mkdir cs374-environments
cd cs374-environments
python3 --version
Python 3.11.9
Next, open the folder in your editor and create one file by hand, trace.md. It holds all of your paper work, so give it these headings now and fill them in as you go:
# Environments Lab Trace
Names: <your name, and your partner's if you have one>
## Part 0
### Step 0.1
### Step 0.2
### Step 0.3
### Step 0.4
## Part 2
### Step 2.1
### Step 2.2
### Step 2.3
A photo of a paper page is fine under any of these headings. You create the Python files when the steps ask for them, all in this same folder. This table shows which file each step works in, so you always know where new work goes:
| Step | File you work in | What you do there |
|---|---|---|
| 0.1 to 0.4 | trace.md, under ## Part 0 |
trace the shadowing expression, decide the unbound-variable error, write two probes |
| 1.1 | environment.py |
create the file from the skeleton and write define, lookup, and assign |
| 1.2 | try_env.py |
paste the usage example and run it (a working file, not graded) |
| 1.3 | test_environment.py, test_output.txt |
download the provided script, run it, and save its output |
| 2.1 | trace.md, under ## Part 2 |
fill in the trace table before you run anything |
| 2.2 | trace_run.py, then trace.md |
translate the trace program into calls, run it, and note any missed prediction |
| 2.3 | trace.md, under ## Part 2 |
answer the two theory questions |
You never edit test_environment.py. Scratch files that a step suggests, such as explore_env.py below, are for you only and are not submitted.
Time budget. About two to three hours: thirty minutes on paper for Part 0, about an hour of coding for Part 1, and thirty to forty-five minutes of prediction and checking for Part 2. See the course schedule for the assigned and due dates.
Part 0: Trace Binding and Scope on Paper
Do this part on paper before you write the class; you may do it alone even if you do the rest of the lab with a partner. Put your answers at the top of trace.md, under the ## Part 0 heading you made in Before You Start (a photo of the paper is fine). Two terms first. Under lexical scope, a name refers to the binding in the enclosing text of the program. Under dynamic scope, a name refers to the most recent binding made by any caller that is still running.
Step 0.1: Trace the Shadowing Expression
Evaluate this expression by hand, drawing the environment at each step. The inner binding of x shadows (hides) the outer one.
You write (in trace.md under the ### Step 0.1 heading): the lexical trace table and the final value. No code.
let x = 2 in let x = x + 1 in x * x
Do this.
- Copy the table below into
trace.md(or draw it on paper). Step 1 is done for you as a model.- Fill in the chain and the result at every step. Write chains innermost-first, each scope a box of bindings with an arrow to its parent, like
E1{x=2} -> E0{}(E0is the empty top-level scope). A drawn picture of boxes and arrows is just as good.- Write the final value of the whole expression under the table.
| Step | Expression being evaluated | Environment chain (lexical) | Result |
|---|---|---|---|
| 1 | let x = 2 in ... binds the outer x |
E1{x=2} -> E0{} |
scope E1 entered |
| 2 | x + 1, evaluated inside E1 |
||
| 3 | let x = <step 2 result> in ... binds the inner x |
||
| 4 | x * x, evaluated inside the inner scope |
||
| 5 | the inner let ends, then the outer let ends |
Step 0.2: Predict It Under Dynamic Scope
You write (in trace.md under the ### Step 0.2 heading): the dynamic trace table and the step where the two tables first differ. No code.
Do this.
- Copy the same table again, headed “Environment chain (dynamic)”, and fill it in under the dynamic rule.
- Mark the exact step where the two tables first give different results. If every row agrees for this expression, say so, and say what a program would have to contain before the two rules could disagree.
Step 0.3: Decide the Unbound-Variable Behavior
You write (in trace.md under the ### Step 0.3 heading): your two-line expression, its trace, and one sentence naming the error and the moment it fires. No code.
Do this.
- Write a two-line expression of your own that uses an unbound variable, a name no environment in the chain defines (for example, an outer
letthat bindsxand a body that mentionsy).- Trace it the same way, and stop at the step where the chain runs out.
- Decide what error the evaluator should raise (name the class) and at what moment. The class you build in Part 1 raises
LangNameErrorfor a missing name, so name that class unless you can argue for another; the moment is the real decision. The choices are: when the program is read, when the enclosingletis entered, or when the lookup of the name actually runs. Write the error and the moment as one sentence intrace.md. Decide this now rather than let Python decide for you.
Step 0.4: Write Two Probe Programs for the Mystery Scoping Language
In class you will run programs against an interpreter whose scoping rule is hidden and deduce the rule from the answers. A probe program is one whose output differs depending on whether the language is lexically or dynamically scoped, so a program that prints the same thing under both rules tells you nothing. Here is the shape of a probe, in pseudocode: a function reads a name it did not bind, and it is called from a place that rebinds that name. This one is mine, so it does not count toward your two.
let x = 1;
fun show() { print x; } # show reads x but does not bind it
fun caller() {
let x = 2; # rebinds x, then calls show
show();
}
caller();
You write (in trace.md under the ### Step 0.4 heading): two probe programs and four predictions, one per program per rule. No code.
Do this.
- Write two short programs of your own whose output differs depending on the scoping rule. Make one of them as short as you can.
- For each program, write your prediction of what it prints under each rule: one line for lexical, one line for dynamic.
- If both predictions for a probe match, it cannot tell the rules apart, so change it.
Have ready. Both probe programs and all four predictions. Run them against the Mystery Scoping Language interpreter in Part II of the Binding, Scope, and Environments activity.
Part 1: Build the Environment Class
Implement Environment in environment.py. It stands alone (no AST or evaluator needed) with this interface:
Environment(parent=None): a scope with an optional enclosing scope.define(name, value): createnamein this scope. This shadows any outer binding of the same name.assign(name, value): update the nearest enclosing binding ofname. If no scope in the chain defines it, raise a language-levelLangNameError(not a bare PythonKeyError) that carries the name.lookup(name): return the nearest enclosing binding’s value, or raiseLangNameError.
Before You Code: How an Environment Lives in Python
Look back at your Part 0 chains, such as E1{x=2} -> E0{}. Each box is a table of names and values, and each arrow points to the enclosing box. In Python, one Environment object holds exactly those two things:
| On paper | In Python | Example value |
|---|---|---|
| the box of bindings for one scope | self._bindings, a dict from name to value |
{'x': 2, 'y': 10} |
| the arrow to the enclosing scope | self._parent, another Environment |
the outer scope’s object |
the end of the chain (E0 has no arrow) |
self._parent is None |
None |
“is x bound in this box?” |
name in self._bindings |
True or False |
| “follow the arrow and ask again” | the same method, called on the object in self._parent |
whatever the outer scope answers |
A lookup walks the chain one box at a time. It checks the current box first and stops at the first box that has the name, which is exactly why an inner binding shadows an outer one. If it reaches a box whose parent is None without finding the name, the name is unbound.
Two details matter for the code you write next:
- Test membership with
in, not.get.self._bindings.get(name)returnsNoneboth when the name is missing and when the name is bound toNone.name in self._bindingstells those two cases apart. - Test the parent with
is not None, not truthiness.if self._parent:asks whether the parent is truthy, not whether it exists. An emptydictis falsy, and an object that defines__len__is falsy whenever it is empty.self._parent is not Noneasks the question you mean: is there an arrow to follow?
To see the box-and-arrow idea before you rely on it, save the lines below as explore_env.py, a scratch file you do not submit, and run python3 explore_env.py from inside cs374-environments. It builds two scopes out of plain dictionaries, so it does not need your class.
outer = {"bindings": {"x": 2, "y": 10}, "parent": None} # the top-level scope
inner = {"bindings": {"x": 51}, "parent": outer} # a block inside it
print("x" in inner["bindings"], "y" in inner["bindings"]) # which names live HERE
print(inner["bindings"]["x"]) # found in the first scope
print(inner["parent"] is outer) # the arrow to the parent
print(inner["parent"]["bindings"]["y"]) # y found one scope out
print(outer["parent"] is None) # the end of the chain
inner["bindings"]["x"] = 52 # write into THIS scope only
print(inner["bindings"], outer["bindings"])
print(bool({}), {} is not None) # an empty table is falsy, but it is not None
You should see.
True False 51 True 10 True {'x': 52} {'x': 2, 'y': 10} False TruePredict before you move on: if you look up
"z"by hand in this chain, which boxes do you check, and where do you stop?
Step 1.1: Create environment.py and Fill In the Methods
The skeleton below names every method and includes the two error classes, so the file runs on its own. LangError carries a line and column that default to zero; the evaluator fills those in during the Interpreter assignment. lookup and assign share a shape: check this scope, else defer to the parent, else raise. The one line that separates define from assign is the point of the lab: define writes into this scope’s table no matter what any parent holds, and assign never writes into this scope unless the name is already here.
You write: environment.py - the bodies of define, lookup, and assign in the Environment class.
You run: python3 environment.py
Do this.
- Create
environment.pyin yourcs374-environments/folder, paste in the skeleton below, and runpython3 environment.pyonce. A clean run prints nothing, and that is the point: a skeleton that runs and prints nothing has no syntax errors. ASyntaxErrororIndentationErrormeans the paste went wrong.- Replace each
raise NotImplementedError(...)with the code the# Stepcomments describe. Inlookupandassign, let the recursive call onself._parentdo the walking; you do not need a loop.- Give each
LangNameErrora message that includes the variable name, such asUndefined variable 'y'forlookupandCannot assign to undefined variable 'y'forassign.- Run
python3 environment.pyagain and confirm it still prints nothing.
"""environment.py: the scope table for the CS374 interpreter."""
class LangError(Exception):
"""Base class for every language-level error (same shape as Interpreter Step 5a)."""
def __init__(self, message, line=0, col=0):
self.message = message
self.line = line # the evaluator fills these in later
self.col = col
super().__init__(str(self))
def __str__(self):
return f"line {self.line}, col {self.col}: {self.message}"
class LangNameError(LangError):
"""Raised when a name has no binding anywhere in the chain."""
class Environment:
"""One scope: a table of bindings plus a link to the enclosing scope."""
def __init__(self, parent=None):
self._bindings = {} # name -> value, for THIS scope only
self._parent = parent # the enclosing Environment, or None at top level
def define(self, name, value):
# Step 1: store value under name in this scope's table.
# Do not look at the parent.
raise NotImplementedError("define")
def lookup(self, name):
# Step 1: if name is bound in this scope, return its value.
# Step 2: otherwise, if there is a parent, ask the parent and return its answer.
# Step 3: otherwise raise LangNameError with a message that names the variable.
raise NotImplementedError("lookup")
def assign(self, name, value):
# Step 1: if name is bound in this scope, update it here and stop.
# Step 2: otherwise, if there is a parent, hand the assignment to the parent and stop.
# Step 3: otherwise raise LangNameError. Never create a new binding here.
raise NotImplementedError("assign")
Fill in the steps one at a time. This table explains the idea behind each # Step comment, in order:
# Step |
How to think about it |
|---|---|
define, Step 1 |
One write into this scope’s dictionary, indexed by the name. No if, and no mention of the parent: define never cares what an outer scope holds. |
lookup, Step 1 |
Use the in test from Before You Code, not .get, so a name bound to None still counts as bound. If the name is here, this scope’s value is the answer, and you are done. |
lookup, Step 2 |
Check the parent with is not None before you touch it. Then call the parent’s own lookup with the same name. Return whatever it gives back. If you forget to return it, the parent’s answer is thrown away and lookup returns None. |
lookup, Step 3 |
You reach this only when the name is not here and there is no parent to ask. Pass just the message to LangNameError, and leave line and col at their defaults. |
assign, Step 1 |
The same membership test as lookup. Overwrite the value in this scope’s table, then return, so you do not also fall through to the parent. |
assign, Step 2 |
Hand the whole job, name and value, to the parent’s own assign. The parent runs the same three steps on its own table, so the recursion does the walking for you. |
assign, Step 3 |
Raise with the assign message. This path must never write into this scope’s table. That one rule is the whole difference between define and assign. |
You should see. Nothing.
python3 environment.pydefines classes and exits, so a clean run prints no output at all, both on the skeleton and on your finished file. Step 1.2 is where your methods first run.
If it fails.
IndentationError: expected an indented block after function definition: you deleted araise NotImplementedError(...)line before writing its replacement, so the method body holds only comments. A comment is not a statement; write the code, or leave theraisein place until you do.RecursionError: maximum recursion depth exceeded(in Step 1.2 or 1.3):lookuporassigncalled its own method onselfinstead of the same method on the parent, so it asks the same scope the same question forever.AttributeError: 'NoneType' object has no attribute 'lookup': the method asks the parent without checking that there is one. At the top levelself._parentisNone, so testis not Nonefirst and raiseLangNameErrorwhen it is.
Watch out.
if self._parent:tests the parent’s truthiness, not whether a parent exists. It happens to work for your class today, because Python treats any object as true by default. If you later giveEnvironmenta__len__method (to count bindings, say), an empty scope becomes false, and lookups skip it with no error at all. Compare the parent againstNoneexplicitly, withis not None.
Watch out. The Interpreter assignment’s Step 5a defines the same
LangErrorhierarchy ininterpreter.py. Keep one definition: importLangErrorandLangNameErrorfromenvironment.py, or move them into the interpreter’s error module and import them back here. With two copies ofLangNameError, anexcept LangNameErrorin the evaluator can miss the one yourEnvironmentraises.
Step 1.2: Run the Usage Example
This is the Interpreter assignment’s shadowing program, translated line by line into calls on your class. A let is a define, a bare assignment is an assign, entering a block is a new Environment whose parent is the current one, and leaving the block means you stop using it.
You write: try_env.py - the usage example below, pasted unchanged.
You run: python3 try_env.py
Do this. Create
try_env.pyin the same folder asenvironment.py, paste in the example below, and runpython3 try_env.pyfrom that folder. Run it as soon asdefineandlookupwork: it prints51and2, then stops at the unfinishedassign, and that crash is your to-do list.
from environment import Environment, LangNameError
outer = Environment() # the program's top-level scope
outer.define("x", 2) # let x = 2;
inner = Environment(parent=outer) # entering the block
inner.define("x", 51) # let x = 51; shadows the outer x
print(inner.lookup("x")) # print x; inside the block
print(outer.lookup("x")) # print x; after the block ends
outer.define("y", 10) # let y = 10;
inner.assign("y", 11) # y = 11; inside the block, no let
print(outer.lookup("y")) # the outer y changed
try:
inner.assign("nope", 0) # nope = 0; nobody defined nope
except LangNameError as err:
print("caught:", err)
You should see. Four lines. The wording after
caught:is whatever message you wrote, but it must contain the namenope.
51
2
11
caught: line 0, col 0: Cannot assign to undefined variable 'nope'
If it fails.
ModuleNotFoundError: No module named 'environment': you ran the command from a different folder.cdintocs374-environments/and run it again.- The third line prints
10instead of11: yourassigncreated a newyininnerinstead of updatingouter. That is the define-versus-assign bug the rubric names.- A
KeyErrortraceback instead ofcaught::assignfell through to a dictionary lookup instead of raisingLangNameError.
Step 1.3: Run the Provided Behavior Script
The example above prints 51 then 2 even if lookup never consults the parent, because both x bindings sit in the scope where the lookup starts. The provided test_environment.py checks that and four more behaviors:
- Lookup through three levels of nesting.
definein an inner scope shadows the outer binding.assignfrom an inner scope updates the outer binding.assignto an undefined name raises.- Scope restoration: after a child scope is discarded, the outer binding is unchanged.
You write: nothing new. Download test_environment.py, and fix environment.py if a behavior fails.
You run: python3 test_environment.py, then python3 test_environment.py > test_output.txt 2>&1
Do this.
- Download test_environment.py into
cs374-environments/, next toenvironment.py, and runpython3 test_environment.pyfrom that folder.- Save the output for your submission: redirect it with
python3 test_environment.py > test_output.txt 2>&1, or take a screenshot of the terminal.
You should see. All five behaviors passing and no Python traceback. The script is built on
unittest, so a passing run prints one dot per test and ends withOK. The time on theRanline may differ on your machine...... ---------------------------------------------------------------------- Ran 5 tests in 0.000s OKA failing test prints an
ForEin place of its dot, then aFAIL:orERROR:block that names the test, such astest_3_assign_updates_nearest_enclosing. The number in the test’s name matches the list above and tells you which method to reread. The redirect command prints nothing to the terminal; opentest_output.txtto check that it holds the same lines.
If it fails.
- Behavior 1 fails but Step 1.2 passed:
lookupfinds names in the current scope but never recurses toself._parent.- Behavior 3 fails:
assignchecksself._bindingsand then creates the name there instead of deferring to the parent.- Behavior 5 fails: something in
defineorassignwrote into the parent’s table directly. Onlyassignmay touch a parent, and only through the parent’s ownassign.
Part 2: Predict and Check the Scope Trace
Write your predictions in trace.md before you run anything. The test script comes with a short program of three nested blocks that mix define and assign. Its shape is below, with every step numbered; if the copy in test_environment.py differs from this listing, trace the script’s copy and renumber the steps to match it.
let x = 1; # step 1
let y = 10; # step 2
{ # step 3 enter block A
let x = 2; # step 4 define: shadows the outer x
y = y + x; # step 5 assign: no let, so it updates an existing y
{ # step 6 enter block B
x = x * 10; # step 7 assign
let z = y; # step 8 define
{ # step 9 enter block C
let y = 0; # step 10 define: shadows an outer y
z = z + x; # step 11 assign
print y; # step 12
} # step 13 leave block C
print x; # step 14
print z; # step 15
} # step 16 leave block B
print x; # step 17
} # step 18 leave block A
print x; # step 19
print y; # step 20
Step 2.1: Fill In the Trace Table Before You Run Anything
You write (in trace.md under the ### Step 2.1 heading): the 20-row trace table and your six predicted printed values. No code, and no runs yet.
Do this.
- Copy the table below into
trace.md, one row per numbered step, and fill in every row from the rules, not by running code. Step 1 is done for you as a model.- Draw each chain in the same innermost-first notation as Part 0, naming the scopes
top,A,B, andC. In the Result column write the value printed (for aletor assignment step), or “enter” and “leave” for the braces.- Under the table, write the six values you expect
| Step | Line | Environment chain | Result |
|---|---|---|---|
| 1 | let x = 1; |
top{x=1} |
x=1 created in top |
| 2 | let y = 10; |
||
| … | one row per step, through step 20 print y; |
Step 2.2: Run the Program Through Your Class and Compare
You do not have an evaluator yet, so run the program the way Step 1.2 did: translate each step into a call on your class. This is a preview of exactly what the Interpreter assignment’s Let, Assign, Block, and Var cases will do.
You write: trace_run.py - one line of Python per step of the trace program, steps 1 through 20, except the three “leave block” steps.
You run: python3 trace_run.py, then python3 test_environment.py --trace once your comparison is written.
Each scope gets its own variable, named after it: top for the top level, a for block A, b for block B, and c for block C. Each line calls a method on the variable for the scope that step runs in, the innermost block you are in at that moment. Your try_env.py from Step 1.2 already uses every kind of call you need.
Do this.
- Create
trace_run.pynext toenvironment.py. ImportEnvironmentthe same waytry_env.pydoes, and maketop, the program’s top-level scope, with no parent.- Work down the listing one step at a time, starting at step 1. For each step, decide which of the five kinds in the table below it is.
- Decide which scope the step runs in: the innermost block still open at that moment. That scope’s variable is the one you call the method on.
- Write the one line for that step. End it with a comment holding the step number and the original line, so you can match your lines against the
--traceoutput later.- For the three “leave block” steps (13, 16, and 18), write only a comment. Leaving a block takes no call: from that step on, your lines use the parent’s variable instead.
- Run
python3 trace_run.pyand compare the six printed values with your predictions. For any step where your prediction missed, write one sentence intrace.mdnaming the rule you misapplied (define versus assign, or which scope a lookup starts in).
Every step of the program is one of five kinds:
| Kind of step | How to think about it |
|---|---|
{ (enter a block) |
Make a new Environment whose parent is the variable of the block you are in now, and give it the new block’s name. Step 3 makes a inside top; step 6 makes b inside a; step 9 goes one level deeper. |
let name = value; (define) |
A let is a define on the current scope, never on an outer one. If the value is another variable, as in let z = y;, read that variable first with a lookup on the current scope. |
name = expression; (assign) |
No let means assign. Build the new value from lookup calls on the current scope, then call assign on the current scope too. Do not call it on the scope you think holds the name; walking the chain is the job of assign. |
print name; |
Print the result of a lookup on the innermost scope still open at that step. |
} (leave a block) |
No call at all. Switch to the parent’s variable for every line after it. |
You should see. Six lines of output, one integer per line, one per
trace.md, runpython3 test_environment.py --trace. It runs the five tests, then the same program step by step, and its last six lines must match your six. The trace output starts like this, matching the model row of your table:Part 2 trace program (steps numbered as on the lab page): line environment chain result step 1 let x = 1; top{x=1} x=1 created in top step 2 let y = 10; top{x=1, y=10} y=10 created in topDo not look at the rest of that output before your table is filled in.
If it fails.
NameError: name 'b' is not defined(or'c'): a line uses a block’s variable before the(enter)line that creates it. Check that steps 6 and 9 come first and use the namesbandc.LangNameError: line 0, col 0: Undefined variable 'z': a line after step 13 used a scope outside block B, wherezlives. Recheck which block is innermost at that step.- Your run and the
--traceoutput disagree on a value: one of your lines useddefinewhere the program has nolet, or called a method on the wrong block’s variable. Find the first step where your values differ from the--tracerow. Its chain column shows which scope holds each name, so check whether your line for that step should have created a binding or updated one.
Step 2.3: Answer the Two Theory Questions
Close trace.md with two theory questions from the Binding, Scope, and Environments session.
You write (in trace.md under the ### Step 2.3 heading): one answer per question. No code.
Do this.
- Which line of your class makes this language lexically scoped rather than dynamically scoped? Quote the line and say what it would have to become for the rule to change.
- Re-predict the trace program’s final output under dynamic scoping, where the lookup chain follows the callers rather than the enclosing text, and name the step where the two rules first diverge. If you conclude that they never diverge for this program, say why, and say what the program would need to contain before they could.
Deliverables
Submit a ZIP containing the files below, with your name (and your partner’s, if you worked in a pair) in trace.md.
| File or artifact | What it shows | Rubric row |
|---|---|---|
trace.md (top section): the shadowing trace tables, the unbound-variable decision, and two probe programs with predictions |
You can settle a scope question on paper before writing code | Part 0: Before You Start |
environment.py |
define, assign, and lookup with parent chaining and LangNameError |
The Environment Class |
The passing test_environment.py output (log or screenshot) |
All five behaviors pass, including the shadowing program’s 51 then 2 |
The Environment Class |
trace.md (Part 2 section): the filled trace table, the comparison against the run, and both theory answers |
Your predictions match the run, and you can point to the line that decides the scoping rule | Scope Trace Exercise |
try_env.py and trace_run.py are working files; include them if you like, but they are not graded.
Self-Check Before You Submit
- Part 0’s shadowing expression is traced with the environment drawn at every step, under both lexical and dynamic scope, with the diverging step marked (or the absence of one explained).
- Part 0 names the error an unbound variable raises and the moment it fires, and includes two probe programs with a prediction under each rule.
definewrites only into the current scope;assignnever creates a binding, andassignto an undefined name raisesLangNameError(notKeyError) with the name in the message.lookupchains through three levels of nesting, andpython3 test_environment.pyreports all five behaviors passing.- The shadowing program prints
51then2, and the outer binding is unchanged after the block. trace.mdshows the environment chain at every step of the trace program, was written before the run, and notes any prediction that missed.- Both theory questions are answered, and question 1 quotes the exact line of the class.
- Your name, and your partner’s if you worked in a pair, is in
trace.md.
Reflection Prompts
- What is the observable difference, in one example program, between a language where assignment creates bindings and one where it only updates them?
- If you worked in a pair, who did what, and name one thing your partner caught that you would have missed. If you worked alone, note that instead.
Submission
In your submission, please include answers to any questions asked on the assignment page, as well as the questions listed below, in your README file.
If you wrote code as part of this assignment, please describe your design, approach, and implementation in a separate document prepared using a word processor or typesetting program such as LaTeX. This document should include specific instructions on how to build and run your code, and a description of each code module or function that you created suitable for re-use by a colleague.
In your README, please include answers to the following questions:
- Describe what you did, how you did it, what challenges you encountered, and how you solved them.
- Please answer any questions found throughout the narrative of this assignment.
- If collaboration with a buddy was permitted, did you work with a buddy on this assignment? If so, who? If not, do you certify that this submission represents your own original work?
- Please identify any and all portions of your submission that were not originally written by you (for example, code originally written by your buddy, or anything taken or adapted from a non-classroom resource). It is always OK to use your textbook and instructor notes; however, you are certifying that any portions not designated as coming from an outside person or source are your own original work.
- Approximately how many hours it took you to finish this assignment (I will not judge you for this at all...I am simply using it to gauge if the assignments are too easy or hard)?
- Your overall impression of the assignment. Did you love it, hate it, or were you neutral? One word answers are fine, but if you have any suggestions for the future let me know.
- Using the grading specifications on this page, discuss briefly the grade you would give yourself and why. Discuss each item in the grading specification.
- Any other concerns that you have. For instance, if you have a bug that you were unable to solve but you made progress, write that here. The more you articulate the problem the more partial credit you will receive (it is fine to leave this blank).
- Please describe any use of outside resources you may have engaged in the completion of this assignment, including the use of generative Artificial Intelligence.
Assignment Rubric
| Description | Pre-Emerging (< 50%) | Beginning (50%) | Progressing (85%) | Proficient (100%) |
|---|---|---|---|---|
| Part 0: Before You Start - Binding and Scope (10%) | Neither the shadowing trace nor the scoping probes are attempted | The let expression is evaluated to an answer but no environment is drawn at any step | The environment is drawn at each step and the dynamic-scope prediction is made, but the divergence step is not identified, or only one distinguishing probe program is written | let x = 2 in let x = x + 1 in x * x is evaluated with the environment drawn at each step, under both lexical and dynamic scope, with the exact step marked where the two disagree; the unbound-variable behavior is decided with a stated error and timing; and two probe programs are written whose output differs by scoping rule, with a prediction for each under each rule |
| The Environment Class (Goals 1, 2) (63%) | The class is missing, or lookup does not consult parent scopes | Lookup chains to parents but define and assign are conflated, so inner assignment creates shadows instead of updating | Define and assign are distinguished and shadowing works, but assignment to an undefined name does not raise a language-level error, or exiting a scope fails to restore the outer binding | define creates in the current scope, assign updates the nearest enclosing binding and raises a positioned language-level name error when no binding exists, lookup chains correctly to any depth, shadowing and scope restoration pass every provided test, and the shadowing program prints 51 then 2 |
| Scope Trace Exercise (Goal 3) (27%) | The trace is missing or contradicts the submitted implementation | The trace predicts final output only, with no per-step environment states | The trace shows environment states per step with one prediction error against the actual run | The trace shows the environment chain at every step, every prediction matches the actual run, and the lexical-vs-dynamic question is answered by pointing to the exact line of the class that decides it |
Please refer to the Style Guide for code quality examples and guidelines.