Skip to main content
A cup of beer
  1. Posts/

做个编译器——语义分析

·618 words·3 mins

General #

什么是语义?语义就是语句的功能。

属性文法 #

属性 #

在上下文无关文法的基础上,为每个符号都附加上其相关的某些信息,称为属性。这些属性依附在所属的符号上面。

对属性进行加工,即进行语义分析。

语义规则 #

因为每个符号都附加了属性,所以每个产生式都附加了属性的计算、操作规则。

这些规则称为语义规则。

b:=f(c1,c2,...,ck)给出了对语义规则的定义。所谓语义规则就是属性c1,c2,…通过运算f得到结果,把结果赋值给接受结果的属性b。

综合属性和继承属性 #

用于语法树中的自下而上(也就是产生式中的右部到左部)地传递信息。

体现在产生式上,应当根据产生式右部的符号的属性,可以计算产生式左部符号的综合属性

体现在语法树上,也就是父节点的综合属性是由其孩子的属性进行计算得出的。

举例:E->E1+T, E.val := E1.val + T.val,则产生式左部的E有属性E.val,右部的E1和T分别有属性E1.val和T.val,那么左侧的E.val就是E的综合属性,它由右部的两个符号的属性相加计算而来。

因为综合属性需要几个已有的属性通过一定的计算才能得到其值,所以得名“综合”。

传递信息的方向和综合属性相反。

体现在产生式上,应当根据产生式左部的符号的或产生式右部其他符号的属性来计算产生式右部符号的继承属性

举例:L->L1, L1.in:=L.in,那么产生式右部L1.in是通过产生式左部的L.in得到的。

举例:D->TL, L.in = T.type,那么产生式右部的T.type是通过产生式右部L.in得到的。

注意,从语义规则的角度来看,综合属性和继承属性的区别是:对于形如b:=f(c1,c2,…)的语义规则,是产生式左部的属性还是产生式右部的属性在赋值号(:=)左边。

注意,只能利用b来区分。切勿利用c1,c2,…是什么来区分。因为这不是重点。

带注释的语法树 #

把语法树的各结点用其语义规则进行替代,称为带注释的语法树。

image-20220617144620011

带注释的语法树能清晰地反映信息传递的方向(自顶向下,自底向上)。

addtype(id.entry, L.in) #

在符号表中找到给定id的入口,并设定其类型信息为L.in。

image-20220617145550570

属性依赖 #

之前提到,式b:=f(c1,c2,...,ck)给出了对语义规则的定义。

我们现在称b属性依赖于c1,c2,…,ck,当且仅当下面两种情况中的一种:

  • 若b是综合属性,且c1,…,ck是产生式右部属性
  • 若b是继承属性,且c1,…,ck是产生式右部属性,或综合属性

实际上,上述这种定义就是又把综合属性、继承属性的定义说了一遍而已。

换言之,只要存在语义规则上面的关系b:=f(c1,c2,…,ck),就可以认为是b属性依赖于c1,c2,…,ck。

终结符只能有综合属性,其值需要由词法分析器给出 #

从产生式的角度来看:显然,终结符不可能有继承属性,因为它仅出现在产生式右部,但是它不会被赋值(因为输入串是只读的),而是只能参与运算,所以不可能存在继承属性。词法分析器负责给终结符赋予综合属性,例如如果是一个标识符,那么赋予其entry(符号表项入口地址)(注意,标识符的值是通过先找到符号表项的入口,再查看该表项得出)。这样它就能参与到运算中去,把自己的这个属性向上传递了。

从语法树的角度来看:终结符是只读的,所以不能被从语法树上方来(自上而下)的信息赋值,所以不存在继承属性。但是,为了让语义分析得以进行,这些叶结点应当被赋予可以向上传递的信息(即综合属性),这样上面才有事干。这个赋值的工作由词法分析器承担。

开始符号的综合属性的值需要事先给出 #

开始符号作为语法树的根,它没有父节点,所以无法计算得出综合属性的值。因此需要人为给他。

