编译原理
编译原理课程的第三章,介绍了语法分析的基本概念、上下文无关文法以及LR(1)分析方法。语法分析器根据上下文无关文法构建解析树,检查程序的语法正确性。课程还介绍了LR(0)、SLR、LR(1)和LALR(1)等不同类型的LR分析器,以及如何处理歧义文法和错误恢复。最后,介绍了Yacc工具,用于自动生成语法分析器。
例如表达式:
1 + 2 * 3经过 Lexer:
num(1) plus num(2) times num(3)经过 Parser:

(), (()), ((()))
((1+2)+3)一个 CFG 由以下部分组成:
| 组成 | 含义 |
|---|---|
| T | terminals(终结符) |
| N | non-terminals(非终结符) |
| S | start symbol(开始符号) |
| Productions | 产生式 / 规则 |
产生式一般写作:
其中:
S -> (S)
S -> ε它描述的语言是:
{ ε, (), (() ), ((())), ... }从开始符号 S 出发:
S例如:
S -> (S) -> ((S)) -> (())若文法 G 的开始符号是 S,则:
其中 表示经过零步或多步推导。
如果一个文法能够对某个字符串生成 两棵不同的 parse tree,则它是 ambiguous grammar。
等价地说:
E -> E * E
E -> E / E
E -> E + E
E -> E - E
E -> id
E -> num
E -> (E)字符串:
id * id + id可以解释为:
(id * id) + idid * (id + id)所以文法是歧义的。
2 * 3 + 4不同 parse tree 会给出不同结果:
(2*3)+4 = 102*(3+4) = 14所以 ambiguous grammar 会让程序含义不明确。
我们通常希望:
* / / 的优先级高于 + / -把歧义文法改写为:
E -> E + T
E -> E - T
E -> T
T -> T * F
T -> T / F
T -> F
F -> id
F -> num
F -> (E)其中:
E = expressionT = termF = factorT,F)推导E -> E + T)为了明确“完整句子已经结束”,通常引入:
$: end of file (EOF)做法:
S'S' -> S $这样 parser 就能判断:
S三类常见 parser:
| 类型 | 特点 |
|---|---|
| Universal | 能处理任意 grammar,但太慢 |
| Top-Down | 从根到叶构造 parse tree |
| Bottom-Up | 从叶到根构造 parse tree |
例如,对于字符输入 begin print num = num end 来说,可以画出语法分析树:

例如文法:
S -> if E then S else S
S -> begin S L
S -> print E
L -> end
L -> ; S L
E -> num = num可以写出:
void S(void) {
switch(tok) {
case IF: eat(IF); E(); eat(THEN); S(); eat(ELSE); S(); break;
case BEGIN: eat(BEGIN); S(); L(); break;
case PRINT: eat(PRINT); E(); break;
default: error();
}
}enum token {IF, THEN, ELSE, BEGIN, END, PRINT, SEMI, NUM, EQ};
extern enum token getToken(void);
enum token tok;
void advance() { tok = getToken(); }
void eat(enum token t) {
if (tok == t) advance();
else error();
}
void S(void) {
switch(tok) {
case IF: eat(IF); E(); eat(THEN); S(); eat(ELSE); S(); break;
case BEGIN: eat(BEGIN); S(); L(); break;
case PRINT: eat(PRINT); E(); break;
default: error(); }}
void L(void) {
switch(tok) {
case END: eat(END); break;
case SEMI: eat(SEMI); S(); L(); break;
default: error(); }}
void E(void) { eat(NUM); eat(EQ); eat(NUM); } 对于如下文法:
E -> E + T
E -> E - T
E -> T当 lookahead 是 num 时,无法仅凭当前 token 决定选择哪条产生式。
如果采用普通 recursive descent:
于是引出:
k 个 token 决定使用哪条产生式其中:
LL(k) = Left-to-right parse, Leftmost derivation, k-token lookahead为了构造 predictive parsing table,需要先计算:
如果某个符号能推出 ε,则它是 nullable。
falseYi 都 nullable,则 X nullablefor each symbol X:
Nullable(x) = False
repeat
for each production X -> Y1 Y2 … Yk:
if Nullable(Yi) = True for 1 <= i <= k:
Nullable(X) = True
until Nullable did not change in this iteration FIRST(γ) 表示:
从字符串
γ推导出的串中,可能出现在最前面的终结符集合
若 ,则:
X 是终结符,则 FIRST(X) = {X}X -> Y1 Y2 ... Yk
FIRST(Y1)Y1 nullable,再加入 FIRST(Y2)FOLLOW(X) 表示在某个句型中,紧接在 X 后面的终结符集合。也就是:
若:
则:
FIRST(β) 加入 FOLLOW(X)β =>* ε,则把 FOLLOW(Y) 加入 FOLLOW(X)
对产生式:
X -> γ建表规则:(实际上,思路就是根据第一个终止符,判断当前产生式有无作用)
t ∈ FIRST(γ),则在 M[X,t] 中填入 X -> γγ nullable,且 t ∈ FOLLOW(X),则在 M[X,t] 中填入 X -> γ
若按上述方法构造出的 parsing table 没有重复表项,则文法是 LL(1)。
若某一格中出现多条产生式:
除了递归版本,也可以使用显式栈实现非递归预测分析:
$ 时 accept 在使用 Predictive Parsing Table 的时候,不可以使用左递归,要将其消除。
例如:
E -> E + T
E -> T会导致:
FIRST(E+T) = FIRST(E)E -> T 冲突因此:
Top-down parsing 不能处理 left-recursive grammar
把:
A -> Aα
A -> β改写为:
A -> β A'
A' -> α A'
A' -> ε例如:
E -> E + T | T改写为:
E -> T E'
E' -> + T E'
E' -> εS -> E $
E -> T E'
E' -> + T E'
E' -> - T E'
E' -> ε
T -> F T'
T' -> * F T'
T' -> / F T'
T' -> ε
F -> id
F -> num
F -> (E)若同一 non-terminal 的两条产生式有公共前缀:
S -> if E then S else S
S -> if E then S在 LL(1) 中,当看到 if 时无法马上决定选哪条。
S -> if E then S X
X -> else S
X -> εA -> αβ
A -> αγ
A -> α改写为:
A -> α A'
A' -> β | γ | ε本质:
常用做法:
skip until a token in FOLLOW(X)例如:
int Tprime_follow[] = {PLUS, RPAREN, EOF};当 T' 出错时,跳过输入直到遇到 +, ) 或 EOF。
Bottom-Up parsing 的一般风格是 shift-reduce parsing。
Bottom-up parsing 本质上是在 逆转 right-most derivation。

