Lua 宏 |
|
http://luaforge.net/frs/download.php/4329/luamacro-1.5.zip
Lua 5.1.4,带有 [tokenf 补丁]。
有一个 Lua 5.1.4 补丁过的 Windows 版本,供好奇者使用 Mingw (即非 Lua for Windows 兼容) [在此处]。
lhf 的 tokenf 补丁 (也参见 [这篇文档]),为 Lua 编译器看到的词法单元流提供了一个简单而强大的钩子。(在 Lua 中,对于一个给定的模块,编译成字节码和执行是两个独立的阶段。) 基本上,您必须提供一个名为 FILTER 的全局函数,它将被以两种截然不同的方式调用。首先,它将以两个参数被调用;一个函数(“getter”)用于获取下一个词法单元,以及源文件。之后,它将被无参数调用,并期望返回三个值。(这一点初看令人困惑,这两个函数可能应该被赋予不同的名称。)
get 函数返回三个值:行号、词法单元和值。词法单元有一些特殊值,如 '<name>'、'<string>'、'<number>' 和 '<eof>',但否则就是实际的关键字或操作符,如 'function'、'+'、'~='、'...' 等。如果词法单元是特殊情况之一,则词法单元的值将作为第三个值返回。(在 tokenf 分发包中有一个示例,名为 fdebug,它只是打印出这些值。)
词法单元过滤器一次读取和写入一个词法单元。协程使得在不管理状态机的情况下维护复杂状态成为可能。
宏这里描述的宏设施类似于 C 预处理器,尽管它工作在已经预处理过的词法单元流上,而不是一个单独的程序,Lua 代码通过它传递。这有几个优点——它更快(没有单独的翻译阶段),并且宏可以交互式地测试。缺点是 LuaMacro 依赖于补丁版本的 Lua,并且宏的调试有时会有点麻烦,因为您看不到转换后的文本结果。
一如既往,宏需要谨慎使用。它们不共享 Lua 的作用域概念(因此应该有独特的命名),过度使用它们可能导致只有原始编写者才能阅读的代码,这就是“私人语言”问题。(参见 http://research.swtch.com/2008/02/bourne-shell-macros.html 获取经典示例。)
即使不是您的生产/发布代码的一部分,宏在调试和构建测试中也很有用。如果您将 Lua 用作 DSL(领域特定语言),那么宏可以方便地定制语法。
此版本 (1.5) 允许 Thomas Lauer 建议的简化表示法,其中简单宏非常类似于其 C 等价物。
一个接受两个参数的宏
macro.define('PLUS(L,C) ((L)+(C))')
以下是等同于 C 风格的 assert,其中实际表达式被转换为字符串,以形成 assert() 的可选第二个参数,使用“字符串化”函数 _STR()。
macro.define('ASSERT('x') assert(x,_STR(x))')
这样做的优点是可以通过更改头文件全局地移除断言。
宏定义需要与要预处理的代码放在不同的文件中,但不需要在您的程序之前加载。取而代之的是,有一个标准的宏 __include。假设 PLUS 和 ASSERT 宏已在 plus.lua 中定义,那么
--test-macro.lua __include 'plus' print(PLUS(10,20)) ASSERT(2 > 4)
$ lua -lmacro test-macro.lua
30
lua: test-macro.lua:3: 2 > 4
stack traceback:
[C]: in function 'assert'
test-macro.lua:3: in main chunk
重要的是,模块 macro 必须在程序被解析之前加载,因为宏在编译阶段运行。
它们可以这样进行交互式测试
D:\stuff\lua\tokenf>lua -lmacro -i
Lua 5.1.2 Copyright (C) 1994-2007 Lua.org, PUC-Rio
> __include 'plus'
> = PLUS(10,20)
30
> = PLUS(10)
=stdin:1: PLUS expects 2 parameters, received 1
> ASSERT(2 > 4)
stdin:1: 2 > 4
stack traceback:
[C]: in function 'assert'
stdin:1: in main chunk
[C]: ?
替换可以是一个函数——这是事情变得有趣的地方。
macro.define('__FILE__',nil,function(ls) return macro.string(ls.source) end)
nil 第二个参数表示我们没有参数,第三个替换参数是一个函数,它总是接收一个包含词法状态的表:source、line 和 get (当前正在使用的 getter 函数)。此函数应返回一个词法单元列表:在本例中是 {'<string>',ls.source}。有三个方便的函数 macro.string()、macro.number() 和 macro.name() 可用。在 LuaMacro 1.5 中,该函数也可以返回一个字符串。
一般来说,替换函数接收传递给宏的所有参数。
local value_of = macro.value_of macro.define('_CAT',{'x','y'},function(ls,x,y) return macro.name(value_of(x)..value_of(y)) end)
这也是处理可变长度参数列表的唯一方法,因为否则形式参数和实际参数的数量必须匹配。请记住,参数始终以词法单元列表的形式出现,它们具有特定的缩写格式。例如,{'<name>','A','+',false,'<name>','B','*',false,'<number>',2.3}。(这里 false 是 nil 的占位符。)
请注意,宏定义是 Lua 模块,因此您可以自由定义局部变量和函数。
新的简化宏定义-作为字符串允许在实际使用它们的源文件中定义简单宏。这甚至适用于交互式解释器。
$ lua -lmacro
Lua 5.1.4 Copyright (C) 1994-2008 Lua.org, PUC-Rio
> __def 'dump(x) print(_STR(x).." = "..tostring(x))'
> x = 10
> dump(x)
x = 10
> dump(10*4.2)
10 * 4.2 = 42
(这可能是一个有用的调试工具箱工具。)
考虑这个为数组中的所有值求值语句的简写
__def 'for_(t,expr) for _idx,_ in ipairs(t) do expr end' for_({10,20,30},print(_))
请参见 functional.lua 获取此风格的更多示例。
使用匿名函数时,一个常见的模式是
set_handler(function() ... end)
如果能像这样简单地表达就好了 ( LeafStorm 的 BeginEndProposal)
set_handler begin
...
end
通过 LuaMacro 1.5,宏可以设置词法扫描器,它们会监视词法单元流以查找指定的词法单元。一个特别有用的扫描器是“结束扫描器”。在这种情况下,扫描器检测块的最后一个 end,并发出 end)。
def ('begin',nil,function() macro.set_end_scanner 'end)' return '(function(...)' end)
另一个 LuaMacro 1.5 特性是,任何词法单元都可以用作宏名。考虑引入一个短的匿名函数形式的问题 (参见 https://lua-users.lua.ac.cn/lists/lua-l/2009-12/msg00140.html)。而不是 function(x) return x+1 end,我们可以说 \x(x+1)。许多读者(尽管不是全部;))在指定短函数时发现这种表示法不太嘈杂。
'\' 是一个词法单元宏的好选择,因为它在语言的其他地方都没有出现。您可以定义一个处理器,如果宏旨在在没有参数列表的情况下调用,则提供参数。这是 define() 的第四个参数。
-- lhf-style lambda notation def ('\\', {'args','body';handle_parms = true}, 'function(args) return body end', function(ls) -- grab the lambda -- these guys return _arrays_ of token-lists. We use '' as the delim -- so commas don't split the results local args = macro.grab_parameters('(','')[1] local body = macro.grab_parameters(')','')[1] return args,body end )
多参数函数(如 \x,y(x+y)和定义函数的函数(如 \x(\y(x+y)))按预期工作。
作为一个实际有用的例子,下面是如何将 try 和 except 定义为围绕 pcall() 的语法糖。
-- try.lua function pack (...) return {n=select('#',...),...} end macro.define ('try',nil, 'do local res = pack(pcall(function()' -- try block goes here ) macro.define ('except',{'e',handle_parms=macro.grab_token}, function() -- make sure that the 'end' after 'except' becomes 'end end' to close -- the extra 'do' in 'try'. -- we start at level 1 (before 'end))') and must ignore the first level zero. macro.set_end_scanner ('end end',1,true) return [[ end)) if res[1] then if res.n > 1 then return unpack(res,2,res.n) end else local e = res[2] ]] -- except block goes here end ) return 'local pack,pcall,unpack = pack,pcall,unpack'
所以,给定这样的代码
a = nil try print(a.x) except e print('exception:',e) end
编译器会看到以下代码
a = nil do local res = pack(pcall(function() print(a.x) end)) if res[1] then if res.n > 1 then return unpack(res,2,res.n) end else local e = res[2] print('exception',e) end end
这些宏的智能性(请注意,我们可以处理关闭额外的 do 语句)意味着,无需修补 Lua 本身,就可以更容易地尝试新的语法提案。用 Lua 编写宏比用 C 编写语法扩展要容易一个数量级!
(请注意,这不是问题的完整解决方案。特别是,我们无法处理显式返回的块,但又不返回任何值。)
最后一个 return 语句需要一些解释。此宏假设它展开的环境可以访问函数 pack、pcall 和 unpack。通常情况并非如此,因为使用 module(...) 创建的模块默认情况下无法访问全局环境。
此宏应在使用 module(...) 调用之前,通过 __include try 引入模块。__include 在内部使用 require,如果它返回一个字符串,那么这就是 __include 宏展开的实际替换值。这样,宏的隐藏依赖关系就能正确地在模块中提供。
作为一个更复杂的代码生成示例,这里有一个 using 宏,它的工作方式类似于 C++ 语句。Lua 中没有真正的模块作用域,因此一个常见的技巧是“展开”一个表。
local sin = math.sin local cos = math.cos ...
我们不仅得到了漂亮的非限定名称,而且访问局部函数引用比查找表中的函数更快。这是一个可以自动生成上述代码的宏。
macro.define('using',{'tbl'}, function(ls,n) local tbl = _G[n[2]] local subst,put = macro.subst_putter() for k,v in pairs(tbl) do put(macro.replace({'f','T'},{macro.name(k),n}, ' local f = T.f; ')) end return subst end)
这里的替换是一个函数,它接收一个名称词法单元(如 {'<name>','math'}),假定它引用一个全局可用的表,然后动态地遍历该表以生成所需的 local 赋值。subst_putter() 给你一个词法单元列表和一个 put 函数;你可以使用 put 函数来填充词法单元列表,然后返回该列表并实际替换到词法单元流中。replace 通过将形式参数(第一个参数)的所有出现替换为实际参数值(第二个参数)来生成一个新的词法单元列表。要使用它,请将宏调用放在模块的开头。
using (math)
这会将表的整个内容引入作用域,并假定该表在编译时确实存在。一个更好的习语是 import(math,sin cos),它展开为 local sin = math.sin; local cos = math.cos。
macro.define ('import',{'tbl','names'}, function (ls,tbl,names) local subst,put = macro.subst_putter() for i = 1,macro.length_of(names) do local name = macro.get_token(names,i) put 'local'; put (name); put '='; put (tbl); put '.'; put (name); put ';' end return subst end )
在 PythonLists 中,FabienFleutot 讨论了一种模仿 Python 风格的列表推导语法。
x = {i for i = 1,5}
{1,2,3,4,5}
这种语句实际上不需要太多转换就可以成为有效的 Lua。我们使用匿名函数。
x = (function() local ls={}; for i = 1,5 do ls[#ls+1] = i end; return ls end)()
但是,要使其作为宏工作,我们需要选择一个名称(此处为“L”),因为我们无法前瞻以查看 for 词法单元。
macro.define('L',{'expr','loop_part',handle_parms=true}, ' ((function() local t = {}; for loop_part do t[#t+1] = expr end; return t end)()) ', function(ls) local get = ls.getter local line,t = get() if t ~= '{' then macro.error("syntax: L{<expr> for <loop-part>}") end local expr = macro.grab_parameters('for') local loop_part = macro.grab_parameters('}','') return expr,loop_part end)
替换相当直接,但我们需要通过自定义函数来捕获参数。第一次调用 macro.grab_parameters 会捕获到 ‘for’,第二次捕获到 ‘}’。这里我们必须小心,不要将逗号视为此捕获的定界符,方法是将第二个参数设置为空字符串。
任何有效的 for 循环部分都可以使用。
L{{k,v} for k,v in pairs{one=1,two=2}}
{ "one", 1 }, { "two", 2 } }
嵌套推导按预期工作。
x = L{L{i+j for j=1,3} for i=1,3}
{ { 2, 3, 4 }, { 3, 4, 5 }, { 4, 5, 6 } }
一个特别酷的习语是一次性将整个标准输入捕获为一个列表。
lines = L{line for line in io.lines()}
有一个变量 macro.verbose,您可以将其设置为查看 LuaMacro 读取和写入的词法单元。如果为 0,则设置调试钩子,但不显示任何调试输出;如果为 1,则显示编译器看到的转换后的词法单元流;如果为 2,则还会显示输入的词法单元流。
将详细程度设置为零(例如,通过 lua -lmacro -e "macro.verbose=0" myfile.lua)很有用,因为 __dbg 内置宏 then 可以动态更改详细程度。
__dbg 1 mynewmacro(hello) __dbg 0
这有助于您缩小特定问题区域,而无需浏览大量的输出。
尽管 LuaMacro 依赖于一个经过词法单元过滤器补丁的 Lua 编译器,但生成的字节码可以在标准的 Lua 5.1.4 上运行。提供了一个非常简单的编译器,基于 Lua 分发包中的 luac.lua。
$ lua macro/luac.lua myfile.lua myfile.luac $ lua51 myfile.luac <runs fine>
-- SteveDonovan