视频加载失败

课程

43479 字
约 125 分钟

编译原理计算大题解题步骤与示例

编译原理note·更新于 2026-09-15

编译原理计算大题解题步骤与示例


模块一:高级程序设计语言的语法描述

题型一:文法设计 (Grammar Design)

如何做:

  1. 明确符号的含义
    • 我们用大写字母(如 SS, AA, BB)代表“中间过程代号”或“组合”。
    • 我们用小写字母、数字和符号(如 aa, bb, 00, 11)代表“最终出现在句子里的具体字符”。
    • 用箭头 \to 表示代换,用竖线 \mid 表示“或者”。
  2. 观察字符的匹配规律
    • 如果两个符号的数量是绑定的(例如有 nnaa 就必须有 nnbb 连着),那就在规则的左右两边分别写上 aabb,中间写上它自己,形成一种“包夹”结构。如:XaXbX \to aXb
    • 确定最简单的情况(递归结束条件)。如果数量可以是 0,最简情况就是一个空符号 ε\varepsilon(表示空无一物),如:XaXbεX \to aXb \mid \varepsilon
    • 如果数量必须大于等于 1,那最简情况就是 abab,如:XaXbabX \to aXb \mid ab
  3. 独立部分拆开写
    • 如果句子的前半部分(如 anbna^n b^n)和后半部分(如 cic^i)互不干扰,数量没有绑定关系,就用两个不同的大写字母拼接,比如先写一行初始规则 SXYS \to X Y,然后再分别写大写字母 XX 的规则和 YY 的规则。
  • 题目:给定语言 L1={anbncin1,i0}L_1 = \{a^n b^n c^i \mid n \ge 1, i \ge 0\},构造其文法。

  • 解答

    1. 句子由两部分拼接而成:前半部分是 anbna^n b^n(数量相等,且至少有一个),后半部分是 cic^i(可以有任意个,包括 0 个)。
    2. 我们设初始组合为大写字母 SS,拆成前半部分组合 XX 和后半部分组合 YYSXYS \to X Y
    3. 前半部分 XX 产生相同数量的 aabb,且至少 1 对,代换规则为: XaXbabX \to aXb \mid ab
    4. 后半部分 YY 产生任意个 cc(包括 0 个,即空字符 ε\varepsilon),代换规则为: YcYεY \to cY \mid \varepsilon
    5. 完整文法为: SXYXaXbabYcYε\begin{aligned} S &\to X Y \\ X &\to aXb \mid ab \\ Y &\to cY \mid \varepsilon \end{aligned}
  • 例题 2:构造一个只能产生奇数(且多位数首位不为 0)的十进制数文法。

  • 解答

    1. 句子如果是一位数,只能是奇数:1,3,5,7,91, 3, 5, 7, 9,我们用非终结符 DoddD_{\text{odd}} 表示。
    2. 句子如果是多位数,首位数字不能是 0,只能是 1199,我们用非终结符 DheadD_{\text{head}} 表示;末位数字必须是奇数,即 DoddD_{\text{odd}};中间部分可以是任意长度的数字串(包含0,也可以为空),我们用非终结符 MM 表示。
    3. 定义单个数字非终结符 DD,它可以是 0 或者是首位数字 DheadD_{\text{head}}
    4. 完整文法定义为: SDoddDheadMDoddMDMεDodd13579Dhead123456789D0Dhead\begin{aligned} S &\to D_{\text{odd}} \mid D_{\text{head}} M D_{\text{odd}} \\ M &\to D M \mid \varepsilon \\ D_{\text{odd}} &\to 1 \mid 3 \mid 5 \mid 7 \mid 9 \\ D_{\text{head}} &\to 1 \mid 2 \mid 3 \mid 4 \mid 5 \mid 6 \mid 7 \mid 8 \mid 9 \\ D &\to 0 \mid D_{\text{head}} \end{aligned}

题型二:最左/最右推导与推导过程判定

如何做:

  1. 最左推导:从初始大写字母(比如 EE)开始。在每一步中,只在当前的符号串里找到最靠左边的那个大写字母,查规则把它代换掉,其余的大写字母和符号保持不变。重复这一步,直到符号串里没有任何大写字母。
  2. 最右推导(规范推导):同理,每一步中只找到最靠右边的那个大写字母,用规则代换掉它,其他部分不动,直到没有大写字母。
  3. 画代换过程树(语法树)
    • 把初始大写字母写在最顶端。
    • 每次代换时(如 EE+TE \to E + T),从这个大写字母往下分叉画出分支,依次连上它变出来的符号(如左分叉连 EE,中间分叉连 ++, 右分叉连 TT)。
    • 重复这个过程,直到树的最底端全都是具体的符号(小写字母、数字或符号)。从左到右把树底部的具体符号串起来,应当刚好等于要推导的句子。
  • 题目:已知代换规则为: ETE+TET , TFTFT/F , F(E)iE \to T \mid E + T \mid E - T \ , \ T \to F \mid T * F \mid T / F \ , \ F \to (E) \mid i 给出句子 i+iii + i * i 的最左和最右推导,并给出语法树。

  • 解答

    • 最左推导(每次换最左边的大写字母): EE+TT+TF+Ti+Ti+TFi+FFi+iFi+iiE \Rightarrow E + T \Rightarrow T + T \Rightarrow F + T \Rightarrow i + T \Rightarrow i + T * F \Rightarrow i + F * F \Rightarrow i + i * F \Rightarrow i + i * i
    • 最右推导(每次换最右边的大写字母): EE+TE+TFE+TiE+FiE+iiT+iiF+iii+iiE \Rightarrow E + T \Rightarrow E + T * F \Rightarrow E + T * i \Rightarrow E + F * i \Rightarrow E + i * i \Rightarrow T + i * i \Rightarrow F + i * i \Rightarrow i + i * i
    • 语法分析树
      graph TD
          E1["E"] --> E2["E"]
          E1 --> P["+"]
          E1 --> T1["T"]
          E2 --> T2["T"]
          T2 --> F1["F"]
          F1 --> I1["i"]
          T1 --> T3["T"]
          T1 --> M["*"]
          T1 --> F2["F"]
          T3 --> F3["F"]
          F3 --> I2["i"]
          F2 --> I3["i"]
  • 例题 2:对于文法 Sa(T) , TT,SSS \to a \mid \wedge \mid (T) \ , \ T \to T, S \mid S,给出句子 (((a,a),,(a)),a)(((a, a), \wedge, (a)), a) 的最左推导和最右推导。

  • 解答

    • 最左推导(每次代换最左侧的非终结符): S(T)(T,S)(S,S)((T),S)((T,S),S)((T,S,S),S)((S,S,S),S)(((T),S,S),S)(((T,S),S,S),S)(((S,S),S,S),S)(((a,S),S,S),S)(((a,a),S,S),S)(((a,a),,S),S)(((a,a),,(T)),S)(((a,a),,(S)),S)(((a,a),,(a)),S)(((a,a),,(a)),a)\begin{aligned} S &\Rightarrow (T) \\ &\Rightarrow (T, S) \\ &\Rightarrow (S, S) \\ &\Rightarrow ((T), S) \\ &\Rightarrow ((T, S), S) \\ &\Rightarrow ((T, S, S), S) \\ &\Rightarrow ((S, S, S), S) \\ &\Rightarrow (((T), S, S), S) \\ &\Rightarrow (((T, S), S, S), S) \\ &\Rightarrow (((S, S), S, S), S) \\ &\Rightarrow (((a, S), S, S), S) \\ &\Rightarrow (((a, a), S, S), S) \\ &\Rightarrow (((a, a), \wedge, S), S) \\ &\Rightarrow (((a, a), \wedge, (T)), S) \\ &\Rightarrow (((a, a), \wedge, (S)), S) \\ &\Rightarrow (((a, a), \wedge, (a)), S) \\ &\Rightarrow (((a, a), \wedge, (a)), a) \end{aligned}
    • 最右规范推导(每次代换最右侧的非终结符): S(T)(T,S)(T,a)(S,a)((T),a)((T,S),a)((T,(T)),a)((T,(S)),a)((T,(a)),a)((T,S,(a)),a)((T,,(a)),a)((S,,(a)),a)(((T),,(a)),a)(((T,S),,(a)),a)(((T,a),,(a)),a)(((S,a),,(a)),a)(((a,a),,(a)),a)\begin{aligned} S &\Rightarrow (T) \\ &\Rightarrow (T, S) \\ &\Rightarrow (T, a) \\ &\Rightarrow (S, a) \\ &\Rightarrow ((T), a) \\ &\Rightarrow ((T, S), a) \\ &\Rightarrow ((T, (T)), a) \\ &\Rightarrow ((T, (S)), a) \\ &\Rightarrow ((T, (a)), a) \\ &\Rightarrow ((T, S, (a)), a) \\ &\Rightarrow ((T, \wedge, (a)), a) \\ &\Rightarrow ((S, \wedge, (a)), a) \\ &\Rightarrow (((T), \wedge, (a)), a) \\ &\Rightarrow (((T, S), \wedge, (a)), a) \\ &\Rightarrow (((T, a), \wedge, (a)), a) \\ &\Rightarrow (((S, a), \wedge, (a)), a) \\ &\Rightarrow (((a, a), \wedge, (a)), a) \end{aligned}

题型三:句型、短语、直接短语与句柄判定

