MuLE Static versus Dynamic Scoping Lab

The SLam Interpreter.

 

Assumptions

Knowledge DrScheme and the MuLE environment.

 

Objective

An understanding of how different results may occur when the only change in the language is whether it is statically or dynamically scoped. An understanding of how the SLam environment is implemented.

 

Overview of SLam and More Generally Functional or Applicative Languages

SLam is an example of a language from the functional paradigm. Functional languages include LISP, Scheme, ML (although some would argue that its typing makes it a hybrid language) and possibly APL (because of its functional forms) and are modeled on mathematical functions. "The execution of an expression is the major concept needed for program sequencing rather than statements of languages like C, Pascal, or Ada. Since they are applicative (or functional), heap storage and list structures become the natural process for storage management rather than the traditional activation record mechanism in procedural languages." Like real functional languages, SLam has a simple syntactic structure, but is not purely functional as is does allow variable assignments.

 

Scoping Background and Static versus Dynamic Scoping Explanation

Language scoping rules determine how an occurrence of a name is associated or bound to a variable. This determination is important only when the references to the variables are outside of the executing block. There are two methods of determining nonlocal access, static scoping and dynamic scoping. The concept of dynamic verses static scoping is concerned which set of rules to apply when finding a binding to a variable and/or changing a value bound to a variable. Additional complications arise depending on the parameter passing mechanism in use. For the current discussion, we assume the pass-by-value method.

In either scoping mechanism, the search for a variable begins in the local environment, that is, in the currently executing program, block, procedure or function. In the simple local variable case, static and dynamic scoping are identical, the local binding is used and/or modified. It is in the nonlocal variable case that the variable must be found in some external environment. Which set of scoping rules followed determines the exact environment or order of environments searched.

Dynamic scoping rules depend on the calling sequence of the (sub)programs and requires searching the executing subprogram’s dynamic parent, i.e., the calling environment first. Whereas, static scoping rules depend only on the static structure of the program and therefore requires searching the executing subprogram’s static parent, i.e., the defining environment first. Although dynamic scoping is not commonly used in recent languages, APL, SNOBOL4 and early versions of LISP employ dynamic scoping. The real motivation, however, in changing the scoping mechanism in an interpreter is we gain deeper insight into how scoping is implemented.

 

Environments in MuLE

The interpreters used in MuLE handle the environment, or name space, in the same manner. Every declared variable is stored in a list as a name-value pair. In SLam, variables are declared implicitly when bound to a value. Whereas in SPoc, variables must be declared explicitly before use. In either case, the first time the interpreter encounters a variable, its name is placed into a list with a value (either the default value 0, as in SPoc or the value in the assignment, as in SLam). This list, containing the name-value pair, is placed into the list holding all program bindings.

For example, if the first variable binding encountered in a SLam program was the assignment

(assign x 5)

the environment would be ( (x 5) ). The variable binding is the list (x 5) while the environment is the list containing that list. If the next binding was the assignment

(assign y 7)

then the environment becomes ( (x 5) (y 7) ). The global environment is treated as a queue with its top on the left of the list. As new global variable declarations are encountered, they are added onto the back of the queue (i.e., the right end of the list).

This environment is global because any definition is visible to any statement in the program. Both SLam and SPoc allow local environments to be defined in addition to the global environment. Because procedural or imperative languages are block-structured it is natural that SPoc allows blocks, i.e., segments of code that include local definitions. SPoc’s blocks can be nested. SLam includes procedures, i.e., segments of code that can be called from other places in the program. These two mechanisms both allow for nonlocal declarations, procedures highlight the differences between scoping rules. Following the dynamic chain to resolve a variable binding may be very different from following the static chain.

To simplify SLam’s environment (and make our task easier) procedures cannot be nested. The programming language, C, is restricted in this way. The gain is every procedure’s static scope is identical, i.e., every procedure is defined in the global environment and thus, has the same static parent.

