转自:图灵社区

原文作者:刘祺

原文地址:http://www.ituring.com.cn/article/215741

本次转载已经过原作者授权,二次转载请自行联系原作者

    

合集简介
市面上已经有很多关于编译原理的书了。无论是普通的程序爱好者、职业的计算机工程师还是***,自制编程语言一直被认为是最有趣的工作之一。即使自制编程语言的中途会遇到各种各样棘手的问题,但是这样一个工作绝对要比按照甲方和产品经理的要求,不断地修改一个无聊透顶的社交App要好玩儿得多。对于市面上的那些书一般都是需要生成语法分析器的工具Yacc,还有词法解析器Lex这两个老家伙。这样一方面就把笔者看来是最好玩的部分跳过了。而另一方面,笔者就难以向各位读者展示出原来我也能独立创造编程语言。此外这本合集是笔者为构思中的新书所写的样章,虽说工作计划安排了七天,但是笔者自己觉得七天之内写不完,但是对于读者来说估计一下午就看完了。另外需要注意的是,本合集是使用C语言进行描述的。分别发表在异步社区和图灵社区,未经许可请勿转载。

第1天 工作计划和开端

 

导读

在自制编程语言之前,我们先要了解我们要做的编程语言是什么样子的。在接下来的一周的时间里我们要一起自制一种类汇编语言。这种汇编语言可以直接控制我们构想的一种计算机上,我们姑且把这台想象出来的计算机叫做kiasm虚拟机。kiasm就是我专属的汇编语言的意思。读者可以根据自己的喜好给自己的虚拟机起名字。当然后续的程序中,笔者会以 kia_ 作为一部分变量名或函数名的前缀。这就是kiasm的前三个字母。


工作计划

在正式开始创造我们专属的虚拟机和汇编语言之前,我们需要制定一个完整的工作计划。一个好的工作计划不仅可以监督我们编程进度。也可以把复杂的问题简单化。对于第一次尝试制作虚拟机的读者来说,一下子就要做出来个虚拟机简直不知道应该从哪里开始。一个好的工作计划可以使编程变得有条理。按照笔者的习惯是先写好各个功能大体的框架,然后再逐一实现。这一点有点儿像是古代修建宫殿的过程——先搭建好骨架,再为这个程序“添砖加瓦”。那么下面就是笔者列出的工作计划:


时间工作内容
第1天制定工作计划虚拟机的开端
第2天栈和寄存器
第3天逻辑运算指令
第4天算数运算指令
第5天可执行文件
第6天控制指令
第7天压缩文件


搭建最初的骨架

要开发我们专属的虚拟机首先要解决的问题就是要选择一种编程语言作为我们的工作语言。市面上的书有很多是用C语言、Java或者是别的语言作为工作语言的,所以经常会听到有读者抱怨如果有用C#或者是Visual Basic作为工作语言的编译原理就好了。笔者在这里选择了无论是学习过什么编程语言都可以很快上手的C语言作为工作语言。一方面是为了向读者展示程序是如何运行起来的,如果使用Visual Basic那样非常方便的就能建立起来图形化界面的语言作为工作语言,在这一方面反而不方便了。另一方面,出于大家(包括笔者在内)都已经非常熟悉C语言了。

在选择C语言作为我们的工作语言之后,按照C语言的老规矩,我们可以写出以下代码:

#include<stdio.h>
#include<conio.h>
#include<windows.h>
int kia_end() {printf("Press any key to return.");getch();return 0;
}
int main(int argc, char *argv[]) {SetConsoleTitle("kiasm virtual machine");return kia_end();
}

接下来我们来解释一下这段源码。首先在主函数中有 SetConsoleTitle("kiasm virtual machine"); 这是一条Windows API,它的功能是改变命令行窗体的窗体名,原型在 windows.h 中声明。读者可以按照自己的喜好修改引号中的内容,例如:  SetConsoleTitle("命令行程序的窗体名");

之后是一条返回语句 return kia_end();  这是笔者喜欢使用的一个小技巧。对于C语言的初学者来说,可能  return 0;  的写法更为习惯。但是这样就会带来一个不便。因为在程序结束之前,我们往往希望让程序暂停一下以便我们观察程序运行的结果。也有时候在程序结束之前我们还需要释放一些我们调用过的资源。所以笔者的习惯是定义一个返回值为0的结束时执行的函数,把暂停的语句和需要释放资源的语句全部写到这个函数里面,这样带来的便利就是可以快速的区分哪里是主程序,哪里是程序结束之前的必要操作了。读者在之后的程序设计的过程中不妨试一试这个小窍门。

关于函数 int kia_end(); 自然也不必多说了。这是笔者之前提到的结束时执行的函数。目前只有输出按任意键返回。并且等待用户按键盘后返回0。不过需要注意的是这里使用了 printf(); 和 getch(); 这两个函数。它们分别在 stdio.h 和 conio.h 这两个头文件中。


设计显示样式

