RU beehive logo ITEC dept promo banner
ITEC 380
2008fall
ibarland

homeinfolecturesexamsarchive

hw06
Writing a Simple Interpreter

update:
  1. If you turn in *all* of hw06 on Friday Dec.12, you'll get a 15% bonus to your score.
  2. Otherwise, you can turn in part on Friday Dec.12, and the whole project on the last day of Finals, Dec.18 (Thu) 17:00.

    The part due Dec.12 is: all of L1 (parse, toString, eval), plus parse and toString for L2.

    To turn in For turning in during finals week, just slip it under my door. (I prefer *not* being emailed solutions, but if you must, then create a single readable file (plain-text or pdf) and send that to me [*not* a zip with many source files]. You don't need to print out any code which is was provided to you (e.g. UtilIan), if you didn't modify it.)

For this project, we'll implement a “tower” of languages (L0, L1, L2, L3, and L4), each language incorporating more features than the previous. L0 is provided for you.

% L0 syntax:
% An Expr is one of:
Expr ::= Num | SumExpr | ProdExpr | IfZeroExpr 

SumExpr ::= {plus Expr Expr}
ProdExpr ::= {times Expr Expr}
IfZeroExpr ::= {ifZero Expr Expr Expr}
A Num can be any conventional1 decimal numeral (and will be interpreted as a number in the standard2 way).
Here is a Scheme interpreter for L0 and hw06-L0-test.ss; helper functions for running the tests are in hw06-test-helpers.ss ( improved detection of certain errors, Wed. 19:30).
Here is a java interpreter for L0 (L0.jar or browse); it includes unit tests.
  1. (20pts) L1 is just like L0, but with two additional types of expressions:

    Expr ::=  | ModExpr | IfNegExpr
    
    ModExpr ::= {mod Expr Expr}
    IfNegExpr ::= {ifNeg Expr Expr Expr}
    
    Update parse, toString (a.k.a. expr->string), and eval appropriately.

    The only method which cares what these new expressions mean (their semantics) is eval: {ifNeg Expr0 Expr1 Expr2} first evaluates just Expr0; if that value is negative, then it evaluates Expr1 and returns its value; otherwise it evalutaes Expr2 and returns its value. (Note how you are making sure that ifNeg short-circuits.)

    {mod a b} evaluates to3 a mod b, where the result is always between 0 (inclusive) and b (exclusive); in particular, the result is never positive if b<0. Notice that this is slightly different behavior than either Java's built-in % (which behaves differently on negative numbers), and from Scheme's built-in modulo (which only accepts integers). In Java, you can use a%b (if b>0 or a%b=0) and a%b+b (otherwise). In scheme or Java, you can use b*(a/b-floor(a/b))

    Hint: Most all the code for this problem is virtually identical to existing code. The only exception (and it's a mild one) is the eval for ModExprs, since the formula is a bit more involved.

    hw06-L1-test.ss

  2. (40pts) L2 adds identifiers to L1:

    Expr ::=  | Id | WithExpr
    
    WithExpr ::= {with Id Expr Expr}
    
    where Id can be any series of letters and digits which isn't interpretable as a number4.

    Update your three methods parse, toString (a.k.a. expr->string), eval. We now need to define the semantics of {with Id E0 E1}:

    The code to make a substitution in an Expr parse-tree is reminiscent of hw03's subst-in-tree (except that a list had only two cond-branches, while Expr has around ten (most of which are similar)). For example: {with x 5 {plus x 3}} ⇒ {plus 5 3} ⇒ 87.

    However, there is one wrinkle: What if E1 contains some non-free (“bound”) occurrences of Id?
    That is, wat if E1 contains a (nested) “{with x }”? In that case the inner one should shadow the outer one:
    {with x 3 {with x 5 {plus x 3}}} ⇒ {with x 5 {plus x 3}} ⇒ {plus 5 3} ⇒ 8
    Thus, when substituting, only substitute “free occurrences” of an Id in E1, not any “bound occurrences”8. Think through exactly what you need to substitute, in the following examples:
    {with y 3 {with x 5 {plus x y}}} ⇒                                  ⇒ 8
    {with y 3 {with x y {plus x y}}} ⇒                                  ⇒ 6
    {with x 5 {with y 3 {plus {with x y {plus x y}} x}}}                                                                                                     ⇒ 11
    (You might find it helpful to draw the parse tree, rather than staring at the flat string, especially for this last example.)

    Observe that when evaluating a (legal) L2 program, eval will never actually encounter an Id -- that Id will have been substituted out before we ever recur down to it.

    hw06-L2-test.ss

  3. (40pts) L3 adds functions and function-application to our language:

    Expr ::=  | FuncExpr | FuncApplyExpr
    
    FuncExpr ::= {fun Id Expr}
    FuncApplyExpr ::= {Expr Expr}
    
    We restrict ourselves to unary functions (functions of one argument). (To think about: if we wanted to 'fake' functions of 2 arguments in L3, how could we do it?

    Just like numbers are self-evaluating, so are FuncExprs. If evaluating (an internal representation of) a FuncExpr, just return that same (internal representation of) function. We won't actually evaluate the body until the function is applied.

    In FuncApplyExpr, the first Expr had better evaluate to a function. (That is, it's either a FuncExpr, or it's an Id which gets substitued to a function value.) For example, here is a function for computing absolute value:

    {fun x {ifNeg x {times -1 x} x}}
    

    A funcApplyExpr represents calling a function. Here are two examples, both computing the absolute value of -5:

    {{fun x {ifNeg x {times -1 x} x}} -5}
    
    {with abs {fun x {ifNeg x {times -1 x} x}}
          {abs -5}}
    
    To evaluate a function-application {Expr0 Expr1}: Hey, those semantics are practically the same as with!

    Observe that our programs can now evaluate to either of two types: numbers or functions. In Java, we'll need to have a class which can represent either, as the return type for our eval.

    hw06-L3-test.ss

  4. (35pts extra credit: This problem and the next are really the crux of the assignment, but due to the impending end-of-semester, it will be extra credit.)
    Deferred evaluation: L4 doesn't add any new syntax, but it is a different evaluation model which will give us more expressive semantics. Use a new file/project for L4, separate from L0-L3.

    There are two problems9 with the substitution approach used above: we can't make recursive functions, and it doesn't generalize to assigning to variables. We solve these problems with deferred evaluation:

    Note that this gives us dynamic scoping.

  5. (35pts) Extra credit: static scope. Extend functions so that they include one extra piece of information: the bindings in effect when the function was declared. Be sure to make a careful suite of test-cases, to test for various ways of capturing bindings in different closures. (For example, consider the midterm's problem for testing scope.)
  6. Further extra-credit options (of varying difficult, hence points):

1 To keep from getting bogged down in tokenizing input, Java implementations can use exactly those strings recognized by java.util.Scanner.nextDouble() (including NaN) and Scheme implementations can use exactly those strings recognized by number?, (including +nan.0, -inf.0, and 2+3i).      

2 For ease of implementation, we'll allow Java implementations to use floating-point arithmetic (doubles) instead of actual arithmetic.      

3 Because we don't need to check for bad inputs, it's fine to have your interpreter crash if b=0.      

4 Note that our different implementations are now varying by more than just precision of arithmetic: in a Java implementation, NaN is a Num, and in a scheme implementation it's an Id. We won't use any test cases involving such subtle differences. However, note how our choices in designing a new language are being influenced by the language we're trying to easily implement it in! This stems from the fact that a primary design constraint on L1 is that implementing an intepreter for L1 doesn't get bogged down in minutae when using either Java or Scheme.      

6 Okay -- if you implement the union data type of Exprs by using the Strategy pattern, then you can regain the simplicity of the functional approach in this case.      

5In particular, even if using Java, you'll want do the substitution using a functional approach: have a method which returns a new parse tree with the substitutions, rather than mutating the existing tree. The mutation approach leads to quite unwieldy code. (Try it, if you don't believe me 6      

7 The notation “{with x 5 {plus x 3}} ⇒ {plus 5 3} ⇒ 8” is shorthand for

  eval(parse("{with x 5 {plus x 3}}"))
= eval(parse("{plus 5 3}"))
= 8
Observe how we definitely don't write “"{with x 5 {plus x 3}}" = "{plus 5 3}" = 8” since strings are not equal to ints. More specifically: we distinguish between “” (“code evaluates to”) and “=” (“equals”, just as “=” has meant since kindergarten).      

8 nor any “binding occurrences”: The first x in {with x 5 {plus x 3}} is a binding occurrence, and the other x is a bound occurrence. (The variable is bound inside the scope of its binding occurrence.)      

9 A third issue, pointed out in Programming Languages and Interpretation, is that evaluating deeply nested withs is an O(n2) algorithm.      

10 Note that the list you recur with has the additional binding, but that recursive call shouldn't end up with any net change to your bindings. If you use lists which require mutation (viz java.util.List), you must either make duplicates before recurring, or be sure to remove your recently-added binding after your recursive call.      

homeinfolecturesexamsarchive


©2008, Ian Barland, Radford University
Last modified 2008.Dec.10 (Wed)
Please mail any suggestions
(incl. typos, broken links)
to iba�rlandrad�ford.edu
Powered by PLT Scheme