小型桌面计算器的实现(javacc)
從開始學計算理論,就對形式語言,編譯原理很感興趣,所以大學對這門課學的也算是最好了。自己也實現過一些簡單的詞法分析器之類的東西,不過也都是學習目的的,質量一般 后來一直在Linux學習,對lex/yacc研究過一點時間,設計過一個小的腳本引擎,可以做一些比較復雜的數學運算,這個可以參考我的這篇博客<hoc>。工作以后,平臺 變成了java,跟C漸漸的離得遠了,也喜歡上java這個提供語言級別的面向對象的語言以前的一些東西用順手了,一時習慣還改不過來,于是就開始找lex/yacc的替代品。
研究過一段時間的antlr,覺得還行,特別是那個GUI做的非常好,但是跟lex/yacc在用法上還是有些差別的,最近才遇到了javacc,覺得跟lex/yacc的寫法相當像,就試著 寫了一個簡單的桌面計算器(desktop-calculator),大家可以參考一下。 (從下載到javacc.zip到寫出這個計算器,大約花了我一個下午的時間,所以你可以很快 的學會javacc,并在實際中應用。)用這類工具的好處是,只要你理解形式語言,那么就可以很方便的做出一個分析起來,而不用關心其實現細節(如詞法分析等部分是十分
枯燥且無意義的)
一來這是一個比較經典的例子,通過它的學習,可以迅速掌握一個編譯器生成器,或者叫語言解析器生成器。二來最近項目中用到了一些類似的東西,正好趁此機會記下來 以便將來參考。
四則運算的計算器的規則非常清晰,可以看一下這個巴克斯范式(BNF)
Java代碼
expr::=term((+|-)term)*
term::=factor((*|/)factor)*
factor::=number|expr
number::=[0-9]+...
形式語言的好處就在于其清晰,準確和強大。如果你對正則表達式比較收悉,那么可以很容易掌握形式語言。(正則表達式其實就是一個精簡而強大的形式語言實例)
首先,我們需要定義在文法描述中需要用到的所有記號(token),這是分析器能認識到的最小單元
Java代碼
TOKEN:{
<ADD:"+">
|<SUB:"-">
|<MUL:"*">
|<DIV:"/">
|<LPAREN:"(">
|<RPAREN:")">
|<NUMBER:
["0"-"9"](["0"-"9"])*
|(["0"-"9"])+"."(["0"-"9"])*(<EXPONENT>)?
|"."(["0"-"9"])+(<EXPONENT>)?
|(["0"-"9"])+<EXPONENT>>
|<#EXPONENT:["e","E"](["+","-"])?(["0"-"9"])+>
}
javacc的記號描述很簡單,就是普通的正則表達式,當然用引號引起來的簡單單詞也算是正則表達式,如記號ADD,就是"+",這樣,當解析器遇到"+"時就返回一個ADD記號,方便在內部使用。
這里詳細說一下NUMBER記號,數字怎么表示呢?
我們用自然語言可以描述成:以0到9中的任一個數字開頭,后邊可以有任意多位(包括0位)數字(此為整數),或者以至少一位數字,后邊跟一個小數點,然后又有任意多位的數字,如果使用科學 計數法,則這個數字串后邊還可以跟一個E,E后邊又可以有正號(+)或者負號(-),也可以沒有,然后,后邊又是至少一位數字,或者……
可以看到,自然語言的描述冗長且不容易理解,我們看看形式語言的描述:首先定義一些符號的意義,如
| "|" | 表示或者 |
| 0-9 | 表示0到9的一個數字 |
| [] | 表示一個區間 |
| "?" | 表示其前的區間中的元素重復一次或零次 |
| "+" | 表示其前的區間中的元素重復至少一次 |
| "*" | 表示其前的區間中的元素重復零次或多次 |
| () | 表示一個分組,是一個整 |
好了,有了這些定義,我們再看看如何用形式語言描述一個浮點數或者整數:
Java代碼
number::=[0-9]([0-9])*|[0-9]+'.'([0-9])+exponent?
|'.'([0-9])+exponent?
|([0-9])+exponent?
exponent::=[e,E]([+,-])?([0-9])+
可以看到,形式語言在描述這種抽象概念上比自然語言要好得多。一旦掌握了形式語言就可以很容易的理解各種復雜的規則。
詞法分析器的一個功能就是剔除源碼中的空白字符,比如,空格,制表符,回車換行等
Java代碼
SKIP:{
""|"\t"|"\r"|"\n"
}
有了BNF,我們看看在javacc中,如何表現這些規則:
Java代碼
voidexpr():{}
{
term()((<ADD>|<SUB>)term())*
}
voidterm():{}
{
factor()((<MUL>|<DIV>)factor())*
}
voidfactor():{}
{
<NUMBER>|
<LPAREN>expr()<RPAREN>
}
javacc基本忠實的體現了BNF的規則定義,當然,現在這個解析器的文法部分(詞法分析和語法分析) 已經算是結束了,但是它還不能完成任何計算,因為它沒有語義的定義部分,即當發現了 此語法現象后該做什么是沒有定義的。
相信大家都已經注意到,每個非終結符后邊都有兩個大括號{},第一個目前為空。在javacc中,每個非終結符都最終會被翻譯成一個方法,(至于怎么翻譯的,你可以自己看看它生成的代 碼,當年我曾痛苦在yacc生成的一堆yy_*中徜徉過一段時間,現在是實在看不下去了,javacc生成的代碼中到處都是jj_*,唉,一個yy,一個jj,生成的代碼是看不成了), 第一個空的大括號中即為將來你自己填寫的一些關于這個方法的一些臨時變量,包括返回值等信息。
好了,我們看看加入了語義解釋后的代碼:
Java代碼
doubleexpr():
{
doubletemp=0;
doublefirst,second;
}
{
//你可以在非終結符前插入變量,等號等,在其后可以插入普通的java代碼
//插入代碼后看起來可能不夠清晰,可以參看上邊的形式定義
first=term(){temp=first;}
(<ADD>second=term(){temp=first+second;}|
<SUB>second=term(){temp=first-second;})*
{returntemp;}//最后,應當返回某值
}
doubleterm():
{
doubletemp=0;
doublefirst,second;
}
{
first=factor(){temp=first;}
(<MUL>second=factor(){temp=first*second;}|
<DIV>second=factor(){temp=first/second;})*
{returntemp;}
}
doublefactor():
{
doubletemp=0;
Tokentoken;
}
{
token=<NUMBER>{
returnDouble.parseDouble(token.image);
}|<LPAREN>temp=expr()<RPAREN>{
returntemp;
}
}
好了,主體部分已經建立好了,我們再來看看聲明信息等(形式語言的學習是重點,其余的 都比較簡單易學,而且不同的cc都提供大同小異的功能)
Java代碼
PARSER_BEGIN(CalcParser)
importjava.io.StringReader;
importjava.io.Reader;
publicclassCalcParser{
publicCalcParser(Stringexpr){
this((Reader)(newStringReader(expr)));
}
publicstaticvoidmain(String[]args){
try{
CalcParserparser=newCalcParser(args[0]);
System.out.println(parser.expr());
}catch(Exceptione){
System.out.println("error:"+e.getMessage());
}
}
}
PARSER_END(CalcParser)
每個分析器需要一個名字,這個名字定義在PARSER_BEGIN(xxx)中,而且應保證與下面的類聲明保持一致:
public class xxx{}
現在,我們可以用javacc提供的命令行程序來生成我們的計算器,需要注意的是,javacc生成的是java源碼,且不再依賴于javacc,你可以將你的分析器源碼放在任何地方使用。
$ javacc CalcParser.jj(你看看這文件名后綴)
可以看到有類似的輸出;
寫道
Java Compiler Compiler Version 4.2 (Parser Generator)
(type "javacc" with no arguments for help)
Reading from file CalcParser.jj . . .
File "TokenMgrError.java" does not exist. Will create one.
File "ParseException.java" does not exist. Will create one.
File "Token.java" does not exist. Will create one.
File "SimpleCharStream.java" does not exist. Will create one.
Parser generated successfully.
你現在可以試著輸入一些表達式讓計算器進行計算了。(事例結果見后)
如果想要加入一些別的功能,比如,計算表達式的正弦,余弦函數?很簡單,我們可以使用 java.lang.Math中提供的一些數學函數。
對規則稍事修改,即可完成我們的需求。當然,這個計算器的還是比較簡單的,比如,不能回溯(這個以后再說),不支持負數,不支持冪計算。但是,如果通過此文,你對形式語言有了比較 好的理解的話,這些問題都是很容易解決的。
下面這些代碼是我再上邊的這個四則運算計算器的基礎上加入了少量規則而成的一個微型函數計算器,其表達式格式類似于JSP中的EL表達式,你可以對其進行擴展,從而使之更加有趣。
Java代碼
PARSER_BEGIN(CalcParser)
importjava.io.StringReader;
importjava.io.Reader;
publicclassCalcParser{
publicCalcParser(Stringexpr){
this((Reader)(newStringReader(expr)));
}
publicstaticvoidmain(String[]args){
try{
CalcParserparser=newCalcParser(args[0]);
System.out.println(parser.elexpr());
}catch(Exceptione){
System.out.println("error:"+e.getMessage());
}
}
}
PARSER_END(CalcParser)
//聲明到此結束
SKIP:{
""|"\t"|"\r"|"\n"
}
TOKEN:{
<ADD:"+">
|<SUB:"-">
|<MUL:"*">
|<DIV:"/">
|<MOD:"%">
|<LPAREN:"(">
|<RPAREN:")">
|<NUMBER:
["0"-"9"](["0"-"9"])*
|(["0"-"9"])+"."(["0"-"9"])*(<EXPONENT>)?
|"."(["0"-"9"])+(<EXPONENT>)?
|(["0"-"9"])+<EXPONENT>>
|<#EXPONENT:["e","E"](["+","-"])?(["0"-"9"])+>
|<EXPRPREFIX:"${">
|<EXPRSUFFIX:"}">
|<SIN:"sin">
|<COS:"cos">
}
//記號部分聲明到此結束,下面是語法聲明,包括語義解釋
doubleelexpr():
{doubletemp=0;}
{
<EXPRPREFIX>temp=expr()<EXPRSUFFIX>
{returntemp;}
}
doubleexpr():
{
doubletemp=0;
doublefirst,second;
}
{
first=term(){temp=first;}
(<ADD>second=term(){temp=first+second;}|
<SUB>second=term(){temp=first-second;})*
{returntemp;}
}
doubleterm():
{
doubletemp=0;
doublefirst,second;
}
{
first=factor(){temp=first;}
(<MUL>second=factor(){temp=first*second;}|
<DIV>second=factor(){temp=first/second;})*
{returntemp;}
}
doublefactor():
{
doubletemp=0;
Tokentoken;
}
{
token=<NUMBER>{
returnDouble.parseDouble(token.image);
}|<LPAREN>temp=expr()<RPAREN>{
returntemp;
}|
<SIN><LPAREN>temp=expr()<RPAREN>{
returnjava.lang.Math.sin(temp);
}|
<COS><LPAREN>temp=expr()<RPAREN>{
returnjava.lang.Math.sin(temp);
}
//如果有興趣,可以加入更多的java.lang.Math中的數學函數,
//當然,你也可以加入自己實現的一些方法,如返回前一個運算結果
//記錄歷史信息等等。
}
演示計算過程,如:
[juntao@juntao CalcParser]$ java CalcParser '${sin(1/2)*cos(1/4) + 12.3}'
12.418611776418413
[juntao@juntao CalcParser]$ java CalcParser '${(12+45)*(3-23)}'
-1140.0
[juntao@juntao CalcParser]$ java CalcParser '${3e-5}'
3.0E-5
[juntao@juntao CalcParser]$ java CalcParser '${sin(3/4)/2}'
0.34081938001166706
關于JJTree后邊再說吧,最重要的還是上邊提到的形式語言,它是一切的基礎。
這篇文章可以算是這篇小型桌面計算器的實現(javacc)的續。
可以這么說,使用javacc作分析器生成器,如果沒有用到jjTree,那么就是對語義分析的過程理解不夠深入。如果用到了jjTree而且用好了,那么對編譯原理,BNF等的理解才算是比較到位了。
jjTree中最重要的概念是Node接口,所有的非終結符都可以規約為一個節點。這個節點一般來講是實現了Node接口的節點類
其中主要有這樣幾個方法:
Java代碼
……
/**Thismethodtellsthenodetoadditsargumenttothenode's
listofchildren.*/
publicvoidjjtAddChild(Noden,inti);
/**返回孩子節點,下標從0開始,從左到右*/
publicNodejjtGetChild(inti);
/**返回字節點個數*/
intjjtGetNumChildren();
……
在本文所舉的例子中,每個節點還要實現這樣一個接口中聲明的方法:
Java代碼
voidinterpret()
這個方法中為每個節點具體的計算過程。
jjTree處理的腳本(jjTree規則列表)以jjt結尾,這個文件通過jjtree工具可以生成.jj文件,然后用javacc編譯.jj文件即可生成分析器生成器的java代碼,然后于一些輔助解析的類進行編譯,從而最終完成整個腳本引擎。
jjTree有什么用?
這個是最核心的問題了,我們都知道jjTree的作用是為了將終結符通過規則規約成非終結符節點。但是,規約的目的又是什么呢?其實,如果你的目標是簡單的行解析器的話,根本不需要jjTree。但是,如果需要做一些比較有規模的腳本解析器,比如支持if,while等代碼塊的話,就需要解析器將這些臨時的狀態記錄下來,那就必須用到jjTree了。
加入了語法樹之后,解析器就需要做出一些修改了,比如需要加入全局的符號表,堆棧等數據結構,以方便規約出非終結符后再做動作時可以取出這些數據。當然,如果你的腳本引擎支持IO的話,這些全局的流描述符也應該和符號表,堆棧放在一起,比如一個單獨的類中。
涉及到使用jjTree的項目,即使是演示目的的,一般也比較大,所以,這篇文章中給出的都是一些片段,如果需要,我可以在blog中做一個小系列來說。
比如,看一個例子:
Java代碼
/**Conditionalorexpression.*/
voidConditionalOrExpression()#void:
{}
{
ConditionalAndExpression()
("||"ConditionalAndExpression()#OrNode(2))*
}
上邊這段中 #void意思為當遇到ConditionalOrExpression規則時,不生成節點(不規約),而#OrNode(2)則表示如果發現有恰好2個ContionalAndExpression()規則,則規約到一個OrNode節點(需要在外部寫一個ASTOrNode類)。
而在ASTOrNode類中,會有下面的動作定義
Java代碼
publicvoidinterpret()
{
jjtGetChild(0).interpret();
if(((Boolean)stack[top]).booleanValue())
{
stack[top]=newBoolean(true);
return;
}
jjtGetChild(1).interpret();
stack[--top]=newBoolean(((Boolean)stack[top]).booleanValue()||
((Boolean)stack[top+1]).booleanValue());
}
在語法解析時,當規約出非終結符后,設置ASTOrNode父類的children的數組,然后在ASTOrNode先取出這個數組中的第一個Node進行
遞歸解析,完成后取出第二個Node進行解析,最后,將這兩個解析后的結果進行bool的或運算。整個過程很自然,計算過程放在外部的單獨
的類中進行。
再看一個例子:
Java代碼
/**Ablock.*/
voidBlock():
{}
{
"{"(Statement())*"}"
}
用花括號括起來的一些代碼表示一個塊,怎么解析這個快呢?在ASTBlock中,有這樣的運算過程:
Java代碼
publicvoidinterpret()
{
inti,k=jjtGetNumChildren();//取出代碼塊中的代碼條數
for(i=0;i<k;i++)
jjtGetChild(i).interpret();//遞歸解析
}
取出代碼塊中的代碼條數,然后依次執行,如果遇到其他的Node,則遞歸調用這個Node上的interpret過程,從而執行整個代碼塊。
希望這篇文章可以說明jjTree的運行機制,但是由于規模所限,有些地方可能還是不太清晰。歡迎留言,我會盡快在blog上寫一個小的系列,謝謝!
解釋器(Interpreter)模式:
解釋器模式是類的行為模式。給定一個語言之后,解釋器模式可以定義出其文法的一種表示,并同時提供一個解釋器。客戶端可以使用這個解釋器
來解釋這個語言中的句子。
一、解釋器模式所涉及的角色
1、抽象表達式角色:聲明一個所有的具體表達式角色都需要實現的抽象接口。這個接口主要是一個interpret()方法,稱做解釋操作。
2、終結符表達式角色:這是一個具體角色。
(1)實現了抽象表達式角色所要求的接口,主要是一個interpret()方法;
(2)文法中的每一個終結符都有一個具體終結表達式與之相對應。
3、非終結符表達式角色:這是一個具體角色。
(1)文法中的每一條規則 R=R1R2.....Rn 都需要一個具體的非終結符表達式類;
(2)對每一個 R1R2.....Rn 中的符號都持有一個靜態類型為Expression的實例變量。
(3)實現解釋操作,即 interpret()方法。解釋操作以遞歸方式調用上面所提到的代表 R1R2.....Rn 中的各個符號的實
例變量
4、客戶端角色:代表模式的客戶端它有以下功能
(1)建造一個抽象語法樹(AST或者Abstract Syntax Tree)
(2)調用解釋操作interpret()。
5、環境角色:(在一般情況下,模式還需要一個環境角色)提供解釋器之外的一些全局信息,比如變量的真實量值等。
(抽象語法樹的每一個節點都代表一個語句,而在每一個節點上都可以執行解釋方法。這個解釋方法的執行就代表這個語句被解釋。
由于每一個語句都代表對一個問題實例的解答。)
Java代碼
//抽象角色Expression
/*
這個抽象類代表終結類和非終結類的抽象化
其中終結類和非終結類來自下面的文法
Expression::=
ExpressionANDExpression
|ExpressionORExpression
|NOTExpression
|Variable
|Constant
Variable::=....//可以打印出的非空白字符串
Constant::="true"|"false"
*/
publicabstractclassExpression{
//以環境類為準,本方法解釋給定的任何一個表達式
publicabstractbooleaninterpret(Contextctx);
//檢驗兩個表達式在結構上是否相同
publicabstractbooleanequals(Objecto);
//返回表達式的hashcode
publicabstractinthashCode();
//將表達式轉換成字符串
publicabstractStringtoString();
}
publicclassConstantextendsExpression{
privatebooleanvalue;
publicConstant(booleanvalue){
this.value=value;
}
//解釋操作
publicbooleaninterpret(Contextctx){
returnvalue;
}
//檢驗兩個表達式在結構上是否相同
publicbooleanequals(Objecto){
if(o!=null&&oinstanceofConstant){
returnthis.value==((Constant)o).value;
}
returnfalse;
}
//返回表達式的hashcode
publicinthashCode(){
return(this.toString()).hashCode();
}
//將表達式轉換成字符串
publicStringtoString(){
returnnewBoolean(value).toString();
}
}
publicclassVariableextendsExpression{
privateStringname;
publicVariable(Stringname){
this.name=name;
}
publicbooleaninterpret(Contextctx){
returnctx.lookup(this);
}
publicbooleanequals(Objecto){
if(o!=null&&oinstanceofVariable){
returnthis.name.equals(((Variable)o).name);
}
returnfalse;
}
publicinthashCode(){
return(this.toString()).hashCode();
}
publicStringtoString(){
returnname;
}
}
publicclassAndextendsExpression{
privateExpressionleft,right;
publicAnd(Expressionleft,Expressionright){
this.left=left;
this.right=right;
}
publicbooleaninterpret(Contextctx){
returnleft.interpret(ctx)&&
right.interpret(ctx);
}
publicbooleanequlas(Objecto){
if(o!=null&&oinstanceofAnd){
returnthis.left.equals(((And)o).left)&&
this.right.equals(((And)o).right);
}
returnfalse;
}
publicinthashCode(){
return(this.toString()).hashCode();
}
publicStringtoString(){
return"("+left.toString()+"AND"+right.toString()+")";
}
}
publicclassOrextendsExpression{
privateExpressionleft,right;
publicOr(Expressionleft,Expressionright){
this.left=left;
this.right=right;
}
publicbooleaninterpret(Contextctx){
returnleft.interpret(ctx)||right.interpret(ctx);
}
publicbooleanequals(Objecto){
if(o!=null&&oinstanceofOr){
returnthis.left.equals(((And)o).left)&&
this.right.equals(((And)o).right);
}
returnfalse;
}
publicinthashCode(){
return(this.toString()).hashCode();
}
publicStringtoString(){
return"("+left.toString()+"OR"+right.toString()+")";
}
}
publicclassNotextendsExpression{
privateExpressionexp;
publicNot(Expressionexp){
this.exp=exp;
}
publicbooleaninterpret(Contextctx){
return!exp.interpret(ctx);
}
publicbooleanequals(Objecto){
if(o!=null&&oinstanceofNot){
returnthis.exp.equals(((Not)o).exp);
}
returnfalse;
}
publicinthashCode(){
return(this.toString()).hashCode();
}
publicStringtoString(){
return"(NOT"+exp.toString()+")";
}
}
importjava.util.HashMap;
publicclassContext{
privateHashMapmap=newHashMap();
publicvoidassign(Variablevar,booleanvalue){
map.put(var,newBoolean(value));
}
publicbooleanlookup(Variablevar)throwsIllegalArgumentException{
Booleanvalue=(Boolean)map.get(var);
if(value==null){
thrownewIllegalArgumentException();
}
returnvalue.booleanValue();
}
}
//客戶端
publicclassClient{
privatestaticContextctx;
privatestaticExpressionexp;
publicstaticvoidmain(Stringargs[]){
ctx=newContext();
Variablex=newVariable("x");
Variabley=newVariable("y");
Constantc=newConstant(true);
ctx.assign(x,false);
ctx.assign(y,true);
exp=newOr(newAnd(c,x),newAnd(y,newNot(x)));
System.out.println("x="+x.interpret(ctx));
System.out.println("y="+y.interpret(ctx));
System.out.println(exp.toString()+"="+exp.interpret(ctx));
}
}
二、解釋器模式適用于以下的情況:
(1)系統有一個簡單的語言可供解釋
(2)一些重復發生的問題可以用這種簡單的語言表達
(3)效率不是主要的考慮。
?如果你喜歡本文, 請長按二維碼,關注公眾號 分布式編程.
作者:分布式編程
出處:https://zthinker.com/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
總結
以上是生活随笔為你收集整理的小型桌面计算器的实现(javacc)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jar命令|jdt的简单使用
- 下一篇: 怎么创建具有真实纹理的CG场景岩石?