Parser:读出 identifier 和 let

本节阅读量:

上一节先看了目标结构:

1
2
Var(name)
Let(name, value, body)

现在看 parser 怎样从 token 组装出这些节点。

第二章的 lexer 仍然很朴素。它只负责:

1
2
给括号两边补空格。
按空白切出字符串 token。

比如:

1
(let x 40 (+ x 2))

会切成:

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
(let x 40 (+ x 2))

会按这个顺序读:

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。下一节让解释器沿着这棵树求值。


2.2 AST:给名字留位置

上一节

2.4 解释器:环境与变量查找

下一节