当前位置:文档之家› PL0编译器源程序分析

PL0编译器源程序分析

PL0编译器源程序分析
PL0编译器源程序分析

PL/0编译器源程序分析

PL/0语言是Pascal语言的一个子集,我们这里分析的PL/0的编译程序包括了对PL/0语言源程序进行分析处理、编译生成类PCODE代码,并在虚拟机上解释运行生成的类PCODE代码的功能。

PL/0语言编译程序采用以语法分析为核心、一遍扫描的编译方法。词法分析和代码生成作为独立的子程序供语法分析程序调用。语法分析的同时,提供了出错报告和出错恢复的功能。在源程序没有错误编译通过的情况下,调用类PCODE解释程序解释执行生成的类PCODE代码。

词法分析子程序分析:

词法分析子程序名为getsym,功能是从源程序中读出一个单词符号(token),把它的信息放入全局变量sym、id和num中,语法分析器需要单词时,直接从这三个变量中获得。(注意!语法分析器每次用完这三个变量的值就立即调用getsym子程序获取新的单词供下一次使用。而不是在需要新单词时才调用getsym过程。)getsym过程通过反复调用getch子过程从源程序过获取字符,并把它们拼成单词。getch过程中使用了行缓冲区技术以提高程序运行效率。

词法分析器的分析过程:调用getsym时,它通过getch过程从源程序中获得一个字符。如果这个字符是字母,则继续获取字符或数字,最终可以拼成一个单词,查保留字表,如果查到为保留字,则把sym变量赋成相应的保留字类型值;如果没有查到,则这个单词应是一个用户自定义的标识符(可能是变量名、常量名或是过程的名字),把sym置为ident,把这个单词存入id变量。查保留字表时使用了二分法查找以提高效率。如果getch获得的字符是数字,则继续用getch获取数字,并把它们拼成一个整数,然后把sym置为number,并把拼成的数值放入num变量。如果识别出其它合法的符号(比如:赋值号、大于号、小于等于号等),则把sym则成相应的类型。如果遇到不合法的字符,把sym置成nul。

语法分析子程序分析:

语法分析子程序采用了自顶向下的递归子程序法,语法分析同时也根据程序的语意生成相应的代码,并提供了出错处理的机制。语法分析主要由分程序分析过程(block)、常量定义分析过程(constdeclaration)、变量定义分析过程(vardeclaration)、语句分析过程(statement)、表达式处理过程(expression)、项处理过程(term)、因子处理过程(factor)和条件处理过程(condition)构成。这些过程在结构上构成一个嵌套的层次结构。除此之外,还有出错报告过程(error)、代码生成过程(gen)、测试单词合法性及出错恢复过程(test)、登录名字表过程(enter)、查询名字表函数(position)以及列出类PCODE代码过程(listcode)作过语法分析的辅助过程。

由PL/0的语法图可知:一个完整的PL/0程序是由分程序和句号构成的。因此,本编译程序在运行的时候,通过主程序中调用分程序处理过程block来分析分程序部分(分程序分析过程中还可能会递归调用block过程),然后,判断最后读入的符号是否为句号。如果是句号且分程序分析中未出错,则是一个合法的PL/0程序,可以运行生成的代码,否则就说明源PL/0程序是不合法的,输出出错提示即可。

下面按各语法单元分析PL/0编译程序的运行机制。

分程序处理过程:

语法分析开始后,首先调用分程序处理过程(block)处理分程序。过程入口参数置为:0层、符号表位置0、出错恢复单词集合为句号、声明符或语句开始符。进入block过程后,首先把局部数据段分配指针设为3,准备分配3个单元供运行期存放静态链SL、动态链DL 和返回地址RA。然后用tx0记录下当前符号表位置并产生一条jmp指令,准备跳转到主程序的开始位置,由于当前还没有知到主程序究竟在何处开始,所以jmp的目标暂时填为0,稍后再改。同时在符号表的当前位置记录下这个jmp指令在代码段中的位置。在判断了嵌套层数没有超过规定的层数后,开始分析源程序。首先判断是否遇到了常量声明,如果遇到则开始常量定义,把常量存入符号表。接下去用同样的方法分析变量声明,变量定义过程中会用dx变量记录下局部数据段分配的空间个数。然后如果遇到procedure保留字则进行过程声明和定义,声明的方法是把过程的名字和所在的层次记入符号表,过程定义的方法就是通过递归调用block过程,因为每个过程都是一个分程序。由于这是分程序中的分程序,因此调用block时需把当前的层次号lev加一传递给block 过程。分程序声明部分完成后,即将进入语句的处理,这时的代码分配指针cx的值正好指向语句的开始位置,这个位置正是前面的jmp指令需要跳转到的位置。于是通过前面记录下来的地址值,把这个jmp指令的跳转位置改成当前cx的位置。并在符号表中记录下当前的代码段分配地址和局部数据段要分配的大小(dx 的值)。生成一条int指令,分配dx个空间,作为这个分程序段的第一条指令。下面就调用语句处理过程statement分析语句。分析完成后,生成操作数为0的opr指令,用于从分程序返回(对于0层的主程序来说,就是程序运行完成,退出)。

常量定义过程:

通过循环,反复获得标识符和对应的值,存入符号表。符号表中记录下标识符的名字和它对应的值。

变量定义过程:

与常量定义类似,通过循环,反复获得标识符,存入符号表。符号表中记录下标识符的名字、它所在的层及它在所在层中的偏移地址。

语句处理过程:

语句处理过程是一个嵌套子程序,通过调用表达式处理、项处理、因子处理等过程及递归调用自己来实现对语句的分析。语句处理过程可以识别的语句包括赋值语句、read语句、write语句、call语句、if语句、while语句。当遇到begin/end语句时,就递归调用自己来分析。分析的同时生成相应的类PCODE指令。

赋值语句的处理:

首先获取赋值号左边的标识符,从符号表中找到它的信息,并确认这个标识符确为变量名。然后通过调用表达式处理过程算得赋值号右部的表达式的值并生成相应的指令保证这个值放在运行期的数据栈顶。最后通过前面查到的左部变量的位置信息,生成相应的sto指令,把栈顶值存入指定的变量的空间,实现了赋值操作。

read语句的处理:

确定read语句语法合理的前提下(否则报错),生成相应的指令:第一条是16号操作的opr指令,实现从标准输入设备上读一个整数值,放在数据栈顶。第二条是sto指令,把栈顶的值存入read语句括号中的变量所在的单元。

write语句的处理:

与read语句相似。在语法正确的前提下,生成指令:通过循环调用表达式处理过程分析write语句括号中的每一个表达式,生成相应指令保证把表达式的值算出并放到数据栈顶并生成14号操作的opr指令,输出表达式的值。最后生成15号操作的opr指令输出一个换行。

call语句的处理:

从符号表中找到call语句右部的标识符,获得其所在层次和偏移地址。然后生成相应的cal指令。至于调用子过程所需的保护现场等工作是由类PCODE解释程序在解释执行cal 指令时自动完成的。

if语句的处理:

按if语句的语法,首先调用逻辑表达式处理过程处理if语句的条件,把相应的真假值放到数据栈顶。接下去记录下代码段分配位置(即下面生成的jpc指令的位置),然后生成条件转移jpc指令(遇0或遇假转移),转移地址未知暂时填0。然后调用语句处理过程处理then语句后面的语句或语句块。then后的语句处理完后,当前代码段分配指针的位置就应该是上面的jpc指令的转移位置。通过前面记录下的jpc指令的位置,把它的跳转位置改成当前的代码段指针位置。

begin/end语句的处理:

通过循环遍历begin/end语句块中的每一个语句,通过递归调用语句分析过程分析并生成相应代码。

while语句的处理:

首先用cx1变量记下当前代码段分配位置,作为循环的开始位置。然后处理while语句中的条件表达式生成相应代码把结果放在数据栈顶,再用cx2变量记下当前位置,生成条件转移指令,转移位置未知,填0。通过递归调用语句分析过程分析do语句后的语句或语句块并生成相应代码。最后生成一条无条件跳转指令jmp,跳转到cx1所指位置,并把cx2所指的条件跳转指令的跳转位置改成当前代码段分配位置。

表达式、项、因子处理:

根据PL/0语法可知,表达式应该是由正负号或无符号开头、由若干个项以加减号连接而成。而项是由若干个因子以乘除号连接而成,因子则可能是一个标识符或一个数字,或是一个以括号括起来的子表达式。根据这样的结构,构造出相应的过程,递归调用就完成了表达式的处理。把项和因子独立开处理解决了加减号与乘除号的优先级问题。在这几个过程的反复调用中,始终传递fsys变量的值,保证可以在出错的情况下跳过出错的符号,使分析过程得以进行下去。

逻辑表达式的处理:

首先判断是否为一元逻辑表达式:判奇偶。如果是,则通过调用表达式处理过程分析计算表达式的值,然后生成判奇指令。如果不是,则肯定是二元逻辑运算符,通过调用表达式处理过程依次分析运算符左右两部分的值,放在栈顶的两个空间中,然后依不同的逻辑运算符,生成相应的逻辑判断指令,放入代码段。

判断单词合法性与出错恢复过程分析:

本过程有三个参数,s1、s2为两个符号集合,n为出错代码。本过程的功能是:测试当前符号(即sym变量中的值)是否在s1集合中,如果不在,就通过调用出错报告过程输出出错代码n,并放弃当前符号,通过词法分析过程获取一下单词,直到这个单词出现在s1或s2集合中为止。

