1. 程式人生 > >從零寫一個編譯器(六):語法分析之表驅動語法分析

從零寫一個編譯器(六):語法分析之表驅動語法分析

專案的完整程式碼在 C2j-Compiler

前言

上一篇已經正式的完成了有限狀態自動機的構建和足夠判斷reduce的資訊,接下來的任務就是根據這個有限狀態自動機來完成語法分析表和根據這個表來實現語法分析

reduce資訊

在完成語法分析表之前,還差最後一個任務,那就是描述reduce資訊,來指導自動機是否該進行reduce操作

reduce資訊在ProductionsStateNode各自的節點裡完成,只要遍歷節點裡的產生式,如果符號“.”位於表示式的末尾,那麼該節點即可根據該表示式以及表示式對應的lookAhead set得到reduce資訊

reduce資訊用一個map來表示,key是可以進行reduce的符號,也就是lookahead sets中的符合,value則是進行reduce操作的產生式

public HashMap<Integer, Integer> makeReduce() {
      HashMap<Integer, Integer> map = new HashMap<>();
      reduce(map, this.productions);
      reduce(map, this.mergedProduction);

      return map;
  }

  private void reduce(HashMap<Integer, Integer> map, ArrayList<Production> productions) {
      for (int i = 0; i < productions.size(); i++) {
          if (productions.get(i).canBeReduce()) {
              ArrayList<Integer> lookAhead = productions.get(i).getLookAheadSet();
              for (int j = 0; j < lookAhead.size(); j++) {
                  map.put(lookAhead.get(j), (productions.get(i).getProductionNum()));
              }
          }
      }
  }

語法分析表的構建

語法分析表的構建主要在StateNodeManager類裡,可以先忽略loadTable和storageTableToFile的邏輯,這一部分主要是為了儲存這張表,能夠多次使用

主要邏輯從while開始,遍歷所有節點,先從跳轉資訊的Map裡拿出跳轉關係和跳轉的目的節點,然後把這個跳轉關係(這個本質上對應的是一開始Token列舉的標號)和目的節點的標號拷貝到另一個map裡。接著拿到reduce資訊,找到之前對應在lookahead set裡的符號,把它們的value改寫成- (進行reduce操作的產生式編號),之所以寫成負數,就是為了區分shift操作。

所以HashMap<Integer, HashMap<Integer, Integer>>這個資料結構作為解析表表示:

  1. 第一個Integer表示當前節點的編號
  2. 第二個Integer表示輸入字元
  3. 第三個Integer表示,如果大於0則是做shift操作,小於0則根據推導式做reduce操作
public HashMap<Integer, HashMap<Integer, Integer>> getLrStateTable() {
      File table = new File("lrStateTable.sb");
      if (table.exists()) {
          return loadTable();
      }

      Iterator it;
      if (isTransitionTableCompressed) {
          it = compressedStateList.iterator();
      } else {
          it = stateList.iterator();
      }

      while (it.hasNext()) {
          ProductionsStateNode state = (ProductionsStateNode) it.next();
          HashMap<Integer, ProductionsStateNode> map = transitionMap.get(state);
          HashMap<Integer, Integer> jump = new HashMap<>();

          if (map != null) {
              for (Map.Entry<Integer, ProductionsStateNode> item : map.entrySet()) {
                  jump.put(item.getKey(), item.getValue().stateNum);
              }
          }

          HashMap<Integer, Integer> reduceMap = state.makeReduce();
          if (reduceMap.size() > 0) {
              for (Map.Entry<Integer, Integer> item : reduceMap.entrySet()) {

                  jump.put(item.getKey(), -(item.getValue()));
              }
          }

          lrStateTable.put(state.stateNum, jump);
      }

      storageTableToFile(lrStateTable);

      return lrStateTable;
  }

表驅動的語法分析

語法分析的主要過程在LRStateTableParser類裡,由parse方法啟動.

