文章目录

  • 一、实验介绍
    • 1. PL0文法说明
    • 2. PL0主函数
    • 3. 词法分析函数
    • 4. 语法语义分析
  • 二、功能扩展
    • 1. 功能扩充后的EBNF表示
    • 2. 功能扩充后的语法表述图
    • 3. 新增部分功能设计
      • 3.1 增加 I/O 格式化输入输出
      • 3.2 增加一维数组
      • 3.3 增加for循环
      • 3.4 增加行注释和块注释
      • 3.5 增加运算符
      • 3.6 增加if-then-else
  • 三、成果展示
    • 1. IED配置
    • 2. 斐波那契数列
    • 3. 鸡兔同笼问题
    • 4. 求取最大公因数
    • 5. 一维数组赋值、输入和输出
    • 6. for循环、++、--运算
    • 7. if-then-else结构实现
  • 四、实验思考和总结
  • 五、整体代码实现

内容有点长,代码量7000行+,创造不易,点赞关注加三连!!!有错误可指出,谢谢大咖~

一、实验介绍

1. PL0文法说明

PL0 编译程序总体结构如图所示,该程序的主要语法分析过程采用递归下降子程序法实现。为了将上下文有关文法转化为上下文无关文法,定义符号表进行管理,最终转化为虚拟机代码进行解释执行。PL0 语言编译程序采用以语法分析为核心、一遍扫描的编译方法。词法分析和代码生成作为独立的子程序供语法分析程序调用。语法分析的同时,提供了出错报告和出错恢复的功能。在源程序没有错误编译通过的情况下,调用类 P-code 解释程序解释执行生成的类 P-code代码。

PL/0 的文法分析部分是在编译源程序的 statement 部分,这部分的 EBNF 表示如下:

<语句>::=<赋值语句>|<条件语句>|<当型循环语句>|<过程调用语句>|<读语句>|<写语句>|<复合语句>|<  空>
<赋值语句>::=<标识符>:=<表达式>
<复合语句>::=BRGIN<语句>{<语句>}END
<条件>::=<表达式><关系运算符><表达式>|ODD<表达式>
<表达式>::=[+|-]<项>{<加法运算符><项>}
<项>::=<因子>{<乘法运算符><因子>}
<因子>::=<标识符>|<无符号整数>|‘(’<表达式>‘)’
<条件语句>::=IF<条件>THEN<语句>
<过程调用语句>::=CALL<标识符>
<当型循环语句>::=WHILE<条件>DO<语句>
<读语句>::=READ‘(’<标识符>{,<标识符>}‘)’
<写语句>::=WRITE‘(’<表达式>{,<表达式>}‘)’

2. PL0主函数

PL0主函数 main()执行流程下:首先打开一个文件,返回一个文件指针。如果可以成功打开的话,开始读取字符,如果不是文件结束符时,进入 block()函数,进行语法分析。遇到程序结束符时,统计程序中的语法错误,最后给出相应的提示,并关闭打开的文件,防止文件数据的丢失。

int main()
{bool nxtlev[symnum];printf("Input pl/0 file:   ");scanf("%s", fname);     /* 输入文件名 */fin = fopen(fname, "r");if (fin){fname[0]='y';listswitch = (fname[0]=='y' || fname[0]=='Y');fname[0]='y';tableswitch = (fname[0]=='y' || fname[0]=='Y');fa1 = fopen("fa1.tmp", "w");fprintf(fa1,"Input pl/0 file:   ");fprintf(fa1,"%s\n",fname);init();     /* 初始化 */err = 0;cc = cx = ll = 0;ch = ' ';if(-1 != getsym()){fa = fopen("fa.tmp", "w");fas = fopen("fas.tmp", "w");addset(nxtlev, declbegsys, statbegsys, symnum);nxtlev[period] = true;if(-1 == block(0, 0, nxtlev))   /* 调用编译程序 */{fclose(fa);fclose(fa1);fclose(fas);fclose(fin);printf("\n");return 0;}fclose(fa);fclose(fa1);fclose(fas);if (sym != period){error(9);}if (err == 0){fa2 = fopen("fa2.tmp", "w");interpret();    /* 调用解释执行程序 */fclose(fa2);}else{printf("Errors in pl/0 program");}}fclose(fin);}else{printf("Can't open file!\n");}printf("\n");return 0;
}

3. 词法分析函数

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

