(基於Java)編寫編譯器和解譯器-第3章:掃描-第二部分(連載)

來源:互聯網
上載者:User
文章目錄
  • Pascal Tokens
  • 文法圖(syntax diagrams)
  • 單詞Token
  • 字串token
  • 特殊符號Token。
  • 數字Token

>>>續 第一部分

從這個小例子從可知如下的一些關鍵點:

  • 掃描器掃描並跳過Token之間的空白符(比如空格)。當此操作結束時,當前字元肯定不是空白符。
  • 非Null 字元判定下個要提取的Token類型,且此字元成為這個token的首字元。
  • 掃描器不停地通過掃描和拷貝源字元建立Token,直到字元不能成為這個Token的一部分。
  • 提取token將吞噬掉組成此token的所有源字元。因此,提取過後,當前字元是token尾字元後的下一個字元。
一個Pascal掃描器

上一章在frontend.pascal包中已完成了PascalScanner類的前期工作。現在擴充方法extractToken()並加一個新的方法skipWhilteSpace()。見清單3-8

清單3-8:PascalScanner類的extract()和skipWhiteSpace方法 詳細參見本章原始碼,這裡不再顯示。

extractToken()方法與前面例子描述的基本一樣。首先,它掃描並跳過所有空白符,那麼當前字元(不是空白符)是下一個token的首字元,此token類型通過當前字元判斷。這個方法建立和返回任一PascalWordToken,PascalNumberToken,PascalStringToken或者PascalSpecialSymbolToken對象。因此在extractToken()方法讀取每一Token的首字元之後,新建立的Token對象讀取和拷貝此Token的剩餘字元。extractToken()方法在源檔案結束時同樣建立和返回一個EofToken對象,或者一個PascalErrorToken對象假如碰到一個不能成為任何Pascal Token首字元的無效字元。skipWhiteSpace方法跳過Token間的所有空格,這由Java的Character.isWhiteSpace來判斷。它同樣忽略Pascal注釋內容,因此每個注釋相當於一個空格(好怪)。Pascal Tokens

在上一章中,你定義了語言無關的TokeType佔位介面。現在定義一個枚舉類型PascalTokenType,它的枚舉值表示所有Pascal Tokens。清單3-9 展示了必要的TokenType實現。

清單3-9 :PascalTokeType枚舉類型 詳細參見本章原始碼,這裡不再顯示。

在清單3-1 中,類PascalParserTD的parse()方法中使用了枚舉值ERROR。在清單3-2中,主類Pascal的內部類ParserMessageListener在處理TOKEN訊息時使用了枚舉值STRING。

靜態集合RESERVED_WORDS包含Pascal關鍵字的文本串。其後在類PascalWordToken中使用這個集合判斷一個單詞是一個關鍵字還是一個標識符。因為每個關鍵字的枚舉值在關鍵字後標識,值的文本即是關鍵字的字面串。比如枚舉值BEGIN的值是字串"BEGIN"。因為Pascal單詞大小寫不敏感,通過將它們(枚舉值文本)變成小寫去歸化單詞。

靜態雜湊表SPECIAL_SYMBOLS包含每一個Pascal特別符號項。項的鍵是特殊符號的文本字串,如枚舉值的構造器一樣,項的值是枚舉值本身。比如,某項的鍵是":=",它的值就是COLON_EQUALS。類PascalScanner(見清單3-8)使用雜湊表判斷是否去建立和返回一個PascalSpecialSymbolToken對象。類PascalSpecialSymbolToken也會使用這個雜湊表。

參考原始碼中PascalToken類,它將是所有Pascal Token子類的基類,它擴充了語言無關的架構類Token。儘管當前它沒有添加任何域或方法,它也給後續開發帶來了方便。(也就是一個Mark類,TAG類)

文法圖(syntax diagrams)

我們將開發frontend.pascal.tokens包中的剩餘PascalToken子類,以便提取各種Pascal Token。但首先,你需要一個好的關於語言元素的文法規範,這有幾種常見的方法,但Pascal相對簡單的文法可以很好的使用文法圖即語言文法規則的圖形化表示。(最後一章以文本方式表示一種語言的文法,也就是EBNF範式)

圖3-2 展示了三張圖。第一張明確字母可以是大寫A到Z和小寫a-z中的任何一個字元。第二張圖明確一位元字可以是0到9的任一字元。而第三張明確單詞token是由單個字母加後續0到多個字母或數字構成。

設計筆記

文法圖很好理解:順著箭頭指引的方向。分叉路徑代表選擇:字母可以是A或B或C等等。其它繞迴路徑表示連續:在單詞token的首字母后,有連續的0或多個字母/數字。

圓框表示字面文本,比如字母A或數字0。(在第5章開頭,你也會用圓框表示關鍵字如AND、OR以及AND的字面文本。)矩形框是另一圖形的引用。例如,單詞Token參考資料表示字母和數位文法圖。較正式的說法是,原型框表示末端符(沒法再分),而矩形框表示非末端符(可以再次細分)

