波兰表达式计算
AST树的结构
在上一篇我们已经可以读取分析波兰表达式的内部结构了,这一篇我们将会对表达式进行计算,得到计算的结果
之前有提到过抽象语法树,AST,他是用来表示用户输入的的表达式的结构的,操作数和操作符这些需要被处理的数据都存在叶子节点上,而非叶子节点上存储的是遍历和求值的信息
在这里需要看一下mpc_ast_t
的内部结构
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
-
contents
包含了节点的具体内容,例如add
、(
,对于非叶子节点,这个内容为空 -
state
表示这个节点所处的状态,例如行数和列数,这里不会用到 -
最后两个成员就是表示树的结构了,这里为了支持多叉树,采用指针数组的方式记录
运算符与数字的转换
在我们进行求值之前,我们需要遍历上面的AST,然后对于每一个叶子节点,想办法提取出他们的共同特征,以此分辨他们的功能
MyLisp> * 10 (+ 1 51)
>
regex
operator|char:1:1 '*'
expr|number|regex:1:3 '10'
expr|>
char:1:6 '('
operator|char:1:7 '+'
expr|number|regex:1:9 '1'
expr|number|regex:1:11 '51'
char:1:13 ')'
regex
- tag有number的一定是数字
- expr没有number的第一个字符是左括号,第二个一定是操作符
因为tag和contents都是字符串,因此我们可以使用一些辅助函数
函数名 | 作用 |
---|---|
atof | 将 char* 转化为 double 型 |
strcmp | 接受两个 char* 参数,比较他们是否相等,如果相等就返回 0 |
strstr | 接受两个 char* ,如果第一个字符串包含第二个,返回其在第一个中首次出现的位置的指针,否则返回 0 |
这样就可以写出递归求值的函数了
基本思路如下
- 如果当前节点有number,则转换为数字并返回
- 处理操作符和操作数
- 操作符是当前节点的第二个孩子,存储到op中
- 操作数是第三个孩子的内容,但是这个操作数仍有可能是一个表达式,因此进入递归,返回值则是子表达式的结果
- 处理表达式
- 迭代当前节点的剩余子节点(如果有),也就是处理子表达式
- 对于每个子表达式都具体计算
- 返回得到的结果
写成代码就是下面这个样子
double eval(mpc_ast_t* t) {
// 如果是数字,直接转换返回
if (strstr(t->tag, "number")) {
return atof(t->contents);
}
// 取操作符
char* op = t->children[1]->contents;
// 进入子表达式,如果是数字直接返回数字,如果是表达式则计算子表达式,记录返回的结果到x
long x = eval(t->children[2]);
// 计算其他的子表达式并且合并结果到x
int i = 3;
while (strstr(t->children[i]->tag, "expr")) {
x = eval_op(x, op, eval(t->children[i]));
i++;
}
return x;
}
这其中计算函数eval_op如下,这里只写一下加减乘除,最终代码会补全
double eval_op(double x, char* op, double 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;
}
最后我们只需要将输出的结果打印即可
double result = eval(r.output);
printf("%g\n", result);
mpc_ast_delete(r.output);
异常处理
对于数学计算来说,有一些特殊的处理,例如除数不为0,模的数不为0,当我们直接输入就会出错,但是不同编译器的处理不同,甚至有的会直接崩溃
我们不能这么粗暴,给用户一点错误提示就好
C语言可以设计很多的错误处理的方式,这里我们采用一种使错误也成为表达式求值的结果
简单说就是表达式求值的结果要么是数字,要么是错误
MyLisp Value的封装
为例达到上面的目的,我们将类型(数字或者错误),具体的数值,具体的错误封装成一个结构体,命名为MLval,意为MyLisp val
定义如下
typedef struct
{
int type;
double num;
int err;
}MLval;
枚举处理
我们可以让type等于0的时候,让结构体表示一个数字,等于1的时候,让结构体表示一个错误
当我们使用枚举的时候,就提高了代码的可读性
定义如下
enum err
{
MLERR_DIV_ZERO, // 除0
MLERR_BAD_OP, // 非法操作符
MLERR_BAD_NUM // 非法数字
};
enum type
{
MLVAL_NUM, // 表示数字
MLVAL_ERR // 表示错误
};
初始化
结构体和枚举都有了,接下来就是初始化,将数值转化为结构体对象,将错误转化为结构体对象
MLval MLval_num(double x)
{
MLval v;
v.type = MLVAL_NUM; // 类型赋值
v.num = x; // 数字赋值
return v;
}
MLval MLval_err(int x)
{
MLval v;
v.type = MLVAL_ERR; // 类型赋值
v.err = x; // 错误赋值
return v;
}
打印
因为MLval不是一个简单的数字,而是结构体,因此我们就需要重新写一个打印函数了
void MLval_print(MLval v)
{
switch (v.type)
{
case MLVAL_NUM:printf("%g", v.num); break; // 如果是数字,直接打印
case MLVAL_ERR: // 如果是错误,区分错误类型后报错
if (v.err == MLERR_DIV_ZERO)
printf("Error: Division By Zero!\n");
if (v.err == MLERR_BAD_OP)
printf("Error: Invalid Operator!\n");
if (v.err == MLERR_BAD_NUM)
printf("Error: Invalid Number!");
break;
}
}
求值
因为我们将数字封装了,所以求值函数也需要改动,同时加入错误的判断
MLval eval_op(MLval x, char* op, MLval y)
{
// 如果是错误类型,直接返回
if (x.type == MLVAL_ERR) { return x; }
if (y.type == MLVAL_ERR) { return y; }
// 加减乘除取模乘方取最大最小值
if (strcmp(op, "+") == 0 || strcmp(op, "add") == 0) { return MLval_num(x.num + y.num); }
if (strcmp(op, "-") == 0 || strcmp(op, "sub") == 0) { return MLval_num(x.num - y.num); }
if (strcmp(op, "*") == 0 || strcmp(op, "mul") == 0) { return MLval_num(x.num * y.num); }
if (strcmp(op, "/") == 0 || strcmp(op, "div") == 0)
{
return y.num == 0 ? MLval_err(MLERR_DIV_ZERO) : MLval_num(x.num / y.num);
}
if (strcmp(op, "%") == 0 || strcmp(op, "mod")==0)
{
return y.num == 0 ? MLval_err(MLERR_DIV_ZERO) : MLval_num(fmod(x.num, y.num));
}
if (strcmp(op, "^") == 0) { return MLval_num(pow(x.num, y.num)); }
if (strcmp(op, "min") == 0) { return MLval_num((x.num < y.num) ? x.num : y.num); }
if (strcmp(op, "max") == 0) { return MLval_num((x.num > y.num) ? x.num : y.num); }
return MLval_err(MLERR_BAD_OP);
}
除此之外,计算函数也需要修改一下,而且strtof函数可以通过检测errno变量查看是否转换成功
MLval eval(mpc_ast_t* t)
{
// 如果是数字则进行转换
if (strstr(t->tag, "number"))
{
char* endptr;
double num = strtod(t->contents, &endptr);
// 判断是否转换成功
if (*endptr != '\0')
{
return MLval_err(MLERR_BAD_NUM);
}
return MLval_num(num);
}
char* op = t->children[1]->contents;
MLval x = eval(t->children[2]);
int i = 3;
while (strstr(t->children[i]->tag, "expr"))
{
MLval y = eval(t->children[i]);
x = eval_op(x, op, y);
i++;
}
return x;
}
最后直接进行输出就行
v0.1.1
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<math.h>
#include"mpc.h"
void PrintPrompt()
{
printf("MyLisp Version 0.1.1\n");
printf("By jasmine-leaf\n");
printf("Press Ctrl+c to Exit\n\n\n");
}
// v0.0.1
// 实现了用户输入和读取功能
// v0.0.2
// 增加了波兰表达式的解析功能
// v0.1.0
// 增加了波兰表达式的求值功能
// 增加了min、max、乘方运算
// v0.1.1
// 增加了运算报错
#ifdef _WIN32
// 为实现跨平台功能
// 在windows平台下定义实现editline和history的同名函数
#define INPUT_MAX 2048 // 缓冲区最大值
static char Buffer[INPUT_MAX]; // Buffer输入缓冲区
char* readline(char* prompt) // 模拟实现readline
{
fputs(prompt, stdout);
fgets(Buffer, INPUT_MAX, stdin);
char* tmp = malloc(strlen(Buffer) + 1);
if (tmp != NULL)
{
strcpy(tmp, Buffer);
tmp[strlen(tmp) - 1] = '\0';
}
return tmp;
}
void add_history(char* unused)
{}
#else
#ifdef __linux__ // 在linux平台下
#include<editline/readline.h>
#include<editline.history.h>
#endif
#ifdef __MACH__ // 在mac平台下
#include<editline/readline.h>
#endif
#endif
enum err
{
MLERR_DIV_ZERO, // 除0
MLERR_BAD_OP, // 非法操作符
MLERR_BAD_NUM // 非法数字
};
enum type
{
MLVAL_NUM, // 表示数字
MLVAL_ERR // 表示错误
};
typedef struct
{
int type;
double num;
int err;
}MLval;
MLval MLval_num(double x)
{
MLval v;
v.type = MLVAL_NUM; // 类型赋值
v.num = x; // 数字赋值
return v;
}
MLval MLval_err(int x)
{
MLval v;
v.type = MLVAL_ERR; // 类型赋值
v.err = x; // 错误赋值
return v;
}
// 打印结果
void MLval_print(MLval v)
{
switch (v.type)
{
case MLVAL_NUM:printf("%g", v.num); break; // 如果是数字,直接打印
case MLVAL_ERR: // 如果是错误,区分错误类型后报错
if (v.err == MLERR_DIV_ZERO)
printf("Error: Division By Zero!\n");
if (v.err == MLERR_BAD_OP)
printf("Error: Invalid Operator!\n");
if (v.err == MLERR_BAD_NUM)
printf("Error: Invalid Number!");
break;
}
}
// 解析并计算操作符
MLval eval_op(MLval x, char* op, MLval y)
{
// 如果是错误类型,直接返回
if (x.type == MLVAL_ERR) { return x; }
if (y.type == MLVAL_ERR) { return y; }
// 加减乘除取模乘方取最大最小值
if (strcmp(op, "+") == 0 || strcmp(op, "add") == 0) { return MLval_num(x.num + y.num); }
if (strcmp(op, "-") == 0 || strcmp(op, "sub") == 0) { return MLval_num(x.num - y.num); }
if (strcmp(op, "*") == 0 || strcmp(op, "mul") == 0) { return MLval_num(x.num * y.num); }
if (strcmp(op, "/") == 0 || strcmp(op, "div") == 0)
{
return y.num == 0 ? MLval_err(MLERR_DIV_ZERO) : MLval_num(x.num / y.num);
}
if (strcmp(op, "%") == 0 || strcmp(op, "mod") == 0)
{
return y.num == 0 ? MLval_err(MLERR_DIV_ZERO) : MLval_num(fmod(x.num, y.num));
}
if (strcmp(op, "^") == 0) { return MLval_num(pow(x.num, y.num)); }
if (strcmp(op, "min") == 0) { return MLval_num((x.num < y.num) ? x.num : y.num); }
if (strcmp(op, "max") == 0) { return MLval_num((x.num > y.num) ? x.num : y.num); }
return MLval_err(MLERR_BAD_OP);
}
// 递归计算
MLval eval(mpc_ast_t* t)
{
// 如果是数字则进行转换
if (strstr(t->tag, "number"))
{
char* endptr;
double num = strtod(t->contents, &endptr);
// 判断是否转换成功
if (*endptr != '\0')
{
return MLval_err(MLERR_BAD_NUM);
}
return MLval_num(num);
}
char* op = t->children[1]->contents;
MLval x = eval(t->children[2]);
int i = 3;
while (strstr(t->children[i]->tag, "expr"))
{
MLval y = eval(t->children[i]);
x = eval_op(x, op, y);
i++;
}
return x;
}
void Lisp()
{
// 创建解析器
mpc_parser_t* Number = mpc_new("number");
mpc_parser_t* Operator = mpc_new("operator");
mpc_parser_t* Expr = mpc_new("expr");
mpc_parser_t* MyLisp = mpc_new("mylisp");
// 定义
mpca_lang(MPCA_LANG_DEFAULT,
" \
number : /-?[0-9]+(\\.[0-9]*)?/ ; \
operator : '+' | '-' | '*' | '/' | '%' | '^' | /add|sub|mul|div|min|max|mod/ ; \
expr : <number> | '(' <operator> <expr>+ ')' ; \
lispy : /^/ <operator> <expr>+ /$/ ; \
",
Number, Operator, Expr, MyLisp);
while (1)
{
char* input = readline("MyLisp> ");
add_history(input);
// 尝试分析用户输入
mpc_result_t r;
if (mpc_parse("<stdin>", input, MyLisp, &r))
{
MLval result = eval(r.output);
MLval_print(result);
putchar('\n');
mpc_ast_delete(r.output);
}
else
{
// 其他打印失败的情况
mpc_err_print(r.error);
mpc_err_delete(r.error);
}
free(input); // 释放在readline中malloc的空间
}
mpc_cleanup(4, Number, Operator, Expr, MyLisp);
}
int main()
{
PrintPrompt();
Lisp();
return 0;
}
os
其实是有尝试将中缀表达式转化为前缀表达式的,就可以实现正常的顺序输入计算,达到python的那种计算手感,但是遇到一些问题
如果直接使用栈将中缀转化为前缀,以字符串的方式逐个转化,会遇到空格无法正常转换的问题,而空格非常重要,例如1 2 和12就是两个数字和一个数字的区别,其次是负数的问题,例如 -1和 - 1的问题,既可以解释成负1也可以解释成减1
为此我想有两个解决方案,一是继续尝试利用栈转化,修修补补,二是重新写一个解析器,就是相当于重新设计了一门语言(?),只需要利用输入方案的区别判断是中缀还是前缀
可能会在完成各项基本功能后修修补补逐渐完成,之后也会出C++版本的,如果笔者能力足够的话,会期望为mpc库提供issue,或者写成一个自己的库,使之变成更适配C++的版本