C語言編譯器開發之旅(一):詞法分析掃描器

毅澤發表於2021-06-04

本節我們先從一個簡易的可以識別四則運算和整數值的詞法分析掃描器開始。它實現的功能也很簡單,就是讀取我們給定的檔案,並識別出檔案中的token將其輸出。

這個簡易的掃描器支援的詞法元素只有五個:

  • 四個基本的算術運算子:+-*/
  • 十進位制整數

我們需要事先定義好每一個token,使用列舉型別來表示:

//defs.h

// Tokens
enum {
  T_PLUS, T_MINUS, T_STAR, T_SLASH, T_INTLIT
};

在掃描到token後將其儲存在一個如下的結構體中,當標記是 T_INTLIT(即整數文字)時,該intvalue 欄位將儲存我們掃描的整數值:

//defs.h

// Token structure
struct token {
  int token;
  int intvalue;
};

我們現在假定有一個檔案,其內部的的程式碼就是一個四則運算表示式:

2 + 34 * 5 - 8 / 3

我們要實現的是讀取他的每一個有效字元並輸出,就像這樣:

Token intlit, value 2
Token +
Token intlit, value 34
Token *
Token intlit, value 5
Token -
Token intlit, value 8
Token /
Token intlit, value 3

我們看到了最終要實現的目標,讓我們來一步步分析需要的功能。

  1. 首先我們需要一個逐字元的讀出檔案中的內容並返回的函式。當我們在輸入流中讀的太遠時,需要將讀取到的字元放回(如上例當讀到數字時,因無法直接獲取數字是否結束,只能迴圈讀取,當讀到第一個非數字字元時則判定該十進位制數讀取結束,需將該十進位制數返回並將讀取的非數字字元放回),記錄行號的的功能也是在這裡實現。
// Get the next character from the input file.
static int next(void) {
  int c;

  if (Putback) {                // Use the character put
    c = Putback;                // back if there is one
    Putback = 0;
    return c;
  }

  c = fgetc(Infile);            // Read from input file
  if ('\n' == c)
    Line++;                     // Increment line count
  return c;
}
  1. 我們只需要有效字元,所以需要去除空白字元的功能
// Skip past input that we don't need to deal with, 
// i.e. whitespace, newlines. Return the first
// character we do need to deal with.
static int skip(void) {
  int c;

  c = next();
  while (' ' == c || '\t' == c || '\n' == c || '\r' == c || '\f' == c) {
    c = next();
  }
  return (c);
}

  1. 當讀到的是數字的時候,怎麼確定數字有多少位呢?所以我們需要一個專門處理數字的函式。
// Return the position of character c
// in string s, or -1 if c not found
static int chrpos(char *s, int c) {
  char *p;

  p = strchr(s, c);
  return (p ? p - s : -1);
}


// Scan and return an integer literal
// value from the input file. Store
// the value as a string in Text.
static int scanint(int c) {
  int k, val = 0;

  // Convert each character into an int value
  while ((k = chrpos("0123456789", c)) >= 0) { 
    val = val * 10 + k;
    c = next();
  }

  // We hit a non-integer character, put it back.
  putback(c);
  return val;
}

所以現在我們可以在跳過空格的同時讀取字元;如果我們讀到一個字元太遠,我們也可以放回一個字元。我們現在可以編寫我們的第一個詞法掃描器:

int scan(struct token *t) {
  int c;

  // Skip whitespace
  c = skip();

  // Determine the token based on
  // the input character
  switch (c) {
  case EOF:
    return (0);
  case '+':
    t->token = T_PLUS;
    break;
  case '-':
    t->token = T_MINUS;
    break;
  case '*':
    t->token = T_STAR;
    break;
  case '/':
    t->token = T_SLASH;
    break;
  default:

    // If it's a digit, scan the
    // literal integer value in
    if (isdigit(c)) {
      t->intvalue = scanint(c);
      t->token = T_INTLIT;
      break;
    }

    printf("Unrecognised character %c on line %d\n", c, Line);
    exit(1);
  }
  // We found a token
  return (1);
}

現在我們可以讀取token並將其返回。

main() 函式開啟一個檔案,然後掃描它的令牌:

void main(int argc, char *argv[]) {
  ...
  init();
  ...
  Infile = fopen(argv[1], "r");
  ...
  scanfile();
  exit(0);
}

scanfile()在有新token時迴圈並列印出token的詳細資訊:

// List of printable tokens
char *tokstr[] = { "+", "-", "*", "/", "intlit" };

// Loop scanning in all the tokens in the input file.
// Print out details of each token found.
static void scanfile() {
  struct token T;

  while (scan(&T)) {
    printf("Token %s", tokstr[T.token]);
    if (T.token == T_INTLIT)
      printf(", value %d", T.intvalue);
    printf("\n");
  }
}

我們本節的內容就到此為止。下一部分中,我們將構建一個解析器來解釋我們輸入檔案的語法,並計算並列印出每個檔案的最終值。

本文Github地址:https://github.com/Shaw9379/acwj/tree/master/01_Scanner

相關文章