2011年7月6日 星期三

[編譯器的研究]scanner

前面提到 scanner 的工作,是詞法分析,
針對這項工作,要依序解釋每個詞意義,也就是轉換成 token。
token 將是 parser 處理時的最小單位,scanner 的這項工作做不好,parser 也很難做得好。
因此 scanner 與 parser 通常是排在一起,因為相依性很高。

----------------------
程式的設計路線通常是跟實作路線反過來,我們在設計的過程會先想到需求,
把需求從抽象到實際從上到下一一描述出來,
而實作的時候,卻要由下往上一一實作,才能組合回去。
物件導向的程式語言,是可以幫上一點忙,一開始設計抽象物件,
但是程式要能夠跑起來,物件實作還是要寫好才會能夠動。
寫文章要講求思考脈絡又要講求實作順序,還真的傷腦筋。
----------------------

接下來要為 scanner 準備好它所需要的 function 及 class,
包含字串辨認及字串確定兩個工作。
scanner 要做的字串辨認工作,
要準備 Token 類別來儲存「字串辨認定義」、「字串辨認結果」的資訊及「字串辨認」的操作。
字串辨認結果,要包含字串類別,字串值。

舉一個 timestamp 的 Token 類別來說
Token 定義是 regular express 的 '[0-9]+-[0-9]+-[0-9]+-[0-9]+:[0-9]+:[0-9]+ '
Token 名字是 "timestamp"
Token 的 test 方法,會比對來源字串是否符合 pattern。
如果辨認成功,token.reg 為 true,token.value 為所要的值
因為要連續辨認,所以 test 的回傳值是一個新的 token 物件。
原來的物件留著下一次比對用。
為了輸出的時候,知道 Token 是在檔案的第幾個 byte 及第幾行,
也多兩個欄位 Token.cur_pos 及 Token.cur_line 來儲存訊息。

有人會想用 regular express,但是在 MIT 課程裡有特別提到
它無法應付 recursive,這是什麼意思呢?

我認為是這樣的。
例如,我們有兩個 token,一個是 timestamp token,及 number token。
在 log 裡,2011-06-22-07:56:06 會辨認到一個 timestamp token,及數個 number token。
依照最長辨認的原則,應該是選擇 timestamp token。
那要找到這裡面的年,月,日…等等的 number token,就沒辦法把它分開。
只得把它辨認為 number token,也就是把 timestamp token 的定義拿掉。
在 scanner 階段就無法區分 number token 是否為年,月,日…等等。
要等到 parser 階段才能決定。
但是,若辨認工作是自己實作特殊的 DFA 或 NFA,就可以自行增加 stack、queue 或 tape 等等不同的操作,
提供特殊辨認及輸出。例如處理 non-regular language,{a{n}b{n}},
只要在 DFA 裡面加一個變數記一下看到a有幾次,就可用程式的方法解決這個問題。

這個問題想到這裡就好,因為我們目前只求「大部 分解」因此,
timestamp token 裡面的年,月,日…等等就先不理會。
我個人認為若有必要,可以用二次 scan 來解決。
目標是先把大區塊標顏色,token 切太細,parser 的規則就會變多。
如果 token 切很細,parser 的規則又不想寫很多,那標出來的顏色就是花花緣緣的。
這是一個「資源取捨」問題。

因應各種辨認的問題,我們設計的 Token 類別實際上是個抽象類別。
如果是可以用 regular express 辨認的,就用 Token_RE 類別,
它的實例化方法需要一個 regular express,它的 test 方法就是該 regular express 來辨認。
另外,我們會遇到 XML transaction,因此特別寫了一個 Token_XML 類別,
把 root element <Transaction> 整個視為一個 xml transaction token。

在我們的範例 log,我就定了 16 個 token,實際有用到的是 15 個。

NULL_TOKEN = 0 ### 沒用到
TIMESTAMP_TOKEN = 1
SPACE_TOKEN = 2
NWORD_TOKEN = 3
COLON_TOKEN = 4
SECSSTREAMFUNCTION_TOKEN = 5
SECSCOMMENT_TOKEN = 6
WHITESPACE_TOKEN = 7
SECSEND_TOKEN = 8
SECSLISTSTART_TOKEN = 9
SECSITEMA_TOKEN = 10
SECSLISTEND_TOKEN = 11
EAPLOG1_TOKEN = 12
XMLTRANSACTION_TOKEN = 14
SECSITEMU_TOKEN = 15
SECSITEMB_TOKEN =16
SECSITEMBOOL_TOKEN = 17

這 15 個 token 裡 XMLTRANSACTION_TOKEN 是 Token_XML。其他都是 Token_RE,
Token_RE 的 test 方法,我偷懶用了 python 的 re module 的 match。
因為每次傳入的都是要從頭開始比,所以是 match 而不是 search。

