文章目录
错误处理(Error Handling)
编译器的主要目标:
- 检测无效程序
- 编译有效程序
有很多种程序可能产生的错误,拿C语言举例:
错误类型 | 举例 | 检测器 |
---|---|---|
Lexical(词法错误) | …$… | Lexer(词法检测阶段) |
Syntax(语法错误) | …x*$… | Parser(解析器) |
Semantic(语义错误) | …int x; y = x(3);(类型不匹配) | Type checker(类型检查器) |
Correctness | Your favourite program | Tester/User |
最后一项是编译通过,可能需要其他用户或者测试者发现一些可以优化的问题。
错误处理的要求
- 精准清晰地报告错误
- 可以从错误中很快恢复
- 不会使编译有效代码的速度变慢
错误处理的类型
Panic Mode(恐慌模式)
这是最简单也是最流行的方法,当检测到错误时,解释器将抛弃令牌(token)知道有一个明确角色的出现,然后重新启动继续。
举例:
( 1 + + 2 ) + 3 (1++2)+3 (1++2)+3
恐慌模式做的补救工作:
- 跳过第二个 ➕ ➕ ➕,知道检测到合法的字符( 2 2 2),然后继续。
B
i
s
o
n
Bison
Bison使用终端关键字error
来描述如何在输入中跳过错误:
E − > i n t ∣ E + E ∣ ( E ) ∣ e r r o r i n t ∣ ( e r r o r ) E\ ->\ int\ |\ E\ +\ E\ |\ (\ E\ )\ |\ error\ int\ |\ (\ error\ ) E −> int ∣ E + E ∣ ( E ) ∣ error int ∣ ( error )
表示的是当前面三项不匹配时,匹配后面两项,抛弃所有输入直到下一个整数出现;或者当括号中出现错误时,将括号中的都抛弃。
Error Productions
指定了常见的程序错误。
缺点:
- 时编译器变得复杂。
上面两种错误处理的类型是现在编译器中使用的。
自动全局或局部错误修正(Automatic local or global correction)
思路:找到一个正确的附近的程序
- 尝试令牌插入或者删除(用到最小编辑距离算法)
- 在一定范围内进行详尽的搜索
缺点:
- 难以实现
- 减缓对正确程序的解析速度
- “附近”不一定是想要的程序结果
抽象语法树(Abstract Syntax Trees)
抽象语法树(AST)类似于解析树,但是忽略了一些细节。
举例:
- 语法:
E − > i n t ∣ ( E ) ∣ E + E E\ ->\ int\ |\ (\ E\ )\ |\ E\ +\ E E −> int ∣ ( E ) ∣ E + E
- 字符串:
5 + ( 2 + 3 ) 5\ +\ (\ 2\ +\ 3\ ) 5 + ( 2 + 3 )
- 词法分析之后(lexical analysis):
生成了一系列的令牌: i n t 5 ′ + ′ ′ ( ′ i n t 2 ′ + ′ i n t 3 ′ ) ′ int_5\ \ '+'\ \ '('\ int_2\ \ '+'\ int_3\ ')' int5 ′+′ ′(′ int2 ′+′ int3 ′)′
- 解析器过程生成了解析树:
解析树的特点:
- 追踪了解析器中的符号
- 显示了嵌套结构
- 但是有太多信息,比如括号和单孩子节点。
所以我们使用AST简化解析树:
特点:
- 也包含嵌套结构
- 只包含有实际意义的句法(更加易用、紧凑)
- 是编译器中的一个非常重要的数据结构
递归下降解析(Recursing Descent Parsing)
是一个自顶向下的算法。解析树是自上而下、从左向右生成的。
考虑如下语法:
E − > T ∣ T + E E\ ->\ T\ |\ T\ +\ E E −> T ∣ T + E
T − > i n t ∣ i n t ∗ T ∣ ( E ) T\ ->\ int\ |\ int\ *\ T\ |\ (\ E\ ) T −> int ∣ int ∗ T ∣ ( E )
令牌流:
( i n t 5 ) (\ int_5\ ) ( int5 )
该算法从最高层的非终端 E E E开始,依次尝试 E E E的所有规则,没有符合的就反向跟踪。
步骤:
图中的指针指向当前读入的字符,当算法遍历到字符串最后一个字符的后面位置时,表示解析成功。
限制
观察如下代码:
TOKEN包含 INT, OPEN, CLOSE, PLUS, TIMES
// 判断下一个字符串和当前tok是否匹配
bool term(TOKEN tok)
{
// 先判断是否相等,再自加1
return *next++ == tok; // next是全局变量,指向输入字符串下一个位置,无论是否匹配成功,next都自增1
}
// E规则的两个推导
bool E1() { return T1(); }
bool E2() { return T() && term(PLUS) && E(); }
// 尝试E规则的所有推导
bool E()
{
TOKEN *save = next;
return (next = save, E1()) || (next = save, E2());
}
bool T1(){ return term(INT); }
bool T2(){ return term(INT) && term(TIMES) && T(); }
bool T3(){ return term(OPEN) && E() && term(CLOSE); }
bool T()
{
TOKEN *save = next;
return (next = save, T1()) || (next = save. T2()) || (next = save. T3());
}
如果对于token输入为int * int
,那么程序先调用E()
,再调用E1()
,接着T()
,T1()
,得到int
,是匹配成功的,所以所有函数都返回True
,但是我的token串并没有遍历完全,所以编译器拒绝这个值。所以程序问题出在没有后退机制(即如果一个一个非终端X匹配成功,它将没有办法后退尝试另外一个匹配)。
因此,目前展示的递归下降解析算法不是通用的,但是对于非终端最多一个规则(Production)的语法是使用的。
左递归
左递归是递归下降算法的主要困难,在使用该算法之前必须先消除左递归现象。
- 举例:
对于下面语法和实现代码:
S − > S a S\ ->\ S\ a S −> S a
bool S1(){ return S() && term(a); }
bool S(){ return S1();
由此可以看出,当解析时,S()
会进入一个死循环。
- 另外一个例子:
语法: S − > S a ∣ β S\ ->\ S\ a\ |\ β S −> S a ∣ β
该语法解析出来的样式将会是βα+
(前面一个S,后面一个以上的β
)。左递归有一个非终端S,有
S
−
>
+
S
α
S\ ->^+\ Sα
S −>+ Sα。递归下降算法的解析想要先看到第一部分匹配出来,而这种情况下是无穷匹配后面的项,最前面的S最后匹配,所以对这种情况不适用。
- 解决方法:
可以改写成右递归。上述的例子可以改写为:
S − > β S ′ S\ ->\ βS' S −> βS′
S ′ − > α S ′ ∣ ε S'\ ->\ αS'\ |\ ε S′ −> αS′ ∣ ε
- 推广到一般情况:
有如下的左递归的规则:
S − > S α 1 ∣ . . . ∣ S α n ∣ β 1 ∣ . . . ∣ β m S\ ->\ Sα_1\ |\ ...\ |\ Sα_n\ |\ β_1\ |\ ...\ |\ β_m S −> Sα1 ∣ ... ∣ Sαn ∣ β1 ∣ ... ∣ βm
都可以改写成如下对应的右递归:
S − > β 1 S ′ ∣ . . . ∣ β m S ′ S\ ->\ β_1S'\ |\ ...\ |\ β_mS' S −> β1S′ ∣ ... ∣ βmS′
S ′ − > α 1 S ′ ∣ . . . ∣ α n S ′ ∣ ε S'\ ->\ α_1S'\ |\ ...\ |\ α_nS'\ |\ ε S′ −> α1S′ ∣ ... ∣ αnS′ ∣ ε
《编译原理》龙书