1.2 预备工作 - Source

按照前文的介绍,我们本应该探索如何实现词法解析了。在进行词法分析器的讲解之前,我们最好还是先介绍辅助类 Source。

在读取字符串的过程中,我们需要统一换行符以及支持先行预测「Lookahead」,需要这些功能的原因后面会介绍。

Source 类的结构如下:

为了简化源文件的处理过程,我们将需要处理的源文件一次性整个地读取到属性 code 中,其余属性的含义分别为:

在 JS 中,可以通过下标来检索字符串中的字符。所以我们可以通过属性 ofst 作为下标来检索 code 中的字符,即 this.code[this.ofst] 的方式。

ch 表示我们当前读取到的字符。

ofst 被设定为指向当前字符 chcode 中的索引位置。

比如,对于字符串 abcd 而言,当 cha 时,ofst 的值为 0。为了读取下一个字符 b,我们需要先将 ofst 加 1,以加 1 后的值作为索引来检索字符,即:

this.ofst += 1;
this.ch = this.code[this.ofst];

因此,ofst 的初始值被我们设定为 -1

在不断的读入字符的过程中,我们还需要维护行列号,行列号主要用作生成调试信息。

Source 类目前只包含两个方法,分别是 readpeak。它们的作用都是读取给定数量的字符,组成字符串后返回给调用者。区别在于,read 方法会改变位置信息,包括 chofstlinecol,而 peek 方法则不然。

我们使用 peek 方法来做先行预测,在出现可能分叉的地方进行使用。比如 Lua 中注释以 -- 开头,我们可以直接向前 peek 两个字符,看看它们是否符合条件。

为了使读者能够快速地运行文中的代码,所有代码都可以在 node v11.5.0 下直接运行而不需要借助 Babel 之类的编译器。当然读者还是需要在运行前注意一下代码中的 require 路径是否和自己的环境相匹配。

下面给出 Source 的代码和注释:

// 不同的操作系统可能使用不同的字符作为换行字符,常见的情况有
// \n, \r, \r\n 三种。
// \n 叫做「LF,Linefeed」存在于「*nix,Unix-like」系统中
// \r 叫做「Carriage Return」存在于老式的 MacOS 中
// \r\n 存在于 Windows 中
// 这里我们使用「NL,newline」作为换成的变量名,因为下面会将三种情况都统一成它。
const NL = "\n";
const CR = "\r";
const EOL = "\n";
// 0x03 在 ascii 表中的含义即为 end of text
const EOF = "\x03";

class Position {
  constructor(ofst, line, col) {
    this.ofst;
    this.line = line;
    this.col = col;
  }
}

class Source {
  // 我们的构造函数接收源文件代码,和源文件名
  // 源文件名用于生产调试信息
  constructor(code = "", file = "stdin") {
    this.code = code;
    this.file = file;
    this.ch = "";
    this.ofst = -1;
    this.line = 1;
    this.col = 0;
    this.isPeek = false;
  }

  read(cnt = 1) {
    const ret = [];
    let ofst = this.ofst;
    let c;
    while (cnt) {
      const next = ofst + 1;
      c = this.code[next];
      if (c === undefined) {
        c = EOF;
        ret.push(c);
        break;
      }
      ofst = next;
      // 这里我们将可能的三种换行都统一为 *nix 换行符 '\n'
      if (c === CR || c === NL) {
        if (c === CR && this.code[next + 1] === NL) ofst++;
        // 识别为换行之后,如果处于 read 模式,则需要更新行列号
        // 将行号加 1,表示进入了下一行
        // 将列号重置,表示此时处于新行的行首
        if (!this.isPeek) {
          this.line++;
          this.col = 0;
        }
        c = EOL;
      } else if (!this.isPeek) this.col++;
      ret.push(c);
      cnt--;
    }
    if (!this.isPeek) {
      this.ch = c;
      this.ofst = ofst;
    }
    return ret.join("");
  }

  // 由于 peek 逻辑除了不需要更新位置信息外、和 read 逻辑一致,因此我们通过
  // 设定 `isPeek` 属性使得在 read 内部得以区分当前状态
  peek(cnt = 1) {
    this.isPeek = true;
    const ret = this.read(cnt);
    this.isPeek = false;
    return ret;
  }

  getPos() {
    return new Position(this.ofst, this.line, this.col);
  }
}

module.exports = {
  NL,
  CR,
  EOL,
  EOF,
  Position,
  Source
};

对于类「Position」和方法 Source::getPos(),很明显它们是分别用于存放和获取 Source 的位置信息的。

目前为止,我们 Source 类的功能有这几点:

  1. 所有对源码的读取操作将通过 Source 类的实例来完成

  2. 我们可以在读取的过程中维护当前的读取位置

  3. 我们将不同的换行符都进行了统一,返回 EOL

  4. 我们还提供了预读功能,就是在不改变当前位置信息的情况下,往前读取几个字符。

Last updated