Moonscript是一门极为小众的编程语言

Moonscript是一门编译成为Lua代码并在Lua虚拟机运行的编程语言。主要语法和特性借鉴于Coffeescript。这门语言的优势在于语言简练、具有较强表达力的同时能保留尽可能高的可读性,在表达力和可读性之间取得一个比较好的平衡点。有较为克制不那么corner case的语法糖。用来写一些经常变化的业务逻辑非常省力,实践下来编写相同的游戏开发类的业务逻辑,用Moonscript比写原生的Lua能缩减到1/2,甚至到1/3的代码量,更少的代码对减少Bug的产生或是问题排查也有很多帮助。另外这门语言还有一个重要特点,据discord群里的老哥说,全世界范围内的活跃用户可能只有20多人。还有一个更重要的特点就是这是一门Sailor Moon Themed的编程语言。

logo里暗藏情怀

开源和免费难以为继

Moonscript的作者因使用这门语言开发了一些商业网站,如销售独立游戏的itch.io,以及分享绘画作品的网站streak.club。说为了保持这门语言的稳定性,从2017年开始暂缓了项目的维护,不再增加新特性,甚至issue fix也不积极了。当然生活不易,作者还开了github sponsor希望他开发的开源软件能获得更多支持。我们也没理由要求别人一直免费给大家做贡献。

itch.io上的GameJam,在国外游戏创作的文化已经成了一种生活日常 

不爽就自己重写

当然,作为Moonscript粉丝的我对这样的状况是不能够接受的。原版Moonscript编译器是用Moonscript写的,核心是用C语言实现的PEG文法解析库解析Moonscript代码生成AST结构传到Lua环境中,再由Moonscript生成的Lua代码操作AST结构把Moonscript代码翻译成Lua代码。这个方案只是说是刚好能work,C语言实现的parser很高效,但是后续回到Lua环境创建大量Lua的数据结构,增加资源消耗和Lua GC时间其实并无必要,在数千行Moon代码的项目中不做预编译,在运行时加载Moonscript代码会明显感觉到程序的长时间卡顿。另外用动态类型的语言来操作需要严格检查数据类型的AST结构,完全是动态语言开发的弱项。

当然说得再多不如拿出代码有意义,所以我没有继承已有的code base,而是直接用第二喜欢的编程语言C++进行了完全的重写(第一喜欢的就是Moonscript)。并在重写的同时顺便修复了各类作者未解决的问题,并引入一些缺失了几年的其它语言都已经用烂的编程特性。

详见项目:MoonPlus

Transpilers For Lua和PEG文法

不过说到编译生成另一门编程语言的编译器,现在更准确的叫法是叫做转译器(transpiler)。Lua语言因为语言设计的简洁,实现了只用做一次遍历的递归下降解析器,本身的编译时间极快。又因为大家各自编程喜好的不同,很多人就打起了开发其它编程语言转译成Lua的转译器,扩展Lua语言的开发能力的想法。除了Moonscript外现在已有各类从Javascript、Typescript、Lisp、C、Python、Go和C#等等各种语言转译成Lua的实现。另外也有各种给Lua语言加上静态类型检查的想法。

创造新语言真让人上瘾,详见lua-languages

说到底还是因为大家的审美和个性化的需求的日益增长,以及硬件的发展解放了算力,让大家都不再纠结于程序文法复杂度以及程序编译期间各种开销的问题,解放了大家研发新编程语言的生产力。就如Python之父曾因为历史原因,在三十年前为了确保parser的执行效率,降低文本解析阶段的内存消耗,实现了LL(1)的文法,只要一个token的look ahead就足够完成文法解析。后来算力和内存提供的条件已经大大超过以前,便开始考虑采用对程序开发更加友好的PEG文法,通过使用足够多的缓存支持无限多次的文法匹配回溯(backtrace),提升解析器开发的灵活性,以增强未来Python语言演化的能力。

原版Moonscript也是用PEG文法实现的。一般实现PEG支持的程序库都是提供通过parser combinator的形式编写解析器程序。我在C++中先尝试了使用meta programming实现的在编译期构建parser的黑魔法库PEGTL,结果未获得任何开发上的增益,调试困难就不用说了,如果文法有复杂度太高,或者左递归,直接编译期提示生成函数嵌套超过最大值,左递归报错是应该,正常的嵌套太深就只能尝试调大编译参数看能不能过编译了。好不容易调好了parser生成一看好几个M的binary size,才发现这个库比起应用更多的只是炫技。最终我找到了parserlib。运行时生成parser,带有AST生成还提供一定程度的左递归文法自动解决功能,看了代码关于如何在parse的过程中创建AST的部分很精妙,就决定是它了。

用C++编写Transpiler的优势

有的人形容Moonscript是Lua上的一套宏系统,的确没错,很多Moon的语法其实就是加了能简写代码的Lua语法糖。Moon转译到Lua只要做三步操作,第一步是解析代码生成Moon AST,第二步是把Moon AST转换成对应的Lua AST,最后一步把Lua AST转换成代码文本。用C++操作AST结构的优势就是可以在编译期以及运行时以比较小的代价完成对AST结构的类型检查。

并且到C++17为止C++语言增加了很多新的编程特性,编程的表达力和抽象能力也已经变得更加强大。原版用Moonscript编写的Moonscript编译器用了近5K行代码,现在用C++17实现相同的业务功能也只用5K行多一点的代码量。discord群里另一位老哥也说他在C++98的年代写相同规模的项目预估代码量是不低于上万行的,C++17已经带来了他没想到的语言进步。当然表达力、抽象力是增强了,用了一些黑魔法特性以后,生成的binary size也增大了很多。

通过C++的meta programing的能力,我们可以放心地写这样的代码:

// 检查ast节点是Exp或ExpList
if (item.is<Exp_t, ExpList_t>()) {
    ...
}

// 检查某节点开始是否匹配某个ast结构分支
// 并获取最后一个匹配的节点
if (auto variable =
    node->getByPath<ChainValue_t, Callable_t, Variable_t>()) {
    auto varName = toString(variable->name);
    ...
}

// 用switch语句分别处理不同的ast结构
// id作为编译期常量由编译器自动生成,无需人手工编号
switch (node->getId()) {
    case id<While_t>(): {
        auto while_ = static_cast<While_t*>(node);
        ...
        break;
    }
    case id<For_t>(): {
        auto for_ = static_cast<For_t*>(node);
        ...
        break;
    }
    ...
}

通过利用模板泛型参数的功能,可以将一些参数类型的检查放到编译期。如:

node->getByPath<ChainValue_t, Callable_t>()

就要比类似

node->getByPath("ChainValue", "Callable")

这样的写法少很多潜在的风险,同时进行了编译期参数检查,运行时类型匹配的两重功能,动态类型的语言是很难取代这样的优势的。在这些设施的帮助下,不用额外设计特别复杂的检查机制,错误地操作AST结构就会产生明确的编译报错或是运行时报错,让C++写transpiler无比爽快和省心。

最后就请关注MoonPlus项目给star了。github地址码云地址

有更多想法也可以在项目中开issue和pr我们再共同讨论。