Session 14

An Application of Recursion: A Small Interpreter


CS 3540
Programming Languages and Paradigms


Opening Exercise

List the unused variables in these little language expressions:

    (f x)                      (lambda (x)
                                 y)

    (lambda (x)                ((lambda (x) x)
      (f x))                    (lambda (x) y))
            
                               (a
                                (lambda (z)
    (lambda (x)                   (lambda (y)
      (lambda (x)                   (lambda (x)
        x))                           x))))

Try writing a function named (unused-vars exp) to solve this problem for you. It may help you understand the idea of used and unused variables. It is also good practice for Quiz 2.

These are also fine test cases for Problem 4 on Homework 6!



Google tells dad jokes
Pooh thinks recursion is tasty.

Yesterday, Today, Tomorrow

Where We've Been

For the last few weeks, we have been discussing different techniques for writing recursive programs, all based on the fundamental technique of structural recursion. Last time, we applied these techniques in writing a recursive program to answer this question about programs in our little language from Session 12: Does a particular variable occur bound in a given piece of code? Our program, (occurs-bound? var exp), was mutually recursive with the function occurs-free? because the definitions of bound and free occurrences are mutually inductive.

In order to think and write more clearly about the little language, we used a new design pattern, Syntax Procedures, which allowed us to focus on the meaning of our data rather than their implementation in Racket.

Where We're Going

The next two units of the course explore important concepts in the design of programming languages, syntactic abstraction and data abstraction, by adding features to our little language and writing Racket code to process them. But our little language is so simple that it can be easy to lose track of where we are heading: the ability to write an interpreter for a programming language that actually does something.

Where We Are

Today, we use some of the ideas we have learned about Racket and recursive programming to implement an interpreter for a small language that actually does something, however simple. This trip has three goals: First, along the way, we'll see how we can begin to use the techniques we've been learning to write larger programs. Second, we'll see ways in which the things we will learn over the next few weeks fit into a language interpreter. Finally, we'll even take a few short breaks to have you write functions of your own, as practice.



The Cipher Language

Cipher is a simple language for encoding text.

    $ racket
    Welcome to Racket v8.5 [cs].
    > (require "cipher-v1.rkt")
    > (value '(rot13 "Hello, Eugene"))
    "Uryyb, Rhtrar"

The rot13 operator implements a simple substitution cipher that replaces each letter with the 13th letter after it in the alphabet (mod 26):

a graphical demonstration of letter swaps in ROT-13

The grammar for the Cipher language:

    exp ::= string
          | ( unary-op exp )          ; unary
          | ( exp binary-op exp )     ; binary
          | ( exp mixed-op number )   ; mixed
All values are strings. Numbers are literals in programs.

The operators:

More examples:

    > (value '("Hello, " + "Eugene"))
    "Hello, Eugene"

    > (value '("Eugene" take 3))
    "Eug"

    > (value '("Eugene" drop 4))
    "ne"

I considered including a (exp in? exp) expression, but then we'd need boolean values, too. Let's keep things simpler for a one-day excursion.

My first job: implement syntax procedures. Look at the top of the file, including (exp?) and maybe (unary?).



Quick Exercise

Write a structurally recursive function named mixed?. This function takes one argument, which can be any Racket value. It returns true if that value is a mixed Cipher expression, and false otherwise.

For example:

     > (mixed? '("Hello, Eugene" take 3))
     #t
     > (mixed? '(("Hello" drop 1) take 3))
     #t

     > (mixed? "Hello")
     #f
     > (mixed? '("Hello, Eugene" drop "3"))
     #f
     > (mixed? '("Hello, Eugene" slice 3))
     #f
     > (mixed? '("Hello" drop 3 "Eugene"))
     #f

You may assume that I have already implemented the general type predicate exp?.



Interpreter, Version 1.1

Take quick look at the rest of the syntax procedures:

   - type predicates
   - accessors
   - constructors
   - these functions have
     NO KNOWLEDGE OF THE MEANING
     of the language

Implement (value exp):

   - two options: inline code and helper functions
     why helpers?  the code is already complex, may get bigger

   - a compound example such as
        (("abc" drop 2) + ("abc" take 2))
     reminds us that we 
     MUST EVALUATE ANY PARTS THAT ARE exp


Quick Exercise

Implement one of the helper functions for value:

        $ (eval-mixed 'take "Eugene" 3)
        "Eug"
        $ (eval-mixed 'drop "Eugene" 3)
        "ene"
You will want to use (substring string start end), and probably (string-length string).



Interpreter, Version 1.2

Look more at value and its helpers:

    - I could implement the one-op functions without a cond, but
      - This makes the code consistent: take/drop.
      - This makes the code easier to extend.

    - There are many options for implementing the helper functions:
      - inline               (what I did here)
      - with more focused helper functions
      - with an assoc list   (SAVE THIS FOR LATER)

    - implement rot13 with a look-up table, or math?

When I want to test and play more, having to call value and quote the Cipher becomes annoying. So I implemented a REPL for Cipher, based on a Session 2 reading:

    - I used displayln, for the formatting.
    - But now I want a PROMPT!
    - And a way to EXIT gracefully!

[--- do the rest in a separate file ---].



Interpreter, Version 2.1

When doing this sort of string manipulation, we often want to shift strings like this:

    (("abc" drop 2) + ("abc" take 2))

It would be cool to be able to write a Cipher expression (exp shift num) to do this. Perhaps we could add a primitive function to Racket, or let users write their own Cipher function.

We have seen the idea of global function (defined or imported):

    - Racket user-def functions
    - Racket required functions
    - *Racket primitives*

But how would the evaluator handle a function, or a function definition? We would have to extend the language, and the interpreter, in several ways. That's too much for a one-day excursion.

Another idea... Our language processor could translate a shift expression into a the equivalent primitive Cipher expression, and then use existing machinery to evaluate it. That would save a lot of effort.

This is a powerful idea. We study it in detail in the next unit of the course.

Instead, let's add a simpler extension: names for primitive values, such as FIRST = "eugene" and LAST = "wallingford".

To do this, we have to add variable references to grammar and update the other syntax procedures to work with them. We also need the ability to look up the value associated with a name. That, you can do...



Quick Exercise

Write a structurally recursive function (lookup sym lop), where lop is a list of symbol/value pairs.

        $ (lookup 'w '((e . "Eugene") (w . "Wallingford")))
        "Wallingford"
        $ (lookup 's '((e . "Eugene") (w . "Wallingford")))
        [error]

If you'd like to see recursive solution, check out this file.



Interpreter, Version 2.2

We did not cover this sesstion in class. Feel free to read it and the code, if you'd like. No worries, though: nothing here will be on the quiz.

To add variable references to Cipher, we need a new idea for our interpreter: the idea of an environment. An environment is a data structure that associates names with values. We then use a lookup function to find a value given a name.

Implementation: a list of pairs. I also use a new-to-us primitive Racket function: assoc.

We have to make some changes to the value function:

    - add new case
    - pass env on all recursive calls
    - change signature (*)
    * have to make value an interface procedure to value-aux!
    - now, can have value() validate the exp first

Cipher now supports variable bindings.

    - Here, we use them for global variables.
    - We can use the same mechanism for local variables!
      (also next unit -- finally!)
    - We can *also* use this same mechanism when we implement
      function calls!!!
      (also next unit: we make the connection)

What would it take to add a (def var = exp in exp) expression to Cipher?



Wrap Up



Eugene Wallingford ..... wallingf@cs.uni.edu ..... March 2, 2023