实验内容:
可选择LL1分析法、算符优先分析法、LR分析法之一,实现如下表达式文法的语法分析器:
(1)E→E+T | E-T | T
(2)T→T*F | T/F | F
(3)F→P^F | P
(4)P→(E) | i
实验目的:
1.掌握语法分析的基本概念和基本方法;
2.正确理解LL1分析法、算符优先分析法、LR分析法的设计与使用方法。
实验要求:
1.按要求设计实现能识别上述文法所表示语言的语法分析器,并要求输出全部分析过程;
2.要求详细描述所选分析方法针对上述文法的分析表构造过程;
3.完成对所设计语法分析器的功能测试,并给出测试数据和实验结果;
4.为增加程序可读性,请在程序中进行适当注释说明;
5.按软件工程管理模式完成实验报告撰写工作,最后需要针对实验过程进行经验总结;
6.认真完成并按时提交实验报告。
****** 实验报告内容详述 ******
一、 需求分析
LL(1) 分析法是一种自顶向下的预测分析方法,它从左到右扫描输入符号,并基于当前非终结符和下一个输入符号唯一地选择产生式,从而无需回溯就能高效解析符合该文法的输入字符串。由于 LL(1) 文法要求消除左递归、去除公共左因子并且无二义性,因此可以构造出简单清晰的预测分析表,便于手写解析器实现并在编译器中快速集成。对于大多数编程语言的语法子集,通过适当的左递归消除和左因子提取,通常可将其文法转换为 LL(1) 形式,从而在保留语法结构的同时获得可预测的解析性能。
与 LR(0) 相比,LL(1) 分析表的构造过程更直观,且在遇到不符合预期的符号时能够立即报错并指明具体的产生式,因而更易于调试。
此外,LL(1) 分析器占用内存结构简单、运行效率高,非常适合在资源受限或对性能要求严格的编译器前端场景中使用。因此,在本次实验中选用 LL(1) 分析法,可以兼顾实现难度低、运行效率高和错误定位清晰等多重优势。
二、 实现方法
表驱动(非递归)LL(1) 与自顶向下的递归下降在实现风格和适用场景上各有优劣。递归下降直接把每条文法产生式写成一个函数,通过函数调用和回溯来识别输入,代码可读性和可维护性较好;而表驱动解析则将文法信息(FIRST/FOLLOW、预测分析表)预先计算好,运行阶段用统一的栈和查表逻辑驱动,具有更好的性能、扩展性和自动化生成的潜力。
递归下降每个非终结符对应一个函数,函数体中用 if/else 或 switch 根据下一个输入符号选择产生式,并递归调用其他函数来匹配右部符号。文法必须先消除左递归、左因子化,才能避免无限递归或回溯;手工处理较繁琐,但逻辑直观。
表驱动先通过算法(或手工)计算出 FIRST/FOLLOW 集,并据此构造预测分析表;运行时采用栈和map<pair<NonTerminal, Terminal>, Production>进行驱动。其中,分析表通过键值对映射实现 —— 以(非终结符, 终结符)作为键,对应产生式作为值,等价于二维数组的查表逻辑。该结构支持动态查询,无需预先定义固定维度,在文法扩展时更具灵活性,同时保持表驱动解析的高效性。
三、 实验步骤
1、 针对本次实验的表达式语法,需要先做消除左递归处理。改写文法消除左递归后得到:
E → T E'
E' → + T E' | - T E' | ε
T → F T'
T' → * F T' | / F T' | ε
F → P F'
F' → ^ F | ε
P → ( E ) | i
2、 接下来进行FIRST和 FOLLOW的计算
FIRST集计算:
若产生式以终结符开头,直接加入FIRST集
若产生式以非终结符开头,递归加入其FIRST集
若产生式右部包含ε,需考虑后续符号的FIRST集
FIRST(E) = FIRST(T) = FIRST(F) = FIRST(P) = { (, i }
FIRST(E') = { +, -, ε }
FIRST(T') = { *, /, ε }
FIRST(F') = { ^, ε }
FOLLOW集计算:
对每个非终结符,追踪其在产生式中的位置,继承后续符号的FIRST集。
若某非终结符位于产生式末尾,继承左部非终结符的FOLLOW集。
FOLLOW(E) = { ), $ }
FOLLOW(E') = { ), $ }
FOLLOW(T) = { +, -, ), $ }
FOLLOW(T') = { +, -, ), $ }
FOLLOW(F) = { *, /, +, -, ), $ }
FOLLOW(F') = { *, /, +, -, ), $ }
FOLLOW(P) = { ^, *, /, +, -, ), $ }
3、 通过FIRST和FOLLOW集确定每个产生式的选择条件,构造预测分析表:
表格 1 预测分析表
非终结符 输入符号 产生式
E (,i E → T E'
E' + E' → + T E'
E' - E' → - T E'
E' ), $ E' → ε
T ( T → F T'
T i T → F T'
T' * T' → * F T'
T' / T' → / F T'
T' +, -, ), $ T' → ε
F ( F → P F'
F i F → P F'
F' ^ F' → ^ F
F' *, /, +, -, ), $ F' → ε
P ( P → ( E )
P i P → i
所有产生式的SELECT集互不相交,满足LL1文法要求。
四、 实现设计
1. 总体思路
本实验采用表驱动法实现 LL1语法分析器,核心思想是将文法的预测分析表(预测分析矩阵)预先构建好,然后在解析过程中仅通过不断查表和栈操作来驱动分析,避免了递归调用的复杂性,并且能够清晰地输出每一步匹配或规约的过程。
1) 初始化栈:[ $, E ]
2) 读取输入符号流(自动添加末尾 $)
3) 循环直到栈为空:
a. 栈顶为终结符:
与输入符号匹配 → 弹出栈顶,读取下一个输入。
不匹配 → 报错。
b. 栈顶为非终结符:
查表获取产生式 → 弹出栈顶,逆序压入右部符号。
表中无条目 → 报错。
4) 栈空且输入为 $ → 接受;否则 → 报错。
2. 输出分析过程
1) 每一步输出当前步骤编号、栈内容、剩余输入串、动作描述(匹配或应用哪个产生式),例:
栈: [E'] [T] [$] 输入: i + i * i
应用: E -> T E' (产生式 1)
2) 遇错时打印错误类型及位置。
3. 关键数据结构
1) 符号与产生式定义
// 定义终结符枚举类型
enum Terminal { PLUS,MINUS,MULT,DIV,POW,LPAREN,RPAREN,I,END };
// 定义非终结符枚举类型
enum NonTerminal { E, E_PRIME, T, T_PRIME, F, F_PRIME, P };
// 符号结构体(终结符或非终结符)
struct Symbol {
bool isTerminal;
int value;
Symbol(bool isTerminal, int value) : isTerminal(isTerminal), value(value) {}
};
// 产生式结构体
struct Production {
NonTerminal lhs;
vector<Symbol> rhs;
Production() = default;
Production(NonTerminal lhs, vector<Symbol> rhs) : lhs(lhs), rhs(rhs) {}
};
2) 预测分析表
通过 map<pair<NonTerminal, Terminal>, Production> 实现动态查表:
map<pair<NonTerminal, Terminal>, Production> table;
3) 分析栈
基于 std::stack<Symbol>,初始时从栈底依次压入 $(结束符)和起始符号 E
五、 测试与结果
1、 测试数据
序号 输入串 预期结果 说明
1 i 接受 单个标识符,验证基本符号匹配(对应产生式 P→i)
2 i+i 接受 加法表达式,验证E→E+T产生式及运算符优先级
3 i*i*i 接受 乘法连锁,验证T→T*F产生式及左结合性
4 i^i^i 接受 幂运算连锁,验证F→P^F产生式(右结合性:i^(i^i))
5 i+i*i 接受 混合运算,验证优先级(*高于+)
6 (i+i)*i 接受 括号改变优先级,验证P→(E)产生式
7 i+(i*i) 接受 嵌套括号,验证多层表达式解析
8 i-i/i 接受 减法与除法混合,验证E→E-T和T→T/F产生式
9 i^(i+i) 接受 幂运算与加法混合,验证F→P^F和E→E+T的优先级
10 ((i)) 接受 多重括号包裹,验证括号匹配逻辑
11 i+*i 错误 非法运算符连用(+*),验证错误处理机制
12 i+(i*i 错误 未闭合左括号,验证括号匹配错误检测
13 i+i) 错误 多余右括号,验证语法错误定位
14 i^+i 错误 运算符位置错误(^后无操作数),验证表达式结构合法性
15 i i 错误 标识符间无运算符,验证表达式结构完整性
2、 运行结果
1)
2)
3)
4)
5)
6)
7)
8)
9)
10)
11)
12)
13)
14)
15)
六、 经验与体会
通过本次实验,我深刻理解了 LL1 分析法作为自顶向下语法分析方法的核心原理。从理论到实践的过程中,我认识到 LL1 文法需满足消除左递归、提取公共左因子及无二义性的要求,而这些步骤并非孤立 —— 例如,消除左递归后的文法结构直接影响 FIRST/FOLLOW 集的计算,进而决定预测分析表的构造。以表达式文法E→E+T改写为E→T E'为例,这种结构调整使递归变为迭代,不仅消除了语法分析时的无限循环风险,还让 FIRST 集的推导更清晰(如FIRST(E')包含+、-和ε)。这让我明白,理论中的形式化定义需与工程实践结合,才能真正掌握其本质。
七、 源代码
#include <iostream>
#include <vector>
#include <stack>
#include <map>
#include <string>
using namespace std;
// 定义终结符枚举类型
enum Terminal {
PLUS, // +
MINUS, // -
MULT, // *
DIV, // /
POW, // ^
LPAREN, // (
RPAREN, // )
I, // i
END // $
};
// 定义非终结符枚举类型
enum NonTerminal {
E, // E
E_PRIME, // E'
T, // T
T_PRIME, // T'
F, // F
F_PRIME, // F'
P // P
};
// 符号结构体(终结符或非终结符)
struct Symbol {
bool isTerminal;
int value;
Symbol(bool isTerminal, int value) : isTerminal(isTerminal), value(value) {}
};
// 产生式结构体
struct Production {
NonTerminal lhs;
vector<Symbol> rhs;
Production() = default;
Production(NonTerminal lhs, vector<Symbol> rhs) : lhs(lhs), rhs(rhs) {}
};
// 词法分析器:将输入字符串转换为终结符序列
vector<Terminal> tokenize(const string& input) {
vector<Terminal> tokens;
for (char c : input) {
switch (c) {
case '+': tokens.push_back(PLUS); break;
case '-': tokens.push_back(MINUS); break;
case '*': tokens.push_back(MULT); break;
case '/': tokens.push_back(DIV); break;
case '^': tokens.push_back(POW); break;
case '(': tokens.push_back(LPAREN); break;
case ')': tokens.push_back(RPAREN); break;
case 'i': tokens.push_back(I); break;
default: throw runtime_error("非法字符: " + string(1, c));
}
}
tokens.push_back(END); // 添加结束符$
return tokens;
}
// 初始化LL(1)分析表
map<pair<NonTerminal, Terminal>, Production> buildParseTable() {
map<pair<NonTerminal, Terminal>, Production> table;
// 产生式定义
Production E_T_Eprime(E, {Symbol(false, T), Symbol(false, E_PRIME)});
Production Eprime_plus_T_Eprime(E_PRIME, {Symbol(true, PLUS), Symbol(false, T), Symbol(false, E_PRIME)});
Production Eprime_minus_T_Eprime(E_PRIME, {Symbol(true, MINUS), Symbol(false, T), Symbol(false, E_PRIME)});
Production Eprime_epsilon(E_PRIME, {});
Production T_F_Tprime(T, {Symbol(false, F), Symbol(false, T_PRIME)});
Production Tprime_mult_F_Tprime(T_PRIME, {Symbol(true, MULT), Symbol(false, F), Symbol(false, T_PRIME)});
Production Tprime_div_F_Tprime(T_PRIME, {Symbol(true, DIV), Symbol(false, F), Symbol(false, T_PRIME)});
Production Tprime_epsilon(T_PRIME, {});
Production F_P_Fprime(F, {Symbol(false, P), Symbol(false, F_PRIME)});
Production Fprime_pow_F(F_PRIME, {Symbol(true, POW), Symbol(false, F)});
Production Fprime_epsilon(F_PRIME, {});
Production P_lparen_E_rparen(P, {Symbol(true, LPAREN), Symbol(false, E), Symbol(true, RPAREN)});
Production P_i(P, {Symbol(true, I)});
// 填充分析表
// E → T E'
table[{E, LPAREN}] = E_T_Eprime;
table[{E, I}] = E_T_Eprime;
// E' → + T E' | - T E' | ε
table[{E_PRIME, PLUS}] = Eprime_plus_T_Eprime;
table[{E_PRIME, MINUS}] = Eprime_minus_T_Eprime;
table[{E_PRIME, RPAREN}] = Eprime_epsilon;
table[{E_PRIME, END}] = Eprime_epsilon;
// T → F T'
table[{T, LPAREN}] = T_F_Tprime;
table[{T, I}] = T_F_Tprime;
// T' → * F T' | / F T' | ε
table[{T_PRIME, MULT}] = Tprime_mult_F_Tprime;
table[{T_PRIME, DIV}] = Tprime_div_F_Tprime;
table[{T_PRIME, PLUS}] = Tprime_epsilon;
table[{T_PRIME, MINUS}] = Tprime_epsilon;
table[{T_PRIME, RPAREN}] = Tprime_epsilon;
table[{T_PRIME, END}] = Tprime_epsilon;
// F → P F'
table[{F, LPAREN}] = F_P_Fprime;
table[{F, I}] = F_P_Fprime;
// F' → ^ F | ε
table[{F_PRIME, POW}] = Fprime_pow_F;
table[{F_PRIME, MULT}] = Fprime_epsilon;
table[{F_PRIME, DIV}] = Fprime_epsilon;
table[{F_PRIME, PLUS}] = Fprime_epsilon;
table[{F_PRIME, MINUS}] = Fprime_epsilon;
table[{F_PRIME, RPAREN}] = Fprime_epsilon;
table[{F_PRIME, END}] = Fprime_epsilon;
// P → ( E ) | i
table[{P, LPAREN}] = P_lparen_E_rparen;
table[{P, I}] = P_i;
return table;
}
// 获取符号名称的辅助函数
string getSymbolName(Symbol s) {
if (s.isTerminal) {
switch (s.value) {
case PLUS: return "+";
case MINUS: return "-";
case MULT: return "*";
case DIV: return "/";
case POW: return "^";
case LPAREN: return "(";
case RPAREN: return ")";
case I: return "i";
case END: return "$";
default: return "?";
}
} else {
switch (s.value) {
case E: return "E";
case E_PRIME: return "E'";
case T: return "T";
case T_PRIME: return "T'";
case F: return "F";
case F_PRIME: return "F'";
case P: return "P";
default: return "?";
}
}
}
// 语法分析函数
void parse(const vector<Terminal>& tokens) {
auto table = buildParseTable();
stack<Symbol> parseStack;
parseStack.push(Symbol(true, END)); // 结束符$
parseStack.push(Symbol(false, E)); // 初始非终结符E
size_t index = 0;
Terminal currentToken = tokens[index];
cout << "步骤\t栈内容\t\t输入\t\t动作" << endl;
int step = 1;
while (!parseStack.empty()) {
// 打印当前状态
string stackStr;
stack<Symbol> temp = parseStack;
while (!temp.empty()) {
stackStr = getSymbolName(temp.top()) + " " + stackStr;
temp.pop();
}
string inputStr;
for (size_t i = index; i < tokens.size(); ++i) {
inputStr += getSymbolName(Symbol(true, tokens[i])) + " ";
}
Symbol top = parseStack.top();
parseStack.pop();
// 栈顶为终结符
if (top.isTerminal) {
if (top.value == currentToken) {
// 匹配成功,移动到下一个输入符号
cout << step++ << "\t" << stackStr << "\t\t" << inputStr << "\t匹配 " << getSymbolName(top) << endl;
if (currentToken != END) {
currentToken = tokens[++index];
}
} else {
// 错误处理
cerr << "错误:期望 " << getSymbolName(top) << " 但遇到 " << getSymbolName(Symbol(true, currentToken)) << endl;
return;
}
} else { // 栈顶为非终结符
auto key = pair<NonTerminal, Terminal>{(NonTerminal)top.value, currentToken};
if (table.find(key) != table.end()) {
Production prod = table[key];
// 逆序压栈
vector<Symbol> rhs = prod.rhs;
string productionStr = getSymbolName(top) + " -> ";
for (const auto& s : rhs) {
productionStr += getSymbolName(s) + " ";
}
if (rhs.empty()) productionStr += "ε";
cout << step++ << "\t" << stackStr << "\t\t" << inputStr << "\t应用 " << productionStr << endl;
for (auto it = rhs.rbegin(); it != rhs.rend(); ++it) {
parseStack.push(*it);
}
} else {
cerr << "错误:在 " << getSymbolName(top) << " 处无法处理输入符号 " << getSymbolName(Symbol(true, currentToken)) << endl;
return;
}
}
}
cout << "分析成功!" << endl;
}
int main() {
string input;
cout << "请输入表达式: ";
getline(cin, input);
try {
vector<Terminal> tokens = tokenize(input);
parse(tokens);
} catch (const exception& e) {
cerr << "错误: " << e.what() << endl;
}
return 0;
}
在上述实验基础上,实现语法制导翻译功能,输出翻译后所得四元式序列;
2.要求详细描述所选分析方法进行制导翻译的设计过程;
3.完成对所设计分析器的功能测试,并给出测试数据和实验结果;
4.为增加程序可读性,请在程序中进行适当注释说明;
5.整理上机步骤,总结经验和体会,认真完成并按时提交实验报告。
最新发布