PL/0语言

本文详述了PL/0编译器的设计与实现,包括词法分析、语法分析、语义分析和错误处理。编译器采用Java编写,目标代码为栈式计算机的汇编语言。程序通过BNF描述,采用递归下降法进行语法分析,符号表管理处理变量、常量和过程的作用域,同时实现了目标代码生成和解释执行。此外,文章还讨论了活动记录的组织和错误处理机制,确保程序的正确编译和执行。
摘要由CSDN通过智能技术生成

一、设计任务

1.1程序实现要求

PL/0语言可以看成PASCAL语言的子集,它的编译程序是一个编译解释执行系统。PL/0的目标程序为假想栈式计算机的汇编语言,与具体计算机无关。

PL/0的编译程序和目标程序的解释执行程序都是用JAVA语言书写的,因此PL/0语言可在配备JDK的任何机器上实现。

其编译过程采用一趟扫描方式,以语法分析程序为核心,词法分析和代码生成程序都作为一个独立的过程,当语法分析需要读单词时就调用词法分析程序,而当语法分析正确需要生成相应的目标代码时,则调用代码生成程序。

用表格管理程序建立变量、常量和过程标示符的说明与引用之间的信息联系。

用出错处理程序对词法和语法分析遇到的错误给出在源程序中出错的位置和错误性质。

当源程序编译正确时,PL/0编译程序自动调用解释执行程序,对目标代码进行解释执行,并按用户程序的要求输入数据和输出运行结果。

1.2 PL/0语言的BNF描述(扩充的巴克斯范式表示法)

<prog> → program <id>;<block>

<block> → [<condecl>][<vardecl>][<proc>]<body>

<condecl> → const <const>{,<const>};

<const> → <id>:=<integer>

<vardecl> → var <id>{,<id>};

<proc> → procedure <id>([<id>{,<id>}]);<block>{;<proc>}

<body> → begin <statement>{;<statement>}end

<statement> → <id> := <exp>

               |if <lexp> then <statement>[else <statement>]

               |while <lexp> do <statement>

               |call <id>([<exp>{,<exp>}])

               |<body>

               |read (<id>{,<id>})

               |write (<exp>{,<exp>})

<lexp> → <exp> <lop> <exp>|odd <exp>

<exp> → [+|-]<term>{<aop><term>}

<term> → <factor>{<mop><factor>}

<factor>→<id>|<integer>|(<exp>)

<lop> → =|<>|<|<=|>|>=

<aop> → +|-

<mop> → *|/

<id> → l{l|d}   (注:l表示字母)

<integer> → d{d}

注释:

<prog>:程序 ;<block>:块、程序体 ;<condecl>:常量说明 ;<const>:常量;<vardecl>:变量说明 ;<proc>:分程序 ; <body>:复合语句 ;<statement>:语句;<exp>:表达式 ;<lexp>:条件 ;<term>:项 ; <factor>:因子 ;<aop>:加法运算符;<mop>:乘法运算符; <lop>:关系运算符。

1.3假想目标机的代码

LIT 0 ,a 取常量a放入数据栈栈顶

OPR 0 ,a 执行运算,a表示执行某种运算

LOD L ,a 取变量(相对地址为a,层差为L)放到数据栈的栈顶

STO L ,a 将数据栈栈顶的内容存入变量(相对地址为a,层次差为L)

CAL L ,a 调用过程(转子指令)(入口地址为a,层次差为L)

INT 0 ,a 数据栈栈顶指针增加a

JMP 0 ,a无条件转移到地址为a的指令

JPC 0 ,a 条件转移指令,转移到地址为a的指令

RED L ,a 读数据并存入变量(相对地址为a,层次差为L)

WRT 0 ,0 将栈顶内容输出

代码的具体形式:

其中:F段代表伪操作码

      L段代表调用层与说明层的层差值

      A段代表位移量(相对地址)

进一步说明:

