- Node.Js可以模拟后台运行环境,具备后端语言的部分特征。
- JS包含丰富的数组,字符串,对象处理库,ECMAScript新标准中为其增加了面向对象语法的特征。
- JS具备字典对象,可将Token制作为字典,便于查寻。
- 虽然JS效率相比C等较低,但是本项目看重的是对状态机的实现,且个人对于JS的使用,相比C++与C等更加得心应手,因此选择JavaScript来完成本项目。
另外,运行时可在node环境下执行main.js文件,或在网页文件中引入main.js来运行(不推荐)。
操作系统:Windows10
编译器:VsCode
Node版本:v14.15.4
-
缓存整个文件内容,存储为字符串(基于Node的读取文件方法,另外一种方法为流模式,按行读取文件内容)。
-
遍历字符串,进行状态转移
(1)完成标识符识别后,对下一个字符进行预处理,判断进入的第一个状态。
(2)接下来根据状态转移表进行状态转移。
(3)进行当前标识符的结束处理,并存储当前标识符的相关信息(行,列,值,类型,错误)。
-
输出已完成标识符的数组,并在输出过程进行数据处理,最后输出数据统计信息。
从整体思路来看,并不涉及什么算法,但是该项目重点在于各类符号的表示,状态机的实现。
基于JS的对象字典,可以将所有需要用到的符号整理成字典,与状态机的执行过程相互独立,具备很好的可扩展性。
除了符号表中的所有内容表示在Token外,还定义了其它静态量:
- JumpChar表示直接跳过的符号,不算入符号表中。
- SingleChar表示长度为1的符号,碰到这些符号,直接计入符号表,即读取新的字符,重新启动状态机。
- DoubleChar表示长度可能为1或2的符号,即长度为1时,可能可以与下一个字符搭配形成新的符号。
- StateTransition表示每次启动自动机读取的第一个字符,根据可获取的下一个转移状态,通常将SingleChar与DoubleChar写入其中。
// 使用计算属性,否则直接以数据作为属性名无法通过Node编译
const StateTransition = {
[DoubleChar]: 7,
[SingleChar]: 8,
'/': 9,
'"': 13,
'\'': 16,
}
本状态机看起来属于NFA,实际上在每个输入字符后,所达到的下一个状态的确定的,因此属于DFA。(具体状态图见状态转移表)
图**包含18种状态,其中0状态为状态机的启动状态,>=99状态为状态码State>=99时进入的终止状态,下面需要额外说明终止状态。
终止状态一般为state==99时,即可结束,然而为了错误处理,增加了一些其它的终止状态。
说明:这些终止状态为>=100的state,一般出错的state为0~17,但是要进入终止状态必须>=99,为了存储出错的状态即在其基础上+100,如0状态出错,那么state=0+100,既可以终止,又可以知道出错状态为0;
另外,终止后,需要对一些状态进行处理:
- 下一个字符导致终止状态后,该符号要么加入当前word中,要么不加入,此使需要flag来判断
- 下一个字符如果直接跳过,则state=0,此时终止后字符不能加入word中
- 转换下一个字符前需要index++(不处理好容易导致死循环)
- 由于本项目是整块读取文件,因此需要通过\n来计算行数,所以要注意index++的位置,使得它不被统计两次
分为三个模块:
- lexical.js词法模块,用来存储符号表,另外定义一些处理符号表和状态的函数,如根据符号(string)获取其TokenType;根据符号第一个字符确定其初始状态。
- anlyzer.js分析模块,定义一个类Analyzer,执行从文件取字符,挨个分析字符,进行状态转移过程。
- utils.js模块,定义一些工具类,比如判断是否为数字,是否为字母等。
Id:状态从1开始,标识符识别结束后与KeyWords数组值比对,可转换为关键字
KeyWords:C语言自带的关键字,此处仅列举28个,不为全部。
KeyWord: [
"Auto",
"Goto",
"Define",
"For",
"While",
"Do",
"Switch",
"Case",
"Default",
"Continue",
"Break",
"Return",
"If",
"Else",
"Unsigned",
"Signed",
"Static",
"Extern",
"Const",
"Int",
"Long",
"Char",
"Float",
"Double",
"Struct",
"Typedef",
"Void",
"Enum",
],
Operations:进行算术运算,比较,赋值等操作。此处仅列举20个,不为全部。
Op: {
Not: "!",
Add: "+",
Sub: "-",
Mul: "*",
Div: "/",
Equal: "=",
NotEqual: "!=",
AssignAdd: "+=",
AssignSub: "-=",
SelfAdd: "++",
SeldSub: "--",
Less: "<",
LessEqual: "<=",
Greater: ">",
GreaterEqual: ">=",
And: "&",
AndAnd: "&&",
Or: "|",
OrOr: "||",
Eor: "^",
// AssignMul
// AssignDiv
},
LeftParen: "(",
RightParen: ")",
LeftBracket: "[",
RightBracket: "]",
LeftBrace: "{",
RightBrace: "}",
Semicolon: ";",
SingleQuotation: "'",
DoubleQuotation: "\"",
Comma: ",",
Colon: ":",
Point: ".",
Question: "?",
输出主要分为两个部分:
-
输出检测到的每一个符号的自身信息。
未出错格式:
$Relative FilePath:Row:Col --- Value --- TokenType:TokenType$ 出错格式:
$RelativeFilePath:Row:Col [Wrong Infomation]$ -
输出统计信息,包括行数,字符数,各个符号出现的名称及数量。
例如:
Statistic: The file ./samples/test1.c has 16 lines and 240 chars in total. { Id: [ 'main', 'a', 'b', 'printf' ], IdTotal: 4, Op: { Equal: 4, Less: 1, AssignAdd: 1, AndAnd: 1, AssignSub: 1 }, KeyWord: { Int: 2, While: 1, If: 1, Return: 1 }, Annotation: 2, LeftParen: 5, RightParen: 5, LeftBrace: 3, Number: 6, Comma: 1, Semicolon: 5, String: 1, RightBrace: 3 }
此程序中仅处理五类错误,更多错误在语法分析中产生。
-
未知字符错误
[Unexpected character]
在遇到@,%等未知符号时产生此错误
-
指数后错误字符
[Unexpected character after Exponent.]
在指数E或e后没有数字时报错
-
不完整注释
[Uncomplete annotation.]
注释没有结束符号
-
不完整字符串
[Uncomplete String.]
字符串没有结束符号
-
错误字符(不完整字符)
[Wrong Char]
例如:'aasd 或 'www
若采取恐慌模式,会在一定程度上减少所能检测到的字符。
因此,本程序采取的方法为:当发生错误时,存储当前结果;再次从零状态检测下一个字符。
本方法多针对由于失误,多打了一些不合理的符号等。
本项目有四个测试用例,均包含在samples文件夹内。
其中test4.c中写了几种错误,部分输入如下:
以下为指数错误
./samples/test4.c:5:16 [Unexpected character after Exponent.]
./samples/test4.c:6:3 --- int --- TokenType:{"KeyWord":"Int"}
./samples/test4.c:6:8 --- n --- TokenType:"Id"
./samples/test4.c:6:10 --- , --- TokenType:"Comma"
./samples/test4.c:6:12 --- min --- TokenType:"Id"
./samples/test4.c:6:16 --- ; --- TokenType:"Semicolon"
以下为不完整注释
./samples/test4.c:28:3 --- return --- TokenType:{"KeyWord":"Return"}
./samples/test4.c:28:11 --- 0 --- TokenType:"Number"
./samples/test4.c:28:13 --- ; --- TokenType:"Semicolon"
./samples/test4.c:29:3 [Uncomplete annotation.]
以下为统计信息
Statistic:
The file ./samples/test4.c has 31 lines and 575 chars in total.
{
Id: [
'include', 'stdio', 'h',
'stdlib', 'string', 'main',
'a', 'n', 'min',
'scanf', 's', 'm',
'str', 'getchar', 'i',
'gets', 'strlen', 'j',
'strcmp', 'printf'
],
IdTotal: 20,
Op: {
Less: 3,
Greater: 4,
Equal: 11,
And: 1,
Mul: 2,
LessEqual: 4,
SelfAdd: 4,
NotEqual: 1,
Add: 6,
Sub: 2
},
KeyWord: { Float: 1, Int: 6, Char: 2, For: 4, If: 2, Return: 1 },
Point: 3,
LeftParen: 13,
RightParen: 13,
LeftBrace: 4,
Comma: 5,
Semicolon: 22,
String: 2,
LeftBracket: 14,
RightBracket: 14,
Number: 16,
RightBrace: 3,
Annotation: 1
}