语法制导翻译 #

根据程序的语法结构进行驱动(例如语法树),在跑语法分析的时候顺便利用语义规则计算各结点的值(一遍扫描法),或进行语法翻译后得到了语法树,对该语法树执行语义规则(依赖图、语法树遍历法),称为语法制导翻译。

为文法的每个产生式配上一个语义规则,在语法分析的过程中同时执行这些规则即可。

语法制导翻译的主要算法有依赖图、语法树遍历、一遍扫描。

依赖图 #

image-20220617154403427

如图是一个依赖图。它是基于注释语法树的。当计算得到注释语法树后,把语法树的每个结点的属性都构建结点,画在所属树结点的旁边,然后对于所有的这样的新结点,如果存在语义规则b:=f(c1,c2,...,ck),则从b到c1,c2,..,ck画有向边,表示b依赖于c1,c2,…,ck。

如果依赖图构建完成后是无环的,那么这是一个良定义的语义规则。对于无环的图,我们可以求出其拓扑序。根据拓扑序依次对属性进行计算即可实现语法分析了。

但是语法树、依赖图建图、拓扑序计算都需要扫描,这是多遍扫描的算法,效率低。

树遍历 #

仍然是先得出注释语法树,然后以某种顺序(例如深度优先且从左到右)遍历该树,把能计算的属性都计算出来,计算不出的先空着,等到下一次遍历的时候再尝试计算。显然这也是一个多遍扫描算法。

递归进入之前,先计算递归对象所有可以计算出来的继承属性。进入了递归,对每个孩子结点从左到右依次进行这个操作(先算其继承,再进入它,再计算其综合)。递归返回之后,紧接着计算该递归对象的综合属性。

抽象语法树:一遍扫描 #

一遍扫描方法可以在进行语法分析的同时完成语义分析。也就是当语法树构造完毕,其所有结点的属性也都有了。

为文法的每个产生式配上一个语义规则,在语法分析的过程中同时执行这些规则即可。

image-20220617160741664

如图是用于算术运算的一棵抽象语法树(AST)。它去除了对翻译(语义分析)没有必要的结点,而用算符(例如其中的+)来代表算式结果。特别地,AST的根是整个表达式的结果。

对于原产生式,每个符号都有属性nptr,即node pointer,指向一个结点的指针。

给出下面几个语义规则,用于建立AST。

  • mknode(op,left,right)建立一个树结点,其子树left和right都是nptr。
  • mkleaf(id,entry),mkleaf(num,val)分别建立标识符(变量)结点和常量数字结点。因为标识符需要对应一段内存空间来存储其值,所以其第二个域是符号表项入口。

然后,对文法的每个产生式都编写一条语义规则,例如:

对产生式E->E1+T,附加语义规则E.nptr:=mknode('+',E1.nptr,T.nptr)。可以想象,这就建立了一棵加法子树。

L属性文法的翻译 #

S-属性文法和L-属性文法 #

如果所有的产生式给出的语义规则都只包含综合属性,那么这是一个S属性文法。

如果所有产生式,例如A->X1X2...Xn,其中的属性要么是综合属性,要么是满足“仅依赖于其左边符号的属性或产生式左部的属性”的继承属性。例如Xi有继承属性,那么该属性必须只依赖于X1,X2,…Xi-1符号或A的某些属性,而不能是Xi+1,…,Xn的。例如产生式A->BC,有语义规则B:=f(C),那这就不满足L属性文法。

S属性文法一定是L属性文法。L属性文法是对S属性文法要求的进一步放松。

翻译模式 #

翻译模式是将语义规则朝着具体实现的方向的进一步改造。语义规则本身不带有执行顺序,它只是附带在产生式上的赋值表达式而已。这些表达的执行顺序由实际翻译过程给出,例如语法树的遍历顺序决定了他们的执行顺序。

image-20220617164516720