INT:为被调用的过程(包括主过程)在运行栈S中开辟数据区,这时A段为所需数据单元个数(包括三个连接数据);L段恒为0。

CAL:调用过程,这时A段为被调用过程的过程体(过程体之前一条指令)在目标程序区的入口地址。

LIT:将常量送到运行栈S的栈顶,这时A段为常量值。

LOD:将变量送到运行栈S的栈顶,这时A段为变量所在说明层中的相对位置。

STO:将运行栈S的栈顶内容送入某个变量单元中,A段为变量所在说明层中的相对位置。

JMP:无条件转移,这时A段为转向地址(目标程序)。

JPC:条件转移,当运行栈S的栈顶的布尔值为假(0)时,则转向A段所指目标程序地址;否则顺序执行。

OPR:关系或算术运算,A段指明具体运算,例如A=2代表算术运算“+”;A=12代表关系运算“>”等等。运算对象取自运行栈S的栈顶及次栈顶。

1.4假想机的结构

两个存储器:存储器CODE,用来存放P的代码数据存储器STACK(栈)用来动态分配数据空间

四个寄存器:

一个指令寄存器I:存放当前要执行的代码

一个栈顶指示器寄存器T:指向数据栈STACK的栈顶

一个基地址寄存器B:存放当前运行过程的数据区在STACK中的起始地址

一个程序地址寄存器P:存放下一条要执行的指令地址该假想机没有供运算用的寄存器。所有运算都要在数据栈STACK的栈顶两个单元之间进行,并用运算结果取代原来的两个运算对象而保留在栈顶

1.5活动记录:

RA:返回地址

SL:保存该过程直接外层的活动记录首地址

DL:调用者的活动记录首地址

过程返回可以看成是执行一个特殊的OPR运算

注意:层次差为调用层次与定义层次的差值

二、功能结构设计

2.1综述

本PL/0编译器共包括词法分析、语法分析、语义分析(包括符号表管理和目标代码生成)、活动记录的组织(解释执行程序)、错误处理四大部分组成。

2.2详细说明

2.2.1词法分析

根据所给的PL/0语言的BNF描述,该语言的组成单词包括以下元素:

关键字(程序保留字){program,const,var,procedure,begin,end,if,else,then,call,while,do,read,write}

运算符:{+,-,*,/,odd}

界符:{“,”,“;”,“(”,“)”}

关系运算符:{=,<,<=,>,>=,<>}

数字:只能为整型,且常量不可以是负数

标识符:由用户定义,以字母开头,由数字和字母组成

词法分析程序读取源文件,识别出上述关键字、界符、关系运算符、数字、标识符五种元素,输出到lex.txt文件中,供后续的语法分析程序使用。其中,

(1)除标识符外,剩余的每一种字符可以用数字表示;

(2)而标识符则统一用一个数字表示其为标识符,再将标识符本身存储起来;

(3)数字的存储和标识符存储类似。

例如:

将关键字、界符、关系运算符共29中符号从1到29依次编号,1表示关键字program,2表示关键字const,以此类推,到29表示关系运算符<>,

为了和下面标识符、数字的存储统一起来,将识别出的关键字以如下形式

保存在lex.txt文件中。如program则存储为(1,-)。

用30表示是标识符,如定义了一个变量v1,则其存储为(30,v1)。

用31表示是数字,如12,其存储为(31,12)。

2.2.2语法分析

语法分析结合BNF产生式,利用递归下降的方法实现。具体实现方式是,为每一个非终结符编写一个子程序,以<prog> → program <id>;<block>此产生式为列,用伪码描述其子程序如下:

void prog(){

if(当前指向的字符==program){

           指向下一个字符;

           if(当前指向的字符==id){  //是标识符

    指向下一个字符;

    if(当前指向的字符==;){

    block();

}else{

    error;

}

}

}else{

    error

}                       

}

说明:由于之前的词法分析使用数字来描述的,故此处的program,id(即标识符),分号等都可以用相应的数字代替。

