章节目录

Parser:特殊形式之外就是调用

本节阅读量:

第四章不需要修改 lexer。它仍然给左右括号补空格,再按空白切分 token。

贯穿示例:

1
2
(let inc (lambda x (+ x 1))
    (inc 41))

会被切成:

1
2
["(", "let", "inc", "(", "lambda", "x", "(", "+", "x", "1", ")", ")",
 "(", "inc", "41", ")", ")"]

lexer 只看字符,不知道其中哪一对括号表示 let,哪一对表示 lambda 或调用。这个判断由 parse_expr() 完成。

左括号后的分派规则

前三章已经在看到 "(" 后检查 head。第四章保留全部旧分支,再增加 lambda:

1
2
3
4
5
6
7
8
head 是 +         解析两个操作数,产生 AddExpr
head 是 let       解析名字、value、body,产生 LetExpr
head 是 eq?       解析两个操作数,产生 EqExpr
head 是 if        解析条件和两个分支,产生 IfExpr
head 是 lambda    解析参数和函数体,产生 LambdaExpr
head 是 cin       章末可选实验:要求立即结束,产生 InputExpr
head 是 cout      章末可选实验:解析一个值,产生 OutputExpr
没有命中特殊形式  保持在左括号后的当前位置,完整解析 callee 和 argument,产生 CallExpr

第一次学习函数时可以先忽略 cin、cout 两行;它们只服务于章末的 C++ runtime 联调。之所以也列在这里,是因为 code/04_functions 是完成整章后的代码快照。

代码先查看 head,但暂时不统一把它消费掉:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
if (token == "(") {
    const std::string& head = peek();

    if (head == "+") {
        // 已有的加法分支
    }

    if (head == "let") {
        // 已有的 let 分支
    }

    if (head == "eq?") {
        // 已有的 eq? 分支
    }

    if (head == "if") {
        // 已有的 if 分支
    }

    // lambda、章末可选的 cin/cout,以及最后的调用分支
}

之所以只 peek(),是因为特殊形式要把 head 当作固定单词消费,而普通调用不能提前 advance():它要让同一个 parse_expr() 从左括号后的当前位置开始,完整解析 callee。callee 可能只是 inc,也可能是另一个括号表达式。

这些检查必须放在普通调用之前。否则 (if condition then else) 会先落入调用分支,被误当成参数数量不对的调用;eq? 也根本不是普通 identifier。固定特殊形式优先,普通调用兜底,这就是本章括号表达式的完整优先级。

lambda 是固定特殊形式

源码:

1
(lambda x (+ x 1))

对应分支与实现完全一致:

1
2
3
4
5
6
7
if (head == "lambda") {
    advance();
    std::string param = expect_identifier("expected parameter name after 'lambda'");
    auto body = parse_expr();
    expect(")", "expected ')' after lambda body");
    return std::make_unique<LambdaExpr>(param, std::move(body));
}

按顺序看每一步:

1
2
3
4
advance()              消费固定 head:lambda
expect_identifier()    读取恰好一个参数名 x
parse_expr()           读取恰好一个函数体
expect(")")            要求 lambda 到此结束

普通 identifier 规则没有扩大,参数名仍然必须是一个或多个英文字母。x、value 合法,x1、my_value、x? 都不合法。eq? 仍然只作为固定 head 识别。

let、if 和 lambda 由 head 位置决定是否是特殊形式。因此 (let lambda 1 lambda) 可以把 lambda 当作普通名字绑定和读取,但 (lambda 41) 一定先进入 lambda 特殊形式分支,不会被理解成“调用名为 lambda 的变量”。这是当前小语言有意采用的解析优先级。

剩余的二元素括号表达式就是调用

调用没有 call 关键字:

1
(inc 41)

特殊形式都没有匹配后,parser 按下面的流程解析普通调用:

1
2
3
4
5
6
7
auto callee = parse_expr();
if (current_ >= tokens_.size() || peek() == ")") {
    throw std::runtime_error("expected function argument");
}
auto argument = parse_expr();
expect(")", "expected ')' after function argument");
return std::make_unique<CallExpr>(std::move(callee), std::move(argument));

中间的检查让 (inc) 得到直接的“缺少实参”错误。其余代码在语法上保证括号内恰好有两个表达式,但不会判断 callee 最后是不是函数。因此 (42 1) 也能得到合法 AST:

1
Call(Int(42), Int(1))

“不能调用整数”属于求值阶段的类型错误,不属于 parser 错误。

直接调用为什么也能读出来

考虑:

1
((lambda x (+ x 1)) 41)

外层左括号后的 token 又是 "(",不等于任何固定 head,于是进入普通调用分支:

1
2
3
4
5
6
7
8
9
parse callee
  看到内层 (
  head 是 lambda
  得到 Lambda(x, Add(Var(x), Int(1)))

parse argument
  得到 Int(41)

得到 Call(Lambda(...), Int(41))

同理,((make 0) 41) 的 callee 会先递归解析成一个 CallExpr。这正是 AST 把 callee 设计成 std::unique_ptr<Expr> 的原因。

贯穿示例怎样经过分派

1
2
(let inc (lambda x (+ x 1))
    (inc 41))

解析顺序是:

1
2
3
4
5
6
7
8
外层 head = let
├── name: inc
├── value 的 head = lambda
│   ├── param: x
│   └── body 的 head = +
└── body 的 head = inc,不是特殊形式
    ├── callee: Var(inc)
    └── argument: Int(41)

最终得到:

1
Let(inc, Lambda(x, Add(Var(x), Int(1))), Call(Var(inc), Int(41)))

参数个数和括号错误

本章只有单参数 lambda 和单参数调用。parser 通过“读取固定数量的表达式,然后立刻期待右括号”落实这个限制:

1
2
3
4
5
6
7
源码                         结果
(lambda 42)                  expected parameter name after 'lambda'
(lambda x y (+ x y))         expected ')' after lambda body
(lambda x 42 7)              expected ')' after lambda body
(inc)                         expected function argument
(inc 41 42)                  expected ')' after function argument
(inc 41                      expected ')' after function argument

第一行说明零参数 lambda 不在语法内;第二行说明 parser 读完唯一参数 x 后,会把 y 当成 body,随后因为没有立刻遇到右括号而拒绝多参数写法。

调用缺少参数也会直接失败。例如 (inc) 在 parser 读完 callee 后立即遇到 ),会报告:

1
expected function argument

parse_program() 还会检查根表达式后是否有多余 token,因此两个并列的顶层表达式也不会被悄悄忽略:

1
expected end of input

实际检查

1
2
3
4
cd code/04_functions
make
./mini ast examples/function_value.lang
./mini ast examples/call.lang

确认 parser 能稳定产生 Lambda 和 Call 后,下一步才让解释器赋予它们运行时含义。


4.2 AST:保存函数和调用的结构

上一节

4.4 解释器:函数值和一次新的求值环境

下一节