转载自:我的个人博客
一个 Java 实现的 TXT 语言编译器, 目标平台为 RISC-V 32 (指令集 RV32M)。编译器大致分为词法分析、语法分析、语义分析及中间代码生成、目标代码生成四个部分。
源语言的示例代码
int result;
int a;
int b;
int c;
a = 8;
b = 5;
c = 3 - a;
result = a * b - ( 3 + b ) * ( c - a );
return result;
词法分析
编码表
类别 | 正则表达式 |
---|---|
return | return |
= | = |
, | , |
Semicolon | ; |
+ | + |
- | - |
* | * |
/ | / |
( | ( |
) | ) |
id | [a-zA-Z_][a-zA-Z]* |
IntConst | [0-9]+ |
正则文法
G = ( V , T , P , S ) G=(V,T,P,S) G=(V,T,P,S),其中 V = { S , A , B , C , 0 , 1 , 2 , … , 9 , c h a r } V=\{S, A, B, C, 0,1,2,…,9, char\} V={S,A,B,C,0,1,2,…,9,char}, T = { 任意符号 } T=\{任意符号\} T={任意符号}, P P P定义如下
约定: [ s − t ] [s-t] [s−t]表示从s到t的所有ASCII字符中的一个
标识符: S → [ a − z ] A , S \rightarrow [a-z] A, S→[a−z]A, A → [ a − z ] A ∣ [ 0 − 9 ] A ∣ ϵ A \rightarrow [a-z] A | [0-9] A | ϵ A→[a−z]A∣[0−9]A∣ϵ
整常数: S → [ 1 − 9 ] B , S \rightarrow [1-9] B, S→[1−9]B, B → [ 0 − 9 ] B ∣ ϵ B \rightarrow [0-9] B | ϵ B→[0−9]B∣ϵ
运算符: S → C , S \rightarrow C, S→C, C → = ∣ ∗ ∣ + ∣ − ∣ / C \rightarrow = | * | + | - | / C→=∣∗∣+∣−∣/
分隔符: S → D , S \rightarrow D, S→D, D → ; ∣ ( ∣ ) ∣ , D \rightarrow ; | ( | ) | , D→;∣(∣)∣,
状态转换图
根据识别的编码不同,将状态机划分为不同的状态,共计14个状态。在每识别到终止状态时,调用相关的函数,将语法单元添加到维护的表格中,并重新回到起始状态。状态机DFA对应的状态转换图如下图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SZUEAaHS-1675226405910)(/img/A-Simple-Compiler/状态转换图.png)]
流程
使用列表存储每个识别出的语法单元,读入输入代码,从前往后逐个读取字符,若遇到空格等其他空白字符跳过,否则,按照状态转换图识别语法单元,将其加入token列表中。若此语法单元为单词,且不是标识符,且不在符号表中,则其为新识别出的变量,将其添加到符号表中。
语法分析
状态栈和符号栈的数据结构
使用Stack类作为状态栈和符号栈的数据结构,状态栈的元素的基类为Status类,符号栈的基类为Token类(后续发现实际分析过程中不需要用到符号栈)。通过peek()函数获取栈顶元素,通过push()函数向栈中压入元素,通过pop()函数弹出栈顶元素,通过empty()函数判断栈是否为空。
流程
- 向状态栈中压入初始状态,向符号栈中压入符号“$”。
- 依次遍历输入串中的字符,根据当前状态栈栈顶的状态,判断此时应执行的Action。
- 若执行的动作为移进,则根据LR1分析表获取要转移到的状态(程序中存储在action对应的Status对象中),将状态压入状态栈,将字符压入符号栈,并调用移入时对应的观察者的函数(callWhenInShift函数),通知观察者当前读到的字符和要转移到的状态。
- 若执行的动作为规约,则根据LR1分析表获取要规约的产生式(程序中存储在action对象的production属性中),调用规约时对应的观察者的函数(callWhenInReduce函数),通知观察者当前读到的字符和要用于规约的产生式,ProductionCollector观察者会将所有规约的产生式依次存储在列表中;获取产生式的头和体,将产生式的体对应的符号数从状态栈和符号栈中弹出,并将产生式的头压入符号栈。根据当前状态和状态站顶部状态查找LR1表,判断此时需要压入状态栈顶部的状态,并将其压入。
- 若执行的动作为接受,则调用处于接受状态时对应的观察者的函数(callWhenInAccept)通知各个观察者,语法分析结束。
- 重复执行2-5步,直到读完代码中的所有字符,或状态栈为空,或产生错误。
语义分析和中间代码生成
翻译方案
S → D i d ; { i d . t y p e = D . t y p e } S \rightarrow D \ id; \{ id.type = D.type \} S→D id;{id.type=D.type}
D → i n t ; { i n t . t y p e = I N T , D . t y p e = i n t . t y p e } D \rightarrow int ; \{ int.type = INT, D.type = int.type \} D→int;{int.type=INT,D.type=int.type}
其余产生式规约式不执行任何动作
使用的数据结构
**语义分析:**使用基类位SourceCodeType的栈(Stack)作为类型栈typeStack的数据结构,存储当前符号栈内对应符号的类型(type);使用基类为Token的栈(Stack)作为符号栈tokenStack的数据结构,存储移进的单词(token)
**中间代码生成:**使用基类为Instruction的列表(List)instructions作为中间代码存储的数据结构,依次存储生成的中间代码,使用基类为IRValue的栈(Stack)作为符号栈irValueStack的数据结构,存储符号栈对应的ir表达式的值。
流程
- 当动作为接受时,不执行任何操作。
- 当动作为移进时,将对应的符号压入符号栈。如果此符号为关键字int,则将INT压入类型栈,否则将null压入类型栈。
- 当动作为规约时,首先判断要执行规约的产生式编号,若编号为4,则跳到第4步,若编号为5,则跳到第5步,否则,调到第6步.
- 此时待规约的产生式为 S → D i d ; S \rightarrow D \ id; S→D id;。分别弹出并获取符号栈和类型栈顶端的两个元素,并设置为符号栈第一个元素(id)在符号表中的类型为类型栈从上往下第二个元素的类型(D对应的类型)。并向符号栈和类型栈中压入null(对应左部的S)。
- 此时待规约的产生式为 D → i n t ; D \rightarrow \ int ; D→ int; 弹出符号栈和类型栈栈顶的元素,将类型栈栈顶元素的类型作为左部符号的类型,再压入符号栈,向符号栈中压入null(对应左部的D)。
- 此时待规约的产生式没有特殊的翻译动作,只需从符号栈和类型栈中弹出与产生式右部元素个数一致的元素,并压入null作为左部对应的符号/类型。
目标代码生成
对中间代码进行预处理
- 将操作两个立即数的 BinaryOp 直接进行求值得到结果, 然后替换成 MOV 指令。
- 将操作一个立即数的指令 (除了乘法和左立即数减法) 进行调整, 使之满足 a := b op imm 的格式。
- 将操作一个立即数的乘法和左立即数减法调整, 前插一条 MOV a, imm, 用 a 替换原立即数, 将指令调整为无立即数指令.
- 舍弃Ret指令之后的所有指令
识别各变量最后一次使用的位置
从后往前遍历预处理后的中间代码序列。用列表存储当前已经遍历到过的变量。每处理一条指令,分别判断它的两个原操作数中是否为立即数,若不是,再查看此变量是否被遍历到过。若此变量从未被遍历到过,说明此变量最后一次出现的位置便为这条指令,将此信息存入列表中保存。
在遍历过程中,若变量出现在指令的目标操作数位置,且不出现在源操作数中,说明此指令前此变量的值与后续执行过程无关,将此变量从存储但钱已经遍历到过的变量列表中删去。
将中间代码转化为目标代码
创建AsmInstruction类,表示riscv形式的汇编代码类,创建AsmInstructionKind枚举类,表示riscv代码的各种类型。
创建RegisterAssigner类,用于分配和回收空闲的寄存器。其中,通过列表维护空闲寄存器,利用双射Map维护变量和已利用的寄存器之间的双射关系。当请求分配寄存器时,从空闲寄存器列表随机分配一个寄存器,并将其添加到双射Map中。当回收某一寄存器时,将其从双射Map中删除,并将相应寄存器重新存放回空闲寄存器列表。
遍历处理后的中间代码,提取每条指令的左右操作数,将其转化为相应的AsmInstruction对象,并存放到列表中。
根据维护的变量最后一次出现的位置信息,若当前指令为变量最后一次出现的指令,则将相应变量对应的寄存器释放。