翻译模式把语义规则的执行顺序表现出来。 例如通过翻译模式,我们可以得知R的产生式的print操作应当在T扩展(或者说是匹配)完成,而没轮到R1扩展(或者说是匹配)的时候执行。但是语义规则就没法做到这么详细。

如图,翻译模式就是把语义规则用花括号括起来,插入到产生式的正确位置上去,从而赋予执行的时机信息。

image-20220617165025025

例如如图的情况,可以看到语义规则的执行在树上以虚线表示。根据给出的翻译模式,每次num匹配了之后,就紧接着print;每次T展开已完成,R还没展开,此时中间会有个print。

设计翻译模式 #

对于综合属性,因为它需要产生式右部的符号的属性值,所以把它的语义规则放在右部末尾是最安全的,因为能保证这些值都计算完了。(当然如果能提前算完,其位置还能往前提)

非考点,其他要点略。

总之,翻译模式的设计过程,就是要决定要把语义规则插入到产生式右部的哪个位置上,才能保证其能被正确翻译(每次应用语义规则都不会引用到尚未计算出的属性值)

L属性文法的自顶向下翻译 #

根据L文法的特点,它能保证在使用翻译模式进行自顶向下翻译时,每次应用语义规则都不会引用到尚未计算出的属性值。

中间语言 #

中间语言、前端和后端 #

image-20220617171532412

中间语言是介于源语言和目标语言之间的一种语言,其描述的复杂程度介于二者之间。

因此,其是独立于机器的,可以做到从源语言看,把机器代码抽象化(透明化),从机器看,把源语言抽象画(透明化)。由此,我们称源语言到中间语言的过程是编译器的前端,中间语言到目标语言是后端,二者可以独立进行开发。

后端可以针对目标机器对同一种中间语言给出不同的优化,实现更优的适配。不同的机器有不同的后端,但是如果统一了中间语言,那么代码可以非常方便地进行移植。

常见的中间语言 #

  • 后缀式表示的中间语言
  • 图表示的中间语言
    • AST
    • DAG
  • 三地址代码表示的中间语言
    • 三元式
    • 四元式
    • 间接三元式

后缀式中间语言 #

后缀式的构成原则(递归定义)

  • 对于单独的标识符或常量,后缀式是其本身。
  • 对于二元操作符op,形如A1opB1被表示为A2B2op。其中A2、B2分别是A1、B1的后缀式。
  • 对于括号操作,例如(A),那么其后缀式是A的后缀式。(因此可以看出,后缀式无需括号即可实现对任意运算顺序的无歧义表达。)

后缀式运算的代码实现

  • 从左向右扫描后缀式,并维护一个值栈,每次遇到数值就push进去,每次遇到k目运算符就对栈顶的k个值进行计算并替换他们。

中缀式转后缀式的属性文法

  • 这三条产生式对应了后缀式的三条递归定义。
image-20220617174403629
  • 代码实现如下。例如要归约E -> E1 op E2,那么认为现在POST的最大下标下分别已经是E1的后缀式和E2的后缀式了。因为他俩都归约完成了才能轮到这一条进行归约。那么只需要在k处(目前最小空闲下标)处放上op,再更新POST数组目前最小空闲下标k为k+1即可。
E -> E1 op E2	|{POST[k]:=op;k:=k+1}
E -> (E1)	|{}
E->id	|{POST[k]:=id;k:=k+1}

AST中间语言 #

略,见语法制导翻译部分。

DAG中间语言 #

DAG与AST的区别是:DAG的每个结点可能有多个父节点。

DAG是对AST的一种优化,**是基于AST改造而来。**它遵循如下方式构建。

  • 内部节点代表操作符,其孩子是该操作符的操作数
  • 每个子表达式,DAG中都有一个结点对应。但是DAG中的一个结点可能对应源表达式中多个完全相同的子表达式(公共子表达式)。换言之,公共子表达式对应的DAG结点可能有多个父节点。如下图所示。
image-20220617172630947

