当前位置:   article > 正文

上下文无关文法(CFG)

上下文无关文法

正则表达式(RE)、有限自动机(FA)和词法分析(LA) 一文中介绍了用于描述正则语言(RL, Regular Language)的两种等价方法——正则表达式(RE, Regular Expression)和有限自动机(Finite Automata)。但是 RE 和 FA 在描述有递归结构的语言时就显得力不从心了,例如诸如 { 0 n 1 n ∣ n ≥ 0 } \{ 0^{n}1^{n} | n \ge 0 \} {0n1nn0} 这样的递归结构。本文介绍一种适合处理这种语言的表达能力更强的新方法——上下文无关文法(CFG, Context-free Grammar)。

使用上下文无关文法描述和产生的语言被成为上下文无关语言(CFL, Context-free Language)。

上下文无关文法的形式化定义

下面这个是一个上下文无关文法的一个例子,成为 G 1 G1 G1
S − > a S b S − > T T − > ∗

S>aSbS>TT>
SST>aSb>T>

CFG 是由一系列产生式组成,每个产生式是由被一个箭头分成左右两个部分。左边的符号被称为非终结符,一般用大写字母表示。右边是一个字符串,包括非终结符和被称为终结符的其他符号组成,终结符可以是小写字母、数字和特殊符号组成。第一个产生式左边的非终结符被成为起始符。例如, G 1 G1 G1 有三个产生式, S S S T T T 是非终结符,并且 S S S 是起始符, G 1 G1 G1 的终止符为 a a a b b b ∗ *

上下文无关文法的形式化定义如下:

上下文无关文法是一个四元组 ( V , Σ , R , S ) (V, \Sigma,R,S) (V,Σ,R,S),其中

  • V V V 是非终结符的有限集,
  • Σ \Sigma Σ 是终结符的有限集,
  • R R R 是产生式的有限集,每个产生式是有非终结符和非终结符、终结符的字符串组成,
  • S S S 是起始符, S ∈ V S \in V SV

使用文法产生一个具体的语句的过程被成为推导,推导的过程就是不断挑选文法中的产生式将起始符的右边替换成只有终结符的字符串为止。例如,使用 G 1 G1 G1 产生字符串 a a a ∗ b b b aaa*bbb aaabbb 的推导如下:
S − > a S b − > a a S b b − > a a a S b b b − > a a a ∗ b b b

S>aSb>aaSbb>aaaSbbb>aaabbb
S>aSb>aaSbb>aaaSbbb>aaabbb

这里安利一个非常好用的工具,可戳 cfg-grammar-tool,能够根据 CFG 文法推到句子和判断句子是否可以被推导。
在这里插入图片描述

上下文无关文法之所以是“上下文无关”的,那是因为该文法中每个产生式的左边只有唯一符号(即非终结符)。每个产生式左边的非终结符可以自由地被替换成右边的字符串,而不管这个非终结符出现的位置(所处的上下文)。例如:
S − > a S b S − > a b

S>aSbS>ab
SS>aSb>ab

这个就是一个上下文无关文法。而下面的例子则不是。

a S b − > a a S b b S − > a b

aSb>aaSbbS>ab
aSbS>aaSbb>ab

这是个上下文相关文法,它的第一个产生式左边有不止一个符号,所以你在匹配这个产生式中的 S S S 的时候必需确保 S S S 所处的“上下文”,即其左边的 a a a 和右边的 b b b,所以是上下文相关文法。

在 CFG 中可能存在多个产生式的左边非终结符相同,可以将这样的产生式合并简化。例如:
S − > a S b S − > a b

S>aSbS>ab
SS>aSb>ab

可以简化为:
S − > a S b ∣          a b

S>aSb|        ab
S>aSb        ab

或者:
S − > a S b   ∣   a b

S>aSb | ab
S>aSb  ab

巴科斯-瑙尔范式

上述 CFG 的符号表示法起源于传统的 CFG 符号表示法——巴科斯-瑙尔范式(BNF, Backus-Naur Form)。例如:
< S h e e p N o i s e > : : = b a a ‾ < S h e e p N o i s e > ∣     b a a ‾ <SheepNoise> ::= \underline{baa} <SheepNoise> \\ | \ \ \ \underline{baa} <SheepNoise>::=baa<SheepNoise>   baa

BNF 使用 < > <> <> 包围的字符串标记非终结符,终结符带有下划线,符号 : : = ::= ::= 表示“推导”。

语法分析

