03.從0實現一個JVM語言系列之語法分析器-Parser-03月02日更新

03.從0實現JVM語言之語法分析器-Parser

相較於以前有較大更新, 老朋友們能夠覆盤或者針對bug留言, 我會看到以後答覆您!

源碼github倉庫, 若是這個系列對您有幫助, 但願得到您的一個star!

本節相關語法分析package地址

本節相關前端語法樹package地址

系列導讀 00.一個JVM語言的誕生

致親愛的讀者:

    我的的文字組織和寫文章的功底屬實通常, 寫的也比較趕時間, 因此係列文章的文字可能比較粗糙,
不免有詞不達意或者寫的很迷惑抽象的地方 

    若是您看了有疑問或者以爲我寫的實在亂七八糟, 這個很抱歉, 確實是個人問題, 您若是有不懂的地方
的地方或者發現個人錯誤(文字錯誤, 邏輯錯誤或者知識點錯誤都有可能), 能夠直接留言, 我看到都會回覆您!

系列食用方法建議

因爲時間緣由, 目前測試並不完善, 因此推薦以下方式根據您的目的進行閱讀

    若是您是學習用, 建議您先將整個項目clone到本地, 而後把感興趣的章節刪除, 本身重寫對照着重寫
    書寫完每一步測試一下可否正常運行(在指定的路徑去讀取源碼測試可否編譯成功並在命令行執行

    java Application(類名)

嘗試可否輸出指望結果, 我沒有研究Junit對編譯器輸出class文件進行測試, 因此目前可能須要您手動測試)

    按照以上步驟, 等您將全部模塊重寫一遍, 大概也對這個系列的脈絡有深入理解了! 若是您重頭開始重寫, 
每每可能因爲出現某些低級錯誤致使長時間debug才找獲得錯誤, 因此對於初學者, 推薦採用本身補寫替換模塊的
方式

    對於但願貢獻代碼的朋友或者對Cva感興趣的朋友, 歡迎貢獻您的源碼與看法, 或者對於該系列一些錯誤/
bug願意提出指正的朋友, 您能夠留言或者在github發issue, 我看到後必定及時處理!

本節提綱

  1. 抽象語法樹介紹html

    1.1. 簡介前端

    1.2. Cva程序的樹形結構示意圖java

    1.3. ast包內詳細介紹及繼承關係git

  2. 語法分析實現程序員

    2.1. 遞歸降低分析算法github

    2.2. 遞歸降低算法解析Cva代碼算法

    2.3. 語法分析實例簡析設計模式

    2.4. 給出語法錯誤提示及警告jvm

  3. 設計模式淺析函數

    3.1. ast中的設計模式淺析

抽象語法樹介紹

簡介

前面咱們實現了詞法分析器, 會發現詞法分析器的功能是有限的, 他只能像個復讀機或者說
翻譯官同樣將咱們的源代碼按照必定的規則切割成Token流, 可是咱們的代碼實際上是樹狀的結構,
爲了將代碼抽象成樹狀的POJO(咱們的前端ast的Program類), 這個類能夠延伸出咱們代碼的
每個節點, 每個類, 每個方法, 每個方法本地變量, 每個運算符, 他是以
Program這個節點爲root的一棵樹, 這棵樹叫作抽象語法樹(AST:abstract syntax tree),
語法分析器將用戶(程序員)寫的這段代碼按照咱們的語法規則進行分層次理解轉化成方便後期處理的
一種中間表示, 這個中間表示是井井有條的樹形結構, 就是咱們的抽象語法樹

咱們這裏沒有畫圖, 按照樹的層次, 咱們這裏採用自頂向下語法分析, 這課樹的各個節點與咱們
表示Cva語言文法的樹形結構是相似的

