Educational Objectives: After completing this assignment, the student should be able to do the following:
Operational Objectives: Implement a calculator with assignment in Java using an L-attributed grammar
Deliverables: One file Calc5.java
Consider the following augmented LL(1) grammar for an expression language:
<expr> -> <term> <term_tail> term_tail.subtotal := term.value; expr.value := term_tail.value <term> -> <factor> <factor_tail> factor_tail.subtotal := factor.value; term.value := factor_tail.value <term_tail1> -> '+' <term> <term_tail2> term_tail2.subtotal := term_tail1.subtotal+term.value; term_tail1.value := term_tail2.value | '-' <term> <term_tail2> term_tail2.subtotal := term_tail1.subtotal-term.value; term_tail1.value := term_tail2.value | empty term_tail1.value := term_tail1.subtotal <factor1> -> '(' <expr> ')' factor1.value := expr.value | '-' <factor2> factor1.value := -factor2.value | number factor1.value := number <factor_tail1> -> '*' <factor> <factor_tail2> factor_tail2.subtotal := factor_tail1.subtotal*factor.value; factor_tail1.value := factor_tail2.value | '/' <factor> <factor_tail2> factor_tail2.subtotal := factor_tail1.subtotal/factor.value; factor_tail1.value := factor_tail2.value | empty factor_tail1.value := factor_tail1.subtotal
Note: the indexing (1 and 2) used with nonterminals, such as <factor1> and <factor2>, is only relevant to the semantic rules to identify the specific occurrences of the nonterminals in a production. (See text.)
Draw the decorated parse tree for -2*3+1 that shows the attributes and their values.
The following calculator Java program implements the attribute grammar shown above to calculate the value of an expression. To this end, the synthesized value attributes are returned as integer values from the methods that correspond to nonterminals. Inherited subtotal attributes are passed to the methods as arguments:
/* Calc.java Implementes a parser and calculator for simple expressions Uses java.io.StreamTokenizer and recursive descent parsing Compile: javac Calc.java Execute: java Calc or: java Calc <filename> */ import java.io.*; public class Calc { private static StreamTokenizer tokens; private static int token; public static void main(String argv[]) throws IOException { InputStreamReader reader; if (argv.length > 0) reader = new InputStreamReader(new FileInputStream(argv[0])); else reader = new InputStreamReader(System.in); // create the tokenizer: tokens = new StreamTokenizer(reader); tokens.ordinaryChar('.'); tokens.ordinaryChar('-'); tokens.ordinaryChar('/'); // advance to the first token on the input: getToken(); // parse expression and get calculated value: int value = expr(); // check if expression ends with ';' and print value if (token == (int)';') System.out.println("Value = " + value); else System.out.println("Syntax error"); } // getToken - advance to the next token on the input private static void getToken() throws IOException { token = tokens.nextToken(); } // expr - parse <expr> -> <term> <term_tail> private static int expr() throws IOException { int subtotal = term(); return term_tail(subtotal); } // term - parse <term> -> <factor> <factor_tail> private static int term() throws IOException { int subtotal = factor(); return factor_tail(subtotal); } // term_tail - parse <term_tail> -> <add_op> <term> <term_tail> | empty private static int term_tail(int subtotal) throws IOException { if (token == (int)'+') { getToken(); int termvalue = term(); return term_tail(subtotal + termvalue); } else if (token == (int)'-') { getToken(); int termvalue = term(); return term_tail(subtotal - termvalue); } else return subtotal; } // factor - parse <factor> -> '(' <expr> ')' | '-' <expr> | identifier | number private static int factor() throws IOException { if (token == (int)'(') { getToken(); int value = expr(); if (token == (int)')') getToken(); else System.out.println("closing ')' expected"); return value; } else if (token == (int)'-') { getToken(); return -factor(); } else if (token == tokens.TT_WORD) { getToken(); // ignore variable names return 0; } else if (token == tokens.TT_NUMBER) { getToken(); return (int)tokens.nval; } else { System.out.println("factor expected"); return 0; } } // factor_tail - parse <factor_tail> -> <mult_op> <factor> <factor_tail> | empty private static int factor_tail(int subtotal) throws IOException { if (token == (int)'*') { getToken(); int factorvalue = factor(); return factor_tail(subtotal * factorvalue); } else if (token == (int)'/') { getToken(); int factorvalue = factor(); return factor_tail(subtotal / factorvalue); } else return subtotal; } }
Copy this example Calc.java program from ~cop4020p/examples/, and compile and run it:
javac Calc.java java Calc
Explain why the input 1/2; to this program produces the value 0. What are the relevant parts of the program involved in computing this result?
Extend the attribute grammar with two new productions and two new attributes for all nonterminals:
The two new productions with corresponding semantic rules are as follows:
<expr1> -> 'let' identifier '=' <expr2> expr2.in := expr1.in; expr1.value := expr2.value expr1.out := expr2.out.put(identifier=expr2.value) | <term> <term_tail> term.in := expr1.in; term_tail.in := term.out; term_tail.subtotal := term.value; expr1.value := term_tail.value; expr1.out := term_tail.out <factor1> -> '(' <expr> ')' expr.in := factor1.in; factor1.value := expr.value factor1.out := expr.out | '-' <factor2> factor2.in := factor1.in; factor1.value := -factor2.value; factor1.out := factor2.out | identifier factor1.value = factor1.in.get(identifier) | number factor1.value := number; factor1.out := factor1.in
The first production introduces an assignment construct as an expression, similar to the C/C++ assignment which can also be used within an expression, as in this example:
(let x = 3) + x; Value = 6
The semantic rule expr2.in := expr1.in copies the symbol table of the context in which expr1 is evaluated to the context of expr2. The evaluation of expr2 may change the symbol table and the table is copied to expr1 with the semantic rule expr1.out := expr2.out. For this part of the assignment, you have to change the semantic rules of all other productions in the grammar to include assignments for the in and out attributes to pass the symbol table. Write down the grammar with these new semantic rules.
Implement the two new productions and semantic rules in an updated Calc.java program.
To implement a symbol table with identifier-value bindings, you can use the Java java.util.Hashtable class as follows:
import java.util.*; ... public class Calc { ... public static void main(String argv[]) throws IOException { ... Hashtable<String,Integer> exprin = new Hashtable<String,Integer>(); Hashtable<String,Integer> exprout; ... int value = expr(exprin, exprout); ... private static int expr (Hashtable<String,Integer> exprin, Hashtable<String,Integer> exprout) throws IOException { if (token == tokens.TT_WORD && tokens.sval.equals("let")) { getToken(); // advance to identifier String id = tokens.sval; getToken(); // advance to '=' getToken(); // advance to <expr> int value = expr(exprin, exprout); exprout.put(id, new Integer(value)); } else { int subtotal = term(exprin, termout); return term_tail(subtotal, termout, exprout); } } private static int factor (Hashtable<String,Integer> factorin, Hashtable<String,Integer> factorout) throws IOException { ... else if (token == tokens.TT_WORD) { String id = tokens.sval; getToken(); factorout = factorin; return ((Integer)factorin.get(id)).intValue(); } ...
The put method puts a key and value in the hashtable, where the value must be a class instance so an Integer instance is created. The get method returns the value of a key. The intValue method of Integer class returns an int. Test your new Calc.java application. For example:
let x = 1; Value = 1 (let x = 1) + x; Value = 2 (let a = 2) + 3 * a; Value = 8 1 + (let a = (let b = 1) + b) + a; Value = 5
Turn in this assignment as a working Java program named "Calc5.java", using the submit script "pr5submit.sh". Your answers for the non-programming questions should be inserted in the documentation at the top of your file. Use plain text to draw trees and write grammars as required.
Suggestions on drawing trees. There are (at least) two basic ways to illustrate trees using ascii text. The first is "pyramidal":
<expr>(-3) -------------------------------- / \ <term>(-5) <term_tail1>[-5](-3) -------- ------------- / \ / | \ ................................................................. Note: [] represents inherited attribute values () represents synthesized attribute values
A second way to represent the same tree is a squared off version:
<expr>(-3) ---------------------------------------------------- | | <term>(-5) <term_tail1>[-5](-3) --------------- ---------------------------- | | | | | ..................................................................... Note: [] represents inherited attribute values () represents synthesized attribute values
The latter may be easier to use, especially for decorated parse trees where a lot of information is displayed for each node. Note that in either case we are using [] to enclose inherited attribute values and () to enclose synthesized attribute values.