單詞Token

PascalScanner類中的extractToken()方法(參見清單3-8)在當前字元是字母時建立一個新的Pascal 單詞Token。

   1: else if (Character.isLetter(currentChar)) {

   2:     token = new PascalWordToken(source);

   3: }

frontend.pascal.tokens包中的PascalWordToken類是PascalToken的子類。清單3-11 展示了它的extract()方法。詳細參見本章原始碼,這裡不再顯示。

extract()實現了3-2文法圖所示的Pascal單詞Token文法規則。在提取第首字母后,它吞噬任意緊隨其後的字母和數字以便構造單詞token的文本串。當這結束後,currentChar的值將不是字母或數字,因此這個字元將是token的後一位字元。然後extract()方法判斷單詞是否一個關鍵字。如果單詞文本串在TokenTypes.RESERVED_WORDS集合(見清單3-9) 中出現,它必定是一個關鍵字。因為Pascal關鍵字不區分大小寫,測試集合所屬關係通過小寫完成(都以小寫來比較),token的類型是相應的PascalTokenType枚舉值。如果token類型是標識符,現在暫且將其值置為null(後續在中間碼,符號表章節會處理這個值)。

清單3-4 展示了PascalWordToken的一些執行個體。(這裡省略,請運行原始碼 java -classpath classes Pascal compile scannertest.txt 並留意從9到12行的輸出)

原始碼第11行包含了四個關鍵字BEGIN用以檢驗PascalWordToken的大小寫敏感性。毫無疑問,begins的token類型一定是標識符(begins 不等於 begin)。字串token

圖3-3 展示了Pascal 字串token的文法圖

Pascal字串以單引號'開始,以單引號結束。單引號之間是組成字串token值的0到多個連續字元。在字元序列中,兩個相鄰的單引號表示字串中的一個單引號(此時的單引號是文本字元,不是表示Pascal字串的起止字元)。類PascalScanner在當前字元是單引號時,建立一個PascalToken對象。
else if (currentChar == '\'') {
token = new PascalStringToken(source);
}
    清單3-12 展示了PascalToken子類PascalStringToken中的extract方法。詳細參見本章原始碼,這裡不再顯示。方法extract()吞噬字串字元。它替換所有空白符(比如行結束符,再比如\t等) 為單個空格。它必須檢測意外終止的情況(如果有開始的單引號,沒有結束的單引號,字串token將會把起始單引號之後的所有字元都認為是字串的一部分,直到檔案結束)。在字串中如果碰到一個單引號(我們認為起止的單引號不算字串內容,只算字串標識),extract將調用peekChar()去檢測這個字元(單引號)是字串結束符還是相鄰兩個單引號的前一個。如果是後一種情況,此方法將噬掉這對相鄰單引號,並附加一個單引號在字串內容上。如果檔案意外終止,方法將token的類型設為ERROR並且把值置成PascalErrorCode枚舉值UNEXPECTED_EOF。 清單3-4 展示了PascalStringToken的一些輸出例子:(這裡省略,請運行原始碼 java -classpath classes Pascal compile scannertest.txt 並留意從13到21行的輸出)PascalStringToken很好的處理了Null 字元串(行016)和相鄰單引號問題(行017)。它把每一個字串中的分行符號替換成為空白格(行018,019,020)。特殊符號Token。如果當前字元是PascalTokenType.SPECIAL_SYMBOLS雜湊表某一項的值,類PascalScanner(見清單3-8) 會建立一個PascalSpecialSymbolToken對象。
   1: }else if (PascalTokenType.SPECIAL_SYMBOLS.containsKey(Character.toString(currentChar))){

   2:     token = new PascalSpecialSymbolToken(source);

   3: }

清單3-13 展示了PascalToken子類PascalSpecialSymbolToken的extract方法,詳細參見本章原始碼,這裡不再顯示。此extract()方法嘗試提取一個特殊符號token并吞噬當前token字元。Pascal特殊字元token由1或2個字元構成。如果成功提取,extract方法通過SPECIAL_SYMBOLS雜湊表去設定token的類型為恰當的枚舉值。然如果有錯,此方法設定token的類型為ERROR並設其值為PascalErrorCode的枚舉值INVALID_CHARACTER。清單3-14 顯示了PascalSpecialSymbolToken的一些例子。(這裡省略,請運行原始碼 java -classpath classes Pascal compile scannertest.txt 並留意從22行到25行的輸出)數字Token圖3-4 展示了Pascal數字Token的文法圖。一個不帶正負號的整數是一個數字序列。一個Pascal整數token是一個不帶正負號的整數。一個Pascal實數token以一個整數部分開始,接著是以下一種:I:一個小數點,緊接著一個不帶正負號的整數(小數部分)。II:一個E或e,之前可能有+或-,緊接著是一個不帶正負號的整數(指數部分)。III:一個小數,緊接著一個指數部分。    

 

 

