目录

前文列表

用 C 语言开发一门编程语言 — 交互式解析器l

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

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

抽象语法树的结构

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_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)
>
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 中是否含有某个字段:

#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 Exit lispy> - (* 10 10) (+ 1 1 1)
97
lispy> + 5 6
11

抽象语法树与行为树



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

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

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

用 C 语言开发一门编程语言 — 抽象语法树的更多相关文章

  1. 编译器开发系列--Ocelot语言1.抽象语法树

    从今天开始研究开发自己的编程语言Ocelot,从<自制编译器>出发,然后再自己不断完善功能并优化. 编译器前端简单,就不深入研究了,直接用现成的一款工具叫JavaCC,它可以生成抽象语法树 ...

  2. JavaScript的工作原理:解析、抽象语法树(AST)+ 提升编译速度5个技巧

    这是专门探索 JavaScript 及其所构建的组件的系列文章的第 14 篇. 如果你错过了前面的章节,可以在这里找到它们: JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述! Jav ...

  3. AST抽象语法树

    抽象语法树简介 (一)简介 抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这所以说是抽象的,是因为抽象语法树并 ...

  4. 理解Babel是如何编译JS代码的及理解抽象语法树(AST)

    Babel是如何编译JS代码的及理解抽象语法树(AST) 1. Babel的作用是?   很多浏览器目前还不支持ES6的代码,但是我们可以通过Babel将ES6的代码转译成ES5代码,让所有的浏览器都 ...

  5. 【Static Program Analysis - Chapter 2】 代码的表征之抽象语法树

    抽象语法树:AbstractSyntaxTrees 定义(wiki): 在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是 ...

  6. 抽象语法树(AST)

    AST描述 在计算机科学中,抽象语法树(AST)或语法树是用编程语言编写的源代码的抽象语法结构的树表示.树的每个节点表示在源代码中出现的构造.语法是“抽象的”,因为它不代表真实语法中出现的每个细节,而 ...

  7. 从零写一个编译器(九):语义分析之构造抽象语法树(AST)

    项目的完整代码在 C2j-Compiler 前言 在上一篇完成了符号表的构建,下一步就是输出抽象语法树(Abstract Syntax Tree,AST) 抽象语法树(abstract syntax ...

  8. 五分钟了解抽象语法树(AST)babel是如何转换的?

    抽象语法树 什么是抽象语法树? It is a hierarchical program representation that presents source code structure acco ...

  9. 抽象语法树 Abstract syntax tree

    什么是抽象语法树? 在计算机科学中,抽象语法和抽象语法树其实是源代码的抽象语法结构的树状表现形式 在线编辑器 我们常用的浏览器就是通过将js代码转化为抽象语法树来进行下一步的分析等其他操作.所以将js ...

  10. 从Babel开始认识AST抽象语法树

    前言 AST抽象语法树想必大家都有听过这个概念,但是不是只停留在听过这个层面呢.其实它对于编程来讲是一个非常重要的概念,当然也包括前端,在很多地方都能看见AST抽象语法树的影子,其中不乏有vue.re ...

随机推荐

  1. 从模型到部署,教你如何用Python构建机器学习API服务

    本文分享自华为云社区<Python构建机器学习API服务从模型到部署的完整指南>,作者: 柠檬味拥抱. 在当今数据驱动的世界中,机器学习模型在解决各种问题中扮演着重要角色.然而,将这些模型 ...

  2. Python读写json文件--json

    import json # 将数据写入json文件 def json_write_file(): data={'name':'张三','age':12} with open('json.json',' ...

  3. 鸿蒙智联生态产品《接入智慧生活App开发指导》(官方更新版)

    原文:https://mp.weixin.qq.com/s/BDC-12aiZz2EhtjYLR7QIg,点击链接查看更多技术内容. 在HarmonyOS Connect生态产品应用开发过程中,很多开 ...

  4. 今天我们来聊一聊Java中的Semaphore

    写在开头 在上几天写<基于AQS手写一个同步器>时,很多同学留言说里面提到的Semaphore,讲得太笼统了,今天趁着周末有空,咱们就一起详细的学习和梳理一把 Semaphore. 什么是 ...

  5. CentOS文件备份|还原

    CentOS文件备份|还原 时间:2014-03-18 00:27 来源:51cto博客 作者:sailyangfan的空间 举报 点击:205次 dump备份 restore还原 dd数据备份 mk ...

  6. 【布局进阶】巧用 :has & drop-shadow 实现复杂布局效果

    最近,群里聊到了一个很有意思的布局效果.大致效果如下所示,希望使用 CSS 实现如下所示的布局效果: 正常而言,我们的 HTML 结构大致是如下所示: <div class="g-co ...

  7. 设备像素、css像素、设备独立像素、dpr、ppi 之间的区别

    一.背景 在css中我们通常使用px作为单位,在PC浏览器中css的1个像素都是对应着电脑屏幕的1个物理像素 这会造成一种错觉,我们会认为css中的像素就是设备的物理像素 但实际情况却并非如此,css ...

  8. kolla-ansible部署OpenStack Train版技术方案

    简单架构示意 项目目标 1. 实现容器化部署docker+ Ansible+openstack-tarin 2. 使用keeplived监控nova服务实现在单台服务器宕机的情况下能迅速切断连接减轻平 ...

  9. 编译 OpenCV 的 Python 依赖

    这一次编译 OpenCV 的 Python 依赖为了方便运行我们使用 Docker 进行编译,环境准备如下: 系统依赖:Ubuntu 18.04 Python 版本:3.6,Ubuntu 18.04 ...

  10. 力扣202(java&python)-快乐数(简单)

    题目: 编写一个算法来判断一个数 n 是不是快乐数. 「快乐数」 定义为: 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和.然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终 ...