根据 CFG 推导句子的过程也可以使用图形化展示,被成为语法分析树(parse tree)。例如 G 1 G1 G1 产生字符串 a a a ∗ b b b aaa*bbb aaabbb

最左推导和最右推导

最左推导:每个步骤都替换最左侧的非终结符。
最右推导:每个步骤都替换最右侧的非终结符。

给定一个括号表达式的 CFG:
E X P R − > (   E X P R   ) ∣         E X P R    O P    I D ∣         I D O P − > + ∣         − ∣         ∗ ∣         ÷ I D − > a ∣         a ∣         b ∣         c

EXPR>( EXPR )|       EXPR  OP  ID|       IDOP>+|       |       |       ÷ID>a|       a|       b|       c
EXPROPID>( EXPR )       EXPR  OP  ID       ID>+                     ÷>a       a       b       c

考虑 ( a + b ) ∗ c (a+b)*c (a+b)c,它的最左推导为:
E X P R − > E X P R    O P    I D − > (   E X P R   )    O P    I D − > (   E X P R    O P    I D   )    O P    I D − > (   I D    O P    I D   )    O P    I D − > ( a    O P    I D   )    O P    I D − > ( a + I D   )    O P    I D − > ( a + b )    O P    I D − > ( a + b ) ∗ I D − > ( a + b ) ∗ c

EXPR>EXPR  OP  ID>( EXPR )  OP  ID>( EXPR  OP  ID )  OP  ID>( ID  OP  ID )  OP  ID>(a  OP  ID )  OP  ID>(a+ID )  OP  ID>(a+b)  OP  ID>(a+b)ID>(a+b)c
EXPR>EXPR  OP  ID>( EXPR )  OP  ID>( EXPR  OP  ID )  OP  ID>( ID  OP  ID )  OP  ID>(a  OP  ID )  OP  ID>(a+ID )  OP  ID>(a+b)  OP  ID>(a+b)ID>(a+b)c

对应的语法分析树为:

再看最右推导:
E X P R − > E X P R    O P    I D − > E X P R    O P    c − > E X P R    ∗ c − > (   E X P R   ) ∗ c − > (   E X P R    O P    I D   ) ∗ c − > (   E X P R    O P    b   ) ∗ c − > (   E X P R + b   ) ∗ c − > (   I D + b   ) ∗ c − > (   a + b   ) ∗ c

EXPR>EXPR  OP  ID>EXPR  OP  c>EXPR  c>( EXPR )c>( EXPR  OP  ID )c>( EXPR  OP  b )c>( EXPR+b )c>( ID+b )c>( a+b )c
EXPR>EXPR  OP  ID>EXPR  OP  c>EXPR  c>( EXPR )c>( EXPR  OP  ID )c>( EXPR  OP  b )c>( EXPR+b )c>( ID+b )c>( a+b )c

对应的语法分析树为:

这个例子最左推导和最右推导得到的语法分析树是相同的。

二义性

如果一个语法使用不同的方式推导一个语句产生不同的语法分析树,而不同的语法分析树有不同的含义,这可能不是我们期望的,称这个语法具有二义性。

如果语法 G G G 的语言 L ( G ) L(G) L(G) 中有一个以上的最左(或最右)推导,那么语法 G G G 就是二义性的。

还是上文给的 CFG:
E X P R − > (   E X P R   ) ∣         E X P R    O P    I D ∣         I D O P − > + ∣         − ∣         ∗ ∣         ÷ I D − > a ∣         a ∣         b ∣         c

EXPR>( EXPR )|       EXPR  OP  ID|       IDOP>+|       |       |       ÷ID>a|       a|       b|       c
EXPROPID>( EXPR )       EXPR  OP  ID       ID>+                     ÷>a       a       b       c

考虑 a + b ∗ c a + b * c a+bc,很容易得到下面两个语法分析树:

求值表达式的一种方式是采用后根遍历的方法,左图的含义是 a + b ∗ c a+b*c a+bc,而右图的含义为 ( a + b ) ∗ c (a+b)*c (a+b)c。这样的语法显然有问题,它的问题是设计语法的时候没有考虑运算符的优先级。我们可以根据表达式中运算符的优先级进行层次分组设计,例如, ( ) () () 有最高优先级, ∗ * ÷ \div ÷ 有中优先级, + + + − - 有最低优先级。下面给出改进的语法:
G O A L − > E X P R E X P R − > E X P R + T E R M ∣         E X P R − T E R M ∣         T E R M T E R M − > T E R M ∗ F A C T O R ∣         T E R M − F A C T O R ∣         F A C T O R F A C T O R − > (   E X P R   ) ∣         I D I D − > a ∣         b ∣         c

