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
}