Lua 宏

lua-users home
wiki

Lua 宏是一个使用 token filters 的 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 兼容) [在此处]

Token Filters (词法单元过滤器)

lhf 的 tokenf 补丁 (也参见 [这篇文档]),为 Lua 编译器看到的词法单元流提供了一个简单而强大的钩子。(在 Lua 中,对于一个给定的模块,编译成字节码和执行是两个独立的阶段。) 基本上,您必须提供一个名为 FILTER 的全局函数,它将被以两种截然不同的方式调用。首先,它将以两个参数被调用;一个函数(“getter”)用于获取下一个词法单元,以及源文件。之后,它将被无参数调用,并期望返回三个值。(这一点初看令人困惑,这两个函数可能应该被赋予不同的名称。)

get 函数返回三个值:行号、词法单元和值。词法单元有一些特殊值,如 '<name>''<string>''<number>''<eof>',但否则就是实际的关键字或操作符,如 'function''+''~=''...' 等。如果词法单元是特殊情况之一,则词法单元的值将作为第三个值返回。(在 tokenf 分发包中有一个示例,名为 fdebug,它只是打印出这些值。)

词法单元过滤器一次读取和写入一个词法单元。协程使得在不管理状态机的情况下维护复杂状态成为可能。

Lua

这里描述的宏设施类似于 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。假设 PLUSASSERT 宏已在 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}。(这里 falsenil 的占位符。)

请注意,宏定义是 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 获取此风格的更多示例。

使用 End-scanners (结束扫描器)

使用匿名函数时,一个常见的模式是

set_handler(function()
  ...
end)

如果能像这样简单地表达就好了 ( LeafStormBeginEndProposal)

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 语句

作为一个实际有用的例子,下面是如何将 tryexcept 定义为围绕 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 语句需要一些解释。此宏假设它展开的环境可以访问函数 packpcallunpack。通常情况并非如此,因为使用 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()}

调试 LuaMacro 代码

有一个变量 macro.verbose,您可以将其设置为查看 LuaMacro 读取和写入的词法单元。如果为 0,则设置调试钩子,但不显示任何调试输出;如果为 1,则显示编译器看到的转换后的词法单元流;如果为 2,则还会显示输入的词法单元流。

将详细程度设置为零(例如,通过 lua -lmacro -e "macro.verbose=0" myfile.lua)很有用,因为 __dbg 内置宏 then 可以动态更改详细程度。

__dbg 1
mynewmacro(hello)
__dbg 0

这有助于您缩小特定问题区域,而无需浏览大量的输出。

编译 LuaMacro 代码

尽管 LuaMacro 依赖于一个经过词法单元过滤器补丁的 Lua 编译器,但生成的字节码可以在标准的 Lua 5.1.4 上运行。提供了一个非常简单的编译器,基于 Lua 分发包中的 luac.lua

$ lua macro/luac.lua myfile.lua myfile.luac
$ lua51 myfile.luac 
<runs fine> 

-- SteveDonovan


RecentChanges · preferences
编辑 · 历史
最后编辑于 2011 年 3 月 8 日上午 7:20 GMT (差异)