清單3-15 展示了PascalToken子類PascalNumberToken中的extract方法。詳細參見本章原始碼,這裡不再顯示。

字串類型的域wholeDigits,fractionDigits和exponentDigits分別表示小數點之前的序列,小數點之後的序列和E或e之後的序列。3-4展示的文法圖那樣,域fractionDigits和exponentDigits可能為null。此方法(PascalNumberToken)方法初始設定token的類型為INTEGER,如有小數部分或指數部分,則改類型為REAL。它調用unsignedIntegerDigits方法在三部分(整數,小數和指數)提取數字,這可以保證此每個部分至少有一個數字。(如果有小數部分,指數部分,才能保證響應的部分至少有1個數字,但記住這兩個部分是可選的)。

清單 3-16 展示了PascalNumberToken類的unsignedIntegerDigits方法。詳細參見本章原始碼,這裡不再顯示。

如果extractNumber方法在整數部分後遇到一個'.'字元,它不能馬上假定這是一個小數點,因為它可能是 .. Token(1..10表示範圍)的第一個字元。調用peekChar()方法前探一個字元就是用來判斷這種情況。在數位所有部分都被提取之後,它視類型為INTEGER或REAL調用computeIntegerValue或computeFloatValue 方法計算數位值。見清單3-17和清單3-18

清單3-17:類PascalNumberToken中的computeIntegerValue方法 詳細參見本章原始碼,這裡不再顯示。

computeIntegerValue方法計算一串數位整數值。它通過確定值折回(二進位的位元有限,如果超過表示的最大值,可能將當前的位元置為0,比如 1111 加上 一個1 就變成 0000,這個算折回,從頭開始了)且小於前一個值來檢查溢出。如果有溢出,方法設token的類型為ERROR且設定token的值為PascalErrorCode的枚舉值RANGE_INTEGER。

清單3-17:類PascalNumberToken中的computeFloatValue方法 詳細參見本章原始碼,這裡不再顯示。

computerFloatValue()方法計算包含整數部分、小數部分、指數部分和指數符號的數字串的float值。它調用computerIntegerValue計算指數的整數值,如果指數符號為'-',取整數值的反數。如果有小數,方法會調整值即減去小數部分的長度。最後,如果調整後的指數值加上指數部分的值超過MAX_EXPONENT,則數字越界了(超過表示範圍),方法設定token的類型為ERROR且設定token的值為PascalErrorCode的枚舉值RANGE_REAL。(MAX_EXPONENT的值依賴於底層機器架構和附帶的浮點數實現標準,最小指數不比是最大指數的反數,比如最大指數為64,則最小指數不一定是-64)

computerFloatValue()方法將整數和小數部分和在一起計算,並將值乘上調用以調整後的指數為參數的Math.pow方法得到的值,得到數字token的最終值。

這兒有一個關於extractNumber方法怎樣計算數字token的例子。有token字串為31415.926e-4,extractNumber方法將以下值傳遞給computerFloatValue方法:

wholeDigits:       "31415"
fractionDigits: "926"
exponentDigits: "4"
exponentSign: '-'
方法computerFloatValue調用computerIntegerValue計算exponentValue(參考代碼中變數)的值,接著它調整兩次,一次是因為exponentSign是'-'將值(exponentValue)取反,接著(還是exponentValue)減去fractionDigits字串的長度3。
exponentValue:  4 as computed by computeIntegerValue()
exponentValue: -4 after negation since exponentSign is '-'
exponentValue: -7 after subtracting fractionDigits.length()
最後,computerFloatValue方法計算wholeDigits + fractionDigits聯合字串的值,得到31415926.0,接著乘以Math.pow(10, exponentValue),得到此數字token的最終值3.1415926。清單3-5 顯示了PascalNumberToken的一些例子。:(這裡省略,請運行原始碼 java -classpath classes Pascal compile scannertest.txt 並留意從26到31行的輸出)。

設計筆記

掃描器是編譯器器/解譯器前端的一個重要組件,這章寫了很多的代碼。但你遵循策略設計模式實現PascalToken的子類,這樣每個Token子類都知道怎麼從來源程式中提取token字串。每個子類的實現方法只有一個職責,都是高彙總。比如PascalNumberToken方法只負責提取數字token。高彙總類別結合程度少,易於維護。如果你決定改進掃描器的實數計算方式,減少舍入(類似於四捨五入的舍入)錯誤,你只需要修改PascalNumberToken類。假設你的設計決定是讓PascalToken自己提取所有Pascal Token類型(而不是通過子類),那麼這個類(PascalToken)的彙總性極低。很難在一種token的提取方式發生改變的時候不影響另一個。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.