从零开始编译器-1

Posted by 翡翠小屋 on March 2, 2021

前言

有段时间对自己使用的编程语言( C++, Java, Javascript 之类)很不满,于是有了自己设计一个编程语言的想法,所以对如何写编译器进行了一些研究,断断续续也花了不少时间,工程 开源在 github 上:https://github.com/jadedrip/simpleLang ,可惜懒病入骨,编译器还出于比较早期的阶段,刚可以跑简单的代码,也一直想把自己学到的东西写下来,于是就有了这篇东西。

编译的基本步骤

自己设计一个编译器其实并没有想象中那么难,那是因为现在有许多相关的工具可以使用,特别是开源的神器 llvm ,它可以帮你解决绝大部分问题,所以首先要有自信,这事真不难。 其实编译一个文件的关键步骤并没多少: 1. 词法分析:把输入文件翻译为符号(Token) 2. 语法分析:把符号翻译为语法树 3. 生成本机代码或者解释执行 搞定一个编译器其实就这么点事,很简单吧~~

词法分析

什么是词法分析

​ 简单介绍一下词法分析是什么,如果懂编译原理可以跳过这段。

​ 输入的源文件可以视为一个字符串流,我们要对它进行分析,第一步就是进行词法分析,这其实是一个分类的过程,比如 123 是数字,归为数字 int 是类型名等等。 举个例子,比如有如下 c 语言代码:

int a = 1234;

​ c语言词法分析器接受字符流输入,按设定的词法分析规则,把他切分为几个词块(Token),当然,每个块除了类型,还可以附带额外数据:

匹配文本 分类 额外数据 规则
int 类型保留字 int 当字符流出现 int, float, short 后接分割符(空格等)时
a 单词 a 当出现字母 a~z A~Z 时
= 符号 = 当字符流出现 ++, +, -, = 时
1234 数字 1234 当出现连续数字后接 分割符(空格等)
+ 符号 +  
b 单词 b  
; 分隔符   当字符流出现 ; 时

​ ↑ 词法表1

规则是处理字符串的,处理字符串的规则我们一般使用正则表达式来表示,各个规则之间可能有重复,比如类型保留字和单词的规则可能同时匹配,我们可以设置优先级,比如匹配了类型保留字就是类型而不是单词了。还可以使用贪婪匹配的方法,以便当字符串里出现 ++ 时匹配为一个词,而不是被分别匹配为两个 +。 另外,匹配规则也可以是有状态的,比如当发现 “ (引号) 时,进入字符串匹配状态,一直到发现另一个 “ 退出,把中间的部分作为一个字符串块(Token)。

​ 所以如果你打算完全手写一个词法分析器,用有限状态机加正则表达式就可以轻松搞定(大雾),当然我们不会这么麻烦,我们会去找现成的轮子。

词法分析工具:flex

前面说过 llvm 是很好的工具,他有 clang 前端可以完成词法分析和后面的语法分析过程,不过由于历史原因,我没用他们,而选择了更古老的工具组合 flex + bison ,flex 就是词法分析器,这里简单介绍一下。

SimpleLang 的 flex 规则文件: https://gitee.com/jadedrip/simpleLang/blob/master/compiler/lex/silang.l

flex 工具会处理的这个规则文件,并生成一个解析用的 c 文件,你把这个生成的 c 文件放你的工程里,你的词法分析部分就搞定了,简单吧。

规则前面 %{ … %} 之间的内容是 c 代码,这个会放在生成的 c 代码前面。

然后可以设置一些配置,%% 分隔符之后就是正式的匹配规则了,后面还有个 %% 的分隔符,它后面放另一些额外 c 代码,会放在生成的 c 代码后面。

两个 %% 之间就是规则,规则类似这样:

[a-zA-Z]+                    printf("word: %s", yytext);

前面是一个正则表达式,后面是如果匹配了,会被执行的 c 代码。

我的 flex 规则文件里的代码类似这样:

0[lL]|(\-?[1-9]{D}*)[lL]	{ yylval.expr=makeValue(yytext,std::stoll(yytext));		return INT64_TOKEN; } 

其中 yytext 是内部变量,表示匹配的文本,yylval 也是一个内部变量,不过类型可以自己定义,我是通过 bison 里定义的,生成的 c 类型定义似这样:

union YYSTYPE
{
	AstType		*tp;	// AstType,AstNode 是我自己定义的类型
	AstNode		*expr;
	char*		str;
	int			type;
};

它可以用来保存上门说的额外信息,返回的是 TOKEN 类型, 是一个 int 整数,我同样在 bison 里定义的,自动生成了宏定义。我的代码里,后面这些是联合 bison 用的,我们后面再说。

然后,你就可以用 flex 工具生成一个 c/cpp 文件,把他加入自己的工程,你的工程就有了解析能力了

flex -o[输出文件] [输入文件]

调用解析也很简单:

extern int yyparse(void);
extern FILE *yyin;
...
yyin = fopen(argv[1], "r"); /* 首先打开要被处理的文件(参数1)yyin是lex默认的文件输入指针,设置了则不处理控制台输入 */
return 0 == yyparse(); 		/* 开始解析,返回0表示无错误 */

你看,我们轻松就搞定了词法分析,简单吧?(一个不成熟的小建议:真要写还是稍微再研究一下 flex 为好。

Next…