上节符号表的部分只是涉及到一个具体的思路,这节才是具体的实现,考试很重要!
一、符号表管理
1.1 语义分析部分三个核心知识点
- abcd选择和当前位置最近的声明性出现
- 映射关系建立之后,变量的名字即无用,知道语义信息即可
1.2 处理原则
符号表是在程序运行到某一个位置的时候是有效的,考试的时候一般都会给出一个具体的时刻然后写出这个时刻的符号表。
1.3 Pascal语言的符号表的管理
- Scope栈中的每个区域指向符号表中当前仍有效的每个嵌套作用域的开始地址。
- 在Pascal语言中允许嵌套过程声明所以层数可以>1
Pascal语言的符号表的实例(删除法)
删除法是把整个当前退出的层的符号表删除,并删除指向当前层符号表起始位置的Scope栈中的指针。
分析过程
-
首先进入主程序,第一层为0,即P。Scope(0)指向符号表第一个元素P的位置。
-
然后进入主程序,定义了
var i,j,k
三个integer类型的变量,此时层数+1,Scope(2)指向当前层,同时下一个函数(局部化区的开始)写入符号表。
-
继续进入函数Q的内部,执行
var a,b
a和b写入符号表,同时下一个函数R的局部化区域开始也进入符号表。Scope(3)指向当前层的开始位置。
4. 进入函数R内部,执行定义var a,b
将a和b写入符号表,同时Scope(4)指向当前区域的起始位置。
5. 执行使用性说明即R的过程体之后,退出当前局部化区域,删除符号表中属于当前区域的全部元素,并将Scope栈中的指针清除。并继续执行Q的过程体,Q函数部分结束,将符号表中所在层的全部内容删除,并删除Scope栈中的相应指针。同时进入新的procedure即函数S,将S写入符号表中。
6. 继续分析函数S,执行var c,e
c和e写入符号表
7. 执行S的过程体,将所有S的部分清除
8. 执行P的过程体,将Scope(1)指向的部分清除
关于Pascal语言的符号表的实例(跳转法)
主要是在执行完相应局部区域的函数体之后没有采用将符号表的内容删除的方式,也没有将Scope栈中的内容删除,而是在符号表中开辟另外的空间存储相应的指向局部化区域头部的指针,原来的局部化区域中的内容并没有被删除,而是在从下到上顺序查找符号表的时候将原来的区域略过。
- 前面的部分和删除法相同
2. 当局部化区域退出的时候,重新开辟一部分空间指向当前局部化区域的头部,然后进入新的局部化区域即函数体S,将S入符号表,同时Scope部分继续生成指针指向符号表中的S位置。
3. 继续向下执行相应函数,将c和e入符号表,然后执行S的过程体(这个时候执行S的过程体会出现访问错误但是也继续执行),这个时候删除Scope栈中指向当前过程体的指针,并在符号表中生成一个新的指针指向当前局部化区域S的头部。
然后执行P的过程体,再生成一个指针指向P过程体的头部,同时删除Scope栈中的指针。
1.4 C语言的符号表管理
- C语言不允许函数的嵌套声明。
- 一个C程序是由若干个函数并列组成的(其中一定要有main函数),这些函数的地位都是相同的。从main函数调用开始执行,在后面声明的函数可以调用在它之前声明的函数。
- C语言的每个标识符的层次只有两个,即0层和1层。全局标识符和所有的函数标识符的层数是0,函数的形式参数和局部标识符的层数是1。
(1)简单的C语言符号表——驻留法
- x和y属于全局变量,在当前符号表中的层数为0(x、y与其他的存储区域不同),swap函数进入局部化区域入口层数也是0(后面的size前的部分指向当前的变量入口,考试的时候不写也行)
- 然后是参数a、b和定义的局部变量temp写入符号表,他们的层数都是1,Off偏移从Off0开始根据每个变量的类型向上累加
- 在退出当前局部化区域的时候同理生成一个指针指向当前局部化区域开始位置
- 向下继续是main函数,main入符号表,执行完函数体之后生成一个指针指向main入口
- 最后一个C语言程序执行结束之后需要再生成一个指针指向符号表的最顶端。
(2)带有分程序的C语言全局符号——驻留法
带有分程序的,每一个{}
开始的区域都使用指针指向用来进行标记,而且后面的每个变量的偏移都是在前一个变量偏移的基础上的。后面的d可以有两种取值的原因是,考虑了跳过上面的c和d,实际上在退出上一个局部化区域之后,c和d就不会被使用了,接上面的d部分。
(3)带有分程序的C语言全局符号——删除
和之前的删除没有什么区别
1.5 嵌套式语言并列式语言的比较
- 特殊情形,从程序设计语言的角度来说有嵌套式语言和并列式语言
- 从处理难度角度来说,可能嵌套式语言复杂,并列式简单。 嵌套式语言里还要考虑变量运行环境。
- 并列式语言的局部化区比较固定,层数分成两层,全局量定义成0层,局部量定义成1层。分配地址特殊情形就是分程序结构,分程序可以是嵌套的,他的层数都是看成同一层,查局部化区每个分程序看成一个独立的局部化区。
- 分配抽象地址的时候为了节省空间,并列的分程序可以是并行的分配,嵌套的分程序必须要连续的往下分,前一个分程序从10100,并列的可以还是10100。
二、goto语句
2.1 goto语句及‘goto之争’
2.2 标号的出现形式
标号在程序中出现共有三种形式
- 声明性出现 在程序的声明部分给出一个声明,声明哪些标识符是标号。例如:label R1,R2
- 定位性出现 起到定位的作用,例如: R1:s, s是一个语句,R1是一个语句标号,表示R1这个语句标号定位在s语句前面
- 转移性出现 例如goto R1, 即把程序的控制结构转到标号为R1的位置上开始去执行
2.3 可能出现的错误
- 标号的重复声明
- 标号的重复定位
- 使用的标号未声明(语言决定的)
- 有转移但是无定位
2.4 实现方式
- 使用第一种方式的时候,目标程序中产生一个直接的跳转指令指向一个地址,因为跳转地址一开始没有方法确定,只有在定位性标好出现的时候才能确定
- 使用第二种方式,为每一个标号分配一个存储单元,遇到goto命令的时候就产生一个跳转指令,真正实现的时候会从这个存储单元中读取该标号定位的地址,而遇到定位性标号的时候已经知道了对应代码的入口地址,就把该地址存到对应的存储单元中,这样的话从实现的角度来说就变得比较简单。
上述内容是在目标代码生成的时候需要完成的,现在应该处理的内容如下:
- 标号表的作用:
设置状态有定位填1,没有定位填0的目的是检查有无重复定位,有没有这个定位。 - 未找到定位标号表作用:
记录有转移但是还没有发现定位的情况,goto语句的转移不只是往前转移,不是定位的标号出现了才可以转移,也有可能是要往程序的后面转,比如goto L,而L在goto语句的后面,那么当处理到goto语句的时候没有发现L有定位,没有定位并不意味着错误,这个时候要把没找到定位的标号保存起来,当一个程序单位结束的时候,再去标号表中去查找如果有定位按照定位处理如果没有定位才说没有定位。
2.5 实现算法
如果遇到转移性标号的时候(goto的时候),到标号表中去找,找有没有这个标号,找到就找到了。如果找到这个标号状态是0,并不意味着它出错了,而是要把这个标号存在另一个表里,表示该标号目前没有找到定位,一直处理到整个程序单位结束的。
示例:
首先根据r1,r2构建标号表,当前的标号为0,然后r1定位使用r1位置变成1
然后执行goto r2,但是当前标号表中的r2位置是0,将r2先写入未找到定位标号表中,等待程序段完成进行复查。
接着执行goto r1,在标号表中r1的值为1,🙆;继续向下执行有r2定位,将标号表中的r2标号改成1,最后复查未找到标号表,检查r2对应符号表中的值为1,🙆。