Parser:读出 identifier 和 let
本节阅读量:
上一节先看了目标结构:
1
2
|
Var(name)
Let(name, value, body)
|
现在看 parser 怎样从 token 组装出这些节点。
第二章的 lexer 仍然很朴素。它只负责:
1
2
|
给括号两边补空格。
按空白切出字符串 token。
|
比如:
会切成:
1
|
["(", "let", "x", "40", "(", "+", "x", "2", ")", ")"]
|
这些 token 只是字符串。let 是特殊形式,x 是变量名,这些判断都在 parser 里完成。
把 identifier 规则写成函数
代码在:
1
|
code/02_let/src/front/parser.cpp
|
上一节已经把 identifier 定成“一个或多个字母”。parser 只需要把这条规则写成判断函数:
1
2
3
4
5
6
7
8
9
10
11
12
|
bool is_identifier(const std::string& text) {
if (text.empty()) {
return false;
}
for (char c : text) {
if (!std::isalpha(static_cast<unsigned char>(c))) {
return false;
}
}
return true;
}
|
空字符串不是名字。其余情况逐个检查字符,只要有一个字符不是字母,就返回 false。所以 x 可以通过,x1 和 + 不能通过。
完整的 parse_expr
Parser 保存 token 和当前位置的方式没有变化,parse_program()、peek()、advance()、expect() 也沿用第一章。第二章的主要变化集中在 parse_expr():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
std::unique_ptr<Expr> Parser::parse_expr() {
std::string token = advance();
if (is_integer(token)) {
return std::make_unique<IntExpr>(std::stol(token));
}
if (is_identifier(token)) {
return std::make_unique<VarExpr>(token);
}
if (token == "(") {
const std::string& head = peek();
if (head == "+") {
advance();
auto lhs = parse_expr();
auto rhs = parse_expr();
expect(")", "expected ')' after addition");
return std::make_unique<AddExpr>(std::move(lhs), std::move(rhs));
}
if (head == "let") {
advance();
std::string name = expect_identifier("expected variable name after 'let'");
auto value = parse_expr();
auto body = parse_expr();
expect(")", "expected ')' after let body");
return std::make_unique<LetExpr>(name, std::move(value), std::move(body));
}
throw std::runtime_error("expected '+' or 'let' after '('");
}
throw std::runtime_error(
"expected integer, variable, '(+ expr expr)', or '(let name expr expr)'");
}
|
按顺序看,它只有四种结果:
1
2
3
4
|
整数 -> IntExpr
identifier -> VarExpr
左括号 -> 根据 head 解析 + 或 let
其他 token -> 报错
|
加法分支沿用第一章。新的变量分支只生成 VarExpr,不会检查名字有没有绑定;绑定检查要等解释器或 IR lowerer 查环境时完成。
let 分支先读取绑定名,再递归读取 value 和 body。这里调用两次 parse_expr(),因为 value 和 body 都可以是任意表达式。
expect_identifier 读取绑定名
expect_identifier 检查下一个 token 是不是合法名字,并把名字返回给调用者:
1
2
3
4
5
6
|
std::string Parser::expect_identifier(const char* message) {
if (current_ >= tokens_.size() || !is_identifier(tokens_[current_])) {
throw std::runtime_error(message);
}
return advance();
}
|
第一章的 expect() 只检查并读走固定 token,所以返回 void。这里还要把绑定名交给 LetExpr,因此 expect_identifier() 返回 std::string。
例如:
会按这个顺序读:
1
2
3
4
5
6
|
读到 "("
head 是 "let"
绑定名是 "x"
value 是 Int(40)
body 是 Add(Var(x), Int(2))
最后读到 ")"
|
组合成:
1
|
Let(x, Int(40), Add(Var(x), Int(2)))
|
常见错误从哪里报出来
几个非法程序可以这样看:
1
2
3
4
|
(let 1 40 1) let 后面期待 identifier,却看到 1
(let x 40) 读完 value 后还要读 body,却遇到 )
(let x 1 2 3) 读完 body 后期待 ),却看到 3
(* 2 3) 读到 ( 后,head 不是 + 或 let
|
这些错误仍然来自 parser 的固定期待:当前位置不符合语法规则,就停下来报错。
试一下递归解析
运行:
1
2
3
|
cd code/02_let
make
./mini ast examples/let_value.lang
|
输出:
1
|
Let(x, Add(Int(20), Int(20)), Add(Var(x), Int(2)))
|
这个输出说明 let 的 value 和 body 都通过 parse_expr() 递归解析:value 变成一个 Add,body 也变成一个 Add。
到这里,源码已经能变成带变量和 let 的 AST。下一节让解释器沿着这棵树求值。