Turn-in Checklist: For this assignment, there is no paper turn-in. All of your answers should be clearly marked in the charme.py file you modify and submit electronically using https://church.cs.virginia.edu/cs1120/ps7.html. Submit your answers electronically by 12:30pm on Monday, 12 April. If you work with a partner, both partners should submit the same file including both of your email IDs in the authors variable defined at the top of charme.py.

Collaboration Policy - Read Carefully

For this assignment, you make work either alone or with a partner of your choice.

Remember to follow the pledge you read and signed at the beginning of the semester. For this assignment, you may consult any outside resources, including books, papers, web sites and people, you wish except for materials from previous cs1120, cs150, and cs200 courses. You may consult an outside person (e.g., another friend who is a CS major but is not in this class) who is not a member of the course staff, but that person cannot type anything in for you and all work must remain your own. That is, you can ask general questions such as "can you explain recursion to me?" or "how do lists work in Scheme?", but outside sources should never give you specific answers to problem set questions. If you use resources other than the class materials, lectures and course staff, explain what you used in your turn-in.

You are strongly encouraged to take advantage of the scheduled help hours and office hours for this course.

Purpose

Background

Languages are powerful tools for thinking. One way to solve problems is to think of a language in which a solution to the problem can be easily expressed, and then to implement that language. This is the "great Hop forward" that Grace Hopper made in the 1950s: we can produce programs that implement languages. The input to the program is an expression specification in some language. If the program is an interpreter, the result is the value of that expression.

In this problem set, we provide a working interpreter for the Charme language, which is approximately a subset of Scheme. The interpreter implements the Scheme evaluation rules with state. Your assignment involves understanding the interpreter and making some additions and changes to it. If you are successful, you will produce an interpreter for the language Mesmerize in which the original Fibonacci procedure can be applied to 60 to produce the correct result in a reasonable amount of time.

Read Chapter 11 (Interpreters) of the course book.
You may also want to refer to the Python Guide (used for PS6).
Download: Download ps7.zip to your machine and extract the charme.py file into your home directory J:\cs1120\ps7. This file contains a Python implementation of an interpreter for the Scheme-like language Charme. The implementation closely matches the description in Chapter 11.
For the questions in this assignment, you will modify charme.py which you will submit electronically when you are finished. Please mark clearly (using comments) the changes you have made for Questions 3 - 8.

Getting Started With Charme

In the charme.py window, select Run | Run Module (F5) to load the definitions into the interpreter. This file contains all the code from Chapter 11 of the course book.

It defines a procedure meval that takes two inputs: a list that represents a Charme expression and an environment. It outputs the value of the input expression. The initializeGlobalEnvironment() procedure initializes the global environment in the global variable globalEnvironment. We can use the as the second input to meval. You can try some evaluations using meval directly:

>>> initializeGlobalEnvironment()

>> meval('23', globalEnvironment)

23

>>> meval(['+', '1', '2'], globalEnvironment)

3

This is a bit awkward since the first input is not in the standard Charme notation, but the parsed structure.

The parse procedure takes a string representing one or more Charme expressions and produces as output a list containing each input expression as a structured list. For example:

>>> parse("23")

['23']

>>> parse("(+ 1 2 (* 3 4))")

[['+', '1', '2', ['*', '3', '4']]]

>>> parse("(define square (lambda (x) (* x x)))")

[['define', 'square', ['lambda', ['x'], ['*', 'x', 'x']]]]

Since parse takes one or more expressions and produces a list of expressions as its output, we cannot pass the result from parse direction to meval. If we just parse one expression, we just want to pass the first element of the parse output to meval:

>>> meval(parse("(+ 1 2 (* 3 4))")[0], globalEnvironment)

15

It is a bit awkward to remember the [0] and to pass in the globalEnvironment, so we have defined a procedure evalInGlobal(expr) that takes a string representing one Charme expression and evaluates it in the globalEnvironment:

def evalInGlobal(expr):
    return meval(parse(expr)[0], globalEnvironment)
For example:

>>> initializeGlobalEnvironment()

>>> evalInGlobal("(define square (lambda (x) (* x x)))")

>>> evalInGlobal("(square 4)")

16

>>> evalInGlobal("square")

<__main__.Procedure instance at 0x03C0BE18>

We have also provided the evalLoop() procedure that provides an interactions buffer for Charme.

From the perspective of our shiny new interpreter, a Charme program is really just a string that we parse and evaluate.

Question 1: Explain the result of the last evaluation above, evalInGlobal("square"). (Hint: use str to turn this value into a string.)
Question 2: Define a factorial procedure in Charme. Express your procedure as string in Python by defining a variable called charmeFactorialDefinition. When you evaluate evalInGlobal(charmeFactorialDefinition), it should define a Charme procedure called factorial. Note that the Charme interpreter does not support the full Scheme language.

When you've done it correctly, you should see output like this:

>>> initializeGlobalEnvironment()

>>> evalInGlobal(charmeFactorialDefinition)

>>> evalInGlobal("(factorial 5)")

120