Like variable names, names of procedures must also be entered into the environment. In SLam, a procedure is similar to any other binding except that its value is the parse tree of its code. This lab does not require entering procedures into the environment, but does require recognizing procedure bindings. Because procedures cannot be nested, every procedure binding is entered into the global environment.

When procedures are called, a new binding type needs to be created. When a procedure is called, any variables declared will take precedence over global bindings. In other words, a local environment is created. Therefore, the procedure’s local environment must be checked first for variables referenced within the procedure.

SLam’s local environment, like its global environment, is a list of variable name-value pairs. A local environment is created when a procedure is called. The procedure’s parameters are the first name-value pairs entered into the environment. (Recall, for simplicity in this lab, all parameters are pass-by-value. The "copy in" semantics calls for the actual argument’s value to be copied to the formal parameter, which in effect is a local variable that is bound at the time of the procedure call.) Local environments are added to the front of the queue when the procedure is called. (In this way we can view the added environments as a stack of environments, much like adding activation records to the run-time stack.) This will make scoping easier. Variables within the local environment are added to the back of the queue. So when adding a variable we view the list as a queue, but when adding an environment we view the list as a stack. (The example below should clear this up if it doesn't make sense.)

In summary, there are two types of entries in the environment to consider in SLam, global variable bindings (regular variables or procedure names) and local environment created by procedure calls. As an example, consider the global environment defined earlier,

( (x 5) (y 7) )

and assume that a procedure p1 is defined with a single parameter x. The environment becomes

( (x 5) (y 7) (p1 #def-1) )

where #def-1 is used to stand for the parse tree for p1. Further assume some variable z is defined with the code (assign z 12) and then p1 is called with the call (p1 y) where the function p1 has one parameter know to the function as x. At this point the environment would be

( ( (x 7) ) (x 5) (y 7) (p1 #def-1) (z 12 ) )

The new local environment (simply a list that contains the binding of its parameter x) created by the procedure call is entered into the global environment. If procedure p1 contained the line (assign y 30), the environment would become

( ( (x 7) (y 30) ) (x 5) (y 7) (p1 #def-1) (z 12 ) )

while the line (assign y (+ z x) ) would change the environment into

( ( (x 7) (y 19) ) (x 5) (y 7) (p1 #def-1) (z 12 ) )

 

Overview of this Laboratory Assignment

Currently dynamic scoping rules are implemented in SLam. This lab requires you to implement static scoping rules in SLam instead. This will neither affect the parsing of a SLam program or the execution of SLam programs. However, this modification will affect how variables are looked up and changed in the environment.

Begin by copying the SLam Scoping directory from the Hobbled Mule directory. You can load
launcher.s and run Mule with the sample .slam programs provided. You will see that dynamic scoping has been implemented. (To help you, there are comments in each sample program detailing what each program produces depending on the scoping. You should work through the sample programs and convince yourself the comments are correct.) To convert the code to implement static scoping, follow the following steps:
  1. Make a copy of dynamic.slam and call it static.slam.

  2. Modify slam.s to load static.slam instead of dynamic.slam. (Change the call (load-relative "dynamic.slam") to the call (load-relative "static.slam").) This is the only change that you are to make to slam.s.

  3. (optional -- depends on your hobbled version.) Complete two functions get-global and get-all-local located in static.slam as described in The Details below. The functions will be in slam.s if you do not need to complete them.

  4. Modify get-val and enter-env as described in The Details below. Once you make the change to get-val, you can use sample SLam programs staticvdynamic.slam, staticvdynamic1.slam, staticvdynamic2.slam, and staticvdynamic3.slam to test it. Once you make the change to enter-env, you can use sample SLam programs staticvdynamic4.slam and staticvdynamic5.slam to test it.

    To recap, there are two subprograms used to manipulate the environment. The first function, get-val, is used to look up variables in the environment to find their corresponding value. This function is found in the file dynamic_slam.s which is included in slam.s through the call (load-relative "dynamic.slam").

    The second procedure is enter-env either enters a new value into an environment or, if the variable is already in the environment, changes its binding. This subprogram is currently in the file dynamic.slam (or the newly created static.slam ) and it currently implements dynamic scoping.

 

The Details

Optional step 3 requires you complete two functions that you will need to manipulate the environments, i.e., the nested lists as described on the previous pages. What is nice about this step, is that these functions can "stand alone." That is, you can execute static.slam and test the functions independently. See that last page of this handout for some sample tests. The function get-global returns the global environment as a list of variable-value pairs. The function get-all-local returns a list of all of the local environments.

Step 4 requires you to modify the two subprograms used to manipulate the environment. The first function, get-val, is used to look up variables in the environment to find their corresponding value. The second function is enter-env either enters a new value into an environment or, if the variable is already in the environment, changes its binding. In each of these functions, you will change only one line of code. The description below is provided to assist you in understanding the functions and locating that one line. As you would expect, these two changes will use the functions that you completed in Step 3.

The get-val function

The function get-val is passed two parameters, the look-up name, and the environment to look in. The function currently implements dynamic scoping, and the following description describes how it does that. First, the function determines whether the first item in the environment is a list (i.e., a local environment) or not.

If the first item is a list, that environment must be recursively searched. If the name we are searching for is in that environment, we are finished. Otherwise, we must continue to search the environment after popping the first item (the local environment that we just searched) off the environment stack. How this search proceeds is the difference between implementing static scoping versus dynamic scoping. If the next item is not a list, then no change is needed as static rules and dynamic rules would produce the same result. But if the next item on the stack is a list which would represent a local environment (due to a procedure call) then changes to the implementation must be made. Local environments are entered in the order in which the corresponding procedures are called, and the next local environment represents the procedure that called the procedure whose environment was just searched. If dynamic scoping rules were to continue to be followed, this is would be the correct environment to search next. Since static scoping rule implementation is the goal, this must change. The global environment must be searched instead because in following static scoping rules, the static (declaring) parent must be searched for a name rather than the dynamic (calling) parent. Since we have the restriction that procedures cannot be nested, the static (declaring) parent is in fact the global environment.

If the first item in the environment being searched is not a list as described in the previous paragraph, then it must be a variable-value binding. In this case, it checks this first item for a match on the search name. If the first item matches the name being searched for, the function is finished. Otherwise, the function continues recursively searching the remainder of the list. The recursion grounds out when the list is empty, signifying that the variable was not found and '() is returned.

The enter-env function

The overall structure of this function is similar to the structure of get-val. The function receives two parameters, a name-value pair and an environment. There are several helper functions, but the principal function is change-val. This function receives the old environment and returns a new environment with either the name-value pair replacing the value of the old name-value pair (if the name was already in the environment) or the name-value pair added (if the name was not in the environment).

The function change-val recursively searches through the environment, taking the environment list apart one element at a time. If the end of the environment is reached and the name has not been found, then the name-value pair is returned (which results in it being added to the end of the environment once the recursion has unwound). If the environment is not empty and the name is not in the first item in the list, then change-val concatenates the first item with the result of a recursive call that passes the environment with the first item removed. In this manner, the environment list is taken apart one item at a time and then put back together one item at a time.

As in the procedure get-val, there only two possible scenarios, either the first item in the environment is a list of lists (i.e., a local environment) or a name-value pair. First, the function change-val must determine if the list is a local environment. If the first item in the environment is itself a list, then it is a local environment and change-val calls another locally defined function, checkLocal (described below).

If the first item is a local environment, then checkLocal is sent the local environment and returns #t or #f, depending on whether the name was found in the local environment. If it was, then changeLocal is called to change the value. If not, then change-val must continue the search. In the current form of change-val, which uses dynamic scoping, change-val merely begins searching at the next item in the list. It does this by considering the first item in the list (the local environment that was just searched) with a recursive call, passing the environment with the first item removed as the parameter.

Static scoping, however, cannot simply remove the local environment and recurse. Since the only blocks or local environments in SLam are procedures, a variable that is not defined in a procedure must have been defined in global environment according to static scoping rules. Thus, change-val must, at this point, skip over all of the local environments and search the global environment to implement static scoping correctly.

Sample programs for testing

Under dynamic scoping rules, the function p1 returns 30 and the function p2 returns 9 in the SLam program named staticvdynamic.slam, whereas under static scoping rules, both p1 and p2 return 30.

staticvdynamic.slam:
(assign y 10)
(assign p1 (proc (x) (* x y)))
(assign p2 (proc (y) (p1 y)))
(p1 3)
(p2 3)

 

Under either static or dynamic scoping rules, the function p1 returns 8 and the function p2 returns 20 in the SLam program named staticvdynamic1.slam.

staticvdynamic1.slam:
(assign x 5)
(assign y 10)
(assign z (+ x y))
(assign x 2)
(assign p1 (proc (a) (* a x)))
(assign p2 (proc (b) (p1 b)))
(p1 4)
list
(p2 10)
list

 

staticvdynamic2.slam:
(assign y 10)
(assign x 0)
(assign p1 (proc (x) (* x y)))
(assign p2 (proc (y) (p1 y)))
(p1 3)
(p2 3)
(assign x (p2 3))

// Static:
// p1 returns 30, p2 returns 30, assign returns 30

// Dynamic:
// p1 returns 30,p2 returns 9, assign returns 9

staticvdynamic3.slam:
(assign x 5)
(assign y 10)
(assign z (+ x y))
(assign x 2)
(assign p1 (proc (a) (* a x)))
(assign p2 (proc (x) (p1 x)))
(p1 4)
list
(p2 10)
list
// Static: p1 returns 8, p2 returns 20
// Dynamic: p1 returns 8, p2 returns 100

staticvdynamic4.slam:
(assign y 10)
(assign x 0)
(assign z 7)
(assign a 5)
(assign p1 (proc (z) (assign y a)))
(assign p2 (proc (y) (p1 y)))
(assign p3 (proc (a) (p2 a)))
(p3 z)
list

// Static: y ends with the value 5
// Dynamic: y ends with the value 10

staticvdynamic5.slam:
clear
(assign x 5)
(assign y 10)
(assign z (+ x y))
(assign x 2)
(assign p1 (proc (a) (assign x (* a x))))
(assign p2 (proc (x) (p1 x)))
(p1 4)
list
(p2 10)
list
// Static: p1 returns 8 and the value of x
// becomes 8 in the first list
// p2 returns 80, x has value 80
// in the final list
// Dynamic: p1 returns 8, p2 returns 100,
// x has value 8 in the final list

Sample tests for Optional Step 3


> (get-all-local '((x 0) (y 0)))
()
> (get-all-local '(((x 1) (y 1)) (x 0) (y 0)))
(((x 1) (y 1)))
> (get-all-local '(((x 1) (y 1)) ((a 5) (b 5)) (x 0) (y 0)))
(((x 1) (y 1)) ((a 5) (b 5)))
> (get-all-local '(((x 1) (y 1)) ((a 5) (b 5)) ((r 2) (s 2)) (x 0) (y 0)))
(((x 1) (y 1)) ((a 5) (b 5)) ((r 2) (s 2)))
> (get-global '())
()
> (get-global '(()))
()
> (get-global '((x 0) (y 0)))
((x 0) (y 0))
> (get-global '(((x 1) (y 1)) (x 0) (y 0)))
((x 0) (y 0))
> (get-global '(((x 1) (y 1)) ((a 5) (b 5)) (x 0) (y 0)))
((x 0) (y 0))
> (get-global '(((x 1) (y 1)) ((a 5) (b 5)) ((r 2) (s 2)) (x 0) (y 0)))
((x 0) (y 0))
>

What to Turn in

The faculty member using this lab should fill in the details for project submission and perhaps grading details.