> For the complete documentation index, see [llms.txt](https://hsiaosiyuan0.gitbook.io/icj/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hsiaosiyuan0.gitbook.io/icj/part1/1-2-source.md).

# 1.2 预备工作 - Source

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

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

Source 类的结构如下：

![](/files/-LgNHY865T2oiIKtrl1O)

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

| 属性     | 作用                 |
| ------ | ------------------ |
| file   | 源文件名               |
| ch     | 当前偏移量指代的字符         |
| ofst   | 当前的偏移量             |
| line   | 当前所在行号             |
| col    | 当前所在列号             |
| isPeek | 用来标识当前是否处于 peek 模式 |

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

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

`ofst` 被设定为指向当前字符 `ch` 在 `code` 中的索引位置。

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

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

因此，`ofst` 的初始值被我们设定为 `-1`。

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

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

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

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

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

```javascript
// 不同的操作系统可能使用不同的字符作为换行字符，常见的情况有
// \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. 我们还提供了预读功能，就是在不改变当前位置信息的情况下，往前读取几个字符。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://hsiaosiyuan0.gitbook.io/icj/part1/1-2-source.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
