本篇为 mini-dog-c 编译器开发系列第四篇,介绍递归下降解析器的原理与实现。
1. 什么是递归下降解析器?
递归下降(Recursive Descent Parsing) 是最简单直观的语法分析方法。它的核心思想是:每个语法规则对应一个函数,函数负责消费(匹配)该规则对应的 Token 序列,并构建相应的 AST 节点。
相比之下,传统的解析器生成工具(如 yacc/bison)需要写一套 .y 语法文件,然后用工具生成 C 代码。递归下降不需要任何外部工具,直接用 C 代码表达语法规则,代码即语法。
语法规则:
add_expr ::= mul_expr (('+' | '-') mul_expr)*
代码实现:
static ASTNode *parse_additive_expr(Parser *parser) {
ASTNode *left = parse_multiplicative_expr(parser);
while (match(parser, TOKEN_PLUS) || match(parser, TOKEN_MINUS)) {
TokenType op = previous()->type;
ASTNode *right = parse_multiplicative_expr(parser);
left = ast_create_binary(op, left, right);
}
return left;
}
一个规则 + 一个函数,代码和语法一一对应,这就是递归下降最大的优点。
2. 解析器数据结构
typedef struct {
TokenList *tokens; // Token 列表
int current; // 当前扫描位置索引
Error error; // 错误信息
} Parser;
current 指向下一个要处理的 Token。peek() 查看当前 Token,advance() 消费并前进,match() 匹配并前进:
static Token *peek(Parser *parser) {
return parser->tokens->data[parser->current];
}
static Token *advance(Parser *parser) {
if (!is_at_end(parser))
parser->current++;
return parser->tokens->data[parser->current - 1];
}
static bool match(Parser *parser, TokenType type) {
if (check(parser, type)) {
advance(parser);
return true;
}
return false;
}
static bool check(Parser *parser, TokenType type) {
return peek(parser)->type == type;
}
3. 表达式解析:优先级与结合性
表达式解析是递归下降的核心。mini-dog-c 支持以下优先级层次(从低到高):
| 优先级 | 表达式类型 | 操作符 |
|---|---|---|
| 1(最低) | 赋值表达式 | = |
| 2 | 逻辑或 | || |
| 3 | 逻辑与 | && |
| 4 | 相等性 | == != |
| 5 | 关系 | < > |
| 6 | 加减 | + - |
| 7 | 乘除 | * / |
| 8(最高) | 一元 | ! - |
| 9 | 基本表达式 | 字面量、标识符、括号 |
递归下降按照优先级从低到高依次实现各层函数,高优先级层递归调用低优先级层:
parse_assignment_expr (优先级1)
└── parse_logical_or_expr (优先级2)
└── parse_logical_and_expr (优先级3)
└── parse_equality_expr (优先级4)
└── parse_relational_expr (优先级5)
└── parse_additive_expr (优先级6)
└── parse_multiplicative_expr (优先级7)
└── parse_unary_expr (优先级8)
└── parse_primary (优先级9)
以加法表达式为例:
static ASTNode *parse_additive_expr(Parser *parser) {
ASTNode *expr = parse_multiplicative_expr(parser);
while (match(parser, TOKEN_PLUS) || match(parser, TOKEN_MINUS)) {
TokenType op = parser->tokens->data[parser->current - 1]->type;
ASTNode *right = parse_multiplicative_expr(parser);
expr = ast_create_binary(op, expr, right);
}
return expr;
}
while 循环处理左结合的多个同优先级操作(如 1 - 2 - 3),每次迭代把新的二元表达式节点作为左操作数,层层包裹,最终形成一棵左深树:
1 - 2 - 3 → ((1 - 2) - 3)
BinaryExpr(-)
├── BinaryExpr(-)
│ ├── IntLiteral: 1
│ └── IntLiteral: 2
└── IntLiteral: 3
4. 赋值表达式
赋值表达式是优先级最低的表达式,支持链式赋值:
static ASTNode *parse_assignment_expr(Parser *parser) {
ASTNode *expr = parse_logical_or_expr(parser);
if (match(parser, TOKEN_ASSIGN)) {
if (expr->type == AST_IDENTIFIER) {
ASTNode *value = parse_assignment_expr(parser);
return ast_create_assign(expr->data.identifier.name, value);
} else {
error_at_current(parser, "赋值操作符左侧必须是标识符");
return expr;
}
}
return expr;
}
注意两点:
-
右侧是递归调用
parse_assignment_expr而不是parse_logical_or_expr,这使得赋值右结合:a = b = c被解析为a = (b = c) -
赋值左侧必须是标识符,如果不是则报错
5. 主表达式(Primary)
static ASTNode *parse_primary(Parser *parser) {
// 布尔字面量
if (match(parser, TOKEN_BOOL_LITERAL)) {
Token *token = parser->tokens->data[parser->current - 1];
return ast_create_bool(token->value.bool_value);
}
// 整数字面量
if (match(parser, TOKEN_INT_LITERAL)) {
Token *token = parser->tokens->data[parser->current - 1];
return ast_create_int(token->value.int_value);
}
// 浮点数字面量
if (match(parser, TOKEN_DOUBLE_LITERAL)) {
Token *token = parser->tokens->data[parser->current - 1];
return ast_create_double(token->value.double_value);
}
// 字符字面量
if (match(parser, TOKEN_CHAR_LITERAL)) {
Token *token = parser->tokens->data[parser->current - 1];
return ast_create_char(token->value.char_value);
}
// 字符串字面量
if (match(parser, TOKEN_STRING_LITERAL)) {
Token *token = parser->tokens->data[parser->current - 1];
return ast_create_string(token->value.string_value);
}
// 标识符或函数调用
if (match(parser, TOKEN_IDENT)) {
char *name = parser->tokens->data[parser->current - 1]->lexeme;
// 检查是否是函数调用
if (match(parser, TOKEN_LPAREN)) {
int arg_count;
ASTNode **args = parse_arguments(parser, &arg_count);
consume(parser, TOKEN_RPAREN, "期望 ')'");
ASTNode *call = ast_create_call(name, args, arg_count);
free(name);
free(args);
return call;
}
return ast_create_identifier(name);
}
// 分组表达式
if (match(parser, TOKEN_LPAREN)) {
ASTNode *expr = parse_assignment_expr(parser);
consume(parser, TOKEN_RPAREN, "期望 ')'");
return expr;
}
error_at_current(parser, "期望表达式");
advance(parser);
return ast_create_int(0);
}
这里有个巧妙的处理:标识符后面跟 ( 说明是函数调用,否则就是普通标识符引用。
6. 语句解析
6.1 let 声明
static ASTNode *parse_let_decl(Parser *parser) {
if (!match(parser, TOKEN_LET)) return NULL;
Token *name_token = advance(parser);
char *name = strdup_custom(name_token->lexeme);
consume(parser, TOKEN_ASSIGN, "期望 '='");
ASTNode *initializer = parse_expression(parser);
consume(parser, TOKEN_SEMICOLON, "期望 ';'");
return ast_create_var_decl(name, initializer);
}
顺序:let → 标识符 → = → 表达式 → ;
6.2 函数声明
static ASTNode *parse_fn_decl(Parser *parser) {
if (!match(parser, TOKEN_FN)) return NULL;
Token *name_token = advance(parser);
char *name = strdup_custom(name_token->lexeme);
consume(parser, TOKEN_LPAREN, "期望 '('");
// 解析参数列表
char **params = NULL;
int param_count = 0;
if (!check(parser, TOKEN_RPAREN)) {
params = (char **)malloc(sizeof(char *) * 16);
while (check(parser, TOKEN_IDENT)) {
Token *param_token = advance(parser);
params[param_count] = strdup_custom(param_token->lexeme);
param_count++;
if (!match(parser, TOKEN_COMMA)) break;
}
}
consume(parser, TOKEN_RPAREN, "期望 ')'");
ASTNode *body = parse_block(parser);
return ast_create_fn_decl(name, params, param_count, body);
}
6.3 if 语句
static ASTNode *parse_if_stmt(Parser *parser) {
if (!match(parser, TOKEN_IF)) return NULL;
consume(parser, TOKEN_LPAREN, "期望 '('");
ASTNode *condition = parse_expression(parser);
consume(parser, TOKEN_RPAREN, "期望 ')'");
ASTNode *then_branch = parse_block(parser);
ASTNode *else_branch = NULL;
if (match(parser, TOKEN_ELSE))
else_branch = parse_block(parser);
return ast_create_if(condition, then_branch, else_branch);
}
6.4 代码块
static ASTNode *parse_block(Parser *parser) {
consume(parser, TOKEN_LBRACE, "期望 '{'");
ASTNode **statements = (ASTNode **)malloc(sizeof(ASTNode *) * 64);
int count = 0;
while (!check(parser, TOKEN_RBRACE) && !is_at_end(parser)) {
ASTNode *stmt = parse_statement(parser);
if (stmt)
statements[count++] = stmt;
}
consume(parser, TOKEN_RBRACE, "期望 '}'");
return ast_create_block(statements, count);
}
6.5 主解析循环
ASTNode *parser_parse(Parser *parser) {
ASTNode *program = ast_create_program();
while (!is_at_end(parser)) {
ASTNode *stmt = parse_statement(parser);
if (stmt)
ast_program_add_stmt(program, stmt);
else
advance(parser); // 跳过无法解析的 Token
// 错误恢复:跳过到分号或 EOF
if (parser->error.has_error) {
while (!check(parser, TOKEN_SEMICOLON) && !is_at_end(parser))
advance(parser);
if (check(parser, TOKEN_SEMICOLON)) advance(parser);
parser->error.has_error = false;
}
}
return program;
}
7. 错误恢复
解析错误不可避免,解析器采用恐慌模式(Panic Mode) 错误恢复策略:
-
检测到错误后,记录错误信息
-
跳过 Token 直到遇到分号
;或 EOF -
清除错误标记,继续解析后续语句
这样即使部分代码有语法错误,解析器仍能处理剩余代码,报告尽可能多的错误。
8. 测试
测试 tests/test_parser.c 的思路:准备一些源代码字符串,解析后检查 AST 的结构:
static void test_if_statement(void) {
const char *source = "if (x > 0) { return 1; } else { return 0; }";
Lexer *lexer = lexer_create(source);
TokenList *tokens = lexer_tokenize(lexer);
Parser *parser = parser_create(tokens);
ASTNode *ast = parser_parse(parser);
ASTNode *stmt = ast->data.program.statements[0];
assert(stmt->type == AST_EXPR_STMT);
ASTNode *if_stmt = stmt->data.return_stmt.value; // 复用 union
assert(if_stmt->type == AST_IF_STMT);
assert(if_stmt->data.if_stmt.then_branch != NULL);
assert(if_stmt->data.if_stmt.else_branch != NULL);
ast_free(ast);
parser_free(parser);
token_list_free(tokens);
lexer_free(lexer);
printf("test_if_statement PASSED\n");
}
9. 小结
本篇介绍了递归下降解析器的核心思想与实现:
-
每个语法规则对应一个函数,代码即语法
-
表达式通过分层递归实现优先级:优先级低的在外层,优先级高的在内层
-
使用
while循环处理左结合 :如a - b - c→((a - b) - c) -
赋值右结合:右侧递归调用赋值解析函数
-
恐慌模式错误恢复:出错后跳到分号或 EOF,继续解析
下一篇我们将实现解释器,完成对 AST 的遍历执行。