自己动手编Basic解释器.docx
- 文档编号:9531929
- 上传时间:2023-05-19
- 格式:DOCX
- 页数:51
- 大小:36.47KB
自己动手编Basic解释器.docx
《自己动手编Basic解释器.docx》由会员分享,可在线阅读,更多相关《自己动手编Basic解释器.docx(51页珍藏版)》请在冰点文库上搜索。
自己动手编Basic解释器
那些客套话咱就不扯那么多了,啥写编译器是多少程序员梦想阿,多牛逼阿之类云云这之类的话咱就不聊了,主要是这里是C(不是java),C程序没java那么多套路,直接单刀赴会了。
好了既然我们是写basic解释器,那我们至少先要明白两件事情:
第一,什么是解释器;第二,basic语法这个至少要了解大概吧。
第一,什么是解释器?
编译器大家应该都知道,GCC,VC++(当然这个不纯粹是编译器了),简单的说呢,编译器将程序的源代码转化为可执行代码的形式。
通常情况下,这种可执行代码由计算机的CPU指令组成,因此可以直接在计算机上执行。
而解释器就不同了,它顺序读入程序的源代码,然后依次执行每一条语句。
因此,解释器并不真正将源代码转化为目标代码,而是直接执行程序。
第二,basic语法(以及我们为什么选择basic解释器作为编写目标)
先说第一个为什么写basic?
因为basic语法简单,你听名字也大概知道了,比如我不会选择advance作为编写目标,名字就比较吓人。
basic我们不来详细讲了,因为我们做得这个解释器本来就很小,没涉及到多少东东,有需要的可以google一下。
这里先贴一小段代码,混个脸熟:
PRINT"ASimpleSmallBASICProgram"'打印字符串
FORX=1TO10'for循环
GOSUB100'跳到标号100语句执行注意这里100是标号
NEXT'下一个x表示x要加一
END‘程序结束
100PRINTX'这里相当于C中函数
RETURN'返回
代码一目了然,我们解释器基本上也就处理这几个关键字。
现在摆在我们面前的就是如何处理这些关键字,换句话来说,我们如何在程序读取到相应关键字时,做出相应反应了。
比如我们执行print,我们就要用printf把print后面的东东打印出来。
这还比较好办,但是如果是一个for循环呢,我们又该如何处理?
关键字处理是个大麻烦,因为我们面对不同关键字,要做出不同反应来。
这个还是后话,眼前我们还是把主要框架先看看。
第一步装载源代码
用过gcc么?
郁闷gcc都没用过!
看来在windows下生活滋润惯了,学计算机还是要来opensource阵营阿,话扯远了。
我们用gcc编译c文件时,都是gcc-otargetsource。
所以我们也仿照这格式,直接用命令行(图形界面我们就不用实现了,再说俺也不懂)。
main(intargc,char*argv[])
{
char*p_buf;
char*t;
if(argc!
=2){
printf("usage:
%s
exit
(1);
}
/*allocatememoryfortheprogram*/
if(!
(p_buf=(char*)malloc(PROG_SIZE))){
printf("allocationfailure");
exit
(1);
}
/*loadtheprogramtoexecute*/
if(!
load_program(p_buf,argv[1]))exit
(1);
}
这里我们开辟一个PROG_SIZE大小空间,供装载程序源码用。
然后我们把程序代码用load_program函数装入内存。
代码如下:
/*Loadaprogram*/
load_program(char*p,char*fname)
{
FILE*fp;
inti=0;
if(!
(fp=fopen(fname,"rb")))return0;
i=0;
do{
*p=getc(fp);
p++;i++;
}while(!
feof(fp)&&i *(p-2)='\0';/*nullterminatetheprogram*/ fclose(fp); return1; } 代码很简单。 因为我们现在还没有开始处理语法,下面才是正式开始。 上次我们把程序装载入内存,这里我们开始做词法分析了。 hoho~开始 一开始我们先再来回顾下: 当我们的解释器在执行时,每次读入一条语句,并且根据这条语句执行特定的操作;然后再读入下一条语句,依此执行下去。 也就是说解释器执行时,每次从程序的源代码中读入一个标识符。 如果读入的是关键字,解释器就按照该关键字的要求执行规定的操作。 举例来说,当解释器读入一个PRINT后,它将打印PRINT之后的字符;当读入一个GOSUB时,它就执行指定的子程序。 在到达程序的结尾之前,这个过程将反复进行。 按照上面的分析,我们所做的第一步就是要读取标识符,要一个单词一个数字的区分,比如print要作为一个关键字读入,2345要作为一个数字读入,而a=3这里要作为三个标识符读入,分别是变量a、赋值符号=以及数值3。 看来我们在读取标识符时,我们还需要给标识符分类。 #defineDELIMITER1//分界符比如逗号分号等号都属于这之列 #defineVARIABLE2//变量 #defineNUMBER3//数字 #defineCOMMAND4//关键字(命令) #defineSTRING5//字符串(这个比较特殊既包括关键字又包括常量字符串) #defineQUOTE6//常量字符串比如"helloworld" 这里仔细谈分类收获不大,待看了get_token这个取标识符的函数之后我们就大致明白为什么要这样分了。 token主要目的就是读取当前的标识符,放入token字符数组中,并确定标识符类型,放入token_type中,如果是关键字,那么我们还需要处理tok,标记为相应关键字。 /*getatoken *从basic源码中读取一个符号(token)并且区分出符号类型 *我们把获取的符号放在token字符数组里面在token_type中设置符号类型在tok中设置 */ get_token() { registerchar*temp; token_type=0;//记录标识符的类型 tok=0;//记录关键字 temp=token; /**/ if(*prog=='\0'){/*文件结束*/ *token=0; tok=FINISHED;//设置文件结束符号 return(token_type=DELIMITER);/*标号类型设置为分界符*/ } while(iswhite(*prog))++prog;/*这里主要是除去字符串里面的空格*/ if(*prog=='\r') {/*换行符处理*/ ++prog;++prog;//这里跳过的字符"\r\n" tok=EOL;//设置一行结束的标识EOL(EndOfLine) *token='\r';token[1]='\n';token[2]=0; return(token_type=DELIMITER);//设置为分界符 } if(strchr("+-*^/%=;(),><",*prog))/*在"+-*^/%=;(),><"查找prog*/ { *temp=*prog;//把这个分界符拷贝到token中 prog++;/*advancetonextposition*/ temp++; *temp=0;//最后补上零这样让token形成字符串 return(token_type=DELIMITER); } /*这里'"'表明后面的是字符串basic中常常出现的情况是print"helloworld" *下面的例子就以此举例了 */ if(*prog=='"') { prog++;//prog指向了字符'h',注意这里已经进入了字符串 while(*prog! ='"'&&*prog! ='\r')*temp++=*prog++;//只要字符串没结束(prog遇到双引号),或者是没换行('\r')我们就要把原代码拷贝到token中 if(*prog=='\r')serror (1);//字符串不能换行 prog++;*temp=0;//prog已经走出字符串啦,照样的token要补上结束符 return(token_type=QUOTE);//token_type为字符串 } /*数字处理 *唯一要注意的是我们现在的数字是字符串形式的... */ if(isdigit(*prog)) { while(! isdelim(*prog))*temp++=*prog++;//如果没有分界符,拷贝之 *temp='\0'; return(token_type=NUMBER); } /*处理字符仍举例print"helloworld" *这里我们得到print *注意该代码块没有马上返回 */ if(isalpha(*prog)) { while(! isdelim(*prog))*temp++=*prog++;//没有分界符拷贝之 token_type=STRING;//string类型这只是一个中间类型具体要划分为变量、关键字你会想为什么不会是我们打印的常量字符串呢因为上面我们已经处理了 } *temp='\0';//不过这一句放在外面其实没多大意义前面的情况都return了,剩下一个孤零零因为接下来我们还要处理这玩意儿 /*查看究竟是个命令呢,还是一个变量,拭目以待...*/ if(token_type==STRING) { tok=look_up(token);/*在变量hash表中查之*/ if(! tok) token_type=VARIABLE;//变量 else token_type=COMMAND;//关键字 } returntoken_type; } get_token里面还有一些小函数,这里我们同样也罗列出来。 里面嵌套的函数我都注释了的,都比较好懂,注意点我都有解释 /*回滚*/ voidputback() { char*t; t=token; for(;*t;t++)prog--;// } /*这丫是帮我们在关键字table表里面搜索是否包含当前字符串*/ look_up(char*s) { registerinti,j; char*p; /*命令因为我们全都用小写字符这里不得不转换了*/ p=s; while(*p){*p=tolower(*p);p++;} /*顺序查找呃这个是个tiny所以效率不太考虑了 *想想如果我们关键字很多的话应该做一个hash表 *这里如果查找成功返回一个标号失败返回0请参阅table结构 */ for(i=0;*table[i].command;i++) if(! strcmp(table[i].command,s)) returntable[i].tok; return0; } /*查字符c是否是分界符 *恩有一点要注意比如我们的数字是909那么这里传进来的c的ASCII码可不是909应该是9+'0','0',9+'0'*/ isdelim(charc) { if(strchr(";,+-<>/*%^=()",c)||c==9||c=='\r'||c==0)//所以这里c==9表示'\t'0自然是'\0' return1; return0; } /*查是不是"空白",就是空格和tab键*/ iswhite(charc) { if(c==''||c=='\t')return1; elsereturn0; } 至此标识符处理我们全部完成,工作完成了一大块,剩下就是关键字处理了,里面涉及到语句逻辑。 经过get_token,我们可以获取一个标识符,按理说来下步可以进行程序逻辑处理了,但是标识符获取后我们离后面的真正程序逻辑运行还有一个大障碍,这就是表达式求解的问题。 比如说我们遇到一条语句PRINT3*(a+b),这里print倒是能识别是个关键字,我们按照程序逻辑应该打印后面的东东,是字符串就直接打印,是变量就取值打印,但是这里偏偏是一个表达式,我们需要事先计算出表达式的值。 接下来的问题就演变成了如何计算一个表达式,或者说写一个四则运算器。 关于如何写四则运算器,网上相关资料很多,源代码里的函数我没有解读,主要是看着不爽,逻辑性不够。 再者这个东西其实我大一自学数据结构的时候写过,主要思想是建立二叉树,然后类似一个中序遍历即可。 这里稍作解释,具体代码分析就不写了。 大家可以参照我原来的博客《四则运算》《重言式判别》。 我们策略是设置运算符权值,具体规则是设置初始权值为0,扫描整个表达式,遇到+-号权值加1,赋给当前运算符号,遇到乘除号,权值加2,赋值;遇到乘方号,权值加3,赋值;遇到左括号,权值加4,遇到右括号,权值减4。 比如 3+3*(4-2)初始权值赋值为0第一个加号,加上1,赋给加号。 第二个乘号权值加上2,权值为2(注意前面权值加一只是为了赋值,本身不变),赋给乘号。 后面遇到左括号,权值本身再加4,这时权值为4,但是这里不赋值,因为括号不参与运算,当然这只是我这里图方便,你说要赋值也不是不可,就是后面麻烦点儿。 马上又是减号,权值加1赋值,减号的权值变5,接着是右括号,权值减4。 所以整个下来有 3+3*(4-2)表达式 125权值 权值分析完毕,然后就是找出最小权值的一个符号,作为二叉树的根,权值越大的越往下生长,叶子节点全是数字。 然后递归遍历解决。 我原来那个程序只能计算表达式,但其实稍作改动就能计算变量了。 在此不再赘述。 下面贴出计算核心代码: doublecalculator(binarytree*root) { if(root) { if(root->left==NULL&&root->right==NULL) returnatof(&root->data); else { switch(root->data) { case'*': returncalculator(root->left)*calculator(root->right); break; case'/': returncalculator(root->left)/calculator(root->right); break; case'+': returncalculator(root->left)+calculator(root->right); break; case'-': returncalculator(root->left)-calculator(root->right); break; case'^': returnpow(calculator(root->left),calculator(root->right)); default: break; } } } } 经过表达式求解函数get_exp(value)处理后,我们就得到这个表达式的值了。 这样,万事具备,就等着程序的逻辑处理... 表达式已求,下面可以进入程序逻辑处理了,这里的代码量比较大,不过都很简单,后面主要是以程序注释为主。 先来看看完整版的主函数: main(intargc,char*argv[]) { charin[80]; intanswer; char*p_buf; char*t; if(argc! =2){ printf("usage: run exit (1); } /*allocatememoryfortheprogram*/ if(! (p_buf=(char*)malloc(PROG_SIZE))){ printf("allocationfailure"); exit (1); } /*loadtheprogramtoexecute*/ if(! load_program(p_buf,argv[1]))exit (1); if(setjmp(e_buf))exit (1);/*initializethelongjump*/ prog=p_buf; scan_labels();/*搜索所有的标签*/ ftos=0;/*初始化栈指针这个是为for循环作准备的*/ gtos=0;/*初始化栈指针这个是为gosub作准备的*/ do{ token_type=get_token(); /*如果当前是变量*/ if(token_type==VARIABLE){ putback();/*回退prog指针到变量前*/ assignment();/*赋值*/ } else/*除了变量那就是关键字了可能有同学会问呃那个比如一个数字怎么没考虑请想想一个数字怎么会单独出现*/ switch(tok){ casePRINT: print(); break; caseGOTO: exec_goto(); break; caseIF: exec_if(); break; caseFOR: exec_for(); break; caseNEXT: next(); break; caseINPUT: input(); break; caseGOSUB: gosub(); break; caseRETURN: greturn(); break; caseEND: exit(0); } }while(tok! =FINISHED); } main函数其实没啥好说的,主要是这个函数是个花架子,真正实在的逻辑处理不在这里。 不过特别需要强调的是prog这个字符指针,这是程序的命根子,它打到哪儿我们就得运行到哪儿,get_token就得取哪儿的标识符。 当然这种重要的东西肯定是掌握在我们自己手里。 另外是setjmp函数,这个可以理解为windows系统中的系统还原,一旦出错我们程序可以跳到这个还原点。 在while循环里,我们一行一行处理源代码,注意是一行一行的进行,比如printa,b,c我们会在print函数里面循环打印a,b,c。 而不会多次调用print,这种设计很巧妙。 来先看看变量赋值函数assignment: /*给变量赋值比如a=3 *注意这里为了简化起见,我们的变量就设置为26个字母 */ assignment() { intvar,value; /*getthevariablename*/ get_token(); if(! isalpha(*token))//因为变量我们用字母代替所以必定是字母类型 { serror(4); return; } var=toupper(*token)-'A';//转化为大写字母然后减去'A'这样让变量在hash表中有了座次比如A减去A为0这样A字符变量在变量hash表中第一个位置 /*gettheequalssign *这里我们取a=3中间的等号*/ get_token(); if(*token! ='=')//既然赋值么肯定有等号了 { serror(3); return; } /*a=3等号取走了我们来取数值*/ get_exp(&value); /*把我们取到的变量比如a值为3存放在hash表中*/ variables[var]=value; } 这里的变量我们用26个字母表示,比较简单,减少了我们代码逻辑判断,当然缺点就是变量不能见名知义,还有有时不够用,要知道我们这个basic没有所谓的全局变量和局部变量,都作为全局处理的。 这里面又嵌套了一个函数——serror,看名字就知道是错误处理的: /*displayanerrormessage*/ voidserror(interror) { char*e[]={ "syntaxerror", "unbalancedparentheses", "noexpressionpresent", "equalsignexpected", "notavariable", "labeltablefull", "duplicatelabel", "undefinedlabel", "THENexpected", "TOexpected", "toomanynestedFORloops", "NEXTwithoutFOR", "toomanynestedGOSUB", "RETURNwithoutGOSUB" }; printf("%s\n",e[error]); longjmp(e_buf,1);/*returntosavepoint*/ } 这个函数就是个打印,里面有个longjmp,就是上面所说的跳到"系统恢复点",与setjmp隔江相望。 具体可以查查 是所谓先来后到,我们还是按照主函数出现的顺序,依次分析逻辑函数。 1、print print函数主要的关注两点,第一点是打印格式,第二点是打印方式。 比如我举两例: printa print"helloworld"'要分清楚打印变量还是打印字符串这是打印方式的问题 printa,b printa;b'要分清楚打印格式,注意一个是分号一个是逗号两者有区别! print代码: /*executeasimpleversionoftheBASICPRINTstatement *执行打印这里我们还是举例说明*/ voidprint() { intanswer; intlen=0,spaces; charlast_delim; do{
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 自己动手 Basic 解释
![提示](https://static.bingdoc.com/images/bang_tan.gif)