这个过程在实际使用中很灵活,主要有两个用法:

在进入某个语法单位时,调用本过程,检查当前符号是否属于该语法单位的开始符号集合。若不属于,则滤去开始符号和后继符号集合外的所有符号。

在语法单位分析结束时,调用本过程,检查当前符号是否属于调用该语法单位时应有的后继符号集合。若不属于,则滤去后继符号和开始符号集合外的所有符号。

通过这样的机制,可以在源程序出现错误时,及时跳过出错的部分,保证语法分析可以继续下去。

语法分析过程中调用的其它子过程相对比较简单,请参考源程序的注释。

类PCODE代码解释执行过程分析

这个过程模拟了一台可以运行类PCODE指令的栈式计算机。它拥有一个栈式数据段用于存放运行期数据、拥有一个代码段用于存放类PCODE程序代码。同时还拥用数据段分配指针、指令指针、指令寄存器、局部段基址指针等寄存器。

解释执行类PCODE代码时,数据段存储分配方式如下:

对于源程序的每一个过程(包括主程序),在被调用时,首先在数据段中开辟三个空间,存放静态链SL、动态链DL和返回地址RA。静态链记录了定义该过程的直接外过程(或主程序)运行时最新数据段的基地址。动态链记录调用该过程前正在运行的过程的数据段基址。返回地址记录了调用该过程时程序运行的断点位置。对于主程序来说,SL、DL和RA 的值均置为0。静态链的功能是在一个子过程要引用它的直接或间接父过程(这里的父过程是按定义过程时的嵌套情况来定的,而不是按执行时的调用顺序定的)的变量时,可以通过静态链,跳过个数为层差的数据段,找到包含要引用的变量所在的数据段基址,然后通过偏移地址访问它。

在过程返回时,解释程序通过返回地址恢复指令指针的值到调用前的地址,通过当前段基址恢复数据段分配指针,通过动态链恢复局部段基址指针。实现子过程的返回。对于主程序来说,解释程序会遇到返回地址为0的情况,这时就认为程序运行结束。

解释程序过程中的base函数的功能,就是用于沿着静态链,向前查找相差指定层数的局部数据段基址。这在使用sto、lod等访问局部变量的指令中会经常用到。

类PCODE代码解释执行的部分通过循环和简单的case判断不同的指令,做出相应的动作。当遇到主程序中的返回指令时,指令指针会指到0位置,把这样一个条件作为终至循环的条件,保证程序运行可以正常的结束。

以下源程序是以清华大学出版社《编译原理》中的源代码为基础作了少量改动而成。

程序在Turbo Pascal 7.0上编译运行通过。

******************************************************************************* *****

program pl0(fa,fa1,fa2); (* PL/0编译程序与代码生成解释运行程序*)

(* PL/0 compiler with code generation *)

label 99; (* 声明出错跳转标记*)

(* 在Turbo Pascal 7.0中已不允许跨过程的GOTO转移,因此后面的GOTO语句均被我去除了,因此这里的label也没有意义了*)

const (* 常量定义*)

norw = 13; (* of reserved words *) (* 保留字的个数*)

txmax = 100; (* length of identifier table *) (* 标识符表的长度(容量)*)

nmax = 14; (* max number of digits in numbers *) (* 数字允许的最长位数*)

al = 10; (* length of identifiers *) (* 标识符最长长度*)

amax = 2047; (* maximum address *) (* 寻址空间*)

levmax = 3; (* max depth of block nesting *) (* 最大允许的块嵌套层数*)

cxmax = 200; (* size of code array *) (* 类PCODE目标代码数组长度(可容纳代码行数)*) type (* 类型定义*)

symbol = (nul, ident, number, plus, minus, times, slash, oddsym,

eql, neq, lss, leq, gtr, geq, lparen, rparen, comma,

semicolon, period, becomes, beginsym, endsym, ifsym,

thensym, whilesym, writesym, readsym, dosym, callsym,

constsym, varsym, procsym); (* symobl类型标识了不同类型的词汇*)

alfa = packed array[1..al] of char; (* alfa类型用于标识符*)

object1 = (constant, variable, procedur); (* object1为三种标识符的类型*)

(* 原程序在此使用object作为类型名称,在支持面向对象的Turbo Pascal 7.0中编译不能通过*)

