语句
目前我们的解析器已经初具规模了。我们已经实现了数字和字符串的解析,实现了空白符和注释的跳过。在这篇文章中,我们继续添加 语句 的解析。
语句是表示某种操作的独立代码单元。在很多编程语言中,语句以分号结束,我们也采用这种方式。
程序代码由多条语句组成。所以如代码清单 1 所示,我们修改程序的文法定义,现在程序由 StatementList 组成。
- /**
- * Main entry point
- *
- * Program
- * : StatementList
- * ;
- */
- Program() {
- return {
- type: 'Program',
- body: this.StatementList(),
- };
- }
如代码清单 2 所示,StatementList 的文法定义是左递归形式的,不断用“StatementList Statement”替换“StatementList”,可以看到 StatementList 就是由多条 Statement 组成的。
理论化的左递归转右递归,看着过于繁杂。实际中的概念非常简单。
- /**
- * StatementList
- * : Statement
- * | StatementList Statement
- * ;
- */
- StatementList() {
- const statementList = [this.Statement()];
- while (this._lookahead != null) {
- statementList.push(this.Statement());
- }
- return statementList;
- }
如代码清单 3 所示,目前语句只包含表达式语句。
- /**
- * Statement
- * : ExpressionStatement
- * ;
- */
- Statement() {
- return this.ExpressionStatement();
- }
如代码清单 4 所示,表达式语句的文法是表达式,后接分号。所以我们此时需要“吃掉”一个分号并移进到下一个 token。
- /**
- * ExpressionStatement
- * : Expression ';'
- * ;
- */
- ExpressionStatement() {
- const expression = this.Expression();
- this._eat(';');
- return {
- type: 'ExpressionStatement',
- expression,
- };
- }
如代码清单 5 所示,目前表达式就是之前实现的字面量。
- /**
- * Expression
- * : Literal
- * ;
- */
- Expression() {
- return this.Literal();
- }
现在分号已经需要具有 token 含义了,所以如代码清单 5 所示,我们添加分号 token 解析(第 14 行)。
- /**
- * Tokenizer spec.
- */
- const Spec = [
- // Whitespace:
- [/^\s+/, null],
- // Single-line comments:
- [/^\/\/.*/, null],
- // Multi-line comments:
- [/^\/\*[\s\S]*?\*\//, null],
- // Symbols, delimiters
- [/^;/, ';'],
- // Numbers:
- [/^\d+/, 'NUMBER'],
- // Strings:
- [/^"[^"]*"/, 'STRING'],
- [/^'[^']*'/, 'STRING'],
- ];
AST 节点的设计可以参照 astexplorer.net 这个网站。
增加测试用例
我们引入 TDD(Test-Driven Development)开发模式,即测试驱动开发。在此之前,我们需要把目前已经实现了的功能,补上测试用例。
如代码清单 7 所示,我们指定字面量的测试用例。
- module.exports = test => {
- test(`42;`, {
- type: 'Program',
- body: [
- {
- type: 'ExpressionStatement',
- expression: {
- type: 'NumericLiteral',
- value: 42,
- }
- }
- ]
- });
- test(`"hello";`, {
- type: 'Program',
- body: [
- {
- type: 'ExpressionStatement',
- expression: {
- type: 'StringLiteral',
- value: 'hello',
- }
- }
- ]
- });
- test(`'hello';`, {
- type: 'Program',
- body: [
- {
- type: 'ExpressionStatement',
- expression: {
- type: 'StringLiteral',
- value: 'hello',
- }
- }
- ]
- });
- };
顺带学 JavaScript:
这边导出的是箭头函数,基本语法为 (parameters) => { // 函数体 };。如果只有一个参数,可以省略参数周围的括号。
箭头函数可以按 C++ 的 lambda 函数理解。此处传入的 test 参数,可以当成函数指针理解。
代码清单 8 是本节语句解析的测试用例。
- module.exports = test => {
- test(
- `
- "hello";
- // Number
- 42;
- `,
- {
- type: 'Program',
- body: [
- {
- type: 'ExpressionStatement',
- expression: {
- type: 'StringLiteral',
- value: 'hello',
- }
- },
- {
- type: 'ExpressionStatement',
- expression: {
- type: 'NumericLiteral',
- value: 42,
- }
- }
- ]
- });
- };
最后,我们来看如何使用测试用例。如代码清单 9 所示,我们首先定义需要传递的“基础”测试函数 test,它解析 input 字符串,将得到的 ast 和 expected 进行比较,如果不一致就代表测试失败。
我们将多个测试函数导出,存放在 tests 数组,并用 forEach 方法依次遍历调用。
- const tests = [
- require("./literals-test.js"),
- require("./statement-list-test.js")
- ];
- function test(program, expected) {
- const ast = parser.parse(program);
- assert.deepEqual(ast, expected);
- }
- tests.forEach(testRun => testRun(test));