通常,操作符的优先级越高,位置就越低。例如在3+45这个表达式中,*的优先级高于+,那么就是+节点的子节点。但如果3+4添加了(),即:(3+4)*5,两个符号的优先级就颠倒回来。
如果我们用nearyley(一种使用类BNF语法表达的parser生成器)表达,就有如下代码,可以看到每一种不同优先级的运算符,对应了一条语法规则。 当不同优先级的运算符增加,我们要写的语法规则也会相应增加:
expression -> additive {% d => d[0] %}
additive ->
additive "+" multiple {% d => d[0] + d[2] %}
| additive "-" multiple {% d => d[0] - d[2] %}
| multiple {% d => d[0] %}
multiple ->
multiple "*" factor {% d => d[0] * d[2] %}
| multiple "/" factor {% d => d[0] / d[2] %}
| factor {% d => d[0] %}
factor -> "(" additive ")" {% d => d[1] %}
| %number {% d => Number(d[0]) %}
这样新增一个运算符,就会给我们的parser实现带来一定的成本,那么有没有一种通用的方式处理我们的运算符优先级呢?算符优先算法就是为了这个目的而生的。
下面是算符优先算法的代码,为了方便学习,这里我们lexer和parser,写的尽可能的简单,所以我们的parser不允许符号之间有空格,且只支持解析个位数数(即支持3+4,不支持33+4):
class BinaryExpr{
constructor(left, op, right){
this.left = left
this.op = op
this.right = right
}
}
class OpPrecedenceParser{
constructor(lexer, precedenceMap){
this.lexer = lexer
this.precedenceMap = precedenceMap
}
expression(){
let right = this.factor()
let next;
while(next = this.nextOperator()){
right = this.doShift(right)
}
return right
}
doShift(left){
const op = this.lexer.read()
let right = this.factor()
let next;
while((next = this.nextOperator()) && this.rightIsExpr(op, next)){
right = this.doShift(right)
}
return new BinaryExpr(left, op, right)
}
nextOperator(){
const op = this.lexer.nextToken()
if(this.precedenceMap.get(op)) return op
return null
}
factor(){
if(this.lexer.nextToken() === "("){
this.token()
const exp = this.expression()
this.token()
return exp
} else {
return this.lexer.read()
}
}
token(){
return this.lexer.read()
}
rightIsExpr(op1, op2){
return this.precedenceMap.get(op1) < this.precedenceMap.get(op2)
}
}
class Lexer{
constructor(content){
this.currentIndex = 0
this.content = content || ""
this.tokens = []
}
peek(index){
return this.content[this.currentIndex + index]
}
nextToken(){
if(this.content[this.currentIndex])
return this.peek(0)
}
read(){
return this.content[this.currentIndex++]
}
}
function parse(expr){
const precedenceMap = new Map()
precedenceMap.set("+", 2)
precedenceMap.set("-", 2)
precedenceMap.set("*", 3)
precedenceMap.set("/", 3)
const lexer = new Lexer(expr)
const parser = new OpPrecedenceParser(lexer, precedenceMap)
const tree = parser.expression()
return tree
console.log(ast)
}
const ast = parse("3+4*5")
console.log(ast)
上述代码的打印结果是:
BinaryExpr {
left: '3',
op: '+',
right: BinaryExpr { left: '4', op: '*', right: '5' }
}
我们改变一下入参为:(3+4)*5
,打印结果为:
BinaryExpr {
left: BinaryExpr { left: '3', op: '+', right: '4' },
op: '*',
right: '5'
}
可以看到结果是符合我们预期的,优先级低的操作符更靠近根节点。那如果我们需要增加新的符号呢?只需这样做:
precedenceMap.set("<", 1)
precedenceMap.set(">", 1)
当我们执行parse(3+4>5)
时,得到的结果为:
BinaryExpr {
left: BinaryExpr { left: '3', op: '+', right: '4' },
op: '>',
right: '5'
}
现在我们领略了算符优先算法的强大,增加符号只需要往规则map里面增加规则及优先级即可。让我们分析一下算符优先算法。OpPrecedenceParser中expression和factor方法比较容易理解,只是根据我们的文法做自顶向下的匹配。核心在于doShift函数:
doShift(left){
const op = this.lexer.read()
let right = this.factor()
let next;
while((next = this.nextOperator()) && this.rightIsExpr(op, next)){
right = this.doShift(right)
}
return new BinaryExpr(left, op, right)
}
假设有NUMBER1+NUMBER2*NUMBER3
这样的表达式,doshift函数向后读取,拿到符号,然后将+和进行比较,如果前面的符号优先级更高,则构造出Expr,并返回。如果后面的符号优先级更高,则对右侧的表达式做doShift。
可以看出,算符优先算法对于parse表达式,真的是一件利器,当我们使用LL算法做parser时,不妨在表达式解析上,使用算符优先算法。
参考:
- 《编程语言实现模式》
- 《两周自制脚本语言》
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。