龙书阅读笔记

星期四, 九月 28, 2006

2.4 PARSING

解析是判断一个token字符串是否能够被一个文法生成的过程。在讨论这个问题的时候,考虑将被构造的一棵解析树是很有帮助的,即使一个编译器不一定构造这样一棵数。但是,一个解析器必须能够构造树,不然的话,翻译不能保证一定是正确的。

本节介绍一种解析方法,可以应用来构造语法导向翻译器。一个完整的C程序,实现Fig 2.13的translation scheme,出现在下一节中。一种可选的方式是使用一种软件工具直接从一个translation scheme产生一个翻译器。参见4.9描述这样一种工具。他可以不加修改实现Fig 2.13的translation scheme.

可以为任何文法构造一棵解析树。但是实际中使用的文法,有一种特殊的形式。对任何上下文无关的文法,有一个解析器取最多O(n^3)时间来解析一个n个token的字符串。但是3次方的时间太昂贵了。给定一个编程语言,我们一般能够构造一个快速解析的文法。实际中,线性算法基本上足以解析所有的语言。编程语言解析器基本上总是在输入上进行一次从左到右的扫描,每次只看一个token.

最大多数解析方法属于两种类别之一,叫做top-down自顶向下和bottom-up自底至上.这些术语指解析树的节点被构造的顺序。前者,构造从根节点开始,朝着叶节点进发,而后者,构造开始于叶节点,朝着根前进。top-down解析器的流行归功于采用top-down方法能够用手工比较简单地构造高校的解析器。但是Bottom-up解析,能够处理更多类别的文法和translation schemes,因此直接用来从文法产生解析器的软件工具趋向于采用bottom-up方法。

Top-Down Parsing

我们用适合这种方法的一种文法介绍top-down解析。本节后面,我们讨论一般性的top-down解析器构造。下面的文法生成Pascal的一个子集。我们使用dotdot代表"..",重点突出字符序列当作一个单元。

typesimple
| ↑id
| array [simple] of type
simpleinteger
| char
| num dotdot num

一棵解析树的top-down构造,从root开始,标以开始非终结符,然后重复下面的两步(Fig 2.15为例).
  1. 在节点n,标以非终结符A,选择A的其中一个产生式,用产生式右边的符号构造n的子节点
  2. 寻找将被构造子树的下一个节点


对某些文法而言,上面各步可以在对输入字符串进行一次从左到右的扫描即可实现。输入中当前被扫描的token经常被引用为lookahead符号。开始,lookahead 符号是第一个,也就是最左边,输入字符串的token。Figure 2.16演示了解析下面字符串的过程:
array [ num dotdot num] of integer



最初,array这个token是lookahead symbol, 解析树的已知部分由root组成,标签是Fig. 1.16(a)的开始非终结符type。目标是构造解析树的剩余部分,以便让这个解析树生成的字符串和输入字符串匹配。

当一个匹配发生的时候,Fig. 2.16(a)的非终结符type必须派生一个字符串,从lookahead symbol array开始。在文法(2.8)中,只有一个type的产生式可以派生这样的字符串,我们选择它,用它右边构造root的子节点。

Fig2.16三个快照的每一个都有一个箭头标记输入字符串中的lookahead symbol,以及当前被考虑的解析树中的节点。当某一个节点的子节点被构造时,我们接下去考虑最左边的子。在Fig2.16(b)中,root的子刚刚被构造出来,最左边的子array正被考虑。

