基于JavaScript的智能错误处理C语言编译器(词法分析器+语法分析器)

1. 项目亮点

1.1 词法分析

  • 考虑了大文件的编译,为了提高效率,不采用一次性读入整个文件而是采用双缓冲区交替进行读入。并且考虑了不同编码下中文字符和英文字符所占字节不同的影响。
  • 读取时字符时维护行列信息,在生成词素Token时,会记录该词素的行列信息。
  • C语言考虑全面,覆盖C语言全部关键字,种别码共有82个,覆盖绝大部分的C语言中的符号。

1.2 语法分析

  • 能够识别出语法错误,并自动预测修正方案,采用填补法,将修正方案填补到输入代码中继续语法分析。其中预测修正方案是依据action表中当前行的可接受的终结符进行预测。如果无法预测时,即填补法无法修正时,那么会抛出所有已经预测解决的错误,如果填补法顺利预测,那么会在编译结束时提示用户编译时的所有错误。

    在这里插入图片描述

  • 能够生成LR1分析过程中的语法树。在语法分析顺利结束后会实时生成语法树,展示整个规约过程。

    在这里插入图片描述

1.3 项目代码地址

项目完整代码位于本人github仓库,欢迎指点。https://github.com/Vanghua/compiler.git

在这里插入图片描述

2.项目内容

2.1 词法分析

2.1.1 确定关键字和种别码表

其中保留字表按照字典序排序,在识别关键字和标识符的接收状态中需要判断时标识符还是关键字。每次都要查找关键字表(保留字表),为了提高效率,这里进行排序并采用折半查找(O(logn))。种别码表是为了压缩生成的Token的大小,在生成的Token中内容只保存种别码表中的数字,在语法分析阶段再根据种别码表翻译出具体的词素内容。

// 保留字表,已按照字典序排序,便于折半查找
const reserveWords = [
    'auto',     'break',    'case',
    'char',     'const',    'continue',
    'default',  'do',       'double',
    'else',     'enum',     'extern',
    'float',    'for',      'goto',
    'if',       'int',      'long',
    'register', 'return',   'short',
    'signed',   'sizeof',   'static',
    'struct',   'switch',   'typedef',
    'union',    'unsigned', 'void',
    'volatile', 'while'
]

// 种别码表
let type = [
    ['auto', 1],      ['break', 2],     ['case', 3],
    ['char', 4],      ['const', 5],     ['continue', 6],
    ['default', 7],   ['do', 8],        ['double', 9],
    ['else', 10],     ['enum', 11],     ['extern', 12],
    ['float', 13],    ['for', 14],      ['goto', 15],
    ['if', 16],       ['int', 17],      ['long', 18],
    ['register', 19], ['return', 20],   ['short', 21],
    ['signed', 22],   ['sizeof', 23],   ['static', 24],
    ['struct', 25],   ['switch', 26],   ['typedef', 27],
    ['union', 28],    ['unsigned', 29], ['void', 30],
    ['volatile', 31], ['while', 32],    ['-', 33],
    ['--', 34],       ['-=', 35],       ['->', 36],
    ['!', 37],        ['!=', 38],       ['%', 39],
    ['%=', 40],       ['&', 41],        ['&&', 42],
    ['&=', 43],       ['(', 44],        [')', 45],
    ['*', 46],        ['*=', 47],       [",", 48],
    ['.', 49],        ['/', 50],        ['/=', 51],
    [':', 52],        [';', 53],        ['?', 54],
    ['[', 55],        [']', 56],        ['^', 57],
    ['^=', 58],       ['{', 59],        ['|', 60],
    ['||', 61],       ['|=', 62],       ['}', 63],
    ['~', 64],        ['+', 65],        ['++', 66],
    ['+=', 67],       ['<', 68],        ['<<', 69],
    ['<<=', 70],      ['<=', 71],       ['=', 72],
    ['==', 73],       ['>', 74],        ['>=', 75],
    ['>>', 76],       ['>>=', 77],      ['"', 78],
    ['注释', 79],      ['常数', 80],      ['标识符', 81],
    ["'", 82],        ['字符', 83],      ['字符串', 84]
]
2.1.2 双缓冲区完成文件读入