/*
* 词法分析,获取一个符号
*/
int getsym()
{int i,j,k;while (ch==' ' || ch==10 || ch==9)  /* 忽略空格、换行、回车和TAB */{getchdo;}if (ch>='a' && ch<='z'){           /* 名字或保留字以a..z开头 */k = 0;do {if(k<al){a[k] = ch;k++;}getchdo;} while (ch>='a' && ch<='z' || ch>='0' && ch<='9');a[k] = 0;strcpy(id, a);i = 0;j = norw-1;do {    /* 搜索当前符号是否为保留字 */k = (i+j)/2;if (strcmp(id,word[k]) <= 0){j = k - 1;}if (strcmp(id,word[k]) >= 0){i = k + 1;}} while (i <= j);if (i-1 > j){sym = wsym[k];}else{sym = ident; /* 搜索失败则,是名字或数字 */}}else{if (ch>='0' && ch<='9'){           /* 检测是否为数字:以0..9开头 */k = 0;num = 0;sym = number;do {num = 10*num + ch - '0';k++;getchdo;} while (ch>='0' && ch<='9'); /* 获取数字的值 */k--;if (k > nmax){error(30);}}else{if (ch == ':')      /* 检测赋值符号 */{getchdo;if (ch == '='){sym = becomes;getchdo;}else{//sym = nul;  /* 不能识别的符号 */sym=colon;//识别为冒号 }}else{if (ch == '<')      /* 检测小于或小于等于符号 */{getchdo;if (ch == '='){sym = leq;     /* 小于等于号 */getchdo;     /* 再读一个符号 */}else{sym = lss;     /* 检单独的小于号 */}}else{if (ch=='>')        /* 检测大于或大于等于符号 */{getchdo;if (ch == '='){sym = geq;     /* 大于等于号 */getchdo;}else{sym = gtr;     /* 单独的大于号 */}}else if(ch=='+')     /* 读到+号 */{getchdo;if(ch=='+'){sym=plusplus;  //++getchdo;}else if(ch=='='){sym=plusbk;  //+=getchdo;}                        else{sym=plus;}                                        } else if(ch=='-')     /* 读到-号 */{getchdo;if(ch=='-'){sym=minusminus;  //--getchdo;}else if(ch=='='){sym=minusbk;  //-=getchdo;}                        else{sym=minus;}                                       }else if(ch=='/')//添加注释 {getchdo;if(ch=='*'){getchdo;while(1){while(ch!='*')getchdo;getchdo;if(ch=='/')//注释/*...*/ break;}getchdo;getsym();}else if(ch=='/') //行注释// {cc=ll;ch=' ';getsym();}else{sym=slash;}}else{sym = ssym[ch];     /* 当符号不满足上述条件时,全部按照单字符符号处理 *///getchdo;//richardif (sym != period){getchdo;}//end richard}}}}}return 0;
}

4. 语法语义分析

(1)Block()函数主要用来对常量、变量和过程申明的处理,并进行填表工作。在判断了嵌套层数没有超过规定的层数后,开始分析源程序。首先判断是否遇到了常量声明,如果遇到则开始常量定义,把常量存入符号表。接下去用同样的方法分析变量声明,变量定义过程中会用 dx 变量记录下局部数据段分配的空间个数。然后如果遇到 procedure 保留字则进行过程声明和定义,声明的方法是把过程的名字和所在的层次记入符号表,过程定义的方法就是通过递归调用 block 过程,因为每个过程都是一个分程序。

/*
* 编译程序
*
* lev:    当前分程序所在层
* tx:     名字表当前尾指针
* fsys:   当前模块后跟符号集?
*/
int block(int lev, int tx, bool* fsys)
{int i;int dx;                 /* 名字分配到的相对地址 */int tx0;                /* 保留初始tx */int cx0;                /* 保留初始cx */bool nxtlev[symnum];    /* 在下级函数的参数中,符号集合均为值参,但由于使用数组实现,传递进来的是指针,为防止下级函数改变上级函数的集合,开辟新的空?传递给下级函数*/dx = 3;tx0 = tx;               /* 记录本层名字的初始位置 */table[tx].adr = cx;gendo(jmp, 0, 0);if (lev > levmax){error(32);}do {if (sym == constsym)    /* 收到常量声明符号,开始处理常量声明 */{getsymdo;do { constdeclarationdo(&tx, lev, &dx);  /* dx的值会被constdeclaration改变,使用指针 */while (sym == comma){getsymdo;constdeclarationdo(&tx, lev, &dx);}if (sym == semicolon){getsymdo;}else{error(5);   /*漏掉了逗号或者分号*/}} while (sym == ident); }if (sym == varsym)      /* 收到变量声明符号,开始处理变量声明 */{getsymdo;           /*调用getsym()的宏*/do {vardeclarationdo(&tx, lev, &dx);  /*添加符号表信息*/while (sym == comma)//, {getsymdo;vardeclarationdo(&tx, lev, &dx);}if (sym == semicolon)//;{getsymdo;}else{error(5);}}while (sym == ident);}while (sym == procsym) /* 收到过程声明符号,开始处理过程声明 */{getsymdo;if (sym == ident){enter(procedur, &tx, lev, &dx); /* 记录过程名字 */getsymdo;}else{error(4);   /* procedure后应为标识符 */}if (sym == semicolon){getsymdo;}else{error(5);   /* 漏掉了分号 */}memcpy(nxtlev, fsys, sizeof(bool)*symnum);nxtlev[semicolon] = true;if (-1 == block(lev+1, tx, nxtlev)){return -1;  /* 递归调用 */}if(sym == semicolon){getsymdo;memcpy(nxtlev, statbegsys, sizeof(bool)*symnum);nxtlev[ident] = true;nxtlev[procsym] = true;testdo(nxtlev, fsys, 6);}else{error(5);   /* 漏掉了分号 */}}memcpy(nxtlev, statbegsys, sizeof(bool)*symnum);nxtlev[ident] = true;nxtlev[period]=true;testdo(nxtlev, declbegsys, 7);} while (inset(sym, declbegsys));   /* 直到没有声明符号 */code[table[tx0].adr].a = cx;    /* 开始生成当前过程代码 */table[tx0].adr = cx;            /* 当前过程代码地址 */table[tx0].size = dx;           /* 声明部分中每增加一条声明都会给dx增加1,声明部分已经结束,dx就是当前过程数据的size */cx0 = cx;gendo(inte, 0, dx);             /* 生成分配内存代码 */if (tableswitch)        /* 输出名字表 */{printf("TABLE:\n");if (tx0+1 > tx){printf("    NULL\n");}for (i=tx0+1; i<=tx; i++){switch (table[i].kind){//名字表的输出 case constant:printf("    %d const %s ", i, table[i].name);printf("val=%d\n", table[i].val);fprintf(fas, "    %d const %s ", i, table[i].name);fprintf(fas, "val=%d\n", table[i].val);break;case variable:printf("    %d var   %s ", i, table[i].name);printf("lev=%d addr=%d\n", table[i].level, table[i].adr);fprintf(fas, "    %d var   %s ", i, table[i].name);fprintf(fas, "lev=%d addr=%d\n", table[i].level, table[i].adr);break;case procedur:printf("    %d proc  %s ", i, table[i].name);printf("lev=%d addr=%d size=%d\n", table[i].level, table[i].adr, table[i].size);fprintf(fas,"    %d proc  %s ", i, table[i].name);fprintf(fas,"lev=%d addr=%d size=%d\n", table[i].level, table[i].adr, table[i].size);break;case array://数组符号表输出 printf("%d arr %s ", i, table[i].name);printf("lev=%d addr=%d size=%d startid=%d\n", table[i].level, table[i].adr, table[i].size,table[i].startid);fprintf(fas, "%d array %s ", i, table[i].name);fprintf(fas, "lev=%d addr=%d size=%d startid=%d\n", table[i].level, table[i].adr, table[i].size,table[i].startid);break;}}printf("\n");}/* 语句后跟符号为分号或end */memcpy(nxtlev, fsys, sizeof(bool)*symnum);  /* 每个后跟符号集和都包含上层后跟符号集和,以便补救 */nxtlev[semicolon] = true;nxtlev[endsym] = true;statementdo(nxtlev, &tx, lev);gendo(opr, 0, 0);                       /* 每个过程出口都要使用的释放数据段指令 */memset(nxtlev, 0, sizeof(bool)*symnum); /*分程序没有补救集合 */testdo(fsys, nxtlev, 8);                /* 检测后跟符号正确性 */listcode(cx0);                          /* 输出代码 */return 0;
}

(2)Statement()函数主要用来进行基本语句的判断。判断文法为:
<语句>::=<赋值语句>|<条件语句>|<当型循环语句>|<过程调用语句>|<读语句>|<写语句>|<复合语句>|

/*
* 语句处理
*/
int statement(bool* fsys, int* ptx, int lev)//修改最多的函数
{int i, cx1, cx2,cx3,cx4,cx5;bool nxtlev[symnum];if (sym == ident)   /* 准备按照赋值语句处理 */{i = position(id, *ptx);if (i == 0){error(11);  /* 变量未找到 */}else{if(table[i].kind != variable&&(table[i].kind != array)){error(12);  /* 赋值语句格式错误 */i = 0;}else{enum fct fct1 = sto;getsymdo;if(sym == becomes){getsymdo;memcpy(nxtlev, fsys, sizeof(bool)*symnum);expressiondo(nxtlev, ptx, lev); /* 处理赋值符号右侧表达式 */if(i != 0){/* expression将执行一系列指令,但最终结果将会保存在栈顶,执行sto命令完成赋值 */gendo(sto, lev-table[i].level, table[i].adr);}}else if(sym==lparen)//数组 {getsymdo;memcpy(nxtlev, fsys, sizeof(bool)*symnum);expressiondo(nxtlev, ptx, lev); /* 处理赋值符号右侧表达式 */getsymdo;//一个;=getsymdo;//后面和var赋值相同,除了最后生成的语句 memcpy(nxtlev, fsys, sizeof(bool)*symnum);expressiondo(nxtlev, ptx, lev); /* 处理赋值符号右侧表达式 *//* expression将执行一系列指令,但最终结果将会保存在栈顶,执行sto命令完成赋值 */gendo(sto, lev-table[i].level, table[i].adr);}else if(sym==plusplus) // 后++运算 {getsymdo;gendo(lod,lev-table[i].level,table[i].adr);//找到变量地址,将其值入栈gendo(lit,0,1);//将常数1取到栈顶if(i!=0){gendo(opr,0,2);     //执行加操作gendo(sto,lev-table[i].level,table[i].adr);}}else if(sym==minusminus) // 后--运算 {getsymdo;gendo(lod,lev-table[i].level,table[i].adr);//找到变量地址,将其值入栈gendo(lit,0,1);//将常数1取到栈顶if(i!=0){gendo(opr,0,3);     //执行减操作gendo(sto,lev-table[i].level,table[i].adr);}}else if(sym==plusbk)        //增加运算符+={if(i!=0){gendo(lod,lev-table[i].level,table[i].adr);}getsymdo;memcpy(nxtlev,fsys,sizeof(bool)* symnum);nxtlev[number]=true;nxtlev[ident]=true;expressiondo(nxtlev,ptx,lev);gendo(opr,0,2);if(i!=0){gendo(fct1,lev-table[i].level,table[i].adr);}}else if(sym==minusbk)        //增加运算符-={if(i!=0){gendo(lod,lev-table[i].level,table[i].adr);}getsymdo;expressiondo(nxtlev,ptx,lev);gendo(opr,0,3);if(i!=0){gendo(fct1,lev-table[i].level,table[i].adr);}}else{error(13);  /* 没有检测到赋值符号 */}             }}//if (i == 0)}else if (sym == forsym)      //检测到for语句{getsymdo;if(sym != lparen)  error(34);//没有左括号出错else {getsymdo;statementdo(nxtlev, ptx, lev);  //S1代码if(sym != semicolon)  error(10); //语句缺少分号出错else{cx1=cx;getsymdo;conditiondo(nxtlev, ptx, lev);   //E代码if(sym != semicolon)  error(10);  //语句缺少分号出错else {    cx2=cx;gendo(jpc,0,0);cx3=cx;gendo(jmp,0,0);getsymdo;cx4=cx;statementdo(nxtlev, ptx, lev);   //S2代码if(sym != rparen)  error(22);  //缺少右括号出错else {gendo(jmp,0,cx1);getsymdo;cx5=cx;statementdo(nxtlev, ptx, lev);  //S3代码code[cx3].a=cx5;gendo(jmp,0,cx4);code[cx2].a=cx;}}}}                         }else{if (sym == readsym) /* 准备按照read语句处理 */{getsymdo;if (sym != lparen){error(34);  /* 格式错误,应是左括号 */}else{do {getsymdo;if(sym!=percent){error(33);//缺少% }int flag_d=0;getsymdo;if(sym==diosym){flag_d=1;} getsymdo;if(sym!=comma){error(33);//缺少, }getsymdo;                   if (sym == ident){i = position(id, *ptx); /* 查找要读的变量 */}else{i=0;}if (i == 0){error(35);  /* read()中应是声明过的变量名 */}else{if (table[i].kind == constant || table[i].kind == procedur){error(32);  /* read()参数表的标识符不是变量, thanks to amd */}else{if (table[i].kind == constant || table[i].kind == procedur) {error(32);     // read()中的标识符不是变量}else {getsymdo;if (sym != lparen) {         //非数组,即变量gendo(opr, 0, 16);gen(sto, lev - table[i].level, table[i].adr);}else {     //处理数组getsymdo;memcpy(nxtlev, fsys, sizeof(bool) * symnum);nxtlev[rparen] = true;expressiondo(nxtlev, ptx, lev);int ltmp = lev - table[i].level;int adrtmp = table[i].adr;gendo(arrcheck, table[i].startid, table[i].size);gendo(jpc, 0, 0);gendo(opr, 0, 16);gendo(sta, ltmp, adrtmp);getsymdo;}}}                      }
//                  getsymdo;又是一个坑,上面已经读取过了 } while (sym == comma); /* 一条read语句可读多个变量 */}if(sym != rparen){error(33);  /* 格式错误,应是右括号 */while (!inset(sym, fsys))   /* 出错补救,直到收到上层函数的后跟符号 */{getsymdo;}}else{getsymdo;}}else{if (sym == writesym)    /* 准备按照write语句处理,与read类似 */{getsymdo;if (sym == lparen){do {getsymdo;if(sym!=percent){error(33);//缺少% }int flag_d=0; getsymdo;if(sym==diosym){flag_d=1;}getsymdo;if(sym!=comma){error(33);//缺少, }getsymdo;if(sym==ident){i=position(id,*ptx);if(i==0){error(11);//未找到 }                           }memcpy(nxtlev, fsys, sizeof(bool)*symnum);nxtlev[rparen] = true;nxtlev[comma] = true;       /* write的后跟符号为) or , */expressiondo(nxtlev, ptx, lev); /* 调用表达式处理,此处与read不同,read为给变量赋值 */gendo(opr, 0, 14);  /* 生成输出指令,输出栈顶的值 */} while (sym == comma);if (sym != rparen){error(33);  /* write()中应为完整表达式 */}else{getsymdo;}}gendo(opr, 0, 15);  /* 输出换行 */}else{if (sym == callsym) /* 准备按照call语句处理 */{getsymdo;if (sym != ident){error(14);  /* call后应为标识符 */}else{i = position(id, *ptx);if (i == 0){error(11);  /* 过程未找到 */}else{if (table[i].kind == procedur){gendo(cal, lev-table[i].level, table[i].adr);   /* 生成call指令 */}else{error(15);  /* call后标识符应为过程 */}}getsymdo;}}else{if (sym == ifsym)   /* 准备按照if语句处理 */{getsymdo;memcpy(nxtlev, fsys, sizeof(bool) * symnum);nxtlev[thensym] = true;nxtlev[dosym] = true;conditiondo(nxtlev, ptx, lev);if (sym == thensym){getsymdo;}else{error(16);}cx1 = cx;gendo(jpc, 0, 0);statementdo(fsys, ptx, lev);if(sym == elsesym)//else语句增加 {getsymdo;cx2 = cx;gendo(jmp, 0, 0);code[cx1].a = cx;statementdo(fsys, ptx, lev);code[cx2].a = cx;}elsecode[cx1].a = cx;}else{if (sym == beginsym)    /* 准备按照复合语句处理 */{getsymdo;memcpy(nxtlev, fsys, sizeof(bool)*symnum);nxtlev[semicolon] = true;nxtlev[endsym] = true;  /* 后跟符号为分号或end *//* 循环调用语句处理函数,直到下一个符号不是语句开始符号或收到end */statementdo(nxtlev, ptx, lev);while (inset(sym, statbegsys) || sym==semicolon){if (sym == semicolon){getsymdo;}else{error(10);  /* 缺少分号 */}statementdo(nxtlev, ptx, lev);}if(sym == endsym){getsymdo;}else{error(17);  /* 缺少end或分号 */}}else{if (sym == whilesym)    /* 准备按照while语句处理 */{cx1 = cx;   /* 保存判断条件操作的位置 */getsymdo;memcpy(nxtlev, fsys, sizeof(bool)*symnum);nxtlev[dosym] = true;   /* 后跟符号为do */conditiondo(nxtlev, ptx, lev);  /* 调用条件处理 */cx2 = cx;   /* 保存循环体的结束的下一个位置 */gendo(jpc, 0, 0);   /* 生成条件跳转,但跳出循环的地址未知 */if (sym == dosym){getsymdo;}else{error(18);  /* 缺少do */}statementdo(fsys, ptx, lev);    /* 循环体 */gendo(jmp, 0, cx1); /* 回头重新判断条件 */code[cx2].a = cx;   /* 反填跳出循环的地址,与if类似 */}else{memset(nxtlev, 0, sizeof(bool)*symnum); /* 语句结束无补救集合 */testdo(fsys, nxtlev, 19);   /* 检测语句结束的正确性 */}}}}}}}return 0;
}

(3)Expression()函数用来进行表达式的处理,其中嵌套项处理,因子处理。文法如下:
<表达式>::=[+|-]<项>{<加法运算符><项>}
<项>::=<因子>{<乘法运算符><因子>}
<因子>::=<标识符>|<无符号整数>|‘(’<表达式>‘)’

/*
* 表达式处理
*/
int expression(bool* fsys, int* ptx, int lev)
{enum symbol addop;  /* 用于保存正负号 */bool nxtlev[symnum];if(sym==plus || sym==minus) /* 开头的正负号,此时当前表达式被看作一个正的或负的项 */{addop = sym;    /* 保存开头的正负号 */getsymdo;memcpy(nxtlev, fsys, sizeof(bool)*symnum);nxtlev[plus] = true;nxtlev[minus] = true;termdo(nxtlev, ptx, lev);   /* 处理项 */if (addop == minus){gendo(opr,0,1); /* 如果开头为负号生成取负指令 */}}else    /* 此时表达式被看作项的加减 */{memcpy(nxtlev, fsys, sizeof(bool)*symnum);nxtlev[plus] = true;nxtlev[minus] = true;termdo(nxtlev, ptx, lev);   /* 处理项 */}while (sym==plus || sym==minus){addop = sym;getsymdo;memcpy(nxtlev, fsys, sizeof(bool)*symnum);nxtlev[plus] = true;nxtlev[minus] = true;termdo(nxtlev, ptx, lev);   /* 处理项 */if (addop == plus){gendo(opr, 0, 2);   /* 生成加法指令 */}else{gendo(opr, 0, 3);   /* 生成减法指令 */}}return 0;
}
/*
* 项处理
*/
int term(bool* fsys, int* ptx, int lev)
{enum symbol mulop;  /* 用于保存乘除法符号 */bool nxtlev[symnum];memcpy(nxtlev, fsys, sizeof(bool)*symnum);nxtlev[times] = true;nxtlev[slash] = true;factordo(nxtlev, ptx, lev); /* 处理因子 */while(sym==times || sym==slash){mulop = sym;getsymdo;factordo(nxtlev, ptx, lev);if(mulop == times){gendo(opr, 0, 4);   /* 生成乘法指令 */}else{gendo(opr, 0, 5);   /* 生成除法指令 */}}return 0;
}
/*
* 因子处理
*/
int factor(bool* fsys, int* ptx, int lev)
{int i;bool nxtlev[symnum];testdo(facbegsys, fsys, 24);    /* 检测因子的开始符号 *//* while(inset(sym, facbegsys)) */  /* 循环直到不是因子开始符号 */if(inset(sym,facbegsys))    /* BUG: 原来的方法var1(var2+var3)会被错误识别为因子 */{if(sym == ident)    /* 因子为常量或变量 */{i = position(id, *ptx); /* 查找名字 */if (i == 0){error(11);  /* 标识符未声明 */}else{switch (table[i].kind){case constant:  /* 名字为常量 */gendo(lit, 0, table[i].val);    /* 直接把常量的值入栈 */break;case variable:  /* 名字为变量 */gendo(lod, lev-table[i].level, table[i].adr);   /* 找到变量地址并将其值入栈 */break;case procedur:  /* 名字为过程 */error(21);  /* 不能为过程 */break;case array:getsymdo;if (sym == lparen){                   //是数组int ltmp = lev - table[i].level;int adrtmp = table[i].adr;getsymdo;memcpy(nxtlev, fsys, sizeof(bool) * symnum);nxtlev[rparen] = true;expressiondo(nxtlev, ptx, lev);gendo(lda, ltmp, adrtmp);//getsymdo;                            }if (sym == rparen){//getsymdo;}break;}}getsymdo;}else{if(sym == number)   /* 因子为数 */{if (num > amax){error(31);num = 0;}gendo(lit, 0, num);getsymdo;}else{if (sym == lparen)  /* 因子为表达式 */{getsymdo;memcpy(nxtlev, fsys, sizeof(bool)*symnum);nxtlev[rparen] = true;expressiondo(nxtlev, ptx, lev);if (sym == rparen){getsymdo;}else{error(22);  /* 缺少右括号 */}}testdo(fsys, facbegsys, 23);    /* 因子后有非法符号 */}}}return 0;
}

二、功能扩展

本综合实验根据已经掌握的编译原理和技术,在读懂读通 “PL/0 编译程序”的基础上,根据要求修改编译源程序,在符合 PL/0 语言基本词法、语法规则的前提下,对 PL/0 语言的功能进行扩充。

同时,完成了基于 EditPlus 的 PL0 语言语言集成开发环境配置,作为 PL0 语言的 IDE 使用,实现了代码折叠、代码缩进、自动补全、语法高亮、显示行号等功能。

其中包含了以下的功能设计:

  1. I/O 功能扩展:增加了格式化输入、格式化输出。
  2. 数据结构的扩展:增加了一维数组,包括申明、赋值、读写等基本功能。
  3. 计算功能的扩展:增加了后置++、后置–、+=、-=运算符号。
  4. 控制逻辑上的扩展:增加 for 循环、增加 if-then-else 结构。
  5. 语句注释和代码块的注释。
  6. 完成基于 EditPlus 的 PL/0 预演集成开发环境配置,实现代码折叠、代码缩进、自动补全、语法高亮、显示行号等功能。

1. 功能扩充后的EBNF表示

这里只展示扩充部分:

<一维数组说明部分>::=VAR<标识符>‘(’<上界>:<下界>‘)’
<上界>::=<常量>|<变量>|<表达式>
<下界>::=<常量>|<变量>|<表达式>
<行注释>::=//<语句>
<块注释>::=/*<语句>{<语句>}*/
<复合赋值运算符>::=-=|+=
<后置自增运算符>::=++
<后置自减运算符>::=--
<for 型循环语句>::=for‘(’<赋值语句>;<条件>;<赋值语句>‘)’
<读语句>::=READ‘(’%d{%d}<标识符>{,<标识符>}‘)’
<写语句>::=WRITE‘(’%d{%d}<表达式>{,<表达式>}‘)’

2. 功能扩充后的语法表述图

语法分析过程依赖关系:

分程序语法描述图分别描述了常量申明,变量申明,过程申明和一维数组申明。如图可知,不能将常量定义写在变量定义前面:
)
语句语法描述图中分别对赋值语句,call 语句,begin 语句,if 语句,else 语句,while 语句,read 语句,write 语句,for 循环语句等进行了描述。(关于 for 语句的描述有点不太确定,循环时的箭头应该从括号两边,还是括号内部开始?这里采用括号内部的方案)

3. 新增部分功能设计

符号表更新:

/* 符号 */
enum symbol {nul,         ident,     number,     plus,      minus,//0-4times,       slash,     oddsym,     eql,       neq,  //5-9lss,         leq,       gtr,        geq,       lparen,//10-14rparen,      comma,     semicolon,  period,    becomes,//15-19beginsym,    endsym,    ifsym,      thensym,   whilesym,//20-24writesym,    readsym,   dosym,      callsym,   constsym,//25-29varsym,      procsym,   percent,    colon,     diosym,  //30-34elsesym,     forsym,    plusplus,   minusminus,plusbk, //35-39minusbk,                                               //40
};

Percent(%):用来进行格式化输出输出。
Colon(:):用来实现一维数组的申明,位于上界和下界之间作为分隔符。 Diosym(d):用来进行格式化输出输出。
Elsesym(else):增加 if-then-else 控制逻辑。Forsym(for):增加 for 循环。
Plusplus(++):增加复合运算符后置++。Minusminus(–):增加复合运算符后置–。Plusbk(+=);增加复合运算+=。
Minusbk(-=):增加复合赋值运算-=。
Slash(/):原有除法运算符,用来实现单行注释和块注释。Times(*):原有的乘法运算符,用来实现多行的块注释。

保留字更新:

/* 设置保留字名字,按照字母顺序,便于折半查找 */strcpy(&(word[0][0]), "begin");strcpy(&(word[1][0]), "call");strcpy(&(word[2][0]), "const");strcpy(&(word[3][0]), "d");//addstrcpy(&(word[4][0]), "do");//addstrcpy(&(word[5][0]), "else");//addstrcpy(&(word[6][0]), "end");strcpy(&(word[7][0]), "for");//addstrcpy(&(word[8][0]), "if");strcpy(&(word[9][0]), "odd");strcpy(&(word[10][0]), "procedure");strcpy(&(word[11][0]), "read");strcpy(&(word[12][0]), "then");strcpy(&(word[13][0]), "var");strcpy(&(word[14][0]), "while");strcpy(&(word[15][0]), "write");/* 设置保留字符号 */wsym[0] = beginsym;wsym[1] = callsym;wsym[2] = constsym;wsym[3] = diosym;//addwsym[4] = dosym;//add  wsym[5] = elsesym;//addwsym[6] = endsym;wsym[7] = forsym;//addwsym[8] = ifsym;wsym[9] = oddsym;wsym[10] = procsym;wsym[11] = readsym;wsym[12] = thensym;wsym[13] = varsym;wsym[14] = whilesym;wsym[15] = writesym;

3.1 增加 I/O 格式化输入输出

设计思路

采用“%d”对输入和输出进行格式化,在 statement() 函数中,当判断该语句为读(写) 语句时,读入一个字符,判断 sym 是否为 lparen,若非,则报错。否则,读入一个字符,判断 sym 是否为percent,若非,报错。否则,读入一个字符,判断 sym 是否为 diosym,再读入一个字符,判断 sym 是否 为 comma,若非,报错。否则,读入一个字符,判断 sym 是否为 ident,后面的处理方式与原程序相同。

其语法如下:

<读语句>::=READ‘(’%d{%d}<标识符>{,<标识符>}‘)’
<写语句>::=WRITE‘(’%d{%d}<表达式>{,<表达式>}‘)’

3.2 增加一维数组

设计思路:

考虑读入数组,假如我们有定义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 的地址是从栈顶拿到的,这样我们就解决了找到数组元素并赋值的过程。

其语法如下:

<一维数组说明部分>::=VAR<标识符>‘(’<上界>:<下界>‘)’
<上界>::=<常量>|<变量>|<表达式>
<下界>::=<常量>|<变量>|<表达式>

3.3 增加for循环

设计思路:

首先获取 for,然后获取 lparen,读入一个符号,调用语句处理过程算得赋值号右边的值并通过相应的指令将此值放到数据栈顶。接着获取分号,然后用 condition 过程处理条件语句,把当前 condition 生成的判断条件代码地址 cx 保存到 cx1 中。接着再读取一个分号,调用表达式处理过程算得循环变量的值并判断是否退出循环,把本次循环结束的下一个位置保存到 cx2 生成跳转,并在循环结束时用 cx2 更新为目前循环结束跳转地址。

其语法如下:

<for 型循环语句>::=for‘(’<赋值语句>;<条件>;<赋值语句>‘)’

3.4 增加行注释和块注释

设计思路:

判断当前读取字符为‘/’时,继续读取,若为*,则一直读取,不进行处理,直到遇见*,结束循环,再读取一个字符(‘/’),判断为块注释。否则若为‘/’,则判断为行注释。两者都不满足,判断该符号为 slash。

其语法如下:

<行注释>::=//<语句>
<块注释>::=/<语句>{<语句>}/

3.5 增加运算符

设计思路:

判断当前符号为‘+’时,继续获取字符,若为‘+’,则 sym=plusplus,若为‘=’,则 sym=plusbk,否则 sym 为 plus。
判断当前符号为‘-’时,继续获取字符,若为‘-’,则 sym=minusminus,若为‘=’,则 sym=minusbk,否则 sym 为 minus。
然后在 statement 函数里根据 sym 值的不同,进行对应的操作,实现运算功能。

其语法如下:

<复合赋值运算符>::=-=|+=
<后置自增运算符>::=++
<后置自减运算符>::=–

3.6 增加if-then-else

设计思路:

首先对读到的词调用 getsym 进行词法分析,把当前的代码地址 cx 保存在 cx2 中,调用 gen(jmp,0,0) 生成条件跳转,地址未知。将当前代码的地址加 1 赋给 cx,同时把 cx 的地址保存在 cx2 中。

其语法如下:(其中 else 部分可有可无)

<条件语句>::=IF<条件>THEN<语句>[ELSE<语句>]

三、成果展示

1. IED配置

以下测试均在已经配置好的EditPlus 上测试,同时也可以看到代码高亮显示,至于自动补全应用于写代码的过程中,已通过测试。

2. 斐波那契数列

同时也可用来做格式化输入和输出测试。

3. 鸡兔同笼问题

输入头和脚数,输出兔子和鸡的个数。可用来测试格式化输入和输出

4. 求取最大公因数

输入 2 个数,求最大公因数。可同时用来测试格式化输入和输出,以及代码注释

5. 一维数组赋值、输入和输出

通过下标访问数组的某个元素并进行赋值,读入某个数组元素的值,并原貌输出。(键盘输入的是 34)

6. for循环、++、–运算

递增输出 1,2,3,4,5;
递减输出 100,99,98,97,96;

7. if-then-else结构实现

四、实验思考和总结

保留字名字word[][]和保留字符号 wsym[]下标设置要对应同一个符号,不然会出现各种位置错误! 如图为正确设置方式:(按照数序排列,便于进行折半查找,加快查找速率)

对于中文注释的显示问题,EditPlus 保存文件编码为 ANSI。保存为 UTF-8 会显示乱码。

五、整体代码实现

基于C++实现PL0的编译
1700+行代码,包括运行代码所需要的PL0文件,编写不易,多多支持!!!

基于C++的PL0语言编译器及功能扩充相关推荐

  1. 编译原理大作业-PL0语言编译器

    编译原理大作业-PL0语言编译器 一.实验目的 二.源码说明 1.头文件pl0.h (1 词法分析主要数据结构(通过enum symbol类实现) 1.保留字(13+4个): 2.运算符及界符(16+ ...

  2. 编译原理实验报告一:PL0语言编译器分析(PL0,词法分析,语法分析,中间代码生成)

    实验报告一:PL0语言编译器分析 一.实验目的 通过阅读与解析一个实际编译器(PL/0语言编译器)的源代码, 加深对编译阶段(包括词法分析.语法分析.语义分析.中间代码生成等)和编译系统软件结构的理解 ...

  3. 小型c语言编译,GitHub - ming311/Compiler: 小型C语言编译器

    基于java的C语言编译器 采用LL(1)文法进行分析,文法是C语言文法的子集,支持报错,以及错误原因 以前只用编译器编译程序,现在学完编译原理这门课以后,通过编译大作业,我对编译器的工作原理有了比较 ...

  4. 安卓下的c语言ide,C语言编译器IDEapp-C语言编译器IDE安卓版下载v2.4.0-飞飞世界

    C语言编译器IDE是一款学习软件.这款软件能够帮助我们更好的学习C语言,里面有很多的相关的学习资源.这款软件你可以直接在这里创建C语言项目,非常的方便.创建以后就可以直接运行,直接就能看出写得是否争取 ...

  5. 安卓下的c语言ide,C语言编译器IDE安卓版下载-C语言编译器IDE下载v1.7 最新版-腾牛安卓网...

    C语言编译器IDE安卓版下载,一款专业实用的C语言编程工具,C语言编译器IDE帮助更多小伙伴们创建C语言项目,进行高效便捷的代码编辑,有需要就来下载. C语言编译器IDE介绍 C语言编译器IDE,是一 ...

  6. 《C语言编程魔法书:基于C11标准》——1.3 主流C语言编译器介绍

    本节书摘来自华章计算机<C语言编程魔法书:基于C11标准>一书中的第1章,第1.3节,作者: 陈轶 更多章节内容可以访问云栖社区"华章计算机"公众号查看. 1.3 主流 ...

  7. 基于web的c语言在线编译器的设计步骤,基于WEB的C语言在线学习系统毕业设计(全)..doc...

    PAGE 学科分类号 0806 本科生毕业设计 题目(中文):基于WEB的C语言在线学习系统的实现 (英文) The Implementation of an Online Learning Syst ...

  8. 基于web的c语言在线编译器的设计步骤,定稿毕业论文基于WEB的C语言在线学习系统毕业设计完整版(范文1)...

    <[毕业论文]基于WEB的C语言在线学习系统毕业设计.doc>由会员分享,可免费在线阅读全文,更多与<(定稿)毕业论文基于WEB的C语言在线学习系统毕业设计(完整版)>相关文档 ...

  9. 单片机c语言编译软件6,eUIDE下载-单片机c语言编译器 v1.07.32.23 官方版 - 安下载...

    eUIDE是一款专业的单片机c语言编译器,EM78系列集成开发环境是面向项目的ELAN EM78系列微控制器的开发工具,它包括UICE开发在线仿真器和eUIDE软件工具:eUIDE是基于PC端的UIC ...

最新文章

  1. python sys.argv是什么?
  2. mysql 基础篇(二) 账号、权限管理
  3. 微信小程序 遇到问题1
  4. 技术她力量,鹅厂女博士的寻“豹”之旅
  5. 【硬盘整理】使用UltimateDefrag将常用文件放置在磁盘最外圈
  6. Jenkins持续集成——用户管理
  7. ROOBO公布A轮1亿美元融资 发布人工智能机器人系统
  8. 计算机网络—体系结构相关真题练习(二)
  9. VS2015 经常不出现智能提示,代码颜色也没有了
  10. Excel闪退问题解决办法
  11. matlab由滤波的系数得到传输函数 设计带通滤波器 design fdatool设计IIR带通滤波器
  12. Docker——网络
  13. open erp java_OpenERP API 接口
  14. PHP-SDK实现微信付款码支付
  15. Goldfish 介绍
  16. 网络号和主机号具体计算原理-ipv4篇
  17. 守护线程------setDaemon(true)设置成守护线程
  18. 一文看尽2020上半年阿里、腾讯、百度入选AI顶会论文
  19. 为什么要清除浮动?清除浮动的方式?
  20. 通过Opencv打开指定摄像头的方法

热门文章

  1. 计算机软件要学哪些课程,计算机软件专业主要学习哪些课程?
  2. shader学习之路——更复杂的光照之Base Pass 和 Additional Pass和光照的衰减
  3. 以太网通信时4芯网线和8芯网线的区别(百兆网络 OR 千兆网络)
  4. Springboot, link failure、SSL peer shut down incorrectly、Unsupported record version Unknown-0.0问题
  5. 主流部署端深度学习框架
  6. 推荐几款好用的VsCode色彩主题
  7. SpringCloud分布式微服务搭建(一)
  8. 科学计算机怎么玩游戏,计算机也能玩文本游戏
  9. DIRECTPLAY
  10. 树莓派的应用--人体感应器HC-SR501