(* wirth used the word "procedure" there, whick won't work! *)

(* 上面一行是课本上的程序清单中的注释,说本程序的原作者Wirth在这里用了procedure 这个词作为标识符类型,是不可以的。

事实上Wirth原本在这里用的词是prozedure,是可以的。*)

symset = set of symbol; (* symset是symbol类型的一个集合类型,可用于存放一组symbol *) fct = (lit, opr, lod, sto, cal, int, jmp, jpc); (* fct类型分别标识类PCODE的各条指令*) instruction = packed record

f: fct; (* function code *)

l: 0..levmax; (* level *)

a: 0..amax; (* displacement addr *)

end; (* 类PCODE指令类型,包含三个字段:指令f、层差l和另一个操作数a *)

(*

lit 0, a load constant a

opr 0, a execute opr a

lod l, a load variable l, a

sto l, a store variable l, a

cal l, a call procedure a at level l

int 0, a increment t-register by a

jmp 0, a jump to a

jpc 0, a jump conditional to a

*)

var (* 全局变量定义*)

fa: text; (* 文本文件fa用于列出源程序*)

fa1, fa2: text; (* 文本文件fa1用于列出类PCODE代码、fa2用于记录解释执行类PCODE代码的过程*)

listswitch: boolean; (* true set list object code *) (* 如果本变量置true,程序编译后将为列出类PCODE代码,

否则不列出类PCODE代码*)

ch: char; (* last char read *) (* 主要用于词法分析器,存放最近一次从文件中读出的字符*) sym: symbol; (* last symbol read *) (* 词法分析器输出结果之用,存放最近一次识别出来的token的类型*)

id: alfa; (* last identifier read *) (* 词法分析器输出结果之用,存放最近一次识别出来的标识符的名字*)

num: integer; (* last number read *) (* 词法分析器输出结果之用,存放最近一次识别出来的数字的值*)

cc: integer; (* character count *) (* 行缓冲区指针*)

ll: integer; (* line length *) (* 行缓冲区长度*)

kk: integer; (* 引入此变量是出于程序性能考虑,见getsym过程注释*)

cx: integer; (* code allocation index *) (* 代码分配指针,代码生成模块总在cx所指位置生成新的代码*)

line: array[1..81] of char; (* 行缓冲区,用于从文件读出一行,供词法分析获取单词时之用*) a: alfa; (* 词法分析器中用于临时存放正在分析的词*)

code: array[0..cxmax] of instruction; (* 生成的类PCODE代码表,存放编译得到的类PCODE 代码*)

word: array[1..norw] of alfa; (* 保留字表*)

wsym: array[1..norw] of symbol; (* 保留字表中每一个保留字对应的symbol类型*) ssym: array[' '..'^'] of symbol; (* 一些符号对应的symbol类型表*)

(* wirth uses "array[char]" here *)

mnemonic: array[fct] of packed array[1..5] of char;(* 类PCODE指令助记符表*) declbegsys, statbegsys, facbegsys: symset; (* 声明开始、表达式开始和项开始符号集合*) table: array[0..txmax] of record (* 符号表*)

name: alfa; (* 符号的名字*)

case kind: object1 of (* 符号的类型*)

constant: (* 如果是常量名*)

(val: integer); (* val中放常量的值*)

variable, procedur: (* 如果是变量名或过程名*)

(level, adr, size: integer) (* 存放层差、偏移地址和大小*)

(* "size" lacking in orginal. I think it belons here *)

end;

fin, fout: text; (* fin文本文件用于指向输入的源程序文件,fout程序中没有用到*) fname: string; (* 存放PL/0源程序文件的文件名*)

(* 我修改的代码:原程序在此处使用alfa类型,无法在Turbo Pascal 7.0中通过,readln函数的参数不能为alfa型*)

err: integer; (* 出错总次数*)

(* 出错处理过程error *)

(* 参数:n:出错代码*)

procedure error(n: integer);

begin

writeln('****', ' ': cc-1, '!', n:2); (* 在屏幕cc-1位置显示!与出错代码提示,由于cc 是行缓冲区指针,所以!所指位置即为出错位置*)

writeln(fa1, '****', ' ': cc-1, '!', n:2); (* 在文件cc-1位置输出!与出错代码提示*)

err := err + 1 (* 出错总次数加一*)

end (* error *);

(* 词法分析过程getsym *)

procedure getsym;

var

i, j, k: integer;

(* 读取原程序中下一个字符过程getch *)

procedure getch;

begin

if cc = ll then (* 如果行缓冲区指针指向行缓冲区最后一个字符就从文件读一行到行缓冲区*)

begin

if eof(fin) then (* 如果到达文件末尾*)

begin

write('Program incomplete'); (* 出错,退出程序*)

close(fa);

close(fa1);

close(fin);

halt(0);

{ goto 99 }

(* 我修改的代码,由于Turbo Pascal 7.0中不允许跨过程的goto,就只能用上面的方法退出程序了。*)

end;

ll := 0; (* 行缓冲区长度置0 *)

cc := 0; (* 行缓冲区指针置行首*)

write(cx: 4, ' '); (* 输出cx值,宽度为4 *)

write(fa1, cx: 4, ' '); (* 输出cx值,宽度为4到文件*)

while not eoln(fin) do (* 当未到行末时*)

begin

ll := ll + 1; (* 行缓冲区长度加一*)

read(fin, ch); (* 从文件读入一个字符到ch *)

write(ch); (* 在屏幕输出ch *)

write(fa1, ch); (* 把ch输出到文件*)

line[ll] := ch; (* 把读到的字符存入行缓冲区相应的位置*)

end;

(* 可见,PL/0源程序要求每行的长度都小于81个字符*)

writeln;

ll := ll + 1; (* 行缓冲区长度加一,用于容纳即将读入的回车符CR *)

read(fin, line[ll]);(* 把#13(CR)读入行缓冲区尾部*)

read(fin, ch); (* 我添加的代码。由于PC上文本文件换行是以#13#10(CR+LF)表示的,所以要把多余的LF从文件读出,这里放在ch变量中是由于ch变量的

值在下面即将被改变,把这个多余值放在ch中没有问题*)

writeln(fa1);

end;

cc := cc + 1; (* 行缓冲区指针加一,指向即将读到的字符*)

ch := line[cc] (* 读出字符,放入全局变量ch *)

end (* getch *);

begin (* getsym *)

while (ch = ' ') or (ch = #13) do (* 我修改的代码:这句原来是用于读一个有效的字符(跳过读出的字符中多余的空格),但实际上还要跳

过多余的回车*)

getch;

if ch in ['a'..'z'] then (* 如果读出的字符是一个字母,说明是保留字或标识符*)

begin

k := 0; (* 标识符缓冲区指针置0 *)

repeat (* 这个循环用于依次读出源文件中的字符构成标识符*)

if k < al then (* 如果标识符长度没有超过最大标识符长度(如果超过,就取前面一部分,把多余的抛弃) *)

begin

k := k + 1;

a[k] := ch;

end;

getch (* 读下一个字符*)

until not (ch in ['a'..'z','0'..'9']); (* 直到读出的不是字母或数字,由此可知PL/0的标识符构成规则是:

以字母开头,后面跟若干个字母或数字*)

if k >= kk then (* 如果当前获得的标识符长度大于等于kk *)

kk := k (* 令kk为当前标识符长度*)

else

repeat (* 这个循环用于把标识符缓冲后部没有填入相应字母或空格的空间用空格补足*) a[kk] := ' ';

kk := kk - 1

until kk = k;

(* 在第一次运行这个过程时,kk的值为al,即最大标识符长度,如果读到的标识符长度小于kk,

就把a数组的后部没有字母的空间用空格补足。

这时,kk的值就成为a数组前部非空格字符的个数。以后再运行getsym时,如果读到的标识符长度大于等于kk,

就把kk的值变成当前标识符的长度。

这时就不必在后面填空格了,因为它的后面肯定全是空格。反之如果最近读到的标识符长度小于kk,那就需要从kk位置向前,

把超过当前标识长度的空间填满空格。

以上的这样一个逻辑,完全是出于程序性能的上考虑。其实完全可以简单的把a数组中a[k]元素以后的空间不管三七二十一全填空格。

*)

(* 下面开始二分法查找看读出的标识符是不是保留字之一*)

id := a; (* 最后读出标识符等于a *)

i := 1; (* i指向第一个保留字*)

j := norw; (* j指向最后一个保留字*)

repeat

k := (i + j) div 2; (* k指向中间一个保留字*)

if id <= word[k] then (* 如果当前的标识符小于k所指的保留字*)

j := k - 1; (* 移动j指针*)

if id >= word[k] then (* 如果当前的标识符大于k所指的保留字*)

i := k + 1 (* 移动i指针*)

until i > j; (* 循环直到找完保留字表*)

if i - 1 > j then (* 如果i - 1 > j表明在保留字表中找到相应的项,id中存的是保留字*) sym := wsym[k] (* 找到保留字,把sym置为相应的保留字值*)

else

sym := ident (* 未找到保留字,把sym置为ident类型,表示是标识符*)

end(* 至此读出字符为字母即对保留字或标识符的处理结束*)

else (* 如果读出字符不是字母*)

if ch in ['0'..'9'] then (* 如果读出字符是数字*)

begin (* number *) (* 开始对数字进行处理*)

k := 0; (* 数字位数*)

num := 0; (* 数字置为0 *)

sym := number; (* 置sym为number,表示这一次读到的是数字*)

repeat (* 这个循环依次从源文件中读出字符,组成数字*)

num := 10 * num + (ord(ch) - ord('0')); (* num * 10加上最近读出的字符ASCII减'0'的ASCII 得到相应的数值*)

k := k + 1; (* 数字位数加一*)

getch

until not (ch in ['0'..'9']); (* 直到读出的字符不是数字为止*)

if k > nmax then (* 如果组成的数字位数大于最大允许的数字位数*)

error(30) (* 发出30号错*)

end(* 至此对数字的识别处理结束*)

else

if ch = ':' then (* 如果读出的不字母也不是数字而是冒号*)

begin

getch; (* 再读一个字符*)

if ch = '=' then (* 如果读到的是等号,正好可以与冒号构成赋值号*)

begin

sym := becomes; (* sym的类型设为赋值号becomes *)

getch (* 再读出下一个字*)

end

else

sym := nul; (* 如果不是读到等号,那单独的一个冒号就什么也不是*)

end(* 以上完成对赋值号的处理*)

else (* 如果读到不是字母也不是数字也不是冒号*)

if ch = '<' then (* 如果读到小于号*)

begin

getch; (* 再读一个字符*)

if ch = '=' then (* 如果读到等号*)

begin

sym := leq; (* 购成一个小于等于号*)

getch (* 读一个字符*)

end

else (* 如果小于号后不是跟的等号*)

sym := lss (* 那就是一个单独的小于号*)

end

else (* 如果读到不是字母也不是数字也不是冒号也不是小于号*)

if ch = '>' then (* 如果读到大于号,处理过程类似于处理小于号*)

begin

getch; (* 再读一个字符*)

if ch = '=' then (* 如果读到等号*)

begin

sym := geq; (* 购成一个大于等于号*)

getch (* 读一个字符*)

end

else (* 如果大于号后不是跟的等号*)

sym := gtr (* 那就是一个单独的大于号*)

end

else(* 如果读到不是字母也不是数字也不是冒号也不是小于号也不是大于号*)

begin (* 那就说明它不是标识符/保留字,也不是复杂的双字节操作符,应该是一个普通的符号*)

sym := ssym[ch]; (* 直接成符号表中查到它的类型,赋给sym *)

getch (* 读下一个字符*)

end

(* 整个if语句判断结束*)

end (* getsym *);

(* 词法分析过程getsym总结:从源文件中读出若干有效字符,组成一个token串,识别它的类型

为保留字/标识符/数字或是其它符号。如果是保留字,把sym置成相应的保留字类型,如果是

标识符,把sym置成ident表示是标识符,于此同时,id变量中存放的即为保留字字符串或标识

符名字。如果是数字,把sym置为number,同时num变量中存放该数字的值。如果是其它的操作符,

则直接把sym置成相应类型。经过本过程后ch变量中存放的是下一个即将被识别的字符*) (* 目标代码生成过程gen *)

(* 参数:x:要生成的一行代码的助记符*)

(* y, z:代码的两个操作数*)

(* 本过程用于把生成的目标代码写入目标代码数组,供后面的解释器解释执行*) procedure gen(x: fct; y, z: integer);

begin

if cx > cxmax then (* 如果cx>cxmax表示当前生成的代码行号大于允许的最大代码行数*) begin

write('program too long'); (* 输出"程序太长",退出*)

close(fa);

close(fa1);

close(fin);

halt(0)

{ goto 99 }

(* 我修改的代码,由于Turbo Pascal 7.0中不允许跨过程的goto,就只能用上面的方法退出程序了。*)

end;

with code[cx] do (* 把代码写入目标代码数组的当前cx所指位置*)

begin

f := x;

l := y;

a := z;

end;

cx := cx + 1 (* 移动cx指针指向下一个空位*)

end (* gen *);

(* 测试当前单词是否合法过程test *)

(* 参数:s1:当语法分析进入或退出某一语法单元时当前单词符合应属于的集合*)

(* s2:在某一出错状态下,可恢复语法分析正常工作的补充单词集合*)

(* n:出错信息编号,当当前符号不属于合法的s1集合时发出的出错信息*)

procedure test(s1, s2: symset; n: integer);

begin

if not (sym in s1) then (* 如果当前符号不在s1中*)

begin

error(n); (* 发出n号错误*)

s1 := s1 + s2; (* 把s2集合补充进s1集合*)

while not (sym in s1) do (* 通过循环找到下一个合法的符号,以恢复语法分析工作*) getsym

end

end (* test *);

(* 语法分析过程block *)

(* 参数:lev:这一次语法分析所在的层次*)

(* tx:符号表指针*)

(* fsys:用于出错恢复的单词集合*)

procedure block(lev, tx: integer; fsys: symset);

var

dx: integer; (* data allocation index *) (* 数据段内存分配指针,指向下一个被分配空间在数据段中的偏移位置*)

tx0: integer; (* initial table index *) (* 记录本层开始时符号表位置*)

cx0: integer; (* initial code index *) (* 记录本层开始时代码段分配位置*)

(* 登陆符号表过程enter *)

(* 参数:k:欲登陆到符号表的符号类型*)

procedure enter(k: object1);

begin (* enter object into table *)

tx := tx + 1; (* 符号表指针指向一个新的空位*)

with table[tx] do (* 开始登录*)

begin

name := id; (* name是符号的名字,对于标识符,这里就是标识符的名字*)

kind := k; (* 符号类型,可能是常量、变量或过程名*)

case k of (* 根据不同的类型进行不同的操作*)

constant: (* 如果是常量名*)

begin

if num > amax then (* 在常量的数值大于允许的最大值的情况下*)

begin

error(31); (* 抛出31号错误*)

num := 0; (* 实际登陆的数字以0代替*)

end;

val := num (* 如是合法的数值,就登陆到符号表*)

end;

variable: (* 如果是变量名*)

begin

level := lev; (* 记下它所属的层次号*)

adr := dx; (* 记下它在当前层中的偏移量*)

dx := dx+1; (* 偏移量自增一,为下一次做好准备*)

end;

procedur: (* 如果要登陆的是过程名*)

level := lev (* 记录下这个过程所在层次*)

end

end

end (* enter *);

[楼主] | Posted: 2006-03-21 00:00

angel

级别: 管理员

精华: 23

发帖: 489

威望: 534 点

金钱: 5330 RMB

贡献值: 0 点

好评度: 0 点

注册时间:2006-02-26

最后登录:2006-08-24

(* 登录符号过程没有考虑到重复的定义的问题。如果出现重复定义,则以最后一次的定义为准。*)

(* 在符号表中查找指定符号所在位置的函数position *)

(* 参数:id:要找的符号*)

(* 返回值:要找的符号在符号表中的位置,如果找不到就返回0 *)

function position (id: alfa): integer;

var

i: integer;

begin (* find identifier in table *)

table[0].name := id; (* 先把id放入符号表0号位置*)

i := tx; (* 从符号表中当前位置也即最后一个符号开始找*)

while https://www.doczj.com/doc/1418211269.html, <> id do (* 如果当前的符号与要找的不一致*)

i := i - 1; (* 找前面一个*)

position := i (* 返回找到的位置号,如果没找到则一定正好为0 *)

end(* position *);

(* 常量声明处理过程constdeclaration *)

procedure constdeclaration;

begin

if sym = ident then (* 常量声明过程开始遇到的第一个符号必然应为标识符*)

begin

getsym; (* 获取下一个token *)

if sym in [eql, becomes] then (* 如果是等号或赋值号*)

begin

if sym = becomes then (* 如果是赋值号(常量生明中应该是等号) *)

error(1); (* 抛出1号错误*)

(* 这里其实自动进行了错误纠正使编译继续进行,把赋值号当作等号处理*)

getsym; (* 获取下一个token,等号或赋值号后应接上数字*)

if sym = number then (* 如果的确是数字*)

begin

enter(constant); (* 把这个常量登陆到符号表*)

getsym (* 获取下一个token,为后面作准备*)

end

else

error(2) (* 如果等号后接的不是数字,抛出2号错误*)

end

else

error(3) (* 如果常量标识符后接的不是等号或赋值号,抛出3号错误*)

end

else

error(4) (* 如果常量声明过程遇到的第一个符号不为标识符,抛出4号错误*)

end(* constdeclaration *);

(* 变量声明过程vardeclaration *)

procedure vardeclaration;

begin

if sym = ident then (* 变量声明过程开始遇到的第一个符号必然应为标识符*)

begin

enter(variable); (* 将标识符登陆到符号表中*)

getsym (* 获取下一个token,为后面作准备*)

end

else

error(4) (* 如果变量声明过程遇到的第一个符号不是标识符,抛出4号错误*)

end(* vardeclaration *);

(* 列出当前一层类PCODE目标代码过程listcode *)

procedure listcode;

var

i: integer;

begin (* list code generated for this block *)

if listswitch then (* 如果用户选择是要列出代码的情况下才列出代码*)

begin

for i := cx0 to cx - 1 do (* 从当前层代码开始位置到当前代码位置-1处,即为本分程序块*) with code do

begin

writeln(i: 4, mnemonic[f]: 5, l: 3, a: 5); (* 显示出第i行代码的助记符和L与A操作数*) (* 我修改的代码:原程序此处在输出i时,没有指定占4个字符宽度,不美观也与下面一句不配套。*)

writeln(fa, i: 4, mnemonic[f]: 5, l: 3, a: 5) (* 同时把屏显打印到文件*)

end;

end

end(* listcode *);

(* 语句处理过程statement *)

(* 参数说明:fsys: 如果出错可用来恢复语法分析的符号集合*)

procedure statement(fsys: symset);

var

i, cx1, cx2: integer;

(* 表达式处理过程expression *)

(* 参数说明:fsys: 如果出错可用来恢复语法分析的符号集合*)

procedure expression(fsys: symset);

var

addop: symbol;

(* 项处理过程term *)

(* 参数说明:fsys: 如果出错可用来恢复语法分析的符号集合*)

procedure term(fsys: symset);

var

mulop: symbol;

(* 因子处理过程factor *)

(* 参数说明:fsys: 如果出错可用来恢复语法分析的符号集合*)

procedure factor(fsys: symset);

var

i: integer;

begin

test(facbegsys, fsys, 24); (* 开始因子处理前,先检查当前token是否在facbegsys集合中。*) (* 如果不是合法的token,抛24号错误,并通过fsys集恢复使语法处理可以继续进行*)

while sym in facbegsys do (* 循环处理因子*)

begin

if sym = ident then (* 如果遇到的是标识符*)

begin

i := position(id); (* 查符号表,找到当前标识符在符号表中的位置*)

if i = 0 then (* 如果查符号表返回为0,表示没有找到标识符*)

error(11) (* 抛出11号错误*)

else

with table do (* 如果在符号表中找到了当前标识符的位置,开始生成相应代码*)

case kind of

constant: gen(lit, 0, val); (* 如果这个标识符对应的是常量,值为val,生成lit指令,把val 放到栈顶*)

variable: gen(lod, lev - level, adr); (* 如果标识符是变量名,生成lod指令,*)

(* 把位于距离当前层level的层的偏移地址为adr的变量放到栈顶*) procedur: error(21) (* 如果在因子处理中遇到的标识符是过程名,出错了,抛21号错*) end;

getsym (* 获取下一token,继续循环处理*)

end

else

if sym = number then (* 如果因子处理时遇到数字*)

begin

if num > amax then (* 如果数字的大小超过允许最大值amax *)

begin

error(31); (* 抛出31号错*)

num := 0 (* 把数字按0值处理*)

end;

gen(lit, 0, num); (* 生成lit指令,把这个数值字面常量放到栈顶*)

getsym (* 获取下一token *)

end

else

if sym = lparen then (* 如果遇到的是左括号*)

begin

getsym; (* 获取一个token *)

expression( [rparen] + fsys ); (* 递归调用expression子程序分析一个子表达式*)

if sym = rparen then (* 子表达式分析完后,应遇到右括号*)

getsym (* 如果的确遇到右括号,读取下一个token *)

else

error(22) (* 否则抛出22号错误*)

end;

test(fsys, facbegsys, 23) (* 一个因子处理完毕,遇到的token应在fsys集合中*) (* 如果不是,抛23号错,并找到下一个因子的开始,使语法分析可以继续运行下去*)

end

end(* factor *);

begin (* term *)

factor([times, slash] + fsys); (* 每一个项都应该由因子开始,因此调用factor子程序分析因子*)

while sym in [times, slash] do (* 一个因子后应当遇到乘号或除号*)

begin

mulop := sym; (* 保存当前运算符*)

getsym; (* 获取下一个token *)

factor(fsys + [times, slash]); (* 运算符后应是一个因子,故调factor子程序分析因子*)

if mulop = times then (* 如果刚才遇到乘号*)

gen(opr, 0, 4) (* 生成乘法指令*)

else

gen(opr, 0, 5) (* 不是乘号一定是除号,生成除法指令*)

end

end (* term *);

begin (* expression *)

if sym in [plus, minus] then (* 一个表达式可能会由加号或减号开始,表示正负号*)

begin

addop := sym; (* 把当前的正号或负号保存起来,以便下面生成相应代码*)

getsym; (* 获取一个token *)

term(fsys + [plus, minus]); (* 正负号后面应该是一个项,调term子程序分析*)

if addop = minus then (* 如果保存下来的符号是负号*)

gen(opr, 0, 1) (* 生成一条1号操作指令:取反运算*)

(* 如果不是负号就是正号,不需生成相应的指令*)

end

else (* 如果不是由正负号开头,就应是一个项开头*)

term(fsys + [plus, minus]); (* 调用term子程序分析项*)

while sym in [plus, minus] do (* 项后应是加运算或减运算*)

begin

addop := sym; (* 把运算符保存下来*)

getsym; (* 获取下一个token,加减运算符后应跟的是一个项*)

term(fsys + [plus, minus]); (* 调term子程序分析项*)

if addop = plus then (* 如果项与项之间的运算符是加号*)

gen(opr, 0, 2) (* 生成2号操作指令:加法*)

else (* 否则是减法*)

gen(opr, 0, 3) (* 生成3号操作指令:减法*)

end

end (* expression *);

(* 条件处理过程condition *)

(* 参数说明:fsys: 如果出错可用来恢复语法分析的符号集合*)

procedure condition(fsys: symset);

var

relop: symbol; (* 用于临时记录token(这里一定是一个二元逻辑运算符)的内容*) begin

if sym = oddsym then (* 如果是odd运算符(一元) *)

begin

getsym; (* 获取下一个token *)

expression(fsys); (* 对odd的表达式进行处理计算*)

gen(opr, 0, 6); (* 生成6号操作指令:奇偶判断运算*)

end

else (* 如果不是odd运算符(那就一定是二元逻辑运算符) *)

begin

expression([eql, neq, lss, leq, gtr, geq] + fsys); (* 对表达式左部进行处理计算*)

if not (sym in [eql, neq, lss, leq, gtr, geq]) then (* 如果token不是逻辑运算符中的一个*) error(20) (* 抛出20号错误*)

else

begin

relop := sym; (* 记录下当前的逻辑运算符*)

getsym; (* 获取下一个token *)

expression(fsys); (* 对表达式右部进行处理计算*)

case relop of (* 如果刚才的运算符是下面的一种*)

eql: gen(opr, 0, 8); (* 等号:产生8号判等指令*)

neq: gen(opr, 0, 9); (* 不等号:产生9号判不等指令*)

lss: gen(opr, 0, 10); (* 小于号:产生10号判小指令*)

geq: gen(opr, 0, 11); (* 大于等号号:产生11号判不小于指令*)

gtr: gen(opr, 0, 12); (* 大于号:产生12号判大于指令*)

leq: gen(opr, 0, 13); (* 小于等于号:产生13号判不大于指令*)

end

end

end

end (* condition *);

begin (* statement *)

if sym = ident then (* 所谓"语句"可能是赋值语句,以标识符开头*)

begin

i := position(id); (* 在符号表中查到该标识符所在位置*)

if i = 0 then (* 如果没找到*)

error(11) (* 抛出11号错误*)

else

if table.kind <> variable then (* 如果在符号表中找到该标识符,但该标识符不是变量名*) begin

error(12); (* 抛出12号错误*)

i := 0 (* i置0作为错误标志*)

end;

getsym; (* 获得下一个token,正常应为赋值号*)

if sym = becomes then (* 如果的确为赋值号*)

getsym (* 获取下一个token,正常应为一个表达式*)

else

error(13); (* 如果赋值语句的左部标识符号后所接不是赋值号,抛出13号错误*) expression(fsys); (* 处理表达式*)

if i <> 0 then (* 如果不曾出错,i将不为0,i所指为当前语名左部标识符在符号表中的位置*)

with table do

gen(sto, lev - level, adr) (* 产生一行把表达式值写往指定内存的sto目标代码*)

end

else

if sym = readsym then (* 如果不是赋值语句,而是遇到了read语句*)

begin

getsym; (* 获得下一token,正常情况下应为左括号*)

if sym <> lparen then (* 如果read语句后跟的不是左括号*)

error(34) (* 抛出34号错误*)

else

repeat (* 循环得到read语句括号中的参数表,依次产生相应的“从键盘读入”目标代码*) getsym; (* 获得一个token,正常应是一个变量名*)

if sym = ident then (* 如果确为一个标识符*)

(* 这里略有问题,还应判断一下这个标识符是不是变量名,如果是常量名或过程名应出错*)

i := position(id) (* 查符号表,找到它所在位置给i,找不到时i会为0 *)

else

i := 0; (* 不是标识符则有问题,i置0作为出错标志*)

if i = 0 then (* 如果有错误*)

error(35) (* 抛出35号错误*)

else (* 否则生成相应的目标代码*)

with table do

begin

gen(opr, 0, 16); (* 生成16号操作指令:从键盘读入数字*)

gen(sto, lev - level, adr) (* 生成sto指令,把读入的值存入指定变量所在的空间*) end;

getsym (* 获取下一个token,如果是逗号,则read语还没完,否则应当是右括号*) until sym <> comma; (* 不断生成代码直到read语句的参数表中的变量遍历完为止,这里遇到不是逗号,应为右括号*)

if sym <> rparen then (* 如果不是我们预想中的右括号*)

begin

error(33); (* 抛出33号错误*)

while not (sym in fsys) do (* 依靠fsys集,找到下一个合法的token,恢复语法分析*) getsym

end

else

getsym (* 如果read语句正常结束,得到下一个token,一般为分号或end *)

end

else

if sym = writesym then (* 如果遇到了write语句*)

begin

getsym; (* 获取下一token,应为左括号*)

if sym = lparen then (* 如确为左括号*)

begin

repeat (* 依次获取括号中的每一个值,进行输出*)

getsym; (* 获得一个token,这里应是一个标识符*)

expression([rparen, comma] + fsys); (* 调用expression过程分析表达式,用于出错恢复的集合中加上右括号和逗号*)

gen(opr, 0, 14) (* 生成14号指令:向屏幕输出*)

until sym <> comma; (* 循环直到遇到的不再是逗号,这时应是右括号*)

if sym <> rparen then (* 如果不是右括号*)

error(33) (* 抛出33号错误*)

else

getsym (* 正常情况下要获取下一个token,为后面准备好*)

end;

gen(opr, 0, 15) (* 生成一个15号操作的目标代码,功能是输出一个换行*)

(* 由此可知PL/0中的write语句与Pascal中的writeln语句类似,是带有输出换行的*) end

else

if sym = callsym then (* 如果是call语句*)

begin

getsym; (* 获取token,应是过程名型标识符*)

if sym <> ident then (* 如果call后跟的不是标识符*)

error(14) (* 抛出14号错误*)

else

begin

i := position(id); (* 从符号表中找出相应的标识符*)

if i = 0 then (* 如果没找到*)

error(11) (* 抛出11号错误*)

else

with table do (* 如果找到标识符位于符号表第i位置*)

if kind = procedur then (* 如果这个标识符为一个过程名*)

gen(cal,lev-level,adr) (* 生成cal目标代码,呼叫这个过程*)

else

error(15); (* 如果call后跟的不是过程名,抛出15号错误*)

getsym (* 获取下一token,为后面作准备*)

end

end

else

if sym = ifsym then (* 如果是if语句*)

begin

getsym; (* 获取一token应是一个逻辑表达式*)

condition([thensym, dosym] + fsys); (* 对逻辑表达式进行分析计算,出错恢复集中加入then 和do语句*)

if sym = thensym then (* 表达式后应遇到then语句*)

getsym (* 获取then后的token,应是一语句*)

else

error(16); (* 如果if后没有then,抛出16号错误*)

cx1 := cx; (* 记下当前代码分配指针位置*)

gen(jpc, 0, 0); (* 生成条件跳转指令,跳转位置暂时填0,分析完语句后再填写*) statement(fsys); (* 分析then后的语句*)

code[cx1].a:=cx (* 上一行指令(cx1所指的)的跳转位置应为当前cx所指位置*)

end

else

if sym = beginsym then (* 如果遇到begin *)

begin

getsym; (* 获取下一个token *)

statement([semicolon, endsym] + fsys);(* 对begin与end之间的语句进行分析处理*) while sym in [semicolon] + statbegsys do (* 如果分析完一句后遇到分号或语句开始符循环分析下一句语句*)

begin

if sym = semicolon then (* 如果语句是分号(可能是空语句)*)

getsym (* 获取下一token继续分析*)

else

error(10); (* 如果语句与语句间没有分号,出10号错*)

statement([semicolon, endsym] + fsys) (* 分析一个语句*)

end;

if sym = endsym then (* 如果语句全分析完了,应该遇到end *)

getsym (* 的确是end,读下一token *)

else

error(17) (* 如果不是end,抛出17号错*)

编译原理(PL0编译程序源代码)

/*PL/0编译程序(C语言版) *编译和运行环境: *Visual C++6.0 *WinXP/7 *使用方法: *运行后输入PL/0源程序文件名 *回答是否将虚拟机代码写入文件 *回答是否将符号表写入文件 *执行成功会产生四个文件(词法分析结果.txt符号表.txt虚拟代码.txt源程序和地址.txt) */ #include #include"pl0.h" #include"string" #define stacksize 500//解释执行时使用的栈 int main(){ bool nxtlev[symnum]; printf("请输入源程序文件名:"); scanf("%s",fname); fin=fopen(fname,"r");//以只读方式打开pl0源程序文件 cifa=fopen("词法分析结果.txt","w"); fa1=fopen("源程序和地址.txt","w");//输出源文件及各行对应的首地址 fprintf(fa1,"输入pl0源程序文件名:"); fprintf(fa1,"%s\n",fname); if(fin){ printf("是否将虚拟机代码写入文件?(Y/N)");//是否输出虚拟机代码 scanf("%s",fname); listswitch=(fname[0]=='y'||fname[0]=='Y'); printf("是否将符号表写入文件?(Y/N)");//是否输出符号表scanf("%s",fname); tableswitch=(fname[0]=='y'||fname[0]=='Y'); init();//初始化 err=0; cc=cx=ll=0; ch=' '; if(-1!=getsym()){ fa=fopen("虚拟代码.txt","w"); fas=fopen("符号表.txt","w"); addset(nxtlev,declbegsys,statbegsys,symnum); nxtlev[period]=true; if(-1==block(0,0,nxtlev)){//调用编译程序 fclose(fa); fclose(fa1); fclose(fas); fclose(fin); return 0; } if(sym!=period){ error(9);//结尾丢失了句号 }

(完整word版)PL0源代码(C语言版)

/*PL/0 编译系统C版本头文件pl0.h*/ # define norw 13 //a number of reserved word /*关键字个数*/ # define txmax 100 //length of identifier table /*名字表容量*/ # define nmax 14 //max number of digits in numbers /*number的最大位数*/ # define al 10 //length of identifier /*符号的最大长度*/ # define amax 2047 //maximum address /*地址上界*/ # define levmax 3 //max depth of block nesting /*最大允许过程嵌套声明层数[0,lexmax]*/ # define cxmax 200 //size of code array /*最多的虚拟机代码数*/ /*符号*/ enum symbol{ nul, ident, number, plus, minus, times, slash, oddsym, eql, neq, //slash斜线 lss, leq, gtr, geq, lparen, //leq :less than or equal to; gtr: great than;lparen:left parenthesis rparen, comma, semicolon,period, becomes,//comma逗号semicolon分号period句号becomes赋值号 beginsym, endsym, ifsym, thensym, whilesym, writesym, readsym, dosym, callsym, constsym, varsym, procsym, }; #define symnum 32 /*-------------*/ enum object{ //object为三种标识符的类型 constant, variable, procedur, }; /*--------------*/ enum fct{ //fct类型分别标识类PCODE的各条指令 lit, opr, lod, sto, cal, inte, jmp, jpc, //书本P23 }; #define fctnum 8 /*--------------*/ struct instruction //指令 { enum fct f; //功能码 int l; //层次差 int a; //P23 }; FILE * fas; //输出名字表

第2章 PL0编译程序的实现 完整课后习题答案+吕映芝编

第2章 PL/0编译程序的实现 第1题 PL/0语言允许过程嵌套定义和递归调用,试问它的编译程序如何解决运行时的存储管理。 答案: PL/0语言允许过程嵌套定义和递归调用,它的编译程序在运行时采用了栈式动态存储管理。(数组CODE存放的只读目标程序,它在运行时不改变。)运行时的数据区S是由解释程序定义的一维整型数组,解释执行时对数据空间S的管理遵循后进先出规则,当每个过程(包括主程序)被调用时,才分配数据空间,退出过程时,则所分配的数据空间被释放。应用动态链和静态链的方式分别解决递归调用和非局部变量的引用问题。 第2题 若PL/0编译程序运行时的存储分配策略采用栈式动态分配,并用动态链和静态链的方式分别解决递归调用和非局部变量的引用问题,试写出下列程序执行到赋值语句b∶=10时运行栈的布局示意图。 var x,y; procedure p; var a; procedure q; var b; begin (q) b∶=10; end (q); procedure s; var c,d; procedure r; var e,f; begin (r) call q; end (r); begin (s) call r; end (s); begin (p) call s;

end (p); begin (main) call p; end (main). 答案: 程序执行到赋值语句b∶=10时运行栈的布局示意图为: 第3题 写出题2中当程序编译到r的过程体时的名字表table的内容。 size name kind level/val adr 答案: 题2中当程序编译到r的过程体时的名字表table的内容为: name kind level/val adr size x variable 0 dx y variable 0 dx+1 p procedure 0 过程p的入口(待填) 5

编译原理PL0编译程序的实现1(希赛教育基础学院)

◇第二章PL/0编译程序的实现 【课前思考】 复习第1章介绍的一个高级程序设计语言编译程序的功能和实现的步骤。编译程序就是一个语言的翻译程序,通常是把一种高级程序设计语言(称源语言)书写的程序翻译成另一种等价功能语言(称目标语言)的程序。换句话说,编译是指把一种用源语言表示的算法转换到另一种等价的用目标语言表示的算法。编译程序实现的必要步骤有词法、语法、语义分析和代码生成。此外必需有符号表管理程序和出错处理程序。本章介绍的PL/0编译程序的实现是用PASCAL语言书写的。 【学习目标】 本章目的:以PL/0语言编译程序为实例,学习编译程序实现的基本步骤和相关技术,对编译程序的构造和实现得到一些感性认识和建立起整体概念,为后面的原理学习打下基础。 ◇了解并掌握用语法图和扩充的巴科斯-瑙尔范式(EBNF)对PL/0语言的形式描述。 ◇了解并掌握PL/0语言编译程序构造和实现的基本技术和步骤。 ◇了解并掌握PL/0语言编译程序的目标程序在运行时数据空间的组织管理。 【学习指南】 ◇要求读者阅读PL/0语言编译程序文本,了解一个编译程序构造的必要步骤和实现技术。一个编译程序的实现比较复杂,读懂一个典型的程序从设计思想到实现技术也有一定难度,特别是入门开始需要耐心。一但读懂,不仅了解编译程序的实现方法和技术,还可学到许多编程技巧和好的编程风格。 ◇阅读PL/0语言编译程序文本时,应从整体结构开始逐步细化,弄清楚每个过程的功能和实现方法及过程之间的相互关系。 ◇建议用一个PL/0源程序的例子为导引作为阅读PL/0语言编译程序文本的入门,然后再逐步全面读懂。 ◇通过对PL/0语言编译程序某些指定功能的扩充,加深对编译程序构造步骤和实现技术的理解,并能在实践中应用。 【难重点】 重点: ◇弄清源语言(PL/0)目标语言(类pcode)实现语言(pascal) 这3个语言之间的关系和作用。 ◇掌握用语法图和扩充的巴科斯-瑙尔范式(EBNF)对一个高级程序设计语言的形式描述。 ◇了解PL/0语言编译程序的语法分析技术采用的是自顶向下递归子程序法。 ◇掌握PL/0语言编译程序的整体结构和实现步骤,并弄清词法分析、语法分析、语义分析、代码生成及符号表管理每个过程的功能和相互联系。 ◇掌握PL/0语言编译程序的目标程序在运行时采用栈式动态存储管理的实现技术。 难点: ◇符号表管理起着编译期间和目标程序运行时信息联系的纽带,符号表的建立并不困难,但信息之间的关系往往需要反复学习才能理解。 【知识结构】

编译原理PL0报告(附源码教程)

编译原理课程设计 学院计算机学院 专业计算机科学与技术班级 学号 姓名 指导教师 20 年月日

一、课程设计要求 基本内容(成绩范围:“中”、“及格”或“不及格”) (1)扩充赋值运算:*= 和/= 扩充语句(Pascal的FOR语句): ①FOR <变量>:=<表达式> TO <表达式> DO <语句> ②FOR <变量>:=<表达式> DOWNTO <表达式> DO <语句> 其中,语句①的循环变量的步长为2, 语句②的循环变量的步长为-2。 (3)增加运算:++ 和--。 选做内容(成绩评定范围扩大到:“优”和“良”) (1)增加类型:①字符类型;②实数类型。(2)扩充函数:①有返回值和返回语句;②有参数函数。(3)增加一维数组类型(可增加指令)。 (4)其他典型语言设施。 二、概述 目标:实现PL0某些特定语句 实现语言:C语言 实现工具平台:VS201 运行平台:WIN7 三、结构设计说明与功能块描述 PL/0编译程序的结构图

PL/0编译程序的总体流程图 四、主要成分描述 1、符号表 编译程序里用了一个枚举类型enum symbol,然后定义了enum symbol sym来存放当前

的符号,前面讲过,主程序定义了一个以字符为元素的一维数组word,称保留字表,这个保留字表也存放在符号表里,为了识别当前的符号是属于哪些保留字;还有标识符,拼数,拼符合词等的符号名都存放在符号表里,当sym存放当前的符号时,我们可以判断它是属于哪类的符号,然后加以处理。 在运行的过程中,主程序中又定义了一个名字表,也就是符号表,来专门存放变量、常量和过程名的各个属性,里面的属性包括name,kind,val/level,adr,size,我们来举一个PL/0语言过程说明部分的片段: Const a=35,b=49; Var c,d,e; Procedure p; Var g; 当遇到标识符的引用时就调用position函数,根据当前sym的符号类型来查table表,看是否有过正确的定义,若已有,则从表中取相应的有关信息,供代码的生成用。若无定义则调用出错处理程序。 2、运行时存储组织和管理 对于源程序的每一个过程(包括主程序),在被调用时,首先在数据段中开辟三个空间,存放静态链SL、动态链DL和返回地址RA。静态链记录了定义该过程的直接外过程(或主程序)运行时最新数据段的基地址。动态链记录调用该过程前正在运行的过程的数据段基址。返回地址记录了调用该过程时程序运行的断点位置。对于主程序来说,SL、DL和RA的值均置为0。静态链的功能是在一个子过程要引用它的直接或间接父过程(这里的父过程是按定义过程时的嵌套情况来定的,而不是按执行时的调用顺序定的)的变量时,可以通过静态链,跳过个数为层差的数据段,找到包含要引用的变量所在的数据段基址,然后通过偏移地址访问它。

PL0编译程序设计说明书

PL/0编译程序设计说明书 小组组长:李文(00000000) 小组成员:******(00000000) ******(00000000) ******(00000000) ******(00000000) 2006年1月16日

1引言 (3) 1.1编写目的 (3) 1.2背景 (3) 1.3定义 (3) 1.4参考资料 (5) 2总体设计 (5) 2.1需求规定 (5) 2.2运行环境 (6) 2.3模块流程 (6) 2.4模块机理说明 (7) 2.5模块设计与实现 (10) 2.6人工处理过程 (12) 2.7程序的亮点 (13) 2.8尚未问决的问题 (13) 3程序介绍和使用说明 (13) 4程序测试结果分析 (16)

概要设计说明书 1引言 1.1编写目的 此文档为VK 1.0版PL/0编译器详细设计说明书,旨在说明该编译器的设计思想和实现。此说明书可以在阅读本编译器代码时有效地帮助您理解本编译器的设计方法和实现过程,希望能提供给您所需的帮助。 1.2背景 名称:VK1.0版PL/0编译器 说明: 此编译器为北京师范大学信息科学学院计算机科学与技术系2003级2005-2006学年度第一学期编译原理课程实验作业。 本软件由李文小组合作开发,组长李文,同组人员有吕叶、刘晟忻、肖纯、曲文星。 本软件主要用于PL/0程序的编译,软件提供生成中间代码,并执行检测。本软件为开源软件,故除了用于编译以外还可给编译器开发者提供开发参考。 本软件开发运行在Windows 32位平台上,尚未开发其他平台版本。 1.3定义 本软件开发中,用到了一些PL/0语言和PCODE的定义以及编译原理术语,下面给予说明。 ●PL/0语言: ?BNF范式: 〈程序〉∷=〈分程序〉. 〈分程序〉∷=[〈常量说明部分〉][〈变量说明部分〉][〈过程说明部分〉]〈语 句〉 〈常量说明部分〉∷=CONST〈常量定义〉 {,〈常量定义〉}; 〈常量定义〉∷=〈标识符〉=〈无符号整数〉 〈无符号整数〉∷=〈数字〉{〈数字〉} 〈变量说明部分〉∷=VAR〈标识符〉{,〈标识符〉}; 〈标识符〉∷=〈字母〉{〈字母〉|〈数字〉} 〈过程说明部分〉∷=〈过程首部〉〈分程序〉{;〈过程说明部分〉}; 〈过程首部〉∷=PROCEDURE〈标识符〉; 〈语句〉∷=〈赋值语句〉|〈条件语句〉|〈当型循环语句〉|〈过程调用语句〉| 〈读语句〉|〈写语句〉|〈复合语句〉|〈空〉; 〈赋值语句〉∷=〈标识符〉∶=〈表达式〉

实验一 PL0编译程序

编译原理上机实验一 1.软件准备 (1)、pl/0编译程序:pl0 .exe (2)、pl/0编译程序源代码程序(PASCAL程序):pl0.pas (或pl0pl0.pas) (3)、pl/0程序实例(文本文件): text.pl0, text1.pl0, test2.pl0, … text9.pl0共10个例子 2.实验要求: (1)阅读pl/0源程序实例(text.pl0,test1.pl0,………text9.pl0共10个例子)理解每个PL0程序的功能;熟悉并掌握pl/0语言相关规则。 (2)用pl/0编译程序,对提供的10个例子逐一进行编译并运行。熟悉pl/0编译程序使用操作方法。通过理解每个实例程序的功能,编程思想, 进一步理解PL/0语言的语法及其语义;提高阅读程序的能力,应用 程序设计语言的编程能力;增强程序语言语法、语义意识。 (3)用pl/0语言编写以下程序,进一步熟悉PL/0语言: a、由三角形的三条边长计算三角形面积。 b、编写一个PL0过程,计算以a为直径的圆的面积。用主程序确定圆 的直径,输出计算结果。 c、编写递归程序,计算n!。 d、计算1000之内的所有素数,并输出结果。求出1000之内所有素数之 和。(要求编写3个以上的pl/0过程实现) 3.文件(软件)使用说明 (1)、打开文件目录“PL0编译…”,运行PL/0编译程序(pl0.exe),按提示要求输入PL0源程序文件名(如test1.pl0),┉ (2)、打开Fa2.txt文件,可看到当前经过编译并运行后的pl/0程序的目标代码及其执行结果。 (3)、打开out1.txt或out2.txt、┉或out9.txt文件,可看到各个pl/0程序实例的编译、运行、操作过程结果。 4.提交实验报告及要求 (1)、简述test4.pl0 …test7.pl0各程序语法含义,执行结果。 (2)、提交实验要求编写的4个PL/0源程序及其执行结果。 (3)、简写本次实验的收获与体会。

PL0+语言编译器分析实验

编译原理实验报告 课题名称:PL/0 语言编译器分析实验 学院: 专业: 姓名: 学号: 指导老师: 完成时间:

一、实验目的 通过阅读与解析一个实际编译器(PL/0语言编译器)的源代码,加深对编译阶段(包括词法分析、语法分析、语义分析、中间代码生成等)和编译系统软件结构的理解,并达到提高学生学习兴趣的目的。 二、实验要求 (1)要求掌握基本的程序设计技巧(C语言)和阅读较大规模程序源代码的能力; (2)理解并掌握编译过程的逻辑阶段及各逻辑阶段的功能; (3)要求能把握整个系统(PL/0语言编译器)的体系结构,各功能模块的功能,各模块之间的接口; (4)要求能总结出实现编译过程各逻辑阶段功能采用的具体算法与技术。 三、实验原理

四、实验报告 pl/0语言是pascal语言的一个子集,我们这里分析的pl/0的编译程序包括了对pl/0语言源程序进行分析处理、编译生成类pcode代码,并在虚拟机上解释运行生成的类pcode代码的功能。 pl/0语言编译程序采用以语法分析为核心、一遍扫描的编译方法。词法分析和代码生成作为独立的子程序供语法分析程序调用。语法分析的同时,提供了出

错报告和出错恢复的功能。在源程序没有错误编译通过的情况下,调用类pcode 解释程序解释执行生成的类pcode代码。 词法分析子程序分析: 词法分析子程序名为getsym,功能是从源程序中读出一个单词符号(token),把它的信息放入全局变量sym、id和num中,语法分析器需要单词时,直接从这三个变量中获得。(注意!语法分析器每次用完这三个变量的值就立即调用getsym 子程序获取新的单词供下一次使用。而不是在需要新单词时才调用getsym过程。)getsym过程通过反复调用getch子过程从源程序过获取字符,并把它们拼成单词。getch过程中使用了行缓冲区技术以提高程序运行效率。 词法分析器的分析过程: 调用getsym时,它通过getch过程从源程序中获得一个字符。如果这个字符是字母,则继续获取字符或数字,最终可以拼成一个单词,查保留字表,如果查到为保留字,则把sym变量赋成相应的保留字类型值;如果没有查到,则这个单词应是一个用户自定义的标识符(可能是变量名、常量名或是过程的名字),把sym 置为ident,把这个单词存入id变量。查保留字表时使用了二分法查找以提高效率。如果getch获得的字符是数字,则继续用getch获取数字,并把它们拼成一个整数,然后把sym置为number,并把拼成的数值放入num变量。如果识别出其它合法的符号(比如:赋值号、大于号、小于等于号等),则把sym则成相应的类型。如果遇到不合法的字符,把sym置成nul。 语法分析子程序分析: 语法分析子程序采用了自顶向下的递归子程序法,语法分析同时也根据程序的语意生成相应的代码,并提供了出错处理的机制。语法分析主要由分程序分析过程(block)、常量定义分析过程(constdeclaration)、变量定义分析过程(vardeclaration)、语句分析过程(statement)、表达式处理过程(expression)、项处理过程(term)、因子处理过程(factor)和条件处理过程(condition)构成。这些过程在结构上构成一个嵌套的层次结构。除此之外,还有出错报告过程(error)、代码生成过程(gen)、测试单词合法性及出错恢复过程(test)、登录名字表过程(enter)、查询名字表函数(position)以及列出类pcode代码过程(listcode)作过语法分析的辅助过程。 由pl/0的语法图可知:一个完整的pl/0程序是由分程序和句号构成的。因此,本编译程序在运行的时候,通过主程序中调用分程序处理过程block来分析分程序部分(分程序分析过程中还可能会递归调用block过程),然后,判断最后读入的符号是否为句号。如果是句号且分程序分析中未出错,则是一个合法的pl/0程序,可以运行生成的代码,否则就说明源pl/0程序是不合法的,输出出错提示即可。 语法单元分析: 1、分程序处理过程: 语法分析开始后,首先调用分程序处理过程(block)处理分程序。过程入口参数置为:0层、符号表位置0、出错恢复单词集合为句号、声明符或语句开始符。进入block过程后,首先把局部数据段分配指针设为3,准备分配3个单元供

PL0编译程序原理实验报告

编译原理实验报告——理解PL/0编译程序原理

实验4.1 理解PL/0编译程序原理 一、实验目的 1.学习使用教学辅助软件THPL0CAI 2.掌握PL/0源程序的编译和解释过程 二、实验平台 Windows + THPL0CAI 三、实验内容 1.运行THPL0CAI 程序 (1)选择0 - Static Link 方式进入; (2)选择Open/Create a source file 打开一个PL/0源程序,如Test2.pl0:const a=10; var b,c; procedure p; var k; begin c:=b+10; end; begin read(b); while b#0 do begin call p; write(2*c); read(b) end end. 2.按F9键开始单步编译Test2.pl0 程序 (1)观察符号表的构造过程Table.dat 窗口; (2)观察目标代码的构造过程Code.dat 窗口。 3.按F9键开始单步执行编译Test2.pl0 生成的代码

(1)观察运行栈的变化过程Stack.dat 窗口; (2)观察数据的输入输出Result.dat 窗口。 四、实验分析 1.PL/0编译程序结构 (a)PL/0编译程序的结构图 (b)PL/0的解释执行结构PL/0语言是PASCAL语言的子集 ----指令功能表

2.给出编译过程中符号表的建立过程Code.dat:

符号表table.dat如图所示: 符号表建立过程: (1)运行主程序,“main”存入符号表,类型为procedure,地址:8,大小:6;(2)常量a存入符号表,值为10; (3)存入b,c,类型为variable,地址分别为3,4; (4)执行p程序,存入p,类型为procedure,地址为1; (5)存入k,类型为variable,地址为3;被引用变量或过程所在层次为1;3.给出运行过程中运行栈的变化过程,只给出部分说明即可 程序开始 (1)输入b=5,将变量b的取值取至栈顶。 (2)将常数值0进栈,与b的值作比较; B≠0,值为true,取至栈顶;

理解PL0编译程序原理

课程: 编译原理理解PL/0编译程序原理 实验报告 系 专业 班级 姓名 学号 指导教师

1.实验目的 1. 学习使用教学辅助软件THPL0CAI 2. 掌握PL/0源程序的编译和解释过程 2.实验平台 Windows + THPL0CAI 3.实验内容 目录:pl0演示 1.运行THPL0CAI 程序 1) 选择0 - Static Link 方式进入 2) 选择Open/Create a source file 打开一个PL/0源程序, 如Test2.pl0 const a=10; var b,c; procedure p; var k; begin c:=b+10; end; begin read(b); while b#0 do begin call p; write(2*c); read(b) end end.

