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

homelecturesrecipeexamshwsD2Lbreeze (snow day; distance)

S2
Interpreting S
project

Deliverables:

Due 2017-Nov-27 (Mon.) 23:59.
I strongly recommended downloading S0 and getting it running, and then completing the required test cases, by Nov-17 (Fri), and having the homework basically completed by class on Nov.27.
Note that S1 is “add code that is extremely similar to what already exists”, but S2 requires a solid understanding of what the parse-tree representation and how the functions work.

Over the course of several homeworks, we'll implement a “tower” of languages (S0, S1, …, S6) each language incorporating more features than the previous. S0 is provided for you.


  1. (15pts) Implement S1 in both racket, and Java. S1 is just like S0, but with two additional types of expressions:

    Expr ::=  | Ifgt
    Ifgt ::=  (Expr >? Expr) = Expr : Expr    Interpretation: “If greater-than”
    
    Op ::=  | mo     Interpretation: “mod”
    
    Update parse, toString (a.k.a. expr->string), and eval appropriately, for both the racket and Java implementations.

    Be sure to write test cases first; To ensure everybody makes test cases to cover the basics, I've spelled out these S2: initial tests.

    The only method which cares what these new expressions mean (their semantics) is eval:

    Complete two versions of S1: both racket, and java. (For S2 and beyond, you can choose which implementation to continue.)

  2. (25pts) Implement S2 in either racket or Java (your choice). S2 adds identifiers to S1:

    Expr ::=  | Id | LetExpr
    
    LetExpr ::= let Id Expr in Expr2
    
    where Id can be any series of letters and digits which isn't interpretable as a number3. (Assume for now that any nested letExpr expressions use different Ids. We'll handle shadowing in S3, later.)

    Update your three methods parse, toString (a.k.a. expr->string), eval.

    1. add Ids to your data-definition (after deciding what data-type will represent them); then:
      1. update expr->string (or, Expr.toString) to handle this new type of expression (after test cases)
      2. update parse! (or, Expr.parse) to handle this new type of expression (after test cases)
      3. update eval (or, Expr.eval) to handle this new type of expression. As we said in class, eval'ing just an identifier simply throws an error.

        You don't test cases for evaling Ids. Though if you want to be spiffy, you can use check-error. Or, in JUnit4, put “ExpectedException.none().expect(RunTimeException.class)” on the line before the one that should trigger an error. (Better yet, JUnit5 has an assertThrows.)

    2. Next, add LetExprs to your data-definition (after deciding what data-type will represent them); then:
      1. update expr->string (or, Expr.toString) to handle this new type of expression (after test cases)
      2. update parse! (or, Expr.parse) to handle this new type of expression (after test cases)
      3. think about how to update eval to handle this new type of expression. Now we get to the heart of the issue! Write test cases, after reading the rest of this bullet.

        In order to write eval, we need to define the semantics of let Id E0 in E1:

        • Evaluate E0; let's call the result v0.
        • Then, substitute v0 for all occurrences of Id inside the tree E1; name the result of the substitution E′.
          (Note: you must do substitution in the parse tree; no credit given for string-substitution 5.)
        • Evaluate E′, and return that result.

        For example: let x 5 in ?ad x 3?ad 5 386. Be sure to write test cases for your substitution function before you write its code; include several trivial and easy tests, along with a couple of more complicated nestings and one deeply nested expression.

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

      4. Now that we realize that eval will need to do substitution in a tree, and that's a smaller, simpler, self-contained task — perfect for its own helper-function substitute. This function only does substition in a tree, and does not attempt to do any evaluating.
        Go and write substitute (after test cases for it), before implementing eval for LetExprs. (And when starting substitute, start from the template for Exprs.) For the test cases, think about exactly types you'll be wanting to sent to substitute.
        Hint: Substituting a variable with a value in an syntax-tree is essentially the same as replacing every blue-eyed child with a brown-eyed one in an anc-tree. (The only difference is that an anc-tree had only two cond-branches, while Expr has around seven, though the code for most of those are very similar.)
      5. Finally, with the substitute helper written, we're ready: write eval for LetExprs.
        Hint:Your code will correspond almost word-for-word to the semantics given above.

1 Because we don't need to check for bad inputs, it's fine to have your interpreter crash if y=0. If you prefer to "control" crash — creating a meaningful error message and calling error or throw yourself — you are also welcome to do that.      
2 For comparison, here is what comparable constructs look like in other languages:
ML-like: let x = 2+3 in x*9 end;
lisp-like: (let {[x (+ 2 3)]} (* x 9))
lisp-like, simplified: (let x (+ 2 3) (* x 9))
C#-like: using (var x = 2+3) { return x*9; }
javascript-like: var x = 2+3; return x*9;
Java-like: { int x = 2+3; return x*9; }
Haskell-like: * x 9 \n where x = + 2 3 \n
Another option for the assignment-character is “:=” (Ada,Pascal), or “” (indicating which way the data flows), or even something like “ExprId { }” (which might make CS1 students happier — the processing happens left-to-right, just like we read the statement).

Note that you can (and should) test and write a “substitute” function w/o worrying about the exact syntax of a LetExpr. Substituting one thing in a tree for another is its own independent task, de-coupled from eval’ing a local-binding statement.

     
3 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 racket 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 P is that implementing an intepreter for P doesn't get bogged down in minutae when using either Java or Racket.      
5 For example: what if a S2 programmer uses a variable named “mod” or “say” or “fun” [which we might make into a keyword in the future]? While it's not advisable for somebody to do this, and perhaps our parse should disallow this, our eval shouldn't give wacky results in this situation.      
4 All our real code should work on the parse tree itself. String-substitution (like C pre-processor macros) can't be generalized to handle shadowed variables (scope) for S3, and is in general fraught with error5. A local-variable construct which requires globally-unique names isn't very impressive!      
6 The notation “let x 5 in ?ad x 3?ad 5 38” is shorthand for
            eval(parse!("let x 5 in ?ad x 3"))
            = eval(parse!("?ad 5 3"))
            = eval(parse!("8"))
          
Observe how we definitely don't write “"let x 5 in ?ad x 3" = "?ad 5 3" = 8” since the two strings are not .equals(·) to each other, and strings are never equal to ints. More specifically: we distinguish between “” (“code evaluates to”) and “=” (“equals”, just as “=” has meant since kindergarten).      

homelecturesrecipeexamshwsD2Lbreeze (snow day; distance)


©2017, Ian Barland, Radford University
Last modified 2017.Nov.25 (Sat)
Please mail any suggestions
(incl. typos, broken links)
to ibarlandradford.edu
Rendered by Racket.