home—lectures—recipe—exams—hws—D2L—breeze (snow day; distance)
hw07
Interpreting Q
project
The full Q1,Q2 (both racket and Java for Q1, plus any add'l tests) due Nov.30 (Mon) 14:00.
It is strongly recommended to complete this before Thanksgiving break,
so you can either not worry about it,
or (if you really want to put in some hours over the break) get a head-start on hw08.
However, these test cases are due Nov.20 (Fri) 14:00.
Over the course of several homeworks,
we'll implement a “tower” of languages (Q0, Q1, …, Q6)
each language incorporating more features than the previous.
Q0 is provided for you.
-
For each language, you must implement three methods:
-
parse, which turns source-code
into an internal representation,
-
toString which turns internal representation back
into source-code,
and
-
eval, which actually interprets the program,
returning a value (a number for Q0-Q2, and a number-or-function in Q3).
I recommend implementing and testing the methods in this order.
See the provided test-cases, for examples.
There might be other helper functions to implement as well.
-
Your solution may be in Java or in Racket
(or see me, if you want to use a different language).
(The exception is Q1, which you must implement in both Racket and Java.)
-
You do not need to worry about error-checking —
we will only concern ourselves with syntactically correct Qi programs.
(As a matter of defensive programming, you might still
want to add some assertions/sanity-checks in your code.)
-
Use the Desire2Learn discussion board for group questions and answers.
-
This project is based on the first chapters of
Programming Languages and Interpretation,
by Shriram Krishnamurthi.
As a result, this homework assignment is covered by the
Creative Commons
Attribution-NonCommercial-ShareAlike 3.0 United States License.
Although we're using a different dialect of scheme/racket than that book,
you might find it helpful to skim it.
(15pts) Implement Q1 in both racket, and Java.
Q1 is just like Q0, but with two additional
types of expressions:
Expr ::= … | IfZeroExpr
IfZeroExpr ::= if Expr is zero then Expr else Expr @
BinOp ::= … | 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.
The only method which cares what these new expressions mean
(their semantics)
is
eval:
-
For the expression (x mod y),
eval should evaluate to1
x mod y,
where the result is always between 0 (inclusive) and y (exclusive);
in particular, the result should never be positive if y<0.
Notice that this is slightly different behavior than
either Java's built-in %
(which behaves differently on negative numbers),
and from Racket's built-in modulo (which only accepts integers).
In both racket and Java, you can calculate this as
y*(x/y-floor(x/y)).
Note that you are provided sufficient test cases for modExprs,
in the comments of the Q0 test-case files.
For if Expr0 is zero then Expr1 else
Expr2@,
your eval should first evaluate just Expr0;
if that value is positivezero, then evaluate Expr1
and return its value;
otherwise evaluates Expr2 and return that value.
(Note how you are implementing short-circuit semantics for IfZeroExpr!)
You must make your own test cases for IfZeroExprs;
include at least two as-simple-as-possible tests, and two tests with more deeply nested
Exprs.
I suggest includine one where the
IfZeroExpr is not the top-level comment
(e.g., a add expression which contains a if … is zero
as one of its operands).
Complete two versions of Q1: both racket, and java.
(For Q2 and beyond, you can choose which implementation to continue.)
-
(25pts) Implement Q2 in either racket or Java (your choice).
Q2 adds identifiers to Q1:
Expr ::= … | Id | LetExpr
LetExpr ::= say Id be Expr in Expr matey // Subject to votes in class, for exact syntax.2
|
where Id can be
any series of letters and digits which isn't interpretable as a number3.
(Assume for now that any nested let
expressions
use different Ids.
We'll handle shadowing in Q3, later.)
Update your three methods
parse,
toString (a.k.a. expr->string),
eval.
In order to write eval, we need to define the semantics of
say Id be E0 in E1 matey:
-
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.
Observe that when evaluating a (legal) Q2 program,
eval will never actually encounter an Id --
that Id will have been substituted out before
we ever recur down to it.
In order to make a substitution in an Expr
parse-tree,
write a helper function that does only substituting
(and does not do any evaluating in any way).
This task
would be similar to taking an Ancestor-tree,
and replacing every blue-eyed Child with a brown-eyed one.
(The only difference
is that an AncTree had only two cond-branches,
while Expr has around seven,
though the code for most of those are very similar.)
For example:
say x be 5 in (x add 3) matey ⇒ (5 add 3) ⇒ 86.
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.
You can choose implement Q2 in either in Racket, or in Java.
Repeating the steps above in more words: For Q2, you will:
- add Ids to your data-definition (after deciding what data-type will represent them); then:
- update expr->string (or, Expr.toString) to handle this new type of expression (after test cases)
- update string->expr (or, Expr.parse) to handle this new type of expression (after test cases)
- update eval (or, Expr.eval) to handle this new type of expression As we
said in class, eval'ing just an identifier causes an error. I recommend actually adding a
case for eval that throws your own error (with an appropriate error-message), but it's okay to just leave this off [which will also cause an error, but it'll be an error about your code, rather than an error suited for the Q2 programmer].
- Next, add LetExprs to your data-definition (after deciding what data-type will represent them);
then:
- update expr->string (or, Expr.toString) to handle this new type of expression (after test cases)
- update string->expr (or, Expr.parse) to handle this new type of expression (after test cases)
- update eval to handle this new type of expression.
Oh right; for this one, after reading the hw 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:
-
Go and write substitute (after test cases for it), before updating
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.
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
In class, we will choose one of the following or (most likely) a variant, as a class:
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 “Expr → Id { … }”
(which might make CS1 students happier — the processing happens left-to-right, just like we
read the statement).
Or, if we want to include emoji keywords,
good candidates are the entries on this page
which have the left-most column “native” filled in.
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 Q2 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 Q3,
and is in general
fraught with error5.
A local-variable construct which requires
globally-unique names isn't very impressive!
↩
6
The notation
“say x be 5 in (x add 3) matey
⇒ 5 add 3 ⇒ 8”
is shorthand for
eval(parse("say x be 5 in (x add 3) matey"))
= eval(parse("(5 add 3)"))
= eval(parse("8"))
|
Observe how we definitely don't write
“"say x be 5 in (x add 3) matey" = "(5 add 3)" = 8”
since the two strings are not .equals(·) to each other,
and strings are never ints.
More specifically:
we distinguish between “⇒”
(“code evaluates to”)
and
“=” (“equals”,
just as “=” has meant since kindergarten).
↩
home—lectures—recipe—exams—hws—D2L—breeze (snow day; distance)