例如上图中,DAG把AST中的下面两棵子树合并成了同一个子图,这就消除了一定的冗余(可以实现后端代码优化)。而且这启发我们:DAG可以看做是在AST的基础上进行的改造,并不是新东西。

注意,边都是父节点指向子节点的,只是这里省略。

在代码实现上,每个符号仍然都有nptr属性。在构建过程中,也利用前面所说的mknode、mkleaf语义规则,其用法不变。

赋值语句的翻译 #

隐式类型转换的实现 #

简言之,每次隐式类型转换都会额外生成一个newtemp,额外生成一条三地址代码,从而把被转换的值转换为目标类型后,存到newtemp中,然后把后续其出现的三地址代码中的位置用这个newtemp替换。

例如x:=y + i*j,生成如下三地址代码。其中x、y是real,i、j是int。

T1 := i int* j
T3 := int_to_real T1
T2 := y real+ T3
x := T2

这里我们对不同类型数据类型之间的运算操作进行区分,例如real+代表对实数做加法。这样区分是有道理的,因为计算机的整数加法和浮点加法于不同的部件内进行。

对于各算术运算符,我们需要为其翻译模式加上类型转换的逻辑。也就是在翻译时多了一步类型判断,决定是否生成类型转换相关的三地址代码,逻辑如上例。

布尔表达式的翻译 #

nextstat,nextquad #

如果把中间代码输出文件看做是保存了由中间代码构成的数组,nextstat代表其中下一个空闲的索引。每次gen操作,都是把三地址代码放到nextstat位置,然后再自增nextstat。

如果中间代码是四元式形式,该变量可能又称nextquad,其记录了四元式的编号。约定俗成来讲,四元式的编号往往从100开始。

布尔表达式的本质是GOTO结构 #

布尔表达式可以翻译为if E then goto 真出口 else goto假出口,所以它实际上就是选择性跳转逻辑。

例如对a<b or c<d进行翻译。

  • 对于其本身,则a<b若真,则跳转到c<d的三地址代码前,否则直接跳过判断。
  • 如果它作为了控制语句的条件,那就是则a<b若真,则跳转到c<d的三地址代码前,否则直接跳到整体的假出口;如果c<d为真,则跳到整体的真出口,否则也跳转到假出口。

可见,布尔表达式实质上是由GOTO指令来实现相应的逻辑的,是一些GOTO和标号的组合。例如“c<d的三地址代码前、真出口”什么的都属于标号。

回填实现一遍扫描翻译 #

翻译的困难在于:在翻译布尔表达式本身时,还不能确定其出口地址。出口地址需要扫描到下面一段代码才能确定。但是我们只希望一遍扫描,所以需要有新的骚操作。

仅布尔表达式本身是无法确定跳转目标(即真假出口)的,必须等到更高层次的代码块被翻译完成,才能确定。

虽然对于布尔表达式的结构中,很多跳转指令的目标都不确定,但是无非只有两种结果,要么跳到真出口,要么跳到假出口。因为除了这两类目标外的其他跳转,肯定都是布尔表达式goto结构的必要跳转,这些都是可以当场填上的。

当真出口或假出口一旦被确定,一切的涉及到其的不确定的跳转都会被一次性填上。这一操作就是回填。为了实现“一次性填上”,可以把需要填的这种未完成的四元式的索引集中存储,到可以回填的时候就全部应用即可,为了做到这一点,为非终结符E增加综合属性truelist和falselist,他们是需要回填真出口标号和假出口标号的四元式的下标构成的链表。

  • 具体实现,可以把这些未完成的四元式的第四元当做链表的next域,自发构成逻辑链表。特别地,链表尾的第四元为0.
  • 例如现有未完成的四元式i:(a,b,c,0),j:(a,b,c,i),k:(a,b,c,j),且当前的E.truelist的值是k,那么当真入口确定了,就可以顺着E.truelist找到k,k找到j,j找到i,且在这个找的过程中一路上把第四元都换成真入口的四元式编号。