当解析树中被考虑的节点是一个终结符,并且终结符匹配lookahead symbol的时候,我们同时前进解析树和输入。输入中的下一个token变成新的lookahead symbol,解析树的下一个子节点被考虑。在Fig. 2.16(c)中,解析树的箭头前进到root的下一个子节点,输入中的箭头也前进到下一个token [。在下一次前进以后,解析树中的箭头将指向非终结符simple这个子节点。当一个非终结符的节点被考虑时,我们将重复为非终结符选择一个产生式的过程。

一般来说,非终结符产生式的选择可能回引起trail-and-error;也就是,我们可能必须尝试一个产生式,如果发现不合适的时候回退并尝试另外一个产生式。如果使用了一个产生式,我们不能完成匹配输入字符串的一棵树,那么这个产生式是不合适的。但是,这里有一种重要的特殊情况,叫做predictive parsing,不会发生回退。

Predictive Parsing

Recursive-descent parsing递归下降解析是一种top-down的语法分析方法,其中我们执行一系列递归过程去处理输入。一个过程和文法的每一个非终结符关联。这里我们考虑递归下降解析的一种特殊情况,叫做predictive parsing,其中lookahead symbol无二义性地确定了每一个非终结符选择的过程。处理输入时过程的被调用序列隐式地定义了输入的一棵解析树。

procedure match(t:token);
begin
if lookahead = t then
lookahead := nexttoken
else error
end;

procedure type
begin
if lookahead is in {integer, char, num} then
simple
else if lookahead = '↑' then begin
match('↑');match(id)
end
else if lookahead = array then begin
match(array);match('[');simple;match(']');match(of);type
end
else error
end;

procedure simple
begin
if lookahead = integer then
match(integer)
else if lookahead = char then
match(char)
else if lookahead = num then begin
match(num);match(dotdot);match(num);
end
else error
end;

Fig. 2.17. Pseudo-code for a predictive parser

Fig. 2.17的predictive解析器由文法(2.8)中的非终结符type和simple对应的过程组成,另外还有一个附加的过程match.我们使用match来简化type和simple的代码。它前进到下一个输入token,如果它的参数t匹配lookahead symbol。于是match改变变量lookahead,它是当前被扫描的输入token.

解析从我们文法中的开始非终结符type对应的过程开始。和Fig. 2.16一样的输入,lookahead开始是第一个token array。过程type 执行代码
match(array);match('[');simple;match(']');match(of);type

对应产生式的右边
typearray [simple] of type
注意右边的每一个终结符被lookahead symbol匹配,而每一个非终结符导致对它相应过程的调用。

使用Fig. 2.16的输入,在array后面,[被匹配之后,lookahead符号是num。在这一点过程simple被调用,执行代码:
match(num);match(dotdot);match(num);

lookahead符号引导对产生式的选择。如果产生式的右边开始于一个token,那么在lookahead symbol匹配这个token的时候,这个产生式就能够使用。现在考虑一个右边开始于一个非终结符,例如:
type → simple (2.10)
这个产生式在lookahead symbol 能够从simple生成出来的时候使用。例如,在执行代码片断(2.9)的过程中,假设lookahead symbol是integer,当控制到达过程调用type的时候。没有任何type的产生式开始于token integer。但是,simple的一个产生式确实开始于integer,因此产生式(2.10)被使用,在lookahead是integer的时候,type调用simple过程。

Predictive parsing依赖于一个信息,就是一个产生式的右边能够产生什么样的第一个symbol。更加精确地说,让α非终结符A的右边。我们定义FIRST(α)为由α生成的字符串中,可能出现为第一个symbol的token集合。如果α是ε,或者能够生成ε,那么ε也在FIRST(α)之内。例如:

FIRST(simple) = {integer, char, num}
FIRST(↑id) = {↑}
FIRST(array [ simple ] of type) = {array}


实践中,很多产生式右边以token开始,从而简化了FIRST集合的构造。计算FIRST的一个算法在4.4中。

如果有两个产生式A →α和A →β,那么必须考虑FIRST集合。没有回退的递归下降解析需要FIRST(α)和FIRST(β)是分离的。这样lookahead符号能够用来决定使用哪一个产生式。如果lookahead符号在FIRST(α)中,那么采用α。不然,如果lookahead 符号在FIRST(β)中,那么使用β。


When to Use ε-Productions

右边带有 ε的产生式需要特别的对待。递归下降解析器将使用ε-Production作为缺省,如果没有其他的产生式可用。例如,考虑
stmtbegin opt_stmts end
opt_stmtsstmt_list | ε

在解析opt_stmts的时候,如果lookahead符号不在FIRST(stmt_list)中,那么将使用ε-Production。这个选择非常正确,如果lookahead符号是end.任何不是end的lookahead符号将导致错误,在解析stmt的过程中。


Designing a Predictive Parser


一个predictive parser 是一个程序,其中每一个非终结符都有一个过程。每一个过程做两件事情:
  • 它通过察看lookahead符号决定采用哪个产生式。如果产生式的右边为α,lookahead符号在FIRST(α),那么将采用这个产生式。如果对任何lookahead 符号,两个右边存在着一个冲突,那么我们就不能在这种文法上使用这种解析方法。一个右边具有ε的产生式将被使用,如果lookahead符号不再任何其它右边的FIRST集合中。
  • 过程通过模拟右边使用一个产生式。一个非终结符导致对应过程的调用,一个匹配lookahead符号的token导致下一个token被读入。如果在某些点,产生式中的token和lookahead符号不匹配,那么就会报告错误。Fig 2.17是把这些规则应用到文法2.8的结果。
如同一个translation scheme是通过扩展一个文法实现的,一个语法导向的翻译器可以通过扩展一个predictive parser形成的。为此目的的一个算法在Section 5.5给出。下面的受限构造suffices for the present,因为本章实现的translation scheme没有把非终结符和属性关联起来:
  • 构造一个predictive parser, 忽略产生式中的动作。
  • 把translation scheme中的动作复制到解析器。如果动作出现在产生式p中的文法符号X之后,那么他就被复制到实现X的代码之后。如果它出现在产生式的开始,那么他就被复制到实现该产生式的代码之前。

我们将在下一节构造这样一个翻译器。

Left Recursion

一个递归下降解析器可能无穷循环。例如
exprexpr + term

其中右边最左边的符号和产生式左边的非终结符一样。假设expr的过程决定应用此产生式。右边以expr开始,因此对expr将被递归调用。于是解析器就陷入了无穷循环。注意,lookahead符号仅在右边的一个终结符被匹配的时候才会发生变化。因此产生式以非终结符expr开始,那么就不会对输入发生任何改变,导致无穷循环。

一个左递归产生式可以通过重写产生式排除。考虑一个非终结符A具有两个产生式:
A → Aα | β
其中α和β是不以A开始的终结符和非终结符。例如,表达式:
expr expr + term | term
中,A = expr, α = + term, β = term.

非终结符A是left recursive左递归的,因为产生式A → Aα的右边还是以A开始。重复此产生式在A的右边构造起一序列的α,例如图2.18(a)。当A最终被β替换的时候,我们有一个β后面跟着一系列的0或者多个α.

相同的效果,可以通过Fig 2.18(1)实现,通过用下面的方式重写A的产生式:
A → βR
R → αR | ε (2.11)
这里R是一个新的非终结符。



产生式R → αR 是右递归的,因为R的产生式右边,R是最后一个符号。右递归的产生式导致树朝右下方增长,如Fig. 2.18(b)所示。朝右下方增长的数使得它难以翻译包含左结合操作符的表达式,例如减。但是,在下一节中,我们可以看到表达式正确翻译成后缀记法,如果在一个右递归文法上仔细定义translation scheme。
在第4章中,我们会讨论左递归更加通用的形式,显示可以从一种文法中排除所有的左递归。

星期一, 九月 25, 2006

2.3 SYNTAX-DIRECTED TRANSLATION

为了翻译一个编程语言构造,除了为该构造生成的代码外,一个编译器需要跟踪许多量.例如,编译器需要知道该构造的类型,或者目标代码中第一条指令的位置,或者是生成的指令数目。我们因此谈论和该构造相关的属性attributes。一个属性可以代表任何量,例如类型、一个字符串、一个内存地址,等等。

在本节中,我们给出一种形式叫做语法导向的定义,用来指定对编程语言构造的翻译。一个语法导向的定义一种构造的翻译,用它语法成员的属性来定义。在后面的各章,语法导向的定义用来指定许多翻译,发生在一个编译器的前端。

我们也将介绍一种更加过程型的记法,叫做translation scheme,用来指定翻译。本章,我们使用translation scheme把中缀表达式翻译为后缀记法。更加的细节和它们的实现在第5章。

Postfix Notation

一个表达式E的postfix notation 后缀记法可以归纳定义如下:

  1. 如果E是一个变量或常量,那么E的后缀记法就是E自己
  2. 如果E是一个具有E1 op E2 形式的表达式,op是任何二元操作符,那么E的后缀记法是E1' E2' op,其中E1'和E2'分别是E1和E2的后缀记法
  3. 如果E是一个具有(E1)形式的表达式,那么E1的后缀记法就是E的后缀记法

后缀记法中不需要括号,因为操作符的位置和arity(参数的数目)只允许一种后缀表达式编码方式。例如(9-5)+2的后缀记法是95-2+而9-(5+2)的后缀记法是952+-。


Syntax-Directed Definitions

一个syntax-directed definition 使用一个上下文无关文法来指定输入的语法结构。对每一个文法符号,它关联一系列属性,以及对于每一个产生式,都有一套语义规则semantic rules来计算出现在该产生式中的符号的属性值。文法和语义规则集合组成语法导向定义。

一次翻译就是一次输入输出映射。每一个输入x的输出用下列方式指定。
首先,构造x的一棵解析树。假设解析树中一个节点n用文法符号X作为标签。我们写成X.a来表示该节点上X属性a的值。在n上X.a值的计算是通过在节点n使用的X-产生式相关的属性a的语义规则得到的。一棵解析树,每一个节点都显示属性值,我们称之为annotated parse tree.


Synthesized Attributes

如果一个节点的某一属性值是由该节点的子节点的属性值决定的,我们称之为synthesized合成的。合成属性具有一个很好的特性,就是它们能够通过一次自底向上的解析树遍历计算得到。本章只使用合成属性;“继承”属性在chap 5介绍。

Example 2.6 翻译一个由数字组成并由加减号分隔的表达式的后缀表达式的一个语法导向的定义如Fig. 2.5 所示。和每个非终结符相关是一个字符值属性t,它代表在一棵解析树中该非终结符生成的表达式的后缀记法。







PRODUCTION SEMANTIC RULE

expr → expr1 + term expr.t := expr1.t || term.t || '+'

expr → expr1 - term expr.t := expr1.t || term.t || '-'

expr → term expr.t = term.t

term → 0 term.t := '0'

term → 1 term.t := '1'

.... ...

term → 9 term.t = '9'

Fig 2.5 syntax-directed definition for infix to postfix translation

一个数字的后缀表达式是它自己,例如,和产生式term→ 9相关的语义规则定义term.t 是9,无论何时这个产生式用在一个解析树中的节点。当产生式expr → term被应用的时候,term.t的值变成expr.t的值。

产生式expr → expr1 + term 派生一个表达式包含一个加号操作符(expr1中的下标区分右边和左边两个不同的expr实例).加法操作符左边的操作数由expr1和右边的操作数term给出。和这个产生式相关连的语义规则:

expr.t := expr1.t || term.t || '+'

连接epr1.t和term.t的后缀形式,再加上加号,这样定义了属性expr.t的值。语义规则中的||操作符代表字符串连接。

Fig 2.6包含了一个annoted解释树对应Fig 2.2的树。


Example 2.7 假设一个机器人可能被指令从它的当前位置向东、南、西、北移动一步。这样的一系列指令可以用下面的文法产生:

seq → seq instr | begin
instr → east | morth | west | south

输入序列

begin west south east east east north north

发生的机器人位置改变如Fig 2.7


在图中,一个位置是通过一对(x,y)标记的,其中x和y 代表从开始位置往东和北的步数

让我们构造一个语法导向的定义把指令序列翻译为一个机器人的位置。我们应该使用两个属性seq.x和seq.y,跟踪由非终结符seq生成的指令序列得到的结果位置。最初seq生成begin,seq.x和seq.y都是0,如所示在begin west south解析树最左边的内部节点。



位置的改变是由于从instr派生的单个指令找成的,我们给它们instr.dx和instr.dy属性。例如,如果instr派生west,那么instr.dx = -1,instr.dy = 0。假设一个序列是由一个序列seq加上一个新的指令instr组成的。那么robot的新位置是由下列规则给出的:

seq.x = seq1.x + instr.dx
seq.y = seq1.y + instr.dy

把指令序列翻译成机器人位置的语法导向的定义如Fig 2.9所示。


Depth-First Traversals

一个语法导向的定义并不对一棵解析树上属性的计算强加任何顺序;任何计算的顺序,只要它在计算a的属性前,计算好所有a依赖的属性,那么都是允许的。通常,在遍历解析树的时候,我们有时侯在某一个节点首次到达的时候必须计算某些属性,其它的则在它所有的子被访问以后,或者在访问节点的子节点之间的某个点。合适的计算顺序以后讨论。

本章的翻译都可以用固定的顺序、根据语义规则计算解析树的属性。一个树的遍历traversal从根节点开始,以某些顺序访问树中的每个节点。语义规则将通过Fig 2.10 定义的深度优先算法进行计算。

procedure visit(n:node)
begin
for each child m of n. from left to right do
visit(m);
evaluate semantic rules at node n
end

Fig 2.10 树的深度优先遍历

Fig2.11 演示这一过程,从root开始,按照从左到右的次序递归访问每一个节点的子



给定节点的语义规则在所有它的后代节点被访问才进行计算。之所以叫做深度优先是因为它尽可能首先访问一个节点的未被访问的子节点,因此它尝试访问可能的离根节点最远的节点。

Translation Schemes

本章剩下部分,我们使用一个过程型的规格定义一种翻译。一个translation scheme是一个上下文无关文法,其中叫做semantic action的程序片断嵌入到产生式的右边。一个translation scheme 就象一个语法导向定义,除了语义规则的计算顺序是显式展现出来的。

动作执行的点通过{}号表示,并且写在产生式右边的中间,例如:

rest → + term {print('+')} rest1

一个translation scheme 为每一个语句x生成输出,由底下的文法通过执行动作生成的,次序是它们在x解析树中的深度优先遍历顺序。例如,考虑一个解析树节点标为rest代表这个产生式。那么动作{print('+')}将在term子树被遍历但在rest1子树被访问前执行。

在为一个translation scheme画出一棵解析树时,我们把动作作为一个额外的子节点,通过一条虚线和它产生式的节点相连。例如,上述产生式的解析树部分和动作显示在Fig 2.12。一个语义动作节点没有子节点,所以访问到该节点的时候动作将被执行。



Emitting a Translation

本章中,translations schemes的语义动作将把一个翻译的输出写到一个文件中,每次一个字符或字符串。例如,我们把9-5+2翻译为95-2+,其中每个字符被打印一次,不使用任何存储用于子表达式的翻译。当输出用这种方式增量创建时,字符被打印的次序非常重要。

注意,到现在为止提到的语法导向的定义有下列重要的属性:代表每个生成式左边的非终结符的翻译的字符串是把所有右边非终结符翻译连接起来的结果,按照产生式中的相同顺序,在中间加入附加的字符串(可能没有)。具有这个属性的语法导向定义有一个术语叫做simple。例如,考虑下面的第一个产生式和语义规则,取自Fig2.5的语法导向定义:

PRODUCTION SEMANTIC RULE
expr → expr1 + term expr.t := expr.t || term.t || '+' (2.6)
这里expr.t的翻译是把expr1的翻译和term的翻译连接起来,跟上符号+.注意产生式的右边expr1出现在term的前面。


在term.t和rest1.t中间出现附加的字符串:
PRODUCTION SEMANTIC RULE
rest → + term rest1 rest.t := term.t || '+' || rest1.t (2.7)

但是,再次,非终结符term在右边出现在rest1的前面。

简单的语法导向定义可以用translation scheme来实现,其中action打印附加的字符串,以他们在定义中出现的顺序。下面产生式中的动作分别打印(2.6)和(2.7)中的附加字符串:

expr → expr1 + term {print('+')}
rest → + term {print('+')} rest1

Example 2.8. Figure 2.5包含一个简单的定义,把表达式翻译成后缀形式。Fig. 2.13 给出一个从该定义派生的translation scheme,以及一棵有动作的解析树9-5+2 在2.14:

expr → expr + term {print('+')}
expr → expr -term {print('-')}
expr → term
term → 0 {print('0')}
term → 1 {print('1')}
...
term → 9 {print('9')}

Fig. 2.13 Actions translating expressions into postfix notation.


注意虽然Fig 2.6 和2.14代表相同的输入输出映射,但是两种情况下翻译用不同的方式构造出来。

Fig 2.6把输出附加到解析树的根,而Fig 2.14增量的打印输出。

Fig 2.14的根代表Fig 2,13的第一个产生式。在一个深度优先遍历中,我们首先执行左边操作符expr子树的所有动作,当我们遍历根的最左子树。我们接着访问叶子+,没有动作。我们接着执行右边的操作数term的子树,最后,语义动作{print('+')}这个额外节点被执行。

因为term的产生式右边只有一个数字,该数字被打印。对产生式expr → term而言不需要任何输出,前两个产生式也只有操作符需要被打印。在执行解析树的深度优先遍历时,Fig. 2.14的动作打印95-2+.

作为一个通用规则,绝大多数解析方法以一种“贪婪”的方式处理它们的输入,从左到右,也就是说,它们尽可能多地构造一棵解析树的部分,在阅读下一个输入token前。在一个简单translation scheme(从一个简单语法导向定义派生而来)中,动作也是按照从左到右的次序完成的。因此,为了实现一个简单的translation scheme,我们可以在解析的同时执行语义动作,根本没有必要去构造解析树。

星期六, 九月 23, 2006

2.2 SNYTAX DEFINITION

我们介绍上下文无关文法context-free grammar这种记法

一种文法自然地描述许多编程语言构造的层次型结构。例如C的if-else语句

if (expression) statement else statement
这个语句连接了关键字if,一个打开的括号,一个表达式,一个关闭的括号,一条语句,关键字else,以及另外一条语句。(C里面没有关键字then)。

使用变量expre代表一个表达式,变量stmt代表一个语句,上面的规则可以表达为:

stmt → if (expr) stmt else stmt (2.1)

其中箭头可以读为"具有形式"。这样一条规则叫做一个production产生式。在一个产生式中,词法元素例如关键字if和括号叫做tokens。expr和stmt等代表token序列的变量叫做nonterminals。非终结符

一个context-free grammer 具有四个组成部分:
  • 一系列token,叫做终结符号terminal symbols
  • 一系列非终结符
  • 一系列产生式,其中每一个产生式由一个非终结符,叫做产生式的left side左边,一个箭头,以及一系列token和/或非终结符,叫做产生式的right side右边组成
  • 指定的某一个非终结符叫做start symbol开始符号

传统上,文法是通过列出它们的产生式来指定的,
  • start symbol列在第一个。
  • 我们假定数字、象<=这样的符号,以及粗体字符串如while是终结符。
  • 一个斜体的名字是一个非终结符
  • 任何非斜体的名字和符号可以假设为token。
为了记法方便,左边具有相同非终结符的产生式可以把它们的右边组合起来,通过|分隔可选的各个右边,我们读作"或者"



Example 2.1 本章的几个例子使用数字、加减号组成的表达式,例如9-5+2,3-1和7等等。因为一个加号或者减号必须出现在两个数字之间,我们把这样的表达式称为"由加减号分隔的数字列表"。下面的文法描述这些表达式的语法,这些产生式为:

listlist + digit (2.2)
listlist - digit (2.3)
listdigit (2.4)
digit → 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 (2.5)

按照我们的约定,文法的token是
+ - 0 1 2 3 4 5 6 7 8 9
而非终结符是斜体的listdigit,其中list是开始非终结符,因为它的产生式出现在第一条

我们说一个产生式是针对for一个非终结符的,如果这个非终结符出现在产生式的左边。一个token字符串是0个或多个token组成的一个序列。包含0个token的字符串,写作ε,叫做空字符串empty string.

一个文法派生字符串,从start符号symbol开始,用一个非终结符产生式的右边重复替换该非终结符。能够从start symbol派生出来的token字符串形成该文法定义的语言language

Example 2.2 由文法Example2.1 定义的语言由用加减号分隔的数字列表组成。

非终结符digit的10个产生式允许它代表任何一个token 0,1,.....,9。根据产生式(2.4),一个数字本身是一个列表。产生式(2.2)和(2.3)表示,我们可以取任意列表,后面加上一个加号或减号,再后跟一个数字,我们就可以得到一个新的列表。

因此,产生式(2.2)到(2.5)可以让我们定义感兴趣的语言。例如,我们可以推断9-5+2是一个list,按照:
  • a) 9是一个list(2.4),因为9是一个digit
  • b) 9-5是一个list(2.3),因为9是一个list,而5是一个digit
  • c)9-5+2是一个list(2.2),因为9-5是一个list,而2是一个digit
这个推理过程由Fig. 2.2 的树演示。树中的每一个节点都用一个文法符号作为标签。一个内部节点和他的子对应一个产生式;内部节点对应产生式的左边,子对应右边。这样的树叫做解析树,下面讨论





Example 2.3 有点不同类型的列表是Pascal begin-end 块中的用分号隔开的语句序列。这些列表可能是一个空的语句列表在begin和end之间。我们可能开始一个文法如下:

block → begin opt_stmts end
opt_stmt → stmt_list | ε
stmt_list → stmt_list; stmt | stmt

注意opt_stmts第2个可能的右边是ε,代表空字符串。也就是说,opt_stmts可以被一个空字符串替换,因此一个block可由两个token字符串begin end组成。注意stmt_list的产生式和Example 2.1中的list相似。只不过分号代替了算术操作符,而stmt代替了digit。我们没有展示stmt的产生式。我们马上就会讨论各种不同语句的产生式,例如if语句、赋值语句等等。



Parse Tree

一棵解析树能够非常形象化地显示文法的开始符号是如何派生语言的字符串的。如果非终结符A有一个产生式A → XYZ,那么解析树就具有一个内部节点A和三个子节点X,Y,Z,从左到右




正式讲,给定一个上下文无关文法,一棵解析树parse tree是具有下列属性的一棵树:
  1. 根标以开始符号
  2. 每一个叶节点标以一个token或者ε
  3. 每一个内部节点都标以非终结符
  4. 如果A是一个标以非终结符的内部节点,而且X1,X2,....Xn是节点所有子节点的标签(从左到右),那么A → X1X2...Xn是一个产生式。这里,X1,X2...Xn可以是一个终结符或者非终结符。特殊情况下,如果A → ε,那么标签为A的节点可能只有一个子标签为ε。