2.按F9键开始单步编译Test2.pl0 程序 1) 观察符号表的构造过程Table.dat 窗口 2) 观察目标代码的构造过程Code.dat 窗口 3. 按F9键开始单步执行编译Test2.pl0 生成的代码 1) 观察运行栈的变化过程Stack.dat 窗口 2) 观察数据的输入输出Result.dat 窗口 4.实验报告 给出编译过程中符号表的建立过程, 1.选定编译内容test2 2.按空格键进行编译

3.得出的符号表table.dat 给出运行过程中运行栈的变化过程,只给出部分说明即可。 1.F9开始运行栈 2.输入2,给变量赋值

3.输入3,给变量赋值,得出结果 4.输入0,结束运行栈 5.思考题 1) 理解编译和解释的含义,目标代码是按何种方式执行的? PL/0编译程序所产生的目标代码是一个假想栈式计算机的汇编语言,可称为类PCODE指令代码,它不依赖任何具体计算机,其指令集极为简单,指令格式也很单纯,其格式如下: f l a 其中f代表功能码,l表示层次差,也就是变量或过程被引用的分程序与说明该变量或过程的分程序之间的层次差。a的含意对不同的指令有所区别,对存取指令表示位移量,而对其它的指令则分别有不同的含义,见下面对每条指令的解释说明。 目标指令有8条:

PL0编译器功能扩充要点

Ningxia Normal University PL0编译器 题目 Pl0编译器功能扩充 姓名 学号 院(系)数学与计算机科学学院 专业班级计算机技术与科学2班 时间 2014-1-5 1

目录 一、实验目的 ---------------------------- 3 二、实验内容 ---------------------------- 3 三、实验框图 ---------------------------- 4 四、过程分析 ---------------------------- 6 1、词法过程分析 ----------------------- 6 2、语法过程分析 ----------------------- 6 3、整体过程分析 ----------------------- 7 4、扩充过程分析 ----------------------- 9 五、测试结果 --------------------------- 11 六、问题及感受 --------------------------- 12 2

本次实验设计主要是在分析理解PL/0编译程序的基础上,对其词法分析程序、语法分析程序和语义处理程序进行部分修改扩充,使其增加并且实现了更多的功能。 二、实验内容 PL/0语言是Pascal语言的一个子集,这里分析的PL/0的编译程序包括了对PL/0语言源程序进行分析处理、编译生成类PCODE代码,并在虚拟机上解释运行生成的类PCODE代码的功能。 PL/0语言编译程序采用以语法分析为核心、一遍扫描的编译方法。词法分析和代码生成作为独立的子程序供语法分析程序调用。语法分析的同时,提供了出错报告和出错恢复的功能。在源程序没有错误编译通过的情况下,调用类PCODE解释程序解释执行生成的类PCODE代码。 扩充PL0语言是在PL0语言的基础上增加对整型一维数组的支持、扩充IF-THEN-ELSE条件语句、增加REPEAT语句。如下所示:(1)整型一维数组,数组的定义格式为: VAR <数组标识名>(<下界>:<上界>) 其中上界和下界可以是整数或者常量标识名 访问数组元素的时候,数组下标是整型的表达式,包括整数、 常量或者变量和它们的组合 (2)扩充条件语句,格式为: <条件语句>::= IF <条件>THEN <语句>[ELSE <语句>] (3)增加REPEAT语句,格式为: <重复语句> ::= REPEAT <语句> UNTIL <条件> (4) 注释 单行注释以 { 开始,以 } 结束,注释内容不包括{和 } 3

