实验三 Tiny扩充语言的语法树生成

期末复习用,主要用来梳理思路,不涉及具体操作界面的实现。

一、实验内容:

(一)为Tiny语言扩充的语法有

1.实现改写书写格式的新if语句;☑️

2.增加for循环;☑️

3.扩充算术表达式的运算符号:+= 加法赋值运算符号(类似于C语言的+=)、求余%、乘方^☑️

4.扩充比较运算符号:=(等于),>(大于)、<=(小于等于)、>=(大于等于)、<>(不等于)等运算符号☑️

5.增加正则表达式,其支持的运算符号有:  或(|)  、连接(&)、闭包(#)、括号( ) 、可选运算符号(?)和基本正则表达式。☑️

6.增加位运算表达式,其支持的位运算符号有 and(与)、or(或)、 not(非),如果对位运算不熟悉,可以参考C/C++的位运算。☑️

(二)对应的语法规则分别为:

1. 把TINY语言原有的if语句书写格式

    if_stmt-->if exp then stmt-sequence end  |  | if exp then stmt-sequence else stmt-sequence end

改写为:

    if_stmt-->if(exp) stmt-sequence else stmt-sequence | if(exp) stmt-sequence

2.for语句的语法规则:

(1) for-stmt-->for identifier:=simple-exp  to  simple-exp  do  stmt-sequence enddo    步长递增1

  (2) for-stmt-->for identifier:=simple-exp  downto  simple-exp  do  stmt-sequence enddo    步长递减1

3. += 加法赋值运算符号、求余%、乘方^等运算符号的文法规则请自行组织。

4.=(等于),>(大于)、<=(小于等于)、>=(大于等于)、<>(不等于)等运算符号的文法规则请自行组织。

5.为tiny语言增加一种新的表达式——正则表达式,其支持的运算符号有:  或(|)  、连接(&)、闭包(#)、括号( ) 、可选运算符号(?)和基本正则表达式,对应的文法规则请自行组织。

6.为tiny语言增加一种新的语句,ID:=正则表达式 

7.为tiny语言增加一种新的表达式——位运算表达式,其支持的运算符号有  and(与)  、or (或)、非(not)。

8.为tiny语言增加一种新的语句,ID:=位运算表达式 

9.为了实现以上的扩充或改写功能,还需要注意对原tiny语言的文法规则做一些相应的改造处理。

Tiny语言原来的文法规则,可参考:云盘中参考书P97及P136的文法规则。

二、实验思路

看起来很ex对不对?是的,你没看错,它就是很ex。

三、知识点梳理

大局!

这部分我也不会,所以复习时知识点会覆盖的比较全面。

结束了词法分析器,我们就即将进入语法分析器的构造了。

还记得图3-1吗?它是从词法分析器到语法分析器的构造,词法分析器用来给语法分析器自己分析好了的词法单元,语法分析器向词法分析器发送获得下一个Token的命令。

图4-1是编译器模型中语法分析器的位置,编译器中常用的方法可以分为自底向上和自顶向下的,自顶向上,就是从叶子节点开始向上构造语法树,自顶向下,就是从根节点开始,向下构造语法树。

词法分析器用来给语法分析器自己分析好了的词法单元,我们称其为记号流。

语法分析器的输入就是记号流,另一个隐含的输入是语言的语法规则,输出为语法树。

经过词法分析实验后,我们可以得到记号流,但语言的语法规则应该如何实现?或者说,如何在计算机里被表示呢?这就要说到上下文无关文法。

上下文无关文法

在之前的词法分析器里学到的正则式,它可以表示词法

上下文无关文法,它可以表示语法

我们可以把上下文无关文法看作一个四元组:G = (T, N, P, S)

T:终结符集合  N:非终结符集合  P:一组产生式规则  S:唯一的开始符号

注意:规则的形式都类似X -> a的形式

【实例】:

红框框起来的部分就是上下文无关文法G,‘|’表示或

这个文法的意思是把正常的句子抽象出来,是一个名词+动词+名词的组合(S -> NVN)

其中,可选名词有4种(N -> ...);可选动词有2种(V -> .....)

在词法分析最小DFA实验里,我们学到了终结和非终结的概念,在语法分析里也差不多,我们可以简单地把左式认为是非终结符(比如说例子中的S、N、V),它一般用大写字母表示,把右式中左式没有出现过的符号认为是终结符,它一般用小写字母表示。非终结符集合和终结符集合不重叠。

同学们可能经常看到一句话:“直到不出现非终结符为止,最终的串称为句子。

什么意思?

举个例子:

蓝框内为给定的文法G。

有意思的问题:文法G可以推出多少个不同的句子?

答案是4*2*4 = 32

语言,就是一切句子的集合,比如说文法G推出的32种句子,就组成了一种语言。

最左推导和最右推导

刚才引入了推导的概念,现在着重整理一下在语法分析中重要的两个推导。

最左推导,顾名思义,就是每次选择最左侧的符号进行替换。

举个例子:

还是拿刚才上下文无关文法部分的文法G举例,当我没有规定推导的顺序时,每一步推导可以随便选定符号,比如说这个例子,我第一次选定了‘V’进行推导,第二次选定了‘N’进行推导。

但如果是最左推导,我每次只能选定最左边的符号进行推导:

最右推导,顾名思义,就是每次选择最右侧的符号进行替换。

推导与分析树

引入上下文无关文法和推导的概念后,我们就可以表示语言的语法规则了。

回过头来看这张图,我们可以说,语法分析器的作用就是:回答文法G是否存在对句子s的推导。(我们可以把记号流看作是一个句子s)

我们可以将推导表现为树状结构

图片比较潦草,所以文字描述一下:

首先,我们将开始符号S作为根节点,开始推导,将S划分为1号边界

S -> NVM,将其写入树中,将SVM划分为2号边界

NVM -> sVM,将s写入树中,将sVM划分为3号边界

sVM -> sdM,将d写入树中,将sdM划分为4号边界

sdM -> sdw,将w写入树中,将sdw划分为5号边界

我们发现,5号边界组成了最后的句子,也可以说是叶子节点组成了最后的句子。

所以我们可以把分析树构造过程视为从根节点不断扩展,最终叶子节点构成最后的句子。

有趣的问题:对分析树进行什么顺序的遍历,可以得到最终的句子?

答案:后序遍历

将分析树的特点总结如下:

顺便提一嘴规约,规约就是推导的逆过程,目前实验还用不到,所以就不赘述了。

实验中要构建语法分析树,所以我们先讨论它需要有怎样的数据结构:

我们需要确定:(1)每个节点在语法分析树中的位置。(2)各个节点中的相互关系。

写出语法分析树的存储结构:

看起来构造过程有些复杂,我们来仔细分析一下,其实分析树就是模拟了我们手动的整个推导过程,还是拿这棵树举例。

从开始符开始,将它建立为根节点,寻找根节点可能与当前句子匹配的推导,作为开始符的子节点,现在我们得到了:

 ​​

然后还没结束,从左到右检查开始符的子节点,发现仍然存在非终结符,从最左(最先发现)的非终结符开始处理:找到该非终结符对应的推导,选择可以匹配当前句子的推导。

从左到右检查N的子节点,发现不存在非终结符,往上走,从左到右看N所在层是否存在非终结符,发现仍然存在非终结符,从最左(最先发现)的非终结符开始处理:找到该非终结符对应的推导,选择可以匹配当前句子的推导。

从左到右检查V的子节点,发现不存在非终结符,往上走,从左到右看V所在层是否存在非终结符,发现仍然存在非终结符,从最左(最先发现)的非终结符开始处理:找到该非终结符对应的推导,选择可以匹配当前句子的推导。

从左到右检查N的子节点,发现不存在非终结符,往上走,从左到右看V所在层是否存在非终结符,发现不存在非终结符,往上走,一直走到根节点,构造结束。

代码如下:

分析树的优点是可以反映推导过程,比较全面。

缺点是过于复杂,耗费空间,所以为了节省空间,引出语法树的概念。

附加部分:

我们也可以通过构造词法分析树的方式获得正则表达式的运算顺序。

语法树

为了节省空间,我们压缩分析树,只存储其中有用的信息,构造语法树。

假如我们需要推导的句子是v*v+d,最终由推导得到了左边的分析树。

我们发现,最终有用的信息是v*v+d,所以我们将左边的分析树简化为右边的语法树。

在PPT里我们看到这句话,可能有些难以理解,此处解释“无法还原记号序列”的原因。

记号序列 = 记号产生的顺序

我们可以看到,分析树的每个叶子节点都代表源代码的一个记号,其余内部节点都表示其对应的语法规则,所以我们可以通过倒推分析树从而还原推导顺序,即记号产生的顺序。

而语法树所有节点都是源代码的记号,无其推导所对应的语法规则,所以我们无法还原记号的序列。

算术表达式对应语法树C语言描述: 

我们知道,算术表达式中对后续分析有用的信息就是标识符(a,b),运算符(+\-\*\/),常数值(1,2),所以在语法分析树中,我们需要保留上述信息。

typedef enum{Plus, Minus, Times, Division} OpKind;//运算符类型
typedef enum{OpKind, ConstKind, VarKind} ExpKind;//式子类型:运算符/常数/标识符
typedef struct streenode{
    ExpKind kind;//式子类型:运算符/常数/标识符
    OpKind op;//若为运算符,运算符类型
    struct *lchild, *rchild;//左右节点
    int val;//若为常数,常数的值
    char varname[20];//若为标识符,标识符
}STreeNode;
typedef STreeNode *Syntax Tree;

条件判断语句的语法树C语言描述:

同样我们需要分析,if语句中什么对后续分析有用?

(1)测试表达式exp,即if(测试表达式exp)

(2)通过测试表达式后执行的动作statement,即if(exp) {statement}

statement简写为Stmt

   (3) else部分(如果出现的话)

所以我们需要在if的语法分析树中保留上述信息:

typedef enum{ExpK, StmtK}NodeKind;
typedef enum{Zero, One}ExpKind;
typedef enum{IfK, OtherK}StmtKind;
typedef struct streenode{
    NodeKind kind;
    ExpKind ekind;
    StmtKind skind;
    struct streenode *test, *thenpart, *elsepart;
}STreeNode;
typedef STreeNode *Syntax Tree; 

作为练习,研究一下repeat,assign语句的语法树C语言描述:

repeat:

 

同样分析,repeat语句里什么信息是有用的,我们就在语法树里保留这些信息:

(1)测试表达式exp

(2)body里的执行动作statement

typedef enum{ExpK, StmtK}NodeKind;
typedef enum{Zero, One}ExpKind;
typedef enum{RepeatK, OtherK}StmtKind;
typedef struct streenode{
    NodeKind kind;
    ExpKind Ekind;
    StmtKind sKind;

    struct streenode *test, *body;
}STreeNode;

typedef STreeNode* Syntax_Tree;

assign:

有时候这些书写得有种生怕被读懂的美。

说人话,就是我们在赋值时,比如a = 12*5 ,我们把变量a存在父节点,将赋予它的表达式12*5存在子节点。

同样分析,assign语句里什么信息是有用的,我们就在语法树里保留这些信息:

(1)赋予变量的表达式exp

(2)变量名

typedef enum{ExpK, NameK}NodeKind;
typedef enum{Plus, Minus, Times, Division}OpKind;
typedef enum{OpKind, ConstKind}ExpKind;

typedef struct streenode{

    Nodekind kind;
    ExpKind ekind;//如果是表达式,存储表达式类型
    OpKind okind;//如果是符号,存符号类型
    int val;//如果是常量,存常量
    char Name[20];//如果是名字,存名字

    struct streenode* exp;

}STreeNode;

文法分类与书写方法

文法分类

之前说到,我们可以把上下文无关文法看作一个四元组:G = (T, N, P, S)

其实我们可以把所有类型的文法看作一个四元组G = (T, N, P, S),T,N,P,S含义不变。

但我们可以通过对产生式施加不同的限制,把文法及其对应的语言分为四种类型:0型、1型、2型、3型。

书写方法:

首先,文法有四要素,G = {T, N, P, S}

我们将开始符设定为S,T = {a, b, c}, N = {S, S1, S2, S3 }

由于i,j,k >= 1

构造文法G如下:

1. S -> A B C
2. A -> aA | ε
3. B -> bB | ε
4. C -> cC | ε

首先,文法有四要素,G = {T, N, P, S}

我们将开始符设定为S,T = {a, b}, N = {M}

由于i >= 0

文法G如下:

S -> aN

N -> bN | ε

重点来了,不会推也得背下来

我们可以很容易地写出下列文法:

但我们注意到,算术表达式运算符是有优先级的,但在刚才写出的文法中,我们没有表现出运算符的优先级,所以需要改写文法。

最后将文法改写如下:

由刚才的改写,我们可以归纳出一条重要的结论:语法树中谁深谁优先。

除了写出文法规则,我们还需要分析文法中规则是否有效的问题,我们需要删除文法中无效的规则,即有害规则和多余规则。

二义性文法

给定文法G,若存在句子S,它可以画出两棵不同的分析树,则G是二义性文法。

再举一个例子:

以下给出文法G,回答是否存在对句子3+4*5的推导

我们按照文法规则,写出了两个不同的最左推导和其对应的分析树

我们规定,以后序遍历(左右根)的方式,取得最终的句子(分析树的含义)

所以第一棵分析树的计算顺序是3+4*5

第二棵分析树的计算顺序是(3+4)*5

此刻我们的文法就出现了歧义,也就是有二义性。

为什么要消除二义性?

因为二义性会导致同一个程序有不同含义,从而导致程序运行结果不唯一。

所以就要进行二义性文法的重写,使其无歧义:

重写没有套路,具体问题具体分析。

将刚才的文法重写如下:

仔细观察刚才的文法,它生成分析树满足了加法的左结合性,所以消除了二义性。

再举一个在具体程序实现中,消除二义性的例子:else的悬挂问题

分析引起歧义的原因,是因为我们不知道应该在哪里悬挂else。

解决方法1:设置一个限制规则,在分析程序中实现。

即,else要与最近的上一个未被匹配的if匹配。

解决方法2:改造文法:

解决方法3:重新设计书写语法:

方案1:else一定出现。

引起歧义的原因是else悬挂问题,即我不知道什么时候else应该出现,那我在每一个if后面必须挂一个else,问题就解决了。

方案2:使用一个if匹配的关键字来作为语句的结束。

  • 35
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
tiny是一种扩充语言,它使用语法树生成代码。语法树是用来表示代码结构的树形结构,每个节点代表着代码的一个组成部分,例如变量声明、条件语句和循环等。生成语法树的过程叫做语法分析。 扩充语言语法树生成包括以下几个步骤: 1. 词法分析:扫描代码,将代码分割为一个个的词法单元,例如变量名、运算符和关键字等。每个词法单元被标记为特定的标识符,并存储在一个列表中。 2. 语法分析:使用文法规则将词法单元列表转换为语法树。文法规则定义了代码的语法规则和语法结构。可以使用自上而下的语法分析方法(如LL(1)分析法)或自下而上的语法分析方法(如LR分析法)来生成语法树。 3. 语法树构建:根据文法规则和词法单元列表,逐步构建语法树。每个语法树节点都有一个类型和可能的子节点,并且每个子节点连接到其父节点。节点的类型可以是变量、运算符或函数调用。 4. 语法树优化:可以对生成语法树进行优化。例如,可以进行常量折叠、无用代码删除和循环展开等优化操作,以提高代码的效率和可读性。 5. 代码生成:根据语法树生成目标代码。可以通过遍历语法树的节点,并根据节点的类型和属性生成相应的目标代码。目标代码可以是机器码、中间代码或其他高级语言的代码。 总的来说,tiny扩充语言语法树生成是一个将代码转换为结构化表示的过程。它通过词法分析、语法分析、语法树构建和代码生成等步骤,将代码转换为可执行的目标代码。这种生成过程不仅使得代码更易于理解和优化,也为进一步的编译、解释和执行操作提供了基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值