LL(1) 语法分析实战:C++ 实现 First/Follow 集与预测分析表(附 500 行源码)

LL(1) 语法分析器:从理论到 C++ 工程实践

在编译器的构建过程中,语法分析器扮演着至关重要的角色。LL(1) 作为最经典的自顶向下分析方法,以其清晰的逻辑结构和高效的线性时间复杂度,成为许多编译器前端的选择。本文将带你深入 LL(1) 分析器的实现细节,通过 500 行 C++ 代码的完整实现,揭示 First/Follow 集计算与预测分析表构建的核心算法。

1. LL(1) 分析器的核心架构

LL(1) 分析器的实现可分为三个关键模块:

  1. 文法预处理 :处理文法中的空产生式、左递归等问题
  2. 集合计算 :构建 First 集和 Follow 集
  3. 分析引擎 :基于预测分析表的语法分析

典型的类设计如下:

class Grammar {
    set<string> nonTerminals;  // 非终结符集合
    set<string> terminals;     // 终结符集合
    string startSymbol;        // 开始符号
    map<string, vector<vector<string>>> productions; // 产生式规则
    
    void eliminateLeftRecursion();
    void leftFactor();
};

class LL1Analyzer {
    map<string, set<string>> firstSets;
    map<string, set<string>> followSets;
    map<pair<string, string>, vector<string>> parsingTable;
    
    void computeFirstSets();
    void computeFollowSets();
    void buildParsingTable();
    
public:
    bool parse(const vector<string>& input);
};

2. First 集计算的两种实现范式

2.1 递归求解法

递归方法直接根据 First 集的定义实现:

set<string> LL1Analyzer::computeFirst(const string& symbol) {
    if (isTerminal(symbol)) {
        return {symbol};  // 终结符的 First 集是其本身
    }
    
    set<string> first;
    for (auto& production : productions[symbol]) {
        bool allNullable = true;
        for (auto& s : production) {
            auto sFirst = computeFirst(s);
            first.insert(sFirst.begin(), sFirst.end());
            
            if (sFirst.find("ε") == sFirst.end()) {
                allNullable = false;
                break;
            }
        }
        if (allNullable) {
            first.insert("ε");
        }
    }
    return first;
}

2.2 迭代求解法

迭代法通过不断传播 First 元素直到收敛:

void LL1Analyzer::computeFirstSets() {
    // 初始化
    for (auto& nt : nonTerminals) {
        firstSets[nt] = {};
    }
    
    bool changed;
    do {
        changed = false;
        for (auto& [head, bodies] : productions) {
            for (auto& body : bodies) {
                size_t oldSize = firstSets[head].size();
                
                bool nullable = true;
                for (auto& symbol : body) {
                    if (isTerminal(symbol)) {
                        firstSets[head].insert(symbol);
                        nullable = false;
                        break;
                    }
                    
                    firstSets[head].insert(
                        firstSets[symbol].begin(),
                        firstSets[symbol].end()
                    );
                    
                    if (firstSets[symbol].find("ε") == firstSets[symbol].end()) {
                        nullable = false;
                        break;
                    }
                }
                
                if (nullable) {
                    firstSets[head].insert("ε");
                }
                
                if (firstSets[head].size() > oldSize) {
                    changed = true;
                }
            }
        }
    } while (changed);
}

两种方法的对比:

特性 递归法 迭代法
实现复杂度 简单直观 需要显式处理收敛条件
性能 可能重复计算 效率更高
适用场景 小型文法 大型复杂文法
空产生式处理 需要额外标记 自然融入迭代过程

3. Follow 集计算的关键技巧

Follow 集计算需要特别注意文法开始符号和产生式右部末尾的情况:

void LL1Analyzer::computeFollowSets() {
    // 初始化
    for (auto& nt : nonTerminals) {
        followSets[nt] = {};
    }
    followSets[startSymbol].insert("$");  // 结束符号
    
    bool changed;
    do {
        changed = false;
        for (auto& [head, bodies] : productions) {
            for (auto& body : bodies) {
                for (size_t i = 0; i < body.size(); ++i) {
                    if (!isNonTerminal(body[i])) continue;
                    
                    string A = body[i];
                    size_t oldSize = followSets[A].size();
                    
                    // 情况1:A后面跟着符号B
                    if (i + 1 < body.size()) {
                        string B = body[i+1];
                        auto firstB = firstSets[B];
                        followSets[A].insert(firstB.begin(), firstB.end());
                        followSets[A].erase("ε");
                        
                        // 如果B可推出空串,需要加上Follow(head)
                        if (firstB.find("ε") != firstB.end()) {
                            followSets[A].insert(
                                followSets[head].begin(),
                                followSets[head].end()
                            );
                        }
                    } 
                    // 情况2:A是产生式最后一个符号
                    else {
                        followSets[A].insert(
                            followSets[head].begin(),
                            followSets[head].end()
                        );
                    }
                    
                    if (followSets[A].size() > oldSize) {
                        changed = true;
                    }
                }
            }
        }
    } while (changed);
}

注意:Follow 集计算必须等待 First 集完全计算完成后才能开始,且需要多次迭代直到不再变化。

4. 预测分析表的构建策略

预测分析表是 LL(1) 分析器的核心数据结构,我们使用 unordered_map 实现高效的二维查找:

