最后一次更新,前面C++ 实现的版本已经很好的解释了递归下降的做法和威力,这里给一个scheme 的代码(前缀表达式的解析),利用递归的方法直接解决,代码中用到了racket 的正则表达式,可以去官网上查看非常丰富的相关文档内容。
#lang racket (display "this is a simple calulater,please input S-expression: eg. operator num1 num2 oprator can use : + - * / ** %") (define (power base n) (if (= n 0)1 (* base (power base (- n 1))))) (define calulate (lambda (exp) (match exp [(? number? x) x] [`(, op , e1 , e2) ( let( (v1 (calulate e1)) (v2 (calulate e2)) ) (match op ('+ (+ v1 v2)) ('- (- v1 v2)) ('* (* v1 v2)) ('/ (/ v1 v2)) ('** (power v1 v2)) ('% (remainder v1 v2)) ))] ) ))
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
再进行一次更有意思的扩充,上次解决了+ - * / 和括号等,那么再加一些操作符怎么办呢?比如再加>= > < <= == != && || ! ~ &等等(这些操作符不是传统的算术运算,但是他们确实返回值是数,可以极好的当作之前文法定义的一个练习)
首先,优先级大概像下面这个:从上到下优先级越来越高
digit
&& || < <= == != > >= + - * / % * & | ~ ( )
因此,扩充的语法定义:
digti=[0-9]+ fact1=digit | "(" exp3 ")" fact2=(!|~)fact1 term = fact2 ( ( "*" | "/" ) fact2) * exp = term ( ( "+" | "-" ) term) * exp2=exp ((<=|>=|<|>|==|!=) exp)* exp3=exp2 ((&&| || ) exp2)*
最后得到的结果就是exp3的result成员。
关于上面扩充的文法的实现如果理解了以前的内容,就非常简单了。
------------------------------------------------------------------------------------------------------------------------------------------------------------------
修改:getNumber中添加一个简单的bool判断就可以扩充到负数的数值域中。
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
学习数据结构的时候学到栈的时候都会学习使用栈来实现求解中缀表达式,思路就是扫描一遍,数字输出,符号根据优先级决定入栈和出栈的时间,然后生成后缀表达式,对后缀表达式的求解是容易的,直接扫描一遍,遇到数字入栈,遇到操作符就直接取栈顶的两个元素做运算。
好久之前实现上面所说的程序是第一个感受“编译”的感觉,感觉很爽,因此一直想学一下真正的编译原理,龙书比较深奥,一直未得其奥义。这两天阅读了vczh大大的系列文章 《手写语法分析器》、《正则表达式引擎》等文章,颇有感触,遂有此文。
表达式比定义语言要容易的多,因为它可以把词法分析和语法分析很容易的结合起来,因为表达式中除了数字其它的符号均是单字符,直接判断就可以知道token类型,比较简单。
递归下降的核心在于写出无左递归的语法,写出来之后就直接写个程序去解释这个正则表达式就可以容易的搞定。
源码点击 这里
下面简单介绍一下前缀或者后缀表达式的递归下降分析:
对于前缀,scheme中的表达式都采取这样的形式,这样的形式就无需词法分析了,没有优先级的歧义。
语法定义如下:
expr="(" op number number ")" | "(" expr ")" |number number= [0-9]* op='+' | '-' | '* | '/'我们之间对每个部分进行翻译即可;
比如对于numbergetnumber函数应该这么写:expr GetNumber(char* &stream) { expr number; //存储结果,为一个expr类对象 char* lookup = stream; while ((*lookup) == ' ') lookup++; bool flag = false;
bool symbol=false;
if(*lookup == '-')
symbol=true;
while (true) { if ((*lookup )<= '9' && (*lookup) >= '0') { number.result = number.result * 10 + (*lookup) - '0'; ++lookup; flag = true; } else break; } if (flag) //获得当前开头的数字,并将当前指针所指调整到数字后面一位 stream = lookup; else //当前开头开始的不是数字,当前指针不变 { number.error = "there need a number! "; number.error_position = lookup; } if(symbol)
number.result=-number.result; return number; }
有了getnumber之后getexpression就很容易了,核心部分如下:
expr expression; char *lookup = stream; expression = GetNumber(lookup); if (expression.error) { if (Is(lookup, "(")) { expression.error = 0; char op = 0; if (op=IsOperator(lookup)) { expr left_expr = Getexpression(lookup); if (left_expr.error) return left_expr; char *copy_lookup = lookup; expr right_expr = Getexpression(lookup); if (right_expr.error ) return right_expr; switch (op) { case '+': expression.result = left_expr.result + right_expr.result; break; case '-': expression.result = left_expr.result - right_expr.result; break; case '*': expression.result = left_expr.result * right_expr.result; break; case '/': { if (right_expr.result == 0) { expression.error = "the number divided can not be zero! "; expression.error_position = copy_lookup; break; } else { expression.result = left_expr.result / right_expr.result; break; } } default: { expression.error = "the wrong operator! "; expression.error_position = lookup; return expression; } } } else { expression= Getexpression(lookup); if (expression.error != 0) return expression; } if (!Is(lookup, ")")) { expression.error = "there need right bracket! "; expression.error_position = lookup; } } } if(expression.error==0) //成功获得了数字,因此将stream指针置于数字后面 { stream = lookup; } return expression;中缀的代码就得自己写啦,我也是参考vczh大大的前缀代码写了一份前缀之后,自己写了一份中缀,我们很容易从中得到启示,找到递归下降的核心所在,据此可以写出中缀表达式的求解(中缀的求解代码完全是我自己写的,错误处理部分可能存在一些漏洞),相信根据上文自己写一个中缀一定有很多收获。