To make sure you can interpret languages with functions as values and nested scopes.
On all assignments, include your test cases. They should be sufficient to check the various nesting combinations of expressions and cases that might go wrong in your implementations. For example, see the examples developed in the notes for understanding substitution (and therefore testing the subst code).
Turn in one interpreter (with needed auxiliary functions like subst) with your solutions to both problems.
In class, we extended the language containing with-statements (local identifiers) to also support function definition and application (function calls). The concrete and abstract syntax for this new language (FWAE) is as follows: (the concrete syntax uses square brackets instead of angle brackets so that the web browser doesn't see the terms as html tags):
[FWAE] ::= [num] | {+ [FWAE] [FWAE]} | {- [FWAE] [FWAE]} | [id] | {with {[id] [FWAE]} [FWAE]} | {fun {[id]} [FWAE]} | {[FWAE] [FWAE]} (define-datatype FWAE FWAE? [num (n number?)] [add (lhs FWAE?) (rhs FWAE?)] [sub (lhs FWAE?) (rhs FWAE?)] [id (name symbol?)] [with (name symbol?) (named-expr FWAE?) (body FWAE?)] [fun (param symbol?) (body FWAE?)] [app (fun-expr FWAE?) (arg-expr FWAE?)])
Extend the parser and interpreter to implement FWAE. Here are some examples of expressions and what they should return (since our language is growing beyond numbers, rename the interpreter to interp in place of calc):
(interp (fun 'x (id 'x))) = (fun 'x (id 'x)) (interp (app (fun 'x (add (num 1) (id 'x))) (num 5))) = (num 6) (interp (with 'double (fun 'x (add (id 'x) (id 'x))) (app (id 'double) (app (id 'double) (num 4))))) = (num 16) (interp (with 'staged-adder (fun 'x (fun 'y (add (id 'x) (id 'y)))) (with 'add5 (app (id 'staged-adder) (num 5)) (sub (app (id 'add5) (num 3)) (app (id 'add5) (num 1)))))) = (num 2)
The last example is a bit subtle for those of you new to functional programming. Staged-adder is a function that takes a number and returns a function. When the returned function gets a number, those two numbers are added together. You can experiment with this concept in Scheme. Here's the code:
(define (staged-adder x) (lambda (y) (+ x y))) (define add5 (staged-adder 5)) (add5 1) (add5 2) etc
Start with the code from class/notes for the WAE language. Make sure that code is running before starting the modifications for this assignment.
If you look closely at the examples, you'll notice that interp now returns FWAE instead of Scheme numbers. Once we add functions, this becomes necessary so we can return funs. In other words, the contract on interp is:
;; interp : FWAE -> FWAE ;; evaluates FWAE expressions by reducing them to their corresponding values ;; return values are either num or funthis change will impact the existing code from the WAE calc in a few places. Edit that code as needed to handle the new return type.
As you do this, use auxiliary functions to keep your code clean. The old cases of your main interpreter function should not get much more complex, though they may use auxiliary functions for any extra processing.
Follow a similar style of evaluation for apps as for with statements: evaluate the arguments before calling the functions.
Stick to single argument functions for now, unless you feel ambitious. We'll relax that later.
Watch for FWAE in the define-datatype to suggest where interp needs to be called recursively.
The add and sub cases of interp look very similar, because the only difference between them in which operator to use to combine the results. We would notice similar overlaps if we added other operations, such as multiplication and division. In place of having separate rules for these operations in the abstract syntax, define a single syntactic rule for all binary arithmetic operators (don't change the concrete syntax though). Parse these into a binop datatype variant in the abstract syntax. Define a table (function) that maps operator names (symbols) to actual functions (Scheme procedures) that perform the corresponding operation. Having a single rule like this, accompanied by a table, makes your language easier to extend: once you have modified your parser and interpreter once to support binary operators, you won't need to touch either one to add any number of new operators. To demonstrate this, define multiplication and division (using * and / to represent them in the language's concrete syntax).