void LL1Analyzer::buildParsingTable() {
    for (auto& [head, bodies] : productions) {
        for (auto& body : bodies) {
            auto firstAlpha = computeSequenceFirst(body);
            
            // 规则1:对于First(α)中的每个终结符a,加入M[A,a]
            for (auto& a : firstAlpha) {
                if (a != "ε") {
                    parsingTable[{head, a}] = body;
                }
            }
            
            // 规则2:如果ε在First(α)中,对Follow(A)的每个b加入M[A,b]
            if (firstAlpha.find("ε") != firstAlpha.end()) {
                for (auto& b : followSets[head]) {
                    parsingTable[{head, b}] = body;
                }
            }
        }
    }
    
    // 处理错误条目(可选)
    for (auto& nt : nonTerminals) {
        for (auto& t : terminals) {
            if (parsingTable.find({nt, t}) == parsingTable.end()) {
                parsingTable[{nt, t}] = {"error"};  // 同步记号
            }
        }
    }
}

预测分析表的可视化表示(以简单算术表达式文法为例):

非终结符 id + * ( ) $
E E→TE' E→TE'
E' E'→+TE' E'→ε E'→ε
T T→FT' T→FT'
T' T'→ε T'→*FT' T'→ε T'→ε
F F→id F→(E)

5. 语法分析引擎的实现

分析引擎使用栈结构管理推导过程:

bool LL1Analyzer::parse(const vector<string>& input) {
    stack<string> stk;
    stk.push("$");
    stk.push(startSymbol);
    
    size_t inputPtr = 0;
    string a = input[inputPtr];
    
    while (!stk.empty()) {
        string X = stk.top();
        stk.pop();
        
        if (isTerminal(X)) {
            if (X == a) {
                inputPtr++;
                a = inputPtr < input.size() ? input[inputPtr] : "$";
            } else {
                return false;  // 不匹配
            }
        } 
        else if (X == "$") {
            if (X == a) {
                return true;  // 成功
            } else {
                return false;
            }
        } 
        else {
            auto it = parsingTable.find({X, a});
            if (it == parsingTable.end() || it->second[0] == "error") {
                return false;  // 分析表无条目
            }
            
            // 逆序压栈
            const auto& production = it->second;
            if (!(production.size() == 1 && production[0] == "ε")) {
                for (auto it = production.rbegin(); it != production.rend(); ++it) {
                    stk.push(*it);
                }
            }
        }
    }
    
    return false;
}

分析过程的追踪输出示例:

步骤  栈         输入        动作
1     $E         id+id*id$  E → TE'
2     $E'T       id+id*id$  T → FT'
3     $E'T'F     id+id*id$  F → id
4     $E'T'id    id+id*id$  匹配id
5     $E'T'      +id*id$    T' → ε
6     $E'        +id*id$    E' → +TE'
7     $E'T+      +id*id$    匹配+
8     $E'T       id*id$     T → FT'
...

6. 工程实践中的优化技巧

6.1 避免递归死循环

在计算 First/Follow 集时,需要特别处理左递归文法:

void detectLeftRecursion() {
    for (auto& [head, bodies] : productions) {
        for (auto& body : bodies) {
            if (!body.empty() && body[0] == head) {
                throw runtime_error("直接左递归: " + head);
            }
        }
    }
}

6.2 内存优化策略

对于大型文法,可以采用以下优化:

// 使用位集表示First/Follow集
class FastSet {
    bitset<256> bits;  // 假设终结符不超过256个
public:
    void add(char c) { bits.set(c); }
    bool contains(char c) const { return bits.test(c); }
    // ... 其他集合操作
};

// 生产式编号替代完整产生式
map<pair<int, int>, int> compactParsingTable;

6.3 错误恢复机制

增强分析器的容错能力:

bool parseWithRecovery(const vector<string>& input) {
    // ...
    while (!stk.empty()) {
        // ... 正常分析逻辑
        
        if (分析表无条目) {
            // 恐慌模式恢复
            skipUntil(inputPtr, followSets[X]);
            stk.pop();  // 弹出X
            continue;
        }
    }
    // ...
}

7. 完整项目结构与测试案例

项目目录结构建议:

LL1-Parser/
├── include/
│   ├── grammar.h       # 文法定义与处理
│   ├── ll1_analyzer.h  # 核心分析器类
│   └── utils.h         # 工具函数
├── src/
│   ├── main.cpp        # 测试程序
│   └── ll1_analyzer.cpp
├── test/
│   ├── arithmetic.txt  # 算术表达式文法
│   └── test_cases/     # 测试用例
└── CMakeLists.txt

测试文法示例(arithmetic.txt):

E  -> T E'
E' -> + T E' | ε
T  -> F T'
T' -> * F T' | ε
F  -> ( E ) | id

编译与运行:

mkdir build && cd build
cmake .. && make
./ll1_parser ../test/arithmetic.txt "id+id*id"

8. 扩展与进阶方向

  1. 语法树生成 :在分析过程中构建具体/抽象语法树
  2. 错误诊断 :增强错误定位与提示能力
  3. 自动化测试 :构建文法测试框架
  4. 性能分析 :对大型文法进行性能剖析
// 语法树节点示例
class ASTNode {
    string type;
    string value;
    vector<ASTNode> children;
    
    void print(int indent = 0) const {
        cout << string(indent, ' ') << type;
        if (!value.empty()) cout << ": " << value;
        cout << endl;
        for (auto& child : children) {
            child.print(indent + 2);
        }
    }
};

实现完整的 LL(1) 分析器不仅需要理解形式语言理论,还需要考虑工程实践中的各种边界条件和性能因素。本文展示的代码框架已在 GitHub 开源项目中经过验证,能够正确处理教材中的典型文法案例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值