目录

文章目录

  • 目录
  • 前文列表
  • 抽象语法树的结构
    • 使用递归来遍历树结构
    • 实现求值计算
  • 抽象语法树与行为树

前文列表

《用 C 语言开发一门编程语言 — 交互式解析器l》
《用 C 语言开发一门编程语言 — 跨平台的可移植性》
《用 C 语言开发一门编程语言 — 语法解析器》

抽象语法树的结构

lispy> + 5 (* 2 2)
>regexoperator|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_numchildren:帮助我们来遍历 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)
>regexoperator|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 中是否含有某个字段:

#include <stdio.h>
#include <stdlib.h>
#include "mpc.h"#ifdef _WIN32
#include <string.h>static char buffer[2048];char *readline(char *prompt) {fputs(prompt, stdout);fgets(buffer, 2048, stdin);char *cpy = malloc(strlen(buffer) + 1);strcpy(cpy, buffer);cpy[strlen(cpy) - 1] = '\0';return cpy;
}void add_history(char *unused) {}#else#ifdef __linux__
#include <readline/readline.h>
#include <readline/history.h>
#endif#ifdef __MACH__
#include <readline/readline.h>
#endif#endif/* Use operator string to see which operation to perform */
long eval_op(long x, char *op, long y) {if (strcmp(op, "+") == 0) { return x + y; }if (strcmp(op, "-") == 0) { return x - y; }if (strcmp(op, "*") == 0) { return x * y; }if (strcmp(op, "/") == 0) { return x / y; }return 0;
}long eval(mpc_ast_t *t) {/* If tagged as number return it directly.* 有 number 标签的节点一定是一个数字,并且没有子节点* 直接将其转换为一个数字。*/if (strstr(t->tag, "number")) {return atoi(t->contents);}/* The operator is always second child.* 如果一个节点有 expr 标签,但没有 number 标签,那么它的第二个子节点肯定是操作符。* 这个操作符后面的子节点肯定是操作数。*/char *op = t->children[1]->contents;long x = eval(t->children[2]);/* 迭代剩余的子节点,并求值。 */int i = 3;while (strstr(t->children[i]->tag, "expr")) {x = eval_op(x, op, eval(t->children[i]));i++;}return x;
}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 Exitlispy> - (* 10 10) (+ 1 1 1)
97
lispy> + 5 6
11

抽象语法树与行为树


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

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

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

用 C 语言开发一门编程语言 — 抽象语法树相关推荐

  1. 用 C 语言开发一门编程语言 — 变量元素设计

    目录 文章目录 目录 前文列表 变量 变量语法规则 变量的读取和存储 将变量加入 Lisp Value 体系 变量的计算 变量的定义与赋值 异常处理优化 源代码 前文列表 <用 C 语言开发一门 ...

  2. 用 C 语言开发一门编程语言 — Q-表达式

    目录 文章目录 目录 前文列表 Q-表达式 读取并存储输入 实现 Q-Expression 语法解析器 读取 Q-Expression 实现 Q-Expression 的函数 Head & T ...

  3. 用 C 语言开发一门编程语言 — S-表达式

    目录 文章目录 目录 前文列表 使用 S-表达式进行重构 读取并存储输入 实现 S-Expression 语法解析器 实现 S-Expression 存储器 实现 lval 变量的构造函数 实现 lv ...

  4. 用 C 语言开发一门编程语言 — 异常处理

    目录 文章目录 目录 前文列表 异常捕获 定义 Lisp Value 函数 前文列表 <用 C 语言开发一门编程语言 - 交互式解析器l> <用 C 语言开发一门编程语言 - 跨平台 ...

  5. 用 C 语言开发一门编程语言 — 字符串与文件加载

    目录 文章目录 目录 前文列表 字符串 读取字符串 注释 文件加载函数 命令行参数 打印函数 报错函数 源代码 前文列表 <用 C 语言开发一门编程语言 - 交互式解析器> <用 C ...

  6. 用 C 语言开发一门编程语言 — 条件分支

    目录 文章目录 目录 前文列表 条件分支 排序函数 等于函数 if 函数 递归函数 源代码 前文列表 <用 C 语言开发一门编程语言 - 交互式解析器> <用 C 语言开发一门编程语 ...

  7. 用 C 语言开发一门编程语言 — 基于 Lambda 表达式的函数设计

    目录 文章目录 目录 前文列表 函数 Lambda 表达式 函数设计 函数的存储 实现 Lambda 函数 函数的运行环境 函数调用 可变长的函数参数 源代码 前文列表 <用 C 语言开发一门编 ...

  8. 用 C 语言开发一门编程语言 — 语法解析器

    目录 文章目录 目录 前文列表 编程语言的本质 词法分析 语法分析 使用 MPC 解析器组合库 安装 快速入门 实现波兰表达式的语法解析 波兰表达式 正则表达式 代码实现 前文列表 <用 C 语 ...

  9. 用 C 语言开发一门编程语言 — 跨平台的可移植性

    目录 文章目录 目录 前文列表 实现跨平台的可移植性 使用预处理器指令 前文列表 <用 C 语言开发一门编程语言 - 交互式解析器l> 实现跨平台的可移植性 理想情况下,我希望我的代码可以 ...

最新文章

  1. 逼自己玩命学了3个多月,吃透了Python技术核心!分享给你,让你今年进个大厂!...
  2. 2019宁波本科计算机招聘工资,@宁波人,76282!2019年度平均工资是这么多,你拖后腿了吗?...
  3. 子域名枚举工具Sublist3r
  4. MSE与MAE的区别与如何选择
  5. 壊小子的学习【日常阅读篇】(四)与阅读有关的那些事儿
  6. java中的分层概念_Java分层概念
  7. 递归算法(一)递归概念与思路
  8. arcgis 出图背景_ArcGIS空间制图分析视频教程(二狮兄出品)含ArcMap
  9. CentOS7 最小化安装工具包精简版本
  10. Java变量声明在循环体内还是循环体外,你用哪一个?
  11. python xlwt_python – 使用xlwt写入现有工作簿
  12. 嵌入式工程师有发展前途吗?
  13. 关于大内存(大于4G)支持的问题
  14. 【BZOJ4134】【树上博弈】【博弈论】【线段树合并】ljw和lzr的hack比赛 题解
  15. CoffeeScript基础
  16. Mac 自动代理切换
  17. LeetCode455分发饼干
  18. Mac OS系统进不去,重装也不行,只能抹盘安装,Espionage的加密文件如何救出?
  19. 人工智能AI课 推荐算法详解和实现
  20. 企业WiFi覆盖找时讯无线

热门文章

  1. python迅雷远程下载页面_【教程】Chrome浏览器添加迅雷下载支持
  2. 32拟合分析_SigmaPlot14.5:指导用户逐步完成图形创建和数据分析过程
  3. 研究人员利用脑电ErrP信号实时控制机器人
  4. 多尺度熵---Understanding Multiscale Entropy
  5. 这份工程师简历火了:手磨14nm咖啡,在微软传播性病,90%公司伸橄榄枝
  6. 低头族的第三只眼,“赛博朋克”新装备让你走路不再撞树
  7. 用上AI后,银行每年竟然能多赚1万亿美元丨麦肯锡最新调查报告
  8. “30位新生代数字经济人才”选拔开启!@乌镇·世界互联网大会
  9. 阿里平头哥首次交货!“让天下没有难造的芯片”
  10. 领扣-191 位1的个数 Number of 1 Bits MD