C語言編譯器開發之旅(二):解析器

毅澤發表於2021-06-09

本節是我們這個編譯器系列的第二節,進入語法分析與語義分析的部分解。在本節我們會編寫一個簡單的解析器。

解析器的主要功能分為兩個部分:

  • 識別輸入的語法元素生成AST(Abstract Syntax Trees)並確保輸入符合語法規則
  • 解析AST並計算表示式的值

在開始程式碼編寫之前,請先了解本節最重要的的兩個知識點。

抽象語法樹(AST):https://blog.csdn.net/lockhou/article/details/109700312

巴科斯-瑙爾正規化(BNF):https://www.bilibili.com/video/BV1Us411h72K?from=search&seid=2377033397008241337

我們需要識別的元素包含四個基本的數學運算子+-,*,/與十進位制整數共五個語法元素。那麼首先讓我們為我們的解析器將識別的語言定義一個語法。我麼這裡採用BNF描述:

expression: number
          | expression '*' expression
          | expression '/' expression
          | expression '+' expression
          | expression '-' expression
          ;

number:  T_INTLIT
         ;

我們都知道BNF定義的語法是遞迴定義的,那麼我們也需要一個遞迴函式去解析輸入的表示式。在我們現有的語法元素可以構成的表示式中第一個語法元素始終為數字,否則就是語法錯誤。其後可能是一個運算子,或者只有一個數字。那麼我們可以用如下虛擬碼表示我們的遞迴下降解析函式:

function expression() {
  Scan and check the first token is a number. Error if it's not
  Get the next token
  If we have reached the end of the input, return, i.e. base case

  Otherwise, call expression()
}

讓我們來模擬一次此函式的執行,輸入為2 + 3 - 5 T_EOF其中T_EOF 是反映輸入結束的標記。

expression0:
  Scan in the 2, it's a number
  Get next token, +, which isn't T_EOF
  Call expression()

    expression1:
      Scan in the 3, it's a number
      Get next token, -, which isn't T_EOF
      Call expression()

        expression2:
          Scan in the 5, it's a number
          Get next token, T_EOF, so return from expression2

      return from expression1
  return from expression0

為了進行語義分析,我們需要程式碼來解釋識別的輸入,或者將其轉換為另一種格式,例如彙編程式碼。在旅程的這一部分,我們將為輸入構建一個直譯器。但要實現這一目標,我們首先要將輸入轉換為抽象語法樹。

抽象語法樹的節點結構定義如下:

// defs.h
// AST node types
enum {
  A_ADD, A_SUBTRACT, A_MULTIPLY, A_DIVIDE, A_INTLIT
};

// Abstract Syntax Tree structure
struct ASTnode {
  int op;                               // "Operation" to be performed on this tree
  struct ASTnode *left;                 // Left and right child trees
  struct ASTnode *right;
  int intvalue;                         // For A_INTLIT, the integer value
};

節點元素op表示該節點的型別,當op的值為A_ADDA_SUBTRACT等運算子時,該節點具有左右兩顆子樹,我們將使用op代表的運算子對左右兩棵子樹的值做計算;當op的值為A_INTLIT時,代表該節點是整數值,是葉節點,節點元素intvalue儲存著該整數的值。

tree.c 中的程式碼具有構建 AST 的功能。函式mkastnode()生成一個節點並返回指向節點的指標:

// tree.c
// Build and return a generic AST node
struct ASTnode *mkastnode(int op, struct ASTnode *left,
                          struct ASTnode *right, int intvalue) {
  struct ASTnode *n;

  // Malloc a new ASTnode
  n = (struct ASTnode *) malloc(sizeof(struct ASTnode));
  if (n == NULL) {
    fprintf(stderr, "Unable to malloc in mkastnode()\n");
    exit(1);
  }
  // Copy in the field values and return it
  n->op = op;
  n->left = left;
  n->right = right;
  n->intvalue = intvalue;
  return (n);
}

我們對其進一步封裝出兩個常用的函式,分別用來建立左子樹與葉節點:

// Make an AST leaf node
struct ASTnode *mkastleaf(int op, int intvalue) {
  return (mkastnode(op, NULL, NULL, intvalue));
}

// Make a unary AST node: only one child
struct ASTnode *mkastunary(int op, struct ASTnode *left, int intvalue) {
  return (mkastnode(op, left, NULL, intvalue));

我們將使用 AST 來儲存我們識別的每個表示式,以便稍後我們可以遞迴遍歷它來計算表示式的最終值。 我們確實想處理數學運算子的優先順序。 這是一個例子。 考慮表示式 2 * 3 4 * 5。現在,乘法比加法具有更高的優先順序。 因此,我們希望將乘法運算元繫結在一起並在進行加法之前執行這些操作。

如果我們生成 AST 樹看起來像這樣:

