0%

86:NJU_PA_STUDY(3)

我们先前反复提到了一个式子EXPR,它代表着表达式含义,常见的表达式是通过操作符连接的。这里我们需要为我们的框架实现一个表达式求值的功能。我们要解决的问题是求值一个字符串表示的表达式。

PA1 表达式求值

为了简单起见,我们先从数学表达式求值实现开始。

数学表达式求值

如下的一个表达式的字符串:

1
"5 + 4 * 3 / 2 - 1"

我们应该怎么求出它的值?我们可以分成两个简单的步骤:

  • 首先识别出表达式中的单元
  • 根据表达式的归纳定义进行递归求值

词法分析

词法分析要做的事情就是识别出表达式中的单元,这里的单元指的就是有着独立含义的子串,我们称之为token。具体而言我们需要将表达式中所有的单元都赋予一个标签,方便之后根据标签来对表达式进行处理。这里我们使用正则表达式来对token进行处理。

出于简单起见,这里我们暂时只对以下的token类型进行处理:

  • 十进制整数
  • +-*/
  • ()
  • 空格串(一个或者多个空格)

首先我们需要使用正则表达式来编写用于识别这些token类型的规则,框架中演示了怎么对其进行匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
static struct rule {
const char *regex;
int token_type;
} rules[] = {

/* TODO: Add more rules.
* Pay attention to the precedence level of different rules.
*/

{" +", TK_NOTYPE}, // spaces
{"\\+", '+'}, // plus
{"==", TK_EQ}, // equal
};

这些规则,在init_sdb()时调用了init_regex()来将其编译成正则结构体并存储在re中:

1
2
3
4
5
6
7
8
9
10
11
12
void init_regex() {
int i;
char error_msg[128];
int ret;
for (i = 0; i < NR_REGEX; i ++) {
ret = regcomp(&re[i], rules[i].regex, REG_EXTENDED);
if (ret != 0) {
regerror(ret, &re[i], error_msg, 128);
panic("regex compilation failed: %s\n%s", error_msg, rules[i].regex);
}
}
}

如果编译不通过,可能是你编写的token匹配的规则不符合正则表达式的语法。现在,我们可以给出一个待匹配的表达式,我们利用make_token()来识别出其中的token。用position变量指示当前待匹配的位置,然后按顺序对当前位置的字符串进行匹配。当一个规则匹配成功,并且匹配出的字串属于position指出的位置时,我们就成功的识别出了一个token。我们则使用Token结构体来将它的内容记录下来。

1
2
3
4
5
6
typedef struct token {
int type;
char str[32];
} Token;
static Token tokens[32] __attribute__((used)) = {};
static int nr_token __attribute__((used)) = 0;

其中type成员用于记录token的类型,然后str用来保存token相应的字串。tokens数组用来保存识别出的token信息,而nr_token则用来指示被识别出的token数目。

现在我们需要向算数表达式中的各种tokens类型添加规则,在成功识别出tokenhi后,将token的信息一次记录到tokens数组之中。

1
2
3
4
5
6
7
8
9
10
11
12
rules[] = {
// 这里需要注意对这些内容的匹配是有优先顺序的,从上到下
{" +", TK_NOTYPE}, // spaces
{"==", TK_EQ}, // equal
{"\\+", '+'}, // plus
{"-", '-'}, // minus
{"\\*", '*'}, // multi
{"/", '/'}, // div
{"\\(", '('}, // left brace
{"\\)", ')'}, // right brace
{"[0-9]+", TK_NUM}, // number
};

