【前言】2008年Boston CGO上Open64 workshop,当时有个讨论很激烈的议题是前端选用gcc的哪个版本。当时Open64利用gcc的前端,引起的问题就是要不要跟随gcc演进以及如何演进。采用三方软件构建自研系统的时候这是一个永远无法回避的问题。在开启编译器项目的时,做一个自研前端的念头异常强烈,特别是随着经验的积累,对新的编程语言的理解以及实际产品业务的理解,促使我下了决心。但是,现实是一直到最近才有了真正的时间,特别是最近的Typescript转c++项目,它背后隐含的价值和长远意义引起了我极大的兴趣,促使整个进度提升,才有了今天的初步成果。-叶寒栋
Maple前端也叫MapleFE,是编程体系的重要环节,负责把多种语言的源代码解析生成语法树(AST),并进一步生成MapleIR或者用于其他用途。其在Maple编程体系中的位置如下图所示。Maple编程体系是一个完整的软件开发的全栈,包括编程语言、多语言前端、编译器、执行引擎以及相关工具链。整个体系依靠MapleIR贯穿前后。
在编译器前端这个领域,业界已经有很多成熟的产品,实现的方法也有多种不同,主要可以分为规则生成和手工实现。大家经常用的gcc,借助Flex, Bison等工具,基于规则描述自动生成词法分析器和LALR(1) 语法分析器。而Clang则是手工开发的。两种方法各有千秋。自动生成方案降低了开发的难度,却降低了代码可读性。手工开发提高了难度,但提高了代码可读性,非常便于对语法树进行各种分析与变换。
MapleFE综合了两种方法,基本思路为:
(1)用户根据特定的spec描述方法输入目标语言的语法规则;
(2)MapleFE的autogen工具基于语法规则生成c++语言的表格;
(3)MapleFE的parser通过traverse这些表格来match源程序。Parser的算法是手写的,而且本质上就是遍历不同表格的算法,所以,可读性非常高。另外,目标语言的语法描述也比较简单。
因此,总体来看MapleFE的可读性和易用性都是不错的。
MapleFE是基于LL Parsing算法。这类算法比较大的一个缺点是难以处理左递归(Left Recursion)的语法规则。业界有一个类似的产品叫Antlr,也是LL Parsing,也是基于规则。据了解,Antlr V4应该还是只能处理直接左递归(direct left recursion)的语法。所谓直接左递归指的是右边产生式的第一个元素就是该non-terminal自己,例如:
expr = expr * integer
而间接左递归(indirect left recursion)则是通过其他non-terminal产生递归,例如
expr = expo + string
expo = expr * integer
此处expr通过expo间接形成了左递归。
MapleFE的parsing实现了一种特殊算法,是基于recursion的表格遍历方法,可以解决所有左递归问题。为此,MapleFE特意设计和实现了recursion detector,后者可以识别并生成所有left recursion的数据库。
MapleFE的结构图如下,除了parser以外,还包括了几个重要的辅助算法,autogen, recursion detector, lookahead detector。
这里简单介绍各个模块的作用以及工作流程。
1. Autogen是一个c++表格自动生成器,把用户输入的语言spec生成c++ table并输出到c++源文件。在MapleFE中我们把一个产生式叫做rule,所以输出对应的c++表格我们叫rule table。
2. Recdetect负责查找语言spec中隐藏的左递归,并且把这些左递归以c++ table的形式输出到c++源文件。这些左递归信息是parser的主要依据。
3. Ladetect负责查找spec里面所有rule的look ahead,就是查找rule所有可能的第一个 terminal,并且也是以c++table的形式输出到c++源文件。Look ahead的主要目的是为了提高rule table遍历的速度,可以迅速排除不匹配的rule table,而不需要继续深入。
4. 上述三种表格都是c++表格,存在于源文件中。这些源文件配合parser的主要算法代码,链接在一起就构成了MapleFE的parser。Parser为了能够分析左递归的规则,把所有的规则通过产生式的关联,形成一个图(也可能是几个图),图中就包含了所有的loop,也就是左递归。Parser碰到一个loop的时候会进行迭代。每一次迭代,如果尝试成果,则可以得到一棵子树,并继续下一次迭代。每个迭代的分析会建立在前一次迭代的分析结果之上。直到迭代结束,就形成一个完整的树。
5. 语义检查和报错是另外一个大课题,它需要建立在复杂的语义分析基础之上,是特定语言相关的。目前我们并没有短期计划把它做完善。每个语言的设计者可以自行补充。
6. Ast2mpl和ast2cpp是两个示范用例,展示如何使用生成的语法树。Ast2mpl把生成的语法树分析并生成MapleIR,作为编译器的输入。Ast2cpp则是把生成的语法树生成c++源代码。目前,ast2cpp是我们正在工作的重点。
MapleFE当前社区的主要状态如下:
(1)parsing部分已经比较成熟,目前可以正常parse的语言包括Java和Typescript(含Javascript);
(2)ast2mpl是做了一个初步的原型,当前可以把Java程序的语法树生成MapleIR,但我们没有精力完善它,大家可以在社区代码中看到具体情况;
(3)最有意思的事情是现在我们重点做的Ast2cpp,当前正在做的事情是把Typescript程序的语法树生成c++程序,这里的工作包括非常多的内容,此处不一一列出。MapleFE作为前端,只是其中不大的一块内容。在这个项目中,很多非常有意义的题目引起我们的深思,尤其是对于编程语言的设计,多语言混合编程、跨语言函数调用等。这些问题我们在17年正式启动编译器项目的时候就开始思考,今天仍旧在理解类似的问题,积累相关的经验。
前端是一个编程体系的入口,它把程序员的代码翻译成语法树和相关数据结构,这个工作相当于给编程体系开了个大门,基于此可以做很多事情,生成编译器的IR只是很小的一部分,我相信它的用处更多的会用在程序的高级分析以及优化,验证,新语言设计以及解决跨语言的问题。
开发者可以参考java和typescript的实现做尝试,有问题欢迎在gitee中提交issue。
源代码链接:https://gitee.com/openarkcompiler-incubator/MapleFE