home—lectures—recipe—exams—hws—D2L—breeze (snow day; distance)
S2
Interpreting S
project
Deliverables:
-
For the D2L dropbox, please submit all files individually (no .zip/.jar).
I should be able to download your files and run your code from the download-directory.
-
Since S2 subsumes S1, you only need to submit S1 in one language, and S2 in the other.
(For example, S2.rkt and S1-java/*.java; or, S1.rkt and S2-java/*.java.)
-
If possible, please add a comment “>>>S1” or “>>>S2” to parts of code which you added/modified from the original code provided.
This helps immensely, when grading.
-
For the hardcopy, you need only print out those files that changed.
If you really want, you can just print out only the *parts* of the files that changed
(w/ a brief indication of what function the changed-code is in).
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.
-
For each language, you must implement three methods:
-
parse!, which turns source-code
into an internal representation,
-
expr->string which turns internal representation back
into source-code,
and
-
eval, which actually interprets the program,
returning a value (a number for S0-S2, and a number-or-function in S3).
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 S1, which you must implement in both Racket and Java.)
-
You do not need to worry about error-checking the programs we process —
we will only concern ourselves with syntactically correct Si programs.
(As a matter of defensive programming, you might still
want to add some assertions/sanity-checks in your code.)
-
Use the D2L 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 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:
-
For the expression ?mo x y,
eval should evaluate to
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 - ⌊x/y⌋),
where ⌊r⌋ means the the floor of r.
Note that you are provided sufficient test cases for ?mos,
in the comments of the S0 test-case files.
For (Expr0 >? Expr1) = Expr2 : Expr3,
your eval should first evaluate just Expr0
and
Expr1;
if the first result is greater than the second,
then evaluate Expr2
and return its value;
otherwise evaluates Expr3 and return that value.
(Note how you are implementing short-circuit semantics for Ifgt!)
You must make your own test cases for Ifgts;
include at least two as-simple-as-possible tests, and two tests with more deeply nested
Exprs.
I suggest including one where the
Ifgt is not the top-level comment
(e.g., a ?ad expression which contains a ( … >? …) … : …
as one of its operands).
Complete two versions of S1: both racket, and java.
(For S2 and beyond, you can choose which implementation to continue.)
-
(25pts) Implement S2 in either racket or Java (your choice).
S2 adds identifiers to S1:
Expr ::= … | Id | LetExpr
LetExpr ::= let Id Expr in Expr
|
where Id can be
any series of letters and digits which isn't interpretable as a number.
(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.
- 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 parse! (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 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.)
-
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 parse! (or, Expr.parse) to handle this new type of expression (after test cases)
- 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
.)
-
Evaluate E′,
and return that result.
For example:
let x 5 in ?ad x 3 ⇒ ?ad 5 3 ⇒ 8.
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.
-
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.)
-
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.
home—lectures—recipe—exams—hws—D2L—breeze (snow day; distance)