跳转到内容

Lesson 51: LL(1) 预测分析表构造与语法解析

练习任务

难度:中

用位掩码不动点迭代计算 FIRST/FOLLOW 集,自动构建预测分析表,实现表驱动 LL(1) 语法解析器,解析表达式 id + id * id。你需要完成七个核心组件:

  1. token_id() — 将词法字符串映射为终结符枚举值("id"→0, "+"→1, ...)
  2. 栈操作push/pop/empty/stack_str 四个栈操作函数
  3. compute_first() — 位掩码不动点迭代计算所有非终结符的 FIRST 集
  4. compute_follow() — 位掩码不动点迭代计算所有非终结符的 FOLLOW 集
  5. build_table() — 由 FIRST/FOLLOW 集自动构建二维预测分析表 table[NT][T]
  6. print_sets() — 按指定格格式打印 FIRST 和 FOLLOW 集
  7. LL(1) 解析主循环 — 使用预测分析表驱动栈式解析

验证方式:make test 编译运行,通过 diff 比对 expected_output.txt

=== LL(1) Table-Driven Parser ===
Grammar: E→TE' E'→+TE'|ε T→FT' T'→*FT'|ε F→id|(E)
Input:   id + id * id

=== FIRST Sets ===
FIRST(E) = {id, (}
FIRST(E') = {+}
FIRST(T) = {id, (}
FIRST(T') = {*}
FIRST(F) = {id, (}

=== FOLLOW Sets ===
FOLLOW(E) = {), $}
FOLLOW(E') = {), $}
FOLLOW(T) = {+, ), $}
FOLLOW(T') = {+, ), $}
FOLLOW(F) = {+, *, ), $}

[00] $E                 id+id*id$        E  → T E'
[01] $E'T               id+id*id$        T  → F T'
...
[17]                                     ACCEPT

提示:LL(1) 的核心洞察是——每一步根据栈顶的语法符号和当前输入符号,查表决定使用哪条产生式。表由 FIRST/FOLLOW 自动生成,解析循环是通用引擎。学会这一课,你就理解了 yacc/bison 等解析器生成工具的底层原理。


核心知识点

  • LL(1) 架构 — Left-to-right scan, Leftmost derivation, 1 lookahead。栈 + 输入缓冲 + 预测分析表的三角协作
  • 位掩码表示 — 用 int 的 6 个 bit 分别表示 6 个终结符。HAS(mask, t)ADD(mask, t) 宏实现 O(1) 集合操作
  • RHS 逆序编码 — 每个产生式的 RHS 符号按逆序存储(最右侧符号在索引 0),rhs_at(p, i) 实现正向索引映射。LIFO 压栈天然配合逆序
  • FIRST 不动点迭代 — 反复扫描产生式、传播非 ε 终结符,直到没有新元素加入。可空符号(E', T')不阻断传播
  • FOLLOW 不动点迭代 — 两条规则:FOLLOW(B) ⊇ FIRST(β)-{ε},若 β 可空则 FOLLOW(B) ⊇ FOLLOW(A)。开始符号的 FOLLOW 初始含 $
  • 预测分析表构造 — 对每个产生式 A→α:FIRST(α) 中的终结符填入产生式编号;若 α 可推导出 ε,FOLLOW(A) 中的终结符也填入
  • 表驱动解析循环 — while 栈非空:栈顶是终结符则匹配消耗,是非终结符则查表展开。解析逻辑与文法完全分离

代码框架

ll1_parser.c
c
#include <stdio.h>
#include <string.h>

#define STACK_SZ 64
#define PROD_CNT 8

/* ─── 符号定义 ─── */
enum { T_ID, T_PLUS, T_STAR, T_LPAR, T_RPAR, T_EOF, T_COUNT = 6 };
enum { NT_E, NT_Ee, NT_T, NT_Te, NT_F, NT_COUNT = 5 };
enum { SYM_TERM, SYM_NONTERM };

typedef struct { int type; int id; } Sym;

const char *tname[T_COUNT] = {"id", "+", "*", "(", ")", "$"};
const char *nname[NT_COUNT] = {"E", "E'", "T", "T'", "F"};

const char *prod_label[PROD_CNT] = {
    "E  → T E'", "E' → + T E'", "E' → ε",
    "T  → F T'", "T' → ε", "T' → * F T'",
    "F  → id", "F  → ( E )",
};

/* RHS 展开序列:终结符用枚举值 (0-5), 非终结符 = id+100, -1=结束
 * 逆序存储:rhs[p][0] 是最右侧符号,rhs[p][last] 是最左侧符号 */
#define NT(x) ((x) + 100)
int rhs[PROD_CNT][4] = {
    {NT(NT_Ee), NT(NT_T), -1},         /* 0: E  → T E'      */
    {NT(NT_Ee), NT(NT_T), T_PLUS, -1}, /* 1: E' → + T E'    */
    {-1},                              /* 2: E' → ε          */
    {NT(NT_Te), NT(NT_F), -1},         /* 3: T  → F T'      */
    {-1},                              /* 4: T' → ε          */
    {NT(NT_Te), NT(NT_F), T_STAR, -1}, /* 5: T' → * F T'    */
    {T_ID, -1},                        /* 6: F  → id         */
    {T_RPAR, NT(NT_E), T_LPAR, -1},    /* 7: F  → ( E )      */
};

int prod_lhs[PROD_CNT] = {NT_E, NT_Ee, NT_Ee, NT_T, NT_Te, NT_Te, NT_F, NT_F};

/* ─── FIRST / FOLLOW / 预测分析表(由学生填充) ─── */
int first[NT_COUNT], follow[NT_COUNT], table[NT_COUNT][T_COUNT];

/* ─── 位掩码辅助宏 ─── */
#define HAS(bitmask, t) ((bitmask) & (1 << (t)))
#define ADD(bitmask, t) ((bitmask) |= (1 << (t)))

/* ─── 工具函数(已提供) ─── */
static int rhs_len(int p) { int len = 0; while (rhs[p][len] != -1) len++; return len; }

static int rhs_at(int p, int i) { return rhs[p][rhs_len(p) - 1 - i]; }

static int is_terminal(int sym_id) { return sym_id >= 0 && sym_id < T_COUNT; }

static void input_str(int *tokens, int pos, int n, char *buf) {
    int p = 0;
    for (int i = pos; i < n; i++) {
        const char *s = tname[tokens[i]];
        while (*s) buf[p++] = *s++;
    }
    buf[p] = '\0';
}

static void set_str(int mask, char *buf) {
    int p = 0;
    buf[p++] = '{';
    int first_item = 1;
    for (int t = 0; t < T_COUNT; t++) {
        if (HAS(mask, t)) {
            if (!first_item) { buf[p++] = ','; buf[p++] = ' '; }
            first_item = 0;
            const char *s = tname[t];
            while (*s) buf[p++] = *s++;
        }
    }
    buf[p++] = '}'; buf[p] = '\0';
}

/* ─── TODO 1: token_id ─┌─────────────────────────── */
static int token_id(const char *s) {
    // 遍历 tname 数组,strcmp 比较后返回枚举值
}

/* ─── TODO 2: 栈操作 ─┌───────────────────────────── */
Sym stack[STACK_SZ];
int top_idx = -1;

static void push(int type, int id)   { /* Sym s = {type, id}; stack[++top_idx] = s; */ }
static Sym pop(void)                 { /* return stack[top_idx--]; */ }
static int empty(void)               { /* return top_idx < 0; */ }
static void stack_str(char *buf)     { /* 遍历 stack, 按 type 拼接 tname/nname */ }

/* ─── TODO 3: compute_first ─────────────────────── */
static void compute_first(void) {
    // 初始化 first[] = 0
    // do-while(changed) 循环:遍历产生式,传播非 ε 终结符
    // E' 和 T' 可空(不阻断传播),其他非终结符不可空
}

/* ─── TODO 4: compute_follow ────────────────────── */
static void compute_follow(void) {
    // 初始] = follow[] = 0, ADD(follow[NT_E], T_EOF)
    // do-while(changed): 规则1 FIRST(β), 规则2 FOLLOW(A) 传播
}

/* ─── TODO 5: build_table ───────────────────────── */
static void build_table(void) {
    // 初始化 table[nt][t] = -1;对每个产生式 p: A→α
    // 若 α=ε: 对 b∈FOLLOW(A), table[A][b]=p
    // 否则: 对 a∈FIRST(α), table[A][a]=p;若 α⇒*ε 还填 FOLLOW(A)
}

/* ─── TODO 6: print_sets ────────────────────────── */
static void print_sets(void) {
    // 用 set_str() 打印 FIRST 集,空一行,再打印 FOLLOW 集
}

/* ─── 主解析循环 ─── */
int main(void) {
    char *input[] = {"id", "+", "id", "*", "id", "$", NULL};
    int tokens[16], n = 0;
    for (int i = 0; input[i]; i++) tokens[n++] = token_id(input[i]);

    compute_first();
    compute_follow();
    build_table();

    printf("=== LL(1) Table-Driven Parser ===\n");
    printf("Grammar: E→TE' E'→+TE'|ε T→FT' T'→*FT'|ε F→id|(E)\n");
    printf("Input:   id + id * id\n\n");

    print_sets();

    push(SYM_TERM, T_EOF);
    push(SYM_NONTERM, NT_E);

    /* TODO 7: LL(1) 解析主循环 */
    return 0;
}

阅读骨架后,尝试自己填充所有 TODO 标记的部分。核心挑战:rhs_at 的索引映射逻辑是什么?compute_firstdo-while(changed) 如何检测收敛?compute_followbeta_all_nullable 标志位如何维护?build_table 中 ε 产生式为什么需要查 FOLLOW 集?解析主循环中栈顶是终结符和非终结符的处理有何不同?


深度讲解

1. LL(1) 解析器架构

LL(1) 是自顶向下语法分析的经典算法:

  LL(1) = Left-to-right scan + Leftmost derivation + 1 lookahead token

核心思想:每一步根据栈顶的语法符号和当前输入符号,查表决定使用哪条产生式

┌──────────────────────────────────────────────────────────┐
                    LL(1) 解析器架构
├──────────────────────────────────────────────────────────┤
   输入缓冲区                  栈(LIFO)
  ┌───┬───┬───┬───┬───┬───┐  ┌───────┐
  │id + │id * │id $    E 栈顶
  └───┴───┴───┴───┴───┴───┘   $    栈底
                    └───────┘
    前瞻指针 (ip)                                          │
              预测分析表 table[NT][T]
              ┌─────┬─────┬─────┬─────┐
 id  +  * ...
         ┌────┼─────┼─────┼─────┼─────┤
 E  0  -  - ...
 E' │  -  │  1  │  -  │ ... │                    │
│         │ T  │  3  │  -  │  -  │ ... │                    │
│         └────┴─────┴─────┴─────┴─────┘                    │
└──────────────────────────────────────────────────────────┘

解析循环伪代码:

while (栈非空):
    X = 栈顶符号, a = 当前输入符号
    if X 是终结符:
        if X == a: pop X, ip++
        else: ERROR
    if X 是非终结符:
        p = table[X][a]
        if p == -1: ERROR
        else: pop X, 按产生式 p 的 RHS 压栈(从右到左)

2. 文法与 RHS 编码

文法定义(消除左递归后)

E T E'     (0)         T  → F T'     (3)
E' → + T E'   (1)         T' → * F T'   (5)
E' → ε        (2)         T' ε        (4)
                          F ( E )    (7)
                          F id       (6)

为什么需要 E' 和 T'? 原始文法 E → E + T | T 含左递归,会导致递归下降解析器无限循环。引入 E' 和 T' 消除左递归后,文法满足 LL(1) 要求。

RHS 逆序编码

RHS 数组使用不同编码区分终结符和非终结符,且逆序存储

值范围       含义             示例
0 ~ 5       终结符枚举值      T_ID(0), T_PLUS(1)
100 ~ 104   非终结符(NT+100)  NT(NT_E)=100, NT(NT_T)=102
-1          结束标记

示例:E' → + T E' RHS = {NT_E'+100, NT_T+100, T_PLUS, -1}
                            = {101, 102, 1, -1}
                           ↑                    ↑
                     最右侧符号(E')            ← 最右侧符号(+)

压栈顺序(i=0 -1):
  i=0: 101 push(E')  ← 最右侧先压入(栈底)
  i=1: 102 → push(T)   ← 中间
  i=2: 1   → push(+)   ← 最左侧最后压入(栈顶)

IMPORTANT

正向向读取 RHS 符号必须使用 rhs_at(p, i)return rhs[p][len - 1 - i]。直接索引 rhs[p][i] 会得到错误的符号顺序。但压栈时直接遍历 rhs[pid] 即可,因为逆序正是 LIFO 所需。

可空性判断

非终结符可空?原因
EE→TE',T 不可空
E'E'→ε 产生式
TT→FT',F 不可空
T'T'→ε 产生式
FF→id 或 F→(E),均以终结符开头

本练习中可空性固定,可硬编码:X_id != NT_Ee && X_id != NT_Te

3. FIRST 集——不动点迭代

位掩码表示与算法

每个 FIRST 集用一个 int 位掩码表示,6 个终结符映射到 6 个 bit:

bit 0→id  bit 1→+  bit 2→*  bit 3→(  bit 4→)  bit 5→$
HAS(mask, t): (mask) & (1 << (t))
ADD(mask, t): (mask) |= (1 << (t))

不动点迭代算法:

初始化:first[NT] =

重复直到收敛:
  for 每个产生式 A → X1 X2 ... Xn:
    for i = 1 to n:
      if Xi 是终结符 a:  first[A] ∪= {a}; break
      else:  first[A] ∪= first[Xi];  if Xi 不可空: break

迭代过程追踪

初始: first[E]=∅, first[E']=∅, first[T]=∅, first[T']=∅, first[F]=∅

第1轮:  F→id first[F]={id};  F→(E) → first[F]={id,(}
第2轮:  T→FT' → first[T]={id,(};  E→TE' first[E]={id,(}
第3轮:  E'→+TE' first[E']={+};  T'→*FT' → first[T']={*}
第4轮:  无变化 收敛

代码实现要点

c
static void compute_first(void) {
    for (int i = 0; i < NT_COUNT; i++) first[i] = 0;
    int changed;
    do {
        changed = 0;
        for (int p = 0; p < PROD_CNT; p++) {
            int A = prod_lhs[p], len = rhs_len(p);
            for (int i = 0; i < len; i++) {
                int X = rhs_at(p, i);
                if (is_terminal(X)) {
                    if (!HAS(first[A], X)) { ADD(first[A], X); changed = 1; }
                    break;
                } else {
                    int X_id = X - 100;
                    for (int t = 0; t < T_COUNT; t++)
                        if (HAS(first[X_id], t) && !HAS(first[A], t))
                            { ADD(first[A], t); changed = 1; }
                    if (X_id != NT_Ee && X_id != NT_Te) break;
                }
            }
        }
    } while (changed);
}

CAUTION

终结符后必须 break——终结符不可空,不能继续处理后续符号。否则 FIRST 集会错误地包含不应有的终结符。

4. FOLLOW 集——传播与可空性

FOLLOW(A) = { a | S ⇒* ...Aa..., a 是终结符 }。对每个产生式 A → αBβ

规则1: FOLLOW(B)  FIRST(β) - {ε}
规则2: β* ε,FOLLOW(B)  FOLLOW(A)

迭代过程(关键步骤)

初始: follow[E]={$}, 其他=∅

产生式 0 (E→TE'):
  B=T, β=E': first[E']={+}(规则1), E'可空(规则2)  follow[T]={+,$}
  B=E', β=∅(规则2) → follow[E']={$}

产生式 1 (E'→+TE'):
  B=T, β=E': first[E']={+}, E'可空 → follow[T]∪={+}, follow[T]∪=follow[E']
  B=E', β=∅ → 无变化

... 继续迭代:F→(E)中 B=E β=) → follow[E]∪={)}
  第2轮: follow[E]={),$}, follow[E']获取{)} → follow[E']={),$}
  第2轮: 产生式7 (F→(E)): 规则2→follow[E]∪=follow[F] 但有)=first[E])...
  继续迭代... 最终收敛