Example 2.4 在Fig2.2中,root标签是list,Example 2.1的开始节点。根节点的子从左到右分别标以list,+和digit,注意

list → list + digit
是Example 2.1文法的一个产生式。


一个解析树的叶子从左到右阅读形成tree的yield,它是一个从解析树的根节点的非终结符生成或派生的字符串。在Fig2.2,生成的字符串是9-5+2。在那个图中所有的叶子都显示在底层。以后,我们没有必要再这样排列叶子。任何树的叶子都分出一个自然的从左到右的顺序,因为如果a和b是同一个节点的两个子节点,并且a在b的左边,那么所有a的后代肯定在b后代的左边。

一种文法生成的语言的另一个定义是能够用某些解析树声称的字符串集合。根据一个给定的token字符串寻找一个解析树的过程叫做parsing解析这个字符串。


Ambiguity

我们必须小心谈论一个字符串的结构按照一个文法。虽然,每一棵解析树可以确定它派生的字符串,但是一种文法可以有多个解析树生成一个给定的token字符串。这样的文法成为ambiguous.为了显示一种文法是ambiguous的,我们只需要找出一个token字符串具有多棵解析树。这样的一个字符串通常具有多种含义,为了编译应用程序,我们必须设计无二义性的文法,或者用特殊的规则来解决文法中的二义性。

Example 2.5 假设我们不区分Example 2.1中的数字和列表,我们可以写作:

string → string + string | string - string | 0 | 1 | 2 | 3| 4| 5|6|7 |8 | 9

把digit和list合并成为非终结符string看起来是可行的,因为一个digit是list的一种特殊情况。

但是, Fig. 2.3显示象9-5+2这样的表达式现在有多棵解析树。9-5+2的两棵解析树对应表达式的两种括号形式(9-5)+2和9-(5+2)。而Example 2.1的文法不允许后面一种解释。




Associativity of Operators

按照习惯,9+5+2和(9+5)+2是等价的,而9-5-2和(9-5)-2是等价的。当一个操作数例如5左边和右边有操作符的时候,需要有约定来决定哪个操作符使用该操作数。
我们说操作符+ 结合到左边 associates to the left,因为如果一个操作符的左右两边都有加号的话,那么取左边那个操作符。在绝大多数编程语言中,四个算术操作符,加减乘除是左结合的。

某些常用的操作符例如指数是有右结合的。另外一个例子,C里面的赋值操作符=也是右结合的,在C里面表达式a=b=c和a-(b=c)是等价的。

象a=b=c这样具有右结合操作符是由下列文法生成的。

right → letter = right | letter
letter → a | b | ... | z

一个象-一样的左结合操作符和一个象=这样的右结合操作符的解析树之间的对比如图2.4:




注意9-5-2的解析树朝左下方增长,而a=b=c的解析树朝右下方增长

Precedence of Operators

考虑表达式9+5*2.有两种可能的解释方式:(9+5)*2和9+(5*2)。+和*的结合性不能解决这种模糊性。因此我们必须知道操作符的相对优先级(如果同时出现两个操作副的时候)

