<网页blog已经上线,一大波干货即将来袭:https://faiculty.com/>
编译器构造
一、设计文法
<func-body>::=<statement>
<statement>::="{"<statement>"}" /*blockstatement*/
|[<type>] <identi> ["="<expr>]";" /*assignment*/
|"return"<expr>";"
|"if""("<expr>")"<statement>["else"<statement>]
|"while""("<expr>")"<statement> |<expr>";"
<expr>::=<bitwise-expr>
|<bitwise-expr>=<expr>
<bitwise-expr>::=<eq-expr>
|<bitwise-expr>&<eq-expr>
|<bitwise-expr>|<eq-expr>
<eq-expr>::=<rel-expr>
|<eq-expr>==<rel-expr>
|<eq-expr>!=<rel-expr>
<rel-expr>::=<shift-expr>
|<rel-expr><<shift-expr>
|<rel-expr>><shift-expr>
<shift-expr>::=<add-expr>
|<shift-expr><<<add-expr>
|<shift-expr>>><add-expr>
<add-expr>::=<mul-expr>
|<add-expr>+<mul-expr>
|<add-expr>-<mul-expr>
<mul-expr>::=<postfix-expr>
|<mul-expr>*<postfix-expri>
|<mul-expr>/<postfix-expri>
<postfix-expr>::=<prim-expr>
|<postfix-expr>[<expr>]
|<postfix-expr>(<expr>{","<expr>})
<prim-expr>:=<number>|<ident>|<string>|"'"<char>"'"|"("<expr>")"
二、词法分析器
1.主要目标
这一个部分的主要内容是:
1. 根据输入的文件切割成一个个字符
2. 构造基本的符号表
3. 后期每次调用readToken函数得到一个token,然后给语法分析器。(这里不是一次性切割)
2.基本思想
利用文件指针一个个从源文件读一个字符,利用自动机判断, 切割出的单词放入 tok 字符数组中。这里我们将每个符号分为单独的一组,比如 标识符是一类、 ‘+’号、 ‘/’号等等一系列的符号都作为单独的一类,每种类别 都有一个 ID。调用该函数就能得到当前 token 的类别码 ID。
这里正规的分词是使用的自动机。
3.单词的分类
常见的分类是:
1. 标识符。用户给变量起的一些名字。
2. 常数。
3. 关键字。
4. 界符。包含单字界符(*,+….)和双字界符(:=, <=….)
本实验中,我们按如下分类:
1. 标识符。ID = 0,
2. 字符。ID = 1
3. 字符串。ID = 2
4. 数字(这里暂时只支持简单的整数)。ID = 3
5. 所有的关键字和界符进行排列。ID依次增加。
//关键词和界符
char *ch[31] ={"int", "char",
"main","void","if","else","while","return",
"+","-","*","/","|","&","||","&&",">","<",">=","<=",
"==","!=","=",",",";","(",")","{","}","[","]"};
/*
* scanner : words
* */
identi: ID is 00. // name of variable, can be characters, inter and _;
// const
characters: ID is 01.
string: ID is 02.
algrithem number: ID is 03.
keyWord: ID is 04 to 11:
all the keyworlds as followed: int char main void if else while return
//partion:
algrithem operator: + - * / 12 to 15
logical operator: | & || && > < >= <= == != 16 to 25
others: = , ; ( ) { } [ ] 26 to 34
4.关键点
- 读单个字符用fgetc。
- 其中涉及到很多C的函数,isspace、isnumber等等。
其中可能涉及到小数的识别。
1. n:拼尾数值变量;每当读入尾数数字d时: n:=10*n+val(d);
2. m:小数位数变量;每当读入小数数字d时: m:=m+1;
3. num = n * 10 ^ (-m)
5.数据结构
/**
*相应数据结构:
* 符号表: struct SYNBL{
* char name[100];
* TYPE* type; //只有当为数组时才有指向,如果只是Internet char类型那么直接为NULL
* char cat; // 种类编码:a代表数组,i代表整型,c代表字符型
* int offset; //该变量起始偏移量
* int memory; //变量长度
* }
*
* 类型表: struct TYPE{
* char cat; // 种类编码:a代表数组,i代表整型,c代表字符 型..暂时不支持多维数组
* ARRAY* array; //数组表指针
* }
*
* 数组表: struct ARRAY{
* int num; //元素个数;
* char cat; //元素类型,i代表整型,c代表字符型;
* int ArrayTM; //占用总内存;
* }
*
* */
三、语法分析
1. 目标
其任务是识别和处理比单词更大的语法单位,如:程序设计语言中的表达式、各种说明和语句乃至全部源程序,指出其中的语法错误;必要时,可生成内部形式,便于下一阶段处理。
2.常见使用方法
2.1 递归子程序
算法:
对每一个非终结符,构造一个子程序,用以识别该非终结符所定义的符号串。每个子程序以产生式左部非终结符命名,以产生式右部构造子程序内容。
文法要求:
LL(1)文法
- 具有相同左部的产生式,首符号不同。 A -> a|a (首符号相同,×);
- 文法不能有左递归。 A -> A| (直接左递归,×)。
2.2 LL(1)分析法
LL(1)分析法的要求:
1. 利用一个分析表,登记如何选择产生式的知识;
2. 利用一个分析栈,记录分析过程;
3. 此分析法要求文法必须是 LL(1)文法。
算法:
- 先根据文法建立一个分析表。因为具有相同左部的产生式首符号不同,所以建立的表示唯一的。如上图。
- 然后利用栈对符号串进行语法分析。首先先把Z压入栈,再弹栈,读第一个单词,查找分析表,把相应的产生式逆序压栈。如果没有相应产生式,那么证明这句话有语法错误。如图:
LL(1)文法判定:
2.3 LR分析法
3.本实验
本实验中采用递归子程序法。如下图:
4.语义分析
语义分析主要包含语法制导和中间代码的生成。
5.语法制导
语法制导就是在原文法中加入语义动作符号,方便后续的中间代码生成。这里加入语义动作符号后,可以很方便的生成 逆波兰式。
6.中间代码生成
逆波兰式运算符进栈之日就是四元式生成之时。
7.设计方法
/**
* 中间代码数据结构:
* struct GEN_CODE{
* char op[10]; // operation, it's maybe a '*' or '<' or 'if' and so on, "_" means nothing
* char R1[20]; // operate object 1
* char R2[20]; // operate object 2
* char des[20]; // destination or answer of this operation
* };
*
* 中间代码生成方式:
* 1. a = b + c:
* 结果:(+, b, c, t)
* (=, a, _, t)
*
* 2. a == b:
* 结果: (==, a, b, t) // 这里t就是表达式的值
*
* 3. if(exp):
* 结果:(if, exp, _, _)
*
* 4. else{}:
* 结果:(else, _, _, _)
*
* 5. while(exp){}
* 结果:(while, _ , _, _)
* ......
* (do, exp, _, _)
* ......
* (whend, _, _, _)
*
* 6. return i;
* 结果:(RET, i, _, _)
*
* 7. char a = 'c':
* 结果:(=, 'c', _, a)
*
* 8. int main(){} //函数定义
* 结果:(main, _, _, _)
*
* 9. if or while语句块结束标签:
* 结果:(ifend, _, _, _)
* 结果:(whend, _, _, _)
* 结果:(elend, _, _, _)
*
**/
四、符号表
1. 作用
本实验中符号表主要用来预分配内存。也就是根据语义分析的结果,预先知道整个程序有多少个变量,给一个虚拟空间来分配内存。(这里只需要存贮用户定义的变量)
2.数据结构
/**
*相应数据结构:
* 符号表: struct SYNBL{
* char name[100];
* TYPE* type; //只有当为数组时才有指向,如果只是 Internet char类型那么直接为NULL
* char cat; // 种类编码:a代表数组,i代表整型,c代 表字符型
* int offset; //该变量起始偏移量
* int memory; //变量长度
* }
*
* 类型表: struct TYPE{
* char cat; // 种类编码:a代表数组,i代表整型,c代表 字符 型..暂时不支持多维数组
* ARRAY* array; //数组表指针
* }
*
* 数组表: struct ARRAY{
* int num; //元素个数;
* char cat; //元素类型,i代表整型,c代表字符型;
* int ArrayTM; //占用总内存;
* }
*
* */
五、目标代码生成
这一部分主要功能是由语义生成的四元式生成可编译、可执行的汇编语言。主要 依据和处理事情是:
1. 根据符号表开辟运行空间,生成汇编语言头部
2. 扫描中间代码,生成相应的汇编语言段
1. 汇编语言生成规则
A. 环境:在本次课程设计中,我们采用的是 8086 汇编语言,主要使用到的 指令有:LEA、MOV、ADD、MUL、JMP、JE、SUB、CMP 等等。
B. 原则:每条四元式都会对应一段汇编语言,每次运算结束后都将变量存入 开辟的相应的地址中。所以没有相应的活跃信息表,不需要寄存器的调度算法。
2. 设计思路:
按照上述给出的四元式进行分类,每种类型的四元式对应一段具体的汇编代 码,具体请见四元式到汇编语言的翻译。其中需要解决的是回填问题,有以下几 类回填:
A. 在逻辑表达式中,如 a>b 中。我们将得到的四元式是: (>,a,b,t1),这里需要对 t1 进行赋值,当为真时赋值为 1,否 则为 0。 这里的不需要反填,按照具体汇编代码可以直接写出。
B. 在 if 语句中,如果不成立那么直接要跳转到 else 语句块或者 if 语句块结束 的下一条语句。处理如下:
1.遇见 if 保存当前汇编语言的标签,或者是当条四元式编号。
2.遇见 ifend 表示 if 语句块已经结束,此时添加一条空白汇编语言,待反 填。再加一条跳转汇编语句,如果执行到这,那么表示 if 条件成立,这里需要跳 转到 else 语句块的结束标签
3.遇见 else 反填 if 中的 JMP 4.遇见 elend 反填 ifend 中的 JMP
C. 在 while 语句块中,如果 do 成立则循环,否则跳出循环。处理如下: 1. 在 do 语句块结束处加上 JMP 跳转到 while 语句开始 2. 遇见 we,回填 do 中不成立时预留的 JMP
2.规则
/** 中间代码生成规则:
* [a]: [0004H],其中4是指a的偏移量
* //注意这里都是 16位的寄存器,对于int型,还有缺陷。可以改用32位
*
* 0. 查询符号表,编写汇编头部:
* DSEG SEGMENT
* ....
* .... //为每个变量分配空 间,根据size
* DSEG ENDS
* CSEG SEGMENT
* ASSUME: CS:CSEG, DS:DSEG
* START: MOV AX,DSEG
* MOV DS,AX
*
* 1. (=, 8, _, a) --->
* MOV AX, 8
* MOV SI, a
* MOV [SI], AX
*
* (=, t, _, b) --->
* MOV AX, [t]
* MOV SI, b
* MOV [SI],AX
* 2. (+, a, b, t0) --->
* MOV AX, [a] //可以换成: MOV AX,[a] MOV BX, [b] ADD AX,BX 注意乘法和除法
* ADD AX, [b]
* MOV SI, t0 //t0 指偏移地址
* MOV [SI],AX
* 3. (=, 'c', _, a) --->
* MOV AL, 'c';
* MOV SI, a
* MOV [SI],AL
* 4. (==, a, b, t1) --->
* LEA SI, a
* MOV AX, [SI]
* LEA SI,b
* MOV BX, [SI]
* CMP AX, BX
* MOV SI,t1
* JE Label_4_10
* MOV AX,0
* JMP Label_4_11
* Label_4_10: MOV AX,1
* Label_4_11: MOV [SI],AX
* MOV SI, t1
* MOV [SI], AX
* 5. (>, a, b, t2) ---> //这里暂时只 允许变量和 立即数
* LEA SI, a
* MOV AX, [SI]
* LEA SI,b
* MOV BX, [SI]
* CMP AX, BX
* JG Label_5_11
* MOV SI, t2
* MOV AX,0
* MOV [SI], AX
* JMP Label_5_14 // 遇到这条语句 不用反填,直接就是当前语句编号+3就是他应该跳转的地方
* Label_5_11: MOV SI, t2
* MOV AX,1
* MOV [SI],AX
* Label_5_14: MOV AX,0
*
* 6. (if,t2, _, _) ---> // 必须在中间代 码中设计一个语句块结束标签
* Label_6_1: MOV AX, [t2]
* CMP AX, 0
* JE //else 的标签,
*
* 7. (ifend, _, _, _) --->
* Label_7_1: MOV AX, 0 //读到ifend或者 whend后就回填
* JMP //else语句块结束 后的标签
*
* 8. (else, _, _, _) --->
* Label_8_1: MOV AX, 0
*
* 9. (elend,_,_,_) --->
* Label_9_1: MOV AX,0
*
* 10. (while, ,_,_) --->
* Label_10_1: MOV AX,0
*
* 11. (do, exp, _, _) --->
* Label_11_1: MOV AX, [t3]
* CMP AX, 0
* JE // while 语句块 结束标签
* 12. (whend, _, _, _) --->
* Label_12_1: JMP // while 语句
* Label_12_2: MOV AX,0
*
* 13. (RET, _, _, _) --->
* Label_13_1: INT 21H
* CSEG ENDS
* END START
3.数据结构
/**
* 汇编语言生成数据结构:
* 1. 汇编代码栈:
* struct ASSEM{
* char lab[20]; //label of this instruction
* char instr[10]; //instruction name
* char op1[20]; // op1 of this instruction
* char op2[20]; // op2 of this instrcution
* };
*
* 2. 转移指令反填栈:
* int INST // 存的是待反填转移指 令的汇编语句编号,这里只有简单的反填。反填的是当前汇编语句的lab
*
* 反填规则:
* 遇到:(ifend, _, _, _)
* (else, _, _, _)
* (whend, _, _, _)
* */