L = left-to-right scanR = construct the reverse of a rightmost derivationk = lookahead token 数| 动作 | 含义 |
|---|---|
| Shift | 把下一个输入移入栈 |
| Reduce | 用某条产生式归约 |
| Goto | 归约后跳转到新状态 |
| Accept | 成功分析结束 |
A -> α . β表示:
α 已经处理过β若在集合 I 中有:
A -> α . X β且 X 是非终结符,则对每条产生式:
X -> γ加入:
X -> . γ重复直到不再变化。
若 I 中有:
A -> α . X β则把点右移:
A -> α X . β收集后再做 closure。
shiftgotoA -> α .:对所有终结符填 reduceS' -> S . $:在 $ 列填 acceptLR(0) 的 reduce 太激进:
A -> α .,就对所有终结符都 reduceSLR 改进为:
只在
FOLLOW(A)中的 token 上放置 reduce 动作
若状态 I 中含有:
A -> α .则仅对:
t ∈ FOLLOW(A)填写:
reduce A -> α在 item 中加入 lookahead。
(A -> α . β, x)含义:
A -> αβ 的某个分析状态x若有:
(A -> α . X β, z)则对每个产生式:
X -> γ以及每个:
w ∈ FIRST(β z)加入:
(X -> . γ, w)若状态中有:
(A -> α ., z)则只在 lookahead z 上 reduce。
LR(1) 表太大。
把 LR(1) 项目核相同、只有 lookahead 不同 的状态合并。
LR(0) ⊂ SLR ⊂ LALR(1) ⊂ LR(1)通常:
经典例子:dangling else
S -> if E then S else S
S -> if E then S
S -> other输入:
if a then if b then s1 else s2存在两种解释:
else 匹配最近的 thenelse 匹配外层 then大多数语言选择:
else匹配最近可能的then
引入:
M:matched ifU:unmatched if这会自然得到“最近 then 匹配 else”的效果。
因此可以使用 parser generator。
.y 规格文件tab.c)definitions
%%
rules
%%
auxiliary routines可用 Yacc 写表达式计算器:
%token NUMBER
%%
command: exp { printf("%d\n", $1); };
exp: exp '+' term { $$ = $1 + $3; }
| exp '-' term { $$ = $1 - $3; }
| term { $$ = $1; }
;
term: term '*' factor { $$ = $1 * $3; }
| factor { $$ = $1; }
;
factor: NUMBER { $$ = $1; }
| '(' exp ')' { $$ = $2; }
;yyparse()yylex()yyerror()yylval$$, $1, $2, ...$i:产生式右部第 i 个符号的值$$:产生式左部的值例如:
exp: exp '+' term { $$ = $1 + $3; }表示:
$$ = 当前 exp 的值$1 = 左边 exp 的值$3 = term 的值默认情况下:
#define YYSTYPE int若不同符号需要不同值类型,可用:
%union {
double val;
char op;
}
%type <val> exp term NUMBER
%type <op> op这样可以让:
exp 存 doubleop 存 char有时需要在整条产生式识别完成前执行代码:
decl: type { current_type = $1; } var_list这类 action 常用于:
Yacc 会报告:
默认处理方式:
但一般来说:
大多数 shift-reduce 冲突,以及所有 reduce-reduce 冲突,都应通过改写 grammar 消除。
Yacc 支持用优先级/结合性规则消解歧义。
%nonassoc EQ NEQ
%left PLUS MINUS
%left TIMES DIV
%right EXP表示:
PLUS / MINUS:同优先级,左结合TIMES / DIV:更高优先级,左结合EXP:右结合EQ / NEQ:不结合若发生冲突:
%left → reduce%right → shift%nonassoc → error%prec可给规则强行指定优先级,例如一元负号:
%left PLUS MINUS
%left TIMES
%left UMINUS
exp: MINUS exp %prec UMINUS有些问题不应该在 parser 阶段解决,而应放到 semantic analysis。
例如:
a + 5 & b从语法上可以是合法表达式; 但从语义上可能类型不匹配。
原则:
开发者通常希望编译器报告 尽可能多的错误,而不是遇到第一个错误就停止。
| 类型 | 含义 |
|---|---|
| Local error recovery | 在出错点附近恢复 |
| Global error repair | 尝试全局最小修改 |
Yacc 提供特殊终结符:
error例如:
exp -> ( error )
exps -> error ; exp作用:
error 的状态) 或 ;)这些同步 token 叫做:
synchronizing tokensWritten by
Comments