如何做:

  1. 第一步,画出代换树:根据给定的符号串,从初始大写字母开始代换,画出对应的树,使树的底部叶子节点从左到右连起来刚好是给定的符号串。
  2. 第二步,找短语:树里所有有分叉的代号(即大写字母)都是一个“子树根”。每一个子树根往下看,把从它身上分叉出去的所有底部叶子节点(不管是大写字母还是具体符号)从左到右串成一个子串,这个子串就是该句子的一个短语
  3. 第三步,找直接短语:寻找那些“一步就生成叶子”的大写字母(即它的分叉下面全部是树底部的叶子,没有更深的分叉了)。这些一步生成出来的子串就是直接短语
  4. 第四步,找句柄:在找出来的所有直接短语里,看哪个位置最靠左边,最左侧的那一个直接短语就是句柄
  • 题目:已知规则如下,证明 E+TFE + T * F 是它的一个符号串,并指出所有短语、直接短语与句柄。 EE+TT , TTFF , F(E)iE \to E + T \mid T \ , \ T \to T * F \mid F \ , \ F \to (E) \mid i

  • 解答

    1. 证明是符号串:存在推导 EE+TE+TFE \Rightarrow E + T \Rightarrow E + T * F,故它是由初始字母推导出来的合法符号串。
    2. 画出代换树
      graph TD
          E1["E"] --> E2["E"]
          E1 --> P["+"]
          E1 --> T1["T"]
          T1 --> T2["T"]
          T1 --> M["*"]
          T1 --> F["F"]
    3. 分析各分叉代号生成的底部叶子(找短语)
      • T1T1 为根的子树,底部的分叉叶子是 TT*FF,所以 TFT * F 是短语
      • E1E1 为根的子树(整棵树),底部的分叉叶子是 EE++TT*FF,所以 E+TFE + T * F 是短语
    4. 找直接短语
      • 观察树中,只有 T1T1 这个节点是一步分叉直接产生了叶子节点 TT, *, FF,它没有更深的分支。
      • 所以,直接短语只有 TFT * F
    5. 确定句柄
      • 直接短语中,最左边的就是 TFT * F
      • 句柄是 TFT * F
  • 例题 2:已知文法 GG 为: Ttε(F) , FT+FTT \to t \mid \varepsilon \mid (F) \ , \ F \to T + F \mid T 给出句型 ((t)+T)((t)+T) 的所有短语、直接短语与句柄。

  • 解答

    1. 最右推导过程T(F)(T+F)(T+T)((F)+T)((T)+T)((t)+T)T \Rightarrow (F) \Rightarrow (T+F) \Rightarrow (T+T) \Rightarrow ((F)+T) \Rightarrow ((T)+T) \Rightarrow ((t)+T)
    2. 画出语法树
      graph TD
          T1["T"] --> LP1["("]
          T1 --> F1["F"]
          T1 --> RP1[")"]
          F1 --> T2["T"]
          F1 --> P["+"]
          F1 --> F2["F"]
          F2 --> T3["T (叶)"]
          T2 --> LP2["("]
          T2 --> F3["F"]
          T2 --> RP2[")"]
          F3 --> T4["T"]
          T4 --> t["t"]
      (注意:右侧的非终结符 TT 未展开,作为叶子节点直接出现在句型中)
    3. 寻找短语: 根据语法树,找每一个有子树的非终结符节点,其所有叶子子节点组合成的符号串为短语:
      • T4T_4 为根的子树,叶节点为 t,故 tt 是短语
      • F3F_3 为根的子树,叶节点为 T4T_4(推导为 t),故 tt 是短语
      • T2T_2 为根的子树,叶节点为 (, F3F_3(推导为 t), ),故 (t)(t) 是短语
      • T3T_3 为根的子树,叶节点为 TT 本身,故 TT 是短语
      • F2F_2 为根的子树,叶节点为 T3T_3(即 TT),故 TT 是短语
      • F1F_1 为根的子树,叶节点为 T2,+,F2T_2, +, F_2,推导为 (t)+T(t)+T,故 (t)+T(t)+T 是短语
      • T1T_1(根节点)为根的子树,叶节点为所有,即 ((t)+T)((t)+T),故 ((t)+T)((t)+T) 是短语。 去重后,短语集合为{t,T,(t),(t)+T,((t)+T)}\{ t, T, (t), (t)+T, ((t)+T) \}
    4. 寻找直接短语: 找出高度为 2 的子树(即一步推导就全变成叶节点的非终结符):
      • T4tT_4 \to t 是一步推导,所以 tt 是直接短语
      • F2TF_2 \to T 是一步推导(因为 T3T_3 作为叶子节点不再展开),所以 TT 是直接短语直接短语集合为{t,T}\{ t, T \}
    5. 确定句柄: 句柄是最左边的直接短语。在句型 ((t)+T)((t)+T) 中,直接短语 tt 的位置比 TT 更靠左,因此句柄是 tt

题型四:文法二义性证明

如何做:

  1. 二义性的含义:一个句子用同一套规则,可以画出两棵不同形状的树(或者写出两个不同的最左代换过程)。
  2. 证明步骤
    • 挑一个尽可能简单的例子句子,比如带有连加或加乘混合的句子(如 i+iii + i * i)。
    • 第一种画法:按照“先加后乘”的代换逻辑画一棵树。
    • 第二种画法:按照“先乘后加”的代换逻辑画另一棵树。
    • 展示这两个推导并说明它们不同,即可完成证明。
  • 题目:证明规则 EE+EEE(E)iE \to E + E \mid E * E \mid (E) \mid i 是二义的。

  • 解答

    1. 选择测试句子:i+iii + i * i
    2. 我们可以写出两种不同的最左代换过程:
      • 推导方式 1(先算乘法): EE+Ei+Ei+EEi+iEi+iiE \Rightarrow E + E \Rightarrow i + E \Rightarrow i + E * E \Rightarrow i + i * E \Rightarrow i + i * i
      • 推导方式 2(先算加法): EEEE+EEi+EEi+iEi+iiE \Rightarrow E * E \Rightarrow E + E * E \Rightarrow i + E * E \Rightarrow i + i * E \Rightarrow i + i * i
    3. 推导方式 1 对应的树中,乘法位于加法的下方(乘法优先);推导方式 2 对应的树中,加法位于乘法的下方(加法优先)。同一个句子可以生成两棵不同的语法树,因此该规则是二义的。
  • 例题 2:证明文法 SiSeSiSiS \to iSeS \mid iS \mid i(经典的 Dangling-Else 悬挂else文法)是二义的。

  • 解答

    1. 选择测试句子:iiieii i i e i (代表 if C1 then if C2 then S1 else S2)。
    2. 我们给出该句子对应的两个不同的最左推导:
      • 推导方式 1else 与最外层的第一个 if 结合): SiSeSiiSeSiiieSiiiei\begin{aligned} S &\Rightarrow i S e S \\ &\Rightarrow i i S e S \\ &\Rightarrow i i i e S \\ &\Rightarrow i i i e i \end{aligned}
      • 推导方式 2else 与内层的第二个 if 结合): SiSiiSeSiiieSiiiei\begin{aligned} S &\Rightarrow i S \\ &\Rightarrow i i S e S \\ &\Rightarrow i i i e S \\ &\Rightarrow i i i e i \end{aligned}
    3. 由于同一个句子 iiieii i i e i 存在两个不同的最左推导(可画出两棵不同结构的语法分析树),因此该文法是二义的。

模块二:词法分析与正规式

题型五:正规式的代数性质与等价性证明