然后我们就需要将匹配到的token放入tokens中,并对它的各个字段进行赋值。对于大多数类型,我们只需要简单的保存它的类型就行了,但是对于有数据内容的token,我们需要保存它的data,在这里既是保存它的子串内容信息,方便我们进行进一步的计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
static bool make_token(char *e) {
int position = 0;
int i;
int index=0;
regmatch_t pmatch;
nr_token = 0;
while (e[position] != '\0') {
/* Try all rules one by one. */
for (i = 0; i < NR_REGEX; i ++) {
if (regexec(&re[i], e + position, 1, &pmatch, 0) == 0 && pmatch.rm_so == 0) {
char *substr_start = e + position;
int substr_len = pmatch.rm_eo;
Log("match rules[%d] = \"%s\" at position %d with len %d: %.*s",
i, rules[i].regex, position, substr_len, substr_len, substr_start);
position += substr_len;
/* TODO: Now a new token is recognized with rules[i]. Add codes
* to record the token in the array `tokens'. For certain types
* of tokens, some extra actions should be performed.
*/
switch (rules[i].token_type) {
case TK_NUM:
tokens[index].type = rules[i].token_type;
for(int j=0;j<substr_len;j++)
tokens[index].str[j] = substr_start[j];
// printf("The num is %s\n",tokens[index].str);
break;
default:
tokens[index].type = rules[i].token_type;
}
index++;
break;
}
}
if (i == NR_REGEX) {
printf("no match at position %d\n%s\n%*.s^\n", position, e, position, "");
return false;
}
}
return true;
}

我们可以看到整个识别token的过程,每当我们获取一个token时,我们都会使用Log()将它的匹配规则,子串下标,子串长度,和对应的子串信息打印出来。便于我们观察标识符匹配的中间过程。对于未匹配到的类型,我们也会将其内容进行返回。

中间部分则是我们对token信息的存储过程,目前我们只对十进制整数的内容进行了保存,对于其他的标识符号,则是简单的保存类型。现在我们可以进一步的进行表达式求值了。

递归求值

我们现在将求值表达式的token识别出来之后就可以进行求值了。我们现在是对token数组进行处理,方便起见,我们称其为token表达式。(将空格忽略)

接下来,根据表达式的归纳定义特性,我们可以使用递归来进行求值:

1
2
3
4
5
6
<expr> ::= <number>    # 一个数是表达式
| "(" <expr> ")" # 在表达式两边加个括号也是表达式
| <expr> "+" <expr> # 两个表达式相加也是表达式
| <expr> "-" <expr> # 接下来你全懂了
| <expr> "*" <expr>
| <expr> "/" <expr>

这种表示方法就是我们常说的BNF,我们可以这里理解,长表达式是由短表达式构成的,那么我们可以先对短表达式进行求值,然后再进一步的对长表达式进行求值。

为了在token表达式中指示一个子表达式,我么可以使用两个整数pq来指示子表达式的开始和结束位置,现在我们就可以写出一个求值函数的框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
eval(p, q) {
if (p > q) {
/* Bad expression */
}
else if (p == q) {
/* Single token.
* For now this token should be a number.
* Return the value of the number.
*/
}
else if (check_parentheses(p, q) == true) {
/* The expression is surrounded by a matched pair of parentheses.
* If that is the case, just throw away the parentheses.
*/
return eval(p + 1, q - 1);
}
else {
/* We should do more things here. */
}
}

其中check_parentheses()用于判断表达式是否被一堆匹配的括号所包围,同时检查表达式左右的括号是否匹配,如果不匹配,那么这个表达式就是非法的。我们以此来尝试实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
static int check_parentheses(int start, int end){
int cond=0;
if(tokens[start].type!='('||tokens[end].type!=')') return 0;
for(int i=start;i<=end;i++){
if(tokens[i].type=='(') cond++;
if(tokens[i].type==')'){
cond--;
if(cond<0) return -1;
}
if(cond==0) return (i == end) ? 1 : 0;
}
return (cond==0)?1:-1;
}

现在我们的框架代码,已经可以处理BNF的开始两种情况啦。像现在我们需要进一步的考虑剩下的情况,即四则运算的处理。但是问题在于,我们怎么对一个长表达式处理,将其分解为各个小的短表达式,并保证语法的正确呢?