2.2.3语义分析

语义分析在语法分析的基础上完成,涉及到的操作有符号表的管理和目标代码的生成,分别对应说明语句和处理语句,下面分开来说。

(1)符号表管理

符号表中存储以下数据:定义的变量、定义的常量、定义的过程;

定义的变量需要存储:变量的标识符,变量定义所在层次,相对于该层次基地址的偏移量(对于基地址将在后面活动记录中详细说明)

定义的常量需要存储:常量的标识符,常量的值,定义所在层次

定义的过程需要存储:过程名,过程处理语句的开始地址(处理语句不是说明语句,说明语句中涉及到符号表的操作,而处理语句中涉及到产生目标代码的操作),过程定义所在层次;

具体操作用伪码描述:

a.常量说明的翻译:

void condecl(){

    if(当前指向的字符==const){

       指向下一个字符;

       const();

       while(当前指向的字符==,){

           指向下一个字符;

           const();

       }

    }

}

void const(){

    if(当前指向的字符==id){

       指向下一个字符;

       用name记录下字符;

       if(当前指向的字符== := ){

           指向下一个字符;

           if(当前指向的字符==数字){

              用value记录下值;

              enter(name,value,level,addr);//将其记录到符号表

           }

       }

    }

}

b.变量说明的翻译:

void vardecl(){

    if(当前指向的字符==id){

       用name记录下字符;

       enter(name,level,addr);

       指向下一个字符;

       while(当前指向的字符==,){

           指向下一个字符;

           if(当前指向的字符==id){

              用name记录下字符;

              enter(name,level,addr);

              指向下一个字符;

           }

       }

    }

}

c.过程说明的翻译:

proc的登录符号表的操作与上述方法类似,不再赘述。

d.补充

但是上面我们并没有涉及如何求得层次差和偏移量,获得层次差和偏移量的思路大体如下,在整个语法分析函数中,定义两个全局变量level和address,level初始化为1,其中主程序是第一层,每次遇到过程嵌套定义就将level加1,过程定义结束后再将其减1,这样下来就实现了获得说明语句所在层次的功能;address初始化为0,每次在符号表中登录变量后就将address加1,其实address的作用是,结合level,可以获得每一层有几个变量,这样在活动记录的分配(也叫运行时存储空间的划分,运行时只为变量事先划分存储空间),可以根据该层变量的个数划分相应的存储空间。

(2)目标代码的产生

这部分涉及到翻译模式相关的知识,由于说明语句是不产生目标代码的,在结合PL/0的BNF描述,会产生目标代码的只有<block>、<body>、<statement>、<lexp>、<exp>、<term>、<factor>、<lop>、<mop>,下面一次用伪码说明其翻译模式:

a.<block>:

分析可以发现,主程序除去program<id>;,过程除去procedure<id>([id{,id}]),剩下的部分都是block。则进入block有两种情况:(1)从主程序进入,此时符号表中没有任何元素,也没有产生任何目标代码;(2)从过程进入,此时符号表中有了该子过程所有外层过程定义的变量,常量和过程,并且还产生了一部分目标代码;对于第一种情况比较简单,不再赘述,而对于第二种情况,则比较复杂。现在我们将主程序处理语句开始的地址叫做程序的入口,由于嵌套过程的存在,导致寻找程序入口有些麻烦,在这里我们用到了回填技术:

l  在进入block的第一步就先产生一个无条件跳转指令,由于中间子过程嵌套还会产生目标代码 ,导致不知道跳转到哪里才是主程序的入口,这里我们先记下这个无条件跳转指令在目标代码中的位置,当程序分析完变量说明、常量说明和过程说明即将进入语句处理的分析时,这时目标代码的地址就是主程序的入口,我们根据刚才记下的无条件跳转指令的位置将这个地址回填到跳转指令的目标地址上,从而目标代码第一句就是跳转到主程序入口处的指令,对于程序中嵌套程序,用这个方法依然是可行的;

