代码不贴了,主要讲解一下思路。
//BNF定义: //exprN代表优先级>=N的算符表达式 expr := expr20 expr100 := value //数值常量优先级最高,当然也可以把expr100合并到expr80,这样可以少写一个parseExpr100解析子函数 expr80 := ( expr ) | expr100 //其次是括号表达式 expr60 := sin expr60 | cos expr60 | ln expr60 | log expr60 | sqrt expr60 | ... | expr80 //其次是一元函数, 一元取负暂不考虑 expr50 := expr60 x^y expr50 | expr60 //二元函数的优先级高于乘除运算,低于一元函数,不加括号的情况下,二元函数从右往左运算; expr40 := expr50 '*' expr50 | expr50 '/' expr50 | expr50 //接下来是二元乘除运算 ==> expr50 | expr50 ( '*' expr40 )* | expr50 ( '/' expr40)* expr20 := expr20 + expr40 | expr20 - expr40 | expr40 //接下来是二元加减运算 ==> expr40 | expr40 ('+' expr40)* | expr40 ('-' expr40)* //注意,减法运算不满足交换律,右边的被减数优先级必须至少是乘和除 //糟糕的问题:expr40、expr20正常的写法会导致左递归,需要改写
function AdvancedCalculator(){ //语法分析的原始输入流: this.tokens = [];//中缀带括号的, 3种语法分析输入单位:类型为String的(和)、类型为Number的value、类型为Object/String的运算符 this.tokens_scan_index = 0; this.saved_tokens_scan_index_stack = []; this.value_buffer = []; } AdvancedCalculator.prototype = { emitButton: function(token){ ...
1、首先是写出BNF,这里要点是,非终结符对应的子表达式具有不同的优先级,优先级低的涵盖了高的,同时:
正常情况下,优先级低的会嵌套调用优先级高的,如果有左递归(+-*/的情况),此时相当于算符是左结合的,也可能是直接的右递归,如expr50二元中缀运算表达式(或一元函数),此时相当于右结合了;
左递归在直接转换为递归下降的语法分析之前,需要引入空表达式,以一般地转换为右递归的形式。
2、文法分析可以简化:可以把计算器上的按钮直接对应于一个Token,它要么是运算符,如+-*/sin cos等等(括号作为特殊的运算符考虑);要么是数字包含点号。
2个运算符之间的数字序列可以直接拼接,然后直接用JS Number()就可以转换为一个double value。
3、错误处理可以简化:每输入一个Token,则将累计的Token序列解析求值一次,如果遇到错误,则说明输入有问题(或者不完整)
4、优先级最低的是expr20加减表达式,但是语法递归解析却又是从expr20开始的,注意括号表达式expr80,它将优先级最低的expr20提升为最高
5、JS按 ES5标准,var变量是函数作用域,不是文法作用域(没有ES6 let),因此循环里声明的var下次再用的话最好reset
6、JS可以用数组[]直接作为一个栈,而且注意其core API:unshift/shift/push/pop,却没有add/remove/insert这样的命名;
7、使用下面的代码来定义Token常量:
AdvancedCalculator.prototype = { //复杂的运算符定义为单独的Object: SQRT: "Sqrt", SIN: "Sin", COS: "Cos", TAN: "Tan", COT: "Cot", LOG: "Log", //以10为底 LN: "Ln", //以e为底 POW: "Pow",//x^y PI: Math.PI, //这是数值常量,不是运算符
这些所谓的常量其实是prototype上关联的属性对象,但是用起来很方便,==即可比较。
8、通过return和throw同时使用2种控制流,return false表示当前流位置解析为子表达式a失败,但可以尝试作为b来重新解析;throw则代表确信输入有错误,或者期望一个Token输入的时候流已经结束
9、返回多个值:直接返回一个[]数组对象result,第一个元素true/false代表成功失败,第二个则是result[0]==true时的value
这实际上是Erlang的习俗,当然,ES6里有方便的destructing可以用。
实际上可以考虑用ES6来写代码,然后再用自动化工具翻译为ES5?不过我这里写代码仍然受到了Java的影响,比如hasMoreTokens/nextToken什么的
TODO:这里的后端(即除去语法解析的表达式求值部分)逻辑写的比较简单,相当于直接对AST作eval递归调用,可以考虑下面2种改进:
(1)支持翻译为最终的一个完整的JS计算表达式——这里,如果遇到需要特别的自定义函数的话,可以写一个匿名函数(function(a,b){...})(x,y)这样,当然,正常的都可以直接映射为JS Math.xxx。
(2)尽管如此,以上的2种求值方法仍然是解释器的思路,而不是编译器的思路,可考虑如何将结果翻译为序列化的带临时变量的语句序列。。。
这种情况下,稍微复杂一点。麻烦的是如何处理二元表达式,如:(1+2)-(3+4),翻译为单个的前缀/后缀表达式行不通:因为即使可翻译为前缀的-+12+34,或者后缀的12+34+-,但后面的形式仍然无法序列化地简单的单遍遍历求值。
尝试将操作数和操作符分开?比如,(1+2)-(3+4)翻译为2个序列:操作数的1234、和操作符的++-。
问题还是一样的:前2个+运算产生的中间临时变量怎么处理?
对于编译器的思路而言,由于要引入中间临时变量,实际上,就需要像Scheme的CPS转换,或者是LLVM这样的高级虚拟机指令,如alloca分配局部变量什么的。对于这里的表达式求值而言,操作符序列里需要引入2条虚拟的指令:push/pop,用于在序列求值的时候操作另外一个运行时的临时变量栈。
不需要考虑其他的额外指令。临时(局部)变量对应的就是栈,而栈只需要push/pop控制访问即可。注意,表达式求值这种问题没有复杂的控制流,也没有动态堆分配的new变量,相当于通用的编程语言的语法分析而言要简单多了。
版权声明:本文为博主原创文章,未经博主允许不得转载。