Wednesday, January 14, 2009

ANTLR (3) - Simple Lexer Example

下面是一個簡單的Lexer例子,用來判讀字串中的數字和英文字母。

simple.g
class SimpleLexer extends Lexer;

options { k=1; filter=true; }

ALPHA : ('a'..'z'|'A'..'Z')+
{ System.out.println("Found alpha: "+ getText()); }
;

NUMERIC : ('0'..'9')+
{ System.out.println("Found numeric: "+ getText()); }
;

EXIT : '.'
{ System.exit(0); }
;

接著我們一行一行解釋這段程式的意義。

class SimpleLexer extends Lexer;

一開始,宣告一個繼承類別Lexer的新類別-SimpleLexer。

options { k=1; filter=true; }

接著設定在此Lexer中的一些相關選項。k在這裡是有意義的,若把k值設為1,是代表在比對rule內容時,只會比對第一個字元;如果設成2,則會看前面的兩個字元後,再判斷到底字串是屬於哪個rule。假設我現在有兩條rule,並輸入一個字串"ac":
RULE1 : "ab" ;
RULE2 : "ac" ;
如果我把k設為1 (即k=1),則程式會無法判斷讀這個字串到底是符合RULE1或是RULE2,因為我們在option的設定中,告訴程式只需要看最前面的第一個字元就可以停了,啊結果問題來了,兩個rule的第一個字元都是a,啊我的字串"ab"到底是要歸誰?RULE1的ab?還是RULE2的ac啦?啊還是a錢…。因此,若要能正確判讀輸入的字串"ac",需把k設為2 (即k=2)。

無法正確判讀並不代表程式在compile的時候會失敗,他會顯示類似下面警告:

BadExample.g: warning:lexical nondeterminism between rules RULE1 and RULE2 upon
BadExample.g: k==1:'a'

他在抱怨將來在判讀文件字串時,可能會無法辨認是RULE1還是RULE2,然而程式在執行時,會以先出現的RULE為準。承上面的例子,在k=1時,程式會把字串"ac"歸給RULE1,就因為RULE1比RULE2先出現,現實就是這麼殘酷....

回來看option中第二個選項的設定:filter=true;

filter設成true,是把filter的功能打開,也就是當輸入的字串中,沒有符合任何一條rule時,會自動跳過。像這個Lexer只能用來判讀英文字母和數字,所以當我們輸入一些特殊字元時 (ex. '%', '\', '@'...etc.),程式會睜一隻眼,閉一隻眼,當作沒看到,繼續往後找是否還有符合rule的字串。

如果沒有設定filter這個選項,在一行字串中,當Lexer發現他不認識的東西時,就會停住。例如,"ABCD@$EFG",Lexer在讀完ABCD之後就被噎死了。但若設定了filter=true;你會發現Lexer讀的很開心,會把這一行文字乖乖讀完。

剛剛說明的只是Lexer的一些判讀設定,接下來進入rule的部份。既然這個例子的目的是判讀字串中的數字和英文字母,我們定義的rule當然就和「數字」、「英文字母」有關,要告訴程式,什麼是數字,什麼是英文字母。


ALPHA : ('a'..'z'|'A'..'Z')+
{ System.out.println("Found alpha: "+ getText()); }
;


這是一條叫做ALPHA的rule,目的是要讓Lexer從字串中,找出所謂的ALPHA。
('a'..'z'|'A'..'Z')+,這個括號旁的加號,是代表括號中的字元,不管出現一次,或是1次以上,都可以把它視為ALPHA。而括號中的'a'..'z'則是定義可接受的字母範圍,中間的OR邏輯運算元(|),則代表小寫或大寫的英文字母都可以接受。

例如,字串"A123BCD",在ALPHA這條rule下,他會辨識出兩個ALPHA,分別是A和BCD。在這裡,可能有點疑惑為什麼BCD這三個英文字母不會被視為三個ALPHA,這就是()+的作用-他能接受括號中的字元重複出現1次以上。如果我們把加號(+)拿掉,剛剛的"A123BCD"就會被辨識出四個ALPHA。

下一行的System.out.println("Found alpha: "+ getText());的目的是當我們發現ALPHA時,請它順便幫我把它print出來。我們需要用getText()這個ANTLR提供的方法把辨識出來的文字抓出來。


NUMERIC : ('0'..'9')+
{ System.out.println("Found numeric: "+ getText()); }
;


第二條rule叫做NUMERIC,和上面幾乎一模一樣,只是把英文字元換成數字。也就是0~9的數字不管出現一次或是一次以上,都只會被視為一個NUMERIC。例如"0123ABC9",所以這段字串在NUMERIC這條單一rule下,只會被辨識出兩個NUMERIC-0123和9。


EXIT : '.'
{ System.exit(0); }
;


最後一條rule叫EXIT。根據上面的模式,哦,只要發現字串中出現這個句點(.),那就離開程式吧(System.exit(0))。例如一個字串"ABC123.a123",在這個例字中,只會被辨識出一個ALPHA(ABC)和一個NUMERIC(123),因為接下來讀到了EXIT(.),所以很遺憾的,判讀就到此為止了。

最後,需要特別注意的是,當我們在處理真正的多行檔案時,假設我們也需要讓Lexer判讀出檔案換行的行為時,可以加入下面的rule。因為在filter設定為true的時候,換行字元會自動被忽略。換句話說,如果filter沒有被設定為true,Lexer在讀到換行字元時,又會被噎死了,造成這個Lexer只能讀一行。

NEWLINE : ( "\r\n"   // DOS
                                 | '\r'     // MAC
                                 | '\n'     // Unix
)
{ newline();
$setType(Token.SKIP);
}
;


關於NEWLINE這個rule,我還沒參透newline();和$setType(Token.SKIP)寫或不寫的差別....。

終於定義好所有的rule了,我們就可以執行ANTLR Pareser Generator,請他幫我們產生我們所需要的程式。在這裡我們想要它產生Java的檔案。若要在命令提示字元上執行,則需先設定好程式的classpath。
E:\workspace\ANTLR_Test>SET classpath=C:\Program Files\eclipse\plugins\antlr-3.1
.1.jar;E:\workspace\ANTLR_Test

E:\workspace\ANTLR_Test>java antlr.Tool simple.g
ANTLR Parser Generator Version 2.7.7 (20060906) 1989-2005
若都沒有錯誤的訊息,則Java的程式就已經被gen出來了。若只有warning,Java一樣會被gen出來,但要小心先前提到lexical nondeterminism的問題。

我們還需要寫一隻含有main的程式,來呼叫SimpleLexer.java。
import java.io.*;
public class Main {
  public static void main(String[] args) {
    SimpleLexer simpleLexer = new SimpleLexer(System.in);
      while(true) {
        try {
          simpleLexer.nextToken();
      } catch(Exception e) {}
    }
  }
}
編譯過後就可以執行了。
E:\workspace\ANTLR_Test>javac Main.java
E:\workspace\ANTLR_Test>java Main


所以這隻程式是從鍵盤接受輸入的字串,若要讓這隻程式可以讀文字檔案(test.txt),可以這麼做。讀入文字檔案,就可以試驗換行的行為。

E:\workspace\ANTLR_Test>java Main < test.txt
ABCD1234C567
Found alpha: ABCD
Found numeric: 1234
Found alpha: C
Found numeric: 567


上一篇:ANTLR (2) - Lexer和Parser的文件格式
下一篇:ANTLR (4) - Simple Lexer/Parser Example

No comments:

Post a Comment