Parser:把 token 组成 AST
本节阅读量:上一节 lexer 已经把源码切成了 token。
比如源码:
|
|
会先变成:
|
|
但 token 只是小块,还不是结构。
parser 要做的是:
|
|
也就是把这一串 token 读成:
|
|
parser 不再关心字符
lexer 已经把连续字符切成了一个个 token,也处理了空白和括号边界。因此 parser 不需要在整段源码上寻找下一个小块从哪里开始、在哪里结束,只需要依次读取 token:
|
|
不过,token 目前仍然只是字符串。"40" 是否真的符合整数规则,仍然要由 parser 里的 is_integer 逐个检查字符。
parser 问的是:
|
|
第一章的语言规则是:
|
|
parser 的代码基本就是把这两条规则翻译成 C++。
Parser 记住当前位置
代码在:
|
|
Parser 这个类主要保存两个东西:
|
|
可以先这样理解:
|
|
如果 token 是:
|
|
刚开始:
|
|
读走一个 token 后:
|
|
parser 就是靠这个位置,一步一步往前读。
peek、advance 和 expect
先看三个小工具函数:
|
|
peek() 只看当前 token,不前进。
|
|
advance() 做两件事:
|
|
比如当前是:
|
|
调用 advance() 后:
|
|
再看 expect:
|
|
expect("+", "...") 的意思是:
|
|
它适合处理语法里固定必须出现的东西,比如:
|
|
parse_program 只允许一个表达式
对外使用 parser 时,调用的是:
|
|
parse 先调用 lex(source),拿到 token 列表,再创建 Parser。
真正读完整程序的是:
|
|
第一章规定:
|
|
所以 parse_program() 会先读一个表达式。读完以后,它还要检查后面没有多余 token。
比如:
|
|
可以。
但:
|
|
不可以。因为第一个表达式 42 已经读完了,后面还剩一个多余的 43。
parse_expr 对应语言规则
最核心的函数是 parse_expr():
|
|
先不要被 C++ 写法吓住。按语言规则看,它只有两种分支:
|
|
这正好对应:
|
|
换句话说,这整个函数就是把 expr 的两条规则写成两个 if。
如果 token 是整数,比如:
|
|
parser 会生成:
|
|
代码是:
|
|
is_integer(token) 判断字符串是不是整数形式。它允许:
|
|
但只有一个 "-" 不行,因为负号后面必须还有数字。
std::stol(token) 把字符串变成真正的整数值。std::make_unique<IntExpr>(...) 创建 IntExpr 节点,并把它交给 unique_ptr 管理。
如果第一个 token 是 "(",第一章只允许它是加法:
|
|
所以 parser 接下来期待看到 +:
|
|
然后读左边表达式:
|
|
再读右边表达式:
|
|
最后期待右括号:
|
|
如果这些都成功,就把左右两边包成一个加法节点:
|
|
这里的 lhs 和 rhs 本身也是 unique_ptr<Expr>。std::move(lhs) 和 std::move(rhs) 表示:
|
|
递归怎样读出嵌套结构
这两行最关键:
|
|
parse_expr() 的任务是读一个表达式。加法的左边和右边也都是表达式,所以它要调用自己。
例如:
|
|
外层加法可以拆成:
|
|
读左边时,parser 又调用一次 parse_expr(),把 (+ 1 2) 读成:
|
|
读右边时,再调用一次 parse_expr(),把 3 读成:
|
|
最后外层加法组合成:
|
|
把调用和游标前进放在一起看:
| 调用位置 | 读到什么 | 动作或结果 |
|---|---|---|
外层 parse_expr() |
"(" |
开始读取外层加法 |
左边 parse_expr() |
"(" |
递归读取 (+ 1 2),得到内层 Add |
右边 parse_expr() |
"3" |
得到 Int(3) |
| 回到外层 | ")" |
组合两个结果,得到外层 Add |
这不是炫技。递归正好对应第一节语言规则里的这句话:
|
|
这种写法通常叫递归下降 parser:每条语法规则对应一个读取函数,规则里再次出现 expr,代码就再次调用 parse_expr()。“递归下降”只是这个方法的名字,真正重要的是看见语法的递归结构怎样变成函数调用。
常见错误从哪里报出来
看几个非法程序,错误都来自刚才那些固定期待:
|
|
这些错误信息还没有行号和列号,也不追求一次指出所有问题。第一章先保证一件事:一旦接下来的输入无法满足当前语法规则,parser 就停下来报错,而不是勉强生成一棵错误的 AST。
试一下 parser
运行:
|
|
输出:
|
|
再运行:
|
|
输出:
|
|
这说明 parser 已经把 token 组装成了 AST。
到这里,源码已经从文本变成了结构。下一节就可以沿着这棵 AST 直接算结果了。