最终结果:

非终结符FIRSTFOLLOW
E
E'
T
T'
F

CAUTION

FOLLOW 迭代中最容易出错:beta_all_nullable 只在 β 中所有符号都可空时才为真。有一个终结符或不可空非终结符就立即置 0 并 break

c
static void compute_follow(void) {
    for (int i = 0; i < NT_COUNT; i++) follow[i] = 0;
    ADD(follow[NT_E], T_EOF);
    int changed;
    do {
        changed = 0;
        for (int p = 0; p < PROD_CNT; p++) {
            int A = prod_lhs[p], len = rhs_len(p);
            for (int i = 0; i < len; i++) {
                int X = rhs_at(p, i);
                if (is_terminal(X)) continue;
                int B = X - 100, beta_all_nullable = 1;
                for (int j = i + 1; j < len; j++) {
                    int Y = rhs_at(p, j);
                    if (is_terminal(Y)) {
                        if (!HAS(follow[B], Y)) { ADD(follow[B], Y); changed = 1; }
                        beta_all_nullable = 0; break;
                    } else {
                        int Y_id = Y - 100;
                        for (int t = 0; t < T_COUNT; t++)
                            if (HAS(first[Y_id], t) && !HAS(follow[B], t))
                                { ADD(follow[B], t); changed = 1; }
                        if (Y_id != NT_Ee && Y_id != NT_Te)
                            { beta_all_nullable = 0; break; }
                    }
                }
                if (beta_all_nullable)
                    for (int t = 0; t < T_COUNT; t++)
                        if (HAS(follow[A], t) && !HAS(follow[B], t))
                            { ADD(follow[B], t); changed = 1; }
            }
        }
    } while (changed);
}