Hint: Remember that Charme does not support the define shortcut for quickly listing function and their arguments. You must write your answer out longhand with lambda.

  • Right: charmeDoubleDefinition = "(define double (lambda (x) (+ x x)))"
  • Wrong: charmeDoubleDefinition = "(define (double x) (+ x x))"

Adding Primitives

The set of primitives provided by our Charme interpreter is sufficient, in that it is enough to express every computation. However, it is not enough to express every computation in a convenient way.
Question 3: Extend the Charme interpreter by adding a primitive procedure <= to the global environment. You will need to define a procedure that implements the primitive, and modify initializeGlobalEnvironment to install your primitive.

>>> initializeGlobalEnvironment()

>>> evalInGlobal("(<= 5 3)")

False

>>> evalInGlobal("(<= 3 7)")

True

Our Charme interpreter does not provide any primitives for lists. As we saw in Chapter 5, it is possible to define cons, car and cdr using only the language already defined by Charme. However, it would be more convenient if some primitives for manipulating cons cells and lists are provided.
Question 4: Extend the Charme interpreter by adding primitive procedures cons, car and cdr that behave similarly to the primitive Scheme procedures.

You should start by defining a class that represents a cons cell. For example, you might define a Cons class that has a constructor (__init__) that takes two inputs (the first and second parts of the pair), and provides methods for getFirst and getSecond that retrieve the respective parts of the pair.

You must also define a __str__(self): method for your class so that evalLoop and evalToplevelExp will print out Cons sells similarly to how they are displayed in Scheme.

You should get the following interactions in the evalLoop():

Charme> (cons 1 2)

(1 . 2)

Charme> (car (cons 1 2))

1

Charme> (cdr (cons 1 2))

2

Charme> quit

Or the equivalent ones with evalInGlobal:

>> evalInGlobal("(cons 1 2)")

(1 . 2)

Question 5: Extend the Charme interpreter by defining the null and null? primitives. (Note that names in Python cannot include question marks, so you will have to use a different name for the Python procedure you use to implement null?.)

Hint: You could use Python's None value to represent null.

Question 6: Extend the Charme interpreter by defining the list primitive procedure. Like the Scheme list primitive procedure, it should take any number of operands and produce as output a list containing each operand as an element in order.

After finishing these questions, you should get the following interactions in the evalLoop():

Charme> (define a (list 1 2 3 4))

Charme> (car a)

1

Charme> (null? a)

False

Charme> (cdr (cdr a))

(3 4)

Note that this is the way Scheme prints out lists, and you are encouraged to make your Charme interpreter also print out lists this way. It is acceptable, though, to print out lists in a more straightforward way, so they would be displayed as normal cons pairs. Then, this would look like, (3 . (4 . None)), instead.

Charme> (null? (list ))

True

Special Forms

The provided Charme interpreter does not include the cond special form. Before attempting to answer the next question, we recommend carefully understanding the evaluation rule for the conditional expression (as described in Section 10.1.2 of the course book).
Question 7: Extend the Charme interpreter to support the cond special form, with the same meaning as the Scheme cond expression. (Your Charme cond expression does not need to support the special else syntax supported by Scheme.)
After adding cond to your Charme interpreter, you should get the following interactions using evalLoop():

Charme> (cond)

None

Charme> (cond ((> 1 2) 2))

None

Charme> (cond ((> 1 2) 2) ((> 3 2) 3))

3

Charme> (define fibo (lambda (n) (cond ((= n 1) 1) ((= n 2) 1) (true (+ (fibo (- n 1)) (fibo (- n 2)))))))

None

Charme> (fibo 10)

55

Memoizing

Memoizing is a technique for improving efficiency by storing and reusing the results of previous procedure applications. If a procedure has no side effects and uses no global variables, everytime it is evaluated on the same operands it produces the same result.

To implement memoizing, we need to save the results of all procedure applications in a table. When a procedure application is evaluated, first, we lookup the application in the table to see if it has already been computed. If there is a known value, that is the result of the evaluation and no further computation need be done. If there is not, then the procedure application is evaluated, the result is stored in the table, and the result is returned as the value.

Question 8: Modify the Charme interpreter to support memoizing for all procedure applications.

(Hint: review your class notes for non-memoized procedure applications.)

(Hint 2: about five lines should do it. Consider adding an if statement far before the call to meval in mapply.)

Once you have modified the interpreter, you should be able to evaluate (fibo 60) without changing the definition of fibo above. By changing the meaning of the application evaluation rule, a procedure that previously had running time exponential in the value of the input, now has running time that is linear in the value of the input!

Question 9: Fill in the missing text for the variables defined for Question 9 with your commands:
  1. pleased = """ 
    What are you most pleased about regarding this course?
    """
  2. displeased = """ 
    What are you most displeased about regarding this course?
    """
  3. hopetolearn = """
    What did you most hope to learn in this course that we have not yet
    covered?
    """
    
As usual, we appreciate your comments on any subject, but especially any concrete suggestions for improving the class and for making the rest of the class as worthwhile as possible.
Turn-in Reminder: Submit your charme.py file containing all of your answers using https://church.cs.virginia.edu/cs1120/ps7.html.