Alioth 项目始于一个赛事课题,希望设计一款带有诸多语法糖的现代编程语言。赛事结束时,Alioth 仍有大量工作尚未完成,但限于时间安排,开发陷入停滞。时隔多年我们终于重拾夕日旧梦,当年的语法设计在众多现代编程语言的光辉照耀之下已经不值一提。但我们为自研编程语言所做的工作,我们基于实践对编译原理的理解,这些不应随风而逝。现在,Alioth 项目将带着对美和自由的追求和大家一起继续探索编译原理。
Alioth 项目致力于在最小外部依赖基于C++构建一套编译器前端基础设施。您可以使用 alioth
简化自己的编程语言的设计工作和编译器前端的开发工作。
仓库地址: github.com/godgnidoc/alioth.git
您可以为自己的编程语言编写 grammar
文法定义文件,并使用 alioth framework
指令为它生成解析器代码框架。
参考 grammar文法介绍 了解如何编写
grammar
文法定义
framework
指令当前仅支持生成C++
代码框架,后续计划提供更多开发语言供选择
如果您正在设计一款不需要生成可执行指令的标记语言,那么事情就更简单了。您可以在 grammar
文法中对您关注的单词做恰当的标记,直接使用 alioth compile
指令即可从目标源码提取关键信息,以 JSON
格式组织起来。
参考 examples/marking_language
了解如何进通过 grammar
文法定义开发一款简单的标记语言翻译器。
- 带有一定歧义处理能力的语法分析器
- 基于属性标记做语法树剪枝
- 推导语法树最简骨架,生成分析器框架代码
- 内建支持高阶函数的模板引擎
alioth
语法分析器使用 LR(1)
分析表驱动,默认不能识别带有移进归约冲突或归约归约冲突的语法。但 alioth
词法分析器允许利用上下文概念将可能识别相同文本的不同单词划分到不同的上下文定义。
alioth
语法分析器生成器会为每个状态计算展望符号所在的上下文。分析器会维护一个分析线路集合,每个线路都记录了当前的语法分析器格局。在词法分析阶段若展望符号分属多个不同的上下文,则分析器会克隆当前分析线路创建多个分支分别进入不同上下文继续进行分析。
这样设计不会显著增大 LR
分析表,在上下文定义的引导下,相当于在必要时动态增加了前瞻符号的数量,延迟了冲突处理,以较低的成本扩展了 LR(1)
语法分析器的分析能力。
IN<op> = /in/
ID = /[a-zA-Z_]\w+/
expr -> ID
| ID PLUS ID
| ......;
iterating -> FOR LP ID IN expr RP block;
如上例子,分析器仅在 for
语句中 ID
之后才可能尝试分析 IN
,因此即使它匹配的文本实际上与 ID
冲突,也不会影响在其它语境下将 in
匹配为 ID
符号。
语法分析器默认产生包含全部符号的完整语法树,将这样的语法树用作语义分析算法的输入时,有以下几个值得关注的问题:
- 语义分析器通过偏移量访问句子中的符号,语义分析器要确保解析操作符合语法定义
- 相同非终结符的句子结构可能不同,语义分析器需要设法区分
- 为使语法设计成立而添加的中间语法结构会增加语义分析器的解析复杂度
alioth
支持在 grammar
文法定义的产生式上将符号标记为当前语法结构的属性,或者要求分析器将某个符号已经获得的全部属性展开到当前语法结构上。
function_declare -> FN ID@name LP ...params RP SEMI;
params -> ID@params
| ...params COMMA ID@params;
如上例子, ID@name
表示识别一个 ID
符号,将其用作当前语法结构的 name
属性。...params
表示识别一个 params
符号,将其中全部的属性拷贝到当前语法结构上。
同名属性出现多次会被存储为数组。假设用上述文法识别下文输入:
fn my_func ( arg1, arg2, arg3 );
则剪枝后的语法结构如下:
{
"name": "my_func",
"params": [ "arg1", "arg2", "arg3" ]
}
而原始语法结构则臃肿许多:
[
"fn",
"my_func",
"(",
[
[
["arg1"],
",",
"arg2"
],
",",
"arg3"
]
")",
";"
]
显然,基于属性的剪枝操作同时解决了上述三种问题,极大简化了语义分析器的开发。
语义分析器的开发工作还能进一步简化吗?能!
即使提供了语法树剪枝,alioth
仍然不能预测您要设计的语言的语法结构,量身设计语法树节点的数据结构。而使用通用节点数据结构存储语法树和属性表就会在语义分析阶段产生大量的查找操作:
auto name = AttrOf(func_node, "name");
auto params = AttrsOf(func_node, "params");
这样的查找浪费性能,且硬编码字符串总让人坐立不安。我们希望像访问字段一样访问属性:
auto name = func_node->name;
auto params = func_node->params;
这就意味着需要为每一种非终结符单独设计一个结构体和对应的解析算法。幸运的是,我们有 alioth
!
alioth
会从语法定义的属性标记推导每个非终结符可能拥有的属性,将其归纳为骨架定义,用于生成框架代码时指导结构体和分析器函数的生成。
骨架分析除了能够推导语法结构的属性,还能根据句型分组为同一个语法结构拆分生成不同的结构体扩展。
关于句型分组的定义方法请参考 grammar文法
试想,在全局语义分析层面,我们希望全部的表达式都能被统一的类型名指代,就叫它们 Expression
最好。但当我们需要深入分析表达式语义时,我们又希望表达式能提前按照句子结构区分好类型,比如:
BinaryExpression
PrefixExpression
SuffixExpression
SubExpression
骨架分析算法会为我们定义基础节点结构和按句型区分的派生结构。在实际分析过程中,通过归约产生式 ID
来判断句型,使用正确的结构体存储属性。
为了高效提供框架代码生成功能。alioth
内建了一个功能完备的模板引擎。它读取自定义的模板语言源码,将其编译成一个可执行的仿函数,我们称之为过滤器 Filter
。将过滤器作用于一个数据模型,即可得出目标文本。
请参考 template模板引擎介绍 了解模板编写方法和模板引擎的重要设计细节
alioth
模板引擎使用统一的 Value
类型来抽象过滤器和数据单元。这使得 alioth
模板支持高阶函数,可以配合模板表达式动态生成并调用过滤器。
alioth
模板引擎可以通过命令行接口或编程接口的形式调用。后文模板工具章节会介绍如何通过命令行接口使用模板引擎。
alioth
使用 C++
构建,你需要一套能够接受 C++20
标准的构建工具链。alioth
使用 vcpkg
获取依赖并合入构建系统。如果你不打算使用 vcpkg
请手动安装如下依赖:
fmt
C++20标准字符串格式化框架的原版nlohmann/json
接口泛型非常棒的json
处理库gtest
Google 推出的C++
测试框架
此外,alioth
使用 cmake
作为默认构建系统,对于 linux
环境下的简单构建场景,可以使用如下指令:
#!/bin/bash
cmake --preset x64-linux-debug
cmake --build build/x64-linux-debug
alioth
会提供一套核心代码库和一个命令行工具。大部分使用场景下我们需要将二者结合使用。
我们使用命令行工具生成一些骨架代码或静态数据,之后基于 alioth
核心代码库开发我们自己的应用。
使用 grammar
文法定义我们的词法规则和语法规则,保存为 my_language.grammar
文件,使用如下指令生成框架代码:
#!/bin/bash
alioth framework -o my_project/.generated my_language.grammar
参考 grammar文法介绍 了解如何编写
grammar
文法。
创建项目将生成的框架代码和 alioth
核心库加入编译列表即可。生成的框架代码提供了将源码编译为语法树的一站式接口 ParseMyLanguage
。
TODO: 详细讲解基于alioth开发编译器的步骤和要点
alioth
命令行还提供了其它简单工具方便使用。
alioth
开放了用于生成框架代码的内建模板引擎,我们可以编写自己的模板通过命令行或接口调用的形式使用它。
#!/bin/bash
alioth render my_model.json -t my_template.template
上述代码块演示了如何通过命令行使用模板引擎。参考 template模板引擎介绍 了解模板语法以及如何通过接口调用模板引擎。
一些领域专用语言并不需要被翻译成可执行的指令序列,我们只需要从中提取感兴趣的数据结构即可。典型例子是用于描述通信拓扑的 IDL
语言。
此时,我们可以省去自定义编译器的开发过程,仅编写 grammar
文法后,通过 compile
指令将目标源码编译成语法树,并从中提取属性树转换为 json
格式供我们使用。
参考 examples/marking_language
了解如何仅通过 gramamr
文法开发一个简单的标记语言翻译器。
alioth 内建了用于定义命令行应用并分析命令行参数的框架 cli
。此命令行框架支持简便的声明式开发风格助我们简单地创建一个带有参数和选项的命令。
cli
框架是纯头文件框架,参考 include/cli/cli.h
了解更多详情。