此外,还需要注意维护nextquad变量来记录新四元式的编号,并引入以下几个操作。

  • makelist(i)将会创建一个新的链表,它以第i个四元式为首。
  • merge(p1,p2)将会把p1和p2两个链表合并成一个,并返回新链表的表头。这个操作将会用在两条链上的四元式都希望跳转到同一个出口的情况。
  • backpatch(p,t)执行回填操作,把链表p上的所有四元式的第四元都改为t值。

布尔表达式可以翻译为if E then goto 真出口 else goto假出口,所以它实际上就是选择性跳转逻辑。值得注意的是,整个布尔表达式就算已经归约结束(到了E),其真出口和假出口仍是无法确定的。因此整个E仍然一直带着truelist和falselist,等待被更高的层次回填。

改良后的布尔表达式文法 #

考虑如下情况

  • 对于E -> E1 or E2,如果E1为假,那么下一步肯定是判断E2,则我们要记录E2的首个四元式的编号作为E1假出口。
  • 对于E -> E1 and E2,如果E1为真,那么下一步肯定是判断E2,则我们要记录E2的首个四元式的编号作为E1的真出口。

为什么整个产生式右部还没完全完成归约,就要急着记录?因为假设不记录,等到E1,E2全归约完了,现在轮到对E -> E1 or E2E -> E1 and E2进行归约了,那么这个布尔表达式的真出口或假出口是谁,可就找不到了!你没法回头去找哪里是E2的首个四元式的下标吧。

因此,需要在E1已归约完成,E2还没有处理的时刻,做“记录nextquad的值”这个动作。以E -> E1 or E2为例,就是形如E -> E1 or {rec}E2.

为了适配自底向上的语法分析,使之能在语法分析的过程中顺便进行语义分析(从而才能实现一遍扫描),我们需要保证每条翻译模式的语义动作都只能出现在产生式的末尾。像形如E -> E1 or {rec}E2肯定是不合适的。

为此,我们把语义动作尝试提出来。我们引入空产生式M->ε {rec},然后替换E -> E1 or {rec}E2E -> E1 or M E2,就解决了这个问题。

对于M,我们赋予其综合属性M.quad,表示其记录的那个四元式编号,并且相应的语义动作为{M.quad = nextquad}

因此,改良后的文法会有E -> E1 or M E2E -> E1 and M E2这种变化,并且新增了产生式M->ε这是该文法的唯一的三个改动。

引入三种船新的与跳转相关的四元式 #

gen(,id1.place,id2.place,dest); //id1和id2进行θ关系运算,若结果真则跳转到dest
gen(jnz,id.place,_,dest);//id为真则跳转到dest
gen(j,_,_,dest);//无条件跳转到dest

改良后的布尔表达式翻译模式 #

用于OR的产生式

E -> E1 or M E2 {
	backpatch(E1.falselist,M.quad);
	E.truelist := merge(E1.truelist,E2.truelist);
	E.falselist := E2.falselist;
}
  • 对于or而言,正如上文所述,E1的falselist可以立即得出,因为如果E1是假,则可以继续判断E2。这一步是为了构成or的逻辑关系,和整个E没有关系。
  • 对于整个产生式的truelist,其不确定,需要等待更高层次的翻译完成后进行回填。注意到E1.truelist和E2.truelist分别是“短路”情况下的那些跳转到真出口的四元式,和E1假,但是E2为真的情况下那些跳转到真出口的四元式,他们都要跳转到真出口,所以可以merge,而且merge后实际上就代表了整个布尔表达式E的truelist了。
  • 对于整个产生式的falselist,其不确定,需要等待更高层次的翻译完成后进行回填。注意到如果E2为假,那整个表达式都是假的,所以E2的假出口就代表了整个布尔表达式E的falselist了。

用于AND的产生式

  • 和上述逻辑对称。

用于NOT的产生式

  • 只需交换truelist和falselist。相当于交换了真出口、假出口的回填对象,从而实现逻辑上的相反处理。