編譯器前端抽象語法樹Package在 cn.misection.ast 中, 其中對於各類節點都有詳細的歸類
每一個package如他們的名字所示, 分別包含着Expr, statement, 全部的節點都實現空接口
cn.misection.ast.IASTreeNode, 代表他們都是抽象語法樹的節點, 不一樣包內會有一個節點接口
繼承自 cn.misection.ast.IASTreeNode, 如Expr包中有IExpression.以後會有一個抽象類繼承自包內抽象節點, 如 AbstractExpression, 以後全部包內節點都將繼承自抽象節點
或者節點接口, 接口內會規定一些方法, 該包中的類必須實現他們, 如toEnum()

引入toEnum()和枚舉表示是爲了以後咱們更優雅地switch case, 不然對於同屬一類的節點,
咱們可能須要反射去獲取抽象的表達式究竟是加法表達式仍是乘法表達式(或者instanceof運算),
類名雖然能夠表達這些信息, 但這樣用並不優雅, 因此咱們給每種節點都綁定一個枚舉值, 既方便switch,
又能夠取代一些很是簡單的不包含信息的類型(如type包中的基本類型基本只是做爲別人的屬性使用), 這時候
使用他們時, 用他們的枚舉單例其實就夠了, 語法分析器最後就是須要拿到一個從Program始, 向類
方法等發散的前端語法樹, 樹發散的每一個節點就是一個個有了行號信息和字面量(有字面量時)的POJO

語法分析器即是抽象語法樹的製造者, 分析器讀完Token流以後, 在語言的語法規則指導下,
咱們就能獲得語法分析器的結晶-抽象語法樹了

本質來說, 抽象語法樹能夠看做是對該語言文法的"類型化"重寫, 即對於每一個非終結符,
每一個產生式, 給出確切定義, 並賦予不一樣的屬性. 類型實例便成爲要輸出的抽象語法樹的"節點"

Cva程序的樹形結構示意圖

依據以前給出的程序文法, 能夠給出一個樹形表示Cva程序的大體結構
因爲博客園的二級分類展開有點鬆散有點醜, 因此咱們放到text中

+ Program
  + Entry(能夠是入口類, 也能夠直接是入口方法)
    + main方法
      + StatementList
  + ClassList
    + Class
      + VarList
        + Var
        + ...
      + MethodList
        + Method
        + ...
    + ...
/**
 * 方法體;
 */
+ Method
  + Return Type
  + Name
  + FormalList
    + Formal
    + ...
  + VarList
    + Var
    + ...
  + StatementList
    + Statement
    + ...
  + Return Expr
/**
 * 變量聲明;
 */
+ Var
  + Type
  + Identifier

// 固然, Cva 支持在聲明時賦初值, 不過目前的實現比較簡單, 存在着必定的問題

上文所述的每個節點(不包含XXXList類型節點), 咱們都有給出確切的定義, 而且在節點內攜帶足夠的信息(行號, 類型信息等), 方便後期的語義分析和錯誤提示信息。

ast包內詳細介紹及繼承關係

咱們的AST中共包含8種主要的類型以下

+ Program

  程序實體類, 語法樹的根節點, 擁有 EntryClass 和 ClassList 兩個屬性
  + EntryClass   // 主類(程序入口), 若是隻有main方法, 會自動添加到Application類中
  + ClassList   // 用戶自定義的類

+ EntryClass

  主類實體類, 程序入口方法所在類, 若是不顯示聲明(直接上main方法), 那麼會生成Application做爲其默認名 
  擁有 Name StatementList(從屬main方法) 兩個屬性
  + Name        // 類名
  + StatementList   // main方法中的語句 TODO 能夠直接作成BlockStatement

+ Class

  類實體類, 表示用戶自定義的類, 擁有 Name BaseClass FieldList MethodList 四個屬性
  + Name        // 類名
  + ParentClass   // 父類(默認就是java/lang/Object)
  + FieldList   // 字段列表
  + MethodList  // 實例方法列表

+ Method

  方法實體類, 表示用戶定義的實例方法
  + Name            // 方法名
  + ReturnType      // 返回類型
  + ParameterList
    /FormalList
    /ArgList        // 方法的參數
  + LocalVarList    // 方法內聲明的本地變量
  + StatementList   // 語句
  + ReturnExpr       // 返回表達式 TODO 不要寫那麼死 之後要支持方法中return;

