第三章 詞法分析
本章講述ANTLR的詞法分析部分。詞法分析是編譯過程的第一步,是編譯過程的基礎。詞法分析除了上一章講過它爲語法分析提拱記號流,濾掉編譯過程不關心的內容以外,還有一個重要的作用是有了詞法分析可以大大提高編譯的效率。可能有人曾有過疑問,爲什麼一定要有詞法分析?詞法分析和語法分析的關係與其它編譯過程有些不同,如:語義分析,生成代碼在編譯過程中是獨立的步驟與其它步驟有明顯的區別。而詞法分析和語法分析在形式上很相似,都要用文法規則去定義語言的結構,爲什麼不統一起來呢?可以想象一下如果把詞法分析和語法分析合併會有什麼不同,也就是說我們直接對源代碼做語法分析。如C#源代碼中當我們遇到一個字符“c”這時它可能會是關鍵字“class”標識符“c1”等等。如果是class關鍵字那麼接下是要分析一個類代碼,如果是標識符那要看它的具體上下文而定。這樣的話與一個字符“c”有可能對應的情況太多了,分析起來效率會很低。所以要先做一遍詞法分析把源程序的基礎組成單位先分析出來。詞法分析是語法分析的一個緩衝,可以大大提高編譯效率。
3.1詞法分析的規定
在ANTLR中詞法分析定義的規則名必須以大寫字母開頭如“LETTER”,“NewLine”。我們在第一章示例中的詞法分析部分與語法分析部分合寫到一個文件中,ANTLR允許把詞法分析部分和語法分析部分分別寫到兩個文件中。
T.g文件存放語法定義:
grammar T;
Options {tokenVocab = T2;}
a : B*;
T2.g文件存放詞法定義:
lexer grammar T2;
B : ‘b’;
將詞法分析放到單獨的文件中時文法的名稱也要和文件的名稱相同,在grammar關鍵字之前要加入lexer關鍵字。上例中的T.g文件生成語法分析類TParser,T2.g文件生成詞法分析類T2Lexer。在T.g中要加入一個設置項tokenVocab來指定語法文件所需的詞法單詞是來自T2.g。這樣就可以按照第一章示例中的方法編譯運行分析器了。
3.2字符編碼定義
詞法分析與源代碼直接接觸,因爲源代碼是由字符串組成的,所以我們需要定義字符。ANTLR有兩種方法定義字符,第一種方法是ANTLR可以直接使用字符本身很簡單直觀的定義。
CHAR : ‘a’ | ‘b’ | ‘c’;
但這種定義只限於ASCII碼的字符,下面的定義是不合法的。
CHAR : ‘代碼’;
定義漢字這樣除ASCII碼以外的字符只能用第二種方法十六進制編碼定義法。使用“\u”開頭加四位十六進制數定義來定義一個字符。
CHAR : ‘\u0040’;
C#中使用String.Format("{0:x} {1:x}", Convert.ToInt32('代'), Convert.ToInt32('碼'));可以獲得漢字的編碼。如上面的CHAR : ‘代碼’;我們可以定義爲:
CHAR : '\u4ee3' '\u7801';
編碼有很多種GB2312的編碼範圍是A1A1 ~ FEFE,去掉未定義的區域之後可以理解爲實際編碼範圍是A1A1 ~ F7FE。GBK的整體編碼範圍是爲8140 ~ FEFE。 BIG5字符編碼範圍是A140 ~ F97E。在ANTLR詞法規則中定義字符時用16進制編碼就可以了不用標識出編碼的類型。
字符集 |
編碼範圍 |
GB2312 |
A1A1 ~ 7E7E |
GBK |
8140 ~ FEFE |
BIG5 |
A140 ~ F97E |
Unicode |
000000 ~ 10FFFF |
UTF-8 |
000000 ~ 10FFFF |
其中漢字在GB2312中的編碼範圍爲:‘\uB0A1' .. '\uF7FE',漢字在Unicode編碼範圍爲:’\u4E00’.. ‘\u9FA5’ | ‘\uF900’ .. ‘\uFA2D’。
附加說明:
1. GBK (GB2312/GB18030)
x00-xff GBK雙字節編碼範圍
x20-x7fASCII
xa1-xff 中文
x80-xff 中文
2. UTF-8 (Unicode)
x3130-x318F(韓文)
xAC00-xD7A3 (韓文)
u0800-u4e00 (日文)
ps: 韓文是大於[u9fa5]的字符
3.3終結符定義方法
LETTER : ‘A’ | ‘B’ | ‘C’ | ‘D’ | ‘E’ | ‘F’ | ‘G’ | ‘H’ | ‘I’ | ‘J’ | ‘K’ | ‘L’ | ‘M’ | ‘N’ | ‘O’ | ‘P’ | ‘Q’ | ‘R’ | ‘S’ | ‘T’ | ‘U’ | ‘V’ | ‘W’ | ‘X’ | ‘Y’ | ‘Z’ | ‘a’ | ‘b’ | ‘c’ | ‘d’ | ‘e’ | ‘f’ | ‘g’ | ‘h’ | ‘i’ | ‘j’ | ‘k’ | ‘l’ | ‘m’ | ‘n’ | ‘o’ | ‘p’ | ‘q’ | ‘r’ | ‘s’ | ‘t’ | ‘u’ | ‘v’ | ‘w’ | ‘x’ | ‘y’ | ‘z’;
“..”符號,從上面的LETTER示例可以看出,定義一個表示英文字母的符號寫起來非常繁瑣。爲了使定義變得簡單ANTLR加入“..”符號通過指定首尾的字符可以很方便的定義一定範圍內的字符。
LETTER : ‘A’ .. ‘Z’ | ‘a’ .. ‘z’;
“~”符號,如果我們想表示除某些符號以外的符號時,可以使用“~”符號。“~”代表取反的意思。
A : ~ ‘B’;
符號A匹配除字符“B”以外的所有字符。
A : ~ (‘A’ | ‘B’); B : ~(‘A’ .. ‘B’); C: ~‘\u00FF';
這個的例子中定義三個符號。符號A匹配除字符“A”和“B”以外的所有字符,符號B匹配除大寫字符母以外的所有字符。符號C匹配除編碼爲“u00FF”的字符以外的所有字符。
“.”符號,ANTLR中可以用“.”表示單個任意字符,起通配符的作用。
A : .; B : .*; C :.* ‘C’; D : ~ .;//error
這個例子中符號A匹配一個任意字符,符號B符號匹配0到多個任意字符,符號C匹配0到多個任意字符直到遇到字符“C”爲止。D的定義是錯誤的,不能定義任意字符以外的字符。
3.4 skip()方法
有些字符是不屬於源程序範疇內的,這些字符在分析過程中應該忽略掉。在ANTLR中可以在詞法定義中加入skip();,(如果是C#爲目標語言爲Skip();)。在規則的定義的之後與表示定義結束的分號之前加入“{skip();}”。例如下面定義了一個跳過空白的詞法定義。
WS : ( ' ' | '\t' | '\n' | '\r' ) + {skip();} ;
空白符號WS中有空格符、製表符、回車和換行符,當遇到這些字符時詞法分析程序會調用skip()方法跳過這些字符,在語法分析時就可以不考慮這些空白字符的存在了。下面再看另一個skip()的例子。
B : 'A' | 'B' {Skip();} | 'C' ;
這個例子中符號B只在匹配字符“B”時跳過,從這個例子可以看出{Skip();}要寫在忽略內容的後面,如果它處於某選擇分支中那麼它只對某分支起作用。下面我們定義一些實際中經常出現的詞法定義。
INT : DIGIT+;
DIGIT : ‘0’ .. ‘9’;
INT 定義了整型數,整型數是由1個或多0到9的數字組成的。下面我們來定義浮點數,浮點數的整數部分至少要有一位數字,小數部分是可有可無的,如要有小數部分則至少要有1位小數位。
FLOAT : DIGIT+ (‘.’ DIGIT+)?;
在編程語言中註釋是必不可少的,各種編程語言都有它的註釋方法。下面看java語言中註釋的定義。
COMMENT : '/*' . * '*/' {skip();} ;
LINE_COMMENT : '//' ~ ('\n' | '\r') * '\r'? '\n' {skip();} ;
/* */ 中和 //後代表的註釋部分應該被忽略掉,下面我們給出完全的示例並運行它。
grammar T;
options {
language=CSharp;
output=AST;
}
a : INT;
INT : DIGIT+;
DIGIT : ‘0’ .. ‘9’;
COMMENT : '/*' . * '*/' {Skip();} ;
LINE_COMMENT : '//' ~ ('\n' | '\r') * '\r'? '\n' {Skip();} ;
WS : ( ' ' | '\t' | '\n' | '\r' ) + {Skip();} ;
文法中的COMMENT、LINE_COMMENT和WS規則只是爲了濾掉相應內容,沒有必要與語法規則關聯,這樣它們也可以正確的工作。COMMENT符號使用了“.”符號來匹配一切字符直到匹配“/”字符爲止。LINE_COMMENT從“//”字符開始匹配除了“\r\n”以外的所有字符直到匹配“\r\n”爲止。其中回車換行符中可以沒有回車符所以“\r”是可選項。
將如下源代碼保存到與exe同目錄的f1.txt文件中。
/* 這是
註釋 */
234//這是註釋
/* 這是
註釋 */
下面是運行分析器的代碼。
TestSkipLexer lex = new TestSkipLexer(new ANTLRFileStream("f1.txt"));
CommonTokenStream tokenStream = new CommonTokenStream(lex);
TestSkipParser parser = new TestSkipParser(tokenStream);
TestSkipParser.a_return aReturn = parser.a();
在Visual Studio.NET 2005中單步執行上面幾行代碼,當執行到TestSkipParser.a_return aReturn = parser.a();時用內存監視查看aReturn.Tree可以看到語法樹根節點只有234一個子節點,這說明所有空白和註釋的內容都被忽略了,沒出現在語法樹中。
3.5 $channel = HIDDEN
有時象註釋這樣的部分在編譯時並不是完全放棄的,如java中在/**….*/的註釋形式中的內容可以添加到最終生成程序的文檔中,在.NET中使用 /// 註釋形式也有類似的功能。也就是說我們在編譯代碼時忽略這些註釋,而在分析生成文檔時又要用到這些註釋。這樣的話就不能使用skip();方法了,skip()方法是拋棄其內容。所以ANTLR中加入了channel屬性用來指定當前匹配的內容是屬於哪一個頻道,分析過程中關心哪個頻道就可以只接收哪個頻道的內容。ANTLR中定義有兩個頻道常量DEFAULT_CHANNEL和HIDDEN_CHANNEL,一般情況下匹配的內容都中放到DEFAULT_CHANNEL中。我們可以指定讓當前匹配的內容放到哪個頻道中。
前面示例中的詞法規則可以改成如下形式。
COMMENT : '/*' . * '*/' {$channel=HIDDEN;} ;
LINE_COMMENT : '//' ~ ('\n' | '\r') * '\r'? '\n' {$channel=HIDDEN;} ;
WS : ( ' ' | '\t' | '\n' | '\r' ) + {$channel=HIDDEN;};
COMMENT、LINE_COMMENT和WS詞法規則後面加入{$channel=HIDDEN;},這樣匹配的字符就會被存放到HIDDEN頻道中。下面將這個修改後的文法編譯運行,其結果和使用skip()方法時是一樣的,註釋部分沒有出現在語法樹中。這就是說存在於HIDDEN頻道中的內容被語法分析器忽略了,因爲在默認情況下ANTLR語法分析程序只分析DEFAULT_CHANNEL中的內容。
Channel這個概念是介於詞法分析和語法分析之間的一個概念,將存放匹配的內容存放到各個頻道中是在詞法分析時進行的,而語法分析時可以有選擇的讀某一個頻道中的內容。那麼如何讓語法分析程序處理示例中的註釋內容呢?我們再次修改文法因爲我們只關心註釋所以把WS : ( ' ' | '\t' | '\n' | '\r' ) + {Skip();};改回成skip()方法因爲空白我們是不關心的,下面給出全部的文法。
grammar TestHidden;
options {
language=CSharp;
output=AST;
}
a : b INT b;
b : (COMMENT | LINE_COMMENT)*;
INT : DIGIT+;
DIGIT : '0' .. '9';
COMMENT : '/*' . * '*/' {$channel=HIDDEN;} ;
LINE_COMMENT : '//' ~ ('\n' | '\r') * '\r'? '\n' {$channel=HIDDEN;} ;
WS : ( ' ' | '\t' | '\n' | '\r' ) + {Skip();} ;
下面是運行代碼,運行代碼中加入了tokenStream.SetTokenTypeChannel方法。
TestSkipLexer lex = new TestSkipLexer(new ANTLRFileStream("f1.txt"));
CommonTokenStream tokenStream = new CommonTokenStream(lex);
TestSkipParser parser = new TestSkipParser(tokenStream);
tokenStream.SetTokenTypeChannel(TestSkipLexer.COMMENT, Token.DEFAULT_CHANNEL);
tokenStream.SetTokenTypeChannel(TestSkipLexer.LINE_COMMENT, Token.DEFAULT_CHANNEL);
TestSkipParser.a_return bReturn = parser.a();
/* 這是 註釋 */ |
Nil(根節點) |
/* 這是 註釋 */ |
234 |
//這是註釋 |
SetTokenTypeChannel方法是將指定的頻道中的符號加入到分析程序關心的範圍內,第一個參數是詞法符號,第二個參數是這個符號所處頻道。兩條SetTokenTypeChannel語句將DEFAULT_CHANNEL 頻道中的COMMENT、LINE_COMMENT加入到語法分析程序channelOverrideMap集合中,語法分析程序會分析channelOverrideMap集中註冊的類型,此示例運行後的語法樹有四個子節點。這說明分析程序分析了註釋的內容並將其加入到了語法樹中。
3.6 options {greedy=false;}
ANTLR詞法分析中還可以加入greedy設置項,當greedy=true時當前規則會儘可能的匹配可以匹配的輸入。ANTLR中默認情況greedy爲true,所以COMMENT : '/*' . * '*/' {$channel=HIDDEN;} ;規則中符號“.”可以匹配“*/”字符,也就是說當遇到“*/”字符時它是匹配“.”還是匹配’*/’出現了二義的情況,前面的例子中已經顯示出在這種情況下分析器是可以正確分析的,但加入greedy=false後可以消除這種二義性,就是說greedy=false時分析器遇到“*/”字符時會作爲註釋結束符號來匹配,這樣的話就消除了二義性。
COMMENT : '/*' ( options {greedy=false;} : . ) * '*/' {$channel=HIDDEN;} ;
options {greedy=false;} : 是一種局部設置的寫法,其格式爲:( options {«option-assignmen ts»} : «subrule-alternatives» )。關鍵字options後面用“{}”將設置項括起來,使用“:”符號表示對後面部分進行設置,這種設置的方法必須在子規則中設置它只能在子規則中有效。如:'/*' options {greedy=false;} : . * '*/' 這樣的定義是不無法的。
下面給出一個更加明顯的例子。
grammar TestGreedy;
options {
language=CSharp;
output=AST;
}
c : A B;
A : 'a' 'b' ?;
B : 'b';
此例子中c規則有A和B兩個詞法符號,其中A匹配的是“a”或“ab”字符,B匹配的是“b”字符。這時會出現一個問題就是當輸入爲“ab”時這個“b”是規則A的b還是規則B的b,這屬於二義性問題。此示例在實際運行時如果輸入爲“abb”時可以正確生成有兩個子節點的語法樹。
ab |
Nil(根節點) |
b |
如果輸入爲“ab”會出現MismatchedTokenException異常,因爲現在分析器的greedy屬性爲true。這時分析器會儘可能的匹配輸入的字符“ab”被規則A匹配,這時規則B就沒有了可匹配的字符,所以出現了MismatchedTokenException異常。儘可能匹配的意思是所有的可選項分析器都認爲是需要的。
如果我們將規則A中可選的’b’定義爲greedy=false;代碼如下。
c : A B;
A : 'a' (options {greedy=false;} : 'b')?;
B : 'b';
當輸入“ab”和“abb”時生成的語法樹同樣爲:
a |
Nil(根節點) |
b |
規則A中的’b’由於是可選項並且其後規則B中也需要匹配’b’字符所以在Greedy屬性爲false時規則A不去匹配字符’b’。這樣輸入的字符串中第一個’b’字符與規則B匹配,第二個’b’字符無處匹配。
3.7“.”相關注意
詞法定義中有一些與通配符“.”有關的注意事項,下面給出一個示例。
d : C ANY PLUS;
C : 'c';
PLUS : '+';
ANY : ( options {greedy=false;} : . ) *;
這個文法是要匹配輸入字符“c”和“+”,字符“c”與“+”之間可以是一些不確定的內容所以用通配符“.”進行匹配,如前面學到的應該使用greedy=false設置。但是此文法運行時會出現死循環情況。我們可以使用其它方法改寫定義,由於字符“c”與“+”之間不應該出現“c”與“+”字符以防出現二義性,所以ANY規則可以修改成以下的定義方法。
ANY : ~(C | PLUS)*;
不過如何正確使用“.”通配符呢?在使用通配符定義規則時要在當前的規則中明確的指定開始和結束字符。前面的示例中定義/**/註釋時就是指定了開始結束字符。本示例只能改成:
d : CANYPLUS;
CANYPLUS : ‘c’ ( options {greedy=false;} : . ) * ‘+’;
所以“.”的使用範圍是有限的,請大家注意。
3.8 fragment詞法規則
ANTLR文法中語法規則是在詞法規則基礎上建立的。但不一定每個詞法規則都會被語法規則直接引用到。這就象一個類的公有成員和私有成員,公有成員是對外公開的會被外界直接調用。而私有成員不對外公開只能通達公有成員間接調用。在詞法規則中那些不會被語法規則直接調用的詞法規則可以用一個fragment關鍵字來標識,fragment標識的規則只被其它詞法規則引用。
grammar TestFragment;
options {
language=CSharp;
output=AST;
}
a : INT;
INT : DIGIT+;
fragment DIGIT : '0' .. '9';
此示例中詞法符號INT定義了整型數,而DIGIT定義了一位數字。語法規則a對DIGIT規則的使用是間接的,這時使用fragment來標識DIGIT規則,DIGIT規則將不能被語法規則直接調用。如果如下例語法規則a直接調用DIGIT規則,運行時語法分析器會死循環。
a : DIGIT;
INT : DIGIT+;
fragment DIGIT : '0' .. '9';
在不使用fragment關鍵字的時候,有時語法規則也不能直接使用某些詞法規則。請看下面的示例。
a : DIGIT;
INT : DIGIT+;
DIGIT : '0' .. '9';
運行後輸入“9”分析器生成的語法樹爲空,使用java命令行運行時提示“line 1:0 mismatched input '0' expecting DIGIT ”。這是因爲DIGIT規則匹配的內容對INT規則來說也是匹配的,INT規則干擾了DIGIT規則的匹配,造成DIGIT規則沒有匹配的內容。
a : DIGIT;
fragment INT : DIGIT+;
DIGIT : '0' .. '9';
如果在INT前加入fragment則分析器可以生成有一個節點“9” 的語法樹,不過在實際中不應該這樣定義。在第五章我們會學到fragment詞法規則是可以帶參數的,這樣可以在詞法分析的過程中靈活的控制,fragment詞法規則具有更大的意義。
3.9 filter=true
我們在分析源代碼時有時只想獲得其中一部分信息,例如:我們想知道一個java文件中的類的類名,類有哪些方法,方法的參數和返回值及其類型,屬性及其類型和此類繼承了什麼類。但是我們必須寫出java的全部文法纔可以分析java文件,這很有難度也沒有必要。ANTLR中加入了一個叫filter的設置項,filter爲布爾類型默認爲false。filter爲true時詞法分析器可以處於一種過濾狀態,我們只需定義出我們關心部分的詞法結構,其它部分全部被忽略掉。
filter=true模式中的詞法規則的先後順序很重要,寫在最前面的詞法規則優先級最高之後從高到低依次排列。這樣規定是因爲有些規則會被其它規則所包含造成一些規則無法被識別出來。請看下面的示例:
lexer grammar TestFilter;
options {
filter=true;
language=CSharp;
}
A : aText=AA{Console.Out.WriteLine("A: "+$aText.Text);};
B : bText=BB{Console.Out.WriteLine("B: "+$bText.Text);};
AA : 'ab';
BB : 'a';
執行代碼爲:
ICharStream input = new ANTLRFileStream("t.txt");
FuzzyJava2Lexer lex = new FuzzyJava2Lexer(input);
ITokenStream tokens = new CommonTokenStream(lex);
tokens.ToString();
此文法中規則AA匹配字符串“ab”,規則BB匹配字符串“a”,規則AA包含規則BB。t.txt文件中的內容爲“xxxxxxabxxxxx”。我們看一看是內容規則A匹配優先還是規則B優先匹配,這可以反應出優先關係。這個例中輸出爲:“A : ab”。這說明規則A優先匹配了。(aText和bText叫例變量它們可以保存把AA和BB符號的內容,{ Console.Out…… }是嵌入在文法中的代碼用於輸出AA和BB中的內容。這會在第五章中講述。)下面我們修改一下文法改變一個A和B規則的位置:
B : bText=BB{Console.Out.WriteLine("B: "+$bText.Text);};
A : aText=AA{Console.Out.WriteLine("A: "+$aText.Text);};
BB : 'a';
AA : 'ab';
相同的輸入這次輸出爲:B : a。這說明規則B優先匹配了。但是如果將規則BB改爲匹配字符“b”則輸出爲“A : ab”。
B : bText=BB{Console.Out.WriteLine("B: "+$bText.Text);};
A : aText=AA{Console.Out.WriteLine("A: "+$aText.Text);}; //輸出爲 A : ab
BB : 'b';
AA : 'ab';
下面給出一個從java文件中讀取類,方法,屬性信息的示例。
lexer grammar FuzzyJava2;
options {
filter=true;
}
IMPORT : 'import' WS name=QIDStar WS? ';'
{Console.Out.WriteLine("Import: "+$name.Text);};
PACKAGE : 'package' WS name=QID WS?';'
{Console.Out.WriteLine("Package: "+$name.Text);};
CLASS : 'class' WS name=ID WS? ('extends' WS QID WS?)?
('implements' WS QID WS? (',' WS? QID WS?)*)? '{'
{Console.Out.WriteLine("Class: "+$name.Text);};
METHOD : type1=TYPE WS name=ID WS?
'(' ( ARG WS? (',' WS? ARG WS?)* )? ')' WS?
('throws' WS QID WS? (',' WS? QID WS?)*)? '{'
{Console.Out.WriteLine("Method: "+type1.Text+" "+$name.Text + "()");};
FIELD : defvisi=DEFVISI WS TYPE WS name=ID '[]'? WS? (';'|'=')
{Console.Out.WriteLine("Field:"+$defvisi.Text+" "+$name.Text+";");};
DEFVISI : 'public' | 'protected' | 'private';
USECOMMENT : '/**' (options {greedy=false;} : . )* '*/' ;
COMMENT : '/*' (options {greedy=false;} : . )* '*/'
//{Console.Out.WriteLine("found comment "+Text);};
SL_COMMENT : '//' (options {greedy=false;} : . )* '\r'?'\n' ;
WS : (' '|'\t'|'\r'|'\n' )+ ;
STRING : '"' (options {greedy=false;}: ESC | .)* '"';
CHAR : '\'' (options {greedy=false;}: ESC | .)* '\'';
fragment QID : ID ('.' ID)*;
fragment QIDStar : ID ('.' ID)* '.*'? ;
fragment TYPE : QID '[]'?;
fragment ARG : type=TYPE WS name=ID
{Console.Out.WriteLine("Param: "+$type.Text + " "+$name.Text + " , ");};
fragment ID : ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'_'|'0'..'9')*;
fragment ESC : '\\' ('"'|'\''|'\\');
注意如果我們定義一個詞法規則是由其它詞法規則組成的,如: NODE : STRING | QID | NUM;這樣寫ANTLR會提示下面的錯誤信息“STRING,NUM不可能被匹配,因爲它們之前的規則已經匹配了同樣的內容”而無法生成分析器代碼。這種情況我們應該把Node改爲語法規則寫成:node : STRING | QID | NUM;或將STRING、QID和NUM改成fragment詞法規則就可以了。
我們編譯運行這個分析器來分析下面的java源代碼。
/* [The "BSD licence"] Copyright (c) 2005-2006 Terence Parr
All rights reserved.*/
package org.antlr.analysis;
import org.antlr.misc.IntSet;
import org.antlr.misc.OrderedHashSet;
import org.antlr.misc.Utils;
import org.antlr.tool.Grammar;
import java.util.*;
public class DFAState extends State {
public DFA dfa;
public DFAState(DFA dfa) {
this.dfa = dfa;
}
public int addTransition(DFAState target, Label label) {
transitions.add( new Transition(label, target) );
return transitions.size()-1;
}
/** Print all NFA states plus what alts they predict */
public int getLookaheadDepth() {
return k;
}
public void setLookaheadDepth(int k) {
this.k = k;
if ( k > dfa.max_k ) { // track max k for entire DFA
dfa.max_k = k;
}
}
}
將上面的java程序分析後分析器會輸出下面的結果。
Package: org.antlr.analysis
Import: org.antlr.misc.IntSet
Import: org.antlr.misc.OrderedHashSet
Import: org.antlr.misc.Utils
Import: org.antlr.tool.Grammar
Import: java.util.*
Class: DFAState
Field:public dfa;
Param: DFA dfa ,
Method: public DFAState()
Param: DFAState target ,
Param: Label label ,
Method: int addTransition()
Method: int getLookaheadDepth()
Param: int k ,
Method: void setLookaheadDepth()
3.10 詞法規則的先後順序
前面已經提到了關於詞法規則先後順序的問題,這裏我們再舉一個具體的例子來進一步說明。請看下面兩個詞法規則:
CHINESECHAR : ('A'..'Z' | 'a'..'z'| '0'..'9' | '(' | ')'
| '\u4E00' .. '\u9FA5' | '\uF900' .. '\uFA2D') +;
NUM : '0'..'9'+;
CHINESECHAR規則可以接受中文字符(不包括全角標點符號),而NUM規則可以接受正整數。包含這兩個規則的文法生成分析器時ANTLR會提示錯誤信息:
由於在詞法分析的過程中如果遇到兩個規則都可以匹配當前字符串時,ANTLR會按文法中詞法規則書寫的先後順序來決定優先使用哪個規則來匹配。本例中CHINESECHAR包含'0'..'9'並且CHINESECHAR寫在NUM之前,這樣遇到數字也會用CHINESECHAR規則來匹配NUM規則就不可能匹配輸入了。解決辦法是把NUM規則放在CHINESECHAR規則之前。
3.11常用詞法規則
很多情況下我們需要定義的詞法規則都是相似的,下面給出一組常用的詞法規則定義:
grammar Abstract;
NAME :
(LETTER | UNDERLINE | CHINESECHAR)
(LETTER | UNDERLINE | DIGIT | CHINESECHAR)* ;
LETTER : ('A'..'Z' | 'a'..'z');
CHINESECHAR : '\u4E00' .. '\u9FA5' | '\uF900' .. '\uFA2D';
INT : DIGIT+;
DIGIT : '0' .. '9';
COLON : ':' ;
COMMA : ',' ;
SEMICOLON : ';' ;
LPAREN : '(' ;
RPAREN : ')' ;
LSQUARE : '[' ;
RSQUARE : ']' ;
LCURLY : '{';
RCURLY : '}';
DOT : '.' ;
UNDERLINE : '_';
ASSIGNEQUAL : '=' ;
NOTEQUAL1 : '<>' ;
NOTEQUAL2 : '!=' ;
LESSTHANOREQUALTO1 : '<=' ;
LESSTHAN : '<' ;
GREATERTHANOREQUALTO1 : '>=' ;
GREATERTHAN : '>' ;
DIVIDE : '/' ;
PLUS : '+' ;
MINUS : '-' ;
STAR : '*' ;
MOD : '%' ;
AMPERSAND : '&' ;
TILDE : '~' ;
BITWISEOR : '|' ;
BITWISEXOR : '^' ;
POUND : '#';
DOLLAR : '$';
COMMENT : '/*' . * '*/' {$channel=HIDDEN;} ;
LINE_COMMENT : '//' ~ ('\n' | '\r') * '\r'? '\n' {$channel=HIDDEN;} ;
WS : ( ' ' | '\t' | '\n' | '\r' ) + {Skip();} ;
3.12大小寫敏感
ANTLR3.01中沒有大小寫是否敏感的設置項,所以只能象下面的詞法規則這樣定義大小寫不敏感的單詞。
SELECT : ('S'|'s')('E'|'e')('L'|'l')('E'|'e')('C'|'c')('T'|'t') ;
FROM : ('F'|'f')('R'|'r')('O'|'o')('M'|'m');
還有一種方法是重載輸入的Stream類的LA方法在其中將輸入的內容寫爲大小寫不敏感。
package org.antlr.runtime;
import java.io.*;
/** @author Jim Idle */
public class ANTLRNoCaseFileStream extends ANTLRFileStream {
public ANTLRNoCaseFileStream(String fileName) throws IOException {
super(fileName, null);
}
public ANTLRNoCaseFileStream(String fileName, String encoding)
throws IOException {
super(fileName, encoding);
}
public int LA(int i) {
if ( i==0 ) {
return 0; // undefined
}
if ( i<0 ) {
i++; // e.g., translate LA(-1) to use offset 0
}
if ( (p+i-1) >= n ) {
return CharStream.EOF;
}
return Character.toUpperCase(data[p+i-1]);
}
}
C#代碼如下:( 注 可是在lookahead時臨時轉換成小寫,原來輸入字符不會受到影響。)
public class CaseInsensitiveStringStream : ANTLRStringStream {
public CaseInsensitiveStringStream(char\[\] data, int numberOfActualCharsInArray) : base(data, numberOfActualCharsInArray) {}
public CaseInsensitiveStringStream() {}
public CaseInsensitiveStringStream(string input) : base(input) {}
public override int LA(int i) {
if (i == 0) {
return 0;
}
if (i < 0) {
i++;
}
if (((p + i) - 1) >= n) {
return (int) CharStreamConstants.EOF;
}
return Char.ToLowerInvariant(data\[(p + i) - 1\]);
}
}
3.11本章小結
本章講述了ANTLR如何定義詞法規則,包括:定義各種編碼的字符,通配符“.”和“..”、“~”、“.”符號的用法,skip()方法的用法,$channel = HIDDEN輸入頻道,greedy=false的用法和注意事項以及fragment詞法規則的用法。這些內容爲ANTLR中詞法分析中基礎,後面章節還會講述詞法規則中潛入代碼和詞法中的二義性的內容。學完本章後讀者應該可以定義一種語言的基本詞法規則,下一章我們講述如何在詞法規則的基礎上定義語法規則。