编译程序的工作过程一般可以分为5个阶段:
- 词法分析
- 语法分析
- 语义分析和中间代码的产生
- 优化
- 目标代码生成
词法分析的任务是:输入源程序,对构成源程序的字符串进行扫描和分解,识别出一个个单词(定义符、标识符、运算符、界符、常数)。 在词法分析阶段的工作中所依循的是语言的语法规则(或称构词规则)。 描述语法规则的有效工具是正规式和有限自动机。
语法分析的任务是:在词法分析的基础上,根据语言的语法规则,把单词符号串分解成各类语法单元(语法范畴)(短语、子句、句子、程序段、程序),并确定整个输入串是否构成语法上正确的程序。
语法分析所依循的是语言的语法规则。 语法规则通常用上下文无关文法描述。 词法分析是一种线性分析,而语法分析是一种层次结构分析。
这一阶段的任务是:对语法分析所识别出的各类语法范畴,分析其含义,并进行初步翻译(产生中间代码)。这一阶段通常包含两个方面的工作。
- 对每种语法范畴进行静态语义的检查,例如,变量是否定义、类型是否正确等等。
- 如果语义正确则进行中间代码的翻译。 这一阶段所依循的是语言的语义规则,通常使用属性文法描述语义规则。
对于代码(主要是中间代码)进行加工变换,以期能够产生更为高效(省时间和空间)的目标代码。
优化的主要方面有:公共子表达式的提取、循环优化、删除无用代码等等。 优化所依循的是程序的等价变换规则。
这一阶段的任务是:把中间代码(经过优化处理之后的)变换成特定机器上的低级语言代码(绝对指令、可重定位指令、汇编指令)。
词法分析是编译的第一步,主要任务是读入源程序的输入字符(将代码一个字符一个字符的读入),将其组成词素(一个字符序列),生成并输出一个词法单元序列。由于负责读取程序源码,它还有一些其他任务。如:过滤程序中的注释和空白(合并空格,删除换行符、制表符等);将编译器生成的错误消息与源程序的位置联系起来(给错误赋予行号)。下图为词法单元和词素之间关系:
词法分析使用正则表达式来表示一个匹配模式,然后根据各个需要识别的词法单元的匹配模式来构造出代码。在这其中首先要构造出状态转换图,使得词法分析器在扫描输入串的过程中寻找和某个模式匹配的元素,然后根据一组状态转移图构造出一个词法分析器。
词法分析器一般产生如下格式词法单元:
<token-name, attribute-value>
其中token-name表示词法分析过程中的抽象符号,类似于为该词法单元赋予唯一id,而attribute-value指向存储词法单元相关信息的符号表中的某一条。这条词法语句中的词素将会被映射成词法单元,用于下一阶段的语法分析。
那么在实际的设计中要进行如下的处理:
- 设置双缓冲区
- 进行词法的判断
- 进行基本的错误处理
词法分析器工作的第一步是输入待分析的文本,之后对输入的文本进行词法分析,但是更好的方法是,先进行对输入串的预处理,之后再进行对单词符号的识别工作。
对于扫描缓冲区,使用两个缓冲区,其长度大于所有标识符和常数的实际长度,这两个半区互补使用,假如搜索指示器从单词起点出发搜索到半区的边缘但尚未搜索到整个的单词,则调用预处理程序,令其再装填另外一个半区。
具体实现如下:
在词法分析类中设定两个vector,作为两个缓冲半区,并使用一个命名为now的vector来标记当前使用的缓冲半区,每一个缓冲区的最大长度人为设定为120:
vector<unsigned char> vec1;//双缓冲区
vector<unsigned char> vec2;
vector<unsigned char> now;
设定几个标志位用于标记缓冲区的状态:
标志位 | 初值 | 意义 |
---|---|---|
int nextBuffer | 1 | 下一次应当使用的缓冲区 |
int bufferPlace | 0 | 当前的起点指示器 |
int bufferSum | 0 | 当前缓冲区的总字符数 |
bool wetherBack | false | 是否是因为字符回退而从上一个半区回退到当前半区的 |
bool fileEnd | false | 文件是否已经全部加载 |
并设计三个函数
void packBuffer() //装载缓冲区
unsigned char getBufferChar() //获取当前缓冲区字符
void returnBufferPlace() //缓冲区字符回退
预处理程序读入最原始的文本,去掉一些无用的信息(如注释,多余的空格等)之后装入缓冲区,因而将预处理程序写在函数LexicalAnalyzer::packBuffer()中 对各个变量进行初始化
int sum = 0;
unsigned char ch;
unsigned char next;
bool noteMode = false;
bool stringMode = false;
根据nextBuffer变量的值来确定将要操作的缓冲区,此时注意要保存当前的缓冲区,为字符回退做准备。
if (nextBuffer == 1) {
vec1.clear();
vec2 = now;
now = vec1;
nextBuffer = 2;//
}
else {
vec2.clear();
vec1 = now;
now = vec2;
nextBuffer = 1;
}
之后进行预处理。
在预处理的过程中,对于读入的文本逐字符分析:
a) 假如读到‘/’,则代表这有可能是双斜杠注释“//”,或者是“/ * * /”注释,此时根据下一个读到的字符来判断,假如再次读到’/’,则表明为双斜杠注释,此时读到文末即可,而假如是’*’,则表明为/**/注释,此时需要读到注释结束。
b) 假如读到空格,由于空格的处理放在注释之后,因而此时的空格必为程序中的空格,因而将分割符号与符号的多余的空格都去掉,仅留下一个装入缓冲区。
c) 另外一种情况是,读到的’/’是除号,此时回退一格。
d) 其他字符则装入缓冲区。
在装载的时候,假如读到文件的末尾,则要将fileEnd标志位设为true,这是为了判断是否读完文件而设置的特殊判断。同时,每次装载都需要记录缓冲区的最大长度以便之后使用。
if (code_reader_.eof()) {
fileEnd = true;
}
bufferSum = sum;
在之后就可以通过这两个变量来判断是否EOF
if (bufferPlace >= bufferSum && fileEnd == true)
假设括号里面的条件成立则缓冲区为EOF
函数char LexicalAnalyzer::getBufferChar()用于获取当前字符 假如缓冲区是满的,且当前读到的位置为缓冲区内(119~120),则直接返回当前缓冲区对应的位置的字符;假如缓冲区不满,说明此时已经读到了文件的末尾,假如当前位置并没有到末尾,则返回当前位置的字符。
ch = now[bufferPlace];
bufferPlace++;
return ch;
如果超过末尾则返回最后一位字符。
if (bufferPlace >= bufferSum && fileEnd == true) {//假如EOF了就返回上一个而不是现在的
return now[bufferPlace - 1];
}
而当缓冲区为满的,但是当前位置已经超过了缓冲区的最大长度(达到了120),则需要考虑两种情况,一种情况是当前缓冲区是新装载的,在此时就直接装载下一个缓冲区,重置bufferPlace变量,之后返回相应的字符;另一种情况是,由于双区缓冲的特性,当前缓冲区可能是由于字符回退而从上一个缓冲区回退回来的,此时另一个缓冲区是已经装载好的,因而不需要再次装填缓冲区,只需改变当前的缓冲区now,重置bufferPlace即可。
if (bufferPlace == 120) {
if (wetherBack == true) {//假如是从上一个缓冲区退回来的,则此时下一个缓冲区已经装填好了
wetherBack = false;
if (nextBuffer == 1) {
now = vec1;
bufferPlace = 0;
}
else {
now = vec2;
bufferPlace = 0;
}
}
else {
packBuffer();
bufferPlace = 0;
}
}
函数LexicalAnalyzer::returnBufferPlace()用于表示字符的回退,由于词法分析的识别是使用超前搜索的方法,因而存在回退的情况,需要将bufferPlace减1或更多,甚至会涉及到缓冲区的改变。 假如当前bufferPlace并不是0,可以直接回退,直接将其减1.
if (bufferPlace != 0) {
bufferPlace--;
}
而当bufferPlace为0时,需要回退到上一个缓冲区
else {//假如是零则要退到上一个缓冲区
if (nextBuffer == 1) {
now = vec1;
nextBuffer = 2;
bufferPlace = 119;
wetherBack = true;
}
else {
now = vec2;
nextBuffer = 1;
bufferPlace = 119;
wetherBack = true;
}
}
词法分析判断的首要任务是将对读入双缓冲区的代码(预处理已经忽略注释)进行从左到右扫描,产生一个个单词符号,把作为字符串的源程序改造成单词符号串的中间程序。
通过交替扫描双缓冲区(缓冲区大小为120),对扫描得到的基本单词做出相应的判断(此时缓冲区得到的字符串已经通过预处理将注释自动忽略),其主要策略就是在没有达到文件尾并且没有达到缓冲区尾的时候,对扫描得到的每个单词作如下分析:
- 判断读到的字符若是数字(IsDigital(ch)),则要分析它是整数还是浮点数,具体分析方法就是,继续进行搜索直到非数字且非小数点,那么就得到int型数,若是小数点则继续进行超前搜索,像搜索整数一样直到非数字非小数点,得到float型数;
- 若是字母(IsLetter(ch)),一直超前搜索,直到读到的字符时非字母非数字非下划线即可得到一个单词,再分门别类进行分析,有可能是关键字,标识符,数据类型等;
- 若是边界符号(如(),[],{},;,,),调用相应的函数(BORDERS.find(ch))做出判断即可;
- 若是单符号运算符(+,-,*,/,=),调用相应的函数(IsSingleCharOperator(ch))做出判断即可;
- 若是双符号运算符(==,>=,<=,!=),同样,先调用双符号运算前置运算符判断函数(IsDoubleCharOperatorPre(ch)),若得到该符号的下一个字符为“=”,即可确定该符号是具体哪种双符号运算符;
- 若是其他相关字符(’‘,’\t’,’\n’,255),调用相应的函数(IsBlank(ch))做出判断即可;
- 若是char型常量的前缀(’\’’),则继续向前搜索一个字符(数字或者字母或者其他相关字符),若以char型常量后缀(’\’’)结尾,即可说明该单词为字符型常量。
编译器作为代码的编译程序,在进行调试时往往比较麻烦。并且大部分语法错误均是在输入时产生的,编写编辑器可以达到以下效果:
- 产生友好的可视化环境
- 对输入进行提示,减少语法错误的发生
- 便于对编辑器进行调试
编辑器的编写环境为Qt 5.12。
编辑器首先提供基本的交互功能,如下图,提供基本的功能如:代码输入,文件管理(新建、打开、存储),编译,窗口调整,输出信息,操作提醒等。
void newFile();
void saveFile();
void openFile();
void undo();
void redo();
void run();
//------------------------------
void runFinished(int code);
void updateOutput();
void updateError();
void about();
public:
void inputData(QString data);
protected:
void resizeEvent(QResizeEvent* event)override;
void closeEvent(QCloseEvent* event)override;
文件功能提供文件的新建、存储、打开功能。 文件的新建默认在当前文件夹下,文件默认格式为cpp。打开功能默认的格式同样在当前目录。存储时默认的存储格式为cpp。 编辑器会在用户未保存当前文稿就关闭编辑器时进行提醒。
编辑器会对用户的代码进行高亮。增强代码的可读性,从而减少可能出现的语法错误。
在用户输入时进行正则匹配,将所有输入的字符分为以下的几种情况:
QTextCharFormat keywordFormat;
QTextCharFormat classFormat;
QTextCharFormat singleLineCommentFormat;
QTextCharFormat multiLineCommentFormat;
QTextCharFormat quotationFormat;
QTextCharFormat functionFormat;
每种情况赋予不同的颜色,如下面代码是对注释进行高亮:
singleLineCommentFormat.setForeground(Qt::green);
rule.pattern = QRegularExpression("//[^\n]*");
rule.format = singleLineCommentFormat;
highlightingRules.append(rule);
multiLineCommentFormat.setForeground(Qt::green);
首先将前景色赋为绿色,接下来进行正则匹配,如果符合对应的规则,就赋予同级的颜色高亮。
编辑器同样可对用户进行代码提示。此处的代码提示是通过自定义一个库来完成,通过不断匹配公共前缀来进行可能的提示。 同时编辑器提供自动补全括号,引号的功能。
目前链接的程序仅仅是词法分析的程序。词法分析通过VS编译后生成exe文件,编辑器通过process方法直接调用该exe文件并传入参数。最终结果显示在编辑器的输出区域。
在编辑器中输入代码,分析结果如下: