编译器设计-自下而上分析器-误差恢复-语义分析
编译器设计-自下而上分析器-误差恢复-语义分析
Compiler Design - Bottom-Up Parser
Compiler Design - Error Recovery
Compiler Design - Semantic Analysis
一.Compiler Design - Bottom-Up Parser
自底向上的解析从树的叶节点开始,向上工作直到到达根节点。在这里,我们从一个句子开始,然后以相反的方式应用产生式规则,以达到开始符号。下面给出的图像描述了可用的自下而上的解析器。
移位减少分析
Shift-reduce解析使用两个独特的步骤进行自底向上的解析。这些步骤称为移位步骤和减少步骤。
移位步骤:移位步骤是指输入指针向前移动到下一个输入符号,称为移位符号。这个符号被推到堆栈上。移位的符号被视为解析树的单个节点。
Reduce step:当解析器找到一个完整的语法规则(RHS)并将其替换为(LHS)时,称为reducestep。当堆栈顶部包含句柄时发生这种情况。为了减少,在堆栈上执行POP函数,该函数从句柄弹出,并用LHS非终端符号替换它。
LR分析器
LR解析器是一个非递归、移位减少、自底向上的解析器。它使用了大量的上下文无关语法,这使得它成为最有效的语法分析技术。L R解析器也称为LR(k)解析器,其中L表示从左到右扫描输入流;R表示反向构造最右边的派生,k表示要做出决策的先行符号的数量。
有三种广泛使用的算法可用于构造LR解析器:
- SLR(1) – Simple LR Parser:
- Works on smallest class of grammar
- Few number of states, hence very small table
- Simple and fast construction
- LR(1) – LR Parser:
- Works on complete set of LR(1) Grammar
- Generates large table and large number of states
- Slow construction
- LALR(1) – Look-Ahead LR Parser:
- Works on intermediate size of grammar
- Number of states are same as in SLR(1)
LR解析算法
这里我们描述一个LR解析器的骨架算法:
token = next_token()
repeat forever
s = top of stack
if action[s, token] = “shift si” then
PUSH token
PUSH si
token = next_token()
else if action[s, token] = “reduce A::= β“ then
POP 2 * |β| symbols
s = top of stack
PUSH A
PUSH goto[s,A]
else if action[s, token] = “accept” then
return
else
error()
LL vs. LR
二.Compiler Design - Error Recovery
解析器应该能够检测并报告程序中的任何错误。当遇到错误时,解析器应该能够处理它并继续解析其余的输入。大多数情况下,解析器需要检查错误,但在编译过程的各个阶段可能会遇到错误。一个程序在不同阶段可能有以下几种错误:
词法:某个标识符的名称键入不正确
语法:缺少分号或括号不平衡
语义:不兼容的值赋值
逻辑:代码不可访问,无限循环
可以在解析器中实现四种常见的错误恢复策略来处理代码中的错误。
紧急模式
当解析器在语句中的任何地方遇到错误时,它将忽略语句的其余部分,不处理从错误输入到分隔符(如分号)的输入。这是最简单的错误恢复方法,而且还可以防止解析器开发无限循环。
语句模式
当解析器遇到错误时,它会尝试采取纠正措施,以便语句的其余输入允许解析器提前解析。例如,插入一个缺少的分号,用分号替换逗号等等。解析器设计者在这里必须小心,因为一个错误的更正可能导致无限循环。
错误产品
编译器设计器知道代码中可能出现的一些常见错误。此外,设计人员可以创建要使用的扩充语法,作为在遇到错误时生成错误构造的产品。
整体校正
解析器将手边的程序视为一个整体,并试图找出程序的意图,并试图找出与之最接近的匹配项,这是无错误的。当错误的输入(语句)X被馈送时,它会为某个最接近的无错误语句Y创建一个解析树。这可能允许解析器对源代码进行最小的更改,但由于此策略的复杂性(时间和空间),它尚未在实践中实现。
抽象语法树
解析树表示不容易被编译器解析,因为它们包含的细节比实际需要的多。以下面的解析树为例:
如果仔细观察,我们会发现大多数叶节点都是父节点的单个子节点。在将信息传送到下一阶段之前,可以消除这些信息。通过隐藏额外信息,我们可以获得如下所示的树:
Abstract tree can be represented as:
ASTs是编译器中重要的数据结构,具有最少的不必要信息。ASTs比解析树更紧凑,编译器可以很容易地使用它。
三.Compiler Design - Semantic Analysis
在语法分析阶段,我们学习了解析器如何构造解析树。在那个阶段构造的纯解析树通常对编译器没有用处,因为它不携带任何关于如何计算树的信息。上下文无关语法的产生,决定了语言的规则,不适应如何解释它们。
For example
E → E + T
上面的CFG产品没有与之相关联的语义规则,它无法帮助理解产品。
语义学
语言的语义为其结构提供了意义,如标记和语法结构。语义有助于解释符号、它们的类型以及它们之间
CFG + semantic rules = Syntax Directed Definitions
For example:
int a = “value”;
不应在词汇和语法分析阶段出现错误,因为它在词汇和结构上都是正确的,但应在赋值类型不同时产生语义错误。这些规则由语言的语法设定,并在语义分析中进行评估。语义分析应完成以下任务:
范围分辨率
类型检查
数组绑定检查
语义错误
我们已经提到了一些语义分析器需要识别的语义错误:
类型不匹配
未声明的变量
保留标识符误用。
作用域中变量的多重声明。
访问范围外变量。
实际参数与形式参数不匹配。
属性语法
属性语法是上下文无关语法的一种特殊形式,它将一些附加信息(属性)附加到一个或多个非终端,以提供上下文敏感信息。每个属性都有定义良好的值域,如integer、float、character、string和expressions。
属性语法是为上下文无关语法提供语义的一种媒介,它可以帮助指定编程语言的语法和语义。属性语法(当被视为解析树时)可以在树的节点之间传递值或信息。
Example:
E → E + T { E.value = E.value + T.value }
CFG的右边部分包含语义规则,这些规则指定如何解释语法。这里,将非终端E和T的值相加,结果被复制到非终端E。
语义属性可以在解析时从其域中分配给其值,并在分配或条件时进行计算。根据属性获取值的方式,它们可以大致分为两类:合成属性和继承属性。
综合属性
这些属性从其子节点的属性值中获取值。为了说明这一点,假设以下产品:
S → ABC
如果S从它的子节点(A、B、C)中获取值,则它被称为合成属性,因为ABC的值被合成为S。
如前一个示例(E→E+T)所示,父节点E从其子节点获取其值。合成属性从不从其父节点或任何同级节点获取值。
继承的属性
与合成属性不同,继承属性可以从父属性和/或兄弟属性中获取值。在接下来的制作中,
S → ABC
A可以从S、B和C中获取值。B可以从S、A和C中获取值。同样,C可以从S、A和B中获取值。
扩展:根据语法规则将非终端扩展为终端时
归约:根据语法规则将一个终端归约为其对应的非终端。语法树从上到下和从左到右进行分析。每当进行约简时,我们都会应用相应的语义规则(动作)。
语义分析使用语法导向的翻译来执行上述任务。
语义分析器从其前一阶段(语法分析)接收AST(抽象语法树)。
语义分析器将属性信息附加到AST中,称为属性AST。
属性是两个元组值,<attribute name,attribute value>
For example:
int value = 5;
<type, “integer”>
<presentvalue, “5”>
对于每个产品,我们都附加一个语义规则。
S属性SDT
如果SDT只使用合成属性,则称为S属性SDT。这些属性使用S属性的SDTs进行评估,这些sdt在产品(右侧)之后编写语义操作。
如上所述,S属性的sdt中的属性在自下而上的解析中求值,因为父节点的值依赖于子节点的值。
L属性SDTs
这种形式的SDTs既使用合成属性,也使用继承属性,并且限制不从正确的兄弟节点获取值。
在L属性的SDTs中,非终端可以从其父节点、子节点和同级节点获取值。在下面的生产中
S → ABC
S可以取A、B和C(合成)的值。A只能从S获取值。B可以从S获取值,A可以从S、A和B获取值。任何非终端都不能从其右边的同级中获取值。
L属性SDT中的属性是通过深度优先和从左到右的解析方式计算的。
我们可以得出结论,如果一个定义是S属性的,那么它也是L属性的,因为L属性的定义包含S属性的定义。