GOAL>EXPREXPR>EXPR+TERM|       EXPRTERM|       TERMTERM>TERMFACTOR|       TERMFACTOR|       FACTORFACTOR>( EXPR )|       IDID>a|       b|       c
GOALEXPRTERMFACTORID>EXPR>EXPR+TERM       EXPRTERM       TERM>TERMFACTOR       TERMFACTOR       FACTOR>( EXPR )       ID>a       b       c

根据该语法推导 a + b ∗ c a+b*c a+bc 得到语法分析树如下:

上下文无关文法的设计

上下文无关文法的设计有以下几个思路:

  • 第一,一个复杂的 CFG 可能能够分解成几个简单的 CFG,然后分别进行设计,再合并到一起。

例如 { 0 n 1 n ∣ n ≥ 0 } ∪ { 1 n 0 n ∣ n ≥ 0 } \{ 0^{n}1^{n} | n \ge 0 \} \cup \{ 1^{n}0^{n} | n \ge 0 \} {0n1nn0}{1n0nn0}

先构建 { 0 n 1 n ∣ n ≥ 0 } \{ 0^{n}1^{n} | n \ge 0 \} {0n1nn0} 的 CFG,很容易设计如下:
S 1 − > 0 S 1 1   ∣   ϵ S_1 -> 0 S_{1} 1 \ | \ \epsilon S1>0S11  ϵ

再构建 { 1 n 0 n ∣ n ≥ 0 } \{ 1^{n}0^{n} | n \ge 0 \} {1n0nn0} 的 CFG,如下:
S 2 − > 1 S 2 0   ∣   ϵ S_2 -> 1 S_{2} 0 \ | \ \epsilon S2>1S20  ϵ

最后将二者合并即可:
S − > S 1   ∣   S 2 S 1 − > 0 S 1 1   ∣   ϵ S 2 − > 1 S 2 0   ∣   ϵ

S>S1 | S2S1>0S11 | ϵS2>1S20 | ϵ
SS1S2>S1  S2>0S11  ϵ>1S20  ϵ

  • 第二,正则表达式语法是上下文无关文法的子集,如果一个 CFG 正好是 RE 的话,则可以先构建 NFA,然后把 NFA 转换为 CFG。方式如下:
    (1)构建 NFA 。
    (2)每个 NFA 中的状态 q i q_i qi 对应一个非终结符 R i R_i Ri,对于 DFA 的 δ ( q i , a ) = q j \delta(q_i, a) = q_j δ(qi,a)=qj,则对应添加一个产生式 R i − > a R j R_i -> aR_j Ri>aRj
    (3)对于 DFA 的 δ ( q i , ϵ ) = q j \delta(q_i, \epsilon) = q_j δ(qi,ϵ)=qj,则对应添加一个产生式 R i − > R j R_i -> R_j Ri>Rj
    (4)若 NFA 的 q i q_i qi 是一个接受状态,则添加一个产生式 R i − > ϵ R_i -> \epsilon Ri>ϵ

例如有如下 NFA:

根据上述步骤很容易得到 CFG:
R 0 − > a R 0   ∣   b R 0   ∣   a R 1 R 1 − > b R 2 R 2 − > b R 3 R 3 − > ϵ

R0>aR0 | bR0 | aR1R1>bR2R2>bR3R3>ϵ
R0R1R2R3>aR0  bR0  aR1>bR2>bR3>ϵ

  • 第三,某些 CFG 中有两个相互联系的子串,需要检测一个子串是否正好对应另一个子串,这就需要记住子串的信息。例如 { 0 n 1 n ∣ n ≥ 0 } \{ 0^{n}1^{n} | n \ge 0 \} {0n1nn0},为了检查 0 的个数是否等于 1 的个数,则需要记住 0 的个数。对于这种情况,可以使用 R − > u R v R->uRv R>uRv 形式的产生式。

  • 第四,在更复杂的语言中,字串中可能包含一种被递归使用的结构,例如前文介绍的算术表达式的文法,每次出现一个 I D ID ID, 就会递归地出现一个用括号括起来的完整表达式。可以考虑将这样的递归结构放在对应的位置上。

以上介绍的几个思路可以作为 CFG 设计时的参考。

至此,本文结束。

参考:

  • Engineering a compiler, 2E.
  • Introduction to the Theory of Computation, 3E.
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/331269
推荐阅读
相关标签
  

闽ICP备14008679号

        
cppcmd=keepalive&