最近项目稳定下来,在开发资料片的间隙,我看了看传说中的 metalua。
其实很早以前我就关注了 metalua 了,可是一直都没有文档。所以感觉很混乱。不
过最近在 lua 的邮件列表有人问了关于 metalua 的问题,所以我找了找相关的信息。发
现这个东西实在是强啊。
metalua 是一个用 lua 语言编写的,带元编程扩展的 lua 编译器。它分为两个部分
, mlp 和 bytecode。 其中 mlp 是在 gg (Grammer Generator)的基础上将 lua 的源
代码转换成语法树的库,而 bytecode 则是将语法树解析成 lua 的字节码。metalua 最
大的特点就是在 mlp 中加入了元编程的扩展。即可以在生成语法树的同时,执行一些代
码。这一点很像 Lisp 的宏机制。但是比起 Lisp,metalua 将编译和执行划分成了两个
完全不相关的部分。因此在运行时是不需要带上庞大的运行时库的。 metalua 生成的字
节码可以在标准的 lua 环境中执行。这就解决了 Lisp 发布的执行文件体积巨大的问题
。metalua 的原理是,在编译的时候,你可以直接写出语法树,嵌入待编译的程序。这样
你可以做到 lua 的语法不能做到的一些事情。比如 goto 等等。而且也可以实现宏机制
。宏的好处是它是在编译的时候展开的。因此可以避免运行时效率问题。我们来看一个例
子,这里用到了 metalua 的 dollar 库。这个库的作用是,所有以 $ 开头的标识符都会
被当做一个宏看待, metalua 会去编译期的一个表 dollar 中查找这个标识符。并将其
对应的语法树嵌入到调用的位置,下面是一个 max 宏,用于获得两个值中的较大者:
-{block: require 'metalua.dollar' dollar.max = |a, b| `Stat { +{block: local a, b, res = -{a}, -{b} if a > b then res = a else res = b end }, +{res} } } print($max(100, 2))
首先最引人注目的是 -{ ... } 语法结构。这就是元编程的符号。所有在 -{ ... }
中出现的语句是在编译期,而不是运行期被执行,而它在执行时的环境是编译时环境,包
含了所有的编译时工具,而不是运行时的环境。换句话说,-{ ... } 中的代码是根本“
看不见”你在正常的原代码中声明的函数、变量的。
首先,我们在编译期载入了 metalua 的 dollar 库。这个库修改了编译器,在其中
加入了对 $ 的支持。其次,我们在 dollor 表(由 metalua.dollar 创建)中加入了一
个条目:如果发现名为 max 的宏,则调用我们的函数。参数则是 max 宏的参数,注意,
宏的参数一定是语法树。也就是说,这里的参数 a 和 b 实际上是两颗语法树,而不是参
数执行以后的结构。
|args...| expr 是 metalua 的扩展。相当于 function(args...) return expr
end,上面的函数也可以写成:
function(a, b) return `Stat { +{block: local a, b, res = -{a}, -{b} if a > b then res = a else res = b end }, +{res} } end
当 max 宏被找到时,该函数被调用,参数是 max 宏的参数(即两颗语法树),我们
返回了一个新的语法树。这里的 `Stat { ... } 也是 metalua 的扩展,是
{ tag='Stat', ... }
的语法糖,由此可以看出,所谓的语法树实际上是 Lua 的一个表。表里有个特殊的
项 tag 表示语法树节点的种类,而表中的各个项则是语法树中的各个子项。
在 `Stat 语句语法树中,我们采用了 +{ ... } 结构,不同于可以在程序中任意使
用的,类似于语句的 -{ ... } 结构, +{ ... } 结构更像是个函数:它接受一系列的“
正常”的 lua 语句,并将其转换成语法树,比如, +{ f(a + b) } 会返回这样一个表:
{ tag = 'Call', { tag = 'Id', "f" }, { tag = 'Op', "add", { tag = 'Id', "a" }, { tag = 'Id', "b" } } }
这个表表示出了 f(a + b) 这个简单语句的结构,而编译的过程则是遍历这个表,为
表的每一项生成合适的字节码。
metalua 中,每一种不同的语法树的结构都不一样,比如,Stat 树的第一项是一个
列表,表中的每一项是一个语句的语法树。Stat 还有一个扩展。如果 Stat 有第二项的
话,则这颗语法树可以作为表达式使用,而这个表达式的值则是第二个项所代表的语法树
,而且这第二项的作用域是在 Stat 内的。这意味着你可以返回 Stat 中的局部变量的值
。
最后要说的是在 +{ ... } 中的 -{ ... },+块用于将 lua 代码转换成语法树,那
么+块中的-块则用于将某个已有的语法树“嵌入”到生成的语法树里面去,这有点像
Lisp的 ` 和 ,@ 的功能。
所以,这个函数返回了一颗人工构造的语法树。首先分配了三个局部变量,然后用
if 找到较大的那个,赋值。最后指定整个块返回 res 变量的值。这颗语法树是直接被嵌
到程序里面去的,这意味比如你写 print($max(100, 2)),实际生成的代码是这样的:
local a, b, res = 100, 2 if a > b then res = a else res = b end print(res)
有人说, Scheme 实际上是“没有语法的 Lua”,虽然我很不赞同这句话,但是从刚
才的简短介绍中可以看出, metalua 的确是可以当之无愧地称之为“带着语法的 Scheme
”了。metalua 可以在编译期执行计算、允许编译时修改编译器等等特性,可以完成许多
十分强大的功能。这种能力用在 DSL(领域描述语言)上是威力强大的。可以说,Lua 带
上元编程,就为本来就很简洁强大的 Lua,插上了一双腾飞的翅膀。
Dec 04, 2012 06:39:39 PM
没搞明白该怎么用……