引用:
Ever wondered how an interpreter works ? Well, while evaluating parser generators for executing expressions I wrote this small example.
Getting to the basics
What is an Interpreter ?
An interpreter is a computer program that executes the program given to it as an input. In contrast, a compiler generates machine code for the program given to it, which is then executed by the operating system or computer.
Lets say the language we are going to interpret supports simple assignment statements like
a=1+5;
b=a*3;
c=a+b;
The execution of this small piece of code would involve four steps
Lexical Analysis
Syntax Analysis
Generation of Abstract Syntax Tree
Execute the code
In the lexical analysis phase all the tokensin the code are identified. In out example the tokens are [a, =, 1, +, 5, ; ……]. Once the tokens are identified we need to match them to our defined grammar i.e. many lines of Identifier=Expression;
Defining our language as a Context Free Grammar
The first thing we need to do is to define the grammar of our language as a context free grammar. A context free grammar is a way to define the formal structure in the form
V -> w
language -> (statement)+ # contains one or more statements statement ->variable=expression; variable -> [“a”-“z”]+ # a variable contains one or more letters from a-zexpression -> additiveExpr
additiveExpr -> multiplyExpr ((+ | -) multiplyExpr)* # can be 5 or (5 + 4) or (5 - 2)multiplyExpr -> unaryExpr ((* | %) unaryExpr)* # can be 5 or (5 * 4) or (5 % 2)unaryExpr -> -primaryExpr | primaryExpr # can be 5 or -5primaryExpr -> number | variable | [1-9]+ # can be 1 or abc
Implementing with JavaCC
JavaCC is a parser generator that takes a context free grammar and generates a parser for that language. Lets try to define our grammar in JavaCC. JavaCC would take a .jj file and generate java classes to parse the file and validate the grammar.
Lexical Analysis
We would need to instruct javacc to identify the tokens. The first thing to define in the javacc file is the tokens it needs to skip and the tokens that it needs to scan and parse out.
SKIP :
{” “|“\t“
|“\n“
|“\r“
}
Now we need to define all the tokens that the parser generated by javacc should parse out
TOKEN:
{
|
|
|
|
|
}
Syntax Analysis
In JavaCC the way to define a production of the form V->e is like
V() :
{}
{e()
}
The final grammar defined using the javacc format would look like
PARSER_BEGIN(ExpressionParser)public class ExpressionParser {
public static void main(String args[]) throws Exception {ExpressionParser parser = new ExpressionParser(new java.io.FileReader(args[0]));
}
}
PARSER_END(ExpressionParser)
SKIP :
{” “|“\t“
|“\n“
|“\r“
}
TOKEN:
{
|
|
|
|
|
}ASTstart start() :{}
{
(statement())+
{}
}void statement() :
{}
{identifier()“=”expression()“;”}
void identifier() :
{}
{
{
}
}
void expression():
{}
{additiveExpression()
}
void additiveExpression() :
{}
{multiplicativeExpression()
(
multiplicativeExpression()| multiplicativeExpression()
)*
}void multiplicativeExpression() :
{}
{unaryExpression()
( unaryExpression()|
unaryExpression()
)*
}void unaryExpression() :
{}
{ numberExpression()|
numberExpression()
}void numberExpression() :
{
}
{
|
}
This would generate ExpressionParser.java which would contain the code requried to parse the defined grammar. On execution of the main method with a file as an argument the parser would pass if there is a valid grammar in the file, or it would throw an exception if the file does not confine to the defined grammar. Now we are done with the lexical and the syntactical analysis.
Constructing the Abstract Syntax Tree
Now we need to construct the abstract syntax tree. Lets take a simple example
a=1+5;
b=a*3;
c=a+b;
The abstract syntax tree for this should look like
JavaCC comes along with a preprocessor called jjtree that would create us a abstract syntax tree. Whereever in the grammar we need a tree node we embed #in the grammar definition file.
void statement() #Statement:
{}
{identifier()“=”expression()“;”}
This would create a node ASTStatement.java for the production statement. All nodes by default would extend SimpleNode.java which implements Node that is generated with the node lifecycle methods.
/* Generated By:JJTree: Do not edit this line. Node.java */
/* All AST nodes must implement this interface. It provides basic
machinery for constructing the parent and child relationships
between nodes. */
public interface Node {
/** This method is called after the node has been made the current
node. It indicates that child nodes can now be added to it. */public void jjtOpen();
/** This method is called after all the child nodes have been
added. */public void jjtClose();
/** This pair of methods are used to inform the node of its
parent. */public void jjtSetParent(Node n);
public Node jjtGetParent();
/** This method tells the node to add its argument to the node’s
list of children. */public void jjtAddChild(Node n, int i);
/** This method returns a child node. The children are numbered
from zero, left to right. */public Node jjtGetChild(int i);
/** Return the number of children the node has. */public int jjtGetNumChildren();
/** Accept the visitor. **/public Object jjtAccept(ExpressionParserVisitor visitor, Object data);
}
Our final grammar decorated with all the defintions of the abstract syntax tree nodes would look like
options {MULTI=true;
VISITOR=true;
NODE_DEFAULT_VOID=true;
NODE_EXTENDS=“BaseNode”;
}
PARSER_BEGIN(ExpressionParser)
public class ExpressionParser {
public static void main(String args[]) throws Exception {ExpressionParser parser = new ExpressionParser(new java.io.FileReader(args[0]));
//ExpressionParser parser = new ExpressionParser(System.in);ASTstart expr=parser.start();
ExpressionVisitor v=new ExpressionVisitor();
System.out.println(expr.jjtAccept(v,null));
}
}
PARSER_END(ExpressionParser)
SKIP :
{” “|“\t“
|“\n“
|“\r“
}
TOKEN:
{
|
|
|
|
|
}ASTstart start() #start:{}
{
(statement())+
{ return jjtThis; }
}void statement() #Statement:
{}
{identifier()“=”expression()“;”}
void identifier() :
{}
{
{jjtThis.data.put(“name”,token.image);
}#Variable
}
void expression():
{}
{additiveExpression()
}
void additiveExpression() :
{}
{multiplicativeExpression()
(
multiplicativeExpression()#AddExpr(2)| multiplicativeExpression()#SubractExpr(2)
)*
}void multiplicativeExpression() :
{}
{unaryExpression()
( unaryExpression()#MultiplyExpr(2)|
unaryExpression()#DivideExpr(2)
)*
}void unaryExpression() :
{}
{ numberExpression()#NegateExpr(1)|
numberExpression()
}void numberExpression() :
{
}
{
{jjtThis.data.put(“value”,new Integer(Integer.parseInt(token.image)));
}#Number
|
{jjtThis.data.put(“name”,token.image);
}#VariableValue
}
If you notice in the option, we have instructed jjtree to extend all sources from BaseNode.java. The base node is decorated with a hashmap that would be used to store the scanned tokens during parsing.
import java.util.HashMap;public class BaseNode{public HashMap data=new HashMap();
}
If you observe line 99 of the grammar definition, it shows how the parsed number token is added to the Number node that is constructed by jjtree.
This completes the generation of the abstract syntax tree.
Interpretation and Execution
What now remaing is to walk the tree and execute the code. One of the options that we have specified in the grammar definition is to generate the visitor. JJtree would automatically add accept methods on all the nodes generated and also generate the visitor interface
void statement() #Statement:
/* Generated By:JJTree: Do not edit this line. C:\workspace\JavaCC\target\ExpressionParserVisitor.java */
public interface ExpressionParserVisitor
{public Object visit(SimpleNode node, Object data);
public Object visit(ASTstart node, Object data);
public Object visit(ASTStatement node, Object data);
public Object visit(ASTVariable node, Object data);
public Object visit(ASTAddExpr node, Object data);
public Object visit(ASTSubractExpr node, Object data);
public Object visit(ASTMultiplyExpr node, Object data);
public Object visit(ASTDivideExpr node, Object data);
public Object visit(ASTNegateExpr node, Object data);
public Object visit(ASTNumber node, Object data);
public Object visit(ASTVariableValue node, Object data);
}
All we need to do now is to implement the visitor interface and interpret our language. The Interpreter would need two datastores
Symbol Table
Execution Stack
The symbol table would hold all the variables and their values, and the execution stack would contain all the intermediate results while expression are evaluated.
The Visitor code would look like.
public interface ExpressionParserVisitor
import java.util.HashMap;
import java.util.LinkedList;
public class ExpressionVisitor implements ExpressionParserVisitor{
private LinkedList stack=new LinkedList();
private HashMap symbolTable=new HashMap();
public Object visit(SimpleNode node, Object data) {node.childrenAccept(this,data);
return null;
}
public Object visit(ASTstart node, Object data) {node.childrenAccept(this,data);
return symbolTable;
}
public Object visit(ASTAddExpr node, Object data) {node.childrenAccept(this,data);
Integer arg1=pop();
Integer arg2=pop();
stack.addFirst(new Integer(arg2.intValue()+arg1.intValue()));
return null;
}
public Object visit(ASTSubractExpr node, Object data) {node.childrenAccept(this,data);
Integer arg1=pop();
Integer arg2=pop();
stack.addFirst(new Integer(arg2.intValue()-arg1.intValue()));
return null;
}
public Object visit(ASTMultiplyExpr node, Object data) {node.childrenAccept(this,data);
Integer arg1=pop();
Integer arg2=pop();
stack.addFirst(new Integer(arg2.intValue()*arg1.intValue()));
return null;
}
public Object visit(ASTDivideExpr node, Object data) {node.childrenAccept(this,data);
Integer arg1=pop();
Integer arg2=pop();
stack.addFirst(new Integer(arg2.intValue()/arg1.intValue()));
return null;
}
public Object visit(ASTNegateExpr node, Object data) {node.childrenAccept(this,data);
Integer arg1=pop();
stack.addFirst(new Integer(arg1.intValue()*-1));
return null;
}
public Object visit(ASTNumber node, Object data) {node.childrenAccept(this,data);
stack.addFirst(node.data.get(“value”));
return null;
}
public Object visit(ASTStatement node, Object data) {node.childrenAccept(this,data);
Integer value=(Integer)stack.removeFirst();
String var=(String)stack.removeFirst();
symbolTable.put(var,value);
return null;
}
public Object visit(ASTVariable node, Object data) {node.childrenAccept(this,data);
String var=(String)node.data.get(“name”);
stack.addFirst(var);
return null;
}
public Object visit(ASTVariableValue node, Object data) {node.childrenAccept(this,data);
String var=(String)node.data.get(“name”);
stack.addFirst(symbolTable.get(var));
return null;
}
private Integer pop()
{return (Integer)stack.removeFirst();
}
}
Well, thats it .. our very own interpretter !!
posted on 2007-07-13 15:52 Caribbean 阅读(778) 评论(0) 编辑 收藏 所属分类: 学习