您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 
 订阅
用 C 语言开发一门编程语言 — 抽象语法树
 
作者: JmilkFan 范桂飓

  502  次浏览      19 次
2023-9-8
 
编辑推荐:
本文使用 C 语言开发抽象语法树的结构以及抽象语法树与行为树,希望对您的学习有所帮助 。
本文来自于云物互联,由火龙果软件Alice编辑、推荐。

抽象语法树的结构

lispy> + 5 (* 2 2)
>
regex
operator|char:1:1 '+'
expr|number|regex:1:3 '5'
expr|>
char:1:5 '('
operator|char:1:6 '*'
expr|number|regex:1:8 '2'
expr|number|regex:1:10 '2'
char:1:11 ')'
regex

上篇我们通过 MPC 解析器组合库完成了读取输入,对波兰表达式的语法解析并得到表达式的 AST(抽象语法树),操作数(Number)和操作符(Operator)等需要被处理的有效数据都位于叶子节点上。而非叶子节点上则包含了遍历和求值的信息。

但是现在我们仍不能对它进行计算求值。在实现计算求值之前,我们先好好看看 AST 的结构:

typedef struct mpc_ast_t {
char *tag;
char *contents;
mpc_state_t state;
int children_num;
struct mpc_ast_t **children;
} mpc_ast_t;

  • tag :就是在节点内容之前的信息,它表示了解析这个节点时所用到的所有规则。例如: expr|number|regex 。tag 字段非常重要,因为它可以让我们知道创建节点时所匹配到的规则。
  • contents :包含了节点中具体的操作数和操作符内容,例如 * 、 ( 、 5 。你会发现,对于表示分支的非叶子节点,这个字段为空。而对于叶子节点,则包含了操作数或操作符的字符串形式。
  • state :这里面包含了解析器发现这个节点时所处的状态,例如行数和列数等信息。本书不会用到这个字段。
  • children_num 和 children :帮助我们来遍历 AST。前一个字段告诉我们有多少个子节点,后一个字段是包含这些节点的数组。其中,children 的数据类型为 mpc_ast_t ** 二重指针类型,是一个指针数组。

  • /* Load AST from output。
    * 因为 mpc_ast_t* 是指向结构体的指针类型,所以获取其字段的语法有些许不同。我们需要使用 -> 符号,而不是 . 符号。
    */
    mpc_ast_t *a = r.output;
    printf("Tag: %s\n", a->tag);
    printf("Contents: %s\n", a->contents);
    printf("Number of children: %i\n", a->children_num);
    /* Get First Child */
    mpc_ast_t *c0 = a->children[0];
    printf("First Child Tag: %s\n", c0->tag);
    printf("First Child Contents: %s\n", c0->contents);
    printf("First Child Number of children: %i\n", c0->children_num);

    使用递归来遍历树结构

    树形结构是自身重复的。树的每个子节点都是树,每个子节点的子节点也是树,以此类推。可见,树形结构也是递归和重复的。如果我们想编写函数处理所有可能的情况,就必须要保证函数可以处理任意深度,我们可以使用递归函数的天生优势来轻松地处理这种重复自身的结构。

    递归函数就是在执行的过程中调用自身的函数。理论上,递归函数会无穷尽地执行下去。但实际上,递归函数对于不同的输入会产生不同的输出,如果我们每次递归都改变或使用不同的输入,并设置递归终止的条件,我们就可以使用递归实现预期的效果。例如:使用递归来计算树形结构中节点个数。

    首先考虑最简单的情况,如果输入的树没有子节点,我们只需简单的返回 1 表示根节点就行了。如果输入的树有一个或多个子节点,这时返回的结果就是根节点再加上所有子节点的值。

    使用递归,遍历统计子节点的数量:

    int number_of_nodes(mpc_ast_t* t) {
    if (t->children_num == 0) { return 1; }
    if (t->children_num >= 1) {
    int total = 1;
    for (int i = 0; i < t->children_num; i++) {
    total = total + number_of_nodes(t->children[i]);
    }
    return total;
    }
    }

    实现求值计算

    lispy> + 5 (* 2 2)
    >
    regex
    operator|char:1:1 '+'
    expr|number|regex:1:3 '5'
    expr|>
    char:1:5 '('
    operator|char:1:6 '*'
    expr|number|regex:1:8 '2'
    expr|number|regex:1:10 '2'
    char:1:11 ')'
    regex

    在实现代码之前再好好总结一下 AST 输出的特征:

  • 有 number 标签的节点一定是一个数字,并且没有子节点。我们可以直接将其转换为一个数字。
  • 如果一个节点有 expr 标签,但没有 number 标签,那么第一个子节点永远是 ( 字符,最后一个子节点是 ) 字符。我们需要看他的第二个子节点是什么操作符,然后我们需要使用这个操作符来对后面的子节点进行求值。
  • 在对语法树进行求值的时候,还需要保存计算的结果。在这里,我们使用 C 语言中 long 类型。另外,为了检测节点的类型,或是为了获得节点中保存的数值,我们会用到节点中的 tag 和 contents 字段。这些字段都是字符串类型的。

    我们引入一些辅助性的库函数:

    我们可以使用 strcmp 来检查应该使用什么操作符,并使用 strstr 来检测 tag 中是否含有某个字段:


    int main(int argc, char *argv[]) {

    /* Create Some Parsers */
    mpc_parser_t *Number = mpc_new("number");
    mpc_parser_t *Operator = mpc_new("operator");
    mpc_parser_t *Expr = mpc_new("expr");
    mpc_parser_t *Lispy = mpc_new("lispy");

    /* Define them with the following Language */
    mpca_lang(MPCA_LANG_DEFAULT,
    " \
    number : /-?[0-9]+/ ; \
    operator : '+' | '-' | '*' | '/' ; \
    expr : <number> | '(' <operator> <expr>+ ')' ; \
    lispy : /^/ <operator> <expr>+ /$/ ; \
    ",
    Number, Operator, Expr, Lispy);

    puts("Lispy Version 0.1");
    puts("Press Ctrl+c to Exit\n");

    while(1) {
    char *input = NULL;

    input = readline("lispy> ");
    add_history(input);

    /* Attempt to parse the user input */
    mpc_result_t r;

    if (mpc_parse("<stdin>", input, Lispy, &r)) {
    /* On success print and delete the AST */
    long result = eval(r.output);
    printf("%li\n", result);
    mpc_ast_delete(r.output);
    } else {
    /* Otherwise print and delete the Error */
    mpc_err_print(r.error);
    mpc_err_delete(r.error);
    }

    free(input);

    }

    /* Undefine and delete our parsers */
    mpc_cleanup(4, Number, Operator, Expr, Lispy);

    return 0;
    }

    编译:

    gcc -std=c99 -Wall parsing.c mpc.c -lreadline -lm -o parsing

    运行:

    $ ./parsing
    Lispy Version 0.1
    Press Ctrl+c to Exit

    lispy> - (* 10 10) (+ 1 1 1)
    97
    lispy> + 5 6
    11

    抽象语法树与行为树

    行为树和抽象语法树之间有一个细微但非常重要的区别,我们应该区别对待(这促成了解析器的改写)。

    简单来说,行为树是带有上下文的 AST。上下文是一个函数返回的类型的信息,或者两个地方使用的变量实际上是相同的变量。 因为它需要弄清楚并记住所有这些上下文,生成行为树的代码需要大量的命名空间查找表和其他的东西。

    一旦我们有了行为树,运行代码就很容易了。 每个行为节点都有一个函数 “execute”,它接受一些输入,不管行为应该如何(包括可能调用子行为),返回行为的输出。 这是行为中的解释器。

       
    502 次浏览       19
    相关文章

    编译原理--C语言C文件和头文件的关系
    用 C 语言开发一门编程语言 — 抽象语法树
    C语言 | 嵌入式C语言编程规范
    详解C语言数组越界及其避免方法
     
    相关文档

    C语言-指针讲解
    详解C语言中的回调函数
    ARM下C语言编程解析
    设计模式的C语言实现
    相关课程

    C++高级编程
    C++并行编程与操作
    C++ 11,14,17,20新特性
    C/C++开发基础

    最新活动计划
    UAF架构体系与实践 5-17[北京]
    用户体验与界面设计 5-22[北京]
    MBSE(基于模型的系统工程)6-20[北京]
    大模型微调原理与实操 6-20[厦门]
    图数据库与知识图谱 6-27[北京]
    Linux内核编程及设备驱动 7-25[北京]
     
     
    最新文章
    编译原理--C语言C文件和头文件的关系
    用 C 语言开发一门编程语言 — 抽象语法树
    C语言 | 嵌入式C语言编程规范
    详解C语言数组越界及其避免方法
    最新课程
    C++高级编程
    C++并行编程与操作
    C++ 11,14,17,20新特性
    C/C++开发基础
    成功案例
    某航天科工单位 C++新特性与开发进阶
    北京 C#高级开发技术
    四方电气集团 嵌入式高级C语言编程
    北大方正 C语言单元测试实践