用于关系运算符的产生式

E -> id1 relop id2{
	E.truelist := makelist(nextquad);
	E.falselist := makelist(nextquad + 1);
	gen(,id1.place,id2.place,0);
	gen(j,_,_,0);
}
  • 立即给这个E生成一个truelist和falselist,第一个是jθ四元式,第二个是j四元式。
  • 换言之,该E为真的情况下应当执行的代码的入口,被回填到nextquad处jθ四元式处,因为该四元式完成了id1 relop id2的操作,而且如果是真则会进行跳转。因为是为真才进行跳转,所以其第四元应该是E的真出口。
  • id1 relop id2为假,则那条jθ就不会跳转,就接着执行到nextquad+1处的j四元式。因为当前的情况是假,所以该j四元式只需要负责跳转到E的假出口即可。
  • 值得注意的是,这里生成的跳转四元式的第四元是0,因为他们等待被回填。如何让他们被回填?因为他们分别被指定为了truelist、falselist的首结点,所以他们会被回填。

用于单个id的产生式

E -> id{
	E.truelist := makelist(nextquad);
	E.falselist := makelist(nextquad + 1);
	gen(jnz,id.place,_,0);
	gen(j,_,_,0);
}
  • 不能说是一眼丁真,只能说是完全一致。这里就是关系运算符的产生式的翻版,把第一个gen换成了jnz。其逻辑是一样的,只是这里直接通过判断id的真假来进行跳转,而不是通过比较运算得出真假再进行跳转了。

控制流语句的翻译 #

控制流语句 #

控制流语句主要包括if-else语句、复合语句和while语句。

S -> if E then S
S -> if E then S else S
S -> while E do S
S -> begin L end
S -> A
L -> L;S
L -> S

其中S是Statement(语句),L是语句表(即所有的语句),A是赋值语句Assignment,E是布尔表达式Expression

if-then-else的改良文法 #

首先看if-else的语义。

if语句中的布尔表达式的真出口是then后的代码;假出口是else后的代码。在真代码最后要自动生成j四元式,从而自动跳过假代码。

image-20220618105224636

#

为了实现这一语义,我们来设计其改良文法。

S -> if E then M S1
S -> if E then M1 S1 N else M2 S2
M -> ε{记录nextquad}
N -> ε{生成j指令}

效仿前述的翻译模式设计方法,把M、N要做的动作提出来并增加其ε产生式。

这里M负责记录nextquad,因为S1肯定是真出口,S2肯定是假出口,当碰到他们即可对E进行回填。

Nextlist #

首先,E.truelist是要跳转到布尔表达式E的真出口的四元式构成的链表,我们仿照进行定义S.nextlist是要跳转到任意语句S(不一定是布尔表达式)的后续语句的四元式构成的链表。

注意,“后续语句”不等同于“后继语句”。 后续语句是逻辑上的概念,后继语句是物理上的概念。前者是在逻辑上,下一条将要执行的四元式,后者是在物理存储的位置关系上,下一条紧接着存储的四元式。我们的nextlist记录的是前者。

举例:在上述N.nextlist是要跳转到N的逻辑上的下条四元式的那些四元式。

if-then-else的改良文法(cont.) #

这里N负责生成语义中间那一条goto S.next无条件跳转指令。当然也需要回填。为了实现其回填,我们引入nextlist,其作用和前面俩list一样,只是用于记录待回填的四元式。它回填的结果的整个E之后的代码入口。因为对于多个if-then-else结构嵌套的情况,其中可能不止一个跳转需要指向该if-then-else语句之后的位置。

值得注意的是,nextlist是任意语句S的属性,S可以包括E。但是truelist仅是布尔表达式E的属性。

M -> ε{
	M.quad := nextquad;
}

N -> ε{
	N.nextlist := makelist(nextquad); !指明了要用N的在逻辑上要执行的下一条四元式的编号来回填
	gen(j,_,_,0); !等待回填
}
S -> if E then M S1{
	backpatch(E.truelist,M.quad);
	S.nextlist := merge(E.falselist,S1.nextlist);
}