l  除此之外,在进行其他任何分析之前,还要记录下主程序或者当前过程的数据量,即符号表中的变量数目,也即address,以便后面活动记录的划分可以在结束过程后返回到原来的数据栈的位置;

l  另外,如果是从过程进入到block,在产生过程调用指令需要用到这个入口地址,所以这个过程入口地址也要记录下来,用来填写过程调用指令的目标位置。则在进行其他任何分析之前,此时符号表中最新的一项必定是该过程的相关信息,我们可以将本过程的value值设为其入口地址,因为value值对于符号表procedure来说是无用的,即将开始地址登入到符号表,从而在做任何分析前我们也要记录下符号表中的最新项(记录其序号),在经过变量说明分析、常量说明分析、过程分析后即将进入语句处理分析时,便得到了该过程的入口地址,将其登录到刚刚记录的那个符号表最新项的value域;

可以通过判断符号表中的最新项的序号是否为0来判断是从过程进入<block>还是从过程进入<block>

进行完上述三步操作后即可进入语句的处理部分;

在语句的处理部分完成后,生成目标代码的退出过程或主程序的语句,然后恢复address和符号表的序号。

b.<body>

此部分不直接产生目标代码

c.<statement>

<statement>中有赋值语句(<id>:=<exp>)、if语句、while语句、call语句、read语句、write语句,下面分别说明其翻译模式:

l  赋值语句:此部分涉及到活动记录的组织,先大致说一下:本目标机是哈佛架构,数据存储器和代码存储器是分开的,这里活动记录的组织或者说运行时存储空间的组织是指数据存储器的组织,(代码存储器不需要什么组织,目标代码顺序存放,解释程序按目标代码的意义进行相关控制流的操作等),对于每一个过程或主程序,都要开辟3+变量个数的存储空间,其中3个是用来存放SL,DL,RA的,这里我们遇到一个变量就生成STO L,A目标代码,其中L和A据可由符号表中存储的相关信息获得,意思是将此时数据栈栈顶的元素,通过层差L(调用位置和定义位置之差)和偏移地址A找到这个变量在其定义的过程开辟的空间中为该变量开辟的存储空间,然后将栈顶元素存到这个位置,具体的生成看代码;

l  if语句:用伪码描述

if <lexp>

产生条件转移指令“JPC,0,0”;

用记录cx1上面那条条件转移指令的位置;

then

<statement>

产生无条件转移指令“JMP,0,0”;

用cx2记录上面那条无条件转移指令的位置;

cx3为即将产生的目标代码的位置;

用cx3回填cx1和cx2记录位置的指令

else

<statement>

cx4为即将产生的目标代码的位置;

用cx4回填cx2记录位置的指令

l  while语句:

while<lexp>do

产生田间转移指令“JPC,0,0”

用cx1记录下上面那条条件转移指令的位置

<statement>

产生无条件转移指令“JMP,0,0”

cx2为即将产生的目标代码的位置

用cx2回填cx1记录位置的指令

l  call语句:

产生“CAL,L,A”,意思是通过层差L和过程入口地址A,找到过程的入口地址,这里A可以有该过程在符号表中的value域的值获得(原因见<block>的翻译)

l  read语句:

首先从命令行获取一个值放在栈顶,然后将栈顶值赋值给相应变量(赋值过程见前面赋值语句的翻译)

l  write语句:

直接输出栈顶值

其他运算的翻译较为简单,不再赘述.

2.2.4活动记录的组织

此部分主要是活动记录的组织,具体如下:

为每一个过程开辟的新的存储空间,最低层三个用来存储SL:保存该过程直接外层的活动首地址;DL:调用者的活动记录首地址;RA:返回地址;

具体的执行方式见代码注释。

2.2.5难点说明