+ Statement
    + nullobj // 空對象
    + assign  // 賦值
    + Block     // 語句塊
    + if    // if    
    ... 等等, 詳細可見pkg內
  語句抽象類, 賦值語句, 輸出語句, if-else語句,  while語句, for語句等Statement
     語句塊直接繼承自此類

  // TODO: 插入樹形關係圖

+ Expression

  表達式抽象類, 加減乘運算, 小於運算, 與運算, 非運算, 方法調用, this表達式, 
    對象建立表達式(new), 常量(數字, true/false), 變量訪問直接繼承此抽象類
  + LineNumber  //表達式所帶行號
  // TODO: 插入樹形關係圖

+ Variable

  變量/字段實體類
  + Type    // 該變量/字段的類型
  + Identifier      // 變量/字段名

+ Type
    + basic
        + enum基本類型
    + advance
        + string
        + array
        + pointer
    + reference 
        + classType
  類型抽象類, 整型, 浮點型, 布爾型, string, class類類型直接繼承自此抽象類

  // TODO: 插入樹形關係圖

語法分析實現

語法分析器的任務是讀入記號流, 在語言的語法規則指導下生成抽象語法樹。

分析算法主要分爲自頂向下分析和自底向上分析, 其中自頂向下分析包括遞歸降低分析算法(預測分析算法)和LL分析算法, 自底向上分析包括LR分析算法。

遞歸降低分析算法

  • 簡單來說, 遞歸降低法就是對於現有的Token(現有的信息, 如今手裏捏的牌), 來預測下一個單詞應該是什麼
    好比, 如今程序說了: "你吃",
    那麼咱們預測下一個詞應該是"了嗎?" 或者"飯了嗎?"
    即咱們但願獲得的完整句子是"你吃了嗎?" 或者"你吃飯了嗎?"
    咱們能夠在獲得現有信息的基礎上對下一步可能的狀況進行手動窮舉

  • 回到一個Java中的例子, 好比如今咱們遇到了int這個詞, 咱們判斷他是int型字面量,
    那麼以後的狀況無外乎 int var; 或者 int var = 0; 即聲明或賦值
    (這裏不考慮強轉, 由於強轉前面有括號, 咱們對其處理通常是在另外一個流程中的)
    因此咱們下一步通常要吃掉咱們預期的Token(Parser的eatToken()方法),
    這個被吃掉的Token其實就是咱們當前讀到的Token, 咱們能夠先讀取他的信息
    (好比字面量等保存到如今, 輸出爲咱們抽象語法樹的節點(方法, 類, 聲明變量),
    待他無用以後, 將其eat掉), 若是有多種狀況, 其實就是一個或多個分支的事
    (在eat時, 須要指定須要eat的類型, 若是不符合, 就會報錯)

Cvac編譯器採用的是遞歸降低分析算法(預測分析), 該算法的主要優勢是

  • 分析高效, 線性時間複雜度
  • 容易實現, 方便手工編碼
  • 錯誤定位和診斷信息準確

不少開源編譯器, 商業編譯器也採用了該算法, 好比GCC4.0, LLVM等

該算法的基本思想是:

  • 爲每一個非終結符構造一個分析函數
  • 經過前看符號指導產生式規則的選擇

遞歸降低算法解析Cva代碼

咱們選擇一個簡單的部分來介紹遞歸降低分析算法在程序中的應用。

如上文所述, 用戶自定義的類由兩部分構成, 字段列表和實例方法列表(固然, 這個兩個列表均可覺得空), 那麼咱們就給出這樣一個方法

/**
 *  Class
     -> class Id { VarDecList MethodDecList }
     | class Id : Id { VarDecList MethodDecList }
 */
Class ParseClass()
{
    // 其餘代碼
    fieldList = parseFieldList();
    methodList = parseMethodList();

    return new SomeClass(fieldList, methodList);
}