文件读入考虑大文件编译时可能的效率问题,采用两个缓冲区读入目标文件,保证每次能够读取一个完整的词素。双缓冲实现的核心部分在下面的read函数,read函数为词法分析函数提供接口,每次依据DFA分析32位缓冲区满时都会调用read函数将内容读入下一个缓冲区,两个缓冲区交替读取数据。在实现过程中要注意读取中文的问题,因为代码中可能有中文注释。read函数返回给词法分析函数一个长度为32的数组,每个元素都占一个字节。易错点是这里不能是每个英文字符或中文字符,英文字符是占一个字节,但是中文字符在Unicode编码下长度不止一个字节。

function open(fileName) {
    return new Promise((res, rej) => {
        fs.open(fileName, "r", function(err, fd) {
            if(err)
                rej(err)
            res(fd)
        })
    })
}

function read(fd) {
    return new Promise((res, rej) => {
        fs.read(fd, readBuffer, 0,32, position, function(err,bytesRead, buffer) {
            if(err)
                rej("读取文件出错")
            position += 32

            let len = buffer.length, cString = []
            // 注意把buffer缓冲区处理成字符数组的处理方式,在unicode的utf8下,英文占一个字节,中文占3个字节。
            // 如果使用buffer.toString().split("")那么中文对应的三个字节会被加载成一个字符放入字符数组cString,这不是我们期望的,这样字符数组的长度达不到32。
            // 在这里我们手动处理buffer缓冲区,把每8位即1字节转化成unicode字符,这样转化成的unicode字符实际上是乱码,不在82位C语言种别码表中。
            // C语言不允许中文命名变量,对于注释,词法分析器需要略过。使用上述方法可以保证正确略过,否则长度错误的字符数组会导致很多难以理解的错误。
            for(let i = 0; i < len; i ++)
                cString.push(String.fromCharCode(buffer[i]))
            if(bytesRead < 32)
                cString[bytesRead] = "eof"
            res(cString)
        })
    })
}
2.1.3 根据DFA完成词法分析

本项目中的DFA比较大,总共有77个状态,不便展示。下面列举出依据DFA识别词素的关键函数。