          +
         / \
        /   \
       /     \
      *       *
     / \     / \
    2   3   4   5

然後,在遍歷樹時,我們會先執行 2 * 3,然後是 4 * 5。一旦我們有了這些結果,我們就可以將它們傳遞給樹的根來執行加法。

在開始解析語法樹之前,我們需要一個將掃描到的token轉換為AST節點操作值的函式,如下:

// expr.c
// Convert a token into an AST operation.
int arithop(int tok) {
  switch (tok) {
    case T_PLUS:
      return (A_ADD);
    case T_MINUS:
      return (A_SUBTRACT);
    case T_STAR:
      return (A_MULTIPLY);
    case T_SLASH:
      return (A_DIVIDE);
    default:
      fprintf(stderr, "unknown token in arithop() on line %d\n", Line);
      exit(1);
  }
}

我們需要一個函式來檢查下一個標記是否是整數文字,並構建一個 AST 節點來儲存文字值。如下:

// Parse a primary factor and return an
// AST node representing it.
static struct ASTnode *primary(void) {
  struct ASTnode *n;

  // For an INTLIT token, make a leaf AST node for it
  // and scan in the next token. Otherwise, a syntax error
  // for any other token type.
  switch (Token.token) {
    case T_INTLIT:
      n = mkastleaf(A_INTLIT, Token.intvalue);
      scan(&Token);
      return (n);
    default:
      fprintf(stderr, "syntax error on line %d\n", Line);
      exit(1);
  }
}

這裡的Token是一個全域性變數,儲存著掃描到的最新的值。

那麼我們現在可以寫解析輸入表示式生成AST的方法:

// Return an AST tree whose root is a binary operator
struct ASTnode *binexpr(void) {
  struct ASTnode *n, *left, *right;
  int nodetype;

  // Get the integer literal on the left.
  // Fetch the next token at the same time.
  left = primary();

  // If no tokens left, return just the left node
  if (Token.token == T_EOF)
    return (left);

  // Convert the token into a node type
  nodetype = arithop(Token.token);

  // Get the next token in
  scan(&Token);

  // Recursively get the right-hand tree
  right = binexpr();

  // Now build a tree with both sub-trees
  n = mkastnode(nodetype, left, right, 0);
  return (n);
}

這只是一個子簡單的解析器,他的解析結果沒有實現優先順序的調整,解析結果如下:

     *
    / \
   2   +
      / \
     3   *
        / \
       4   5

正確的樹狀結構應該是這樣的:

          +
         / \
        /   \
       /     \
      *       *
     / \     / \
    2   3   4   5

我們將在下一節實現生成一個正確的AST。

那麼接下來我們來試著寫程式碼遞迴的解釋這顆AST。我們以正確的語法樹為例,虛擬碼:

interpretTree:
  First, interpret the left-hand sub-tree and get its value
  Then, interpret the right-hand sub-tree and get its value
  Perform the operation in the node at the root of our tree
  on the two sub-tree values, and return this value

呼叫過程可以用如下過程表示:

interpretTree0(tree with +):
  Call interpretTree1(left tree with *):
     Call interpretTree2(tree with 2):
       No maths operation, just return 2
     Call interpretTree3(tree with 3):
       No maths operation, just return 3
     Perform 2 * 3, return 6

  Call interpretTree1(right tree with *):
     Call interpretTree2(tree with 4):
       No maths operation, just return 4
     Call interpretTree3(tree with 5):
       No maths operation, just return 5
     Perform 4 * 5, return 20

  Perform 6 + 20, return 26

這是在 interp.c 中並依據上述虛擬碼寫的功能:

// Given an AST, interpret the
// operators in it and return
// a final value.
int interpretAST(struct ASTnode *n) {
  int leftval, rightval;

  // Get the left and right sub-tree values
  if (n->left)
    leftval = interpretAST(n->left);
  if (n->right)
    rightval = interpretAST(n->right);

  switch (n->op) {
    case A_ADD:
      return (leftval + rightval);
    case A_SUBTRACT:
      return (leftval - rightval);
    case A_MULTIPLY:
      return (leftval * rightval);
    case A_DIVIDE:
      return (leftval / rightval);
    case A_INTLIT:
      return (n->intvalue);
    default:
      fprintf(stderr, "Unknown AST operator %d\n", n->op);
      exit(1);
  }
}

這裡還有一些其他程式碼,比如呼叫 main() 中的直譯器:

  scan(&Token);                 // Get the first token from the input
  n = binexpr();                // Parse the expression in the file
  printf("%d\n", interpretAST(n));      // Calculate the final result
  exit(0);

本節內容到此結束。在下一節中我們將修改解析器,讓其對錶達式進行語義分析計算正確的結果。

本文Github地址:https://github.com/Shaw9379/acwj/tree/master/02_Parser

相關文章