如上述代碼所示, 在從記號流解析一個類型實體時, 調用了字段列表和方法列表的解析方法, 並將解析的結果做爲一個類型實體的組成部分(此處並未體現類名, 父類等信息).

很顯然的, 在 ParseFieldList, ParseMethodList兩個方法內部, 一定含有對於單個字段, 單個方法實體的解析方法的調用, 並將單個的實體結果組織起來, 以返回給外界.

思考一下咱們的文法規定的一個方法的構成形式, 返回類型, 方法名, 參數列表, 本地變量列表, 語句列表, 返回語句。這些部分的一個組織是方法, 那麼很天然地, 咱們又能爲此寫一個解析方法。

以上就體現了分析算法中爲每一個非終結符構造一個分析函數思想. 可是還有另一部分思想, 經過前看符號指導產生式規則的選擇還未體現。

考慮另一條文法, 關於"語句"。語句在咱們的程序中有五種形式, 由{}組織的語句塊, if-else語句, while語句, 賦值語句, 輸出語句.
咱們只須要看第一個符號, 就能知道應當選擇哪一條產生式來解析, 僞代碼描述以下所示.

/**
 *  Statement
       -> { StatementList }
       | if (Expr) Statement else Statement
       | while (Expr) Statement
       | println(Expr);
       | Id = Expr;
 */
Statement parseStatement()
{
    switch(firstToken)
    {
        // 寫代碼時儘可能不要出現以下的魔數, 這裏是爲了演示
        // 實際上咱們是把常量放入枚舉中的
        case "{":
            return parseStatementBlock();
        case "if":
            return parseIfElseStatement();
        case "while":
            return parseWhileStatement();
        // ...
        default:
            throw new ParseException(message);
    }
}

從僞碼中很清晰地體現出來, 咱們只須要經過查看一個"前看符號"就能肯定要選擇哪一個方向去解析當前語句。若是解析失敗, 那麼一定是用戶給的源程序出現了問題, 致使咱們程序選擇了錯誤的方向, 或者出現了錯誤, 這就須要用戶修改源代碼, 而後從新編譯。

語法分析實例簡析

對於文法附帶給出的程序樣例中的一行語句

total = num * (this.compute(num-1));

語法分析完成後的輸出的抽象語法樹以下示意

其實語法分析給出的語法樹就是把源代碼又層次打印一遍, 過程比較簡單. 若是朋友有問題能夠留言, 我看到會回答

// TODO: 樹形圖

給出語法錯誤提示及警告

在語法分析階段, 針對源碼中可能出現的錯誤, 會給出錯誤提示信息, 用於告知用戶程序處理到的位置和出現的錯誤

在自頂向下語法分析部分, 可能出現的錯誤的僅有一類, 預期是某個符號, 可是獲得的倒是另外的符號, 這樣就會出現錯誤了

舉例以下:

void doSomething(int )
{
  // ...
}

容易看出, 在這個方法的參數列表部分, 缺失了參數名, 所以語法分析器將會給出錯誤提示:

Line 6: Exprects Identifier, but got CLOSE_PAREN.
Syntax error line 6, compilation aborting...

給出錯誤提示以後就直接退出虛擬機, 拒絕編譯, 等待用戶修改源代碼並從新編譯

設計模式淺析

ast中的設計模式

  1. 空對象模式: 在ast中全部的nullobj包都是空對象模式, 空對象模式能省卻咱們代碼中大量難看的
    判空if (obj != null ) 等等
    在咱們後面遇到空對象時, 也是什麼都不作, 把它當作空氣(可憐的空對象)

  2. 建造者模式: 在前端的method構造時, 使用了建造者模式, 建造者模式能讓咱們的傳參更加清晰,
    不易犯錯, 也能必定程度上封裝複雜的構造過程, 後期將會把全部構造複雜的POJO所有重構成建造者模式構造

  3. 往後會思考將一些構造重構成工廠

相關文章
相關標籤/搜索