1.读取字符时记录行列信息
// 根据当前字符更新当前词素尾指针的行列信息
function updateRowCol(c) {
    // 记录上一个col
    lastCol = col
    // 如果当前字符不是换行,那么列数加1。
    if(c !== "\n")
        col++
    // 如果当前字符是换行,那么行数加1,列数置为0
    if(c == "\n")
        row ++, col = 0
    return c
}
2.双缓冲区读取代码
// 当词素被缓冲区截断时需要将后续内容读入另一个缓冲区
async function reRead() {
    // 将后续内容读入空闲缓冲区
    if(nowBuffer == buffer) {
        reserveBuffer = await read(file)
        nowBuffer = reserveBuffer
    } else {
        buffer = await read(file)
        nowBuffer = buffer
    }
    // 词素尾指针为0,指向新缓冲区的第一个字符。更新尾指针在当前缓冲区中
    forward = 0
    forwardPos = nowBuffer
}
3.多读取一位后的状态回退
// 带有尾指针后退的状态回退操作,此时尾指针多读了一位需要回退
// 虽然JavaScript中不能传递基本类型的引用,在work函数外修改这些变量不会作用到work函数中,但是可以把值传给函数然后修改过后把值再传递回来
function retract(c, tokenType) {
    // 当前标识符已经识别完,回退到初始状态
    state = 0
    // 尾指针多读了一位,当读入非字母数字下划线时会进入状态2,此时尾指针是当前读入的非字母数字下划线的下一位,同时记录的列也要随之减1
    forward -= 1
    col -= 1
    // 生成当前标识符对应的token
    getToken(tokenType)
    // 查看当前代码是否读完,这样的检查仅用于多读了一位后的回退,如果当前接收状态不是由other字符到达的那么就不需要下面的eof检查
    let isFinished = false
    if(c == "eof")
        isFinished = true
    // 如果代码没有读完,那么令词素首指针和尾指针到达相同位置,开始新的词素识别
    lex_begin = forward
    // 如果两个指针不在同一个缓冲区内,那么把首指针放入尾指针所在缓冲区
    if(beginPos != forwardPos)
        beginPos = forwardPos
    return isFinished
}
4.到达接收态后的状态回退
// 不带有尾指针后退的状态回退操作,此时的接收状态不是由other字符到达的
function back(tokenType) {
    // 回退到初始状态
    state = 0
    // 生成当前标识符对应的token
    getToken(tokenType)
    // 如果代码没有读完,那么令词素首指针和尾指针到达相同位置,开始新的词素识别
    lex_begin = forward
    // 如果两个指针不在同一个缓冲区内,那么把首指针放入尾指针所在缓冲区
    if(beginPos != forwardPos)
        beginPos = forwardPos
}
5.到达接收态后不生成词素的状态回退
// 不带有尾指针后退的状态回退操作,专门针对不需要生成token的词素,例如注释
function noTokenBack() {
    // 回退到初始状态
    state = 0
    // 如果代码没有读完,那么令词素首指针和尾指针到达相同位置,开始新的词素识别
    lex_begin = forward
    // 如果两个指针不在同一个缓冲区内,那么把首指针放入尾指针所在缓冲区
    if(beginPos != forwardPos)
        beginPos = forwardPos
}
6.依据DFA进行词法分析

(下面只展示到初始状态到其它状态的跳转,共有77个状态,几百行代码不方便展示)