5. 预测分析表构造

对每个产生式 A → α

规则1: a FIRST(α)  table[A][a] = p
规则2: α* ε, b FOLLOW(A)  table[A][b] = p

构造过程示例:

E→TE':    FIRST(TE')={id,(} table[E][id]=0, table[E][(]=0
E'→ε:     FOLLOW(E')={),$}    → table[E'][)]=2, table[E'][$]=2
E'→+TE':  FIRST(+TE')={+}     → table[E'][+]=1
T'→ε:     FOLLOW(T')={+,),$}  → table[T'][+]=4, table[T'][)]=4, table[T'][$]=4
T'*FT':  FIRST(*FT')={*}     → table[T'][*]=5
F→id:     FIRST(id)={id}      → table[F][id]=6
F→(E):    FIRST((E))={(}      → table[F][(]=7

完整预测分析表:

id+*()$
E00
E'122
T33
T'4544
F67

(数字 = 产生式编号,— = -1 表示语法错误)

c
static void build_table(void) {
    for (int nt = 0; nt < NT_COUNT; nt++)
        for (int t = 0; t < T_COUNT; t++) table[nt][t] = -1;
    for (int p = 0; p < PROD_CNT; p++) {
        int A = prod_lhs[p], len = rhs_len(p);
        if (len == 0) {
            for (int t = 0; t < T_COUNT; t++)
                if (HAS(follow[A], t)) table[A][t] = p;
            continue;
        }
        int nullable_prod = 1;
        for (int i = 0; i < len; i++) {
            int X = rhs_at(p, i);
            if (is_terminal(X)) { table[A][X] = p; nullable_prod = 0; break; }
            int X_id = X - 100;
            for (int t = 0; t < T_COUNT; t++)
                if (HAS(first[X_id], t)) table[A][t] = p;
            if (X_id != NT_Ee && X_id != NT_Te) { nullable_prod = 0; break; }
        }
        if (nullable_prod)
            for (int t = 0; t < T_COUNT; t++)
                if (HAS(follow[A], t)) table[A][t] = p;
    }
}

WARNING

先初始化 table[][] = -1!未显式填充的表项可能是随机值,导致不可预测行为。

6. 解析主循环——完整追踪

输入 id + id * id $ 的完整解析过程:

Step   栈(栈底→栈顶)         输入                动作
────   ─────────────────────   ─────────────────   ────────────
[00]   $E                       id+id*id$           E  → T E'
[01]   $E'T                     id+id*id$           T  → F T'
[02]   $E'T'F                   id+id*id$           F  → id
[03]   $E'T'id                  id+id*id$           match id
[04]   $E'T'                    +id*id$             T' → ε
[05]   $E'                      +id*id$             E' → + T E'
[06]   $E'T+                    +id*id$             match +
[07]   $E'T                     id*id$              T  → F T'
[08]   $E'T'F                   id*id$              F  → id
[09]   $E'T'id                  id*id$              match id
[10]   $E'T'                    *id$                T' → * F T'
[11]   $E'T'F*                  *id$                match *
[12]   $E'T'F                   id$                 F  → id
[13]   $E'T'id                  id$                 match id
[14]   $E'T'                    $                   T' → ε
[15]   $E'                      $                   E' → ε
[16]   $                        $                   match $
[17]                                                ACCEPT ✓

Step 0:栈 $ E(栈顶 E),查 table[E][id]=0 → E→TE'。pop E,压入 T 和 E'(rhs 逆序遍历)。 Step 4:栈顶 T'、输入 +,查 table[T'][+]=4 → T'→ε。pop T',不压入。 Step 16-17:栈只剩 $,输入只剩 $。match → pop → 栈空 → ACCEPT。

IMPORTANT

解析逻辑与文法完全分离——文法信息全部编码在 table 中。换一个 LL(1) 文法,只需重新计算表,解析循环不需修改。这就是 yacc/bison 的核心原理。


参考解答

完整解答:ll1_parser.c(仅 TODO 部分,工具函数保持不变)
solution_51_ll1_parser.c
c
/* TODO 1: token_id — 字符串映射到终结符枚举 */
static int token_id(const char *s) {
    for (int i = 0; i < T_COUNT; i++)
        if (strcmp(s, tname[i]) == 0) return i;
    return -1;
}

/* TODO 2: 栈操作 */
Sym stack[STACK_SZ];
int top_idx = -1;

static void push(int type, int id) {
    Sym s = {type, id};
    stack[++top_idx] = s;
}

static Sym pop(void) { return stack[top_idx--]; }

static int empty(void) { return top_idx < 0; }

static void stack_str(char *buf) {
    int p = 0;
    for (int i = 0; i <= top_idx; i++) {
        const char *s = (stack[i].type == SYM_TERM)
            ? tname[stack[i].id] : nname[stack[i].id];
        while (*s) buf[p++] = *s++;
    }
    buf[p] = '\0';
}

/* TODO 3: compute_first — 不动点迭代 */
static void compute_first(void) {
    for (int i = 0; i < NT_COUNT; i++) first[i] = 0;
    int changed;
    do {
        changed = 0;
        for (int p = 0; p < PROD_CNT; p++) {
            int A = prod_lhs[p], len = rhs_len(p);
            for (int i = 0; i < len; i++) {
                int X = rhs_at(p, i);
                if (is_terminal(X)) {
                    if (!HAS(first[A], X)) { ADD(first[A], X); changed = 1; }
                    break;
                } else {
                    int X_id = X - 100;
                    for (int t = 0; t < T_COUNT; t++)
                        if (HAS(first[X_id], t) && !HAS(first[A], t))
                            { ADD(first[A], t); changed = 1; }
                    if (X_id != NT_Ee && X_id != NT_Te) break;
                }
            }
        }
    } while (changed);
}

/* TODO 4: compute_follow — 不动点迭代 */
static void compute_follow(void) {
    for (int i = 0; i < NT_COUNT; i++) follow[i] = 0;
    ADD(follow[NT_E], T_EOF);
    int changed;
    do {
        changed = 0;
        for (int p = 0; p < PROD_CNT; p++) {
            int A = prod_lhs[p], len = rhs_len(p);
            for (int i = 0; i < len; i++) {
                int X = rhs_at(p, i);
                if (is_terminal(X)) continue;
                int B = X - 100, beta_all_nullable = 1;
                for (int j = i + 1; j < len; j++) {
                    int Y = rhs_at(p, j);
                    if (is_terminal(Y)) {
                        if (!HAS(follow[B], Y)) { ADD(follow[B], Y); changed = 1; }
                        beta_all_nullable = 0; break;
                    } else {
                        int Y_id = Y - 100;
                        for (int t = 0; t < T_COUNT; t++)
                            if (HAS(first[Y_id], t) && !HAS(follow[B], t))
                                { ADD(follow[B], t); changed = 1; }
                        if (Y_id != NT_Ee && Y_id != NT_Te)
                            { beta_all_nullable = 0; break; }
                    }
                }
                if (beta_all_nullable)
                    for (int t = 0; t < T_COUNT; t++)
                        if (HAS(follow[A], t) && !HAS(follow[B], t))
                            { ADD(follow[B], t); changed = 1; }
            }
        }
    } while (changed);
}

/* TODO 5: build_table — 预测分析表 */
static void build_table(void) {
    for (int nt = 0; nt < NT_COUNT; nt++)
        for (int t = 0; t < T_COUNT; t++) table[nt][t] = -1;
    for (int p = 0; p < PROD_CNT; p++) {
        int A = prod_lhs[p], len = rhs_len(p);
        if (len == 0) {
            for (int t = 0; t < T_COUNT; t++)
                if (HAS(follow[A], t)) table[A][t] = p;
            continue;
        }
        int nullable_prod = 1;
        for (int i = 0; i < len; i++) {
            int X = rhs_at(p, i);
            if (is_terminal(X)) { table[A][X] = p; nullable_prod = 0; break; }
            int X_id = X - 100;
            for (int t = 0; t < T_COUNT; t++)
                if (HAS(first[X_id], t)) table[A][t] = p;
            if (X_id != NT_Ee && X_id != NT_Te) { nullable_prod = 0; break; }
        }
        if (nullable_prod)
            for (int t = 0; t < T_COUNT; t++)
                if (HAS(follow[A], t)) table[A][t] = p;
    }
}

/* TODO 6: print_sets — 打印 FIRST/FOLLOW */
static void print_sets(void) {
    char buf[64];
    printf("=== FIRST Sets ===\n");
    for (int nt = 0; nt < NT_COUNT; nt++) {
        set_str(first[nt], buf);
        printf("FIRST(%s) = %s\n", nname[nt], buf);
    }
    printf("\n=== FOLLOW Sets ===\n");
    for (int nt = 0; nt < NT_COUNT; nt++) {
        set_str(follow[nt], buf);
        printf("FOLLOW(%s) = %s\n", nname[nt], buf);
    }
    printf("\n");
}

/* TODO 7: LL(1) 解析主循环(main() 体中) */
    push(SYM_TERM, T_EOF);
    push(SYM_NONTERM, NT_E);

    int ip = 0, step = 0;
    char s_buf[64], i_buf[64];

    while (!empty()) {
        Sym s_top = stack[top_idx];
        int cur = tokens[ip];
        stack_str(s_buf); input_str(tokens, ip, n, i_buf);
        printf("[%02d] %-18s %-16s ", step, s_buf, i_buf);

        if (s_top.type == SYM_TERM) {
            if (s_top.id == cur) {
                printf("match %s\n", tname[cur]);
                pop(); ip++;
            } else { printf("ERROR\n"); return 1; }
        } else {
            int pid = table[s_top.id][cur];
            if (pid < 0) { printf("ERROR\n"); return 1; }
            printf("%s\n", prod_label[pid]);
            pop();
            for (int i = 0; rhs[pid][i] != -1; i++) {
                if (rhs[pid][i] >= 100) push(SYM_NONTERM, rhs[pid][i] - 100);
                else push(SYM_TERM, rhs[pid][i]);
            }
        }
        step++;
    }
    stack_str(s_buf); input_str(tokens, ip, n, i_buf);
    printf("[%02d] %-18s %-16s ACCEPT\n", step, s_buf, i_buf);

工具函数(rhs_len, rhs_at, is_terminal, input_str, set_str)和全局数据定义均保持不变。以上实现完整填入 7 个 TODO,输出与 expected_output.txt 完全一致。

对照检查compute_first 中终结符后 break 了吗?compute_followbeta_all_nullable 在 β 全可空时正确传播了 FOLLOW(A) 吗?build_table 先全填了 -1 吗?ε 产生式正确查了 FOLLOW(A) 吗?解析循环压栈时直接遍历 rhs[pid](不用 rhs_at)了吗?


课堂讨论

  1. 为什么 LL(1) 叫 "LL(1)" 而不是 "LL(k)"? 本题文法只需 1 个前瞻符号即可确定产生式。某些文法(如 if-then-else 悬空 else)需要 k>1。LL(k) 表的大小随 k 指数增长,实际中很少用 k>1。

  2. 如果文法不是 LL(1) 的会怎样? 构建预测分析表时出现"冲突"——同一 table[A][a] 被两个产生式填充。如 E → id | id ( E )table[E][id] 冲突。解决:提取左因子或消除左递归。

  3. FIRST 迭代为什么一定能收敛? 每个 FIRST 集是有限集合(最多 T_COUNT 个元素),每次迭代只增增不减(单调递增)。有限集上的单调递增序列必然在有限步内停止。FOLLOW 同理。

  4. 位掩码的局限性是什么? int 只有 32 位,最多 32 个终结符。对大型文法(C 语言 100+ 关键字)需用 long long 或 bitset 数组。但本练习的 6 个终结符完美适配。

  5. 为什么不直接用递归下降? 递归下降更直观,但文法改变需重写代码。表驱动解析器把文法数据化——FIRST/FOLLOW/table 是数据,解析循环是通用引擎。这是 yacc/bison/ANTLR 的基础。

  6. 能否扩展为表达式求值器? 可以!在每个展开动作时执行语义动作:F→id 查符号表取值,展开完成时执行运算。这就是语法制导翻译(Syntax-Directed Translation)。


课后练习

  1. 扩展词法分析器。修改输入为从 stdin 读取(而非硬编码字符串数组),使解析器可处理任意输入。跳过空白字符。

  2. 添加语法错误恢复。当前遇到语法错误直接 return 1。实现恐慌模式(Panic Mode)恢复:跳过输入直到同步符号(如 )$),弹出栈直到可恢复。

    提示:恐慌模式伪代码
    if (pid < 0) {
        printf("ERROR: unexpected %s, skipping...\n", tname[cur]);
        ip++;
        while (!empty() && table[stack[top_idx].id][tokens[ip]] < 0)
            pop();
        continue;
    }
  3. 实现文法验证。编写 int is_ll1(void) 检查预测分析表是否有多重定义(同一表项被填入多个不同产生式)。发现冲突时打印位置和涉及的产生式。

  4. 打印分析表。编写 print_table() 以表格形式打印完整预测分析表。表头为终结符名称,每行一个非终结符,空位显示 -


参考资料

  • 《编译原理》(Dragon Book, Aho/Lam/Sethi/Ullman) 第 4.4 节 — LL(1) 语法与预测分析
  • 《Engineering a Compiler》(Cooper/Torczon) 第 3.3 节 — Top-Down Parsing
  • Wikipedia: LL parser — https://en.wikipedia.org/wiki/LL_parser
  • Crafting Interpreters (Robert Nystrom) 第 6 章 — Parsing Expressions
  • CS 6120: Advanced Compilers (Cornell) — 不动点迭代在编译器中的应用

"The parser is the heart of any compiler." — Alfred V. Aho

Released under the MIT License.