如何做:

  1. 星号 *:代表“可以重复 0 次或任意多次”。
  2. 括号和竖线 \mid:代表选择(或者)。
  3. 等价化简公式
    • A(BC)=ABACA(B \mid C) = AB \mid AC (分配律,把前面的 AA 分别乘进去)
    • (A)=A(A^*)^* = A^* (多次重复还是任意次重复)
    • A=εAAA^* = \varepsilon \mid A A^* (任意次重复等于“什么都没有”或者“先写一个 AA,后面再接任意次重复”)
  4. Arden代换公式:如果你得到了一个关于大写字母 XX 的等式,形状像 X=AXBX = A X \mid B(表示 XX 等于 AA 后面接 XX,或者等于 BB),那么 XX 的唯一解就是 X=ABX = A^* B(表示先重复任意次 AA,最后接上 BB)。
  • 题目:利用代数规律证明方程 A=baAA = b \mid aA 的唯一解是 A=abA = a^*b

  • 解答

    1. 我们把 A=aAbA = aA \mid b 顺着定义逐步展开代入: A=aAb=a(aAb)b=aaAabb=aaaAaababb=akA(ak1babb)\begin{aligned} A &= aA \mid b \\ &= a(aA \mid b) \mid b = aaA \mid ab \mid b \\ &= aaaA \mid aab \mid ab \mid b \\ &= a^k A \mid (a^{k-1}b \mid \dots \mid ab \mid b) \end{aligned}
    2. 当我们无限展开下去,前面的 aa 可以重复任意多次(记作 aa^*),最后接上尾巴 bb
    3. 根据 Arden 引理,方程 X=AXBX = A X \mid B 的唯一解就是 X=ABX = A^* B。这里令 X=A,A=a,B=bX = A, A = a, B = b,直接得出唯一解为 A=abA = a^* b
  • 例题 2:利用正规式的恒等式规律,证明恒等式 (AB)A=A(BA)(AB)^*A = A(BA)^* 成立。

  • 解答

    1. 方法一:展开法(直观理解)
      • 左边 (AB)A(AB)^*A 展开: (AB)A=(εABABABABABAB)A=AABAABABAABABABA(AB)^*A = (\varepsilon \mid AB \mid ABAB \mid ABABAB \mid \dots)A = A \mid ABA \mid ABABA \mid ABABABA \mid \dots
      • 右边 A(BA)A(BA)^* 展开: A(BA)=A(εBABABABABABA)=AABAABABAABABABAA(BA)^* = A(\varepsilon \mid BA \mid BABA \mid BABABA \mid \dots) = A \mid ABA \mid ABABA \mid ABABABA \mid \dots
      • 展开后的集合形式完全一致,故等式成立。
    2. 方法二:代数推导法(利用恒等律 U=εUUU^* = \varepsilon \mid U U^*U=εUUU^* = \varepsilon \mid U^* U
      • 利用分配律及展开性质: (AB)A=(εAB(AB))A=AAB(AB)A\begin{aligned} (AB)^*A &= (\varepsilon \mid AB(AB)^*)A \\ &= A \mid AB(AB)^*A \end{aligned}X=(AB)AX = (AB)^*A,那么上式可以写为 X=AABXX = A \mid AB X
      • 根据 Arden 引理(若 X=UXVX = U X \mid V,则其唯一解为 X=UVX = U^* V):
        • 这里令 U=ABU = ABV=AV = A。因此该方程的唯一解是 X=(AB)AX = (AB)^*A
      • 另一方面,我们将右边 Y=A(BA)Y = A(BA)^* 带入上述方程检验: AABY=AAB(A(BA))=AA(BA)(BA)=A(ε(BA)(BA))=A(BA)=Y\begin{aligned} A \mid AB Y &= A \mid AB(A(BA)^*) \\ &= A \mid A(BA)(BA)^* \\ &= A(\varepsilon \mid (BA)(BA)^*) \\ &= A(BA)^* = Y \end{aligned} 发现 Y=A(BA)Y = A(BA)^* 也是该方程的解。
      • 由于解是唯一的,所以必有 X=YX = Y,即 (AB)A=A(BA)(AB)^*A = A(BA)^*。得证。

题型六:构造特定语言的正规式

如何做:

  1. 明确要匹配的具体字符。例如二进制只有 01
  2. 任意个 01 的组合写成:(01)(0 \mid 1)^*
  3. 根据限制进行拼接:
    • 如果说“以 01 结尾”,说明前面是任意组合,最后强制加 01,写成:(01)01(0 \mid 1)^* 01
    • 如果说“必须包含 01”,说明 01 前面和后面都可以是任意组合,写成:(01)01(01)(0 \mid 1)^* 01 (0 \mid 1)^*
  • 题目:给出能被 55 整除的十进制正整数的正规式(不含前导零)。

  • 解答

    1. 一个十进制正整数能被 5 整除,它的个位数只能是 05
    2. 如果是个位数,只有 5 这一种可能(0 不是正整数,若包含 0 可单独列出)。
    3. 如果是多位数,首位数字不能是 0,只能是 19 的其中一个,记为:(129)(1 \mid 2 \mid \dots \mid 9)
    4. 中间的位数可以是任意数字 09 的组合,记为:(019)(0 \mid 1 \mid \dots \mid 9)^*
    5. 多位数的个位数必须是 05,记为:(05)(0 \mid 5)
    6. 组合起来,“个位数”或者“多位数”表示为: 5(129)(019)(05)5 \mid (1 \mid 2 \mid \dots \mid 9)(0 \mid 1 \mid \dots \mid 9)^*(0 \mid 5)
  • 例题 2:给出二进制数中包含奇数个 1 奇数个 0 的二进制数串的正规式。

  • 解答

    1. 第一步:构造包含奇数个 1 的正规式
      • 包含偶数个 1 的正规式(中间可以有任意个 0):(0101)(0 \mid 10^*1)^*.
      • 奇数个 1,相当于偶数个 1 后面再跟一个 1,以及任意个 0: R1=(0101)10R_1 = (0 \mid 10^*1)^*10^*
    2. 第二步:同理构造包含奇数个 0 的正规式
      • 偶数个 0 的正规式:(1010)(1 \mid 01^*0)^*.
      • 奇数个 0,相当于偶数个 0 后面再跟一个 0,以及任意个 1: R2=(1010)01R_2 = (1 \mid 01^*0)^*01^*
    3. 第三步:求“或”关系(并集)
      • 将两者用选择符 | 连接即可: R=(0101)10(1010)01R = (0 \mid 10^*1)^*10^* \mid (1 \mid 01^*0)^*01^*

题型七:NFA 确定化与 DFA 最小化

如何做:

  1. 确定化(合并含空箭头的状态)
    • 顺着“空字箭头”(无标记的箭头,通常用 ε\varepsilon 表示)一路走到底,能走到的所有状态合并起来,称为一个“状态包”。
    • 从初始状态开始,先把初始状态和它走空箭头能走到的状态打包(比如 {0}\{0\} 走空箭头能到 {0,1}\{0, 1\},打包为 AA)。
    • 对每个状态包,看它整体读入字符 aa 后能跳到哪些状态,再加上这些新状态能走空箭头到达的状态,打包成新包 BB
    • 重复这个过程,列出转移表。
  2. 最小化(化简状态)
    • 把所有状态分成两大家族:第一家族是“双圈接受状态”,第二家族是“普通状态”。
    • 拿出一个家族,看里面的状态在读入字符 aa 后,跳去的状态是否都在同一个家族里。如果在读入 aa 时,有的跳去第一家族,有的跳去第二家族,说明它们“心不齐”,必须把它们分裂成不同的小组。
    • 重复这个分裂过程,直到每个小组内的状态读入相同字符后,去往的都是同一个小组。最后,把同一个小组内的状态合并成一个状态。
  • 例题 1 (NFA 确定化 —— 子集构造法):对如下含有空弧的 NFA 进行确定化(DFA化):
    • 状态集:{0,1,2,3}\{0, 1, 2, 3\},初态为 00,终态(接受状态)为 33
    • 转移弧关系:
      • δ(0,a)={0,1}\delta(0, a) = \{0, 1\}δ(0,b)={0}\delta(0, b) = \{0\}
      • δ(1,ε)={2}\delta(1, \varepsilon) = \{2\} (空转换)
      • δ(2,b)={3}\delta(2, b) = \{3\}
    • 其 NFA 状态图如下:
      stateDiagram-v2
          [*] --> 0
          0 --> 0 : a, b
          0 --> 1 : a
          1 --> 2 : ε
          2 --> 3 : b
          classDef accept fill:#f9f,stroke:#333,stroke-width:2px;
          class 3 accept;
  • 解答
    1. 基础概念

      • ε\varepsilon-closure(SS):从状态集 SS 中的任何状态出发,仅通过空转换(ε\varepsilon-弧)所能到达的所有状态的集合(包含 SS 本身)。
      • move(S,x)\text{move}(S, x):从状态集 SS 中的任何状态出发,通过输入字符 xx 所能到达的所有状态的集合。
    2. 逐步计算子集转移

      • 初态子集 AAA=ε-closure({0})={0}A = \varepsilon\text{-closure}(\{0\}) = \{0\} (由于 3A3 \notin A,非接受状态)
      • 计算 AA 遇到 aa 的转移
        • move(A,a)=δ(0,a)={0,1}\text{move}(A, a) = \delta(0, a) = \{0, 1\}
        • B=ε-closure({0,1})=ε-closure({0})ε-closure({1})={0}{1,2}={0,1,2}B = \varepsilon\text{-closure}(\{0, 1\}) = \varepsilon\text{-closure}(\{0\}) \cup \varepsilon\text{-closure}(\{1\}) = \{0\} \cup \{1, 2\} = \{0, 1, 2\}
        • 得到新子集 B={0,1,2}B = \{0, 1, 2\}
      • 计算 AA 遇到 bb 的转移
        • move(A,b)=δ(0,b)={0}\text{move}(A, b) = \delta(0, b) = \{0\}
        • ε-closure({0})={0}=A\varepsilon\text{-closure}(\{0\}) = \{0\} = A
      • 计算子集 B={0,1,2}B = \{0, 1, 2\} 遇到 aa 的转移
        • move(B,a)=δ(0,a)δ(1,a)δ(2,a)={0,1}={0,1}\text{move}(B, a) = \delta(0, a) \cup \delta(1, a) \cup \delta(2, a) = \{0, 1\} \cup \emptyset \cup \emptyset = \{0, 1\}
        • ε-closure({0,1})={0,1,2}=B\varepsilon\text{-closure}(\{0, 1\}) = \{0, 1, 2\} = B
      • 计算子集 B={0,1,2}B = \{0, 1, 2\} 遇到 bb 的转移
        • move(B,b)=δ(0,b)δ(1,b)δ(2,b)={0}{3}={0,3}\text{move}(B, b) = \delta(0, b) \cup \delta(1, b) \cup \delta(2, b) = \{0\} \cup \emptyset \cup \{3\} = \{0, 3\}
        • C=ε-closure({0,3})=ε-closure({0})ε-closure({3})={0,3}C = \varepsilon\text{-closure}(\{0, 3\}) = \varepsilon\text{-closure}(\{0\}) \cup \varepsilon\text{-closure}(\{3\}) = \{0, 3\}
        • 得到新子集 C={0,3}C = \{0, 3\} (由于包含终态 33,为接受状态)
      • 计算子集 C={0,3}C = \{0, 3\} 遇到 aa 的转移
        • move(C,a)=δ(0,a)δ(3,a)={0,1}={0,1}\text{move}(C, a) = \delta(0, a) \cup \delta(3, a) = \{0, 1\} \cup \emptyset = \{0, 1\}
        • ε-closure({0,1})={0,1,2}=B\varepsilon\text{-closure}(\{0, 1\}) = \{0, 1, 2\} = B
      • 计算子集 C={0,3}C = \{0, 3\} 遇到 bb 的转移
        • move(C,b)=δ(0,b)δ(3,b)={0}={0}\text{move}(C, b) = \delta(0, b) \cup \delta(3, b) = \{0\} \cup \emptyset = \{0\}
        • ε-closure({0})={0}=A\varepsilon\text{-closure}(\{0\}) = \{0\} = A
      • 子集已经完全闭合,不再产生新子集。
    3. 整理得到 DFA 状态转移表

      DFA 状态对应 NFA 状态子集输入 aa输入 bb是否为接受状态
      AA (初态){0}\{0\}BBAA
      BB{0,1,2}\{0, 1, 2\}BBCC
      CC (终态){0,3}\{0, 3\}BBAA
    4. 画出确定化后的 DFA 状态图

      stateDiagram-v2
          [*] --> A
          A --> B : a
          A --> A : b
          B --> B : a
          B --> C : b
          C --> B : a
          C --> A : b
          classDef accept fill:#f9f,stroke:#333,stroke-width:2px;
          class C accept;

  • 例题 2 (DFA 最小化 —— 分割法):将如下 DFA 最小化:
    • 接受状态:0,10, 1;非接受状态:2,3,4,52, 3, 4, 5
    • 转移关系:状态 0 读 a1a \to 1, 读 b2b \to 2;状态 1 读 a1a \to 1, 读 b4b \to 4;状态 2 读 a1a \to 1, 读 b3b \to 3;状态 3 读 a3a \to 3, 读 b2b \to 2;状态 4 读 a0a \to 0, 读 b5b \to 5;状态 5 读 a5a \to 5, 读 b4b \to 4
  • 解答
    1. 第一步:初始划分
      • 接受状态组:G1={0,1}G_1 = \{0, 1\}
      • 非接受状态组:G2={2,3,4,5}G_2 = \{2, 3, 4, 5\}
    2. 第二步:测试 G1={0,1}G_1 = \{0, 1\} 是否需要拆分
      • 状态 0 和 1 读入 aa 后跳到 {1}\{1\}(都在 G1G_1 内)。
      • 状态 0 读入 bb 跳到 2(在 G2G_2 内),状态 1 读入 bb 跳到 4(在 G2G_2 内)。
      • 它们的行为完全一样(都跳到同组状态),所以 {0,1}\{0, 1\} 不用拆分,记为状态 AA
    3. 第三步:测试 G2={2,3,4,5}G_2 = \{2, 3, 4, 5\} 是否需要拆分
      • 读入 aa 时:
        • 2 读 a1G1a \to 1 \in G_1
        • 3 读 a3G2a \to 3 \in G_2
        • 4 读 a0G1a \to 0 \in G_1
        • 5 读 a5G2a \to 5 \in G_2
      • 发现 2 和 4 跳去 G1G_1,而 3 和 5 跳去 G2G_2。所以必须拆分为两组:G2A={2,4}G_{2A} = \{2, 4\}G2B={3,5}G_{2B} = \{3, 5\}
    4. 第四步:测试 {2,4}\{2, 4\} 是否需要拆分
      • 2 和 4 读 aa 都跳去 G1G_1
      • 2 读 b3G2Bb \to 3 \in G_{2B},4 读 b5G2Bb \to 5 \in G_{2B}
      • 行为一致,不用拆分,记为状态 BB
    5. 第五步:测试 {3,5}\{3, 5\} 是否需要拆分
      • 3 和 5 读 aa 都跳去 G2BG_{2B}
      • 3 读 b2G2Ab \to 2 \in G_{2A},5 读 b4G2Ab \to 4 \in G_{2A}
      • 行为一致,不用拆分,记为状态 CC
    6. 合并结论:化简后的 DFA 只有三个状态:A(0,1)A(0,1)B(2,4)B(2,4)C(3,5)C(3,5)

模块三:自顶向下语法分析

题型八:消除左递归与提取左公因子

如何做:

  1. 消除自己推导自己开头的规则(直接左递归)
    • 如果一条规则长得像:AAαβA \to A\alpha \mid \beta(大写字母 AA 指向的串里,有的又以 AA 开头,比如 AαA\alpha;有的不以 AA 开头,比如 β\beta)。这会导致代换时无限循环。
    • 我们引入一个带撇的新字母 AA',把规则改成两行:
      • 第一行把不以 AA 开头的放在前面,后面强行加上新符号:AβAA \to \beta A'
      • 第二行用新符号去接多出来的尾巴,并在最后加上空符号 ε\varepsilonAαAεA' \to \alpha A' \mid \varepsilon
  2. 提取公因子(解决开头重合的问题)
    • 如果大写字母有几个分支,开头都长得一模一样,例如 AabCabDA \to abC \mid abD。这会导致我们在读到 abab 时不知道该选哪条分支。
    • 我们把相同的开头 abab 提出来,后面接上一个新字母 AA'AabAA \to abA'
    • 然后给新字母写一条规则,把后面不同的部分放进去:ACDA' \to C \mid D
  • 题目:消除规则 Sa(T) , TT,SSS \to a \mid \wedge \mid (T) \ , \ T \to T, S \mid S 的左递归。

  • 解答

    1. 观察发现,SS 的规则没有左递归。
    2. TT 的规则中,第一个分支 T,ST, S 是以 TT 本身开头的(直接左递归)。其中,α\alpha 部分是 , S,非左递归的分支 β\betaSS
    3. 我们套用公式,引入新符号 TT'
      • 第一步,写出不以 TT 开头的分支,并接上 TT'TSTT \to S T'
      • 第二步,让 TT' 变出尾巴 , S 并接上 TT' 自己,或者变为空: T,STεT' \to , S T' \mid \varepsilon
    4. 消除左递归后的完整规则为: Sa(T)TSTT,STε\begin{aligned} S &\to a \mid \wedge \mid (T) \\ T &\to S T' \\ T' &\to , S T' \mid \varepsilon \end{aligned}
  • 例题 2:考虑文法 G[S]G[S]S(L)aSa , LL,SSS \to (L) \mid aS \mid a \ , \ L \to L, S \mid S 消去所有的左因子与左递归。

  • 解答

    1. 第一步:提取左公因子(消除回溯)
      • 观察非终结符 SS 的产生式,SaSaS \to aS \mid a 具有公共左因子 aa
      • 我们把 aa 提出来,引入新状态符号 SS',将产生式改写为: S(L)aSSSε\begin{aligned} S &\to (L) \mid aS' \\ S' &\to S \mid \varepsilon \end{aligned}
    2. 第二步:消除直接左递归
      • 观察非终结符 LL 的产生式,LL,SSL \to L, S \mid S 含有直接左递归。其中 α\alpha, Sβ\betaSS
      • 套用消除左递归公式,引入新非终结符 LL',改写为: LSLL,SLε\begin{aligned} L &\to S L' \\ L' &\to , S L' \mid \varepsilon \end{aligned}
    3. 第三步:合并得到最终文法S(L)aSSSεLSLL,SLε\begin{aligned} S &\to (L) \mid aS' \\ S' &\to S \mid \varepsilon \\ L &\to S L' \\ L' &\to , S L' \mid \varepsilon \end{aligned}

题型九:编写递归下降分析程序

如何做:

  1. 一个代号写一个函数:为规则中的每个大写字母写一个同名的处理函数/过程(比如 procedure S())。
  2. 看字做选择:在过程内部,使用 if 条件去判断当前读到的字符(用变量 lookahead 表示)。如果当前字符符合某条规则的开头,就进入那个分支。
  3. 遇到符号就匹配,遇到代号就调用
    • 如果规则右边是具体符号(如小写字母、括号等),我们就写 match('该符号')
    • 如果规则右边是大写字母(代号),我们就直接写这个大写字母的同名函数调用,例如 T()
  4. 匹配函数 match 的作用:核对当前读到的字符是否就是我们期望的字符。如果是,就读入下一个字符;如果不是,就说明句子写错了,报错退出。
  • 题目:对于产生式 Sa(T),TST,T,STεS \to a \mid \wedge \mid (T), T \to S T', T' \to , S T' \mid \varepsilon,写出递归下降分析程序。

  • 解答

    // 匹配并读取下一个字符的辅助函数
    procedure match(t: char);
    begin
        if lookahead = t then
            lookahead := nextToken()
        else
            error();
    end;
    
    // 大写字母 S 的处理函数
    procedure S();
    begin
        if lookahead = 'a' then
            match('a')
        else if lookahead = '^' then
            match('^')
        else if lookahead = '(' then
            begin
                match('(');
                T();
                match(')');
            end
        else
            error();
    end;
    
    // 大写字母 T 的处理函数
    procedure T();
    begin
        S();
        Tprime();
    end;
    
    // 大写字母 T' 的处理函数
    procedure Tprime();
    begin
        if lookahead = ',' then
            begin
                match(',');
                S();
                Tprime();
            end
        // 如果不是 ',',因为 T' 可以变为空(epsilon),
        // 只要当前字符是随后的合法字符(如右括号或结束符),就直接返回,什么都不用做
        else if (lookahead = ')') or (lookahead = '$') then
            exit
        else
            error();
    end;
  • 例题 2:已知文法如下,写出其递归下降分析程序:

    ETEE+EεTFTTTεFPFFFεP(E)ab\begin{aligned} E &\to T E' \\ E' &\to +E \mid \varepsilon \\ T &\to F T' \\ T' &\to T \mid \varepsilon \\ F &\to P F' \\ F' &\to *F' \mid \varepsilon \\ P &\to (E) \mid a \mid b \mid \wedge \end{aligned}
  • 解答: 为每个非终结符(E,E,T,T,F,F,PE, E', T, T', F, F', P)分别编写分析过程,采用 Pascal/类 Pascal 伪代码实现:

    // 辅助匹配过程
    procedure match(t: char);
    begin
        if lookahead = t then
            lookahead := nextToken()
        else
            error();
    end;
    
    procedure E();
    begin
        T();
        Eprime();
    end;
    
    procedure Eprime();
    begin
        if lookahead = '+' then
            begin
                match('+');
                E();
            end
        // 当面临 FIRST(E') 以外的符号时,若该符号属于 FOLLOW(E') = { ) , $ },可选择 epsilon 产生式归约并返回
        else if (lookahead = ')') or (lookahead = '$') then
            exit
        else
            error();
    end;
    
    procedure T();
    begin
        F();
        Tprime();
    end;
    
    procedure Tprime();
    begin
        // T' -> T | \varepsilon
        // FIRST(T) = { ( , a , b , ^ }
        if (lookahead = '(') or (lookahead = 'a') or (lookahead = 'b') or (lookahead = '^') then
            T()
        // FOLLOW(T') = FOLLOW(T) = { + , ) , $ }
        else if (lookahead = '+') or (lookahead = ')') or (lookahead = '$') then
            exit
        else
            error();
    end;
    
    procedure F();
    begin
        P();
        Fprime();
    end;
    
    procedure Fprime();
    begin
        if lookahead = '*' then
            begin
                match('*');
                Fprime();
            end
        // FOLLOW(F') = FOLLOW(F) = { ( , a , b , ^ , + , ) , $ }
        else if (lookahead = '(') or (lookahead = 'a') or (lookahead = 'b') or (lookahead = '^') or (lookahead = '+') or (lookahead = ')') or (lookahead = '$') then
            exit
        else
            error();
    end;
    
    procedure P();
    begin
        if lookahead = '(' then
            begin
                match('(');
                E();
                match(')');
            end
        else if lookahead = 'a' then
            match('a')
        else if lookahead = 'b' then
            match('b')
        else if lookahead = '^' then
            match('^')
        else
            error();
    end;

题型十:FIRST/FOLLOW集计算与预测分析表构造

如何做:

  1. FIRST集(首字符集合):对一个大写字母,看它顺着规则代换下去,第一个可能变出来的具体字符有哪些。
    • 例如 AaBεA \to aB \mid \varepsilon,那么 AA 的首字符集合就是 {a,ε}\{ a, \varepsilon \}
    • 如果 ABCA \to B C,就要看 BB 的首字符。如果 BB 能变为空,还要把 CC 的首字符也算进来。
  2. FOLLOW集(后跟字符集合):在所有规则的右边寻找某个大写字母(比如 AA),看哪些具体字符有可能紧跟在 AA 的后面出现
    • 初始的大写字母(如 SS)后面默认能放结束标记符 $$$。
    • 如果有规则 SaAbS \to a A b,那么 bb 就在 AA 的后面,把 bb 加入 AA 的后跟集。
    • 如果有规则 SaAS \to a A (后面没东西了),或者后面的大写字母能变为空,那么 SS 的后跟字符也都可以跟在 AA 的后面,把 SS 的后跟集并入 AA 的后跟集。
  3. 填表规则
    • 表的行写大写字母,列写所有具体字符。
    • 看一个大写字母的分支规则,它开头的具体字符有哪些,就把这条规则填到大写字母与那些字符对应的格子里。
    • 如果该规则能变为空(ε\varepsilon),就把这条变为空的规则填到该大写字母的后跟字符(FOLLOW集里所有字符)对应的格子里。
  • 题目:对以下规则,计算每个代号的 FIRST 和 FOLLOW 集合,并构造预测分析表: S(L)aS , SSε , LSL , L,SLεS \to (L) \mid aS' \ , \ S' \to S \mid \varepsilon \ , \ L \to S L' \ , \ L' \to , S L' \mid \varepsilon

  • 解答

    1. 计算 FIRST 集合
      • SS 只能由第一个分支产生 ( 或第二个分支产生 a,所以:FIRST(S)={(,a}\text{FIRST}(S) = \{ (, a \}
      • SS' 可以变成 SS(即有 (, a),或者变为空 ε\varepsilon,所以:FIRST(S)={(,a,ε}\text{FIRST}(S') = \{ (, a, \varepsilon \}
      • LL 变成 SLS L',以 SS 开头,所以:FIRST(L)=FIRST(S)={(,a}\text{FIRST}(L) = \text{FIRST}(S) = \{ (, a \}
      • LL' 可以变成 ,,或者变为空 ε\varepsilon,所以:FIRST(L)={,,ε}\text{FIRST}(L') = \{ ,, \varepsilon \}
    2. 计算 FOLLOW 集合
      • SS 是起始代号,先把 \$$ 加入 \text{FOLLOW}(S)$。
      • 观察所有规则右侧含 SS 的地方:
        • SSS' \to S 中,右侧最末尾是 SS,故将 FOLLOW(S)\text{FOLLOW}(S') 放入 FOLLOW(S)\text{FOLLOW}(S)
        • LSLL \to S L' 中,后面紧跟 LL',故将 FIRST(L){ε}\text{FIRST}(L') \setminus \{\varepsilon\}(即 ,)放入 FOLLOW(S)\text{FOLLOW}(S)。因为 LL' 可变为空,所以要把 FOLLOW(L)\text{FOLLOW}(L) 也放入。
        • L,SLL' \to , S L' 中,同理。
      • 综合计算后,各集合为:
        • \text{FOLLOW}(S) = \{ \, ), , }$ (包括结束符、右括号、逗号)
        • \text{FOLLOW}(S') = \text{FOLLOW}(S) = \{ \, ), , }$
        • FOLLOW(L)={)}\text{FOLLOW}(L) = \{ ) \} (在 S(L)S \to (L) 中,LL 后面是右括号)
        • FOLLOW(L)=FOLLOW(L)={)}\text{FOLLOW}(L') = \text{FOLLOW}(L) = \{ ) \}
    3. 构造预测分析表
      代号a(),$
      SSSaSS \to aS'S(L)S \to (L)
      SS'SSS' \to SSSS' \to SSεS' \to \varepsilonSεS' \to \varepsilonSεS' \to \varepsilon
      LLLSLL \to S L'LSLL \to S L'
      LL'LεL' \to \varepsilonL,SLL' \to , S L'
  • 例题 2:考虑如下改写后的表达式文法:

    ExprExpr(Expr)Var ExprTailExprTailExprεVarid VarTailVarTail(Expr)ε\begin{aligned} \textit{Expr} &\to -\textit{Expr} \mid (\textit{Expr}) \mid \textit{Var} \ \textit{ExprTail} \\ \textit{ExprTail} &\to -\textit{Expr} \mid \varepsilon \\ \textit{Var} &\to \textit{id} \ \textit{VarTail} \\ \textit{VarTail} &\to (\textit{Expr}) \mid \varepsilon \end{aligned}
    1. 计算非终结符的 FIRST\text{FIRST}FOLLOW\text{FOLLOW}
    2. 构造 LL(1)\text{LL}(1) 预测分析表。
    3. 给出输入句子 id - - id ( ( id ) ) 对应的详细移进与预测分析栈步骤表(以 # 作为栈底)。
  • 解答

    1. 计算 FIRST 和 FOLLOW 集合

      • FIRST(Expr)={,(,id}\text{FIRST}(\textit{Expr}) = \{ -, (, \textit{id} \}
      • FIRST(ExprTail)={,ε}\text{FIRST}(\textit{ExprTail}) = \{ -, \varepsilon \}
      • FIRST(Var)={id}\text{FIRST}(\textit{Var}) = \{ \textit{id} \}
      • FIRST(VarTail)={(,ε}\text{FIRST}(\textit{VarTail}) = \{ (, \varepsilon \}
      • \text{FOLLOW}(\textit{Expr}) = \{ \, ) }$
      • \text{FOLLOW}(\textit{ExprTail}) = \text{FOLLOW}(\textit{Expr}) = \{ \, ) }$
      • \text{FOLLOW}(\textit{Var}) = (\text{FIRST}(\textit{ExprTail}) \setminus \{\varepsilon\}) \cup \text{FOLLOW}(\textit{Expr}) = \{ -, \, ) }$
      • \text{FOLLOW}(\textit{VarTail}) = \text{FOLLOW}(\textit{Var}) = \{ -, \, ) }$
    2. 构造预测分析表

      非终结符id-()$
      Expr\textit{Expr}ExprVar ExprTail\textit{Expr} \to \textit{Var} \ \textit{ExprTail}ExprExpr\textit{Expr} \to -\textit{Expr}Expr(Expr)\textit{Expr} \to (\textit{Expr})
      ExprTail\textit{ExprTail}ExprTailExpr\textit{ExprTail} \to -\textit{Expr}ExprTailε\textit{ExprTail} \to \varepsilonExprTailε\textit{ExprTail} \to \varepsilon
      Var\textit{Var}Varid VarTail\textit{Var} \to \textit{id} \ \textit{VarTail}
      VarTail\textit{VarTail}VarTailε\textit{VarTail} \to \varepsilonVarTail(Expr)\textit{VarTail} \to (\textit{Expr})VarTailε\textit{VarTail} \to \varepsilonVarTailε\textit{VarTail} \to \varepsilon
    3. 句子 id - - id ( ( id ) ) 的预测分析控制步骤表(使用分析栈与输入串配对,#为栈底):

      步骤符号栈 (从右至左压栈)剩余输入串所用产生式 / 动作
      1# Expr\textit{Expr}id - - id ( ( id ) ) #ExprVar ExprTail\textit{Expr} \to \textit{Var} \ \textit{ExprTail}
      2# ExprTail\textit{ExprTail} Var\textit{Var}id - - id ( ( id ) ) #Varid VarTail\textit{Var} \to \textit{id} \ \textit{VarTail}
      3# ExprTail\textit{ExprTail} VarTail\textit{VarTail} idid - - id ( ( id ) ) #匹配终结符 id (移进并消耗输入)
      4# ExprTail\textit{ExprTail} VarTail\textit{VarTail}- - id ( ( id ) ) #VarTailε\textit{VarTail} \to \varepsilon (因 - \in FOLLOW(VarTail)\text{FOLLOW}(\textit{VarTail}))
      5# ExprTail\textit{ExprTail}- - id ( ( id ) ) #ExprTailExpr\textit{ExprTail} \to -\textit{Expr}
      6# Expr\textit{Expr} -- - id ( ( id ) ) #匹配终结符 - (移进)
      7# Expr\textit{Expr}- id ( ( id ) ) #ExprExpr\textit{Expr} \to -\textit{Expr}
      8# Expr\textit{Expr} -- id ( ( id ) ) #匹配终结符 - (移进)
      9# Expr\textit{Expr}id ( ( id ) ) #ExprVar ExprTail\textit{Expr} \to \textit{Var} \ \textit{ExprTail}
      10# ExprTail\textit{ExprTail} Var\textit{Var}id ( ( id ) ) #Varid VarTail\textit{Var} \to \textit{id} \ \textit{VarTail}
      11# ExprTail\textit{ExprTail} VarTail\textit{VarTail} idid ( ( id ) ) #匹配终结符 id (移进)
      12# ExprTail\textit{ExprTail} VarTail\textit{VarTail}( ( id ) ) #VarTail(Expr)\textit{VarTail} \to (\textit{Expr}) (因 ( \in FIRST((Expr))\text{FIRST}((\textit{Expr})))
      13# ExprTail\textit{ExprTail} ) Expr\textit{Expr} (( ( id ) ) #匹配终结符 ( (移进)
      14# ExprTail\textit{ExprTail} ) Expr\textit{Expr}( id ) ) #Expr(Expr)\textit{Expr} \to (\textit{Expr})
      15# ExprTail\textit{ExprTail} ) ) Expr\textit{Expr} (( id ) ) #匹配终结符 ( (移进)
      16# ExprTail\textit{ExprTail} ) ) Expr\textit{Expr}id ) ) #ExprVar ExprTail\textit{Expr} \to \textit{Var} \ \textit{ExprTail}
      17# ExprTail\textit{ExprTail} ) ) ExprTail\textit{ExprTail} Var\textit{Var}id ) ) #Varid VarTail\textit{Var} \to \textit{id} \ \textit{VarTail}
      18# ExprTail\textit{ExprTail} ) ) ExprTail\textit{ExprTail} VarTail\textit{VarTail} idid ) ) #匹配终结符 id (移进)
      19# ExprTail\textit{ExprTail} ) ) ExprTail\textit{ExprTail} VarTail\textit{VarTail}) ) #VarTailε\textit{VarTail} \to \varepsilon (因 ) \in FOLLOW(VarTail)\text{FOLLOW}(\textit{VarTail}))
      20# ExprTail\textit{ExprTail} ) ) ExprTail\textit{ExprTail}) ) #ExprTailε\textit{ExprTail} \to \varepsilon (因 ) \in FOLLOW(ExprTail)\text{FOLLOW}(\textit{ExprTail}))
      21# ExprTail\textit{ExprTail} ) )) ) #匹配终结符 ) (移进)
      22# ExprTail\textit{ExprTail} )) #匹配终结符 ) (移进)
      23# ExprTail\textit{ExprTail}#ExprTailε\textit{ExprTail} \to \varepsilon (因 # \in FOLLOW(ExprTail)\text{FOLLOW}(\textit{ExprTail}))
      24##分析成功结束,句子被接受

模块四:自底向上语法分析

题型十一:项目集规范族、DFA 与分析表构造 (LR 家族)

如何做:

  1. 初始规则拓广:在所有规则的最前面,强行加一条规则 SSS' \to S(把初始大写字母用一个带撇的字母包起来),作为整套规则的新起点。
  2. 加圆点表示进度:在规则右侧的字符间塞入一个圆点 .,点在哪就表示我们读到了哪里。比如 EaAE \to a \cdot A 表示我们刚刚读完了具体符号 aa,接下来准备读代号 AA
  3. 求状态的扩展(闭包)
    • 如果圆点后面紧跟着一个大写字母,比如 AA,那就要把所有以 AA 开头的规则全部写进这个状态里。
    • 并且,写出来的这些新规则,它们的圆点必须放在最左边(例如 AcdA \to \cdot cd)。
  4. 状态转移
    • 看圆点后面紧贴的是什么符号(不管大写还是小写)。把圆点往右移过这个符号,得到一个新规则,作为新状态的起点,再重复第 3 步扩展它。
  5. 填分析驱动表
    • 如果圆点在最右边(如 AcdA \to cd\cdot),说明这一支读完了,可以进行“归约”(即把读到的具体符号往回合并成代号 AA),在表里填入 r加上规则编号
    • 如果圆点后面是具体符号(如小写字母 xx),且移位后去了状态 jj,就在表里填 s j
    • 如果圆点后面是大写字母(如 AA),且移位后去了状态 jj,就在跳转表(GOTO栏)里填数字 j
  • 题目:考虑规则 SE,EaA,AcAdS' \to E, E \to aA, A \to cA \mid d,构造 LR(0) 项目集规范族及识别活前缀的 DFA,并构造 LR(0) 分析表。

  • 解答

    1. 编号规则:(1) EaAE \to aA, (2) AcAA \to cA, (3) AdA \to d
    2. 构造状态集(圆点移位与闭包扩展)
      • 状态 0(初始): 先放起点的项目:SES' \to \cdot E。因为点后面是 EE,所以引入以 EE 开头的规则,点放最左:EaAE \to \cdot aA。 即状态 0 为:{SE,EaA}\{ S' \to \cdot E, E \to \cdot aA \}
      • 状态 1(状态 0 读 EE):点往右移: {SE}\{ S' \to E \cdot \}。 (这是接受状态 acc)
      • 状态 2(状态 0 读 aa):点往右移: EaAE \to a \cdot A。因为点后面是大写字母 AA,所以引入 AA 开头的规则: AcAA \to \cdot cA, AdA \to \cdot d。 即状态 2 为:{EaA,AcA,Ad}\{ E \to a \cdot A, A \to \cdot cA, A \to \cdot d \}
      • 状态 3(状态 2 读 AA):点往右移: {EaA}\{ E \to aA \cdot \}。 (读完了,按第 (1) 条规则归约)
      • 状态 4(状态 2 读 cc):点往右移: AcAA \to c \cdot A。点后面是 AA,引入 AA 的规则: AcAA \to \cdot cA, AdA \to \cdot d。 即状态 4 为:{AcA,AcA,Ad}\{ A \to c \cdot A, A \to \cdot cA, A \to \cdot d \}
      • 状态 5(状态 2 读 dd):点往右移: {Ad}\{ A \to d \cdot \}。 (读完了,按第 (3) 条规则归约)
      • 状态 6(状态 4 读 AA):点往右移: {AcA}\{ A \to cA \cdot \}。 (读完了,按第 (2) 条规则归约)
    3. LR(0) 分析驱动表
      状态acd$EA
      0s21
      1acc
      2s4s53
      3r1r1r1r1
      4s4s56
      5r3r3r3r3
      6r2r2r2r2
  • 题目 2 (SLR(1) 证明与表构造):证明文法 GGSAbaAc , AaS \to Ab \mid aAc \ , \ A \to a 不是 LR(0)\text{LR}(0) 文法而是 SLR(1)\text{SLR}(1) 文法,并给出 SLR(1)\text{SLR}(1) 分析表。

  • 解答

    1. 第一步:拓广文法并编号

      • (0) SSS' \to S
      • (1) SAbS \to Ab
      • (2) SaAcS \to aAc
      • (3) AaA \to a
    2. 第二步:构造带圆点的项目状态集

      • 状态 0SSS' \to \cdot S (点后面是 SS,引出 SS 的规则) SAbS \to \cdot Ab, SaAcS \to \cdot aAc (点后面是大写字母 AA 和小写字母 aa) AaA \to \cdot a (引入以 AA 开头的规则) 即状态 0 为:{SS,SAb,SaAc,Aa}\{ S' \to \cdot S, S \to \cdot Ab, S \to \cdot aAc, A \to \cdot a \}
      • 状态 1 (状态 0 读 SS):{SS}\{ S' \to S \cdot \} (接受态)
      • 状态 2 (状态 0 读 AA):{SAb}\{ S \to A \cdot b \}
      • 状态 3 (状态 0 读 aa): 点移位:SaAcS \to a \cdot AcAaA \to a \cdot。由于点后面有大写字母 AA,需引入以 AA 开头的规则且点放最左:AaA \to \cdot a。 即状态 3 为:{SaAc,Aa,Aa}\{ S \to a \cdot Ac, A \to \cdot a, A \to a \cdot \}
      • 状态 4 (状态 2 读 bb):{SAb}\{ S \to Ab \cdot \} (按规则 1 归约)
      • 状态 5 (状态 3 读 AA):{SaAc}\{ S \to aA \cdot c \}
      • 状态 6 (状态 3 读 aa 或状态 0 读 aa 后归入) —— 点移过 aa{Aa}\{ A \to a \cdot \} (按规则 3 归约)
      • 状态 7 (状态 5 读 cc):{SaAc}\{ S \to aAc \cdot \} (按规则 2 归约)
    3. 第三步:指出 LR(0) 冲突并判断不是 LR(0)

      • 观察状态 3,它同时包含了:
        • 移进项目:AaA \to \cdot a (当读到具体字符 aa 时,要把圆点右移)
        • 归约项目:AaA \to a \cdot (读到了最右侧,要按规则 3 归约)
      • 在同一个状态里,对于输入字符 aa,既可以移进,又可以归约,这产生了移进-归约冲突
      • 结论:因为存在冲突,所以该文法不是 LR(0)\text{LR}(0) 文法。
    4. 第四步:利用 FOLLOW 集消解冲突,证明是 SLR(1)

      • 我们求大写字母 AA 后面可能紧跟的具体字符集合 FOLLOW(A)\text{FOLLOW}(A)
        • 在规则 SAbS \to Ab 中,AA 后面是具体符号 bb
        • 在规则 SaAcS \to aAc 中,AA 后面是具体符号 cc
        • 所以,FOLLOW(A)={b,c}\text{FOLLOW}(A) = \{ b, c \}
      • 冲突发生时,移进的字符是 aa。由于 aa 不在 FOLLOW(A)\text{FOLLOW}(A) 集合中,说明当面临输入字符 aa 时,我们绝对不可能把当前栈顶内容归约为 AA(因为如果归约成 AA,后面就不可能合法地接上 aa)。
      • 因此,在面临输入 aa 时,我们只能选择移进;只有在面临输入 bbcc 时,我们才选择归约
      • 结论:移进-归约冲突被成功解决,文法是 SLR(1)\text{SLR}(1) 文法。
    5. 第五步:构造 SLR(1) 分析表

      • 对于归约项目 AaA \to a \cdot(状态 3 和状态 6),我们只在属于 FOLLOW(A)={b,c}\text{FOLLOW}(A) = \{b, c\} 的列中填入归约动作 r3

      SLR(1) 分析表

      状态abc$SA
      0s312
      1acc
      2s4
      3s6r3r35
      4r1
      5s7
      6r3r3
      7r2
  • 例题 3 (SLR(1)/LR(1)/LALR(1) 冲突判定进阶): 考虑文法 SASb , ASAaS \to AS \mid b \ , \ A \to SA \mid a

    1. 列出该文法的所有 LR(0)\text{LR}(0) 项目。
    2. 构造 LR(0)\text{LR}(0) 项目集规范族及识别活前缀的 DFA。
    3. 判定该文法是否是 SLR(1)\text{SLR}(1) 文法,若是,构造其预测分析表;若不是,说明理由。
    4. 该文法是 LALR(1)\text{LALR}(1)LR(1)\text{LR}(1) 的吗?说明理由。
  • 解答

    1. 拓广文法并写出所有 LR(0) 项目: 拓广文法引入 SSS' \to S。共 12 个项目: (1) SSS' \to \cdot S (2) SSS' \to S \cdot (3) SASS \to \cdot AS (4) SASS \to A \cdot S (5) SASS \to AS \cdot (6) SbS \to \cdot b (7) SbS \to b \cdot (8) ASAA \to \cdot SA (9) ASAA \to S \cdot A (10) ASAA \to SA \cdot (11) AaA \to \cdot a (12) AaA \to a \cdot

    2. 构造项目集规范族与 DFA 转移关系

      • I0={SS,SAS,Sb,ASA,Aa}I_0 = \{ S' \to \cdot S, S \to \cdot AS, S \to \cdot b, A \to \cdot SA, A \to \cdot a \}
      • I1=GO(I0,S)={SS,ASA,SAS,Sb,ASA,Aa}I_1 = \text{GO}(I_0, S) = \{ S' \to S \cdot, A \to S \cdot A, S \to \cdot AS, S \to \cdot b, A \to \cdot SA, A \to \cdot a \}
      • I2=GO(I0,A)={SAS,SAS,Sb,ASA,Aa}I_2 = \text{GO}(I_0, A) = \{ S \to A \cdot S, S \to \cdot AS, S \to \cdot b, A \to \cdot SA, A \to \cdot a \}
      • I3=GO(I0,a)={Aa}I_3 = \text{GO}(I_0, a) = \{ A \to a \cdot \}
      • I4=GO(I0,b)={Sb}I_4 = \text{GO}(I_0, b) = \{ S \to b \cdot \}
      • I5=GO(I1,A)={ASA,SAS,SAS,Sb,ASA,Aa}I_5 = \text{GO}(I_1, A) = \{ A \to SA \cdot, S \to A \cdot S, S \to \cdot AS, S \to \cdot b, A \to \cdot SA, A \to \cdot a \}
      • I6=GO(I1,S)={ASA,SAS,Sb,ASA,Aa}I_6 = \text{GO}(I_1, S) = \{ A \to S \cdot A, S \to \cdot AS, S \to \cdot b, A \to \cdot SA, A \to \cdot a \}
      • I7=GO(I2,S)={SAS,ASA,SAS,Sb,ASA,Aa}I_7 = \text{GO}(I_2, S) = \{ S \to AS \cdot, A \to S \cdot A, S \to \cdot AS, S \to \cdot b, A \to \cdot SA, A \to \cdot a \}

      DFA 状态转移表

      状态接收 SS接收 AA接收 aa接收 bb
      I0I_0I1I_1I2I_2I3I_3I4I_4
      I1I_1I6I_6I5I_5I3I_3I4I_4
      I2I_2I7I_7I2I_2I3I_3I4I_4
      I3I_3----
      I4I_4----
      I5I_5I7I_7I2I_2I3I_3I4I_4
      I6I_6I6I_6I5I_5I3I_3I4I_4
      I7I_7I6I_6I5I_5I3I_3I4I_4
    3. 判定是否是 SLR(1)

      • 计算非终结符的 FOLLOW 集:
        • \text{FOLLOW}(S) = \{ \, a, b }$
        • FOLLOW(A)={a,b}\text{FOLLOW}(A) = \{ a, b \}
      • 观察项目集 I5I_5,其中包含:
        • 归约项目:ASAA \to SA \cdot(当面临输入属于 FOLLOW(A)={a,b}\text{FOLLOW}(A) = \{a, b\} 时归约)
        • 移进项目:在 I5I_5 下,读入终结符 aa 移进到 I3I_3,读入 bb 移进到 I4I_4
      • 因为 FOLLOW(A){a,b}={a,b}\text{FOLLOW}(A) \cap \{a, b\} = \{a, b\} \neq \emptyset,所以在面临输入字符 aabb 时,分析器既可以移进,又可以归约,存在移进-归约冲突
      • 结论:因为存在冲突,所以该文法不是 SLR(1)\text{SLR}(1) 文法。
    4. 判定是否是 LALR(1) / LR(1)

      • 结论:它既不是 LALR(1)\text{LALR}(1) 文法,也不是 LR(1)\text{LR}(1) 文法。
      • 原因证明:该文法是一个二义性文法。对于同一个句子(例如 abab),可以构造出两棵不同的语法树:
        • 树 1SASa(AS)a(SAS)a(baS)ababS \to AS \to a (AS) \to a (SAS) \to a (baS) \to abab
        • 树 2SAS(SA)S(ASA)S(abA)bababS \to AS \to (SA)S \to (ASA)S \to (abA)b \to abab 由于二义性文法在 LR 类分析器中必然存在无法消解的移进-归约或归约-归约冲突,因此该文法绝不可能是 LR(1)\text{LR}(1)LALR(1)\text{LALR}(1) 文法。
  • 例题 4 (LR(1) 与 LALR(1) 状态合并机制讲解): 考虑经典的非 SLR(1) 文法 G[S]G[S]SL=RR , LRid , RLS \to L=R \mid R \ , \ L \to *R \mid \text{id} \ , \ R \to L 解释为何 SLR(1) 会产生冲突,以及 LR(1) 和 LALR(1) 如何利用向前看搜索符消解该冲突。

  • 解答

    1. SLR(1) 冲突原因
      • 计算其 FOLLOW\text{FOLLOW} 集有 \text{FOLLOW}(R) = \{ \, = }$。
      • 在 LR(0) 状态集构建中,会产生一个状态 I2={SL=R , RL}I_2 = \{ S \to L \cdot = R \ , \ R \to L \cdot \}
      • 当输入为 = 时,在 I2I_2 下:
        • 根据移进项目 SL=RS \to L \cdot = R,应当移进
        • 根据归约项目 RLR \to L \cdot,因为 =FOLLOW(R)= \in \text{FOLLOW}(R),在 SLR(1) 规则下应当归约
        • 这在 = 处产生了移进-归约冲突,故文法不是 SLR(1)\text{SLR}(1)
    2. LR(1) 的消解机制
      • LR(1) 项目的形式为 [Aαβ,a][A \to \alpha \cdot \beta, a],其中 aa 是向前看搜索符(由逆向分析 FIRST\text{FIRST} 集而来)。
      • 在 LR(1) 中,初始状态 I0I_0 为:
        • [S' \to \cdot S, \]$
        • [S \to \cdot L = R, \]$
        • [S \to \cdot R, \]$
        • [L \to \cdot *R, = / \](因为(因为L后面是后面是=R或继承或继承$$)
        • [L \to \cdot \text{id}, = / \]$
        • [R \to \cdot L, \](因为(因为R后面是继承后面是继承$$)
      • 当从 I0I_0 读入 LL 转移到 I2I_2 时,得到项目集:
        • [S \to L \cdot = R, \]$
        • [R \to L \cdot, \]$
      • 消解效果:观察归约项目 [R \to L \cdot, \],其向前看搜索符仅仅是,其向前看搜索符**仅仅是 $$**,而不包含 =
      • 此时,若面临输入 =,分析器只能选择移进(因为移进 = 属于项目 [S \to L \cdot = R, \]的动作);若面临输入的动作);若面临输入$$,才选择归约。移进-归约冲突被成功消解!
    3. LALR(1) 的合并同心集
      • LR(1) 状态机往往有几百个状态,有很多状态的 LR(0) 核心相同,只有搜索符不同(同心集)。
      • LALR(1) 将这些同心状态进行合并,例如将 [R \to L \cdot, \][R \to L \cdot, =]合并为合并为[R \to L \cdot, = / $]$。
      • 合并搜索符后,LALR(1) 状态数与 LR(0) 一致,在此文法中合并后依然没有冲突,成功证明该文法是 LALR(1) 的。

题型十二:移进-归约过程追踪

如何做:

  1. 准备四列:状态栈(里面只存状态数字,刚开始是 0)、符号栈(存读到的符号,刚开始是 $)、输入串(待处理的句子,末尾加 $)、动作。
  2. 查表走步
    • 看状态栈最顶上的那个数字(例如当前栈顶是 2),和输入串最左边的第一个字符(例如当前是 c)。
    • 去刚才做好的分析表里查对应的格子(交叉点 [2, c])。
    • 如果格子里写着 s4
      • 把当前字符 c 从输入串里删掉,塞入符号栈。
      • 把数字 4 塞入状态栈。
      • 动作记为“移进”。
    • 如果格子里写着 r3(说明要用第 3 条规则归约,假设第 3 条规则是 AdA \to d):
      • 规则右边有几个符号(这里只有 1 个符号 dd),就从符号栈和状态栈的顶部各弹出几个元素(这里各弹出 1 个)。
      • 把规则左边的大写字母(这里是 AA)塞入符号栈。
      • 此时看一眼状态栈顶剩下的数字(假设是 2),查分析表里的 GOTO 栏(格子 [2, A],假设填的是 3)。把这个数字 3 塞入状态栈。
      • 动作记为“用规则 3 归约”。
  3. 重复执行,直到动作出现 acc(接受),说明句子完全符合规则。
  • 题目 1:利用上题构造的分析表,给出句子 accd 的移进-归约分析详细步骤。

  • 解答

    步骤状态栈符号栈输入串动作
    10$accd$读入首字符 a,查表 [0, a] 得到 s2,移进
    20 2$accd$读入 c,查表 [2, c] 得到 s4,移进
    30 2 4$accd$读入 c,查表 [4, c] 得到 s4,移进
    40 2 4 4$accd$读入 d,查表 [4, d] 得到 s5,移进
    50 2 4 4 5$accd$查表 [5, $] 得到 r3(第3条规则是 AdA \to d)。右边长为1,两栈各弹1个元素。符号栈压 AA,查 [4, A] 得到 6,状态栈压 6。用 AdA \to d 归约
    60 2 4 4 6$accA$查表 [6, $] 得到 r2(第2条规则是 AcAA \to cA)。右边长为2,两栈各弹2个元素。符号栈压 AA,查 [4, A] 得到 6,状态栈压 6。用 AcAA \to cA 归约
    70 2 4 6$acA$查表 [6, $] 得到 r2(第2条规则是 AcAA \to cA)。右边长为2,两栈各弹2个元素。符号栈压 AA,查 [2, A] 得到 3,状态栈压 3。用 AcAA \to cA 归约
    80 2 3$aA$查表 [3, $] 得到 r1(第1条规则是 EaAE \to aA)。右边长为2,两栈各弹2个元素。符号栈压 EE,查 [0, E] 得到 1,状态栈压 1。用 EaAE \to aA 归约
    90 1$E$查表 [1, $] 得到 acc。分析成功
  • 例题 2:对于文法 Sa(T) , TT,SSS \to a \mid \wedge \mid (T) \ , \ T \to T, S \mid S,给出句子 (((a,a),,(a)),a)(((a, a), \wedge, (a)), a) 的规范归约过程(最右推导的逆过程,每一步写出当前句型及归约句柄)。

  • 解答: 规范归约就是每次找到句型中最左侧的直接短语(即句柄),查规则将其归约为左边的非终结符。 详细步骤如下:

    1. 当前句型为:(((a, a), ∧, (a)), a)。最左侧的直接短语是第一个 a,使用 SaS \to a 归约。
      • 句型(((S, a), ∧, (a)), a)句柄为:a
    2. 接下来最左侧直接短语是第二个 a,使用 SaS \to a 归约。
      • 句型(((S, S), ∧, (a)), a)句柄为:a
    3. 接下来最左侧直接短语是第一个 S,使用 TST \to S 归约。
      • 句型(((T, S), ∧, (a)), a)句柄为:S
    4. 接着 T, S 一步归约为 TT
      • 句型(((T), ∧, (a)), a)句柄为:T, S
    5. 接着最左直接短语是 (T),使用 S(T)S \to (T) 归约。
      • 句型((S, ∧, (a)), a)句柄为:(T)
    6. 接着最左直接短语是第一个 S,使用 TST \to S 归约。
      • 句型((T, ∧, (a)), a)句柄为:S
    7. 接着最左直接短语是 ,使用 SS \to \wedge 归约。
      • 句型((T, S, (a)), a)句柄为:
    8. 接着 T, S 一步归约为 TT
      • 句型((T, (a)), a)句柄为:T, S
    9. 接着最左直接短语是括号里的 a,使用 SaS \to a 归约。
      • 句型((T, (S)), a)句柄为:a
    10. 接着最左直接短语是 S,使用 TST \to S 归约。
      • 句型((T, (T)), a)句柄为:S
    11. 接着最左直接短语是 (T),使用 S(T)S \to (T) 归约。
      • 句型((T, S), a)句柄为:(T)
    12. 接着 T, S 一步归约为 TT
      • 句型((T), a)句柄为:T, S
    13. 接着最左直接短语是 (T),使用 S(T)S \to (T) 归约。
      • 句型(S, a)句柄为:(T)
    14. 接着最左直接短语是 S,使用 TST \to S 归约。
      • 句型(T, a)句柄为:S
    15. 接着最左直接短语是最后一个 a,使用 SaS \to a 归约。
      • 句型(T, S)句柄为:a
    16. 接着 T, S 一步归约为 TT
      • 句型(T)句柄为:T, S
    17. 最终 (T) 一步归约为开始符号 SS
      • 句型S句柄为:(T)
      • 归约成功!

模块五:语义分析、语法制导翻译与中间代码

题型十三:语法制导定义(SDD)与属性计算

如何做:

  1. 理解属性:属性就是挂在代号(大写字母)身上的值,比如计算出来的长度、结果数值等。
  2. 综合属性(自底向上计算):如果一个代号的属性值,是直接根据它底下的分支子节点的属性值算出来的。
    • 计算顺序:先算出底层叶子的值,再一层层往上加,最后算出树顶(根部)的值。
  3. 属性计算步骤
    • 画出句子的代换树。
    • 从树的最底层开始,按照给定的数学公式算出每一个节点属性的值。
    • 顺着树枝往上代入,直到算出最顶上那个字母的属性。
  • 题目:代换规则如下,其语义规则定义了长度计算 S.len=A.len+2,A.len=A.len+2,A.len=0S.\text{len} = A.\text{len} + 2, A.\text{len} = A'.\text{len} + 2, A'.\text{len} = 0。求句子 a bbcc b(对应结构为 SaAbS \to aAb, AbAcA \to bA'c, AεA' \to \varepsilon)的属性计算步骤。

  • 解答

    1. 画出该句子的代换树结构:
      • SS 节点分支出 aa, AA, bb
      • AA 节点分支出 bb, AA', cc
      • AA' 节点分支出 ε\varepsilon
    2. 根据语义规则,自底向上代入计算:
      • 最底层 AA':其属性 A.len=0A'.\text{len} = 0
      • 中间层 AA:根据公式 A.len=A.len+2A.\text{len} = A'.\text{len} + 2,代入得 A.len=0+2=2A.\text{len} = 0 + 2 = 2
      • 最顶层 SS:根据公式 S.len=A.len+2S.\text{len} = A.\text{len} + 2,代入得 S.len=2+2=4S.\text{len} = 2 + 2 = 4
    3. 最终计算结果为 S.len=4S.\text{len} = 4
  • 例题 2 (S-属性文法设计与属性计算): 设计一个全综合属性的 S-属性文法,能够自底向上计算二进制小数(例如 101.101)对应的十进制数值,给出产生式和对应的语义规则,并写出计算 101.101 值的详细步骤。

  • 解答

    1. 设计属性

      • 为单个二进制位 BB 引入属性 B.valB.val(对应值 0 或 1)。
      • 为二进制串 LL 引入属性 L.valL.val(对应部分计算出的十进制数值)以及 L.lenL.len(对应的二进制位串长度,用于计算小数时的权重移动)。
      • 为开始符号 SS 引入属性 S.valS.val(最终十进制数值)。
    2. 构造 S-属性语法制导定义 (SDD)

      产生式语义规则
      SL1.L2S \to L_1 . L_2S.val=L1.val+L2.val×2L2.lenS.val = L_1.val + L_2.val \times 2^{-L_2.len}
      SLS \to LS.val=L.valS.val = L.val
      LL1BL \to L_1 BL.val=L1.val×2+B.valL.val = L_1.val \times 2 + B.val
      L.len=L1.len+1L.len = L_1.len + 1
      LBL \to BL.val=B.valL.val = B.val
      L.len=1L.len = 1
      B0B \to 0B.val=0B.val = 0
      B1B \to 1B.val=1B.val = 1
    3. 计算 101.101 的具体步骤(自底向上)

      • 整数部分 L1=101L_1 = 101
        • 最左边位 B11B1.val=1B_1 \to 1 \Rightarrow B_1.val = 1
        • 归约为单字符串 LaB1La.val=B1.val=1,La.len=1L_a \to B_1 \Rightarrow L_a.val = B_1.val = 1, L_a.len = 1
        • 第二位 B20B2.val=0B_2 \to 0 \Rightarrow B_2.val = 0
        • 归约为两位串 LbLaB2Lb.val=La.val×2+B2.val=1×2+0=2,Lb.len=La.len+1=2L_b \to L_a B_2 \Rightarrow L_b.val = L_a.val \times 2 + B_2.val = 1 \times 2 + 0 = 2, L_b.len = L_a.len + 1 = 2
        • 第三位 B31B3.val=1B_3 \to 1 \Rightarrow B_3.val = 1
        • 归约为三位串 L1LbB3L1.val=Lb.val×2+B3.val=2×2+1=5,L1.len=Lb.len+1=3L_1 \to L_b B_3 \Rightarrow L_1.val = L_b.val \times 2 + B_3.val = 2 \times 2 + 1 = 5, L_1.len = L_b.len + 1 = 3
      • 小数部分 L2=101L_2 = 101(运算规则与整数部分相同):
        • 经过同样的推导,计算出 L2.val=5,L2.len=3L_2.val = 5, L_2.len = 3
      • 整句归约 SL1.L2S \to L_1 . L_2
        • 根据定义:S.val=L1.val+L2.val×2L2.lenS.val = L_1.val + L_2.val \times 2^{-L_2.len}
        • 代入数据:S.val=5+5×23=5+5×0.125=5.625S.val = 5 + 5 \times 2^{-3} = 5 + 5 \times 0.125 = 5.625
        • 最终十进制结果为 5.6255.625

题型十四:逆波兰式(后缀式)与三地址代码转换

如何做:

  1. 逆波兰式(后缀表达式)
    • 平时我们写的算式是“运算符在中间”,如 a+ba + b。逆波兰式要求把“运算符移到后面”,变成 ab+a b +
    • 遇到括号时,先算括号里面的。括号本身在最后的结果里不写出来
    • 复杂的式子可以先画成一棵运算树(运算符在交叉点上,数字在树叶上),然后按照“左分叉、右分叉、最后运算符”的顺序把它们写下来。
  2. 三地址四元式
    • 把复杂的长算式拆成一步步只包含两个运算数的简短式子,每一步算完的值存入一个临时变量(如 T1T_1, T2T_2)。
    • 每一行写成四个位置的格式:(操作符, 第一个运算数, 第二个运算数, 结果临时变量)。如果是一元运算(如单目减法 C-C),没有第二个运算数,就在第三个位置写下划线 _
  • 题目 1:给出赋值语句 A:=B(C+D)A := B * ( - C + D ) 生成的四元式序列。

  • 解答

    1. 观察优先级,最先计算括号内的单目减法 C-C。写成四元式,存入临时变量 T1T_1(-, C, _, T1) (注意:单目减没有第二个操作数,用下划线占位)
    2. 接着计算 T1+DT_1 + D,存入 T2T_2(+, T1, D, T2)
    3. 接着计算括号外面的乘法,即 BT2B * T_2,存入 T3T_3(*, B, T2, T3)
    4. 最后把乘出来的结果 T3T_3 赋值给变量 AA(:=, T3, _, A)
  • 例题 2 (算术/布尔表达式后缀式转换): 写出以下两个表达式的逆波兰(后缀)表示形式:

    1. a(b+c)a * ( - b + c ) (特别注意单目减号处理)
    2. not A or not (C or not D)\text{not } A \text{ or not } ( C \text{ or not } D )
  • 解答

    1. 对于 a(b+c)a * ( - b + c )
      • 单目减号 - 在这里只对 bb 起作用,可以用特殊的单目负操作符 uminus(或 @)表示。
      • 运算树的根节点是 *,其左子树为 a,右子树为括号内的 ( -b + c )(根节点为 +,左子树为 -b,右子树为 c)。
      • 后缀式按照“左儿子、右儿子、根”的深度遍历:
        • 首先遍历 a
        • 然后遍历右侧,先写 b 和单目负 uminus 得到 b uminus;再写右子树 c;最后写运算 +。得到 b uminus c +
        • 最后输出根操作符 *
      • 最终逆波兰式a b uminus c + a \ b \ \text{uminus} \ c \ + \ * (注:考试中若不特别要求单目负表示,也可简化写为:a b  c + a \ b \ - \ c \ + \ *)
    2. 对于 not A or not (C or not D)\text{not } A \text{ or not } ( C \text{ or not } D )
      • 根据算符优先级,单目操作符 not 的优先级高于双目操作符 or
      • 逐步转换后缀式:
        • 第一个项 not A\text{not } A 转换为:A not
        • 括号内的 C or not DC \text{ or not } D 转换为:C D not or
        • 括号外的 not ()\text{not } ( \dots ) 作用于其上,转换为:C D not or not
        • 最后将前半截和后半截使用根部的 or 连接。
      • 最终逆波兰式A not C D not or not orA \ \text{not} \ C \ D \ \text{not} \ \text{or} \ \text{not} \ \text{or}

题型十五:控制流语句的四元式序列生成

如何做:

  1. 短路计算
    • B1 or B2B_1 \text{ or } B_2:如果前面的条件 B1B_1 成立,就不用看后面的条件 B2B_2 了,直接跳去真目标。
    • B1 and B2B_1 \text{ and } B_2:如果前面的条件 B1B_1 不成立,直接判定为假,不用看 B2B_2,跳去假目标。
  2. 跳转四元式的写法
    • 条件跳转:比如“若 a<ba < b 则跳到第 103 行”,写成:(j<, a, b, 103)。如果不满足,就直接顺着执行下一行。
    • 无条件跳转:直接跳到某一行,写成:(j, _, _, 106)
  3. 行号规划
    • 假设从指定的行号(如 100)开始写,每写一行行号加 1。
    • 遇到需要跳转的目标行号,如果还没写到那一行,可以先空着,等写到目标行后再把行号倒回去填上(回填)。
  • 题目 1:把程序段 while a < b do if c < d then x := y + z 翻译成四元式代码,假设起始行号为 100,采用短路计算。

  • 解答

    1. 100行:这是 while 循环判定条件的开始。我们先测试条件 a<ba < b,如果满足,就跳进循环体内(假设跳到 102 行开始判断 if 条件);如果不满足,循环结束,直接跳出循环(跳到最后一行的下一行,假设是 107 行): 100: (j<, a, b, 102) 101: (j, _, _, 107)
    2. 102行:开始判断 if c < d 条件。如果满足,执行 then 后面的赋值(假设是 104 行);如果不满足,if 结束,无操作,继续下一次循环(回到 100 行): 102: (j<, c, d, 104) 103: (j, _, _, 100)
    3. 104行:执行加法赋值 x := y + z。算完之后,因为是循环体,必须无条件跳回最开始的 100 行进行下一次判定: 104: (+, y, z, T1) 105: (:=, T1, _, x) 106: (j, _, _, 100)
    4. 检查发现,循环结束的出口行应该在 106 行之后,即 107 行。回填 101 行的假跳转目标为 107:
      • 完整四元式序列
        100: (j<, a, b, 102)
        101: (j, _, _, 107)
        102: (j<, c, d, 104)
        103: (j, _, _, 100)
        104: (+, y, z, T1)
        105: (:=, T1, _, x)
        106: (j, _, _, 100)
        107: (nop)
  • 例题 2 (复杂嵌套控制流与短路计算四元式生成): 将如下带有嵌套 while 循环和 and 逻辑运算的控制流语句翻译为四元式序列。假设起始行号为 100,布尔表达式采用短路计算:

    while A < C and B < D do
        if A = 1 then
            C := C + 1
        else
            while A <= D do
                A := A + 2;
  • 解答

    1. 设计分析逻辑(带回填机制)

      • 行 100:开始评估外层 while 的第一个条件 A < C。如果为真,跳转到 102 行评估第二个条件;如果为假,说明条件短路失败,跳出整个外层循环(跳转目标待定为出口行 114)。
      • 行 102:评估外层 while 的第二个条件 B < D。如果为真,说明整个 and 条件成立,跳入循环体(即 104 行的 if 判断);如果为假,退出循环(跳到 114 行)。
      • 行 104:外层循环体内,评估 if A = 1。若为真,跳转到 then 分支的计算(106 行);若为假,跳转到 else 分支(即内层 while 循环起始的 109 行)。
      • 行 106-107then 分支,计算 C := C + 1。计算完后,代表外层循环的一轮体结束,必须无条件跳转回 100 行。
      • 行 109else 分支,开始评估内层 while 条件 A <= D。如果为真,跳入内层循环体(111 行);如果为假,退出内层循环,因为外层循环体在此也结束了,所以直接跳回 100 行。
      • 行 111-112:内层循环体内,计算 A := A + 2。计算完后,无条件跳转回内层条件评估点(109 行)。
      • 行 114:外层循环出口。
    2. 生成完整四元式序列

      序号操作符运算数 1运算数 2结果 (跳转目标 / 临时变量)说明
      100j<AACC102102判断条件 A<CA < C(真则看下一个)
      101j__114114条件不满足,退出外层循环
      102j<BBDD104104判断条件 B<DB < D(真则入循环体)
      103j__114114条件不满足,退出外层循环
      104j=AA11106106外层循环体首:判断 A=1A = 1(真则去 then)
      105j__109109假则去 else 分支(内层循环)
      106+CC11T1T_1then 分支T1=C+1T_1 = C + 1
      107:=T1T_1_CC赋值给 CC
      108j__100100结束后跳转回外层循环开头
      109j<=AADD111111else/内层循环开始:判断 ADA \le D
      110j__100100内层循环结束,返回外层循环开头
      111+AA22T2T_2内层循环体T2=A+2T_2 = A + 2
      112:=T2T_2_AA赋值给 AA
      113j__109109结束后跳转回内层循环开头
      114nop___外层循环退出点

模块六:代码优化

题型十六:基本块划分与控制流图 (CFG) 构造

如何做:

  1. 基本块(一气呵成执行的代码段)
    • 基本块的特点是“一旦从入口进去,就必须一气呵成地执行到出口,中间不能跳走,也不能从别的路插进来”。
  2. 第一步,找到所有块的入口语句(也叫 Leader)
    • 规则一:第一行语句是一个入口。
    • 规则二:任何跳转语句(有条件或无条件跳转)的目标语句是一个入口。
    • 规则三:紧跟在跳转语句后面的那一行语句是一个入口。
  3. 第二步,切分基本块
    • 从每一个入口语句开始往下数,一直到下一个入口语句的前一行,切开,这就是一个独立的基本块。
  4. 第三步,画连接图 (CFG)
    • 把每一个块画成一个方框。
    • 画带箭头的连线:如果执行完块 1,有可能顺着走到块 2,或者通过块 1 末尾的跳转跳到块 2,就画一条从块 1 指向块 2 的箭头。
  • 题目:对下面的程序,划分基本块,并画出其控制流图:
    (1) A := 0
    (2) B := 1
    (3) L1: A := A + B
    (4) if B >= C goto L2
    (5) B := B + 1
    (6) goto L1
    (7) L2: write A
    (8) halt
  • 解答
    1. 寻找入口语句 (Leader)
      • 根据规则一:语句 (1) 是第一行,故 (1) 是入口
      • 根据规则二:
        • 语句 (4) 中的跳转目标是 L2,即语句 (7),故 (7) 是入口
        • 语句 (6) 中的跳转目标是 L1,即语句 (3),故 (3) 是入口
      • 根据规则三:
        • 语句 (4) 是条件跳转,紧跟其后的是语句 (5),故 (5) 是入口
        • 语句 (6) 是跳转,紧跟其后的是语句 (7)(已是入口)。
      • 所有的入口语句为:(1), (3), (5), (7)
    2. 切分基本块
      • 基本块 B1(从入口 (1) 到下一个入口 (3) 之前):包含 (1), (2)。
      • 基本块 B2(从入口 (3) 到下一个入口 (5) 之前):包含 (3), (4)。
      • 基本块 B3(从入口 (5) 到下一个入口 (7) 之前):包含 (5), (6)。
      • 基本块 B4(从入口 (7) 到结尾):包含 (7), (8)。
    3. 画控制流图 (CFG)
      • 块 B1 执行完后,直接顺着走到块 B2。连线:B1 -> B2
      • 块 B2 结尾的条件跳转,若满足跳去 L2(即块 B4);若不满足,直接顺着走到块 B3。连线:B2 -> B3B2 -> B4
      • 块 B3 结尾无条件跳转去 L1(即块 B2)。连线:B3 -> B2
      • 连成的控制流图为:
        graph TD
            B1["基本块 B1<br>(1)-(2)"] --> B2["基本块 B2<br>(3)-(4)"]
            B2 -->|不满足跳转| B3["基本块 B3<br>(5)-(6)"]
            B2 -->|满足跳转| B4["基本块 B4<br>(7)-(8)"]
            B3 --> B2
Profile Image of the Author
Sonder
好想要技术
这是公告标题
这只是一个公告
分类
标签
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.16.7
文章许可
CC BY-NC-SA 4.0
文章目录