Good in study, attitude and health

编译器-PL0解释程序

这次实验不再进行详细说明,参考资料中都讲的很好,主要针对难点进行阐述,难点懂了,这个实验几乎就完成了 ✅。

难点


DL 和 SL


这次实验我一直搞不懂 DL 和 SL 的区别,看了文章也还没搞懂,最终最终问了 AI,让它帮我详细解释解释,才慢慢搞懂。

其实,结合代码来讲是最“形象”🉐。以下的代码来自我的PL0-Compiler仓库中的测试样例 2。

var x;
procedure B;
    var y;
    begin
        y := x;
    end;

procedure A;
    begin
        call B;
    end;


begin
    x := 1;
    call A;
end.

上述代码的调用关系为:

  • 主函数(main)–调用->A
  • A–调用->B

SL 静态链和 DL 动态链都记录了当前层的上一层的信息,但这上一层和上一层之间也并不是完全一样。

  • SL 静态链:从代码中看程序的嵌套结构,过程 A 和 B 的上一层都是主程序,它们都是在主程序中定义的过程。

  • DL 动态链:从(函数)调用的角度看,因为主程序调用了 A,所以主程序是 A 的上一层,又因为 A 调用了 B,所以 A 又是 B 的上一层。这就有点不一样了,B 的上一层在代码层面明明是主程序,而在这里却成了 A。

这次我们发现了 DL 和 SL 的不同。而我们进行变量查找时,当前层次没找到,沿着静态链 SL 去上一层查找。我们进行过程(函数)返回时,需要恢复基址寄存器,这时是按照 DL 返回的。

下面是 AI 的解释:

DL (动态链):

  • 记录的是调用关系(运行时动态调用的顺序)。
  • 表示调用者,只知道当前子程序是由哪一个子程序调用的。
  • 回溯关系是 动态的,依赖实际的调用栈。

SL (静态链):

  • 记录的是词法作用域关系(程序的嵌套结构)。
  • 表示定义者,知道当前子程序是在哪一个作用域中定义的。
  • 回溯关系是 静态的,依赖编译时的嵌套层次。

这里可能会有疑问 🤔,为什么必须按照 SL 去找变量,如果按照 DL 去找有什么问题呢?

var x;
procedure B;
    var y;
    begin
        y := x;
    end;

procedure A;
    var x;
    begin
        x := 2;
        call B;
    end;


begin
    x := 1;
    call A;
end.

我们详细来分析一下,先将之前的代码改一下,给过程 A 也加一个变量 x 并赋值为 2,那么 B 中的 y 会被赋值为 1 还是 2 呢?

如果是按照 SL 肯定是 1,DL 就是 2。那这对不对呢?其实是不对的,我们通过 C 语言来看一下:

...
int x = 1;
void B() {
    y = x;
}
void A(){
    int x = 2;
    B();
}

int main() {
    A();
    return 0;
}

你认为上述代码中的 y 应该是几?此时你应该知道为什么不能按照 DL 找了 8️⃣。

层次差


之前语法分析部分埋下的坑终于要填了。我们之前计算层次差会在哪里计算呢?

  • 变量的调用部分:当前层次的 level - 符号表中声明时的 level
  • 过程的调用部分:CAL 指令生成时,CAL 指令的第一个参数就是层次差

下面我们分开来分析:

首先是变量,如果层次差为 0,说明这个变量的声明和调用都是在同一层次的,就例如下面的 x 和 y:

var x // 声明
procedure m
    var y // 声明
    begin
        y := y + 1; // 调用
    end;
begin
    x := x + 1; // 调用
end.

如果层次差为 1,说明变量的调用比声明多了一层,比如下面的 x:

var x // 声明
procedure m
    var y // 声明
    begin
        y := x + 1; // 调用
    end;
begin
    call m;
end.

这时候我们想要给 y 赋值发现这一层竟然没有 x,那我们不就得不断地向上一层找 x,如果一直找不到,那说明有语义错误,如果找到了就把它的值拿过来。而我们现在就要理解它不断向上层找的过程。根据上面我们对于 SL 和 DL 的理解,这里应该顺着 SL 找,是很容易知道的。SL 记录了代码嵌套结构中该层的上一层,根据上述实际的代码来看:m 的上一层其实是主函数,主函数的 level 是 0,而 procedure 里的变量的 level 都是 1,它们之间有层次差 1,所以就根据自己的 SL,找到主函数的基址,并根据偏移量得到了 x 值,这样就可以进行运算了。

如果层次差为 2 怎么办,其实就是多找一层,每一层都有 SL,就像下面的代码,y 变量的层次为 2,但 x 的层次为 0,之间的层次差为 2。n 的 SL 会获取到 m 的基址,从基址就能得到 m 的 SL,我们再从 m 的 SL 获取到主函数的基址,从而根据偏移量找到 x。

var x
procedure m
    procedure n
        var y
        begin
            y := x + 1;
        end;
    begin
        call n;
    end;
begin
    call m;
end.

之后随着层次差的增大就是一个重复的过程,但我们规定代码中最多嵌套三层,所以层次差最多为 3。

然后是过程的调用,和变量几乎一样。还是拿上面的代码来说明:

var x
procedure m
    procedure n
        var y
        begin
            y := x + 1;
        end;
    begin
        call n;
    end;
begin
    call m;
end.

m 的层次为 0,n 的层次为 1,调用都是同层的,所以生成的 CAL 指令第一个参数都是 0,而这时单纯分析代码也能看出,DL 和 SL 其实都是一样的。

var x;
procedure B;
    var y;
    begin
        y := x;
    end;

procedure A;
    begin
        call B;
    end;

begin
    x := 1;
    call A;
end.

