Lua 宏

lua-users home
wiki

Lua 宏是使用令牌过滤器为 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,它只是打印出这些值。)

令牌过滤器一次读取和写入一个令牌。协程使维护复杂状态成为可能,而无需管理状态机。

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 风格断言的等效项,其中实际表达式被转换为字符串以使用“字符串化”函数 _STR() 形成可选的 assert() 的第二个参数

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

使用结束扫描器

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

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)。我们可以说 \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 语句

作为一个实际有用的示例,以下是将 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` 内置宏可以动态地更改详细程度。

__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


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2011 年 3 月 8 日下午 1:20 GMT (差异)