自制脚本语言
解释器与编译器
语言处理器可大致分为解释器与编译器两种。这两类语言处理器的执行原理有很大差异。
- 解释器 解释器根据程序中的算法执行运算。简单来讲,它是一种用于执行程序的软件。如果执行的程序由虚拟机器语言或类似于机器语言的程序设计语言写成,这种软件也能称为虚拟机。
- 编译器 编译器能将某种语言写成的程序转换为另一种语言的程序。通常它会将原程序转换为机器语言程序。编译器转换程序的行为称为编译,转换前的程序称为源代码或源程序。如果编译器没有把源代码直接转换为机器语言,一般称为源代码转换器或源码转换器(source code translator)。
程序设计语言提供了何种类型的语言处理器不一而论,一些具有解释器,另一些则会提供编译器。例如,尽管 C 语言也提供了解释器,但却很少使用。C 语言通常直接通过编译器转换为机器语言执行。转换后得到的机器语言程序会暂时保存至某个文件,需要借助操作系统来执行。 另一方面,Common Lisp 或 Haskell 等语言一般会同时提供解释器与编译器,供用户根据需要选用。
有些语言混用解释器与编译器。通常,Java 语言首先会通过编译器把源代码转换为 Java 二进制代码,并将这种虚拟的机器语言保存在文件中。之后,Java 虚拟机的解释器将执行这段代码。
传统的狭义的编译器将会以文件形式保存转换后的程序。因此,只要源程序没有变更,编译就仅需执行一次,执行时间也会缩短。然而,一些编译器并不保存转换后的程序文件。这种编译器常见于解释器内部。 大多数 Java 虚拟机为了提高性能,会在执行过程中通过编译器将一部分 Java 二进制代码直接转换为机器语言使用。执行过程中进行的机器语言转换称为动态编译或 JIT 编译(Just-In-Time compile)。转换后得到的机器语言程序将被载入内存,由硬件执行,无需使用解释器。 编译器的用途多样。如上所述,它能够直接在解释器内部执行。此外,编译器的作用也不局限于将源程序转换为机器语言。例如,Ruby 语言的解释器内部会通过编译器来执行预处理工作,将源程序转换为类似于 Java 二进制代码的虚拟机器语言程序。解释器真正执行的是这种经过编译的语言。这种设计提高了执行性能。
语言处理器的结构
无论是解释器还是编译器,语言处理器前半部分的程序结构都大同小异。源代码首先将进行词法分析,由一长串字符串细分为多个更小的字符串单元。分割后的字符串称为单词。之后处理器将执行语法分析处理,把单词的排列转换为抽象语法树。至此为止,解释器与编译器的处理方式相同。之后,编译器将会把抽象语法树转换为其他语言,而解释器将会一边分析抽象语法树一边执行运算。
设计程序设计语言
词法分析器
语言处理器的第一个组成部分是词法分析器(lexical analyzer、lexer 或 scanner)。程序的源代码最初只是一长串字符串。从内部来看,源代码中的换行也能用专门的(不可见)换行符表示,因此整个源代码是一种相连的长字符串。这样的长字符串很难处理,语言处理器通常会首先将字符串中的字符以单词为单位分组,切割成多个子字符串。这就是词法分析。
Token 对象
程序设计领域中的单词包含 + 或 == 之类的符号。例如,下面是某个程序中的一行代码。
while i < 10 {
词法分析会把它拆分为下面这样的字符串。
"while" "i" "<" "10" "{"
这句代码被分割为了 5 个字符串。其中 while 是一个词语,但要把 < 与 { 也称作词语,的确有些不自然,因此,人们通常把词法分析的结果称为单词(token)。 词法分析将筛选出程序的解释与执行必需的成分。单词之间的空白或注释都会在这一阶段被去除。 词法分析器将把程序源代码视作字符串,并把它分割为若干单词。分割后得到的单词并不是简单地用 String 对象表示,而是使用了Token 对象。这种对象除了记录该单词对应的字符串,还会保存单词的类型、单词所处位置的行号等信息。 Token 类根据单词的类型,又定义了不同的子类。Stone 语言含有标识符、整型字面量和字符串字面量这三种类型的单词,每种单词都定义了对应的 Token 类的子类。每种子类都覆盖了 Token 类的 isIdentifier (如果是标识符则为真)、isNumber (如果是整型字面量则为真)及 isString (如果是字符串字面量则为真)方法,并根据具体类型返回相应的值。