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
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 关键字:
特殊形式都没有匹配后,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:
“不能调用整数”属于求值阶段的类型错误,不属于 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
2
3
4
|
cd code/04_functions
make
./mini ast examples/function_value.lang
./mini ast examples/call.lang
|
确认 parser 能稳定产生 Lambda 和 Call 后,下一步才让解释器赋予它们运行时含义。