C语言编译器开发之旅(二):解析器
本节是我们这个编译器系列的第二节,进入语法分析与语义分析的部分解。在本节我们会编写一个简单的解析器。
解析器的主要功能分为两个部分:
- 识别输入的语法元素生成AST(Abstract Syntax Trees)并确保输入符合语法规则
- 解析AST并计算表达式的值
在开始代码编写之前,请先了解本节最重要的的两个知识点。
抽象语法树(AST):https://blog.csdn.net/lockhou/article/details/109700312
巴科斯-瑙尔范式(BNF):https://www.bilibili.com/video/BV1Us411h72K?from=search&seid=2377033397008241337
我们需要识别的元素包含四个基本的数学运算符+,-,*,/与十进制整数共五个语法元素。那么首先让我们为我们的解析器将识别的语言定义一个语法。我么这里采用BNF描述:
expression: number
| expression '*' expression
| expression '/' expression
| expression '+' expression
| expression '-' expression
;
number: T_INTLIT
;
我们都知道BNF定义的语法是递归定义的,那么我们也需要一个递归函数去解析输入的表达式。在我们现有的语法元素可以构成的表达式中第一个语法元素始终为数字,否则就是语法错误。其后可能是一个运算符,或者只有一个数字。那么我们可以用如下伪代码表示我们的递归下降解析函数:
function expression() {
Scan and check the first token is a number. Error if it's not
Get the next token
If we have reached the end of the input, return, i.e. base case
Otherwise, call expression()
}
让我们来模拟一次此函数的运行,输入为2 + 3 - 5 T_EOF其中T_EOF 是反映输入结束的标记。
expression0:
Scan in the 2, it's a number
Get next token, +, which isn't T_EOF
Call expression()
expression1:
Scan in the 3, it's a number
Get next token, -, which isn't T_EOF
Call expression()
expression2:
Scan in the 5, it's a number
Get next token, T_EOF, so return from expression2
return from expression1
return from expression0
为了进行语义分析,我们需要代码来解释识别的输入,或者将其转换为另一种格式,例如汇编代码。在旅程的这一部分,我们将为输入构建一个解释器。但要实现这一目标,我们首先要将输入转换为抽象语法树。
抽象语法树的节点结构定义如下:
// defs.h
// AST node types
enum {
A_ADD, A_SUBTRACT, A_MULTIPLY, A_DIVIDE, A_INTLIT
};
// Abstract Syntax Tree structure
struct ASTnode {
int op; // "Operation" to be performed on this tree
struct ASTnode *left; // Left and right child trees
struct ASTnode *right;
int intvalue; // For A_INTLIT, the integer value
};
节点元素op表示该节点的类型,当op的值为A_ADD、A_SUBTRACT等运算符时,该节点具有左右两颗子树,我们将使用op代表的运算符对左右两棵子树的值做计算;当op的值为A_INTLIT时,代表该节点是整数值,是叶节点,节点元素intvalue存储着该整数的值。
tree.c 中的代码具有构建 AST 的功能。函数mkastnode()生成一个节点并返回指向节点的指针:
// tree.c
// Build and return a generic AST node
struct ASTnode *mkastnode(int op, struct ASTnode *left,
struct ASTnode *right, int intvalue) {
struct ASTnode *n;
// Malloc a new ASTnode
n = (struct ASTnode *) malloc(sizeof(struct ASTnode));
if (n == NULL) {
fprintf(stderr, "Unable to malloc in mkastnode()\n");
exit(1);
}
// Copy in the field values and return it
n->op = op;
n->left = left;
n->right = right;
n->intvalue = intvalue;
return (n);
}
我们对其进一步封装出两个常用的函数,分别用来创建左子树与叶节点:
// Make an AST leaf node
struct ASTnode *mkastleaf(int op, int intvalue) {
return (mkastnode(op, NULL, NULL, intvalue));
}
// Make a unary AST node: only one child
struct ASTnode *mkastunary(int op, struct ASTnode *left, int intvalue) {
return (mkastnode(op, left, NULL, intvalue));
我们将使用 AST 来存储我们识别的每个表达式,以便稍后我们可以递归遍历它来计算表达式的最终值。 我们确实想处理数学运算符的优先级。 这是一个例子。 考虑表达式 2 * 3 4 * 5。现在,乘法比加法具有更高的优先级。 因此,我们希望将乘法操作数绑定在一起并在进行加法之前执行这些操作。
如果我们生成 AST 树看起来像这样:
+
/ \
/ \
/ \
* *
/ \ / \
2 3 4 5
然后,在遍历树时,我们会先执行 2 * 3,然后是 4 * 5。一旦我们有了这些结果,我们就可以将它们传递给树的根来执行加法。
在开始解析语法树之前,我们需要一个将扫描到的token转换为AST节点操作值的函数,如下:
// expr.c
// Convert a token into an AST operation.
int arithop(int tok) {
switch (tok) {
case T_PLUS:
return (A_ADD);
case T_MINUS:
return (A_SUBTRACT);
case T_STAR:
return (A_MULTIPLY);
case T_SLASH:
return (A_DIVIDE);
default:
fprintf(stderr, "unknown token in arithop() on line %d\n", Line);
exit(1);
}
}
我们需要一个函数来检查下一个标记是否是整数文字,并构建一个 AST 节点来保存文字值。如下:
// Parse a primary factor and return an
// AST node representing it.
static struct ASTnode *primary(void) {
struct ASTnode *n;
// For an INTLIT token, make a leaf AST node for it
// and scan in the next token. Otherwise, a syntax error
// for any other token type.
switch (Token.token) {
case T_INTLIT:
n = mkastleaf(A_INTLIT, Token.intvalue);
scan(&Token);
return (n);
default:
fprintf(stderr, "syntax error on line %d\n", Line);
exit(1);
}
}
这里的Token是一个全局变量,保存着扫描到的最新的值。
那么我们现在可以写解析输入表达式生成AST的方法:
// Return an AST tree whose root is a binary operator
struct ASTnode *binexpr(void) {
struct ASTnode *n, *left, *right;
int nodetype;
// Get the integer literal on the left.
// Fetch the next token at the same time.
left = primary();
// If no tokens left, return just the left node
if (Token.token == T_EOF)
return (left);
// Convert the token into a node type
nodetype = arithop(Token.token);
// Get the next token in
scan(&Token);
// Recursively get the right-hand tree
right = binexpr();
// Now build a tree with both sub-trees
n = mkastnode(nodetype, left, right, 0);
return (n);
}
这只是一个子简单的解析器,他的解析结果没有实现优先级的调整,解析结果如下:
*
/ \
2 +
/ \
3 *
/ \
4 5
正确的树状结构应该是这样的:
+
/ \
/ \
/ \
* *
/ \ / \
2 3 4 5
我们将在下一节实现生成一个正确的AST。
那么接下来我们来试着写代码递归的解释这颗AST。我们以正确的语法树为例,伪代码:
interpretTree:
First, interpret the left-hand sub-tree and get its value
Then, interpret the right-hand sub-tree and get its value
Perform the operation in the node at the root of our tree
on the two sub-tree values, and return this value
调用过程可以用如下过程表示:
interpretTree0(tree with +):
Call interpretTree1(left tree with *):
Call interpretTree2(tree with 2):
No maths operation, just return 2
Call interpretTree3(tree with 3):
No maths operation, just return 3
Perform 2 * 3, return 6
Call interpretTree1(right tree with *):
Call interpretTree2(tree with 4):
No maths operation, just return 4
Call interpretTree3(tree with 5):
No maths operation, just return 5
Perform 4 * 5, return 20
Perform 6 + 20, return 26
这是在 interp.c 中并依据上述伪代码写的功能:
// Given an AST, interpret the
// operators in it and return
// a final value.
int interpretAST(struct ASTnode *n) {
int leftval, rightval;
// Get the left and right sub-tree values
if (n->left)
leftval = interpretAST(n->left);
if (n->right)
rightval = interpretAST(n->right);
switch (n->op) {
case A_ADD:
return (leftval + rightval);
case A_SUBTRACT:
return (leftval - rightval);
case A_MULTIPLY:
return (leftval * rightval);
case A_DIVIDE:
return (leftval / rightval);
case A_INTLIT:
return (n->intvalue);
default:
fprintf(stderr, "Unknown AST operator %d\n", n->op);
exit(1);
}
}
这里还有一些其他代码,比如调用 main() 中的解释器:
scan(&Token); // Get the first token from the input
n = binexpr(); // Parse the expression in the file
printf("%d\n", interpretAST(n)); // Calculate the final result
exit(0);
本节内容到此结束。在下一节中我们将修改解析器,让其对表达式进行语义分析计算正确的结果。
本文Github地址:https://github.com/Shaw9379/acwj/tree/master/02_Parser
C语言编译器开发之旅(二):解析器的更多相关文章
- C语言编译器开发之旅(开篇)
编译器写作之旅 最近在Github上看到一个十分有趣的项目acwj(A Compiler Writing Journey),一个用C语言编写编译器的项目.身为一个程序员,这在我看来是一件十分酷的事 ...
- C语言编译器开发之旅(一):词法分析扫描器
本节我们先从一个简易的可以识别四则运算和整数值的词法分析扫描器开始.它实现的功能也很简单,就是读取我们给定的文件,并识别出文件中的token将其输出. 这个简易的扫描器支持的词法元素只有五个: 四个基 ...
- 用java实现编译器-算术表达式及其语法解析器的实现
大家在参考本节时,请先阅读以下博文,进行预热: http://blog.csdn.net/tyler_download/article/details/50708807 本节代码下载地址: http: ...
- Android开发8——利用pull解析器读写XML文件
一.基本介绍 对XML解析有SAX和DOM等多种方式,Android中极力推荐xmlpull方式解析xml.xmlpull不仅可用在Android上同样也适用于javase,但在javase环境中需自 ...
- springmvc配置式开发下的视图解析器
多个视图解析器优先级:
- 7.SpringMVC 配置式开发-ModelAndView和视图解析器
ModelAndView 1.Model(模型) 1.model的本质就是HashMap,向模型中添加数据,就是往HashMap中去添加数据 2.HashMap 是一个单向查找数组,单向链表数组 3. ...
- QT开发之旅二TCP调试工具
TCP调试工具顾名思义用来调试TCP通信的,网上这样的工具N多,之前用.NET写过一个,无奈在XP下还要安装个.NET框架才能运行,索性这次用QT重写,发现QT写TCP通信比.NET还要便捷一些,运行 ...
- C#微信开发之旅(二):基础类之HttpClientHelper(更新:SSL安全策略)
public class HttpClientHelper 2 { 3 /// <summary> 4 /// get请求 5 ...
- atitit.java解析sql语言解析器解释器的实现
atitit.java解析sql语言解析器解释器的实现 1. 解析sql的本质:实现一个4gl dsl编程语言的编译器 1 2. 解析sql的主要的流程,词法分析,而后进行语法分析,语义分析,构建sq ...
随机推荐
- Android木马病毒com.schemedroid的分析报告
某安全公司移动病毒分析报告的面试题目,该病毒样本的代码量比较大,最大的分析障碍是该病毒样本的类名称和类方法名称以及类成员变量的名称被混淆为无法辨认的特殊字符,每个被分析的类中所有的字符串都被加密处理了 ...
- drozer源码学习二:info+scanner
Information: datetime: 输出android中当前日期:time. setToNow() deviceinfo: 输出设备信息 deviceinfo做了三件事: 1. ...
- php基础-php基本语法
http://study.163.com/course/courseMain.htm?courseId=246003
- 基于蒙特卡洛树搜索(MCTS)的多维可加性指标的异常根因定位
摘要:本文是我在从事AIOps研发工作中做的基于MCTS的多维可加性指标的异常根因定位方案,方案基于清华大学AIOPs实验室提出的Hotspot算法,在此基础上做了适当的修改. 1 概述 ...
- K8s Scheduler 在调度 pod 过程中遗漏部分节点的问题排查
问题现象 在TKE控制台上新建版本为v1.18.4(详细版本号 < v1.18.4-tke.5)的独立集群,其中,集群的节点信息如下: 有3个master node和1个worker node, ...
- 【技术博客】基于vue的前端快速开发(工具篇)
一.Vue教程 vue.js是一套构建用户界面的渐进式框架.vue采用自底向上增量开发的设计.vue的核心库只关心视图层,非常容易学习,非常容易与其它库和已有项目整合.vue完全有能力驱动采用单文件组 ...
- H5性能分析
一.所有的浏览器都会支持一个W3C的标准 具体标准可以查看:https://www.w3.org/TR/navigation-timing/ 资源加载指标分析: Prompt for unload:访 ...
- java的"\\s+"什么意思?
例如:String[] tt=addr.split("\\s+");\\s ==\s 表示转义字符 ,\s表示匹配任意空格(包括空格,制表符,换页符)\\s+中的'+'表示多次匹配
- 关于flask的模板注入的学习
flask模板注入的学习 关于flask模版注入,之前不太理解,看了很多文章才弄懂,主要原理就是渲染函数的参数用户可控就造成了模板注入 就会使用户构造恶意的代码进行逃逸从而进行攻击 flask模板渲染 ...
- Linux如何设置用户登录超时(闲置时间)vi /etc/profile ... export TMOUT=900
Linux如何设置用户登录超时(闲置时间) 转载莫负寒夏ai 最后发布于2019-08-08 15:04:22 阅读数 1897 收藏 展开 1. 针对所有用户 # vi /etc/profile ...