如果一种使用命令行程序默认的黑底白字未免太单调了。所以今天接下来的时间我们首先来处理一下输入输出时字符的颜色。这里有一点儿像是集成开发环境中的语法高亮,但又不完全一样。首先我们要声明两个全局变量 kia_fc 和 kia_bc 这两个变量分别控制命令行程序当前的前景色和背景色。它们都是无符号的短整型变量。并且在声明的时候为它们赋初值。其中代码前景色的 kia_fc 的值为7(白字),而代表背景色的 kia_bc 的值为0(黑底)。

unsigned short kia_fc=7;
unsigned short kia_bc=0;

读者可以参照下表的值和颜色对应的关系来选择背景色和前景色,这里给出的颜色是笔者观察得到了,对于颜色的名称并不是那么严谨。

颜色
0黑色
1深蓝色
2深绿色
3藏青色
4深红色
5耦合色
6土×××
7白色
8灰色
9蓝色
10绿色
11天蓝色
12红色
13粉色
14明×××
15亮白

显然得,命令行的颜色不可能随着我们改变变量的值就随之改变。所以当我们改变变量的值之后,需要调用一个函数对颜色的设置进行刷新。笔者定义了一个名为 void kia_refreshcolor()  的函数来刷新命令行的颜色。

void kia_refreshcolor() {HANDLE hCon=GetStdHandle(STD_OUTPUT_HANDLE);SetConsoleTextAttribute(hCon,(kia_fc%16)|(kia_bc%16*16));
}

如果想要理解这个程序就需要有一点儿Windows程序设计的知识。笔者在这里并不想一上来就给大家讲解冗长的句柄什么的。毕竟暂时来看我们用的并不多。我们暂时可以把它看成一个用来刷新控制台(命令行)颜色的黑箱。如果再讲得到细致一点,可以告诉大家 GetStdHandle(); 和SetConsoleTextAttribute(); 都是API函数。它们组合起来就可以被用于设置控制台(命令行)的颜色。

不过每次都要先改变全局变量的值,然后再调用 kia_refreshcolor(); 函数进行刷新好像也挺麻烦的。在笔者设计的这个虚拟机中几乎不会改变背景的颜色,所以笔者只写了用于改变前景色的函数 void kia_color(unsigned short c); ,它的原型如下。

void kia_color(unsigned short c) {kia_fc=c;kia_refreshcolor();
}


简化C的编程

如果想要输出一个带有高亮效果的字符串每次还都要先修改颜色,再输出字符串,这样实在是太麻烦。所以笔者就又写了两个简化的函数来做出语法高亮的效果。分别是 void kia_out(char *str); 和 void kia_outln(char *str); 。 void kia_out(char *str); 的作用是输出字符串后不换行,而 void kia_outln(char *str); 的作用是输出字符串并换行。不过暂时也没有什么特别多的高亮语法,我们暂时只支持把字符串 none 用白色显示的高亮规则。它们的原型如下:

void kia_out(char *str) {unsigned short kia_sfc=kia_fc;if(strcmp(str,"none")==0)kia_color(7);printf("%s",str);kia_color(kia_sfc);
}
void kia_outln(char *str) {unsigned short kia_sfc=kia_fc;if(strcmp(str,"none")==0)kia_color(7);printf("%s\n",str);kia_color(kia_sfc);
}


高亮的标签

接下来需要制作的就是需要加高亮显示的一些标签和标签后的提示内容。这些标签被称作是 Tag 。笔者计划了三个系统标签和一个用户自定义标签。三个系统标签分别是提示(tip)、报错(error)和警告(warning)。生成标签的函数是 void kia_tag(char *str); 。这里我们用绿色标记所有的提示信息,用红色标记所有的报错信息,用粉色标记所有的警告信息。至于用户自定义的标签我们用蓝色显示标签的名称,而标签后的细节则用×××显示。 void kia_tag(char *str);  的原型如下所示:

void kia_tag(char *str) {strlwr(str);if(strcmp(str,"tip")==0) {kia_color(10);kia_out("tip");} else if(strcmp(str,"error")==0) {kia_color(12);kia_out("error");} else if(strcmp(str,"warning")==0) {kia_color(13);kia_out("warning");} else {kia_color(11);kia_out(str);kia_color(14);}
}


整齐划一的输出

如果读者真的使用上面所列出的标签就会发现一个问题,输出的所有信息和标签都混在一起了。控制台(命令行)上面花花绿绿反而看不清楚了。所以我们需要对输出的格式进行调整。我们让标签占用控制台程序中的前10个字符的位置,细节信息从第11个字符开始输出。如果中途换行也不能占用前10个字符的位置。如果想要编写这样一个函数实际是很简单。但是笔者这里为了后续调整方便采用了一种啰嗦的写法。为了方便起见,调整格式的函数名笔者都是用  `kia_limout();` 代替,但是读者需要清楚的是,它们并不是同一个函数。

首先对于标签来说:

void kia_limout(char *str) {unsigned char i=0,len=strlen(str);if(len<=10) {kia_out(str);for(i=0; i<10; i++)kia_out(" ");} else {for(i=0; i<(10-3); i++)printf("%c",str[i]);kia_out("...");}
}

