MyLisp项目日志:波兰表达式计算与错误处理

本文介绍了如何在波兰表达式计算中处理AST树结构,包括转换运算符与数字,封装错误处理机制,以及递归求值过程。作者还讨论了可能的改进方案,如从中缀表达式转为前缀表达式以提高用户体验。
摘要由CSDN通过智能技术生成

波兰表达式计算

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都是字符串,因此我们可以使用一些辅助函数

函数名作用
atofchar* 转化为 double
strcmp接受两个 char* 参数,比较他们是否相等,如果相等就返回 0
strstr接受两个 char*,如果第一个字符串包含第二个,返回其在第一个中首次出现的位置的指针,否则返回 0

这样就可以写出递归求值的函数了

基本思路如下

  1. 如果当前节点有number,则转换为数字并返回
  2. 处理操作符和操作数
    1. 操作符是当前节点的第二个孩子,存储到op中
    2. 操作数是第三个孩子的内容,但是这个操作数仍有可能是一个表达式,因此进入递归,返回值则是子表达式的结果
  3. 处理表达式
    1. 迭代当前节点的剩余子节点(如果有),也就是处理子表达式
    2. 对于每个子表达式都具体计算
  4. 返回得到的结果

写成代码就是下面这个样子

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++的版本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

栖林_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值