波兰表达式计算
AST树的结构
在上一篇我们已经可以读取分析波兰表达式的内部结构了,这一篇我们将会对表达式进行计算,得到计算的结果
之前有提到过抽象语法树,AST,他是用来表示用户输入的的表达式的结构的,操作数和操作符这些需要被处理的数据都存在叶子节点上,而非叶子节点上存储的是遍历和求值的信息
在这里需要看一下mpc_ast_t
的内部结构
1 | typedef struct mpc_ast_t { |
tag
表示节点内容之前的标签,他表示解析这个节点用到的规则,例如expr|number|regex
contents
包含了节点的具体内容,例如add
、(
,对于非叶子节点,这个内容为空state
表示这个节点所处的状态,例如行数和列数,这里不会用到最后两个成员就是表示树的结构了,这里为了支持多叉树,采用指针数组的方式记录
运算符与数字的转换
在我们进行求值之前,我们需要遍历上面的AST,然后对于每一个叶子节点,想办法提取出他们的共同特征,以此分辨他们的功能
1 | MyLisp> * 10 (+ 1 51) |
- tag有number的一定是数字
- expr没有number的第一个字符是左括号,第二个一定是操作符
因为tag和contents都是字符串,因此我们可以使用一些辅助函数
函数名 | 作用 |
---|---|
atof |
将 char* 转化为 double 型 |
strcmp |
接受两个 char* 参数,比较他们是否相等,如果相等就返回 0 |
strstr |
接受两个 char* ,如果第一个字符串包含第二个,返回其在第一个中首次出现的位置的指针,否则返回 0 |
这样就可以写出递归求值的函数了
基本思路如下
- 如果当前节点有number,则转换为数字并返回
- 处理操作符和操作数
- 操作符是当前节点的第二个孩子,存储到op中
- 操作数是第三个孩子的内容,但是这个操作数仍有可能是一个表达式,因此进入递归,返回值则是子表达式的结果
- 处理表达式
- 迭代当前节点的剩余子节点(如果有),也就是处理子表达式
- 对于每个子表达式都具体计算
- 返回得到的结果
写成代码就是下面这个样子
1 | double eval(mpc_ast_t* t) { |
这其中计算函数eval_op如下,这里只写一下加减乘除,最终代码会补全
1 | double eval_op(double x, char* op, double y) { |
最后我们只需要将输出的结果打印即可
1 | double result = eval(r.output); |
异常处理
对于数学计算来说,有一些特殊的处理,例如除数不为0,模的数不为0,当我们直接输入就会出错,但是不同编译器的处理不同,甚至有的会直接崩溃
我们不能这么粗暴,给用户一点错误提示就好
C语言可以设计很多的错误处理的方式,这里我们采用一种使错误也成为表达式求值的结果
简单说就是表达式求值的结果要么是数字,要么是错误
MyLisp Value的封装
为例达到上面的目的,我们将类型(数字或者错误),具体的数值,具体的错误封装成一个结构体,命名为MLval,意为MyLisp val
定义如下
1 | typedef struct |
枚举处理
我们可以让type等于0的时候,让结构体表示一个数字,等于1的时候,让结构体表示一个错误
当我们使用枚举的时候,就提高了代码的可读性
定义如下
1 | enum err |
初始化
结构体和枚举都有了,接下来就是初始化,将数值转化为结构体对象,将错误转化为结构体对象
1 | MLval MLval_num(double x) |
打印
因为MLval不是一个简单的数字,而是结构体,因此我们就需要重新写一个打印函数了
1 | void MLval_print(MLval v) |
求值
因为我们将数字封装了,所以求值函数也需要改动,同时加入错误的判断
1 | MLval eval_op(MLval x, char* op, MLval y) |
除此之外,计算函数也需要修改一下,而且strtof函数可以通过检测errno变量查看是否转换成功
1 | MLval eval(mpc_ast_t* t) |
最后直接进行输出就行
v0.1.1
1 |
|
os
其实是有尝试将中缀表达式转化为前缀表达式的,就可以实现正常的顺序输入计算,达到python的那种计算手感,但是遇到一些问题
如果直接使用栈将中缀转化为前缀,以字符串的方式逐个转化,会遇到空格无法正常转换的问题,而空格非常重要,例如1 2 和12就是两个数字和一个数字的区别,其次是负数的问题,例如 -1和 - 1的问题,既可以解释成负1也可以解释成减1
为此我想有两个解决方案,一是继续尝试利用栈转化,修修补补,二是重新写一个解析器,就是相当于重新设计了一门语言(?),只需要利用输入方案的区别判断是中缀还是前缀
可能会在完成各项基本功能后修修补补逐渐完成,之后也会出C++版本的,如果笔者能力足够的话,会期望为mpc库提供issue,或者写成一个自己的库,使之变成更适配C++的版本