PL0编译程序的语法错误处理

PL0编译程序的语法错误处理 1.基本法则 关键字法则:语法结构,尤其是每种构造语句和说明,以关键字开头。 镇定法则:发现非法结构后,即跳过后面的输入正文,直到下一个可以正确地后随当前正在分析的句子结构的符号为止。亦即每一分析程序知道在其当前活动点的后继符号的集合。 2.处理方法 (1)给每个分析函数提供一个参数FSYS,它指明可能的后继符号。在每个函数的末尾包括一个测试,以保证输入正文的下一个符号真的属于后继符号集(如果有语法错误的话)。 (2)为了尽量减少忽略直到下一个后继符号为止的中间所有正文,在后继符号集添加一些关键字,它们专门标记那些不容忽略的结构的开始符。因此,作为参数传递给分析函数的就不仅是后继符号了,可称为停止符号。具体来说,先用一些明显的关键字给它们赋予初值,然后随着分析子目标的层次的深入,逐步补充别的合法符号。TEST函数就是用来完成这些验证工作的,它有三个参数: ①可允许的下一个符号的集合S1;若当前符号不属于此集合,则当即得到一个错误。 ②另加的停止符号集S2,这些符号的出现虽然是错的,但是它们绝对不应被忽略而跳过。 ③整数N,表示有关错误的代码。 void TEST(SYMSET S1, SYMSET S2, int N){ if (!SymIn(S1)) { ERROR(N); while (!SymIn(S1+S2)) GetSym(); } } (3)TEST也可以用作分析函数的入口,以验证当前符号是否为允许的头符号。在下述情况下,这一方法值得推荐。比如规则A::=a1S1|…|a n S n|X 的翻译结果是 if (SYM==a1)S1(); else … if (SYM==a n)S n(); else X(); 此时,分析函数X是无条件被调用的。此时,参数S1必须为X的头符号集合,而S2则选为A的后继符号集合FOLLOW(A)。 以因子(FACTOR)的语法分析函数为例,在函数FACTOR的入口处调用了一次TEST函数,它的实参S1是因子开始符号的集合(FACBEGSYS),S2是每个函数的形参FSYS调用时实参的传递值。 当编译程序第一次调用BLOCK时,FSYS的实参为:[.]与说明开始符和语句开始符的和集。以后随着调用语法分析函数层次的深入,FSYS的传递值逐步增加。例如,调用STATEMENT时增加了[;]和[ENDSYM];在表达式语法分析中调用TERM时又增加了[+]和[-],进而调用FACTOR时又增加了[*]和[/];这样在进入