我们说*比+优先级更高,如果先取*后取+。在普通算术中,乘法和除法比加法和减法的优先级高。因此,5被*所取,不管是9+5*2还是9*5+2,也就是说,分别等价于9+(5*2)和((*5)+2

Syntax of expression 表达式语法。算术表达式的文法可以用一张表构造,其中显示操作符的结合性和优先级。我们从4个常见的算术操作符和一个优先级表开始,按递增的优先级显示,相同的优先级写在同一行

左结合: + -
左结合: * /




我们创建两个非终结符expr和term,分别对应这两个层次的优先级,以及一个额外的非终结符factor用来生成表达式的基本单位。表达式的基本单位是数字和括号的表达式:

factordigit | (expr)

现在考虑二元操作符,*和/,具有最高优先级。因为这些操作符是左结合的,因此类似于那些左结合的列表

term term * factor
| term / factor
| factor

类似的,expr生成加减

exprexpr + term
| expr - term
| term

结果文法依次为:
exprexpr + term | expr - term | term
termterm * factor | term / factor | factor
factordigit | (expr)

该文法把表达式看作+ 或 - 分隔的item ,而一个term则是* 或/分割的factor。注意任何括号的表达式也是一个factor,因此用括号我们可以开发具有任意深度的嵌套(也就是任意深度的树).

Syntax of statements 语句的语法 关键字允许我们识别绝大多数语言中的语句。任何Pascal语句开始一个关键字,除了赋值和过程调用。某些Pascal语句可以用下面的文法(具有两义性)来定义,其中token id代表一个标识符。

stmt → id := expr
| if expr then stmt
| if expr then stmt else stmt
| while expr do stmt
| begin opt_stmts end

非终结符opt_stmts在Example 2.3中定义。

星期五, 九月 22, 2006

2.1 OVERVIEW

描述一种语言看起来象什么(语言的语法)和它的程序意味着什么(语言的语义)可以定义一种编程语言.
  • 为了指定语法的语法,我们给出一种广泛使用的记法,叫做上下文无关文法或者BNF(Backus-Naur Form)。
  • 有了这种记法,描述一种语言的语义要比语法难得多了。为了指定一种语言的语义,我们应该使用非形式化的描述和建议的范例


除了指定语言的语法,一个上下文无关文法可以用来帮助指导程序的翻译。一种面向文法的编译技术,叫做语法导向的翻译syntax-directed translation,对组织一个编译器前端是非常有帮助的,并且在本章中大量使用。

在讨论语法导向翻译的过程中,我们应该构造一个编译器,把中缀表达式转换为后缀形式,一种记法-其中操作符出现在操作数的后面。例如,中缀表达式9-5+2的后缀表达形式是95-2+。后缀表达式可以直接翻译成一种计算机的代码,这种计算机使用一个stack执行它的所有计算。

我们从构造一个简单的程序开始,把数字组成、加减号分开的表达式转换成后缀形式。基本的想法清楚以后,我们扩展程序处理更加通用的编程语言构造。我们的每个翻译器都通过扩展前一个得到。

在我们的编译器中,词法分析器把输入的字符流变成token流,然后变成下一阶段的输入,如Fig.2.1 所示


图中,syntax-directed translator 是一个语法分析器和一个中间代码生成器的组合。

从数字组成的表达式开始的原因是,词法分析可以很简单。每一个输入字符形成一个token.

后面我们会扩展语言包含词法构造例如数字、标识符和关键字。我们会为之构造一个词法分析器,收集连续的输入字符成为合适的token。

Chapter 2 A Simple One-Pass Compiler

本章是对3-8章中材料的一个介绍。
展现一系列基本的编译技术,开发一个可以工作的C程序,把中缀表达式翻译为后缀形式

重点是编译器的前端-词法分析、解析和中间代码生成。

1.6 COMPILER-CONSTRUCTION TOOLS

两类工具:

  • 通用:编译器的编写者和任何程序员一样,当然可以从各种软件工具获益,例如debugger,version manager, profiles等等。
  • 专用:除了这些通用的软件开发工具之外,还有一些专用的工具,帮助实现一个编译器的不同阶段

就在第一个编译器完成后不久,帮助编写编译器的系统就出现了。这些系统通常称为编译器编译器compiler-compilers,编译器生成器compiler-generators或者翻译器编写系统 translator-writing systems。很大程度上,他们面向一种特定的语言模型,它们最适合生成和该模型类似的语言的编译器。
例如,或许假设所有语言的词法分析器基本上是相同的,除了特定的关键字和符号。
  • 许多编译器编译器实际上产生固定的词法分析例程,用于生成的编译器。
  • 这些例程只在识别的关键字列表上有所不同,而这个列表由用户提供。
    • 这种方法是有效的,但是如果需要识别非标准的token时就可能无法工作,例如某些标识符包含字母和数字之外的字符。


某些通用的工具用于特定编译器部件的自动化设计。这些工具使用专用语言指定并实现部件,很多使用非常复杂的算法。最成功的是那些隐藏了算法细节,同时产生可能轻易集成到编译器其它组成部分的那些工具。下面是一些有用的编译器构造工具:
  1. Parser generator 解析器生成器,这些产生语法分析器,通常从输入基于一个上下文无关文法。在早期的编译器中,被消费的语法分析不但一个编译器运行时间的大部分,而且是编写一个编译器所需智力活动的大部分。现在这个阶段被认为是最容易实现的阶段之一。许多解析器生成器利用了强大的解析算法,手工实现的话就太过复杂了。
  2. Scanner generators 扫描器生成器 自动生成词法分析器,通常来自一个规格,基于正则表达式。结果的词法分析器的基本组织是一个有穷自动机。
  3. Syntax-directed translation engines 语法导向翻译引擎 产生遍历解析树(如图1.4)的一套例程,生成中间代码。基本的思想是一个或多个“翻译”是和解析树的每一个节点相关的,而每一个翻译用它在树中相邻节点的翻译得以定义。
  4. Automatic code generate 自动代码生成器取一套规则,定义把中间语言的每个操作翻译成目标机器机器语言的方法。这些规则必须包含足够的细节,以便我们能够处理数据的不同存取方法,例如,变量可能在寄存器中,在一个内存的固定(静态)位置,可能在一个栈上分配的一个位置。
    基本的技术是“模版匹配”"template matching"。中间代码语句被代表机器指令序列的“模版”替换。因为通常有在哪里替换变量(例如在寄存器之1或者在内存中)有很多选项,因此有很多可能的方式把中间代码“铺”到一个给顶的模版集合,有必要选择一种好的铺法,而不会产生编译器运行时间的组合爆发。
  5. Data-flow engine 数据流引擎。许多代码优化都需要首先进行数据流分析,收集关于值是如何从一个程序的一部分传送到另一部分的信息。这种性质的不同任务可以被基本相同的例程执行,用户提供中间代码语句和被收集信息之间的关系。


1.5 THE GROUPING OF PHASES

1.3的讨论是丛一个编译器的逻辑方面来得。实现的时候,几个阶段的活动通常分组在一起。

Front and Back Ends

通常,阶段分为一个前端front end和一个后端back end

  • 前端包括哪些阶段或者阶段的部分,主要依赖于源程序,而和目标机器尽量无关。通常包括词法和语法分析,符号表的创建,语义分析以及中间代码的生成。一定数量的代码优化也可以在前端完成。前端当然也包括这些阶段相应的错误处理。
  • 后端包括编译器中那些依赖于目标机器的部分,一般来说,这些部分不依赖于源程序,而只依赖于中间语言。包括代码优化阶段,代码生成阶段以及相应的错误处理和符号表操作。如果后端被小心设计,那么可能不必要对后端做太多的重新设计。

分开的好处
  • 可以直接取一个编译器的前端,然后重做相关的后端,那么编译器就可以把同一种源语言编译到不同的机器。
  • 也有可能编译不同的语言到相同的中间语言,为不同的前端使用一个通用的后端,从而在同一台机器上获得不同的编译器。

Passes

编译的不同阶段通常实现为一个pass遍,一遍从输入文件读取代码并且写出到输出文件中。
而实践中,编译器的各个阶段如何被组织成遍有很大的差异,因此我们从阶段而不是遍的角度来讨论更加好一点。

12章会讨论一些代表性的编译器,以及它们如何把阶段组织成遍。

通常把几个阶段组织成一遍,而这些阶段的活动在遍中相互交叉。例如,词法分析、语法分析、语义分析和中间代码生成可能组织成一遍。如果这样做得话,词法分析后的token流可能被直接翻译成中间代码:
  • 更细一点,我们可以把语法分析器考虑为正被"in charge"。
    • 它试图发现所看到的token的文法结构,在需要的时候获取token,
    • 通过调用词法分析器来寻找下一个token.
    • 当它发现文法结构的时候,解析器调用中间代码生成器执行语义分析、并生成这部分代码。
Reducing the Number of Passes

遍数的两难:
  • 我们希望遍数相对较少,因为读取和写出中间文件需要时间。
  • 另一方面,我们把几个阶段组织成一遍,我们可能被强制要求在内存中保留整个程序,因为一个阶段需要的信息和前一个阶段直接产生的信息可能具有不同顺序。
    • 程序的中间形式可能会比整个源程序和目标程序大得多,因此空间不是一件简单的事情。
不同的情况:
  • 对某些阶段而言,把它们组织成一遍不会有太多问题。例如,词法分析器和语法分析器之间的接口通常只限于一个token。
  • 另一方面,如果中间表现形式还没有完全生成,那么就很难进行代码生成。
    • 例如,某些语言如PL/1和Algol 68允许在变量声明前使用它们。我们不能生成该构造的目标代码,如果我们不知道产生这个构造的相关变量类型。
    • 类似,绝大多数语言允许用goto在代码中向前跳跃。我们不能决定一个跳跃的目标地址,如果还没有看到介于中间的源代码和为它生成的中间代码
某些情况下有可能先为缺失的信息保留好空位,然后在信息可用的时候再去填充这些空位。
特别是,中间代码和目标代码生成通常能够被合并成一遍,使用一种叫做"backpatching"的技术。

虽然在第8章中间代码生成之前,我们不能解释所有的细节,但是我们还是能够用汇编器演示backpathcing的过程。

回忆一下,我们将过两遍汇编器,第一遍发现所有代表内存位置的标识符并且推断它们的地址。第2遍用地址替换标识符。

我们可以组合各遍的动作如下:在碰到一个是向前引用的汇编语句时:

GOTO target

我们生成一个骨架指令,GOTO的机器操作码,但是地址为空白。所有地址为空白target的指令都保存在一个列表中,和符号表中的target的条目关联。当我们最后遇到一个下面这样的指令:

target: MOV foobar, R1

的时候,可以确定target的值:就是当前指令的地址,并且可以把空白填上了。
于是我们进行"backpatch",通过遍历target对应的列表,所有的指令都需要它的地址,把地址中空白的位替换为当前的地址。这种方法比较容易实现,如果指令保存在内存中直至所有的目标地址都计算完毕。

这种方法对那些能够把所有的输出保存在内存中的汇编器还是不错的。因为对汇编器而言,代码的中间和最终表现形式大致相同,因此具有近似的长度,baclpatching 整个汇编程序的长度并不是不可行的。

但是,在一个编译器中,中间代码需要消耗大量的空间,我们可能需要小心backpatching发生的距离。

星期四, 九月 21, 2006

1.4 COUSINS OF THE COMPILER

我们在Fig. 1.3看到过,输入到一个编译器之前可能先要经过预处理器,而编译器的输出可能进一步处理才能变成真正可以运行的机器码。

Preprocessors

预处理器为编译器产生输入。可能执行:
  1. Macro processing。允许为长的构造定义短的宏
  2. File inclusion .把头文件包含到程序文本
  3. "Rational " precessors
  4. Language extensions.这些处理器试图向语言加入新的能力。例如Qquel是数据库查询语言嵌入到C。##开始的语句由预处理器处理,翻译为执行数据库存取的例程。
宏预处理器处理两种语句:
  • 宏定义-一般有某些唯一的字符或关键字指定,例如define或者macro,通常包含
      • 被定义的宏的名字
      • 一个body
    • 通常,宏预处理器允许在它们的定义中使用形式参数formal parameters,也就是将被值替换的符号(值在这个上下文中,是一个字符串)
  • 宏使用提供宏的名字和实际参数actual parameters,也就是形式参数的值。
    • 宏处理器用实际参数替换body中的形式参数
      • 转换后的body代替了宏本身

Example 1.2 TEX字处理系统包含一个通用的宏设施。宏定义的格式如下:

\define <macro name> <template> {<body>}


  • 此图中,我们假设一个WORD,包含4个字节,为每一个标识符分配,地址从0开始分配
  • 第2遍,汇编器再次检查输入。这一次,它把每一个操作代码翻译为该操作在机器语言中的bit序列,同时把每一个代表一个位置的标识符翻译为符号表中登记的该标识符的地址。
    • 第2遍的输出通常是relocatable可重定位的机器代码,意味着它可以从任何位置L开始加载:也就是说,L被加到代码中的所有地址,那么所有的引用都能够正确了。因此,汇编器的输出必须区分指令中引用到地址的部分
    • Example 1.3 下面是1.6翻译后假想的机器代码

      0001 01 00 00000000 *
      0011 01 10 00000010
      0010 01 00 00000100 * (1.7)

      我们预想一个很小的指令字:
      • 其中前面4位是指令代码,0001,0010和0011各自代表load,store和add。load和store意味着从内存移到寄存器或相反;
      • 接下去的两位指定一个寄存器,01指三条指令中的寄存器1.
      • 这之后的两位代表一个'tag'
        • 其中00代表普通地址模式,后8位指向内存地址。
        • tag 10代表"立即"immediate模式,最后8为取作操作数的字面意义。该模式出现在(1.7)的第2条指令中。
      • (1.7)的第1行和第3行有1个*,*代表可重定位的位relocation bit,和可重定位机器代码的每一个操作数相关。假设包含数据的地址空间被加载到起始位置L。那么*的存在表示L必须加到指令的地址上。因此,如果L = 00001111,也就是15,那么a和b分别在位置15和19。(1.7)的指令变为:

        0001 01 00 00001111
        0011 01 10 00000010
        0010 01 00 00010011 (1.8)

        这是绝对的,或者不可重定位的机器代码。注意(1.7)的第2条指令没有*,因此L也不会加到它的地址上,这当然是正确的,因为这些bit代表的是常量2,而不是位置2。

Loaders and Link-Editors

通常一个程序loader执行loading和link-editing两个功能。
  • 加载的过程包括取可重定位的机器代码,按照前面讨论的方法改变可重定位的地址,替换内存中适当位置被改变的指令和数据。
  • 连接编辑器允许我们用多个可重定位机器代码文件组成一个程序。这些文件可能是不同编译的结果,以及系统提供例程的一个或多个库文件。
如果这些文件按照通常的方式被使用,那么可能有一些外部引用external references,其中一个文件的代码引用另外一个文件中的位置。
  • 这种引用可能是在一个文件中定义的数据位置,在另外一个文件中使用
  • 或者可能是一个过程的入口点出现在一个文件的代码中,在另一个文件中对它进行调用

可重定位的机器代码文件必须为每一个被外部引用的数据位置或指令标签保留符号表中的信息。如果我们事先不知道什么可能被引用,那么我们必须包含整个汇编器符号表,作为机器代码的一部分。

例如,代码(1.7)可能产生
a 0
b 4
如果一个和(1.7)一起加载的文件引用(1.7)b,那么引用将被4代替,加上文件(1.7)被重定位时的数据位置偏移。

1.3 THE PHASES OF A COMPILER

概念上,一个编译器操作于阶段phases,每一个阶段把源程序从一种表现形式转换为另一种。一个编译器的典型分解如图Fig 1.9所示。实践中,某些阶段可能组合在一起,如1.5所提到的,分组的阶段之间的中间表现形式不需要明确构造出来。



开始3个阶段,形成一个编译器的分析部分,在上一节中已经介绍。另外两个活动,符号表管理和错误处理,显示出和6个阶段之间的交互-词法分析、语法分析、语义分析、中间代码生成、代码优化和代码生成。口头上,我们把symbol-table manager和错误处理也叫做"阶段".

Symbol-Table Management

一个编译器的基本功能之一是记录源程序中使用的标识符,收集和每个标识符相关的各种属性信息。这些属性可能提供一个标识符的存储分配信息、它的类型、范围(在程序的哪些地方有效),以及,标识符是过程名字的情况下,象参数的数目和名字,每个参数传递的方法(例如通过引用)和返回的类型等等。

一个符号表symbol table 是记录每个标识符以及相应属性字段的数据结构。该数据结构允许我们快速寻找每一个标识符的记录,快速存储和获取该该记录中的数据。符号表在第2章和第7章讨论。

当一个源程序中的标识符被词法分析器检测到的时候,该标识符进入符号表。但是,标识符的属性通常无法在词法分析时决定。例如,一个Pascal 声明:

var position , initial , rate :real;

词法阶段看到position,initial和rate的时候,real类型还不知道。

剩下的阶段在符号表中加入标识符相关的信息,并且以各种方式使用这些信息。例如,在语义分析和中间代码生成阶段,我们需要知道标识符的类型,所以我们检查源程序有效使用他们的方式,从而我们能够生成相关的正确操作。代码生成器通常输入并使用标识符存储分配相关的详细信息。

Error Detection and Reporting

每一个阶段都可能碰到错误。
  • 但是,检测到一个错误以后,一个阶段必须处理错误,以便编译能够进行下去,允许检测到源程序中更多的错误。
    • 一碰到错误就停止的编译器没有能够提供它应该可以提供的帮助。


通常语法和语义分析阶段处理编译器能够检测到的大部分错误。词法阶段可以检测到错误其中输入中剩余的字符不能形成语言的任何token。那些token流违反语言的结构规则(语法)的错误由语法分析阶段判断。而在语义分析阶段,编译器试图检测具有正确的语法结构但是没有任何意义的操作,例如,如果我们试图把两个标识符相加,其中一个是数组的名字,而另外一个是过程的名字。

The Analysis Phases

随着翻译的进展,编译器对于源程序的内部表现形式也随之发生变化。我们通过考虑下面语句的翻译来演示这些表现形式:

position := initial + rate * 60 (1.1)

Figure 1.10 显示了该语句在每一阶段之后的表现形式:


词法阶段读取源程序中的字符,把它们分组为token流,每个token代表一个逻辑内聚的字符序列,例如一个标识符,一个关键字(if, while等等),一个标点字符,或者一个多字符操作符如:=。字符序列形成一个token成为token的lexeme.

某些token具有"词法值"。例如,发现rate这个标识符时,词法分析器不但需要生成一个toke,id,还需要把lexeme rate进入到符号表,如果还没有被加入的话。和这一次id出现相关的值指向符号表的rate条目。

这一节中,我们使用id1,id2,id3分别代表postion, initial和rate,重点突出一个标识符的内部表现形式和形成标识符的字符序列是不同的。因此,(1.1)词法分析以后的表现形式是:

id1 := id2 + id3 * 60 (1.2)


当然:=和60也应该有token,但目前先放一放

第2和第3阶段,语法和语义分析,已经介绍过。语法分析在token流之上构造一个层次结构,我们可以用Fig 1.11(a)描述。Fig 1.11(b)显示了实现这种树的一个常用的数据结构,
  • 其中一个内部节点是一个纪录,具有一个操作符相关的字段,以及两个指针字段指向左和右子节点。
  • 一个叶节点具有两个或多个字段,一个确定在叶子上的token,其它的记录该token的相关信息。
关于语言构造的附加信息可以在节点纪录中增加更多的字段来保存。




Intermediate Code Generation

语法和语义分析以后,某些编译器生成源程序的显式中间表现形式。我们可以把这种中间表达形式看作一个抽象机器的程序。这种中间表达形式应该具有两个重要属性:
  • 容易产生
  • 容易翻译为目标程序

可能具有各种形式,第8章有一种中间形式叫做"三地址代码" three-address code,类似于一种机器的汇编语言,其中每一个内存地址都可以当作一个寄存器。

三地址代码由一个指令序列组成,每一条最多可以有三个操作数。(1.1)源程序的三地址形式可能如下:

temp1 := inttoreal(60)
temp2 := id3 * temp1
temp3 := id2 + temp2
id1 := temp3 (1.3)

这种中间形式可以有几个属性。
  • 首先,每条三地址指令除了赋值外最多有一个操作符,因此,在生成这些指令的时候,编译器必须决定操作的顺序,源程序(1.1)中,乘法比加法优先
  • 其次,编译器必须生成临时的名字保存每一条指令计算得到的值。
  • 第三,某些“三地址”指令可能少于三个操作数,例如(1.3)的第一条和最后一条指令
Code Optimization

代码优化阶段试图增强中间代码,以便快速运行将被产生的机器代码。
某些优化很简单,例如一个自然的算法产生中间代码(1.3),语义分析后树表达形式中每一个操作符都需要使用一条指令,即使有更好的方法执行相同的计算。

使用两条指令

temp1 := id3 * 60.0
id1 = id2 + temp1 (1.4)

上面这种简单算法并没有问题,因为代码优化阶段能够修正。
  • 也就是说,编译器能够推断把60从整数转换为实数表现形式可以一次完全,并且可以在编译时刻完成,所以inttoreal操作可以消除。
  • 另外,temp3只使用一次,目的是把值传输给id1,因此用id1替换temp3是安全的,因此(1.3)最后一条语句是不需要的,结果产生1.4代码。

不同的编译器在执行代码优化的数量上有很大的差异。

  • 做得最多的那种,叫做“优化编译器”"optimizing compilers",编译的大部分时间都花在这个阶段。
  • 但是,某些简单的优化可以大大改善目标程序的运行时间而不会太降低编译的速度。

Code Generation

编译器的最后阶段是目标代码的生成,一般由可重定位的机器代码或汇编代码组成。程序使用的每一个变量都会选择好内存地址。然后,中间指令每一条被翻译成执行相同任务的一系列机器指令。一个关键的方面是把变量和寄存器之间的赋值。

例如,使用寄存器1和2,代码(1.4)的翻译可能如下:

MOVF id3, R2
MULF #60.0 R2
MOVF id2, R!
ADDF R2, R1
MOVF R1, id1 (1.5)

每一条指令的第一个和第二个操作数指令相应的源和目标。每条指令中的F告诉我们指令处理浮点数字。
  • 代码把地址id3的内容移到寄存器2,
  • 然后把它和实数常量60相乘。#表示60.0是一个常量。
  • 第3条指令把id2移到寄存器1,
  • 然后把它和前面计算后放在寄存器2中的值相加。
  • 最后,寄存器1的值移到id1的地址,因此代码实现Fig. 1.10中的赋值。

星期三, 九月 20, 2006

1.2 ANALYSIS OF THE SOURCE PROGRAM

本节介绍分析、展示在某些文本格式化语言中的应用。

在编译中,分析包括3个阶段:
  1. Linear analysis: 组成源程序的字符流从左到右读进来,分组为tokens,具有联合意义的字符串序列
  2. Hierarchical analysis 字符或者token被层次型组织为具有联合意义的嵌套集合
  3. Semantic analysis 执行一定的检查,确保程序的组成部分能够有意义地配合在一起
Lexical Analysis

在一个编译器中,线性分析叫做lexical analysis 或者 scanning。例如,在词法分析中,赋值语句中的字符

position := initial + rate * 60

会分组为下列token

  1. 标识符position
  2. 赋值符号 :=
  3. 标识符intial
  4. 加号+
  5. 标识符rate
  6. 乘号*
  7. 数字60

用来分隔这些token的字符通常在词法分析阶段就被排除了

Syntax Analysis

层次型的分析叫做parsing或者syntax analysis。把源程序的token分组为文法词组,编译器可以用来合成输出。通常源程序的文法词组用一棵解析树代表,如下图Fig 1.4


在表达式 initial + rate * 60中,词组rate * 60是一个逻辑单元,因为通常的算术表达式习惯告诉我们乘法应该在加法前计算。因为表达式initial+rate后跟一个* ,所以它们不能组成一个词组。

程序的层次型结构通常用递归规则来表达。例如,我们可以用下面的规则来定义上面的表达式

  1. 任何identifier都是一个expression
  2. 任何number都是一个expression
  3. 如果expression1expression2都是表达式,那么下面都是表达式
    • expression1 + expression2
    • expression1 * expression2
    • ( expression1 )
规则(1)和(2) 是(非递归的)的基本规则,而(3)用对其它表达式进行的操作定义表达式。因此,通过规则(1),initial 和 rate是表达式。通过规则(2), 60是表达式,根据规则(3),我们可以首先推理rate*60是表达式,最终initial+rate*60是一个表达式。

类似地,许多语言用规则递归定义语句如下:
  1. 如果identifier1是一个标识符,而expression2是一个表达式,那么
    identifier1 := expression2
    是一个语句
  2. 如果expression1是一个表达式,而statement2是一个语句,那么
    while ( expression1 ) do statement2
    if ( expression1 ) then statement2
    都是语句
词法和语法分析之间的划分有些随意。我们通常选择能够简化整体分析任务的划分方式。决定这种划分的因素因素之一是一种源语言构造是不是具有内在的递归性。词法分析不需要递归,而语法构造需要。上下文无关文法是递归规则的一种形式化,可以用来指导语法分析。
  • 例如,识别标识符不需要递归,通常它们是字符串,以一个字母开始,后跟字母和数字。我们通过对输入流的简单扫描就能够识别标识符。一直等到一个既不是字母也不是数字的字符,然后把到这一点位置所有已经发现的字母和数字组合起来成为一个token。组合起来的字符记录在一个表格中,叫做symbol table,从输入中移去,然后开始处理下一个token。
  • 另一方面,这种线性扫描在分析表达式或语句的时候不够强大。例如,如果我们不把输入放到某中层次型或嵌套结构中,那么我们就不能正确匹配表达式中的括号,或者匹配语句中的begin和end。
Fig 1.4的解析树parse tree描述了输入的语法结构。该语法结构一种更加通用的内部表现形式是Fig1.5(a)给出的语法树syntax tree。一棵语法树是解析树的压缩表现形式,其中操作符作为中间节点,而一个操作符的操作数成为操作符节点的子节点。构造该树的过程在5.2节讨论。


第2章和第5章详细讨论,syntax-directed translation语法导向翻译,其中编译器使用输入的层次型结构帮助生成输出

Semantic Analysis

语义分析阶段检查源程序是否有语义错误,并且为后继代码生成阶段收集类型信息。它使用语法分析阶段得到的层次型结构确定表达式和语句操作符和操作数

语义分析的一个重要组成部分是类型检查。这里编译器检查每一个操作符,看看它们的操作数类型是否为源语言规格所允许。例如,许多编程语言定义需要一个编译器每次发现把一个real作为数组下标的时候报告错误。但是语言的规格可能允许某些操作数转换,例如,当一个二元算术操作符对一个整数和一个实数操作的时候。此时,编译器可能需要把integer转换成real。

例1在一种机器内,表达一个整数的位模式bit pattern一般来说和表达一个实数的位模式是不同的,就算整数和实数的值一样也是如此。现在假设,所有Fig1.5中的标识符都声明为real,而60自己是一个integer。Fig1.5(a)的类型检查发现*应用于一个实数rate,和一个整数60。一般的做法是把integer转换成real.Fig1.5(b)插入了一个额外的节点,操作符为inttoreal,显式地把整数转换为一个实数。也可以选择两外一种做法,因为inttoreal的操作数是一个常量,编译器可以直接把整数常量替换成相等的实数常量。

Analysis in Text Formatters

可以把一个文本格式化器的输入看作指定boxes的一个层次,box都是矩形区域,被某些位模式所填充,代表浅色和深色像素,然后由输出设备打印。

例如Tex系统用这种方法显示它的输入。每一个不是命令组成部分的字符代表一个包含该字符bit pattern的box,辅以适当的字体和大小。不被“空白”分开的字符组成单词,由一系列横向放置的box组成,Fig1.6图解这一结构。组成单词(或者命令)的字符是文本格式化器分析的线性或者词法方面。

TEX中的Box可能是通过任意横向和纵向组合更小的box生成的。例如:

\hbox{ }

通过横向并列把一个box列表组合起来,而\vbox则实现类似的纵向组合。因此,如果我们在TEX中说:

\hbox{\vbox{ ! 1} \vbox{@ 2}}

那么我们就得到Fig1.7中的box排列。

决定输入所隐含的box的层次安排是TEX中的语法分析部分。

另外一个例子,用于数学的EQN预处理器,或者TEX中的数学处理器,从类似sub下标和sup上标这样的操作符来构造数学表达式。如果EQN碰到下面这种形式的输入文本

BOX sub box

那么它缩小box的大小,把它放到一个BOX的右下角,如Fig 1.8所示。sup类似,只不过是放在右上角。

这些操作符可以递归应用,例如EQN文本

a sub {i sup 2}
产生。把操作符sub和sup分组为token是EQN文本的词法分析部分,但是文本的语法结构需要判断box的大小和放置。

1.1 Compilers

编译器是一个程序,把一个用一种语言(source language)编写的程序翻译成等价的另一种语言的程序(target language)(figure 1.1)。

重要部分之一:向用户报告源代码中的错误



一眼看,编译器的种类太多了
  • 源语言,从传统的Fortran,pascal到几乎每个领域的专用语言
  • 目标语言-可能是另外一种编程语言,也可能是机器语言,从microprocessor到supercomputer

分类:
single-pass,multi-pass,load-and-go,debugging,optimizing等等

虽然看起来很复杂,但是任何编译器执行的基本任务几乎是一样的。理解了这些任务,我们就可以用相同的基本技术构造适用于各种源语言和目标机器的编译器

1950以来,关于组织和编写编译器的知识大大提高了。


The Analysis-Synthesis Model of Compilation

编译有两部分:
分析和合成

  • 分析部分把源代码打碎成为连续的片段,创建了源程序的一个中间表现形式
  • 合成部分从中间表现形式构造出需要的目标程序

此两者中,合成需要更多的专用技术

分析阶段,源程序隐含的操作被判断并纪录在一个层次型的结构-tree中。一种特定类型的树,语法树.syntax tree-每一个节点代表一个操作,节点的子代表操作的参数,例如下面是一个赋值语句的语法树


许多操纵源程序的软件工具首先执行某种类型的分析,例子包括:
  1. structure editor: 取一个命令输入流,构造一个源程序。结构编辑器不但执行文本创建、修改一个普通文本编辑器的功能,同时它也分析程序文本,在源程序上加入一个适当的层次型结构。因此,结构编辑器能够执行对程序准备有用的额外任务。例如,它可以检查输入是否被正确格式化,可以自动提供关键字(例如,当用户输入while的时候,编辑器提供匹配的do,并且提醒用户他们之间必须有一个条件),可以从一个begin或左括号跳到匹配的end或者右括号。另外,这样一种编辑器的输出通常和一个编译器的分析阶段输出很象
  2. Pretty printers。分析程序,打印它,使得程序的结构清晰可见。
  3. Static checkers。读取一个程序,分析它,试图在不运行的情况下发现潜在的bug。分析部分往往和优化编译器采用的技术很相近。例如,一个静态检查器可以检测源代码中永远不会被执行的部分,或者某个变量可能在定义前被使用。另外,通过采用类型检查技术,它可以捕获诸如把一个real变量当作一个指针使用这样的逻辑错误。
  4. Interpreter。不产生目标程序,执行源程序隐含的操作。例如对一个赋值语句,一个解释器可能构造类似figure 1.2这样的一棵树,然后通过遍历这棵树执行该操作:
    • 在树的root,它发现需要执行一个赋值
      • 因此调用一个进程去计算右边的表达式
      • 然后把结果值放到一个可以和position这个标识符关联的地方
    • 在root的右子,进程会发现需要计算两个表达式的和
      • 它递归调用自己计算表达式rate+60的值
      • 然后把这个值和变量initial的值相加
传统上,我们把编译器考虑为一个把一种源语言翻译为汇编或者机器语言的程序。但是,很多看起来无关的地方也大量使用编译器技术。下面例子的分析部分非常类似一个传统的编译器:
  1. Text formatter: 一个文本格式器取一个字符流作为输入,大部分是文字,但某些是命令,指定段落、图表或者数学公式
  2. Silicon compiler 语言的变量代表的并非内存位置,而是电路板上的逻辑信号或信号
  3. Query interpreter,SQL

The Context of a Compiler

除了编译器外,可能还需要其它程序才能产生一个可执行的目标程序。源程序可能分成不同模块存放在不同的文件中。

收集源程序的任何可能交给单独的程序,叫做preprocessor。预处理器可能扩展缩写、宏到源程序语句中。

Figure 3.1是一个典型的“编译”。编译器创建的目标程序在运行前需要更多处理。图中的编译器创建汇编代码,被一个汇编器翻译为机器代码,和其它的库进程连接成为可以实际在机器上run的代码。


Chapter 1 Introduction to Compiling

编译器编写的原则和技术非常普遍,所以在很多时候多要用到。编译器编写横跨程序语言、机器体系结构、语言理论、算法和软件工程。

幸运的是,一些基本的技术可以适用很大的范围

本章的作用是

  • 描述编译器的组成部件
  • 编译器工作的环境
  • 方便构造编译器的一些工具