和第二篇講的一樣需要一個輸入堆疊,節點堆疊,其它的東西現在暫時不需要用到。在初始化的時候先把開始節點壓入堆疊,當前輸入字元設為EXT_DEF_LIST,然後拿到語法解析表

public LRStateTableParser(Lexer lexer) {
    this.lexer = lexer;
    statusStack.push(0);
    valueStack.push(null);
    lexer.advance();
    lexerInput = Token.EXT_DEF_LIST.ordinal();
    lrStateTable = StateNodeManager.getInstance().getLrStateTable();
}

語法解析的步驟:

  • 拿到當前節點和當前字元所對應的下一個操作,也就是action > 0是shift操作,action < 0是reduce操作
  • 如果進入action > 0,也就是shift操作
    1. 把當前狀態節點和輸入字元分別壓入堆疊
    2. 這裡要區分如果當前的字元是終結符,這時候就可以直接讀入下一個字元
    3. 但是這裡如果是非終結符,就應該直接用當前字元跳轉到下一個狀態。這裡是一個需要注意的一個點,這裡需要把當前的這個非終結符,放入到下一個節點的對應輸入堆疊中,這樣它進行reduce操作時彈出退棧的符號才是正確的
  • 如果action > 0,也就是reduce操作
    1. 拿到對應的產生式
    2. 把產生式右邊對應的狀態節點彈出堆疊
    3. 把完成reduce的這個符號放入輸入堆疊
public void parse() {
      while (true) {
          Integer action = getAction(statusStack.peek(), lexerInput);

          if (action == null) {
              ConsoleDebugColor.outlnPurple("Shift for input: " + Token.values()[lexerInput].toString());
              System.err.println("The input is denied");
              return;
          }

          if (action > 0) {
              statusStack.push(action);
              text = lexer.text;

              // if (lexerInput == Token.RELOP.ordinal()) {
              //     relOperatorText = text;
              // }

              parseStack.push(lexerInput);

              if (Token.isTerminal(lexerInput)) {
                  ConsoleDebugColor.outlnPurple("Shift for input: " + Token.values()[lexerInput].toString() + "   text: " + text);

                  // Object obj = takeActionForShift(lexerInput);

                  lexer.advance();
                  lexerInput = lexer.lookAhead;
                  // valueStack.push(obj);
              } else {
                  lexerInput = lexer.lookAhead;
              }
          } else {
              if (action == 0) {
                  ConsoleDebugColor.outlnPurple("The input can be accepted");
                  return;
              }

              int reduceProduction = -action;
              Production product = ProductionManager.getInstance().getProductionByIndex(reduceProduction);
              ConsoleDebugColor.outlnPurple("reduce by product: ");
              product.debugPrint();

              // takeActionForReduce(reduceProduction);

              int rightSize = product.getRight().size();
              while (rightSize > 0) {
                  parseStack.pop();
                  // valueStack.pop();
                  statusStack.pop();
                  rightSize--;
              }

              lexerInput = product.getLeft();
              parseStack.push(lexerInput);
              // valueStack.push(attributeForParentNode);
          }
      }
  }

  private Integer getAction(Integer currentState, Integer currentInput) {
      HashMap<Integer, Integer> jump = lrStateTable.get(currentState);
      return jump.get(currentInput);
  }

歧義性語法

到現在已經完成了語法分析的所有內容,接下來就是語義分析了,但是在這之前還有一個需要說的是,我們當前構造的有限狀態自動機屬於LALR(1)語法,即使LALR(1)語法已經足夠強大,但是依舊有LALR(1)語法處理不了的語法,如果給出的推導式不符合,那麼這個有限狀態自動機依舊不能正確解析,但是之前給出的語法都是符合LALR(1)語法的

小結

這一篇主要就是

  • 利用有限狀態自動機和reduce資訊完成語法解析表
  • 利用語法解析表實現表驅動的語法解析