对于if-then结构,真出口肯定是then。而假出口实际上就是整个语句的后继,所以和nextlist二者合并。

S -> E then M1 S1 N else M2 S2{
	backpatch(E.truelist,M1.quad);
	backpatch(E.falselist,M2.quad);
	S.nextlist := merge(S1.nextlist,N.nextlist,S2.nextlist)
}

对于if-then-else结构,E的真出口肯定是then,E的假出口肯定是else,所以这俩list都能直接回填。

S1、S2是S的子语句,他们跳出整个S的结构的出口就是S的后继。对于该if-else-then语句,其中的N想要跳出,也是走S的后继,因此三者可以合并。

值得注意的是,nextlist也是当S整个翻译完成仍然不能确定的,需要更大的语法单位的语句翻译完成后再回填。例如if-else嵌套结构,内层的想要完全跳出整个嵌套结构,其nextlist肯定和外层的nextlist一样,都要回填为外层if-else语句的后继。即便是这个最外层的nextlist也无法在其本身翻译完成后被确定,因为谁知道是不是最外层。

while语句的改良文法 #

按照惯例,先看语义。

image-20220618113202638

E的真出口是循环体,E的假出口是循环体后继。循环体的末尾自动生成j指令来跳转到E。

S -> while M1 E do M2 S1
M -> ε

M仍然用于记录nextquad,这里分别记录布尔表达式E和循环体S1的四元式编号。

S -> while M1 E do M2 S1{
	backpatch(E.truelist,M2.quad);
	backpatch(S1.nextlist,M1.quad);
	S.nextlist := E.falselist;
}

理解同上文。

复合语句的改良文法 #

复合语句即语句块或几个语句之间用分号隔开,依次执行:

S -> begin L end
L -> L1; M S | S !S是和 L1; M S并列的,不是和MS
M -> ε

对于begin-end语句,只需要把需要回填的nextlist交给S即可,因为仅靠begin和end不能确定回填入口。

对于分号,就是前面L1所有nextlist的回填的时机了,因为nextlist的语义就是“逻辑上的下一个执行的东西”,分号就提供了这样一种逻辑,L1的后续语句实际上就是S,所以只需要把S的四元式编号回填给L1的nextlist即可。这里用M来记录。

给出翻译模式,即:

S -> begin L end{
	S.nextlist := L.nextlist; !L的nextlist还不能确定,需要等待更大的语法单位翻译完成
}

L -> L1; M S{
	backpatch(L1.nextlist,M.quad); !分号的语义:L1后续执行的语句就是S本身!
	L.nextlist := S.nextlist; !S的nextlist还不能确定,需要等待更大的语法单位翻译完成
}

M -> ε{
	M.quad := nextquad;
}

其他几个不重要语句如何传递nextlist #

L -> S{
	L.nextlist := S.nextlist;
}

S -> A{
	S.nextlist := makelist();
}

值得注意的是S -> A的翻译模式:A是赋值语句,它不需要做任何的回填,所以传递给S一个空的nextlist。

符号表 #

作用和内容 #

  • 用于登记各标识符信息
  • 在语义分析阶段,用于分析语义的正确性(符号的引用是否正确);用于分析作用域
  • 在目标代码生成阶段,用于辅助按照符号名进行地址分配,从而方便目标代码的生成

结构 #

符号表以符号名作为主键。各行完全都是通过名字来区分的。

在其余列上会存储该符号的相关信息。因为不同类型的符号可以带有不同的信息,例如过程名和变量名肯定是不同的,所以在一些处理中,选择建立多张符号表,例如变量表、常量表、过程表。很多处理中也会建立一张符号表,在“信息”一栏增加其类型的体现。

使用 #

简言之,在两个时机会对符号表进行操作,一个是符号被声明时,一个是符号被引用时。例如int a = 1;if(a > 1)