我们以下为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"4 + 3 * ( 2 - 1 )"
/*********************/
case 1:
"+"
/ \
"4" "3 * ( 2 - 1 )"
case 2:
"*"
/ \
"4 + 3" "( 2 - 1 )"
case 3:
"-"
/ \
"4 + 3 * ( 2" "1 )"

我们可以看到只有第一种情况是正确的,第二种情况违背了算术符号的优先级,而第三种情况则使得表达式的括号不再匹配。通过以上的例子,我们就可以总结出寻找主运算符(可以将一个长表达式分解成两个短表达式的运算符号)的逻辑了:

  • 非运算符的token不是主运算符
  • 出现在一对括号中的token不是主运算符号
  • 主运算符的优先级在表达式中应该是最低的
  • 有多个运算符的优先级最低时,根据结合性,最后被结合的运算符为主运算符号

现在我们根据以上规则来扫描出每个表达式中的主运算符号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
static int get_token(int start, int end){
int token = -1;
int par = 0;
int min_priority = -1;
for(int i=start;i<=end;i++){
int tmp=tokens[i].type;
if(tmp==TK_NUM) continue;
if(tmp=='(') par++;
else if(tmp==')'){
par--;
if(par<0) return -1;
}
if(par!=0) continue;
switch (tmp){
case '*': case '/':
if(min_priority<=1){
min_priority=1;
token=i;
}
break;
case '+': case '-':
if(min_priority<=2){
min_priority=2;
token=i;
}
break;
default:
continue;
}
}
if(token != -1){
// Log("Main operator: %c at position %d\n", tokens[token].type, token);
} else {
printf("No operator found in range [%d, %d]\n", start, end);
}
return token;
}

找到表达式的主运算符之后,事情就变得简单了,我们先对两个分裂出来的子表达式进行递归求值,然后根据主运算符号的两个子表达式进行计算即可,现在我们的框架代码更新如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
eval(p, q) {
if (p > q) {
/* Bad expression */
}
else if (p == q) {
/* Single token.
* For now this token should be a number.
* Return the value of the number.
*/
}
else if (check_parentheses(p, q) == true) {
/* The expression is surrounded by a matched pair of parentheses.
* If that is the case, just throw away the parentheses.
*/
return eval(p + 1, q - 1);
}
else {
op = the position of 主运算符 in the token expression;
val1 = eval(p, op - 1);
val2 = eval(op + 1, q);

switch (op_type) {
case '+': return val1 + val2;
case '-': /* ... */
case '*': /* ... */
case '/': /* ... */
default: assert(0);
}
}
}

现在,我们需要利用现有的规则和函数来填补实现我们的求值函数eval():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static int eval(int start, int end){
if(start>end){
printf("Unknown expression.\n");
return -1;
}else if(start==end){
return atoi(tokens[start].str);
}else if(check_parentheses(start,end)==1){
return eval(start+1,end-1);
}else if(check_parentheses(start,end)==-1){
printf("parenthese mismatch error\n");
return -1;
}else{
int op = get_token(start,end);
if(op==-1){
printf("Error: No valid operator found in expression\n");
return -1;
}
word_t val1 = eval(start,op-1);
word_t val2 = eval(op+1,end);
switch(tokens[op].type){
case '+': return val1+val2;
case '-': return val1-val2;
case '*': return val1*val2;
case '/':
if(val2==0){
printf("Error: Division by 0\n");
return -1;
}
return val1/val2;
default: assert(0);
}
}
}

经过测试,基本可以处理大多数情况了。我们现在可以将其封装在expr()中,然后实现p方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
word_t expr(char *e, bool *success) {
if (!make_token(e)) {
*success = false;
return 0;
}
word_t val = eval(0,nr_token-1);
*success = (val!=-1);
return val;
}

static int cmd_p(char* args){
bool success;
word_t val=expr(args,&success);
if(success==true)
printf("%d\n",val);
return 0;
}

出于限制,我们只使用uint32_t作为我们的返回类型。

测试代码

按照PA的要求,我们现在应该测试一下我们这个函数,但是我就跳过这一部分了,因为赶时间。