Lua 宏 |
|
http://luaforge.net/frs/download.php/4329/luamacro-1.5.zip
带有 [tokenf 补丁] 的 Lua 5.1.4。
对于好奇的用户,有一个使用 Mingw 的 Lua 5.1.4 补丁 Windows 版本(即不兼容 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 风格断言的等效项,其中实际表达式被转换为字符串以使用“字符串化”函数 _STR()
形成可选的 assert()
的第二个参数
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)。我们可以说 \x(x+1)
,而不是 function(x) return x+1 end
。许多读者(虽然不是全部;)发现,在指定简短函数时,这种表示法更简洁。
'\' 是令牌宏的不错选择,因为它在语言中其他地方没有出现。如果打算在没有参数列表的情况下调用宏,则可以定义一个提供参数的处理程序。这是 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` 内置宏可以动态地更改详细程度。
__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