l  变量(常数、过程)作用域的确定,分为两步:一步是定义变量时,我们要查询在本层有无同名的变量已经定义,若有,则报错;二步是变量使用时,首先查询本层有无定义该变量,若有,则使用本层定义的变量,若无,则寻找本过程直接外层有无定义同名变量,若仍无此变量,则继续向下一个直接外层寻找,以此类推。若有,则使用最靠近本层的那个定义,若无,则报错。由于本符号表采用的是数组管理,在实现第一步时,在变量(常数、过程)登录符号表时记录其所在的层次和所在层次的名字,若符号表中存在变量(常量、过程)的名字、所在层次和所在层次的名字均相同,则报错,若无,则将定义的变量加到符号表中。在实现第二步时,结合语法的特点分析,首先在本调用过程所在的过程进行查找有无定义该变量,若有,则就使用本过程的变量,若无,则根据前面在符号表中记录的本调用过程所在的直接外层的名字查找该直接外层有无定义此变量,以此类推,若最终有定义,则使用该变量,否则保存。

l  过程传递参数:我把在过程定义时的形式参数,当作在本过程定义变量进行处理,在调用过程中出传入的实在参数当作对相应的形式参数进行赋值,具体说来就是用该过程在符号表中的size域存储该过程中的形参个数,再用一个变量记录过程中定义参数的个数(具体做法是定义一个变量,每次进入block时置为3,用来存储SL、DL、RA,然后遇到变量定义就加一来记录)当遇到过程定义时,将其说明的形参按照在本过程中定义变量的相关属性登录到符号表中,并且他们在活动记录的存储位置是紧接着SL,DL,RA;在遇到调用过程时,首先根据栈顶、栈顶-1、栈顶-2、栈顶-形参个数+1根据所存储的位置进行赋值,然后再分配相应的活动存储空间(如果刚开始就分配,则会导致栈顶更新,失去了原来的栈顶元素),该活动存储空间的大小应该是参数个数+本过程定义的变量个数+3。

三、函数说明

3.1类的作用的说明

l  RValue+LexAnalysis+chTable完成词法分析。

l  AllPcode+PerPcode完成目标代码生成。

l  SymTable+TableRow完成符号表管理。

l  Interpreter完成解释执行程序。

l  MpgAnalysis的一个函数showError完成错误处理程序。

l  MpgAnalysis综合上述类完成编译功能。

3.2具体函数功能的说明

在《算法+数据结构=程序》一书中,Niklaus Wirth 设计的 PL/0 语言编译器分成两部分,把源语言翻译成中间语言编译器和中间语言解释器,编译器用的是递归下降的预测分析方法中间语言是一种栈机器代码,其指令集是根据 PL/0 语言的需要来设计的。编译器源码及测试程序可从。一条指令由三个域组成: (1)操作码 f:上面已经列出了所有 8 种操作码。 (2)层次差 l:这里的层次差就是 5.3.2 节介绍嵌套深度时的 n p − n a 。该域仅用于存取指令和调用指令。 (3)多用途 a:在运算指令中,a 的值用来区分不同的运算;在其他情况,a 或是一个数(lit,int),或是一个程序地址(jmp,jpc,cal),或是一个数据地址(lod,sto)。 编译器PL/0 源程序进行一遍扫描,并逐行输出源程序。在源程序无错的情况下,编译器每编译完一个分程序,就列出该分程序的代码,这由编译器的 listcode 过程完成。每个分程序的第一条指令是 jmp 指令,其作用是绕过该分程序声明部分产生的代码(即绕过内嵌过程的代码)。listcode 过程没有列出这条代码。 解释器是编译器中的一个过程,若源程序无错,则编译结束时调用解释过程 interpret。由于 PL/0 语言没有输出语句,解释器按执行次序,每遇到对变量赋值时就输出该值。 由于 PL/0 语言是过程嵌套语言,因此程序运行时,活动记录栈中每个活动记录需要包含控制链和访问。活动记录栈的栈顶以外的存储空间作为代码执行过程中所需要的计算栈,无需另外设立计算栈。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值