04.從0實現一個JVM語言系列之語義分析器-Semantic

從0實現JVM語言之語義分析-Semantic

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

本節相關語義分析package地址

致親愛的讀者:

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

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

系列食用方法建議

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

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

    java Application(類名)

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

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

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

語義分析器工做基於語法分析器輸出的抽象語法樹, 經過對該語法樹的分析作進一步的檢查,
回答咱們, 也回答代碼生成器最後一個問題:源程序是否符合語義規則. 若是確實存在問題,
那麼這段代碼即便翻譯成機器碼(在這裏是咱們後面要生成的JVM彙編指令), 也不可能執行成功,
必然收到JVM的拒絕執行(讀者能夠本身嘗試瞎寫命令而後用jasmin彙編成字節碼, 看報錯反應, 哈哈),
或者形成一些意想不到的問題, 所以語義分析有問題勢必形成後面的錯誤, 因此語義分析旨在蒐集
代碼出現的邏輯錯誤(多數是類型檢查的類型問題), 並指出, 以讓接下來的步驟能正常進行前端

在這個階段要給出儘量準確的報錯信息, 供用戶參考並修改源代碼.
語義分析中最重要的工做即是類型檢查, 此外還會有一些其餘的檢查, 例如變量在使用前是否聲明等.java

語義分析實現

符號表

語義分析的正常進行少不了符號表的參與. 所謂符號, 程序中的變量、方法、字段、類都是符號.
符號表存儲了程序中的符號的相關信息, 這些信息包括類型、做用域、訪問控制信息等等, 並且符號表必須很是高效,
由於程序中符號的規模會很是大. 咱們的符號表都是採用HashMap, JVM針對HashMap的優化可謂是比較極致,
咱們的JDK8後引入了紅黑樹, 對於Map, HotSpot會傾向於將其編譯成本地代碼, 在一些狀況下,
其運行效率甚至超過通常的手寫分支判斷git

咱們的哈希表以符號名字爲鍵, 以符號的相關信息爲值, 創建映射.
並按照樹形結構, 組織各個做用域的符號及信息, 創建全局符號表.github

全局符號表的大體結構以下:後端

// TODO: 樹形圖jvm

+ ClassMap
  + ClassBinding
    + BaseClass
    + FieldMap
      + Field
      + ...
    + MethodMap
      + Method
      + ...
  + ...
+ MethodVariableMap

+ ClassMap

  全局符號表的入口, 直接維護了類名和類的相關信息的

+ ClassBinding

  存儲了類的相關信息, 包括父類, 字段表, 方法表. 其中字段表和方法表是以名爲鍵的映射. 

+ Field

  存儲了字段的聲明類型

+ Method

  存儲了方法的相關信息, 包括聲明的返回類型, 參數的個數及各自類型. 

+ MethodVariableMap

  參數和本地變量表, 是名字和類型的映射. 當分析某個方法的時候, 對於該方法的參數和本地變量的訪問是至關頻繁的,
 所以將他們單獨存儲到某個位置, 分析完畢即銷燬, 優化空間的佔用.

分析思路

本質上說, 所謂「分析」僅僅是語法樹的遍歷而已, 只是附帶上了附加條件, 要求某子樹符合某個要求. 此外咱們應當注意到,
類型的聲明、字段的聲明和方法的聲明, 並無任何值得語義分析的地方, 真正值得咱們去分析和檢查的是語句和表達式,
查看它們是否合法. 所以, 分析過程可簡要分紅兩步.ide

  1. 信息收集和索引函數

    對當前語法樹的前幾層進行遍歷, 掃描並存儲類信息, 各自的字段和方法相關信息, 構建全局隨時可用的「全局符號表」,
    該過程僅在語義分析實際進行以前進行一次.學習

  2. 語義分析和檢查測試

    收集要分析的方法的參數和變量信息, 而後順序遍歷方法的每一條語句和表達式. 若是找到錯誤, 那麼就輸出一條信息,
    提示用戶在某位置發現何種類型的錯誤, 並儘量進行恢復, 繼續檢查下文, 在一趟分析中給出儘量多的錯誤信息.

對於每一個方法, 都執行一遍步驟2, 直到全部的方法都分析過. 若是未發現任何錯誤, 那麼進入下一階段, 若是發現問題,
那麼在給出全部信息後, 退出編譯過程.

分析過程

類型檢查

類型檢查是語義分析的重點所在, 若是此處的檢查沒有經過, 那麼這個程序一定存在問題, 一定不能運行.
下面經過幾個例子來展現類型檢查的工做細節.

表達式

