简易编译器流程图:
一个典型的编译器,可以包含为一个前端,一个后端。前端接收源程序产生一个中间表示,后端接收中间表示继续生成一个目标程序。所以,前端处理的是跟源语言有关的属性,后端处理跟目标机器有关的属性。
复杂的编译器:
词法分析器:
1.词法分析器读入源代码,然后对字符流(源代码)做切分成记号流。举个例子:
这是一个程序员看到的字符流(源代码)
2.词法分析器将字符流读入,根据关键字、标识符、标点、字符串、整形数等进行划分,形成记号流(单词):
举个例子: 假如源语句if(x>5),则词法分析器返回token{k=IF,lexeme=0};token{k=LPAREN,lexeme=0};token{k=ID,lexeme="X"};……
词法分析器的任务:字符流到记号流。
字符流:和被编译语言密切相关(ASCII,Unicode,or……)
记号流:编译器内部定义的数据结构,编码所识别出的词法单元
语法分析器:
语法分析器(Parser)通常是作为编译器或解释器的组件出现的,它的作用是进行语法检查(检查语法是否符合这个语言的规则)、并构建由输入的单词组成的数据结构(一般是语法分析树、抽象语法树等层次化的数据结构)。语法分析器通常使用一个独立的词法分析器从输入字符流中分离出一个个的“单词”,并将单词流作为其输入。实际开发中,语法分析器可以手工编写,也可以使用工具(半)自动生成。
抽象语法树是对程序语法的抽象表示
例如,当在开发语言时,可能在开始的时候,选择LL(1)文法来描述语言的语法规则,编译器前端生成LL(1)语法树,编译器后端对LL(1)语法树进行处理,生成字节码或者是汇编代码。但是随着工程的开发,在语言中加入了更多的特性,用LL(1)文法描述时,感觉限制很大,并且编写文法时很吃力,所以这个时候决定采用LR(1)文法来描述语言的语法规则,把编译器前端改生成LR(1)语法树,但在这个时候,你会发现很糟糕,因为以前编译器后端是对LL(1)语树进行处理,不得不同时也修改后端的代码。
1.抽象语法树的第一个特点为:不依赖于具体的文法。无论是LL(1)文法,还是LR(1),或者还是其它的方法,都要求在语法分析时候,构造出相同的语法树,这样可以给编译器后端提供了清晰,统一的接口。即使是前端采用了不同的文法,都只需要改变前端代码,而不用连累到后端。即减少了工作量,也提高的编译器的可维护性。
2.抽象语法树的第二个特点为:不依赖于语言的细节。在编译器家族中,大名鼎鼎的gcc算得上是一个老大哥了,它可以编译多种语言,例如c,c++,java,ADA,Object C, FORTRAN, PASCAL,COBOL等等。在前端gcc对不同的语言进行词法,语法分析和语义分析后,产生抽象语法树形成中间代码作为输出,供后端处理。要做到这一点,就必须在构造语法树时,不依赖于语言的细节,例如在不同的语言中,类似于if-condition-then这样的语句有不同的表示方法
语义分析器:
对语法树的合法性进行处理(例如:一个变量在使用之前是否先定义声明,所调用的函数是否有对应的定义),产生相应的中间代码或目标代码.
语义分析是编译过程的一个逻辑阶段, 语义分析的任务是对结构上正确的源程序进行上下文有关性质的审查,进行类型审查。语义分析是审查源程序有无语义错误,为代码生成阶段收集类型信息。比如语义分析的一个工作是进行类型审查,审查每个算符是否具有语言规范允许的运算对象,当不符合语言规范时,编译程序应报告错误。如有的编译程序要对实数用作数组下标的情况报告错误。又比如某些程序规定运算对象可被强制,那么当二目运算施于一整型和一实型对象时,编译程序应将整型转换为实型而不能认为是源程序的错误。
经过上面的处理,程序中就没有在包括语言和语义的错误(除非编译器本身就有bug)
中间代码
中间代码也叫中间语言
(Intermediate code /language)是:源程序的一种内部表示,不依赖目标机的结构,复杂性介于源语言和机器语言之间。
中间代码的优点
1、逻辑结构清楚;
2、利于不同目标机上实现同一种语言;
3、利于进行与机器无关的优化;
中间代码可以生成例如 三地址代码 SSA 控制流图 等 (中间码的生成也是取决于编译器设计中的考虑,例如需不需要优化,或者追求速度,性能等).
关于语法分析器和中间代码:
LINK: https://blog.csdn.net/yongchaocsdn/article/details/79056504
代码生成:
中间代码可以被最终的一个代码生成的阶段处理为最后的目标代码(例如机器码,JVM字节码)
符号表
符号表是存储程序编译过程中重要信息,可以给每个阶段提供支持
在计算机科学中,符号表是一种用于语言翻译器(例如编译器和解释器)中的数据结构。在符号表中,程序源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址。
符号表在编译程序工作的过程中需要不断收集、记录和使用源程序中一些语法符号的类型和特征等相关信息。这些信息一般以表格形式存储于系统中。如常数表、变量名表、数组名表、过程名表、标号表等等,统称为符号表。对于符号表组织、构造和管理方法的好坏会直接影响编译系统的运行效率。
举个例子:
一个加法表达式(sum)在编译中的过程:
例如sum的语法规则是:
1.整数数字 n
2.加法表达式式 n1+n2
根据上面的规则
3
1+2
1+2+3 (加法表达式遵循左结合也就是 1+2 的结果 3+3 这也属于加法表达式的一种)
这上面的三种都遵守了加法表达式的规则 所以都是加法表示式的一种
目标机器: 栈式计算机(stack)
是一个LIFO的一个先入后出存储器,跟它向对应的是FIFO存储器先入先出传统顺序
有两条指令:
1.push n (将指定的参数压入栈中)
2.add (将栈顶的两个索引的数据弹出并进行加法运算,再将运算后的结果压入栈中)
//x和y表示栈顶的数据 pop[]表示弹出栈顶的数据
x = pop[]y = pop[]//加法运算
n = x+y//压栈
push n
例如1+2+3加法运算经过语法分析器翻译抽象语法树(AST):
1+2+3 =语法分析器: 语法树(+)(+) (3)(1) (2)
先从左结合开始:
1+2 = 3
3+3 = 6
生成栈式计算机(stack)的代码:
代码生成使用树的后续遍历(从树的左边子节点开始遍历,然后遍历右边子节点最后遍历根节点)
代码生成规则:
1. 遍历树中时如果遇到整数 n 生成代码: push n
2.遍历树中时如果遇到 + 生成代码: add
最后生成的栈式计算机的代码:
push 1push 2addpush 3add