语言解析之回溯法和记忆法

本篇文章想要聊聊语法解析中的回溯法和记忆法,看本篇文章需要了解以下概念:

1、编译中的递归下降识别器
2、词法分析与语法分析
3、bnf
4、LL(1)与LL(k)

语法规则越复杂,就越需要灵活地向前看k个字符,回溯法与LL(k)相比,相当于任意多的向前看符号。能满足更多的场景和需求,但回溯法有一个最大的问题是性能相对较差,所谓的记忆法就是在回溯法的基础上,对于已经计算过的结果进行缓存,以降低回溯法的时间复杂度。

首先我们看下一个基本的回溯法的代码结构,以便对于回溯法进行了解

function parse(){
    if(speculate1()){ // 尝试匹配推断1
        rule1()     // 推断成功之后,就执行rule1
    } else if(speculate2()){ //尝试匹配推断2
        rule2()
    } else if(speculate3()){ //尝试匹配推断3
        rule3()
    } else {
        throw new RecognitionError()
    }
}

function speculate1(){
    let success = true
    mark()      // 标记当前输入的位置
    try{
        rule1()  // 尝试规则1
    } catch(e){
        success = false
    }
    release()   // 放回当前输入的位置
    return success
}

可以看到回溯法相当强大,可以根据实际规则动态地任意向前看,但执行效率会低很多。

从上面的代码可以看出,即使是最顺利的情况下,每条规则的推演也会执行两次(rule1函数执行的两次)。

而在下面代码所示的bnf中,推演失败,在当前token的位置重新推演,很有可能使子规则执行很多次。

stat: list EOF
    | list '=' list

记忆法可以避免同样的输入,同样的规则,进行第二次的计算。

记忆法的核心是每条规则都有一个记忆映射表,状态分为三种情况unkonwn,failed,succeeded。

当没有解析到的时候,默认返回unknown,使用负数标识failed,使用一个正数标识succeded,该正数记录了解析成功的下一个词法单元下标。所以每条规则解析过一次之后,有起始的词法单元下标,就能找到规则结束的词法单元下标,就不用做重复的计算。

记忆法的代码框架如下:

function rule1(){
    let failed = false
    let startTokenIndex = getIndex()
    if(isSpeculating() && alreadyParsedRule(ruleMemo)) return
    try{
        _rule1()
    } catch(e){
        failed = true
        throw e
    } finally{
        // 无论推演成功还是失败,都对结果进行记录
        if(isSpeculating()){
            memoize(ruleMemo, startTokenIndex, failed)
        }
    }
}

这里要注意isSpeculating函数代表是否是推演阶段,推演阶段时,代码不能执行一些有副作用的操作,只有推演通过之后,才会执行一些有副作用的操作。

现在我们来实现alreadyParsedRule:

function alreadyParsedRule(memo){
    const startTokenIndex = getIndex()
    if(!memo[startTokenIndex]){
        return false
    }
    if(memo[startTokenIndex] < 0){
        thorw new RecognitionError()
    }
    const endTokenIndex = memo[startTokenIndex]
    // 跳过去,就像已经解析过这个规则一样
    advance(endTokenIndex)
    return true
}

alreadyParsedRule函数,就是从memo中取缓存的值,如果没有值就返回false,以便rule1函数中继续进行匹配,如果匹配失败,就抛出一个异常。如果匹配成功,就获得对应词法单元的index,将指针前移后,返回true。

如果不使用记忆法,回溯法的解析速度就会很慢,通过记忆法,只需要少量的内存,就能将整个解析过程的复杂度降低到线性水平。

你可能感兴趣的:(javascript,前端,编译,ast)