對於表達式而言, 類型檢查主要分爲如下幾類

  • 操做符

    對於單目運算符邏輯非 !主要檢測其操做數是不是前端boolean
    (只有前端用戶纔會看到boolean, 在編譯器後端boolean會被處理爲int),
    對於雙目運算符如+ - * /類型檢查的步驟是:

    先確認兩側/單側的表達式類型, 而後確認兩側表達式類型是否匹配,
    最後確認當前的操做符可否對該類型進行操做所有沒有問題的話, 才認爲該表達式經過了檢查,
    並確認該表達式的值類型(在代碼中表達式的結果值直接取雙目運算符的左邊)
    (在這裏, 咱們前面的toEnum()方法就派上了用場)

    錯誤語法例子如:

    • 10 + true

      顯然 + 兩側類型不匹配, 類型檢查的給出的信息是,

      Cva往後將遵循JVM規範, 基本運算能夠是範圍小於 int 的整形, 作運算時都強轉爲 int
      如 byte char short 類型轉爲操做數都視爲 int, 編譯器不報錯

      Error: Line 1 Add Expr ression: the type of left is @int, but the type of right is @boolean

    • true < false

      兩側類型是一致的, 但很顯然, 這個比較沒有任何意義, 所以這個表達式也是個錯誤

      Error: Line 1 only numeric can be compared.

    • !200

      只有布爾值才能取邏輯非, 所以這個表達式顯然也是非法的

      Error: Line 1 the Expr r cannot calculate to a boolean.

  • 方法調用

    Cva目前僅支持實例方法調用, 暫不支持靜態方法、方法重載等.

    對於方法調用表達式, 類型檢查主要關注形參及實參.
    首先確認形參和實參數量是相等的, 而後確認參數的類型是一一對應的.
    經過檢查後, 表達式的值類型被設定成爲該方法的返回值類型.

    假定本類中有有方法 int compute(int a, int b), 對它的兩種錯誤調用以下

    • this.compute(10)

      顯然, 這並不能經過第一步: 對於參數個數的檢查

      Error: Line 1 the count of arguments is not match.

    • this.compute(10, false)

      顯然, 第二個參數的類型不是匹配的

      Error: Line 32 the parameter 2 needs a int, but got a boolean

    • println(Expr expr)

      應當注意到, 在本程序中咱們認爲write(控制檯寫操做, println, echo, printf)是一個語句,
      (內置方法關鍵字)而不是函數調用表達式. 這樣作的目的是爲了簡化該編譯器開發,
      但其本質依舊是函數調用, 所以對於它的類型檢查等同函數調用. 寫操做應檢查參數是否爲string 或者基本類型,
      所以Expr expr最終應當能求得一個string 或者 整形(目前, 之後完善boolean和浮點數情形

  • 返回表達式

    這裏是確認方法聲明處的返回類型和實際的返回類型是匹配的. 首先檢查 return 關鍵字後緊跟的表達式是否有意義,
    而後再確認這個有意義的表達式是否符合返回類型. 考慮這樣的一個源程序:

    boolean doSomething()
    {
        // Some VarDecls
        // Some Statements
        return 666;
    }

    很明顯, 實際返回類型和聲明的返回類型不一致, 咱們應當給出相應的錯誤提示
    此外, 在Cva中, void返回類型的方法, 咱們容許不顯式return, 也能夠在方法結束時 使用return; 語句

    Error: Line 3 the return Expr ression's type is not match the method "DoSomething" declared.

  • 標識符 / 字面量 / this / new cvaIdentifierExpr r()

    這裏是討論剩餘的幾類特殊的表達式, 變量/字段引用, 字面量, this關鍵字, 實例化對象表達式.

    • 標識符

      查找標識符大體分爲兩步, 首先在參數列表/本地變量表查找該標識符, 若查找失敗, 再去類/基類字段聲明列表中嘗試查找.
      若最終查找失敗, 則報一個錯.

      Error: Line 1 you should declare "x" before use it.

    • 字面量 / this

      這個種類主要包括常量(數字整形字面量, true ,false), this關鍵字. 應當特別注意this關鍵字,
      它指代當前類的實例, 它的類型天然是該關鍵字所處的類的類型.

    • new cvaIdentifierExpr r()

      應當注意到本程序不支持構造函數, 所以每一個類只有一個形式上的無參構造器. 除主類外,
      對於其餘普通類的聲明順序不作要求. 若是嘗試實例化一個不存在的類, 也會報告錯誤.

      `Error: Line 1 cannot find the declaration of class "XXX".`

語句

有了前面表達式級的類型檢查做爲基礎, 作語句的類型檢查就很方便快捷了.

  • write

    上文(writeExpr )已給出解釋

  • if / while / for

    對於這種類型的語句, 只須要檢查是否符合對應的規則便可. 例如條件判斷處必須是個布爾類型的表達式

  • {StatementList}

    這種類型的表達式, 按順序輪流進行檢查便可

  • cvaIdentifierExpr r = Expr r;

    賦值語句, 只要等號兩側的類型是互相匹配的, 那就容許賦值.

特別注意

應當注意到, 咱們以前提到的一直是「類型匹配」, 而不是「類型相等」. 隱含的, 咱們容許合法的類型隱式轉換.

其餘檢查

在本程序裏, 咱們還作了另外兩個對於標識符的檢查:變量/字段標識符(CvaIdentifierExpr expr),
類名標識符(也是identifier). 因爲這兩類標識符存在於不一樣的符號表裏面, 所以本程序能夠聲明相似
SomeThing SomeThing; 這樣類型和名稱同樣的變量/字段,這種聲明是合法的, 在使用時具體的意義取決於這個符號所在位置的語義.

錯誤恢復

前面已經提到, 在這個階段, 咱們要在一遍掃描中給出儘量多的信息, 所以咱們須要實現錯誤恢復功能. 出現的具體錯誤和恢復思想以下

  • 使用了未聲明的變量

    一旦發現某處使用了未使用的變量, 那麼會當即給出信息提示此處發現一個未聲明變量, 可是分析仍是要繼續, 因而原地定義該變量是
    一個 unkonwn 類型, 使用該類型, 進行接下來的分析.

  • 操做符型表達式

    例如 true + 10, !200這種類型的錯誤, 咱們優先考慮操做符的語義, 例如在咱們的程序中, 加減乘一定得出一個整數,
    比較運算一定得出布爾類型的值. 咱們假定該操做符被正確使用並得出結果, 而後進行下面的分析.

  • 方法調用

    方法調用出現了問題, 例如參數不全、參數類型不匹配, 咱們給出應當提示用戶的信息以後, 假定方法正常調用, 按照該有的返回值進行下面的分析.

相關文章
相關標籤/搜索