Janos

导航

ACWJ_00扫描器

第一部分:词法扫描介绍

​ 我们从一个简单的词汇扫描器开始我们的编译器编写之旅。正如我在之前部分所提到的,扫描器的任务是从输入语言中(用来编译的语句)识别词法元素或者是符号。

​ 我们将定义一个只有5种词法元素的输入语言:

  • 四个基本的数学符号:*, /, +-
  • 有1个或者多个数字的十进制数字0 .. 9

​ 我们所要扫描的每一个符号将会被存放于以下的结构中(来自defs.h

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

​ 其中的token域可以是下列枚举结构中的任一个(来自defs.h

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

​ 当符号是一个T_INTLIT(一个整数符), 那intvalue变量将会存放我们所扫到的这个整数值

scan.c中的函数

scan.c文件中有着词法扫描器的函数代码。我们将从输入文件中一次读入一个字符。然而,在实际情况中会多次遇到我们从输入流里多读了一个字符需要将他“放回去”的情况(这里作者用的一个Putback全局变量时刻保存着多读出的那个字符)。我们同样也希望跟踪我们目前读到了输入文件的第几行,如此我们可以在我们的调试信息中打印出来具体的行号。所有上述的这些都在函数next()中得以完成。

// 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;
}

PutbackLine和全局输入文件指针变量都被定义在 data.h头文件中。

extern_ int     Line;
extern_ int     Putback;
extern_ FILE    *Infile;

​ 所有声明了extern_宏的C文件都将能够使用上面的这些变量

​ 最后,我们如何把一个多读出来的字符放回到输入流中呢?像这样:

// Put back an unwanted character
static void putback(int c) {
  Putback = c;
}

忽略空白字符

​ 我们需要一个函数去读取并且悄悄地跳过所有空格字符直到读到了一个非空格字符并且将其返回,像下面这样:

// 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);
}

扫描符号:scan()

​ 那现在我们可以读取字符并且同时跳过输入流里的空格字符。当我们超读了一个字符的时候,也可以将其放回去。现在我们可以编写我们的第一个词法扫描器如下:

// Scan and return the next token found in the input.
// Return 1 if token valid, 0 if no tokens left.
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:
    // More here soon
  }

  // We found a token
  return (1);
}

​ 这就是简单的单字符处理:对于每一个所识别到的字符,将其转化为token结构体变量的token对应成员。你可能会问:为什么不直接把识别到的字符放入struct token中当作成员呢?答案是之后我们会需要去识别多字符符号比如==if 以及while关键字。所以说用枚举列表去列出符号值会比较省力一些。

整数数值

​ 事实上,我们不得不面对这样的情况:去识别诸如382787731这样的整数数值。下面是上述代码块switch里default处缺失的代码处理:

  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);

​ 当我们击中一个整数字符的时候,我们调用辅助函数 scanint()处理。它将会返回被扫描的整数数值。要做到这一点,他需要依次读取从这个数字开始后面的每一个字符,检查它们是否是合法的数字,并且组建好最终的数值返回,下面是实现:

// 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;
}

​ 我们把val 值初始化为0。每次我们获取到一个09的数字字符,我们用函数`chrpos()`将它转换为`int`值。我们把`val`值乘以10然后再加上它在09序列中的位置,也就是它自己实际值。

​ 比如说,如果我们有这三个连续的字符读取3, 2, 8,我们这样做:

  • val= 0 * 10 + 3, i.e. 3
  • val= 3 * 10 + 2, i.e. 32
  • val= 32 * 10 + 8, i.e. 328

​ 在上述代码的最后部分,你有没有发现putback(c)的调用?程序走到这里的时候我们发现一个字符并不是十进制数子。我们不能简单地将它直接抛弃,幸运的是,我们可以将它放回源输入中供以后使用。

​ 你可能在这个时候也会问:为什么不简单地把每一个输入字符减去对应的'0'的ASCII码值来得到他的整数值呢?答案是,之后我们可能也会使用chrpos("0123456789abcdef") 这样的调用去转换十六进制数字。(09的ASCII码和af的可差得远呢)

​ 下面是函数chrpos()的实现:

// 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.c中的实现。

让扫描器工作起来

main.c 中的代码让上述的扫描器开始工作起来。main.() 函数会打开一个文件并且扫描其中的符号。

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

​ 并且scanfile() 函数中有个循环不停地读取新符号,并将他的详细信息打印出来。

// 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");
  }
}

一些输入例子文件

​ 我提供了一些输入文件的例子便于你们去观察发现扫描器在每个文件中获取到了哪些符号,并且观察扫描器具体拒绝了哪些输入格式的文件。

$ make
cc -o scanner -g main.c scan.c

$ cat input01
2 + 3 * 5 - 8 / 3

$ ./scanner input01
Token intlit, value 2
Token +
Token intlit, value 3
Token *
Token intlit, value 5
Token -
Token intlit, value 8
Token /
Token intlit, value 3

$ cat input04
23 +
18 -
45.6 * 2
/ 18

$ ./scanner input04
Token intlit, value 23
Token +
Token intlit, value 18
Token -
Token intlit, value 45
Unrecognised character . on line 3

总结和展望

​ 我们向前迈进了一小步,并且我们有了一个简单的词法扫描器,可以识别四个主要的数学符号和整数数字。我们注意到了我们需要跳过输入流里的空白字符和将超读的字符放回输入流。

​ 单字符符号很容易扫描,但是多字符连在一起的符号就有一点难度了。但是在最后, scan()函数返回了输入流中的下一个字符存储于一个传入的struct token参数变量中。

struct token {
  int token;
  int intvalue;
};

​ 在编译器编写旅程中的下一章节,我们会编写一个递归下降分析器去翻译我们输入文件里的语法,并且计算和打印每个文件里的表达式的最终的值。

posted on 2023-01-03 20:31  Janoskz  阅读(97)  评论(0编辑  收藏  举报