代码链接:编译原理实验三
文章目录
实验三 语法分析程序
(一)学习经典的语法分析器(1学时)
一、实验目的
学习已有编译器的经典语法分析源程序。
二、实验任务
阅读已有编译器的经典语法分析源程序,并测试语法分析器的输出。
三、实验内容
-
选择一个编译器,如:TINY,其它编译器也可(需自备源代码)。
-
阅读语法分析源程序,加上你自己的理解。尤其要求对相关函数与重要变量的作用与功能进行稍微详细的描述。若能加上学习心得则更好。TINY语言请参考《编译原理及实践》第3.7节。对TINY语言要特别注意抽象语法树的定义与应用。
-
测试语法分析器。对TINY语言要求输出测试程序的字符形式的抽象语法树。(手工或编程)画出图形形式的抽象语法树。
TINY语言:
测试用例一:sample.tny。
(二)实现一门语言的语法分析器(3学时)
一、实验目的
通过本次实验,加深对语法分析的理解,学会编制语法分析器。
二、实验任务
用C或C++语言编写一门语言的语法分析器。
三、实验内容
-
语言确定:C-语言,其定义在《编译原理及实践》附录A中。也可选择其它语言,不过要有该语言的详细定义(可仿照C-语言)。一旦选定,不能更改,因为要在以后继续实现编译器的其它部分。鼓励自己定义一门语言。也可选择TINY语言,但需要使用与TINY现有语法分析代码不同的分析算法实现,并在实验报告中写清原理。
-
完成C-语言的BNF文法到EBNF文法的转换。通过这一转换,消除左递归,提取左公因子,将文法改写为LL(1)文法,以适用于自顶向下的语法分析。规划需要将哪些非终结符写成递归下降函数。
-
为每一个将要写成递归下降函数的非终结符,如:变量声明、函数声明、语句序列、语句、表达式等,定义其抽象语法子树的形式结构,然后定义C-语言的语法树的数据结构。
-
仿照前面学习的语法分析器,编写选定语言的语法分析器。可以自行选择使用递归下降、LL(1)、LR(0)、SLR、LR(1)中的任意一种方法实现。
-
准备2~3个测试用例,测试并解释程序的运行结果。
实验步骤
学习经典的语法分析器(TINY)
主要的程序部分位于parse.c
文件内
语法树节点的结构:globals.h
内
#define MAXCHILDREN 3
typedef struct treeNode {
struct treeNode *child[MAXCHILDREN];
struct treeNode *sibling;
int lineno;
NodeKind nodekind;
union {
StmtKind stmt;
ExpKind exp;
} kind;
union {
TokenType op;
int val;
char *name;
} attr;
ExpType type; /* for type checking of exps */
} TreeNode;
其中重要的部分即child结点,sibling结点以及attr标识参数的结点。在表达式exp部分,op会被赋值为token;在赋值、READ、读取ID部分会对name赋值;在NUM结点则给val赋值。
由于这个parse使用的是递归下降,所以需要一个程序入口来进入,这部分就是TreeNode* parse(void)
函数。
具体的话,就如递归下降分析算法,把每一个非终结符看作一个函数,然后调用它,直到到达一个终结符。非终结符构成的函数如:stmt_sequence()
,statement()
,if_stmt()
,repeat_stmt()
,assign_stmt()
,read_stmt()
,write_stmt()
,exp()
,simple-exp()
,term()
,factor()
,在进入这些函数后,优惠根据产生时进行选择,如果是非终结符,继续调用,否则若是终结符,则需要调用封装的match()
函数,判断token是否与之匹配,匹配则继续进行token的接受,否则语法分析失败。
就这样,在递归调用的过程中,到达终结符节点时则进行树结点的建立,从而完成语法树的建立过程。
对输入文件sample.tny生成的语法树
TINY COMPILATION: sample.tny
Syntax tree:
Read: x
If
Op: <
Const: 0
Id: x
Assign to: fact
Const: 1
Repeat
Assign to: fact
Op: *
Id: fact
Id: x
Assign to: x
Op: -
Id: x
Const: 1
Op: =
Id: x
Const: 0
Write
Id: fact
图形形式:
实现一门语言的语法分析器(TINY)
语言确定
使用TINY语言,与实验一相同
BNF文法到EBNF文法的转换
BNF文法:
program -> stmt-sequence EOF
stmt-sequence -> stmt-sequence ; statement
stmt-sequence -> statement
statement -> if-stmt
statement -> repeat-stmt
statement -> assign-stmt
statement -> read-stmt
statement -> write-stmt
if-stmt -> if exp then stmt-sequence else stmt-sequence end
if-stmt -> if exp then stmt-sequence end
repeat-stmt -> repeat stmt-sequence until exp
assign-stmt -> identifier := exp
read-stmt -> read identifier
write-stmt -> write exp
exp -> simple-exp comparison-op simple-exp
exp -> simple-exp
comparison-op -> <
comparison-op -> =
simple-exp -> simple-exp addop term
simple-exp' -> term
addop -> +
addop -> -
term -> term mulop factor
term -> factor
mulop -> *
mulop -> /
factor -> ( exp )
factor -> number
factor -> identifier
消除左递归:line 2,line 19,line 23
提取左公因子:line 9
EBNF文法:
program -> stmt-sequence EOF
stmt-sequence -> statement stmt'
stmt' -> ; statement stmt'
stmt' -> $
statement -> if-stmt
statement -> repeat-stmt
statement -> assign-stmt
statement -> read-stmt
statement -> write-stmt
if-stmt -> if exp then stmt-sequence else-part'
else-part' -> else stmt-sequence end
else-part' -> end
repeat-stmt -> repeat stmt-sequence until exp
assign-stmt -> identifier := exp
read-stmt -> read identifier
write-stmt -> write exp
exp -> simple-exp cmp-exp'
cmp-exp' -> comparison-op simple-exp
cmp-exp' -> $
comparison-op -> <
comparison-op -> =
simple-exp -> term simple-exp'
simple-exp' -> addop term simple-exp'
simple-exp' -> $
addop -> +
addop -> -
term -> factor term'
term' -> mulop factor term'
term' -> $
mulop -> *
mulop -> /
factor -> ( exp )
factor -> number
factor -> identifier
抽象语法树结构
语法树节点与 parse.c 中定义相同,只是每个结点都具有name,且有最大子结点个数和当前访问子结点的编号:
typedef struct treeNode {
struct treeNode *child[MAXCHILDREN];
struct treeNode *father;
int child_no; // 当前访问的子结点编号
int max_child; // 最大子结点个数
int lineno;
std::string name; // 名称
NodeKind nodekind;
union {
StmtKind stmt;
ExpKind exp;
} kind;
union {
TokenType op;
int val;
} attr;
ExpType type; /* for type checking of exps */
} TreeNode;
语法分析器
由于选择了TINY语言,不能使用递归下降算法,所以这里选择LL(1)语法分析器来进行实现
LL(1)语法分析器的实现
构成LL(1)语法分析器,需要求得四个关键集合:NULLABLE集,FIRST集,FOLLOW集,FIRST_S集。得到四个集合后,便可以开始构建LL(1)预测分析表,从而完成准备过程。
完成LL(1)预测分析表之后,就可以对输入程序进行语法分析,通过预测分析表来指导产生式的选择。
基础数据存储方式
- 产生式:
vector<string> prod_;
unordered_map<int, vector<string>> prod;
prod_以字符串形式存储每一条未被解析过的产生式,具体格式即上述的EBNF语法的产生式。
prod则是将每条产生式解析后作为val,key为该条产生式所在的行数,如上述样例中
line 13: repeat-stmt -> repeat stmt-sequence until exp
得到的prod的key为13,val则为:
[repeat-stmt, repeat, stmt-sequence, until, exp]
将每条产生式读入后如此解析,放入prod中
- 终结符与非终结符:
set<string> nonterminal;
vector<TokenType> terminal;
可以都看作是数组的形式存储,不过在nonterminal中,为了方便之后的查找使用了set进行存储,提高查找的效率。
- NULLABLE,FIRST,FOLLOW,FIRST_S集
set<string> nullable;
unordered_map<string, set<TokenType>> first;
unordered_map<string, set<TokenType>> follow;
unordered_map<int, set<TokenType>> first_s;
对于NULLABLE来说,只需要知道某个非终结符是不是NULLABLE即可,所以使用set存储;而FIRST和FOLLOW则是对于每一个非终结符,都要维持一个终结符构成的集合,所以使用hashmap嵌套set实现;而FIRST_S集则是对于每一个产生式都有一个终结符集合,依旧是通过产生式的编号作为键值。
- 预测分析表
unordered_map<string, unordered_map<TokenType, vector<int>>> LL1_table;
预测分析表中,由于对于每一个非终结符,对其来说每一个终结符都要有一个相应的转移,但是转移的产生式不唯一,所以使用vector来进行保存,产生式依旧采用编号来进行识别。
解析产生式
目的:由于最初输入的是一个字符串形式的产生式,存放在prod_内,但最终为了方便使用,需要将每一个终结符和非终结符解析存储在prod内。
算法:对于产生式头部和产生式体,通过"->“就可以分离。对于产生式体的分离,则每次找到” "所在的位置,以此来进行分割,直到没有空格存在。
具体实现:
vector<string> parse(const string &production) {
vector<string> ans; // 存放最终的字符串
int i;
int before = 0;
for (i = 0; i < production.length(); i++) {
if (production[i] == '-' && production[i + 1] == '>') { // 找到产生式头部的分割点
string head = production.substr(0, i - 1);
ans.push_back(head);
i += 3; // 指针移至产生式体的第一个字符
before = i;
break;
}
}
for (; i < production.length(); i++) {
if (production[i] == ' ') { // 找空格
string nxt = production.substr(before, i - before); // 分割
ans.push_back(nxt);
before = i + 1;
}
}
string nxt = production.substr(before, i - before);
ans.push_back(nxt);
return ans;
}
NULLABLE集合计算
这一部分根据产生式来看,只有能直接推出空串的产生式可以在NULLABLE中
具体实现:
void get_nullable() {
for (auto s : prod_) {
int end;
for (int i = 0; i < s.length(); i++) {
if (s[i] == '-' && s[i + 1] == '>')
end = i - 1;
if (s[i] == '$') { // 如果能直接推出空串
string ret = s.substr(0, end);
nullable.insert(ret); // 属于NULLABLE集
}
}
}
return;
}
FIRST集合计算
这一部分根据上课所讲的伪代码进行实现即可
如果产生式体第一个为终结符,那么其FIRST集即为该终结符;若为非终结符,则该FIRST集应并上该非终结符的FIRST集,若其属于NULLABLE集,则继续向后看,不属于那么直接退出。
直到没有一个FIRST集再被修改过,就表示求解完成。
具体实现:
void get_First() {
int flag = 1; // 判断是否还有FIRST集改变
while (flag) {
flag = 0;
for (int i = 0; i < prod_num; i++) { // 遍历所有产生式
string head = prod[i][0];
set<TokenType> origin = first[head];
int size = origin.size();
for (int j = 1; j < prod[i].size(); j++) { // 遍历该产生式体
if (prod[i][j] == "$") // 空集跳过
continue;
string nxt = prod[i][j];
if (nonterminal.find(nxt) != nonterminal.end()) { // 是非终结符
set<TokenType> temp = first[nxt];
set_union( // 取并集
origin.begin(), origin.end(), temp.begin(), temp.end(),
inserter(origin, origin.begin()));
if (nullable.find(nxt) == nullable.end()) { // 不在NULLABLE,退出内循环
break;
}
} else { // 是终结符,加入后退出内循环
TokenType token = get_Terminal(nxt, head);
origin.insert(token);
break;
}
}
if (origin.size() != size) // 当前FIRST集被修改,while大循环继续执行
flag = 1;
first[head] = origin;
}
}
}
FOLLOW集合计算
伪代码:
与FIRST集不同的,是其对产生式体的每一个非终结符进行,并且是逆序执行。
首先设置temp集合暂存可能赋值的FOLLOW,逆序遍历,如果是终结符,则temp集合为该终结符;如果是非终结符,则该非终结符的FOLLOW与temp取并集,并判断是否在NULLABLE中,不在NULLABLE中则temp修改为其FIRST集,否则并其FIRST集。
具体实现:
void get_Follow() {
int flag = 1; // 判断是否还有FOLLOW集改变
while (flag) {
flag = 0;
for (int i = 0; i < prod_num; i++) { // 对每条产生式遍历
string head = prod[i][0];
set<TokenType> temp = follow[head];
for (int j = prod[i].size() - 1; j >= 1; j--) { // 逆序
if (prod[i][j] == "$") // 空跳过
continue;
string nxt = prod[i][j];
if (nonterminal.find(nxt) != nonterminal.end()) { // 是非终结符
set<TokenType> upd = follow[nxt];
int size = upd.size();
set_union(
temp.begin(), temp.end(), upd.begin(), upd.end(),
inserter(upd, upd.begin())); // 取并集
if (upd.size() != size) { // FOLLOW集被修改,while大循环继续
flag = 1;
}
follow[nxt] = upd; // 更新当前非终结符的FOLLOW
if (nullable.find(nxt) == nullable.end()) { // 不在NULLABLE中
temp.clear(); // 修改temp
temp = first[nxt];
} else { // 在NULLABLE中
set<TokenType> ret = first[nxt];
set_union( // 取并集
temp.begin(), temp.end(), ret.begin(), ret.end(),
inserter(temp, temp.begin()));
}
} else { // 是终结符
TokenType token = get_Terminal(nxt, head);
temp.clear(); // temp清空,修改为当前的终结符
temp.insert(token);
}
}
}
}
}
FIRST_S集合计算
求解FIRST_S集要依赖于FIRST,FOLLOW,NULLABLE集。
FIRST_S集的求法与FIRST相似,只不过是对每个产生式进行,与FIRST不同的,如果遍历到了最后一个符号,那么需要该产生式的FIRST集并上产生式头的FOLLOW集
具体实现:
void cal_First_s(int i) {
string head = prod[i][0]; // 以下部分与FIRST相似
set<TokenType> origin = first_s[i];
for (int j = 1; j < prod[i].size(); j++) {
string nxt = prod[i][j];
if (nxt == "$")
continue;
if (nonterminal.find(nxt) != nonterminal.end()) {
set<TokenType> temp = first[nxt];
set_union(
temp.begin(), temp.end(), origin.begin(), origin.end(),
inserter(origin, origin.begin()));
first_s[i] = origin;
if (nullable.find(nxt) == nullable.end()) {
return;
}
} else {
TokenType token = get_Terminal(nxt, head);
origin.insert(token);
first_s[i] = origin;
return;
}
}
set<TokenType> fl = follow[head];
set_union( // 遍历到最后一个,取其FOLLOW并集
fl.begin(), fl.end(), origin.begin(), origin.end(), inserter(origin, origin.begin()));
first_s[i] = origin; // 修改当集FIRST_S
}
void get_First_s() {
for (int i = 0; i < prod_num; i++) { // 对每条产生式处理
cal_First_s(i);
}
}
产生预测分析表
求完上述的集合后,就可以根据FIRST_S集求得具体的LL(1)预测分析表。
当上述集合求完后,很显然,最终集合中都保存的为终结符。构造预测分析表的算法极其简单,对于每一条语句的产生式头部,他在当前语句的FIRST_S集中的终结符的转一下,会得到该条语句,具体实现如下:
void get_Transtable() {
for (auto it : first_s) { // 对每一个FIRST_S集
int idx = it.first; // 获得该产生式的编号
string head = prod[idx][0]; // 得到对应的产生式头部
for (auto trans : it.second) { // 遍历FIRST_S集
LL1_table[head][trans].push_back(idx); // 在该token下转移到该产生式
}
}
}
parse语法分析
在有了预测分析表之后,就可以结合词法分析器进行语法分析。
伪代码:
先压入一个起始符号,生成具体的产生式,当栈不为空时,判断栈顶元素,栈顶元素为非终结符,则将其弹出,根据当前词法分析器获得的token在预测分析表中获得对应的产生式,将产生式体从右向左压入栈中,如果没有对应的产生式,那么是错误情况;如果是终结符,token与其相匹配的话,那么弹出,获得新token,否则就是错误情况。
具体实现:
stk.push("program"); // 其实符号
token = ERROR;
token = getToken(fp); // 得到第一个token
while (!stk.empty()) {
print_stack_info(stk); // 打印栈信息
string top = stk.top(); // 取栈顶
stk.pop(); // 弹出
if (nonterminal.find(top) == nonterminal.end()) { // 终结符
TokenType tt = ll1.get_Terminal(top); // 得到这个终结符的Token表时
if (tt == token) { // 匹配,则获得下一个token
token = getToken(fp);
} else { // 触发ERROR函数,打印错误信息
ERROR_FUNC(head, "", tt);
}
} else { // 非终结符
vector<int> nxt_prod = ll1.LL1_table[top][token]; // 从预测分析表取出产生式
if (nxt_prod.size() == 0) { // ERROR!
ERROR_FUNC(head, top);
} else {
vector<string> ret = ll1.prod[nxt_prod[0]]; // 取出产生式体
for (int i = ret.size() - 1; i >= 1; i--) { // 反向压栈
if (ret[i] == "$") // 空串不压栈
continue;
stk.push(ret[i]);
}
}
}
}
经过上述过程即可完成语法分析,正确则会打印完整的栈信息,并且输出YES
否则输出相应的错误信息
生成语法树
除了做语法分析之外,还要生成相关的中间代码:抽象语法树
考虑到上述语法分析过程是先压栈,根据栈顶弹出再压栈的过程,此时考虑树的前序遍历算法,如果想要保存每一步遍历的信息,其过程是:压入根节点,弹出栈顶元素,生成左子结点,右子结点,右子结点入栈,左子结点入栈,重复进行,直到到达根节点,只需要出栈。那么这就比较显然了:上述的语法分析过程即树的前序遍历。从而,只需要将前序遍历算法反向,就可以构造出一颗树,也就是上述的语法分析过程即反向构建树的过程。
有了上述的算法基础,就可以着手开始构造树了。
树结点的声明
(参考tiny_tm/globals.h):
typedef struct treeNode {
struct treeNode *child[MAXCHILDREN]; // 子结点
struct treeNode *father; // 指向父结点,方便回溯
int child_no; // 当前遍历的子结点编号(构造树时有用)
int max_child; // 最大的子结点编号
int lineno;
std::string name; // 当前结点的名称
NodeKind nodekind;
union {
StmtKind stmt;
ExpKind exp;
} kind;
union {
TokenType op;
int val;
} attr;
ExpType type; /* for type checking of exps */
} TreeNode;
结点的构造
构造树的根结点:
TreeNode *newRootNode(string name) {
TreeNode *t = new TreeNode; // new一个,然后相应的初始化即可
t->lineno = line_no;
t->child_no = 0;
t->max_child = -1; // 这里设置为-1为了方便后续的判断,关于子结点数目的处理见后续函数
t->nodekind = StmtK;
t->name = name;
return t;
}
普通结点:
TreeNode *newNode(TreeNode *fa, string name) {
TreeNode *t = new TreeNode;
set<string> nonterminal = ll1.get_nonterminal();
if (nonterminal.find(name) != nonterminal.end()) {
t = newStmtNode(name); // 如果是非终结符,则调用上述函数
} else {
t->lineno = line_no;
t->child_no = 0;
t->max_child = 0; // 终结符结点没有子结点,在后续也不需要resize
t->nodekind = ExpK;
TokenType tt = ll1.get_Terminal(name); // 得到当前非终结符的名称
t->attr.op = tt;
t->name = name;
}
t->father = fa; // 设置父节点
return t;
}
有了上述的函数,就可以构造出理想中的数据结构,初始时初始化根节点:
// 生成根节点
TreeNode *t = new TreeNode;
t = newRootNode("program");
// 根节点指针,用于发生错误时输出语法树
TreeNode *head = t;
对于其他节点的生成,由于压栈的过程是在非终结符时进行,所以压栈时,反向建立当前结点的子结点:
t->child[i - 1] = newNode(t, ret[i]);
子结点个数的分配
在生成子结点时,值得注意的是,当前结点应当分配最大子结点个数,所以在弹栈时,对当前结点resize:
为什么不在上面生成子结点时就设置子节点的最大个数呢,因为当时没有得到对应的token,只有得到对应阶段的token才能进行子结点个数的确定。
string top = stk.top();
stk.pop();
resize_treenode(t, head);
resize_treenode函数:
// 重新分配当前结点的最大子结点个数
// 因为在生成子结点时, 没有对应的token, 无法得到正确的最大子结点个数
void resize_treenode(TreeNode *t, TreeNode *head) {
t->child_no = 0; // 初始子结点编号为0
if (t->nodekind == ExpK)
t->max_child = 0;
else {
vector<int> nxt_prod = ll1.LL1_table[t->name][token]; // 得到相应的产生式
if (nxt_prod.size() == 0) {
ERROR_FUNC(head, t->name, token);
}
if (ll1.prod[nxt_prod[0]][1] == "$")
t->max_child = 0;
else
t->max_child = ll1.prod[nxt_prod[0]].size() - 1; // 设置最大的子节点个数
}
}
回溯
由于到达叶子节点即终结符,需要进行回溯,到达非终结符结点时,如果其子结点都被遍历,也许要回溯,此时要回溯到最近的没有完全访问的父结点的第一个未被访问子结点。
对于叶子节点来说,其父结点一定是非终结符结点,只需要指向其父结点,并且当子结点未被全访问时,指向其子结点,否则交给后续非终结符结点的回溯过程进行;对非终结符结点,当他的子结点全被访问过,或者最大子结点为-1(还未分配)时,需要回溯,而如果到达了叶子节点或者根节点,此时即可停止回溯,具体回溯的过程,即指向父结点,并找到第一个未被访问的子结点,而如果这一个子结点还未被分配,那么就应当结束循环。
这一个回溯过程还包括对于新生成的子结点的访问。
具体实现:
// 回溯, 找到最近的一个还有未被访问过的子结点的结点
TreeNode *get_Free_Node(TreeNode *t, int pos) {
if (pos == 1) { // 如果是新生成了子结点,则特判,并且指向其子结点
if (t->max_child != 0) {
return t->child[t->child_no++];
}
}
if (t->nodekind == ExpK && t->father != NULL) { // 叶子节点
t = t->father;
if (t->child_no < t->max_child) {
t = t->child[t->child_no++];
}
}
if (t->nodekind == StmtK) { // 非终结符结点
while (t->child_no >= t->max_child && t->max_child != -1 && t->nodekind == StmtK &&
t->father != NULL) {
t = t->father;
if (t->child_no < t->max_child) { // 指向第一个为访问的结点
t = t->child[t->child_no++];
if (t->max_child == -1) { // 得到一个未被分配的结点
break;
}
}
}
}
return t;
}
得到正确的结点名
由于在初始化结点时,并没有办法得到一个正确的token,所以得到的名称也是不正确的,所以需要在匹配终结符时进行重命名,只需要赋值词法分析中的currString即可实现:
// 对结点进行重命名, 以显示当前ID或NUM接收到的具体的字符串
void rename(TreeNode *t) {
t->name = currString;
}
修改后的语法分析过程
stk.push("program");
token = ERROR;
token = getToken(fp);
// 生成根节点
TreeNode *t = new TreeNode;
t = newRootNode("program");
// 根节点指针,用于发生错误时输出语法树
TreeNode *head = t;
while (!stk.empty()) {
print_stack_info(stk);
string top = stk.top();
stk.pop();
resize_treenode(t, head); // 重分配
if (nonterminal.find(top) == nonterminal.end()) {
TokenType tt = ll1.get_Terminal(top);
if (tt == token) {
rename(t); // 重命名
token = getToken(fp);
// 语法树回溯, 因为到达了叶子节点
t = get_Free_Node(t, 0);
} else { // ERROR!
ERROR_FUNC(head, "", tt);
}
} else {
vector<int> nxt_prod = ll1.LL1_table[top][token];
if (nxt_prod.size() == 0) { // ERROR!
ERROR_FUNC(head, top);
} else {
vector<string> ret = ll1.prod[nxt_prod[0]];
for (int i = ret.size() - 1; i >= 1; i--) {
if (ret[i] == "$") // 空串不压栈
continue;
stk.push(ret[i]);
// 生成当前结点子结点,但不分配最大子结点大小
t->child[i - 1] = newNode(t, ret[i]);
}
}
// 看当前非叶子结点是否需要回溯:可能所以子结点都被遍历
// 若不需要,则进入下一个子结点继续
t = get_Free_Node(t, 1);
}
}
实验结果
具体结果说明见后文或README.md
运行正例即pos.tny
{ Sample program
in TINY language -
computes factorial
}
read x;
if 0 < x then { don't compute if x <= 0 }
fact := 1;
repeat
fact := fact * (x / 3);
x := x - 1 + 0
until x = 0;
write fact { output factorial of x }
else write fact
end
输出的栈:
program
---------------
Token = reserved word: read
stmt-sequence
EOF
---------------
Token = reserved word: read
……
read
identifier
stmt'
EOF
---------------
Token = reserved word: read
identifier
stmt'
EOF
---------------
Token = ID, name= x
stmt'
EOF
---------------
Token = ;
……
YES
得到的语法树(部分):
|program
|stmt-sequence
|statement
|read-stmt
|reserved word: read
|ID, name= x
|stmt'
|;
|statement
|if-stmt
|reserved word: if
|exp
|simple-exp
|term
|factor
|NUM, val= 0
|term'
|simple-exp'
|cmp-exp'
|comparison-op
|<
|simple-exp
|term
|factor
|ID, name= x
|term'
|simple-exp'
|reserved word: then
|stmt-sequence
运行反例neg.tny
{ Sample program
in TINY language -
computes factorial
}
read x;
else 0 < x then { don't compute if x <= 0 }
fact := 1;
repeat
fact := fact * (x / 3);
x := x - 1 + 0
until x = 0;
write fact { output factorial of x }
else write fact
end
输出栈:
;
statement
stmt'
EOF
---------------
Token = ;
statement
stmt'
EOF
---------------
Token = reserved word: else
ERROR!
now token: 4
nonterminal: statement
top token: reserved word: else
输出语法分析树:
|program
|stmt-sequence
|statement
|read-stmt
|reserved word: read
|ID, name= x
|stmt'
|;
|statement
|stmt'
|EOF
与预期相符。
输出结果说明
read
identifier
stmt'
EOF
---------------
Token = reserved word: read
上方为stack的信息,顶行为栈顶;下方则是当前得到的token
在文件的末尾,如果输出YES,表示语法正确,否则会输出相关错误信息。
语法树保存在 output/SyntaxTree 内,不过具体的格式也没有很好看的形式,大概懂什么意思即可。每一列输出的为同一层,在该列的后面且该行的下面为其子结点,如:
|program
|stmt-sequence
|statement
|read-stmt
|reserved word: read
|ID, name= \
|stmt'
statement和stmt’是sibling结点,位于同一层,都是stmt-sequence的子结点;read-stmt是statement的子结点但不是stmt’的子结点
关于代码如何使用,请见README.md