Dustball’s Interpreter
西电函数绘图语言解释器
文章目录
一、实验目的
1.近期学习的cmake和makefile管理项目实际操作一下
2.应用模块化编程范式,面向对象编程范式,追求高内聚低耦合
3.熟悉c++STL,c++11标准,文件流fstream的使用
4.学会递归下降算法
二、实验环境
win11+wsl(kali linux)
项目 | 备注 | |
---|---|---|
os | win11 22H2 + wsl(5.10.102.1-microsoft-standard-WSL2 kali linux) | |
cmake | cmake 3.24.3 | |
language | C++11,python3.8(绘图语言) | |
hardware | 处理器 11th Gen Intel® Core™ i7-11800H @ 2.30GHz 2.30 GHz 机带 RAM 32.0 GB (31.8 GB 可用) |
三、实验内容
构建
linux平台
在linux上要有gcc,g++,makefile,cmake等工具,没有直接apt install
创建build路径
mkdir build && cd build
以下所有工作均在build目录中完成
外部构建
cmake ..
编译链接
make
解释
./DustComp ../test.txt
python运行
python ./Drawer.py
这一步在wsl终端上不行,需要图形界面支持,可以用kex登录wsl桌面或者直接本机上使用python绘图
windows平台
mkdir build && cd build
cmake ..
此后在build目录下生成sln解决方案文件,使用visual studio打开之即可
项目结构
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/compilier]
└─# tree -r
.
├── test.txt
├── semantic
│ ├── semantic.hpp
│ ├── semantic.cpp
│ └── CMakeLists.txt
├── README.md
├── parser
│ ├── parser.hpp
│ ├── parser.cpp
│ └── CMakeLists.txt
├── message
│ ├── message.hpp
│ ├── message.cpp
│ └── CMakeLists.txt
├── main.cpp
├── lexer
│ ├── main.cpp
│ ├── lexer.hpp
│ ├── lexer.cpp
│ └── CMakeLists.txt
└── CMakeLists.txt
4 directories, 17 files
文件/文件夹 | 子文件 | 作用 |
---|---|---|
/CMakeLists.txt | 顶层cmake规则,构建项目使用 | |
/main.cpp | 程序入口点 | |
/README.cpp | 使用方法 | |
/build/ | 外部构建目录 | |
/test.txt | 输入文件,测试用例 | |
/lexer/ | 词法分析器目录 | |
CMakeLists.txt | 词法分析模块的cmake规则 | |
lexer.hpp | 词法分析器头,包括Token,接口的声明 | |
lexer.cpp | 词法分析器模块实现 | |
/parser/ | 语法分析器目录 | |
CMakeLists.txt | 语法分析模块的cmake规则 | |
parser.hpp | 语法分析器头,包括分析树节点,接口声明 | |
parser.cpp | 语法分析器模块实现 | |
/semantic/ | 语义分析器目录 | |
CMakeLists.txt | 语义分析模块的cmake规则 | |
semantic.hpp | 语义分析器头 | |
semantic.cpp | 语义分析器模块 |
设计思路
整个项目分为三个模块,词法分析器,语法分析器,语义分析器
每个模块都制作为静态链接库,方便与main.cpp的链接
lexer,parser,semantic均使用模块化程序设计思想,不使用类表示,而是用整个模块表示.
其组成部分比如Token或者Node采用面向对象思想,自定义Token和Node类,对外提供setter和getter方法
词法分析器
词法分析器中,getToken函数包装了一个硬编码的DFA,这是最简单的DFA实现,如果想使用有向图驱动的或者表驱动的DFA,只需要改写getToken函数内部即可,最终只需要返回一个Token*类型的记号.
至于DFA如何长什么样,放一张图意思意思,具体实现见代码和注释
这个图上所有的数字都用0代表
所有的字母都用a代表
除了START节点,都是终态
至于如何从正规式建立DFA,见词法分析 | Deutschball’s blog (dustball.top),但是本次实验不是重点,我们直接从建立好的DFA开始,只需要知道如何从DFA上进行状态转移即可
语法分析器
语法分析器负责建立抽象语法树
老师给出的代码是严格按照文法编写的
P
r
o
g
r
a
m
→
{
S
t
a
t
e
m
e
n
t
;
}
S
t
a
t
e
m
e
n
t
→
O
r
i
g
i
n
S
t
a
t
e
m
e
n
t
∣
S
c
a
l
e
S
t
a
t
e
m
e
n
t
∣
R
o
t
S
t
a
t
e
m
e
n
t
∣
F
o
r
S
t
a
t
e
m
e
n
t
O
r
i
g
i
n
S
t
a
t
e
m
e
n
t
→
O
R
I
G
I
N
I
S
(
E
x
p
r
e
s
s
i
o
n
,
E
x
p
r
e
s
s
i
o
n
)
S
c
a
l
e
S
t
a
t
e
m
e
n
t
→
S
C
A
L
E
I
S
(
E
x
p
r
e
s
s
i
o
n
,
E
x
p
r
e
s
s
i
o
n
)
R
o
t
S
t
a
t
e
m
e
n
t
→
R
O
T
I
S
E
x
p
r
e
s
s
i
o
n
F
o
r
S
t
a
t
e
m
e
n
t
→
F
O
R
T
F
R
O
M
E
x
p
r
e
s
s
i
o
n
T
O
E
x
p
r
e
s
s
i
o
n
S
T
E
P
E
x
p
r
e
s
s
i
o
n
D
R
A
W
(
E
x
p
r
e
s
s
i
o
n
,
E
x
p
r
e
s
s
i
o
n
)
E
x
p
r
e
s
s
i
o
n
→
T
e
r
m
E
x
p
r
e
s
s
i
o
n
′
E
x
p
r
e
s
s
i
o
n
′
→
{
±
T
e
r
m
}
T
e
r
m
→
F
a
c
t
o
r
T
e
r
m
′
T
e
r
m
′
→
{
×
F
a
c
t
o
r
}
F
a
c
t
o
r
→
±
F
a
c
t
o
r
∣
C
o
m
p
o
n
e
n
t
C
o
m
p
o
n
e
n
t
→
A
t
o
m
P
O
W
E
R
C
o
m
p
o
n
e
n
t
∣
A
t
o
m
A
t
o
m
→
C
O
N
S
T
_
I
D
∣
T
\begin{aligned} Program&\rightarrow \{Statement;\}\\ Statement&\rightarrow OriginStatement|ScaleStatement|RotStatement|ForStatement\\ OriginStatement&\rightarrow ORIGIN\ IS\ (Expression,Expression)\\ ScaleStatement&\rightarrow SCALE\ IS\ (Expression,Expression)\\ RotStatement&\rightarrow ROT\ IS\ Expression\\ ForStatement&\rightarrow FOR\ T\ FROM\ Expression\ TO \ Expression\ STEP\ Expression\ DRAW\ (Expression,Expression)\\ \\ Expression&\rightarrow Term\ Expression'\\ Expression'&\rightarrow \{±Term\}\\ Term&\rightarrow Factor\ Term'\\ Term'&\rightarrow\{×Factor\}\\ Factor&\rightarrow ±Factor|Component\\ Component&\rightarrow Atom\ POWER\ Component|Atom\\ Atom&\rightarrow CONST\_ID|T \end{aligned}
ProgramStatementOriginStatementScaleStatementRotStatementForStatementExpressionExpression′TermTerm′FactorComponentAtom→{Statement;}→OriginStatement∣ScaleStatement∣RotStatement∣ForStatement→ORIGIN IS (Expression,Expression)→SCALE IS (Expression,Expression)→ROT IS Expression→FOR T FROM Expression TO Expression STEP Expression DRAW (Expression,Expression)→Term Expression′→{±Term}→Factor Term′→{×Factor}→±Factor∣Component→Atom POWER Component∣Atom→CONST_ID∣T
然而C++2019版实现中,光是语法树的节点类就有一大堆,使得代码可读性下降
原因是将Statement的文法也建立了分析树节点
如果只对Expression及之下的文法建立分析树,那么顶多有二元节点,只涉及左右操作数和一个运算符
而这些Statement可能有多个子节点,比如ForStatement,他又5个操作数,这就得定义一个五个指针的节点类
实际上不需要
将各种Statement硬编码处理,只建立算术表达式的分析树即可,这样只需要一个节点类即可
语义分析器
语义分析和语法分析的界限没有语法分析和词法分析那么明显
我认为的语义分析就是对语法分析建立的表达式树进行求值
实际上对于一些参数的设置,在语法分析阶段就可以进行,
比如SCALE IS(2,3);
这句在语法分析阶段就可以得到两个放缩尺寸,scale_x=2,scale_y=3
,不需要语义分析
甚至将python绘图语句输出到程序也是语法分析器提醒语义分析器做的
感觉上语法分析器是整个程序的核心,词法分析和语义分析都为其服务
四、心得体会
优点
写之前我参考了老师给出的2018C语言实现和2019C++实现两个版本,还有19级学长的实现arttnba3/compiler_principles
总结并克服了各方缺点,主要有这么几个
1.类体系太复杂,节点类太多了,并且符号类有一个basic_token有一个token,意思是basic_token是最重要的业务,token加上了一些锦上添花的功能.但是这样写在类型转换,参数传递时都会犯浑,到底用的是哪个token类型?
应该用尽可能少的类尽可能简便地实现功能
2.命名不统一,比如函数名,函数名有驼峰规则的,也有下划线的.应该将变量命名统一,函数命名统一,类型命名统一
我的变量全部使用小写和下划线,函数名和类型名使用驼峰,函数名开头小写,类型开头大写
3.c,c++混用.如果选用c++实现,应该尽量不使用c的语法,用相同功能的c++语句实现.
就比如文件读写,用FILE,fgetc又麻烦又丑,为了获取一个token的字符串,需要往缓冲区读入,手动移动缓冲区指针.如果使用fstream流和std::string,那么往缓冲区添加字符就直接buffer+=character
比如这是一个c实现的维护缓冲区,太麻烦了
static inline char* create_letme_buf(void)
{
char *buf;
buf = new char[token_buffer_loc];
if (!buf)
err_exit("run out of memory.", nullptr, -ENOMEM);
memset(buf, 0, sizeof(char) * token_buffer_loc);
memcpy(buf, token_buffer, token_buffer_loc);
return buf;
}
如果使用std::string
Token *token=new Token();//返回值,一个符号
char character;//当前读取的字符
std::string buffer="";//字符缓冲区,用于构造lexme
...
character=fin.get();
...
buffer+=character;
...
token.setLexme(buffer);
还有就是如何维护符号表
用c维护的符号表
// built-in token table
static struct token bulit_in_token_table[] =
{
{ CONST_ID, "PI", 3.1415926, nullptr },
...
{ DRAW, "DRAW", 0.0, nullptr },
};
#define BUILTIN_TOKEN_TABLE_NUM (sizeof(bulit_in_token_table) / sizeof(struct token))
#define MAX_TOKEN_TABLE_NUM 1024
static int append_token_table_size = 0;
static struct token *append_token_table[MAX_TOKEN_TABLE_NUM];
还得定义宏给符号表服务
如果使用vector,相关的宏直接调用vector的成员函数即可
static std::vector<Token> build_in_token_table={
{CONST_ID, "PI", 3.1415926, NULL},
...
{DRAW, "DRAW", 0.0, NULL},
};
static std::vector<Token*> append_token_table;
4.控制耦合
别人的实现中有这么一个函数,其作用是建立一个分析树节点
static struct expr_node* make_expr_node(enum token_type opcode, void *arg1, void *arg2)
{
struct expr_node *new_node;
new_node = new struct expr_node;
if (!new_node)
err_exit("run out of memory.", nullptr, -ENOMEM);
new_node->opcode = opcode;
switch (opcode)
{
case CONST_ID:
new_node->content.case_const = *reinterpret_cast<double*>(arg1);
break;
case T:
new_node->content.case_param = reinterpret_cast<double*>(arg1);
break;
case FUNC:
new_node->content.case_func.func_ptr = reinterpret_cast<double (*)(double)>(arg1);
new_node->content.case_func.child = reinterpret_cast<struct expr_node*>(arg2);
break;
default:
new_node->content.case_operator.left = reinterpret_cast<struct expr_node*>(arg1);
new_node->content.case_operator.right = reinterpret_cast<struct expr_node*>(arg2);
break;
}
return new_node;
}
这个函数的arg1和arg2参数需要根据第一个参数opcode的值决定,函数内的控制流也会根据opcode进入不同的分支,这实际上是控制耦合
缺点一是两个参数arg1和arg2的作用很容易忘记,二是需要使用强制类型转换,太不优雅.
实际上这个函数可以不存在,将相关功能实现在Node类型的成员函数里即可
这个函数也并没有带来多少复用,弊大于利
缺点
1.在语法错误的时候,采取的方式时报告出错位置并且立刻终止程序,没有做到从错误中恢复
2.硬编码的DFA虽然简单,但是不灵活,相对不容易拓展
收获
1.auto语法和初始化列表语法是C++11的特性,编译时需要加入编译选项比如g++ -std=c++11 main.cpp -c
2.类体系复杂了不一定让结构清晰,继承不要太多,甚至可以牺牲空间,尽可能少用类