上面代码 A 调用 B 的时候就有了层次差,因为 A 和 B 的层次都是 0(在主函数中声明和定义),但是调用 B 是在 A 内部进行的,A 内部的层次为 1,所以会有一个 1 的层次差。和变量不同的是,这次我们不会顺着 SL 向上找变量,而是顺着 SL 只要找到上一层的基址就可以了,省略了根据偏移量找变量的过程,其实还变得简单了。

这一部分其实还是加深对于 SL 的理解,虽然这里的说明是先是变量后是过程,但实际上应该是先是过程将自己层的 SL 字段给完善了,后续的变量才能根据自己层的 SL 向上层找其他的变量。

执行过程模拟(手动


上述的代码(第一个代码段)生成的中间代码为:

// 指令数组
codelist:
0 F: JMP L: 0 A: 10
1 F: JMP L: 0 A: 2
2 F: INT L: 0 A: 4
3 F: LOD L: 1 A: 3
4 F: STO L: 0 A: 3
5 F: OPR L: 0 A: 0
6 F: JMP L: 0 A: 7
7 F: INT L: 0 A: 3
8 F: CAL L: 1 A: 1
9 F: OPR L: 0 A: 0
10 F: INT L: 0 A: 4
11 F: LIT L: 0 A: 1
12 F: STO L: 0 A: 3
13 F: CAL L: 0 A: 6
14 F: OPR L: 0 A: 0

符号表:

{address=3, level=1, kind=SYM_VAR, name=x}
{address=1, level=1, kind=SYM_PROCEDURE, name=B}
{address=3, level=2, kind=SYM_VAR, name=y}
{address=6, level=1, kind=SYM_PROCEDURE, name=A}

因为这个程序比较简单,下面我们手动模拟一遍:

先定义各个寄存器和栈空间:

B:0 // 基址寄存器
T:-1 // 栈顶指针
code:null // 要执行的指令
P:0 // 要执行的下一条指令的下标(指令数组的下标

null
stack

首先,获取 JMP 无条件跳转指令

B:0
T:-1
code:JMP,0,10 // 会修改P
P:10

null
stack

跳转到第 10 条指令,分配栈空间

B:0
T:3
code:INT,0,4
P:11

3 0 x  <- T
2 0 RA
1 0 DL
0 0 SL <- B
stack

将常量 1 放入栈顶

B:0
T:4
code:LIT,0,1
P:12

4 1 常量 <- T
3 0 x
2 0 RA
1 0 DL
0 0 SL  <- B
stack

将常量存储到变量 x 中

B:0
T:3
code:STO,0,3
P:13

4 1 常量
3 1 x  <- T
2 0 RA
1 0 DL
0 0 SL <- B
stack

调用 A

B:4
T:4
code:CAL,0,6
P:7

6  14  RA
5  0   DL
4  0   SL <- T/B
3  1   x
2  0   RA
1  0   DL
0  0   SL
stack

跳转

B:4
T:4
code:JMP,0,7
P:7

6  14  RA
5  0   DL
4  0   SL <- T/B
3  1   x
2  0   RA
1  0   DL
0  0   SL
stack

分配空间

B:4
T:6
code:INT,0,3
P:8

6  14  RA <- T
5  0   DL
4  0   SL <- B
3  1   x
2  0   RA
1  0   DL
0  0   SL
stack

调用 B

B:7
T:7
code:CAL,1,1
P:1

9  9   RA
8  4   DL
// 通过代码中get_sl计算
7  0   SL <- T/B
6  14  RA
5  0   DL
4  0   SL
3  1   x
2  0   RA
1  0   DL
0  0   SL
stack

跳转指令

B:7
T:7
code:JMP,0,2
P:2

9  9   RA
8  4   DL
7  0   SL <- T/B
6  14  RA
5  0   DL
4  0   SL
3  1   x
2  0   RA
1  0   DL
0  0   SL
stack

分配空间

B:7
T:10
code:INT,0,4
P:3

10 0   y  <- T
9  9   RA
8  4   DL
7  0   SL <- B
6  14  RA
5  0   DL
4  0   SL
3  1   x
2  0   RA
1  0   DL
0  0   SL
stack

将 x 的值移到栈顶

B:7
T:11
code:LOD,1,3 //层差是1,需要沿着SL到上一层(main),然后获取B+3(在这里为3)地址x的值
P:4

11 1   x  <- T
10 0   y
9  9   RA
8  4   DL
7  0   SL <- B
6  14  RA
5  0   DL
4  0   SL
3  1   x
2  0   RA
1  0   DL
0  0   SL
stack

将栈顶的值存到变量 y 中

B:7
T:10
code:STO,0,3
P:5

11 1   x
10 1   y  <- T
9  9   RA
8  4   DL
7  0   SL <- B
6  14  RA
5  0   DL
4  0   SL
3  1   x
2  0   RA
1  0   DL
0  0   SL
stack

过程返回

B:4
T:6
code:OPR,0,0
P:9

11 1   x
10 1   y
9  9   RA
8  4   DL
7  0   SL
6  14  RA <- T
5  0   DL
4  0   SL <- B
3  1   x
2  0   RA
1  0   DL
0  0   SL
stack

过程返回

B:0
T:3
code:OPR,0,0
P:14

11 1   x
10 1   y
9  9   RA
8  4   DL
7  0   SL
6  14  RA
5  0   DL
4  0   SL
3  1   x  <- T
2  0   RA
1  0   DL
0  0   SL <- B
stack

程序结束

B:0
T:-1
code:OPR,0,0
P:0 // 程序结束

11 1   x
10 1   y
9  9   RA
8  4   DL
7  0   SL
6  14  RA
5  0   DL
4  0   SL
3  1   x
2  0   RA
1  0   DL
0  0   SL <- B
stack

参考资料