class Token_RE(Token_abs):
  ### 省略部份
  def test(self,source):
    r = self.p_compile.match(source)
    if r == None:
      self.reg = False
      self.value = ""
      a = Token_RE(self.token_name,self.token_index,self.re_pattern)
      a.reg = self.reg
      a.value = self.value
      return a
    else:
      self.reg = True
      if len(r.groups())>0:
        self.value = r.groups(0)[0]
      else:
        self.value = r.group()

      a = Token_RE(self.token_name,self.token_index,self.re_pattern)
      a.reg = self.reg
      a.value = self.value
      return a

scanner 主要工作在 read 方法,大部分的實作都是執行一次 read 方法,就傳回一個 token。也就是在這裡就要做字串確定的工作。
在執行 read 方法之前,要把 log 讀進來,指定給 source 這個變數。
在執行 read 方法的時候,scanner 把 source 給 15 個 token 比對,找出比對成功的。
然後選擇比對結果最長的一個 token 當成辨認結果。
接下來,把 source 切掉已辨認的部份,剩下未辨認的部份。
傳回辨認結果之前,要順手把 cur_pos 及 cur_line 算完記下來,下次要用,
也要把值存進 token 裡,讓呼叫的人知道。
然後才是把 token 傳回去。
read 方法回傳 None 的情況有三個,代表 scanner 要停止辨認。
一個是 source 長度為零,因為上次辨認最後了。
一個是所有的 token 都沒有辨認到。
最後一個就是辨認結果的字串長度為零,這是一個很特別的狀況,也要停止辨認。
因為 regular express 有使用到 star operation 的時候,
也就是比對到 empty string 的狀況。
依照 scanner 的設計,empty string 長度為零,
source 等於沒有進行切除已辨認的動作,
下次 read 方法再執行時, source 還是原來的 source,
辨認結果還會是一樣的 empty string。
因此針這個情況要停下來,以免進入無窮迴圈。

class Scanner:
  ### 省略部份
  def Read(self):
    if len(self.source) == 0:
      return None
    r = []
    for i in self.tokens_info:
      r.append( i.test(self.source) )
    r2 = []
    resultlength=[]
    for i in r:
      if i.reg == True:
        r2.append(i)
        resultlength.append(len(i.value))
        print '  match:%s' % i
    if len(resultlength) == 0:
      print 'source a:%s' % self.source[:30]
      print 'pos:%s' % self.cur_pos
      print 'no match!!!!'
      return None
    maxlen = max(resultlength)
    if maxlen == 0:
      print 'source a:%s' % self.source[:30]
      print 'pos:%s' % self.cur_pos
      print 'no match!!!!'
      return None

    self.source = self.source[maxlen:]
    #print 'source b:%s' % self.source[:30]
    #print "new:%s\nold:%s" % (self.source[:40],self._keepsource[:40])
    #print r2
    for i in r2:
      if len(i.value) == maxlen:
        i.cur_pos = self.cur_pos
        i.cur_line = self.cur_line
        r3 = i
        self.cur_pos += maxlen
        line_object = re.findall("\\n",i.value)
        self.cur_line += len(line_object)
        break
    return r3 #r2[0]

scanner 使用方法,就是加入 token 定義,設定 source 的內容,
執行 read 方法直到回傳為 None。
每一次拿到的 token,裡面的資訊都有 token 的名字、內容、位置及行號。
接下來就是交給 parser 處理。

### token 定義
re_1 = '[0-9]+-[0-9]+-[0-9]+-[0-9]+:[0-9]+:[0-9]+' 
lex_timestamp = Token_RE("timestamp",TIMESTAMP_TOKEN, re_1)
### 其餘省略

### 加入 token 定義
s = Scanner()
s.AddTokenDefinition(lex_timestamp)
### 其餘省略

### 設定 source 內容
s.SetSource(source)

### 讀取 token
r =  s.Read()
while r != None:
  count += 1
  if r.token_index != 2 and r.token_index != 7:   ### 不想印出空白及分行
    print '    result(%s):%s' % (count,r)

因為還沒有想到 parser 要怎麼辦,所以先把 token 全寫到檔案裡,用三個連續分行符號分開。像這樣:

tokens(1):timestamp,1,True,0,0,2011-06-22-07:56:06

tokens(3):normal word,3,True,20,0,AYT_Host

tokens(4):colon,4,True,28,0,:

tokens(6):secs SxFy,5,True,30,0,'S1F1'

tokens(8):normal word,3,True,37,0,W

tokens(10):secs comment,6,True,39,0,/* Name=AreYouThere_Host Dir=2 Header=[00 00 81 01 00 00 00 01 BA A3] Rcvd=2 Time=07:56:06 TID=28848 */

tokens(12):secs end,8,True,143,1,.

tokens(14):timestamp,1,True,145,2,2011-06-22-07:56:06

tokens(16):normal word,3,True,165,2,OLD

tokens(17):colon,4,True,168,2,:

沒有留言:

張貼留言