前言
有段时间对自己使用的编程语言( 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 为好。)