这个段函数的意思是如果标签的长度不超过十,则原样输出标签,不足的部分用空格补齐。如果标签的长度超过十,则输出标签的前七位字符,后三位用省略号代替。

接下来我们来处理后续的细节文本的输出:

void kia_limout(char *str) {unsigned char i=0,j=0,len=strlen(str);while((j*70+i)<len) {for(i=0; i<70; i++) {printf("%c",str[j*70+i]);if((j*70+i)>=len)break;}if((j*70+i)<len)for(i=0; i<10; i++)printf(" ");j++;}
}

这段用于处理细节文本输出的函数中定义了用于控制每行输出的字符的变量 i ,和用于控制输出的多少列的变量 j 。首先在一行内输出,如果没输出完一行字符串就结束了则停止输出。如果输出完一行还剩下一些字符没有被输出出来,那么则在下一行开头输出十个空格以错开标签的位置。然后再开始输入,如此反复,直到输出完成所有的字符。

如果想要把输出标签的函数和输出文本的函数合并到一起也非常简单只需要给 void kia_limout(); 增加一个参数,用于判断它是在输出标签还是在输出细节文本就可以了。

不过有的读者可能存在一个问题,为什么输出细节文本的代码中出现了一个数字 70 ,这是因为Windows控制台(命令行)程序默认一行是 80 个字符。那么标签占用了 10 个字符的位置。剩下的 70 字符全部留给细节文本就可以了。这里也就出现了一个问题,因为输出到每一行的最后一个字符的时候Windows控制台(命令行)程序会自动换行。所以直接输出十个空格就可以了,而不需要再输出一个换行符。

如果想要把每一行的字符数调整为少于70个的时候,则需要额外加上一个换行符。笔者在这里为 void kia_limout(); 函数增加了两个参数 lim 和 style 。 lim 用于控制输出文本的最大长度,而 style 用于控制输出文本的格式类型。这里 style 的值如果是 0 则认为是标签,如果不是零则认为在输出细节文本。对于不是零的情况也分成是正数的情况和负数的情况,如果是负数则在输出完一行之后不添加换行符,如果是正数则认为在输出完一行之后需要添加换行符。另外 style 的绝对值表示换行后需要预留的空格数量。所以经过整理后的 void kia_limout(); 则变成了:

void kia_limout(unsigned char lim,char *str,char style) {unsigned char i=0,j=0,len=strlen(str);if(len<=lim) {kia_out(str);for(i=0; i<(lim-len); i++)kia_out(" ");if(style!=0)kia_outln("");}if(len>lim) {if(style==0) {for(i=0; i<(lim-3); i++)printf("%c",str[i]);kia_out("...");} else {while((j*lim+i)<len) {for(i=0; i<lim; i++) {printf("%c",str[j*lim+i]);if((j*lim+i)>=len)break;}if(style==abs(style))printf("\n");if((j*lim+i)<len)for(i=0; i<abs(style); i++)printf(" ");j++;}if(style!=abs(style))printf("\n");}}
}

相应的,之前的 void kia_tag(char *str) 函数要使用新的输出方式进行输出,这样就做到了文本的格式化。

void kia_tag(char *str){strlwr(str);if(strcmp(str,"tip")==0){kia_color(10);kia_limout(10,"tip",0);}else if(strcmp(str,"error")==0){kia_color(12);kia_limout(10,"error",0);}else if(strcmp(str,"warning")==0){kia_color(13);kia_limout(10,"warning",0);}else{kia_color(11);kia_limout(10,str,0);kia_color(14);}
}

另外结束时的命令也要进行更新,笔者认为“按任意键返回”这句话是一种提示,所以就调用了提示标签。这里还使用了 kia_fc=7; kia_bc=0; kia_refreshcolor(); 这三条语句,使程序结束后控制台(命令行)的颜色恢复默认值。这是避免给后续运行的那些不需要进行语法高亮的程序带来不必要的麻烦。修改后的 kia_end() 函数如下:

int kia_end(){kia_tag("tip");kia_limout(69,"Press any key to return.",10);getch();kia_fc=7;kia_bc=0;kia_refreshcolor();return 0;
}

最后不要忘记在主函数里面调用我们设置的各个标签,另外在附加两个用户自定义的标签来测试我们编写的程序。

int main(int argc, char *argv[]){SetConsoleTitle("kiasm virtual machine");kia_tag("tip");kia_limout(69,"test-00000-test-11111-test-22222-test-33333-test-44444-test-55555-test-66666-test",10);kia_tag("error");kia_limout(69,"test-00000-test-11111-test-22222-test-33333-test",10);kia_tag("warning");kia_limout(69,"test-00000-test-11111-test-22222-test-33333-test",10);kia_tag("test");kia_limout(69,"test-00000-test-11111-test-22222-test-33333-test",10);kia_tag("testtesttest");kia_limout(69,"none",10);return kia_end();
}

wKioL1dBMY3Crex5AADpoIv6K2I706.png