编译器的第二个步骤是 语法分析(syntax analysis) 或者称为 解析(parsing)。语法分析器使用由词法分析器生成的各个词法单元的第一个分量来创建树形的中间表示。常用的方法就是 语法树(syntax tree)。编译器的后续步骤都会使用这个语法结构来帮助分析源程序,并声称目标程序。
语义分析语义分析是由 语义分析器(semantic analyzer) 完成的,它使用语法树和符号表中的信息来检查源程序是否和语言定义的语义一致。语义分析器也收集类型信息,并把这些信息放在语法树或者符号表中,以便后续的中间代码生成器使用。
语义分析会进行类型检查(type checking),这是语义分析器的一个最重要的功能。编译器会检查每个运算符是否具有匹配的运算分量。举个例子比如设计语言要求一个数组的下标是整数,如果你用浮点数座位下标,编译器就会出错。
某些程序设计语言比如 Java 会允许自动类型转换(coercion)。如果整数和浮点数进行运算,编译器会把整数转换为浮点数。
中间代码生成在源程序的语法分析和语义分析完成后,很多编译器生成一个明确的低级类机器语言的中间表示。我们可以把中间表示形式看作是抽象,中间形式的代码应该具有两个重要的性质:易于生成,并且能够轻松的被翻译。一般常用的一种是 三地址指令(three-address instructions)的中间表示形式。我们后面会细说。
代码优化代码优化会试图改进代码以便生成更好的目标代码。更好通常情况下意味着更快,但是也可能会有其他目标,比如更短或能耗更低的目标代码。
代码生成代码生成通过中间代码作为输入,并把它映射为目标语言。如果目标语言是机器代码的话,那么必须要为每个变量分配寄存器或内存位置。解释一下上面的运行结果。
每个指令的第一个运算分量指定了一个目标地址,各个指令中的 F 告诉我们它处理的是 浮点数, 上面代码首先把 id3 装载进 R2 寄存器中,然后把 id2 装载进 R1 寄存器中,再对 R1 目标进行 R1 和 R2 寄存器相加的操作。最后把寄存器 R1 的值存放到 id1 的地址中。
符号表管理我们上面提到了符号表的概念,它是一个编译器很重要的功能。符号表能够记录源程序中使用变量的名称,并收集和每个名称相关的属性信息。它相当于一个秘书的作用。符号表还记录了每个变量名字的条目。后面我们会详细的介绍符号表。
编译器构造工具和软件开发一样,写编译器的人可以充分利用现代的软件开发环境进行开发。通常也有 语言编辑器、调试工具、版本管理、测试工具等。除此之外,还需要一些更专业的工具来实现编辑器不同阶段的代码生成。
一些常用的编译器构造工具有
语法分析器生成器:可以根据程序设计语言的语法描述自动生成语法分析器
扫描器生成器:可以根据一个语言的语法单元的正则描述生成词法分析器
语法制导的翻译引擎:用于生成一组遍历分析树并声称中间代码
代码生成器:用于把中间代码转换为目标代码
数据流分析引擎:用于分析输入是如何传递到另一部分的
编译器构造工具:提供用于构造编译器不同阶段的例程
程序设计语言的发展历程计算机从 20 世纪 40 年代创建至今都只能理解二进制语言,亘古不变。这个 0 、 1 组成的序列能够告诉计算机以什么样的顺序执行怎样的运算。运算本身是很底层的:比如把一个数据从一个位置进行移动;把两个寄存器的内容进行相加、比较两个值,为了避免如此枯燥的运算,我们开发了各种各样的编程语言,但是计算机底层的计算方式一直没变,所以学习哪个技术性价比高,明白了吗?下面我们就来一起认识一下程序设计语言的历程。
高级设计语言首先被开发出来的是 20 世纪 50 年代的汇编语言,5 年后发生了重要的进步,用于科学计算的 Fortran 被开发出来,用于商业处理的 Cobol 语言和用于符号计算的 Lisp 语言被开发出来;然后接下来的时间,慢慢很多编程语言被开发出来,比如 C、C++、Java、JavaScript、Python 等。后面还有用于数据处理的 SQL 语言。
语言分类说到给这些编程语言分类,那可是有太多了,不过我们专注一下高频的分类。