Parser:在括号 head 位置识别 `sub1` 和 `letrec`
本节阅读量:第五章不需要修改 lexer。它仍然只给左右括号补空格,再按空白切分 token,并不知道递归是什么。
贯穿示例:
|
|
会被切成下面这串 token:
|
|
lexer 会原样保留 letrec、sub1 和 eq?。它们是不是特殊形式,要由 parse_expr() 查看左括号后的 head 决定。
特殊形式优先,普通调用兜底
parser 读到左括号后,先用 peek() 查看 head,但不立刻统一消费:
|
|
第五章完整快照的分派顺序是:
|
|
这些特殊形式必须放在普通调用之前。否则 (letrec sum n ... ...) 会被误当作“调用名为 letrec 的函数”,sub1 也会落进普通调用分支。
普通调用不能提前消费 head,因为 callee 本身可能是任意表达式。例如 ((lambda x x) 42) 的 callee 是另一个括号表达式。保留当前位置,再递归调用 parse_expr(),才能读出第四章已经支持的动态 callee。
解析 sub1
sub1 是一个固定的一元特殊形式。实现位于 code/05_recursion/src/front/parser.cpp:
|
|
逐步看这四行:
|
|
因此:
|
|
parser 只检查形状,不检查操作数最终是不是数字。(sub1 (lambda x x)) 可以产生 AST,之后由解释器在求值时报告类型错误。
解析 letrec
letrec 分支依次读取两个名字和两个表达式:
|
|
读取顺序与 AST 字段顺序完全相同:
|
|
parser 不会在这里执行 function_body,也不会建立 name -> function 的环境。它只创建 LetRecExpr。自绑定属于下一节解释器和后面的 IR lowering。
贯穿示例怎样递归下降
对 recursive_sum.lang,最外层解析过程是:
|
|
两个 (sum ...) 的 head 都是 sum,没有命中固定特殊形式,所以由最后的普通调用分支生成 CallExpr。(sub1 n) 则命中新的特殊形式分支,生成 Sub1Expr。
最终根节点是:
|
|
普通 identifier 仍然只是 letter+
函数名和参数名都通过 expect_identifier() 读取。底层检查没有因 sub1 或 letrec 扩大:
|
|
所以:
|
|
sub1 含有数字,eq? 含有问号;它们只由 parser 在括号 head 位置按固定拼写识别。它们不是读者可以拿来当普通变量名的 identifier。
letrec 本身只含英文字母。当前 parser 没有全局“保留字表”,所以它在普通变量位置可以被读成 identifier;但只要出现在括号 head 位置,就一定优先进入 letrec 特殊形式分支,而不会被理解成普通 callee。教程示例避免把特殊形式的拼写用作用户变量名,以免制造无关困惑。
固定读取数量就是 arity 检查
sub1 必须有一个操作数,letrec 必须有两个名字和两个表达式。parser 读取固定数量后立刻要求右括号,从而拒绝多余部分。
下面是当前实现的实际错误信息:
| 源码 | parser 报错 |
|---|---|
(sub1) |
expected integer, variable, arithmetic, let, letrec, eq?, if, lambda, or function call |
(sub1 1 2) |
expected ')' after sub1 expression |
(letrec 1 n 0 0) |
expected function name after 'letrec' |
(letrec sum n1 0 0) |
expected parameter name after letrec function name |
(letrec sum n 0) |
expected integer, variable, arithmetic, let, letrec, eq?, if, lambda, or function call |
(letrec sum n 0 1 2) |
expected ')' after letrec body |
缺少操作数或 body 时,parse_expr() 会在本应出现表达式的位置遇到 ),因此当前快照给出通用的“expected …”错误;多写内容时,则会由紧随其后的 expect(")") 给出更具体的右括号错误。
函数名和参数名相同并不是 arity 或 token 形状错误,所以 parser 会接受 (letrec f f f (f 1))。参数怎样遮蔽递归函数名属于环境语义,上一节已经说明。
parse_program() 还会检查根表达式后是否有剩余 token。两个并列的顶层表达式不会被悄悄忽略,而会报告:
|
|
实际运行 parser
在第五章目录运行:
|
|
输出中应该同时出现:
|
|
到这里,源码已经稳定变成包含 Sub1Expr 和 LetRecExpr 的 AST。下一步才会让解释器为这些节点定义行为:sub1 怎样检查数字,以及递归函数值怎样记录 self name、在调用时建立指向当前 callee 的自绑定。