PL_0_语言编译器分析实验报告

PL/0 语言编译器分析实验 一、实验目的 通过阅读与解析一个实际编译器(PL/0语言编译器)的源代码,加深对编译阶段(包括词法分析、语法分析、语义分析、中间代码生成等)和编译系统软件结构的理解,并达到提高学生学习兴趣的目的。 二、实验要求 (1)要求掌握基本的程序设计技巧(C语言)和阅读较大规模程序源代码的能力; (2)理解并掌握编译过程的逻辑阶段及各逻辑阶段的功能; (3)要求能把握整个系统(PL/0语言编译器)的体系结构,各功能模块的功能,各模块之间的接口; (4)要求能总结出实现编译过程各逻辑阶段功能采用的具体算法与技 三、实验报告 pl/0语言是pascal语言的一个子集,我们这里分析的pl/0的编译程序包括了对pl/0语言源程序进行分析处理、编译生成类pcode代码,并在虚拟机上解释运行生成的类pcode代码的功能。 pl/0语言编译程序采用以语法分析为核心、一遍扫描的编译方法。词法分析和代码生成作为独立的子程序供语法分析程序调用。语法分析的同时,提供了出错报告和出错恢复的功能。在源程序没有错误编译通过的情况下,调用类pcode 解释程序解释执行生成的类pcode代码。 词法分析子程序分析: 词法分析子程序名为getsym,功能是从源程序中读出一个单词符号(token),把它的信息放入全局变量sym、id和num中,语法分析器需要单词时,直接从这三个变量中获得。(注意!语法分析器每次用完这三个变量的值就立即调用getsym 子程序获取新的单词供下一次使用。而不是在需要新单词时才调用getsym过程。)getsym过程通过反复调用getch子过程从源程序过获取字符,并把它们拼成单词。getch过程中使用了行缓冲区技术以提高程序运行效率。 词法分析器的分析过程: 调用getsym时,它通过getch过程从源程序中获得一个字符。如果这个字符是字母,则继续获取字符或数字,最终可以拼成一个单词,查保留字表,如果查到为保留字,则把sym变量赋成相应的保留字类型值;如果没有查到,则这个单词应是一个用户自定义的标识符(可能是变量名、常量名或是过程的名字),把sym 置为ident,把这个单词存入id变量。查保留字表时使用了二分法查找以提高效率。如果getch获得的字符是数字,则继续用getch获取数字,并把它们拼成一个整数,然后把sym置为number,并把拼成的数值放入num变量。如果识别出其它合

相关主题
文本预览
相关文档 最新文档