Lua 巨集

lua-users home
wiki

Lua 巨集是使用 Token 篩選程式,建立給 Lua 使用的巨集功能。

原始碼

http://luaforge.net/frs/download.php/4329/luamacro-1.5.zip

相依性

具備 [tokenf 補丁程式] 的 Lua 5.1.4。

對好奇的人而言,這裡提供了一個使用 Mingw 進行修正的 Lua 5.1.4 Windows 版本(即,與 Windows 版 Lua 不相容)[按此]

Token 篩選程式

lhf 的 tokenf 補丁程式(另請參閱 [這篇寫作])提供了一個簡單但強大的鉤子,用來深入 Lua 編譯器看到的 Token 串流。(在 Lua 中,對於指定的模組而言,編譯為位元組碼與執行是分開的階段。)基本上,您必須提供一個稱為 FILTER 的全域函數,將用兩種截然不同的方式呼叫它。首先,它將包含兩個參數:一個讓您能夠取得下一個 Token(「取得程式」)及原始檔的函數。接著,它將呼叫時不包含參數,但預期傳回三個值。(這乍看之下會令人困惑,因此,這兩個函數可能應該給予不同的名稱。)

get 函數傳回三個值:行、Token 及值。Token 包含一些特殊值,例如 '<name>''<string>''<number>''<eof>',但其他部分則為實際的關鍵字或運算子,例如 'function''+''~=''...' 等。如果 Token 是特殊案例之一,則 Token 的值將作為第三個值傳回。(tokenf 配布版有一個指導範例,稱為 fdebug,它僅會印出這些值。)

Token 篩選程式會一次讀寫 Token。協同程式讓維護複雜的狀態成為可能,不必管理一個狀態機。

Lua巨集

此處說明的巨集功能類似於 C 預處理器,儘管它是針對一個已經預先消化過的 Token 串流運作,而且不是一個獨立的程式,Lua 程式必須通過它。這有幾個優點:速度更快(沒有獨立的轉譯階段),而且巨集能夠以互動方式進行測試。缺點是 LuaMacro 相依於一個修補過的 Lua 版本,而且進行巨集除錯有時會造成一些問題,因為您看不到成果以轉換文本方式呈現。

一如往常,巨集必須審慎使用。它不會共用 Lua 的範圍概觀(所以應明確命名),過度使用它們可能會造成只有原始撰寫者能夠閱讀其程式的結果,這就是所謂的「私有語言」問題。(請參閱 http://research.swtch.com/2008/02/bourne-shell-macros.html,取得一個經典範例。)

即使在生產/ 已發布的代碼中沒有宏,宏在除錯與建構測試時可能還是會很有用。如果你將 Lua 當作 DSL (Domain Specific Language),則宏可以輕鬆自訂語法。

此版本 (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 第二個參數表示我們沒有參數,而第三個替換參數是永遠會收到一個包含詞法狀態的表格函式:來源、行與 get(目前正在使用的取得函式)。預期這個函式會傳回一個權杖清單:在本例中,是 {'<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.dev.org.tw/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巨集擴充的實際替代值。如此一來,巨集必要的隱藏相依性就會適當地出現在模組中。

作為較精細的程式碼產生範例,以下是一個類似於 C++ 語法的 using 巨集。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
)

實作 list 概括運算

PythonLists 中,FabienFleutot 討論了建模在 Python 語法的 list 概括運算語法。

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 } }

一種特別酷的寫法是將整個標準輸入作為一個 list 擷取,僅需一行

lines = L{line for line in io.lines()}

除錯 LuaMacro 程式碼

有個變數 macro.verbose,你可以設定它以查看 LuaMacro 讀寫的指示。如果它是 0,會設定一個除錯掛鉤,但不會顯示除錯輸出;如果它是 1,之後它就會顯示編譯器看到的轉換過指示串流;如果它是 2,它還會顯示輸入指示串流。

將詳細程度設為 0(例如透過 lua -lmacro -e "macro.verbose=0" myfile.lua)很有用,因為 __dbg 內建巨集之後就能動態變更詳細程度。

__dbg 1
mynewmacro(hello)
__dbg 0

這個可以幫你精確找出特定問題,無需涉足多頁文件。

編譯 LuaMacro 程式碼

儘管 LuaMacro 仰賴經過補丁的代碼過濾器 Lua 編譯器,但產生的位元組碼可以在 stock Lua 5.1.4 上執行。一個非常簡單的編譯器已經提供,基於 Lua 發行的 luac.lua

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

-- SteveDonovan


RecentChanges · preferences
edit · history
最後編輯於 2011 年 3 月 8 日 下午 1:20 GMT (diff)