Lua的函數調用和協程中,棧的變化情況
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
int
luv_dumpstack(lua_State *L) {
int
i, top;
printf
(
"-------------------\n"
);
printf
(
"dumpstack: L=%p\n"
, L);
top = lua_gettop(L);
printf
(
"top: %d\n"
, top);
for
(i = 1; i <= top; ++i) {
printf
(
"[%d][%s]: %s\n"
,
i,
luaL_typename(L, i),
luaL_tolstring(L, i, NULL));
lua_pop(L, 1);
}
printf
(
"-------------------\n"
);
return
top;
}
static
int
cont(lua_State *L) {
printf
(
"stack after yield\n"
);
luv_dumpstack(L);
lua_pushinteger(L, 21);
lua_pushinteger(L, 22);
lua_pushinteger(L, 23);
return
lua_gettop(L);
}
static
int
test_yield(lua_State *L) {
printf
(
"stack before yield\n"
);
luv_dumpstack(L);
lua_pushinteger(L, 1);
lua_pushinteger(L, 2);
lua_pushinteger(L, 3);
lua_pushinteger(L, 4);
lua_pushinteger(L, 5);
return
lua_yieldk(L, 2, 0, cont);
}
static
int
test_resume(lua_State *L) {
lua_State *L1 = lua_newthread(L);
lua_pushinteger(L1, 11);
lua_pushinteger(L1, 12);
lua_pushcfunction(L1, test_yield);
lua_pushinteger(L1, 13);
lua_pushinteger(L1, 14);
lua_pushinteger(L1, 15);
printf
(
"stack before resume\n"
);
luv_dumpstack(L1);
printf
(
"resume: %d\n"
, lua_resume(L1, L, 3));
printf
(
"stack after resume\n"
);
luv_dumpstack(L1);
lua_pushinteger(L1, 24);
lua_pushinteger(L1, 25);
printf
(
"stack before second resume\n"
);
luv_dumpstack(L1);
/* XXX notice even we pass 2, all values in stack (4,5,24,25)
* will passed to coroutine */
printf
(
"resume: %d\n"
, lua_resume(L1, L, 2));
printf
(
"stack after second resume\n"
);
luv_dumpstack(L1);
return
0;
}
int
main(
void
) {
lua_State *L = luaL_newstate();
lua_pushcfunction(L, test_resume);
lua_call(L, 0, 0);
lua_close(L);
return
0;
}
/* cc: libs+='-llua52' */
|
在Lua里面聲明小數組的最好方法是什么?
在很早以前,就看到“閉包比表要快”的這么一個言論。一直沒有驗證過,只是心里就這么覺得了。所以自己第一次寫的Lua網游,大量利用了閉包,最后估計還是有很嚴重的內存問題……在我的對象模型里面,對象構造函數通常是這樣的:
1
2
3
4
5
6
7
8
9
10
11
12
|
function
Object()
local
t = {}
local
object_state1
local
object_state2
-- init ...
-- methods
function
t:foo(...)
return
...
end
function
t:bar(...)
return
...
end
-- return object
return
t
end
|
這個設計可以保證,在訪問對象的函數的時候,速度可以達到最快——因為沒有元表查詢。但是,這樣做恰好就違背了這樣的原則:“在設計的初期,不要過早地考慮優化”。是的,因為任何優化都是有代價的,這里的代價理所當然就是內存了。
另外,在Lua-5.1中,我后來自己測試的結果是,表貌似還是比閉包要快一點,關鍵點是,這樣貌似占用內存還小很多,自從這么擺了一道以后,反正至少對於“看見大括號就有點恐懼內存分配”的心理上是好過多了。不過至少對於閉包的大小和速度什么的心里頭反而就沒底了。
今天突然想到了一個叫做lua-vararg的lua庫。這個庫可以對vararg進行包裝,提供對用戶來說“比較自然”的vararg的體驗——說白了,這就是用vararg去實現了元組(tuple)嘛。這個庫很早以前就知道了,我還自己親自改過,但是性能到底怎么樣呢?心里沒底,所以決定今天寫個偽測試來看看效果。
首先,我們看一下Lua源代碼里面表和閉包的描述:
560
561
562
563
564
565
566
567
568
569
570
|
typedef
struct
Table {
CommonHeader;
lu_byte flags;
/* 1<<p means tagmethod(p) is not present */
lu_byte lsizenode;
/* log2 of size of `node' array */
struct
Table *metatable;
TValue *array;
/* array part */
Node *node;
Node *lastfree;
/* any free position is before this position */
GCObject *gclist;
int
sizearray;
/* size of `array' array */
} Table;
|
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
|
#define ClosureHeader \
CommonHeader; lu_byte nupvalues; GCObject *gclist
typedef
struct
CClosure {
ClosureHeader;
lua_CFunction f;
TValue upvalue[1];
/* list of upvalues */
} CClosure;
typedef
struct
LClosure {
ClosureHeader;
struct
Proto *p;
UpVal *upvals[1];
/* list of upvalues */
} LClosure;
typedef
union
Closure {
CClosure c;
LClosure l;
} Closure;
|
OK,代碼有了,那么第一個問題是:表和閉包分別占多大的大小呢?
額= =這個問題不好搞啊,看起來好復雜的樣子,還得考慮對齊……額,不好搞的話,直接寫個程序不就可以了嗎?
1
2
3
4
5
6
7
8
9
|
#include "src/lobject.h"
#include <stdio.h>
int
main(
void
) {
printf
(
"size of Table: %d\n"
,
sizeof
(Table));
printf
(
"size of Closure: %d\n"
,
sizeof
(Closure));
return
0;
}
|
恩,輸出結果是32和24……(別打偶……)
好吧,閉包居然比表要小!恩,這是很正常的。而且,這里的表可是“裸表”哦,一點數據都沒有的,而這個被聲明的閉包是默認帶上了一個upvalue的!
那么,對於有十個元素的表和閉包,大小又是怎么樣的呢?注意到表保存數組元素用的是TValue指針,而閉包里面的upvalue(主要指C閉包)也同樣是TValue,我們只需要分別給表和閉包的大小增加10個TValue的大小即可——額,閉包只需要加9,因為前面已經有個元素了,最后得到的大小是:112字節和96字節……恩~
好了,那么在大小方面,的確閉包(特指C閉包)是保存小數組的最好途徑了,那么,除開大小,在速度上面,它們之間有什么變化嗎?我們繼續測試一下!首先聲明一下,這里的測試對於真實的環境是沒有意義的,通常來說,我們並不需要考慮像分配小數組之間的效率問題,這里的測試,只是在於檢驗“閉包比表快”這個說法罷了,這樣的檢驗本身,可以當作是使用vararg模塊的依據,對做出某些設計決策是有好處的。
我們准備測試四種情況下的速度情況
- 用C API創建一個表,向其中壓入10個數字
- 用C API創建一個閉包,其中有10個upvalue
- 用Lua代碼創建一個表,其中有10個數字
- 用Lua代碼創建一個閉包,其中有10個upvalue
下面是測試結果:
1
2
3
4
5
6
7
8
9
10
|
test
c table time: 2.35s
c table memory: 5724
c closure time: 0.20s
c closure memory: 6020
lua table time: 0.40s
lua table memory: 5836
lua closure time: 1.51s
lua closure memory: 4556
Hit any key to close this window...
|
我們來分析一下這個數據,最簡單的分析方法是排序,對於時間的順序是c closure < lua table < lua closure < c table,對於內存剩余的順序是lua closure < c table == lua table < c closure。
好奇怪的結果!!我們試着來分析一下吧!看起來內存里面有一項是相同的:即用c和用lua創建表,占用內存的大小是近似相同的。而用C創建閉包,占用內存最大,但是也大不了多少。這里面是什么情況呢?我們關閉一下垃圾回收試試?對了!原來是這樣……關掉垃圾回收以后,內存的占用非常非常誇張,原來,這是垃圾回收以后的結果(順便說一下,關閉垃圾回收,所有的測試都變慢了,不過相對結果不變)。
那么,內存的意義上就不大了。只能說是垃圾回收的策略不同罷了。我們來分析一下時間問題好了。
首先,這個數據實在是太奇怪了了:如果說C的API比Lua代碼快,那么為什么c table是最慢的呢?如果說閉包比表快,那么為什么lua閉包不如lua的表呢?這是怎么回事呢?
很容易發現問題的是lua閉包,這是倒數第二慢的……因為Lua5.2出了一個新功能:在創建lua閉包的時候,會和之前緩存的進行比較,看看是新的閉包還是以前的,因為我們創建的閉包都是新的函數調用,顯然和以前閉包的upvalue不可能一樣,因此這個比較始終是會失敗的……看來也只能是這個地方會占用時間了。當然了,關聯upvalue、關閉upvalue等等都需要占據時間。lua閉包比較慢幾乎是可以預見到的了。這一點我們不奇怪。
奇怪的是,為什么用C API寫的新建表比lua代碼還要慢呢?注意到,Lua代碼是不需要再創建新的“整數”的,它們是直接從常量表(K表)載入的,所以pushnumber會拖慢一點速度,其次,lua_rawseti會比luaH_setint多做一些檢查,除了這兩點以外,還有一個很容易忽視的地方是API調用開銷:因為Lua是在DLL里面的,因此API調用相對較慢,如果靜態鏈接的話,對其他測試來說,結果影響不大,但是c table測試瞬間就快了20%——也許是因為每次推入新的包,c table測試比別的方法多了10個C API的原因吧……Lua里面沒有一次給表設置多個值的方法,必須一個一個地設置,每次設置都必須壓入新值,所以即使是相對較快的lua_rawseti,也架不住每次設置的兩次API啊……當然,從這一點上也可以看出來,我們的測試是很不合理的,因為即使是API效率,都可以極大地影響測試結果。當然這里需要說明的是,即使是將API效率的原因去除,c table仍然是最慢的。
然后,c cloure和lua table就比較符合我們的期望了。看來的確是閉包會快一點啊……
這里只比較了創建的效率,對於取值的效率,大家可以自己去比較。但是從這里可以看出來,如果希望交換大量小數據塊,那么closure的確是一個合理的選擇:它體積較小,沒有多余的功能(比如表支持的哈希查找或者增加大小或者獨立的元表等等)。作為需要快速保存少量數據,lua-vararg的確是一個很好的選擇——另外,盡量選擇C實現的版本,如果無法使用C庫,那就還是用表來實現吧……這樣比較快……
最后,附上我自己寫的vararg的C實現:https://gist.github.com/starwing/5893607
#define LUA_LIB #include <lua.h> #include <lauxlib.h> static lua_Integer posrelat(lua_Integer pos, size_t len) { if (pos >= 0) return pos; else if (0u - (size_t)pos > len) return 0; else return (lua_Integer)len + pos + 1; } static int tuple(lua_State *L) { int top, n = (int)lua_tointeger(L, lua_upvalueindex(1)); lua_Integer i, j; switch (lua_type(L, 1)) { case LUA_TNIL: /* as iterator */ i = lua_tointeger(L, 2) + 1; if (i <= 0 || i > n) return 0; lua_pushinteger(L, i); lua_pushvalue(L, lua_upvalueindex(i + 1)); return 2; case LUA_TSTRING: /* as length operator */ if (*lua_tostring(L, 1) == '#') { lua_pushinteger(L, n); return 1; } break; case LUA_TNONE: /* get all varargs */ luaL_checkstack(L, n, "too many values"); for (i = 1; i <= n; ++i) lua_pushvalue(L, lua_upvalueindex(i+1)); return n; case LUA_TNUMBER: /* get/set a range */ i = posrelat(luaL_checkinteger(L, 1), n); j = posrelat(luaL_optinteger(L, 2, i), n); if (i > j) return 0; n = (int)(j-i+1); luaL_checkstack(L, n, "too many values"); if ((top = lua_gettop(L)) <= 2) { /* get */ for (; i <= j; ++i) lua_pushvalue(L, lua_upvalueindex(i+1)); } else { int idx; lua_settop(L, top = n + 2); for (idx = 3; idx <= top; ++idx) { lua_pushvalue(L, idx); lua_replace(L, lua_upvalueindex(i+idx-2)); } } return n; } return luaL_argerror(L, 1, "invalid argument"); } static int Lpack(lua_State *L) { int n = lua_gettop(L); if (n >= 255) luaL_error(L, "too many values to pack"); lua_pushinteger(L, n); lua_insert(L, 1); lua_pushcclosure(L, tuple, n+1); return 1; } static int Lrange(lua_State *L) { int n = lua_gettop(L) - 2; lua_Integer i, j; if (n < 0) return 0; i = posrelat(luaL_checkinteger(L, 1), n); j = posrelat(luaL_checkinteger(L, 2), n); if (i > j || j == 0) return 0; if (j > n) luaL_checkstack(L, j-n, "range is too big"); lua_settop(L, j + 2); return j-i+1; } static int Linsert(lua_State *L) { int n = lua_gettop(L) - 2; lua_Integer i = posrelat(luaL_checkinteger(L, 2), n); if (i > n) { luaL_checkstack(L, i-n, "index is too big"); lua_settop(L, i + 1); lua_pushvalue(L, 1); return i; } lua_pushvalue(L, 1); lua_insert(L, i + 2); return n + 1; } static int Lremove(lua_State *L) { int n = lua_gettop(L) - 1; lua_Integer i = posrelat(luaL_checkinteger(L, 1), n); if (i <= n) { lua_remove(L, i + 1); --n; } return n; } static int Lreplace(lua_State *L) { int n = lua_gettop(L) - 2; lua_Integer i = posrelat(luaL_checkinteger(L, 2), n); if (i > n) { luaL_checkstack(L, i-n, "index is too big"); lua_settop(L, i + 1); lua_pushvalue(L, 1); return i; } lua_pushvalue(L, 1); lua_replace(L, i + 2); return n; } static int Lpush(lua_State *L) { lua_pushvalue(L, 1); return lua_gettop(L) - 1; } static int Lpop(lua_State *L) { lua_pop(L, 1); return lua_gettop(L); } static int Ltake(lua_State *L) { int n = lua_gettop(L) - 1; lua_Integer i = posrelat(luaL_checkinteger(L, 1), n); if (i > n) return 0; lua_pop(L, n-i); return i; } static int Ltail(lua_State *L) { int n = lua_gettop(L) - 1; lua_Integer i = posrelat(luaL_checkinteger(L, 1), n); if (i > n) return 0; return n-i+1; } static int Lshift(lua_State *L) { return lua_gettop(L) - 1; } static int Lmap(lua_State *L) { int i, n = lua_gettop(L); luaL_checkany(L, 1); for (i = 2; i <= n; ++i) { lua_pushvalue(L, 1); lua_pushvalue(L, i); lua_call(L, 1, 1); lua_replace(L, i); } return n-1; } static int Lfilter(lua_State *L) { int i, n = lua_gettop(L); luaL_checkany(L, 1); for (i = 2; i <= n; ++i) { lua_pushvalue(L, 1); lua_pushvalue(L, i); lua_call(L, 1, 1); if (!lua_toboolean(L, -1)) { lua_remove(L, i); --i, --n; } lua_pop(L, 1); } return n-1; } static int Lreduce(lua_State *L) { int i, n = lua_gettop(L); luaL_checkany(L, 1); if (n <= 3) { lua_call(L, n-1, 1); return 1; } lua_pushvalue(L, 1); lua_pushvalue(L, 2); lua_pushvalue(L, 3); lua_call(L, 2, 1); for (i = 4; i <= n; ++i) { lua_pushvalue(L, 1); lua_insert(L, -2); lua_pushvalue(L, i); lua_call(L, 2, 1); } return 1; } static int Lunpack(lua_State *L) { int i, n = lua_gettop(L); for (i = 1; i <= n; ++i) { lua_pushvalue(L, i); lua_call(L, 0, LUA_MULTRET); } return lua_gettop(L)-n; } static int Lrotate(lua_State *L) { int n = lua_gettop(L) - 1; lua_Integer i, c = luaL_checkinteger(L, 1) % n; #if LUA_VERSION_NUM >= 503 (void)i; /* unused */ lua_rotate(L, 2, (int)c); #else c = -c + ((c > 0) ? n+1 : 1); if (c > 1) luaL_checkstack(L, c-1, "too many values"); for (i = 2; i <= c; ++i) lua_pushvalue(L, i); #endif return n; } static int Lreverse(lua_State *L) { int i, j, n = lua_gettop(L); for (i = 1, j = n; i < j; ++i, --j) { lua_pushvalue(L, i); lua_pushvalue(L, j); lua_replace(L, i); lua_replace(L, j); } return n; } static int Lrep(lua_State *L) { int top = lua_gettop(L), n = top - 1; lua_Integer i, j, count = luaL_checkinteger(L, 1); if (count <= 0) return 0; if (n == 0) { luaL_checkstack(L, count, "too many values"); lua_settop(L, count+1); return count; } luaL_checkstack(L, n*(count-1), "too many values"); for (i = 1; i < count; ++i) for (j = 2; j <= top; ++j) lua_pushvalue(L, j); return n*count; } LUALIB_API int luaopen_vararg(lua_State *L) { luaL_Reg libs[] = { { "concat", Lunpack }, { "unpack", Lunpack }, { "filter", Lfilter }, { "insert", Linsert }, { "map", Lmap }, { "pack", Lpack }, { "pop", Lpop }, { "push", Lpush }, { "range", Lrange }, { "reduce", Lreduce }, { "remove", Lremove }, { "rep", Lrep }, { "replace", Lreplace }, { "reverse", Lreverse }, { "rotate", Lrotate }, { "shift", Lshift }, { "tail", Ltail }, { "take", Ltake }, { NULL, NULL } }; #if LUA_VERSION_NUM >= 502 luaL_newlib(L, libs); #else luaL_register(L, "vararg", libs); #endif return 1; } /* cc: flags+='-s -O3 -mdll -DLUA_BUILD_AS_DLL' libs+='-llua53' * cc: output='vararg.dll' */
local _G = require "_G" local assert = _G.assert local pcall = _G.pcall local print = _G.print local select = _G.select local type = _G.type local math = require "math" local ceil = math.ceil local huge = math.huge local min = math.min local table = require "table" local unpack = table.unpack or _G.unpack local vararg = require "vararg" local pack = vararg.pack local range = vararg.range local insert = vararg.insert local remove = vararg.remove local replace = vararg.replace local push = vararg.push local concat = vararg.concat local map = vararg.map local rotate = vararg.rotate -- auxiliary functions---------------------------------------------------------- local values = {} local maxstack for i = 1, huge do if not pcall(unpack, values, 1, 2^i) then local min, max = 2^(i-1), 2^i while min < max do local mid = ceil((min+max)/2) if pcall(unpack, values, 1, mid) then min = mid else max = mid-1 end end maxstack = max break end end for i = 1, maxstack, 2 do values[i] = i end local function tpack(...) return {..., n=select("#", ...)} end local function assertsame(v, i, j, ...) local count = select("#", ...) assert(count == j-i+1, count..","..i..","..j) for pos = 1, count do assert(v[i+pos-1] == select(pos, ...)) end end local function asserterror(expected, f, ...) local ok, actual = pcall(f, ...) assert(ok == false, "error was expected") assert(actual:find(expected, 1), "wrong error, got "..actual) end -- test 'pack' function -------------------------------------------------------- local function testpack(...) local v = {...} local n = select("#", ...) local p = pack(...) assertsame(v, 1, n, p()) assert(n == p("#")) for i,pv in p do assert(v[i] == pv) end for i = 1, n do assert(v[i] == p(i)) if n > 0 then assert(v[i] == p(i-n-1)) end end for i = 1, n, 10 do local j = i+9 assertsame(v, i, j, p(i, j)) if n > j then assertsame(v, i, j, p(i-n-1, j-n-1)) end end end testpack() testpack({},{},{}) testpack(nil) testpack(nil, nil) testpack(nil, 1, nil) testpack(unpack(values, 1, 254)) local ok, err = pcall(pack, unpack(values, 1, 255)) if ok then -- Lua version assert(type(err) == "function") else -- C version assert(ok == false and err == "too many values to pack") end -- test 'range' function ------------------------------------------------------- local function testrange(n, ...) local v = {...} for c = 1, 3 do for i = 1, n, c do local j = min(i+c-1, n) assertsame(v, i, j, range(i, j, ...)) local n = select("#", ...) if n > 0 then assertsame(v, i, j, range(i-n-1, j-n-1, ...)) end end end end local ok, err = pcall(range, 0, 0, ...) if ok then -- Lua version assert(err == nil) else -- C version assert(ok == false and err == "bad argument #1 to '?' (index out of bounds (0))") end testrange(10) testrange(10, 1,2,3,4,5,6,7,8,9,0) maxstack = 10000 -- use a smaller value testrange(maxstack, unpack(values, 1, maxstack)) -- test other functions -------------------------------------------------------- assertsame({1,2,3,4,5}, 1, 5, insert(3, 3, 1,2,4,5)) assertsame({1,2,3,4,5}, 1, 5, insert(4,-1, 1,2,3,5)) assertsame({1,2,nil,4}, 1, 4, insert(4, 4, 1,2)) assertsame({nil,nil,3}, 1, 3, insert(3, 3)) assertsame({1,2,3,4,5}, 1, 5, replace(3, 3, 1,2,0,4,5)) assertsame({1,2,3,4,5}, 1, 5, replace(5,-1, 1,2,3,4,0)) assertsame({1,2,nil,4}, 1, 4, replace(4, 4, 1,2)) assertsame({nil,nil,3}, 1, 3, replace(3, 3)) assertsame({1,2,3,4,5}, 1, 5, remove( 3, 1,2,0,3,4,5)) assertsame({1,2,3,4,5}, 1, 5, remove(-1, 1,2,3,4,5,0)) assertsame({1,2,nil,4}, 1, 4, remove( 4, 1,2,nil,0,4)) assertsame({nil,nil,3}, 1, 3, remove( 3, nil,nil,0,3)) assertsame({1,2,3,4,5}, 1, 5, remove(10, 1,2,3,4,5)) assertsame({1,2,3,4,5}, 1, 5, push(5, 1,2,3,4)) assertsame({1,2,nil,4}, 1, 4, push(4, 1,2,nil)) assertsame({nil,nil,3}, 1, 3, push(3, nil,nil)) assertsame({5,1,2,3,4}, 1, 5, rotate(1, 1,2,3,4,5)) assertsame({2,3,4,5,1}, 1, 5, rotate(-1, 1,2,3,4,5)) assertsame({5,1,2,3,4}, 1, 5, rotate(11, 1,2,3,4,5)) assertsame({2,3,4,5,1}, 1, 5, rotate(-11, 1,2,3,4,5)) assertsame({1,2,3,4,5,6,7,8,9}, 1, 9, concat(pack(1,2,3), pack(4,5,6), pack(7,8,9))) -- test function errors and expectional conditions --------------------------- assertsame({"1","2","3","4","5"}, 1, 5, map(tostring, 1,2,3,4,5)) assertsame({"1","2","nil","4" }, 1, 4, map(tostring, 1,2,nil,4)) assertsame({"nil","nil","3" }, 1, 3, map(tostring, nil,nil,3)) assertsame({"1","nil","nil" }, 1, 3, map(tostring, 1,nil,nil)) asserterror("bad argument #2 to '[^']*' %(number expected, got no value%)", insert) asserterror("bad argument #2 to '[^']*' %(number expected, got no value%)", insert, nil) asserterror("bad argument #2 to '[^']*' %(number expected, got nil%)", insert, nil, nil) --asserterror("bad argument #2 to '[^']*' %(index out of bounds %(0%)%)", insert, nil, 0) assert(select('#', insert(nil, 0)) == 1) asserterror("bad argument #2 to '[^']*' %(number expected, got no value%)", replace) asserterror("bad argument #2 to '[^']*' %(number expected, got no value%)", replace, nil) asserterror("bad argument #2 to '[^']*' %(number expected, got nil%)", replace, nil, nil) --asserterror("bad argument #2 to '[^']*' %(index out of bounds %(0%)%)", replace, nil, 0) assert(select('#', replace(nil, 0)) == 0) asserterror("bad argument #1 to '[^']*' %(number expected, got no value%)", remove) asserterror("bad argument #1 to '[^']*' %(number expected, got nil%)", remove, nil) --asserterror("bad argument #1 to '[^']*' %(index out of bounds %(0%)%)", remove, 0) assert(select('#', remove(0)) == 0) assertsame({}, 1, 0, push()) assertsame({nil}, 1, 1, push(nil)) assertsame({}, 1, 0, concat()) asserterror("attempt to call a nil value", concat, nil) asserterror("bad argument #1 to '[^']*' %(value expected%)", map) assertsame({}, 1, 0, map(nil)) asserterror("attempt to call a nil value", map, nil, nil)