精华内容
下载资源
问答
  • PL0扩展(数组,if-else,注释)

    千次阅读 2017-11-08 09:44:57
    PL0扩展(数组,if-else,注释)本文将讲解如何扩展原有pl0编译器的功能,即增加标题所描述的三个功能。 项目github地址:https://github.com/chaolu0/PL0-extend

    PL0扩展(数组,if-else,注释)

    本文将讲解如何扩展原有pl0编译器的功能,即增加标题所描述的三个功能。
    项目github地址:https://github.com/chaolu0/PL0-extend
    该项目为编译原理课程作业,如有错误,欢迎指正。

    编译器(先来点虚的)

    说一下编译器的三个部分。
    词法分析:用来把每个单词拆分出来,并明确单词的类型。
    语法分析:读入多个单词,看是否符合既定语法规则。
    语义分析:即根据语句操作内存和CPU,以完成功能。

    前提

    pl0是基于假象栈式计算机的,即所有变量都会存储到一个栈当中,同时有一个指令的数组,通过一个指针来标识当前指令地址,指令的操作都基于栈来实现。栈底部一般都是变量,顶部是用来计算的临时变量。首先明白了这个,才容易理解后面的代码。

    增加注释

    注释,用来辅助程序员看懂程序的部分,并不会影响真正代码的运行。由于其只涉及到词法分析部分,我们处理它的方法是:当遇到’{’的时候,我们一直获取下一个字母,一直到出现’}’为止,这部分内容我们直接丢弃掉,如此便可使注释不参加进一步的编译。下面来看一下具体实现。Ps:以下所有带有do结尾的,都是宏定义,实际都是函数。为了方便,直接按照函数来处理

    //定位getsymdo
    if (ch == '{')
    {
        do{
            if (cc == ll&&ch != '}'){
                error(31);//没有匹配的右括号
            }
            getchdo;//读入下一个字符
        } while (ch != '}');
        getchdo;//读入下一个符号前,必须先读入下一个字符
        getsymdo;//获取下一个符号,下一个符号为真实符号,当前'}'符号没有意义
    }

    首先,我们需要遇到当前字母是左括号的情况,然后通过do-while循环一直读取下一个字母,直到ch==’}’,在这个循环里面的条件判断是为了处理读完整个符号都没能遇到右括号的情况,即发生括号不匹配的错误。最后一个getsymdo是为了不影响之后程序的运行,当前注释没有意义,所以再读入一个符号作为本次读入的结果。

    增加if-else

    原有pl0语言只支持if-then,即是只支持if语句,为了增加else,我们先来看一下它是如何实现if跳转的。

    //定位statement
    if (sym == ifsym)
    /* 准备按照if语句处理 */
    {
        getsymdo;
        memcpy(nxtlev, fsys, sizeof(bool) * symnum);
        nxtlev[thensym] = true;
        nxtlev[dosym] = true;
        /* 后跟符号为then或do */
        conditiondo(nxtlev, ptx, lev);
        /* 调用条件处理(逻辑运算)函数 */
        if (sym == thensym) {
            getsymdo;
        } else {
            error(16);
            /* 缺少then */
        }
        cx1 = cx;
        /* 保存当前指令地址 */
        gendo(jpc, 0, 0);
        /* 生成条件跳转指令,跳转地址未知,暂时写0 */
        statementdo(fsys, ptx, lev);
        /* 处理then后的语句 */
        code[cx1].a = cx;
        /* 经statement处理后,cx为then后语句执行完的位置,它正是前面未定的跳转地址 */
    }

    代码有点长,但是我们一条一条来看还是容易看懂的。
    第一个getsymdo是为了下面的conditiondo服务,conditiondo上面的三行是用来准备检查下一个符号是不是then或都的,暂时忽略。conditiondo这个函数是用来处理if语句后面的内容,其实就是一个逻辑表达式的处理,执行完这条语句的时候,栈顶会存放计算的结果值,后面jpc指令就是用来根据栈顶值决定要不要跳转的。

    下面是重点!

    cx1=cx;这条语句用cx1记录了下个指令的位置,当前指令即是下面gendo(jpc, 0, 0);所生成的指令jpc,为什么在这里生成了条件跳转指令呢?我们先接着往下看。statementdo(fsys, ptx, lev);这条语句是用来处理then后面的语句,其实就相当于高级语言if为真执行的语句。 code[cx1].a = cx;语句记录了then里面语句执行完之后的下一个地址。现在我们来理解这个过程。我们考虑如果if为真,那么整个程序其实相当于顺序执行,而为假的时候,我们就需要跳过then中的语句,而去执行下面的语句。而它的处理正和我们想法一致:当if为真,条件跳转并不会执行(虽然有语句),当if为假,我们就把then后面语句都走完之后,把下一个条指令的地址放到条件跳转的地址当中去,这样就可以实现跳过then中间的语句。

    我们考虑一下if-else,增加else之后,if为假实际上没有什么影响,因为正好跳转到了else的位置,但是if为真的时候,我们需要跳过的就是else语句部分了。根据原有代码,我们给出下面的代码:

    if (sym == ifsym)
    /* 准备按照if语句处理 */
    {
        getsymdo;
        memcpy(nxtlev, fsys, sizeof(bool) * symnum);
        nxtlev[thensym] = true;
        nxtlev[dosym] = true;
        nxtlev[elsesym] = true;
        /* 后跟符号为then或do */
        conditiondo(nxtlev, ptx, lev);
        /* 调用条件处理(逻辑运算)函数 */
        if (sym == thensym) {
            getsymdo;
        } else {
            error(16);
            /* 缺少then */
        }
        cx1 = cx;
        /* 保存当前指令地址 */
        gendo(jpc, 0, 0);
        /* 生成条件跳转指令,跳转地址未知,暂时写0 */
        statementdo(fsys, ptx, lev);
        /* 处理then后的语句 */
        cx3 = cx;
        gendo(jmp, 0, 0); //将来会直接跳转到else语句后面
        code[cx1].a = cx;
        /* 经statement处理后,cx为then后语句执行完的位置,它正是前面未定的跳转地址 */
        if (sym == elsesym) {
            getsymdo;
            statementdo(fsys, ptx, lev);
            code[cx3].a = cx; //当前是else后面的语句结束位置,if语句执行后应当跳转至此
        }
    
    }

    实际上我们就是在if的then语句执行完毕后,增加了一个直接跳转jmp,因为如果执行了then,说明一定不会执行else,应该直接跳过else,跳过的方式如上面所述。
    至此if-else也完美解决。Ps:有些小细节,比如else关键字什么的自行处理一下就好~

    数组

    数组是这里面最难的部分,我们需要理解里面的几个函数,首先看下enter(填写名字表函数)

    名字表

    /* 名字表结构 */
    struct tablestruct
    {
        char name[al];      /* 名字 */
        enum object kind;   /* 类型:const, var, array or procedure */
        int val;            /* 数值,仅const使用 */
        int level;          /* 所处层,仅const不使用 */
        int adr;            /* 地址,仅const不使用 */
        int size;           /* 需要分配的数据区空间, 仅procedure使用 */
    };
    void enter(enum object k, int* ptx, int lev, int* pdx)
    {
        (*ptx)++;
        strcpy(table[(*ptx)].name, id); /* 全局变量id中已存有当前名字的名字 */
        table[(*ptx)].kind = k;
        switch (k)
        {
        case constant:  /* 常量名字 */
            if (num > amax)
            {
                error(31);  /* 数越界 */
                num = 0;
            }
            table[(*ptx)].val = num;
            break;
        case variable:  /* 变量名字 */
            table[(*ptx)].level = lev;
            table[(*ptx)].adr = (*pdx);
            (*pdx)++;
            break;
        case procedur:  /* 过程名字 */
            table[(*ptx)].level = lev;
            break;
        }
    }

    名字表(是一个结构体数组),用来存储所有变量,常量,过程的东西,里面记录的字段如上面结构体所示,ptx是数组的尾指针,用来插入下一个值。我们要增加的数组也属于变量,所以我们看变量部分,首先是层次,不用关心,照写就可以,然后是地址,地址我们需要变化。因为一个普通变量只占1个位置,而我们的数组是多个位置,这里就是分配内存的地方,所以我们要+=,而不是++。上代码

    /*
    * start:   数组开始位置
    * end:     数组结束为止
    *
    * 其他变量同enter方法
    */
    void enterArray(int* ptx, int lev, int*pdx, int start, int end,char* id){
    
        (*ptx)++;//增加名字表内容
        strcpy(table[(*ptx)].name, id);
        table[(*ptx)].kind = array;//数组类型
        table[(*ptx)].level = lev;//层次
        table[(*ptx)].adr = (*pdx);//数组首地址
        table[(*ptx)].low = start;//用来记录初始的下标值,例如:定义a(10:11),low就是10,将来我们访问元素的时候应该减掉这个值,因为我们实际分配给数组的地址偏移量是从0开始的。即a(10)实际上是a(0);
        (*pdx) += (end - start + 1);//连续内存地址分配给该数组
    }

    变量声明部分

    这段代码比较简单,实际上就是看定义符不符合数组定义,例如a(b,12);我们需要从中获取b和12的值,然后调用上面的enterArray方法。由于逻辑比较简单,不再赘述。

    数组的读入

    读、写、赋值、使用都需要单独处理,我们现在以读入为例讲解,其余实现都比较类似。
    我们先来看原有的读入是怎么写的

    if (table[i].kind == variable) { //read(a);
        gendo(opr, 0, 16);
        /* 生成输入指令,读取值到栈顶 */
        gendo(sto, lev - table[i].level, table[i].adr);
        /* 储存到变量 */
        getsymdo;
    }

    第一句生成输入指令,将来就会把读入的值放到栈顶,第二句,根据名字表中存储的该变量的地址,把栈顶的值放到那个地址中,然后获取下个符号,进行后序的操作。

    现在考虑我们读入数组,假如我们有定义a(0:3),将来要read(a(1))。我们在名字表中能查找到的只有a,也就是a(0)的地址,那我们怎么知道a(1)在哪里存储呢?其实就是基地址+偏移量。基地址就是数组首地址,即a的地址,偏移量就是1,a(1)的地址就是a.addres+1。考虑到可能有read(a(0+1))这样的情况,我们使用原有的表达式处理函数。

    expressiondo这个函数会把表达式的值放到栈顶,这并不符合我们的要求,我们希望他给我们返回一个值,然后使用sto指令,把地址传进去。那我们怎么解决这个问题?答案就是我们也把处理延后

    既然偏移量在栈顶,我们把基地址也放到栈顶,然后相加,那么栈顶的就是真实地址,然后我们就会发现sto指令并不能满足我们,因为它使用的是名字表的地址,而我们的地址在栈顶,所以我们自己再写一个指令sto2来完成我们需要的功能

    case sto:
        /* 栈顶的值存到相对当前过程的数据基地址为a的内存 */
        t--;
        s[base(i.l, s, b) + i.a] = s[t];
        break;
    case sto2:
        t--;
        s[base(i.l, s, b) + s[t - 1]] = s[t];
        break;

    很简单,基本没有变化,只不过sto的地址是传入的,而sto2的地址是从栈顶拿到的,这样我们就解决了找到数组元素并赋值的过程~看代码

    if (table[i].kind == variable) { //read(a);
        gendo(opr, 0, 16);
        /* 生成输入指令,读取值到栈顶 */
        gendo(sto, lev - table[i].level, table[i].adr);
        /* 储存到变量 */
        getsymdo;
    } else {
    
        getsymdo;
        expressiondo(nxtlev, ptx, lev, true, i); //括号内的表达式,将偏移量放到栈顶
        gendo(lit, 0, table[i].adr); //基地址
        gendo(opr, 0, 2); //当前栈顶是真实地址
        gendo(opr, 0, 16);
        /* 生成输入指令,读取值到栈顶 */
        gendo(sto2, lev - table[i].level, 0);
    }

    代码很简单,看一下就懂~,这样我们就完成了~。剩下的写,赋值,使用都类似,只说一下要修改的地方,不再展开讨论。

    lod指令,它与sto存在问题相同,需要修改
    expressiondo,由于存在数组的运算,所以需要修改这个函数。
    词法部分,语法部分,数组导致这两个部分有变化,但修改起来不难。例如数组定义时使用的冒号,需要增加这个符号。

    实现了这几个部分,整个扩展就完成了,完成后面这几部分,将会让你的理解更加深刻!

    **本人大学生程序员一名,以上只是写的过程中的经验之谈,如有错误,欢迎指正!
    热爱代码,所以奋斗!**

    展开全文
  • 扩展了一些功能,++,--,*=,/=,取反,取余,一维数组,for循环,while循环,write包含换行,可以输出文本等功能,里面有具体的测试例子
  • 仿照Pascal语言,对pl0编译程序(清华大学书后源码)进行以下简单扩展—— 不等号: 指针: & @ if - else for ++ -- += -= 数组 程序还有多不足,希望和大家共同学习!!
  • PL0编译器扩展

    2019-01-12 00:24:30
    PL0编译器C语言扩充。包括:++,+=,for,else,注释,不等号,repeat,数组,变量名下划线
  • 支持if then else 语句的pl0扩展c语言文件
  • PL0编译器分析与语法扩展

    万次阅读 多人点赞 2015-12-25 09:54:21
    pl0 编译器扩展

    转载请注明来自 b0t0w1’blog

    一、简介

    1.什么是 pl0 语言

      PL 语言是PASCAL语言的一个子集,该语言不太大,但能充分展示高级语言的最基本成分。PL0具有子程序概念,包括过程说明和过程调用语句。在数据类型方面,PL0只包含唯一的整型,可以说明这种类型的常量和变量。运算符有+,-,*,/,=,<>,<,>,<=,>=,(,)。说明部分包括常量说明、变量说明和过程说明。

    2.pl0语言语法

      给个简单的例子,program 表明这是 pl0 语言, main 为程序名称,接下是变量声明与过程声明,beginend 中间为 main 的执行程序,注意的是声明必须在一开始就全部声明完毕。

    program main;
    var a: integer;
    procedure P1 (a:integer); 
      begin 
         var  b: integer;
         b := a;
         // 输出 b
         call write(b)   
      end;
    begin
      // 输入 a
      call read(a);
      call P1(a)
    end.
    

      需要注意的是每个程序块内如果只有一条语句,可以不加begin..end 结构,但是多条语句必须使用 begin…end结构。同时,除了最后一条语句外,每句都已 ‘;’ 结尾。 上面的 call write(b) 和 call P1(a) 为最后一句,不加分号。

    3.pl0编译器

      编译器由两个项目工程组成, pl 工程将 pl0 语言翻译成中间语言, interpret 工程根据中间语言代码生成可执行文件。
      这次所使用的编译器已实现 while 循环和 if 判断语句,变量类型方面实现了整型和字符型以及数组,运算实现了加减乘除、赋值以及 bool 运算。但不支持 for 循环等语法。

    4.功能扩展

      本次在 pl0 编译器原有的基础上添加了 for 循环 、 repeat 循环, case 条件选择、 +=赋值、 -=赋值、{}注释、–、++ 等。这些功能都是在 PL 工程下实现,本次实验不修改 interpret 工程。

    二、 原编译器分析

    1. 整体思路

    本次分析只分析扩展功能时需要使用的代码,其他略过。
      interpret 工程用于翻译中间语言代码,此次不作修改、不作分析直接使用就好。 PL 工程中 pl.cpp 用于生成中间语言代码,其他 .cpp 用于 I/0 操作,亦不作分析。
      此次分析从各函数入手,着重分析扩展语法时需要用到,需要修改的函数。对函数的分析比较简要,想深入了解的欢迎下载上面链接的源代码,里面有更为详尽的分析。

    2. 函数分析

      main() 函数中,首先输入 pl0 源程序,然后处理源程序输出中间语言代码。main() 函数虽短,但是调用了复杂的函数,函数之间嵌套频繁,个人觉得众多函数才是这个程序的精髓。由于篇幅,只分析需要用到的函数。

    (1)initial()

      initial() 用于初始化,首先是保留字。保留字作为全局变量,应当在文件一开始就声明其个数与具体有哪些。原有为 20 个,此处为我添加后的,每一个对应一个关键字。

    #include "ptoc.h"
    /*
        保留字个数 
    */
    const int norw = 27;      
    enum symbol {nul, ident, intcon, charcon, plus, minus, times, divsym, eql, neq, lss, leq, gtr, geq, ofsym, arraysym, programsym, modsym, andsym, orsym, notsym, lbrack, rbrack, lparen, rparen, comma, semicolon, period, becomes, colon, beginsym, endsym, ifsym, thensym, elsesym, whilesym, repeatsym, dosym, callsym, constsym, typesym, varsym, procsym, forsym, downtosym, tosym, untilsym, casesym, otherwisesym, addsym, subsym, last_symbol};

    同时声明类型、指令等,注意此处不为关键字,只是为了方便本程序处理

    enum oobject {konstant, typel, variable, prosedure, last_oobject};
    
    enum types {notyp, ints, chars, bool_, arrays, last_types};
    
    enum opcod {lit, lod, ilod, loda, lodt, sto, lodb, cpyb, jmp, jpc, red, wrt, cal, retp, endp, udis, opac, entp, ands, ors, nots, imod, mus, add, sub, mult, idiv, eq, ne, ls, le, gt, ge, last_opcod};  

      然后我们看看 initial() 是怎么处理的。只列出扩展的代码,其余详见上面链接下载的工程源码。

      每个 word1[] 元素存放一个关键字,大小只有 10 个字节。这里我只列出扩展的关键字。有没有发现规律?没错,存放时按照大小顺序排列的,因为在查找中使用的是二分查找。
      
      后面的 wsym[] 用于把 word1[] 和前面声明的 symbol{} 对应起来, 注意必须是一一对应,下标顺序必须保持一致。
      然后是 ssym[]mnemonic[],记录着所有的运算符和中间语言指令。同样这些值必须在前面的 enum 处声明。

      扩展功能时,首先扩充前面枚举的值,然后扩充 initial() 的值,注意要分清楚扩充的类型,是关键字还是运算符或者是指令,在相应的地方修改。

    void initial()
    {
        ....
        word1[ 5] = "case      ";
        word1[ 8] = "downto    ";
        word1[11] = "for       ";
        word1[17] = "otherwise ";
        word1[20] = "repeat    ";
        word1[22] = "to        ";
        word1[24] = "until     ";
    
        wsym[ 5] = casesym;
        wsym[ 8] = downtosym;
        wsym[11] = forsym;
        wsym[17] = otherwisesym;
        wsym[20] = repeatsym;
        wsym[22] = tosym;
        wsym[24] = untilsym;
        ....
    }

    initial() 并没有结束,还要初始化一些集合,此处也比较难理解。不懂时略过以后慢慢体会。

      首先思考一个问题,保留字全部声明了,但是保留字的使用也要合乎一定的规则(此处规则不是指类似于 forwhile 循环内部的关键字合法性,而是将 forwhile 看成一个块)。具体来说,比如程序开始,读入 program main ; 确认是 pl0 语言,那么接下来的一定是 varconsttypeprocedure 等声明,而不可能是 forwhileif 等语句,这些语句应该在声明后且也在 begin 后才能出现。也就是说程序中出现就是语法错误,编译器应当报错。

      那如何判断读到的关键字在此处是否合法呢?initial() 集合吧关键字进行了分类,每个类构成一个集合,编译语句时要指明所属的集合,如果超过了说明程序语法错误或者这一块程序编译完需要更换集合。比如前面例子中读到了 begin 说明声明结束,要开始处理 for 等结构了。

    详细如下:
    decbegsys: 声明开始的符号集
    facbegsys: 因子开始的符号集
    typebegsys: 设置类型符号集
    constbegsys: 常量申明符号集
    statbegsys: 语句开始的符号集
      
      其中 statebegsys 因为扩展了 forrepeatcase 等结构,要将该结构的开始关键字放入,但是结构里的关键字不能加入。比如 for 循环里的 to 等,它们的语法是否合法有编译 for 的函数判断。

    statbegsys = set_of_enum(symbol)::of(beginsym, callsym, ifsym, whilesym, forsym, repeatsym, casesym, eos);

    其他集合内容详见代码。

      编译程序时首先要指定关键字的集合,如果不在集合里说明程序语法错误或者要更换集合,比如上面的例子里,都到了 begin , 意味着声明结束,开始处理 forif 等结构了,此时再出现 var 等声明语句就是错误的。initial() 函数还有一些初始化,不再分析。

    废话这么多,也就是 first 集合和 follow 集合的思想。

    (2)getch() 与 getsym()

    下面介绍读入函数。
      getch() 是以字符为单位读入的,遇到空格跳过。每次执行完后读入一个字符到变量 ch 中。getsym() 是以符号位单位读入的,里面有调用 getch() 来完成这一操作,每次执行后读入一个符号到变量 sym 中。比如在编译

    a := b

      赋值语句之前,调用 getsym(),读到了 ‘a’ , 那么 sym = ident, ident 在前面枚举时声明过,代表这是一个变量。

    (3)simpleexpression() 和 expression()

      这两个函数都是处理表达式的值,所不同的是 simpleexpression() 函数用来计算简单表达式,如

    a + b * c

    expression() 用来计算比如大于等于,小于等于。。。等表达式。 如

    a + 1 > b

      其实, expression() 函数中有调用 simpleexpression() 函数,用来计算操作数左右表达式的值。当你只需要计算加减乘除时调用 simpleexpression() 足矣。

      比如按上面的例子,想要处理 a + 1 > b 是真还是假的时候,首先 getsym(), 得到 a, 此时sym = ident。然后调用 expression()expression() 会调用 simpleexpression() 用来计算 a + 1。最后expression() 函数会多调用一次 getsym() 读到 b 之后的下一个符号。simpleexpression() 用法一模一样。

    (4)factor() 和 term()

      这两个函数同样用于计算表达式,计算的表达式也更为简单。 嵌套关系为: simpleexpression() 调用 term()term() 调用 factor()factor() 用于计算因子,而 term() 用于计算项。
      举个例子:
      ++a * 2 + 5
      ++a 由 factor() 计算,* 2 由 term() 计算,+ 5 由 simpleexpression() 计算。因为加减是项与项之间的运算,而乘除是项内的运算,类似于 ++a 则是因子的计算。整个函数嵌套严密,分工明确,我们计算时只用调用 simpleexpression() 就可以了。如果要扩展 ++ 这样的功能,则要去修改 factor()

    (5)position()

      position() 函数顾名思义就是寻找地址,也是众多函数中最简单的一个了。它用来返回声明的一个变量在 nametab[] 中的下表。如果返回值为 0 表示未找到,说明该变量未声明。用法见下面的 case 语句的扩展。

    (6)assignment()

      assignment() 用来完成赋值操作,既可以是整型,也可以是字符型。用法类似于 expression(),比如赋值语句中:

    a := 1

      只需先 getsym() 读到 ‘a‘, 然后执行 assignment() 即可完成赋值。跟 expression() 一样也会多读一个 sym

    (7)block()

      block() 用来把一程序块翻译为中间语言代码,块的范围是指从声明到 begin….end 结束。也就是说 main()只需调用一次 block() 即可完成所有代码的翻译。那么 block() 函数的意思在哪里呢? 比如你声明一个 procedure, procedure 中的格式和 main 是一样的,也是先声明然后是 begin 语句,所以在 procedure 中也只需调用一次 block() 函数。

    (8)statement()

      statement() 函数个人觉得是该编译器的核心,它在 block() 中被调用,用来具体实现代码的翻译,我们许多的扩展都是在这里修改。statement() 每调用一次只能执行一条语句的翻译,但这一条概念不是指一行,而是指一个结构,比如 beginendforwhile 等。虽然这些结构有好多代码,但每个结构只需一个 statement() 便可以解决。所以也就不奇怪为什么 block() 中只调用了一次 statement() 了。因为每个块都是由 begin….end 结构组成的,在 statement() 看来,这是一条语句。至于 begin….end 中使用了很多结构, 比如 forif、赋值等语句每一个都需要调用 statement()

      statement() 函数每识别了一个结构就会调用相应的翻译函数。比如识别了 begin,则会调用 compound() 函数,识别了 while ,则会调用 whilestatement() 函数…….上面所说的调用多条 statement() 就是在 compound() 中实现的,写的非常好,但在此就不讨论了。

    (9)error()

    error()用于告诉程序员错误类型,比如变量未声明,调用 error(10) 来实现。当然是实现规定好的。 

    (10)其他函数

      还有很多函数,这里列举了不到三分之一,有些不会用到,而有些只是直接调用即可不用深入理解。上面着重介绍的也没有给大家分析源码,但建议大家还是看一下下列函数的源码,写的都非常好,深入分析一下会提升对问题的理解程度,真正明白编译器的工作机理。

    • compound()
    • getch()
    • getsym()
    • simpleexpression()
    • expression()
    • factor()
    • term()
    • block()
    • statement()
    • position()
    • assignment()
    • whilestatement()

    虽然有很多,集中时间最多两天就可以看完,还是建议多多看一下。
    函数的分析就到此结束,下面进行指令的简单介绍。

    3.指令简介

    1. 简介

      首先,指令的由 interpret 工程设计,本次并不修改该工程,所以不作详尽分析,只是列出使用的方法。
      为了方便使用,在 PL 工程中使用 gen() 函数边可以生成中间代码指令,比如 gen(add, 0, 0); 便是计算栈顶和次栈顶的加法值,具体过程是 add 指令需要两个操作数,因此弹栈两个,再把加法结果压栈。在使用指令时我们要把操作数放到栈顶和次栈顶。

    三、扩展语法

    1. for 循环

    (1)准备工作

      在全局变量symbol中添加 forsym、tosym、dosym 关键字,然后在initial()函数中的word1[]和 wsym[]数组再次添加,注意这时需要按照ascii码顺序添加,因为在getsy()函数中查找使用了二分查找。最后在initial()函数中的集合statbegsys{}中添加 forsym,不添加tosym和dosym。因为forsym 是作为一个语句的开始符号,而dosym和tosym不可能出现在开始位置。然后在statement()函数中增加对读到 forsym 的判断,当读到 forsym就调用forstatement()函数,当然函数要提前声明。

    (2)代码思路

    for循环例子:
    for a = 1 to 2+3 do ….

    (1)首先判断 for 后面是不是变量
    (2)执行赋值函数 assignment(), 进行 a = 1 的赋值
    (3)赋值语句后面紧接着是保留字 to
    (4)用 cx1 记下此时的 cx。
    (5)执行函数 simpleexpression() 计算 b 表达式的值(2+3)
    (6)调用指令 le, 判断当前 a 是否小于等于 b。 cx2 记下此时的 cx, 如果 a > b 用 jpc 指令跳出 for 循环,但跳出地址未知.
    (7)紧接着是关键字 do。用 statement() 编译程序块,
    (8)把 a 压入栈顶,执行 add 指令 +1, 使用 jmp 跳回到 cx1, 同时记下此时的 cx ,回填前面的 jpc 指令。然后执行第五步……直到跳出for。

    2. 代码

    首先在statement() 中增加判断,函数声明略过

    else if (sym == forsym){
        forstatement(level, fsys, cx1, cx2);
    }

    函数主体

    void forstatement(int &level, symset &fsys, int &cx1, int &cx2){
        item x;
        getsym();
    
        //判断是否为变量
        if (sym == ident){
        //assignment()中会读取下一个符号,下一个符号位 to, 必须把 tosym 加入集合中
            assignment(i, level, fsys + set_of_enum(symbol)::of(tosym, eos));
        }
        else{
            /*
            如果 for 紧接着不是 ident, 为非法字符。
            */
            error(0);
        }
        if (sym == tosym){
            getsym();
    
            /*
            记录条件判断的位置。
            */
            cx1 = cx;
    
            /*
            把循环变量取出放到栈顶
            */
            A1 &with = nametab[i];
            gen(lod, with.lev, with.adr);
    
            /*
            计算 to 后面的表达式
            并读取 'do', 所以集合 fsys 扩充加入 dosym
            */
            simpleexpression(fsys + set_of_enum(symbol)::of(dosym, eos), x, level);
    
            /*
            比较指令, 循环变量与to后面的表达式
            */
            gen(le, 0, 0);
    
            /*
            保存循环结束点
            循环结束点,跳出循环,地址未知。
            */
            cx2 = cx;
            gen(jpc, 0, 0);
    
            if (sym == dosym){
                getsym();
                statement(fsys, level);
    
                /*
                    处理循环变量
                    将循环变量地址压到栈顶
                    将循环变量压到栈顶
                    将步长压到栈顶
                    执行 add 命令, 弹出栈顶和次栈顶, 将 add 结果压到栈顶
                    执行 sto 命令, 弹出栈顶和次栈顶, 将栈顶的值存入次栈顶对应的地址中去,不再压栈
                */
                A1 &with = nametab[i];
                gen(loda, with.lev, with.adr);
                gen(lod, with.lev, with.adr);
                gen(lit, 0, 1);
                gen(add, 0, 0);
                gen(sto, 0, 0);
    
                /*
                    跳回循环开始点
                    回填循环结束地址
                */
                gen(jmp, 0, cx1);
                code[cx2].a = cx;
            }
            else{
                /*
                缺少 do
                */
                error(37);
            }
        }
        else{
            /*
            缺少 to
            */
            error(0);
        }
    }
    

    2. repeat 循环

    (1)准备工作

     在全局变量symbol中添加 repeatsym、untilsym关键字,然后在initial()函数中的word1[]和 wsym[]数组再次添加,同样要注意按顺序添加。最后在initial()函数中的集合statbegsys{}中添加 repeatsym, 然后在statement()函数中增加对读到 repeatsym 的判断,当读到 repeatsym 就调用repeatstatement()函数。

    (2)代码思路

    Repeat循环例子:

    repeat
    begin
    ......
    end
    until ....;

    repeat 语句同 while 语句很类似,
    (1)读到repeat后,用 cx1 记下此时的 cx, 然后运行 statement() 函数执行语句块。
    (2)直到读到 until 时后执行 expression()函数判断bool表达式真假。
    (3)使用 jpc 指令,如果条件为假跳回 cx1 执行。条件为真时跳出 repeat 循环向下执行。

    2. 代码

    void repeatstatement(int &level, symset &fsys, int &cx1){
        /*
        跳回循环执行
        */
        cx1 = cx;
        item x;
        getsym();
    
        /*
            只需一条 statement 语句就可以,只有一条语句可以不加 begin end,
            多条语句必须加入 begin end
        */
        statement(fsys + set_of_enum(symbol)::of(semicolon, untilsym, eos), level);
        if (sym == untilsym){
            getsym();
            expression(fsys + set_of_enum(symbol)::of(semicolon, eos), x, level);
            if (x.typ != bool_)  error(34);
            gen(jpc, 0, cx1);
        }
        else{
            /*
            没有读到 until
            */
            error(0);
        }
    }
    

    3. case 判断

    (1)准备工作

      在全局变量symbol中添加 casesym、otherwisesym关键字,然后在initial()函数中的word1[]和 wsym[]数组再次添加,同样要注意按顺序添加。最后在initial()函数中的集合statbegsys{}中添加 casesym。然后在statement()函数中增加对读到 casesym 的判断,当读到 casesym 就调用casetstatement()函数。

    (2)代码思路

    case循环例子:

     case k of
         1 : b := 1;
         2 : b := 2;
     otherwise : b := 0 
     end;

    (1)首先记下 a + c 表达式计算前的地址 cx1 = cx,考虑到k有可能是表达式然后计算 k 的值,再记下执行后的地址 cx2 = cx
    (2)新建变量case_num = 0; 表示 case 语句情况的个数,每有一个 case 就执行 case_num++,
    (3)每遇到一个 case 语句都要进行 k 值与 case 值的判断,判断前把 code[]数组中从cx2至cx3 的代码复制过来,即每遇到一个 case 都会执行一遍 cx2 至 cx3 的代码,即计算 k 的值。
    (4)每个 case 语句执行完的地址都是相同的,都是用 jmp 跳出 case 语句,但是并不知道地址,用 address[100] 存放各 case 语句的 jmp 指令的地址,读到 end 后用 cx 回填 address[] 中存放的地址对应的 jmp 跳转地址。
    (5)读到 otherwise 说明case值没有匹配上,执行otherwise后面的程序,直接调用 statement()。
    (6)读到 end 表示结束。

    2. 代码

    void casestatement(int &level, symset &fsys, int &cx1, int &cx2) {
        /*
            因为 k 可能为表达式,
            所以 cx3 和 cx2 一同用于保存 k值 计算前后的地址
        */
        int cx3;
    
        getsym();
    
        /*
            记录 k 值计算前的地址
        */
        cx2 = cx;
        item  x;
    
        /*
            计算 k 值
        */
        expression(set_of_enum(symbol)::of(ofsym, eos) + fsys, x, level);
    
        /*
            记录 k 值计算后的地址
        */
        cx3 = cx;
    
        /*
            是保留字
        */
        if (sym != ofsym) {
            error(24);
        }
    
        /*
            记录 case 语句中情况的个数
        */
        int case_num = 0;
    
        /*
            记录各 case 语句中 jmp 跳出循环指令的地址,回填。
            初始化清零。
        */
        int address[100];
        for (int j = 0; j < 100; j++){
            address[j] = 0;
        }
    
        /*
            找到 k 的位置准备放入栈顶
        */
    
        do 
        {
    
            int temp = cx2;
            while (temp < cx3){
                code[cx] = code[temp];
                temp++;
                cx++;
            }
            /*
                读入 case 后面的表达式
                如果是 otherwise 跳出循环
            */
            getsym();
            if (sym == otherwisesym) {
                break;
            }
    
            /*
                计算表达式的值
            */
            simpleexpression(set_of_enum(symbol)::of(colon, eos) + fsys, x, level);
    
            /*
                error(24) y应为冒号
            */
            if (sym != colon){
                error(24);
            }
            getsym();
    
            /*
                比较二者, 相等时执行该对应的程序,
                不相等时到下一个 case 再判断。
            */
            gen(eq, 0, 0);
    
            /*
                记录 jpc 跳转指令地址,后面回填
            */
            cx1 = cx;
    
            /*
                不相等时跳转,地址未知。
            */
            gen(jpc, 0, 0);
    
            /*
                如果相等,执行后面的语句块
            */
            statement(fsys, level);
    
            /*
                记录 jmp 跳转指令的地址,回填
                执行完语句块跳出 case 语句
                jmp无条件跳转,但是地址未知。
                by yz_w
            */
            address[case_num++] = cx;
            gen(jmp, 0, 0);
    
            /*
                回填 jpc 挑战地址
            */
            code[cx1].a = cx;
        } 
        /*
            只要读到 分号 , 继续读下一个 case
            最后一条语句没有 分号
        */
        while (sym == semicolon);
    
        /*
            otherwise 语句执行
            如果有 otherwise
            再读到 : colon
            执行 otherwise 后面的语句块
        */
        if (sym == otherwisesym) {
            getsym();
            if (sym != colon) {
                error(24);
            }
            getsym();
            statement(fsys, level);
        }
    
        /*
            sym = end 表示 case 语句执行完毕
            end 后面有分号, 但不必读入分号作为判断。
            原因是分号是做为整个 case 块的结束标志,同时也是是否存在下一块的标识
            若有分号,说明 case 下面还有程序块,
            因此分号的判断不在这里,而在 casestatement() 返回后,详见 compound()函数
        */
        if (sym != endsym) {
            error(36);
        }
    
        for (int j = 0; j < case_num; j++)
            code[address[j]].a = cx;
        getsym();
    }
    

    4. goto 跳转

    (1)准备工作

      首先新建一些全局变量来实现 goto语句。

    //声明的 label的数目。 
    int labeltotal = 0;    
    
    //程序中调用 goto 的数目
    int gotototal = 0;    
    
    //存放 label 的标识, 最多存放 10 个,每声明一个label 就顺序在      
    //labelnum[] 中存放相应的 label 值。
    int labelnum[10];      
    
    //存放 label 跳转地址,假设声明 label:a 那么程序中 goto a; 的跳   
    //转地址为 labelcx[a]。在程序中中每出现一个 label 标识就记录它       
    //的当前地址赋值给 labelcx[]。goto 语句跳转时会查询 labelcx[]        
    //数组确认它的跳转地址。
    int labelcx[10000];
    
    // gotonum[i][0]存放 goto 语句对应的 label 值,gotonum[i][1]记    
    //录相应的需要跳转的地址,当然跳转地址不知道,必须要在所有代码        
    //扫描完毕后再回填跳转地址。
    int gotonum[10][2]
    
    //某个 label 可能声明了,但是并没有出现。如果 goto 跳转到那个      
    //label 应当报错,建立 labelbool[] 判断 label 是否在程序中出现,
    //出现为 1, 否为 0。
    int labelbool[10000];

      由于 pascal 语言中 goto 语句的label必须是四位整数,因此这里数组只开到 10000,对于label的数量也做了限制,最大为10个,相应数组也只开到了10。
      首先添加 labelsym 和 gotosym 到 symbal{} 中去,并在initial()函数中的declbegsys{}集合中添加labelsym,在statbegsys{}中添加 labelsym 、gotosym、intcon。因为程序中label 的出现是直接出现数字,如例子中的‘10:’,所以把 intcon 也要添加到 statbegsys{}中去。
      最重要的一点是,goto 语句出现时可能label语句并没有出现,像上面的例子一样,所以不知道跳转的地址,因此goto跳转时地址需要回填。
      num ,是原有程序中的一个全局变量,用于保存最近一次读入的数组。

    (2)代码思路

    goto循环例子:

    这里写图片描述
    (1)首先记下 a + c 表达式计算前的地址 cx1 = cx,考虑到k有可能是表达式然后计算 k 的值,再记下执行后的地址 cx2 = cx
    (2)新建变量case_num = 0; 表示 case 语句情况的个数,每有一个 case 就执行 case_num++,
    (3)每遇到一个 case 语句都要进行 k 值与 case 值的判断,判断前把 code[]数组中从cx2至cx3 的代码复制过来,即每遇到一个 case 都会执行一遍 cx2 至 cx3 的代码,即计算 k 的值。
    (4)每个 case 语句执行完的地址都是相同的,都是用 jmp 跳出 case 语句,但是并不知道地址,用 address[100] 存放各 case 语句的 jmp 指令的地址,读到 end 后用 cx 回填 address[] 中存放的地址对应的 jmp 跳转地址。
    (5)读到 otherwise 说明case值没有匹配上,执行otherwise后面的程序,直接调用 statement()。
    (6)读到 end 表示结束。

    2. 代码

    (1)变量初始化

    在 main() 函数中添加(block() 之前)
    这里写图片描述

    (2)在block()函数的 do_while循环中增加对labelsym的识别

    这里写图片描述

    这里写图片描述

    (3)在 Statement() 函数中增加对label的识别。

    这里写图片描述

    这里写图片描述

    这里写图片描述

    (4)在 Statement() 函数中增加对goto的识别。

    这里写图片描述

    这里写图片描述

    这里写图片描述

    (5)回填 jmp 指令地址

      因为回填必须在编译完所有代码后,所以在main()函数中调用完block()后开始回填
      这里写图片描述

    5. += 、-=

    (1)准备工作

      += 和-=实现上是完全类似的,本质上都是赋值语句,因此我们修改 assisnmen() 函数。
      首先新建关键字 addsym 标识 +=, subsym 标识 -=, 并添加到 symbal中,但这里不添加到 initial() 函数中去,因为 += 和 -= 不是单词符号,且是两个字符连在一起,所以不能添加到 word1[] 或者是 ssym[]中去。

    (2)代码思路

      对 += 和 -= 的识别应该在 getsym() 函数中,原来是读入+ ,sym =plusym,这里修改为读入 + 或 - 要判断后面的字符是否为 = 。因为我们还添加了 ++ 和 – 操作,同样在这里处理,我们一并修改。
      识别后赋值操作在assignmen() 函数中实现,添加对 addsym 和 subsym 的识别和相应的赋值处理。

    对于 a += b; 的处理思路为:
    (1)使用指令 loda 把该变量a的地址压入栈
    (2)使用指令 lod 把变量a的值压入栈
    (3)使用expression()函数计算 b 的值计算并压栈。
    (4)执行 add 指令把两值相加(减),即弹两个栈再压一个栈。
    (5)执行 sto 指令把得到的值存入变量地址中(赋值),即弹两个栈但不再压栈

    2. 代码

    (1)+= 和 -= 的识别,在getsym() 函数中找到识别 + 和 - 的位置

    这里写图片描述

    这里写图片描述

    (2)赋值操作,修改assignment()函数,增加对 addsym 和 subsym 的识别。

    +=运算
    这里写图片描述

    这里写图片描述

    -= 运算,把add指令换成sub指令就好
    这里写图片描述

    6. ++ 、–运算

    (1)准备工作

      在 symbal 中新建 ++ 关键字 addaddsym 和 – 关键字 subsubsym。同样也不对 initial() 做修改。

    (2)代码思路

      同样的,++ 和–是类似的,只讨论一种。
    ++ 运算分为两种,一种是作为表达式的因子, 一种则是作为单独的语句。比如:
    ++a * b / 3;
    a++ * b / 3;
    ++ 运算符作为因子

    ++a;
    a++;
    ++ 运算符作为完整表达式
      
      虽然计算方式一样,但是编译程序编译就完全不同了,单独表达式的识别应该在 statement() 函数中实现,而因子处理应该在 factor() 函数中实现。同样的 – 也是如此。
      再往下分类,++a 和 a++ 也是不同的,作为单独表达式时,a++ 的处理应该是先识别变量a, 然后在 assignment() 中实现加法; 而 ++a 先识别 ++ 在运行加法。 而因子处理时factor()函数对于 a++ 也是先识别变量a,然后计算,而++a 先识别 ++。
    所以对于 ++ 有四种不同的方式,– 也类似。

    2. 代码

    (一)++ 作为单独表达式在变量后,如 a++;

    修改 assingnment()函数,当读到 ++ 后执行,注意要返回。
    这里写图片描述
    – 类似
    这里写图片描述

    (二)++ 作为单独表达式在变量前,如 ++a;

    修改statement() 函数,当读入 ++ 执行。
    这里写图片描述
    – 类似
    这里写图片描述

    (三)++ 作为因子表达式在变量后,如 a++ * b + 3;

    修改 factor() 函数。factor()用于计算表达式中的因子,如 a++、b、3。
    在 factor() 中找到读入变量 a的代码 case ident:
    首先先注释一句:
    这里写图片描述
      在 case ident: 块中还有个 switch 用于判断是常量还是变量,++只使用变量,我们在 case variable:的块的最后添加代码:
      最重要的是,例子中计算的是 a + 1 前的值,按照一般顺序会计算成 a + 1 后的值,所以我们压入两个a值,正常计算后,还有一个a值未被弹出,因此计算时使用栈顶的值,是未 + 1前的a值。
      因为在 case variable:代码里已压入a值,这里再压入一个a值,后续加法赋值与前面相同。
    这里写图片描述
    – 类似, 因为 getsym() 在 上图 addaddsym 前已使用,这里不再调用
    这里写图片描述

    (四)++ 作为因子表达式在变量前,如 ++a * b + 3;

      同样在 factor() 函数中增加读到 ++ 后的操作,与上面的 case ident;是并列的。同样先判断类型等,但不同的是最后增加压栈操作,因为 sto 赋值指令不会把赋值后的值压栈,但是表达式的计算需要用到a + 1后的值,所以在压一次栈。下图中的后三行代码。
    这里写图片描述

    这里写图片描述
    – 类似
    这里写图片描述

    这里写图片描述

    7. 注释

    (1)准备工作

      注释跟 c++ 类似,{}用于单行注释,{….}用于多行注释,思路是直接修改 getsym() 函数,一旦getch() 读到{ } 就判断是不是注释功能。

    (2)代码实现

    在 getsym() 中添加:

    if (ch == '{'){
            getch();
            if (ch == '*'){
                while (1){
                    if (ch == '.'){
                        error(101);
                        break;
                    }
                    if (ch == '*'){
                        getch();
                        if (ch == '}'){
                            break;
                        }
                        continue;
                    }
                    getch();
                }
            }
            else{
                while (1)
                {
                    if (ch == '.'){
                        error(105);
                        break;
                    }
                    if (ch == ';'){
                        getch();
                        while (ch == ' '){
                            getch();
                        }
                        if (ch != '}'){
                            error(105);
                            break;
                        }
                        break;
                    }
                    getch();
                }
            }
            getch();
            getsym();
            }   
        }

    四、测试样例

    (1)for 循环

    这里写图片描述

    这里写图片描述

    (2)Repeat循环

    这里写图片描述

    这里写图片描述

    (3)Case 判断

    这里写图片描述

    这里写图片描述

    (4)Goto 跳转

    这里写图片描述

    这里写图片描述

    (5)+= 和 -=

    这里写图片描述

    这里写图片描述

    (6)++ 和 –

    单独表达式:
    这里写图片描述

    这里写图片描述
    ++ – 作为因子处理
    这里写图片描述

    这里写图片描述

    (7)注释

    这里写图片描述

    这里写图片描述

    转载请注明来自 b0t0w1’blog

    展开全文
  • pl0语言编译器的基础上实现了对布尔类型的扩展,能够识别并运行基本的pl0语言代码 test.txt文件为测试用pl0代码
  • PL/0编译器设计扩展

    万次阅读 多人点赞 2017-03-16 14:14:25
    PL/0扩展完成的功能主要有: 语句注释; 扩展条件及短路计算; else语句实现; for语句实现; exit及break语句实现; 多维数组实现; 输入输出语句实现。

    一.扩展完成功能

    1. 语句注释;
    2. 扩展条件及短路计算;
    3. else语句实现;
    4. for语句实现;
    5. exit及break语句实现;
    6. 多维数组实现;
    7. 输入输出语句实现。

    二.词法分析修改情况

    1. 原始PL0中的FIRST(S)集与FOLLOW(S)集情况如下表:
      1
      扩展之后做了以下修改:
      1)语句(statement)的FIRST(S)添加以下元素:for,break,exit
      2)条件(condition)的follow(S)添加了以下元素:not,or,and
    2. 对于保留字、符号、指令的添加如下:
      2
      1)保留字:
      对应的整形符号:
      3
      2)指令:
      4
      增加的各指令作用为:
      JPNC:满足条件则跳转;
      LDA :加载数组值到栈顶;
      STA :存储数组值到偏移地址上;
      RDA :读取数组指令;
      WTA :打印数组指令;
      READ:读取变量值;
      WRITE:打印变量值。
      5
      oprcode在原有13条指令的基础上,额外添加了5条:
      OPR_NOT:对栈顶值取非;
      OPR_WEN:打印空格指令;
      OPR_WRITE:打印栈顶常数指令;
      OPR_ADDPLUS: 进行+=运算;
      OPR_SUBPLUS: 进行-=运算。
      3)符号:
      6
      ‘&’,‘|’,‘!’其作用和and,or,not作用一样,可以用符号代替,而‘[’和‘]’用于数组的判断,‘%’用于求模运算。

    三.实现过程

    1. 注释
      在getsym( )函数中添加以下条件
      7
      当ch == ‘/’时如果,下一个字符是’*’,然后一直循环直到找到‘’*”,然后再取下一个字符‘/’。这样就跳过了/**/之间的所有字符,然后调用getsym( )取下一个符号
      8
      当连续出现两个//时,则令cc=ll,代表跳过该行的所有字符。然后继续getsym( )取下一个字符。

    2. 条件扩展
      添加了”or”,”and”,”not”关键字。
      同时添加了以下全局变量:
      9
      下面看条件扩展代码:
      通过重写ex_condition( )函数,调用condition( )函数。
      注意: condition( )的follow(S)需要添加 sym_or,sym_and,sym_not;
      10
      上图描述的是只有一个条件的情况,即没有and,or 的计算。
      注:如果第一个字符是not,则表示在生成条件指令之后,需要生成一个OPR_NOT指令,取反。
      然后生成真、假值的调转指令,等待回填。
      11
      12
      上面是分析了一个条件之后,遇到or的情况。遇到or之前,如果条件成立,则保持到真值链,生成JPNC指令,表示成立则调转。
      否则继续分析,如果出现not,则分析完一个条件后,产生OPR_NOT进行求反。然后继续分析下一个条件。分析完下一个条件判断是否是then,如果是then则说明条件分析完了。
      13
      上面是分析完一个条件,遇到AND的情况。如果遇到AND条件之前,条件已经不成立,则说明该条件不成立,即保存到假值链,生成JPC指令等到回填。然后继续分析下一个条件。
      分析完下一个条件判断是否是then,如果是then则说明条件分析完了。

    3. else语句实现
      说明:else字句是在if语句中实现的:
      14
      将condition改成扩展后的ex_condition:
      15
      下图是真值出口回填:
      16
      如果条件成立,则跳到执行语句前面; 然后生成一个JMP指令,因为当如果存在else,那么必须跳过else的执行语句。因此这个JMP是有必要的,下面是判断是否存在else,以及条件生成假值链应该跳转的地方。
      17
      分析完‘;’后,再取一个字符,则看下一个字符是否是“else”,
      如果是else,则假值链出口则是else执行语句的前面。
      如果不是else,说明没有else子句,则假出口即是当前的cx。那么问题来了,statement的follow(S)是‘;’,因此if语句出去之后应该是一个“;”,而这时已经取了“;”后面的一个符号,这样将会倒是缺少“;”的错误。 为了解决该问题,另外定义两个变量:
      18
      又在getsym( )中以下修改:
      19
      如果sym_stack有预知符号,则下一个字符是预知符号。这样则能保证符号顺序读取。

    4. for语句实现,仿照c语法
      语句的翻译方案如下:
      20
      语句的FIRST(S)修改如下:
      21
      代码如下:
      22
      23
      真出口跳到循环体前面,假出口跳到循环体后面。循环体执行完后需要跳到更新语句;
      而更新语句执行完后需要跳到条件判断语句。
      24
      上面最后涉及break语句,后面再说(有做修改)。

    5. break及exit语句的实现
      break实现:
      25
      break实现代码如下(在for和while语句的后面加上):
      26
      这样break语句产生的调转指令将会跳到循环的后面。
      exit实现:
      27
      直接生成OPR_RET指令即可。

    6. 数组的实现
      首先在pl.h头文件中,增加数组相关的参数,增加了四条和数组相关的指令,分别为:
      LDA :加载数组值到栈顶;
      STA :存储数组值到偏移地址上;
      RDA :读取数组指令;
      WTA :打印数组指令;
      增加了数组结构:
      28
      修改getsym函数,处理’[’和’]’符号:
      29
      如果遇到左中括号,表示是数组类型,否则为标识符:
      30
      继续读取剩下的右中括号。
      增加array_enter( )函数将ID_ARRAY类型填写入数组符号表中:
      31
      增加array_position()函数定位数组变量在数组符号表中的位置:
      32
      修改vardeclaration函数,增加对数组的处理:
      33
      在factor()函数中增加对数组的指令代码生成:
      34
      在term()函数和expression()函数的fsys集合中添加了数组相关的symtype值:
      35
      36
      在statement()函数中增加对数组的处理,表示语句的左值为数组变量:
      37
      38
      并生成STA指令存储数组到相应数组符号表中的位置。
      在interpret()函数中增加数组四条指令的功能:
      39

    7. 输入输出的实现
      在pl0.h中添加了oprcode的类型OPR_WEN和OPR_WRITE,这两个指令的作用是通过生成指令opr,0,14打印回车符,生成指令opr,0,15打印输出数字或者const常量;添加opcode指令READ和WRITE,前者用于读入普通变量值,后者用于打印输出普通变量值;而opcode中的RDA和WTA指令分别用于读入数组变量值和打印输出数组变量值。
      在statement函数中增加分句:
      40
      41
      42
      43
      解释中间代码部分,在interpret函数添加如下:
      44
      45

    8. +=、-=及%的实现
      仿照其他运算符,并在fsys符号集合中添加+=和-=符号。

    四.测试结果及分析

    1. 注释及条件扩展
      pl0测试代码:
      46
      执行结果:
      47
      分析生成代码:
      48
      分析:上面的指令代码分析中详细说明了短路计算的过程是语句的跳转情况。

    2. if_else语句
      Pl0测试代码:
      49
      结果:
      50

    3. for语句
      pl0测试代码:
      51
      结果:
      52

    4. 、break、exit语句实现
      break:
      53
      结果:
      54
      当i+j=4时break跳出计算下一层循环。
      exit:
      55
      结果:
      56

    5. 数组声明、赋值及输入输出实现
      Pl0测试代码:
      57
      结果:
      58
      读入数据数组变量1,2,3,4后:
      59

    6. 加等、减等及求余运算的实现
      Pl0测试代码:
      60
      结果:
      61

    五.思考

    本次设计中较好地对短路计算进行了合理实现,没有额外增加判断大小的指令,而是调用了原condition()函数,利用原来的oprcode指令;输入输出语句的实现,不仅可以输入输出一般变量,还可以处理数组的输入输出,const常量的输出,数字的输出,并在read()和print()括号内赋予多参数,当print()的括号中不加参数为输出回车符。更详细的内容参见源代码。

    展开全文
  • 编译原理课程设计 pl0功能的扩展
  • 编译原理课程设计,这个是课程设计功能扩展后的
  • 完成对PL/0语言的功能扩展: 1.增加else语句: <条件语句>::=if<条件>then<语句> [else<语句>] 2.增加for语句: FOR <循环变量>:=<初值> STEP <增量> UNTIL <终值> DO <语句>; <初值>是表达式 <增量>、<终值>可以...
  • 练习:使用 PEG.js 的 PL0 分析器 作者:亚历克西斯·洛佩斯、马科斯·路易斯、塞尔吉奥·冈萨雷斯·罗德里格斯 客观的 使用 PEG 为 PL/0 语言执行非预测递归解析器。 除了修改语法以接受 if-then-else 语句。 练习...
  • PL/0语言功能扩展[PASCAL VERSION] pascal 版本
  • 扩展PL0词法分析代码

    2011-12-09 15:40:23
    扩充PL0语法的词法分析程序和常量定义部分的语法分析,后续部分如有需要请给我发消息。 保证正确性。
  • 扩展PL/0编译程序功能

    2018-05-20 14:17:24
    该文档中包含了扩充的PL/0编译程序部分功能的设计思想与代码。
  • (1)计算机及操作系统:PC机,WindowsXP (2)程序设计语言:C++Builder6 (3)教学型编译程序:PL/0 (4)教学型编译程序的设计界面:
  • 按照pl0的语言定义编写,并扩展的数组功能
  • PL0语言功能扩展.(pascal 版) 具体参考http://online19910326.blog.163.com/blog/static/102723918201221125413620/
  • pl0文法解读文档

    2011-12-06 12:42:00
    pl0文法解读文档,pl0扩展文法,详细的对pl0文法进行解读,相信对您很有帮助
  • PL功能扩展for、repeat、case、+=、-=、++、--

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 44,326
精华内容 17,730
关键字:

pl0扩展