async function work() {
    // 初始化当前缓冲区nowBuffer内容,正在使用的缓冲区buffer内容,更新首尾指针当前所在缓冲区
    beginPos = forwardPos = nowBuffer = buffer = await read(file)
    let c, isFinished = false
    while(1) {
        // 如果全部扫描完毕,那么退出词法分析,输出词集
        if(isFinished)
            return tokens
        switch (state) {
            case 0: // 初态
                c = await nextChar()
                // 如果当前读到了eof,那么说明文件已读完
                if(c == "eof")
                    isFinished = true
                // 如果字符时空格回车或者换行或者水平制表符,那么继续扫描下一个字符
                if(utils.judBlank(c) || utils.judEnter(c) || utils.judNewLine(c) || utils.judTab(c)) {
                    ++ lex_begin
                    // 除了词素尾指针在扫描词素字符时可能越过当前缓冲区,还有一种情况是词素首指针和尾指针相等,
                    // 两者一直在扫描空格换行和回车,此时两者共同越过缓冲区
                    // 当尾指针的位置是缓冲区大小加1,说明已经越过缓冲区一位,此时尾指针刚好在nextChar中被放置到新的缓冲区中,于是此时更新词素首指针的位置
                    if(lex_begin == nowBuffer.length + 1) {
                        lex_begin = 1
                        beginPos = forwardPos
                    }
                }
                // 如果是字母或者下划线开头,那么是标识符或保留字
                else if(utils.judAlphabet(c) || utils.jud_(c))
                    state = 1
                // 如果是数字开头,那么是常数
                else if(utils.judNumber(c))
                    state = 3
                // 其他情况由选择函数处理
                else
                    state = utils.stateSelect(c)
                break
               
// 这里只展示到初始状态到其它状态的跳转

2.2 语法分析

2.2.1 确定要使用的C语言文法的子集

下面文法的设计参考了C语言官网ISO C89标准给出的文法,由于该标准注明该文法只适用于理解C语言,不适合LR,LL型的语法分析。因此本人结合该文法,自己设计了以下的C语言文法的子集。

// C语言文法
let statement = [
    // 语句
    ["statement", [";"]], // 语句可以为空
    ["statement", ["exp", ";"]],
    ["statement", ["onlyIfChoice"]],
    ["statement", ["ifElseIfChoice"]],
    ["statement", ["ifElseChoice"]],
    ["statement", ["loopStatement"]],
    // 表达式
    ["exp", ["declaration"]],
    ["exp", ["assignment"]],
    ["exp", ["calStatement"]],
    ["exp", ["cpStatement"]],

    // 声明语句
    ["declaration", ["basicDeclaration"]], // 赋值或不赋值的基本类型声明语句
    ["declaration", ["structDeclaration"]], // 赋值或者不赋值的结构体声明语句
    ["declaration", ["primaryFunctionDeclaration"]], // 不带赋值的函数声明语句
    // 基本类型声明语句
    ["basicDeclaration", ["primaryType", "identifier_list"]],
    ["primaryType", ["char"]],
    ["primaryType", ["int"]],
    ["primaryType", ["const", "int"]],
    ["primaryType", ["long"]],
    ["primaryType", ["const", "long"]],
    ["primaryType", ["short"]],
    ["primaryType", ["const", "short"]],
    ["primaryType", ["unsigned"]],
    ["primaryType", ["float"]],
    ["primaryType", ["const", "float"]],
    ["primaryType", ["double"]],
    ["primaryType", ["const", "double"]],
    ["identifier_list", ["identifier"]], // <标识符列表> => <标识符>
    ["identifier_list", ["assignment"]], // <标识符列表> => <赋值语句>
    ["identifier_list", ["identifier", ",", "identifier_list"]], // <标识符列表> => <标识符><,><标识符列表>
    ["identifier_list", ["assignment", ",", "identifier_list"]], // <标识符列表> => <标识符><,><标识符列表>
    // 多条声明语句
    ["multiDeclaration", ["declaration", ";"]], // 多条基本类型声明语句可以推出一条声明语句
    ["multiDeclaration", ["multiDeclaration", "declaration", ";"]], // 多条基本类型声明语句可以推出多条声明语句
    // 函数声明语句
    // 无赋值的函数声明语句
    ["primaryFunctionDeclaration", ["primaryType", "identifier", "(", "declaration_list", ")"]],
    ["primaryFunctionDeclaration", ["void", "identifier", "(", "declaration_list", ")"]],
    ["primaryFunctionDeclaration", ["primaryType", "identifier", "(", ")"]],
    ["primaryFunctionDeclaration", ["void", "identifier", "(", ")"]],
    // 函数声明时的参数列表
    ["declaration_list", ["primaryType", "identifier"]],
    ["declaration_list", ["primaryType", "identifier", "declaration_list", ","]],
    // 赋值的函数声明语句
    ["valueFunctionDeclaration", ["primaryFunctionDeclaration", "{", "multiStatement", "}"]], // 带有赋值的函数声明特殊,不需要;结尾

    // 基本类型赋值语句
    ["assignment", ["identifier", "=", "identifier"]], // 变量赋值给另一个变量
    ["assignment", ["identifier", "=", "constant"]], // 常数赋给一个变量
    ["assignment", ["identifier", "=", "character"]], // 字符赋给一个变量
    ["assignment", ["identifier", "=", "string"]], // 字符串赋给一个变量
    ["assignment", ["identifier", "=", "identifier", "+", "identifier"]], // 假的
    ["assignment", ["identifier", "=", "identifier", "assignment"]],
    ["assignment", ["identifier", "=", "constant", "assignment"]],
    ["assignment", ["identifier", "=", "character", "assignment"]],
    ["assignment", ["identifier", "=", "string", "assignment"]],
    ["assignment", ["identifier", "+=", "identifier"]],
    ["assignment", ["identifier", "-=", "identifier"]],
    ["assignment", ["identifier", "*=", "identifier"]],
    ["assignment", ["identifier", "/=", "identifier"]],
    ["assignment", ["identifier", "%=", "identifier"]],
    ["assignment", ["identifier", "+=", "constant"]],
    ["assignment", ["identifier", "-=", "constant"]],
    ["assignment", ["identifier", "*=", "constant"]],
    ["assignment", ["identifier", "/=", "constant"]],
    ["assignment", ["identifier", "%=", "constant"]],
    ["assignment", ["identifier", "+=", "character"]],
    ["assignment", ["identifier", "-=", "character"]],
    ["assignment", ["identifier", "*=", "character"]],
    ["assignment", ["identifier", "/=", "character"]],
    ["assignment", ["identifier", "%=", "character"]],

    // 运算语句
    ["calChar", ["identifier"]],
    ["calChar", ["constant"]],
    ["calStatement", ["calChar", "+", "calChar"]],
    ["calStatement", ["calChar", "++"]],
    ["calStatement", ["calChar", "--"]],
    ["calStatement", ["calChar", "*", "calChar"]],
    ["calStatement", ["calChar", "/", "calChar"]],
    ["calStatement", ["calChar", "%", "calChar"]],

    // 比较语句
    ["cpStatement", ["calChar", "<", "calChar"]],
    ["cpStatement", ["calChar", "<=", "calChar"]],
    ["cpStatement", ["calChar", ">", "calChar"]],
    ["cpStatement", ["calChar", ">=", "calChar"]],
    ["cpStatement", ["calChar", "==", "calChar"]],

    // 选择语句
    ["onlyIfChoice", ["if", "(", "exp", ")", "block"]], // <仅含有单个if的选择语句> => <if><终结符"("><语句><终结符")"><语句块>
    ["ifElseIfChoice", ["onlyIfChoice"]], // if,elseif语句可以只是if语句
    ["ifElseIfChoice", ["onlyIfChoice", "else", "ifElseIfChoice"]], // if,elseif语句可以是if+else+多条ifElse语句组成
    ["ifElseChoice", ["onlyIfChoice", "else", "block"]], // if,else语句可以只是if语句

    // 循环语句
    ["loopStatement", ["for", "(", "declaration", ";", "cpStatement", ";", "calStatement", ")", "block"]],
    ["loopStatement", ["while", "(", "cpStatement", ")", "block"]],

    // 代码块
    ["block", ["statement"]], // 代码块可以是一条不被大括号包围的语句
    ["block", ["{", "statement", "}"]], // 代码块可以是多条语句

    // 多条语句
    ["multiStatement", ["statement"]], // 多条语句可以是一条语句
    ["multiStatement", ["statement", "multiStatement"]], // 多条语句可以由多个一条语句组成

    // 程序入口
    ["program", ["valueFunctionDeclaration"]], // 可以是带有赋值的函数声明
]

// 终结符
let Vt = [
    'auto',     'break',    'case',
    'char',     'const',    'continue',
    'default',  'do',       'double',
    'else',     'enum',     'extern',
    'float',    'for',      'goto',
    'if',       'int',      'long',
    'register', 'return',   'short',
    'signed',   'sizeof',   'static',
    'struct',   'switch',   'typedef',
    'union',    'unsigned', 'void',
    'volatile', 'while',    '-',
    '--',       '-=',       '->',
    '!',        '!=',       '%',
    '%=',       '&',        '&&',
    '&=',       '(',        ')',
    '*',        '*=',       ',',
    '.',        '/',        '/=',
    ':',        ';',        '?',
    '[',        ']',        '^',
    '^=',       '{',        '}',
    '|',        '||',       '|=',
    '~',        '+',        '++',
    '+=',       '<',        '<<',
    '<<=',      '<=',       '=',
    '==',       '>',        '>=',
    '>>',       '>>=',      '"',
    'constant', 'identifier',   "'",
    'character','string',   '\x00'
]

// 非终结符
let Vs = [
    'statement',    'allTypeStatement', 'declaration',
    'primaryType',  'identifier_list',  'assignment',
    'assignment_list',  'assignment_type', 'onlyIfChoice',
    'ifElseIfChoice',   'ifElseChoice', 'block',
    'multiStatement',   'basicDeclaration', 'program',
    'multiDeclaration', 'primaryFunctionDeclaration', 'valueFunctionDeclaration',
    'declaration_list', 'program', 'multiFunction',
    'exp',  'calStatement', 'calChar',
    'cpStatement',  'loopStatement'
]
2.2.2 生成action表和goto表

在这里要考虑一下,本计划手动计算出action表和goto表,在词法分析阶段,一边分析一边局部建立DFA,但是由于上述文法比较多,action表和goto表的手动计算不太现实,因此先编写自动生成action表和goto表的代码。(注:下面只给出核心函数,其中会调用一些功能函数,具体调用比较多,可以到github查看源代码)

1.计算向前搜索符
// 计算向前搜索符函数
function getForward(p, nextCharPos, G) {
    let rightExp = p[1], len = rightExp.length, rest = [], forward = [...p[2]]
    if(nextCharPos == len - 1) {
        // 如果该非终结符在产生式末尾,那么rest为”当前项目的向前搜索符“
        loop: for(let i = 0; i < p[2].length; i ++) {
            rest = [p[2][i]]
            let firstSet = new Set()
            getFirst(rest, firstSet, [], G)
            firstSet = [...firstSet];
            // firstSet求出来一定是数组,规定如果含有空时,getFirst返回["\0"],即["\x00"]。此时按照LR1向前搜索符要求,应为#
            // 注意:是含有空而不是只有空。只要firstSet中含有\x00时都应该对其处理
            if(firstSet.indexOf("\x00") != -1)
                firstSet.splice(firstSet.indexOf("\x00"), 1, "#")
            // 把求出的first集作为可能的向前搜索符加入向前搜索符集合中,注意可能求出相同向前搜索符,应满足集合性质
            forward = new Set([...forward, ...firstSet])
            forward = [...forward]
        }
    } else {
        // 如果该非终结符不在产生式末尾,那么rest为“该非终结符后面的符号串”再加上“当前项目的向前搜索符”
        rest = rightExp.slice(nextCharPos + 1)
        for(let i = 0; i < p[2].length; i ++) {
            rest.push(p[2][i])
            let firstSet = new Set()
            getFirst(rest, firstSet, [], G)
            firstSet = [...firstSet];
            rest.pop()
            if(firstSet.indexOf("\x00") != -1)
                firstSet.splice(firstSet.indexOf("\x00"), 1, "#")
            forward = new Set([...forward, ...firstSet])
            forward = [...forward]
        }
    }
    return forward
}

2.计算空闭包
// LR1分析中的求项目集空闭包函数
function getClosure(I, G) {
    for(let p of I) {
        // rightExp表示产生式右侧,pos表示·在表达式右侧的位置,len表示表达式右侧的长度
        let rightExp = p[1], pos = rightExp.indexOf("·"), len = rightExp.length
        if(pos != len - 1) {
            // 当·不在产生式末尾时做如下处理
            // nextChar表示·后面下一个符号,isInVs表示该符号是否是非终结符,nextCharPos表示当前非终结符在产生式右侧的位置
            let nextChar = rightExp[pos + 1], isInVs = G.Vs.indexOf(nextChar) == -1 ? false : true, nextCharPos = pos + 1
            if(isInVs) {
                // 如果是非终结符,需要添加新的项目,在添加新项目前,先找到其向前搜索符
                // 计算向前搜索符集合
                let forward = getForward(p, nextCharPos, G)
                // 如果是非终结符,那么需要在该项目集中添加新的项目
                loop: for(let pp of G.expand) {
                    // LExp表示产生式左侧,RExp表示产生式右侧
                    let LExp = pp[0], RExp = pp[1]
                    if(LExp == nextChar && RExp[0] == "·") {
                        let newExp = [...pp, forward]
                        // hasFindSame表示newExp是否在I项目集中找到相同的产生式。
                        let hasFindSame = update(I, newExp, G.Vs, [], G)
                        if(hasFindSame)
                            continue loop
                        // 如果存在一个项目产生式,左侧是当前非终结符,且右侧第一个符号是项目符号。那么将其加入当前项目集,其向前搜索符上述代码已求解
                        I.push(newExp)
                    }
                }
            }
        }
    }
    return I
}
3.计算goto表
// LR1分析中的项目集转换GO函数
// cnt表示当前项目集的编号,从0开始编号
function go(I, ISet, G, nodes) {
    // allNext表示在当前项目集下,按·后面的字符对产生式进行分类的映射,每一个映射都会转移到下一个项目集
    let allINext = new Map()
    for(let p of I) {
        let rightExp = p[1], pos = rightExp.indexOf("·"), len = rightExp.length, leftExp = p[0], forward = p[2]
        if(pos == len - 1)
            continue
        // char表示·后面的字符
        let char = rightExp[pos + 1]
        // 对rightExp浅拷贝,后续要让·后移一位,不能影响文法
        let nextExp = [...rightExp]
        // 实现项目符号后移一个字符(现在下一个字符后面添加项目符号,之后删除原项目符号)
        nextExp.splice(pos + 2, 0, "·")
        nextExp.splice(pos, 1)

        // 向映射中添加分类的项目集
        if(!allINext.get(char))
            allINext.set(char, [])
        let items = allINext.get(char)
        items.push([leftExp, nextExp, forward])
        allINext.set(char ,items)
    }

    // 对allINext中分好类的项目产生式集求空闭包
    allINext.forEach((item, char) => {
        // I表示一个映射中的项目集,char表示一个映射中的字符
        let INext = getClosure(item, G)
        // 判断该项目集是否在总项目集中存在
        let isExist = judSameItem(ISet, INext)
        // 如果I不存在那么在邻接表顶点中添加
        if(!nodes[toString(I)])
            nodes[toString(I)] = new Nodes(I, [])
        // 如果INext不存在那么在邻接表顶点中添加
        if(!nodes[toString(INext)])
            nodes[toString(INext)] = new Nodes(INext, [])
        // 添加一条I到INext的边
        nodes[toString(I)].firstEdge.push(new Edges(char, INext, []))
        if(!isExist) {
            // 如果该项目在项目集中不存在,那么在总项目集种添加它,并求它能转换到的新状态
            ISet.push(INext)
            go(INext, ISet, G, nodes)
        }
    })
}
4.计算first集
// 求某个串货非终结符的first集函数
function getFirst(symbols, result, vis, G) {
    // 遍历待求串
    let len = symbols.length
    for(let i = 0; i < len; i ++) {
        // isVt用于判断当前字符是否是终结符
        let s = symbols[i], isVt = G.Vt.indexOf(s) == -1 ? false : true
        if(isVt)
            // 如果是终结符,则可说明其一定属于该串的first集(当然并非该串的所有终结符都会被加入,下面有提前退出条件)
            result.add(s)
        else if(!isVt && !vis[s]) {
            // 如果是非终结符且没有被求过first集,则计算first集。求过first集的不再重复计算,避免循环左递归
            for(let p of G.P)
                if(p[0] == s) {
                    vis[s] = true
                    getFirst(p[1], result, vis, G)
                    vis[s] = false
                }
        }
        // 从result中弹出空串,用"\0"表示空串
        // 这表示只有在当前字符是当前串的最后一个字符且能推导出空串时,空串才属于first集合。如果串中某一个非终结符不是最后一个字符,且能推出空串,那么空串不属于first集
        if(result[result.length - 1] == "\0")
            result.delete("\0")
        // isNull表示是否能够推导出空串
        let isNull = nullable([s], [], G)
        if(!isNull)
            // 如果当前字符不能推导出空串,那么该串的first集已求出,即为result
            // 当前字符可能是终结符,也可能是不能多步推导出空串的非终结符
            break
        else if(isNull && i == len - 1)
            // 只有在当前字符是当前串的最后一个字符且能推导出空串时,空串才属于first集合
            result.add("\0")
    }
}
2.2.3 生成action表和goto表中重要算法的流程图
1.求空闭包算法的流程图

在这里插入图片描述

2.求first集算法的流程图

在这里插入图片描述

2.2.4 LR1分析和错误处理
1.LR1分析中的错误捕获
let char = input[charPos]
        // 如果当前指针所指输入串位置的符号不为终结符#,那么令当前符号和当前栈顶状态进行分析
        let state = getTop(stateStack)
        let col = action[0].indexOf(char), act = action[state + 1][col]
        if(!act) {
            // 如果找不到,那么交给错误处理程序处理。错误处理程序能够预测错误的修复方案并修复,从而进行后续的编译
            // return handleError(...)是老版本的错误处理,遇到错误直接抛出,并对当前错误做出预测
            // return handleError(tokens[charPos - 1], state, action)

            // fixedChar表示错误处理程序预测的修补符号。每次遇到错误时都会做出预测尽量弥补,直到预测结果为空或编译结束才抛出错误。
            let token = tokens[charPos - 1]
            let fixedChar = handleError(token, state, action)[0]
            // 保存错误信息
            errors += throwAnalysisError(token.row, token.col, "语法错误", `${token.content}后不符合C语言语法<br>`)
            // 如果无法预测(即:当前状态只有唯一的接收非终结符时,那么直接抛出错误)
            if(!fixedChar)
                return new Promise((res, rej) => {
                    rej(errors)
                })
            // 将预测的结果填入输入代码,需要修改输入代码和token信息
            input.splice(charPos, 0, fixedChar)
            let t = new Token()
            t.content = fixedChar
            tokens.splice(charPos, 0, t)
        }
2.错误处理与修补预测
function handleError(token, state, action) {
    // fix为预测的修复方案,res为实际要填充的内容
    let fix = [], res = []
    for(let i = 1; i < action[0].length; i ++) {
        let act = action[state + 1][i]
        if(act) {
            if(act[0]) {
                let Vt = action[0][i]
                if (Vt != "#" && Vt != "\x00")
                    fix.push(Vt)
            }
        }
    }

    fix.forEach(el => {
        switch(el) {
            case "constant":
                // 如果需要填补数值,那么填补0
                res.push(0)
                break
            case "identifier":
                // 如果需要填补标识符,那么填补一个未声明的标识符
                res.push("undefined_identifier")
                break
            case "character":
                // 如果需要填补字符,那么填补一个空字符
                res.push("\x00")
                break
            case "string":
                // 如果需要填补字符串,那么填补一个空字符串
                res.push("\x00")
                break
            default:
                // 其它情况直接进行填补就行
                res.push(el)
        }
    })

    // output为原来抛出错误版本中显示错误内容的字符串,在当前版本下可以用来debug
    // let output = throwAnalysisError(token.row, token.col, "语法错误", `${token.content}后不符合C语言语法<br>`)
    // output += "建议填补以下符号 " + fix.join(",")
    // return new Promise((res, rej) => {
    //     rej(output)
    // })

    // 如果预测结果为空,res数组第一个预测结果设置为null,用于外界判断预测结果。
    if(!res.length)
        res.push(null)
    return res
}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vanghua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值