mini-dog-c编译器开发 - 04 递归下降解析器

本篇为 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;
}

注意两点:

  1. 右侧是递归调用 parse_assignment_expr 而不是 parse_logical_or_expr,这使得赋值右结合:a = b = c 被解析为 a = (b = c)

  2. 赋值左侧必须是标识符,如果不是则报错

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) 错误恢复策略:

  1. 检测到错误后,记录错误信息

  2. 跳过 Token 直到遇到分号 ; 或 EOF

  3. 清除错误标记,继续解析后续语句

这样即使部分代码有语法错误,解析器仍能处理剩余代码,报告尽可能多的错误。

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 的遍历执行。

相关推荐
无限进步_2 小时前
二叉搜索树完全解析:从概念到实现与应用场景
c语言·开发语言·数据结构·c++·算法·github·visual studio
shada3 小时前
mini-dog-c编译器开发 - 01 功能需求与设计
编译器
顾鉴行思3 小时前
10 字符串常量到底存在哪里?
c语言·汇编·经验分享
Aurorar0rua4 小时前
CS50 x 2024 Notes C - 09
c语言·开发语言·学习方法
shada5 小时前
mini-dog-c编译器开发 - 03 抽象语法树(AST)
编译器
相醉为友5 小时前
040 Linux/裸机/RTOS 项目开发的跨平台兼容性——C语言静态接口抽象底层原理分析
linux·c语言·mcu
weixin_421725267 小时前
2026年C/C++/C#全解析:底层语言的进化与场景抉择,选错直接掉队
c语言·c++·c·编程语言·技术选择
bucenggaibian7 小时前
Nearoh:9年开发者从零造语言,Python的简洁+C的性能
c语言·python·开发者·编程语言·nearoh
水饺编程7 小时前
第5章,[标签 Win32] :设备的尺寸(三)
c语言·c++·windows·visual studio