实现方法指导
实现一个 parser 有很多种方法,这里会提供一些关于代码实现(而不是理论)的方法指导。
对于没有讲到的内容,可以参考 去年的指导书
一些通用的部分
类型定义
对于词法、语法分析时用到的类型,因为类型确定且已知,可以使用继承实现。在支持和类型 (sum type) 的语言里也可以用和类型实现。这样做可以显著降低判断 token 或者语法树节点类型时的工作量,因为可以直接判断变量本身的类型,甚至直接进行模式匹配。比如:
/* 词法分析器 */
class Token {}
class NumberToken : Token {
public double value;
}
// ...
/* 语法分析器 */
class Expr {}
class Literal : Expr {}
class IntegerLiteral : Literal {
public long value;
}
class StringLiteral : Literal {
public string value;
}
class BinaryExpr : Expr {
public Operator op;
public Expr lhs;
public Expr rhs;
}
// ...
或者在支持的语言里使用带标签的联合类型:
enum Expr {
Literal(LiteralExpr),
Binary(BinaryExpr),
// ...
}
enum LiteralExpr {
Integer(i64),
String(String),
// ...
}
struct BinaryExpr {
pub op: Operator,
pub lhs: Ptr<Expr>,
pub rhs: Ptr<Expr>,
}
// ...
迭代器
迭代器(Iterator)是对一系列值的抽象,比如说一列输入的字符或者解析完的 token。使用迭代器可以有效地将输入数据和对数据的获取操作解耦,方便在不同时候使用不同方式输入数据,以及进行测试。常见高级语言都有对于迭代器的抽象,包括:
- Java:
java.util.Iterator
- C#:
System.Collections.Generic.IEnumerator
- C++:
std::iterator::iterator_traits
- C++20: concept
std::ranges::input_iterator
- Python: 实现
__next__
的类型 - JavaScript: 实现
Symbol.iterator
的类型
由于在解析时常常要回溯,使用的迭代器可以提供一些额外的方法,比如 peek()
用于查看下一个值但不移动迭代器,或者 unread(value)
用于将已经读出的值放回迭代器。
词法分析
词法分析这个主题比较简单,基本上就是对于每个 token 使用自动机(或者退化成普通的逻辑分支)进行解析。token 的组成一般比较简单,可以在分析时参考正则表达式的状态来设计自动机或逻辑分支。
当然,也有一些库允许你直接用正则表达式定义 token 来进行自动分析。好耶。
不要学助教用逻辑分支模拟自动机(逃
语法分析
普通的递归下降分析法
递归下降是一个很简单、很直观的分析法,也是大多数人实现语法分析的首选方法。在实现递归下降分析器的时候,有一些可以降低编码难度的方法。
使用迭代器和辅助函数
看 miniplc0 java 版本基本上就够了(逃)
解析器组合子 (Parser Combinator)
助教没有试过这么写,如果你用 Haskell 来写的话或许可以试试 parsec
这个库。
使用 LL/LR 解析器生成器
自动生成解析器代码总感觉有点作弊的意思,不过用了就用了吧(笑)。如果你确定要用的话,记得选一个好用的,比如 ANTLR。