JavaScript实现高级科学计算器库

来源:互联网 发布:男人30岁还不结婚 知乎 编辑:程序博客网 时间:2024/05/06 23:53

代码不贴了,主要讲解一下思路。

   //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变量,相当于通用的编程语言的语法分析而言要简单多了。



0 0
原创粉丝点击