mirror of
https://github.com/lua/lua
synced 2024-11-24 21:59:41 +03:00
6d042a178f
The buffer system from the auxiliary library reuses its buffer as external memory when closing long strings.
1176 lines
28 KiB
Lua
1176 lines
28 KiB
Lua
-- $Id: testes/locals.lua $
|
|
-- See Copyright Notice in file all.lua
|
|
|
|
print('testing local variables and environments')
|
|
|
|
local debug = require"debug"
|
|
|
|
local tracegc = require"tracegc"
|
|
|
|
|
|
-- bug in 5.1:
|
|
|
|
local function f(x) x = nil; return x end
|
|
assert(f(10) == nil)
|
|
|
|
local function f() local x; return x end
|
|
assert(f(10) == nil)
|
|
|
|
local function f(x) x = nil; local y; return x, y end
|
|
assert(f(10) == nil and select(2, f(20)) == nil)
|
|
|
|
do
|
|
local i = 10
|
|
do local i = 100; assert(i==100) end
|
|
do local i = 1000; assert(i==1000) end
|
|
assert(i == 10)
|
|
if i ~= 10 then
|
|
local i = 20
|
|
else
|
|
local i = 30
|
|
assert(i == 30)
|
|
end
|
|
end
|
|
|
|
|
|
|
|
f = nil
|
|
|
|
local f
|
|
local x = 1
|
|
|
|
a = nil
|
|
load('local a = {}')()
|
|
assert(a == nil)
|
|
|
|
function f (a)
|
|
local _1, _2, _3, _4, _5
|
|
local _6, _7, _8, _9, _10
|
|
local x = 3
|
|
local b = a
|
|
local c,d = a,b
|
|
if (d == b) then
|
|
local x = 'q'
|
|
x = b
|
|
assert(x == 2)
|
|
else
|
|
assert(nil)
|
|
end
|
|
assert(x == 3)
|
|
local f = 10
|
|
end
|
|
|
|
local b=10
|
|
local a; repeat local b; a,b=1,2; assert(a+1==b); until a+b==3
|
|
|
|
|
|
assert(x == 1)
|
|
|
|
f(2)
|
|
assert(type(f) == 'function')
|
|
|
|
|
|
local function getenv (f)
|
|
local a,b = debug.getupvalue(f, 1)
|
|
assert(a == '_ENV')
|
|
return b
|
|
end
|
|
|
|
-- test for global table of loaded chunks
|
|
assert(getenv(load"a=3") == _G)
|
|
local c = {}; local f = load("a = 3", nil, nil, c)
|
|
assert(getenv(f) == c)
|
|
assert(c.a == nil)
|
|
f()
|
|
assert(c.a == 3)
|
|
|
|
-- old test for limits for special instructions
|
|
do
|
|
local i = 2
|
|
local p = 4 -- p == 2^i
|
|
repeat
|
|
for j=-3,3 do
|
|
assert(load(string.format([[local a=%s;
|
|
a=a+%s;
|
|
assert(a ==2^%s)]], j, p-j, i), '')) ()
|
|
assert(load(string.format([[local a=%s;
|
|
a=a-%s;
|
|
assert(a==-2^%s)]], -j, p-j, i), '')) ()
|
|
assert(load(string.format([[local a,b=0,%s;
|
|
a=b-%s;
|
|
assert(a==-2^%s)]], -j, p-j, i), '')) ()
|
|
end
|
|
p = 2 * p; i = i + 1
|
|
until p <= 0
|
|
end
|
|
|
|
print'+'
|
|
|
|
|
|
if rawget(_G, "T") then
|
|
-- testing clearing of dead elements from tables
|
|
collectgarbage("stop") -- stop GC
|
|
local a = {[{}] = 4, [3] = 0, alo = 1,
|
|
a1234567890123456789012345678901234567890 = 10}
|
|
|
|
local t = T.querytab(a)
|
|
|
|
for k,_ in pairs(a) do a[k] = undef end
|
|
collectgarbage() -- restore GC and collect dead fields in 'a'
|
|
for i=0,t-1 do
|
|
local k = querytab(a, i)
|
|
assert(k == nil or type(k) == 'number' or k == 'alo')
|
|
end
|
|
|
|
-- testing allocation errors during table insertions
|
|
local a = {}
|
|
local function additems ()
|
|
a.x = true; a.y = true; a.z = true
|
|
a[1] = true
|
|
a[2] = true
|
|
end
|
|
for i = 1, math.huge do
|
|
T.alloccount(i)
|
|
local st, msg = pcall(additems)
|
|
T.alloccount()
|
|
local count = 0
|
|
for k, v in pairs(a) do
|
|
assert(a[k] == v)
|
|
count = count + 1
|
|
end
|
|
if st then assert(count == 5); break end
|
|
end
|
|
end
|
|
|
|
|
|
-- testing lexical environments
|
|
|
|
assert(_ENV == _G)
|
|
|
|
do
|
|
local dummy
|
|
local _ENV = (function (...) return ... end)(_G, dummy) -- {
|
|
|
|
do local _ENV = {assert=assert}; assert(true) end
|
|
local mt = {_G = _G}
|
|
local foo,x
|
|
A = false -- "declare" A
|
|
do local _ENV = mt
|
|
function foo (x)
|
|
A = x
|
|
do local _ENV = _G; A = 1000 end
|
|
return function (x) return A .. x end
|
|
end
|
|
end
|
|
assert(getenv(foo) == mt)
|
|
x = foo('hi'); assert(mt.A == 'hi' and A == 1000)
|
|
assert(x('*') == mt.A .. '*')
|
|
|
|
do local _ENV = {assert=assert, A=10};
|
|
do local _ENV = {assert=assert, A=20};
|
|
assert(A==20);x=A
|
|
end
|
|
assert(A==10 and x==20)
|
|
end
|
|
assert(x==20)
|
|
|
|
A = nil
|
|
|
|
|
|
do -- constants
|
|
local a<const>, b, c<const> = 10, 20, 30
|
|
b = a + c + b -- 'b' is not constant
|
|
assert(a == 10 and b == 60 and c == 30)
|
|
local function checkro (name, code)
|
|
local st, msg = load(code)
|
|
local gab = string.format("attempt to assign to const variable '%s'", name)
|
|
assert(not st and string.find(msg, gab))
|
|
end
|
|
checkro("y", "local x, y <const>, z = 10, 20, 30; x = 11; y = 12")
|
|
checkro("x", "local x <const>, y, z <const> = 10, 20, 30; x = 11")
|
|
checkro("z", "local x <const>, y, z <const> = 10, 20, 30; y = 10; z = 11")
|
|
checkro("foo", "local foo <const> = 10; function foo() end")
|
|
checkro("foo", "local foo <const> = {}; function foo() end")
|
|
|
|
checkro("z", [[
|
|
local a, z <const>, b = 10;
|
|
function foo() a = 20; z = 32; end
|
|
]])
|
|
|
|
checkro("var1", [[
|
|
local a, var1 <const> = 10;
|
|
function foo() a = 20; z = function () var1 = 12; end end
|
|
]])
|
|
end
|
|
|
|
|
|
print"testing to-be-closed variables"
|
|
|
|
local function stack(n) n = ((n == 0) or stack(n - 1)) end
|
|
|
|
local function func2close (f, x, y)
|
|
local obj = setmetatable({}, {__close = f})
|
|
if x then
|
|
return x, obj, y
|
|
else
|
|
return obj
|
|
end
|
|
end
|
|
|
|
|
|
do
|
|
local a = {}
|
|
do
|
|
local b <close> = false -- not to be closed
|
|
local x <close> = setmetatable({"x"}, {__close = function (self)
|
|
a[#a + 1] = self[1] end})
|
|
local w, y <close>, z = func2close(function (self, err)
|
|
assert(err == nil); a[#a + 1] = "y"
|
|
end, 10, 20)
|
|
local c <close> = nil -- not to be closed
|
|
a[#a + 1] = "in"
|
|
assert(w == 10 and z == 20)
|
|
end
|
|
a[#a + 1] = "out"
|
|
assert(a[1] == "in" and a[2] == "y" and a[3] == "x" and a[4] == "out")
|
|
end
|
|
|
|
do
|
|
local X = false
|
|
|
|
local x, closescope = func2close(function (_, msg)
|
|
stack(10);
|
|
assert(msg == nil)
|
|
X = true
|
|
end, 100)
|
|
assert(x == 100); x = 101; -- 'x' is not read-only
|
|
|
|
-- closing functions do not corrupt returning values
|
|
local function foo (x)
|
|
local _ <close> = closescope
|
|
return x, X, 23
|
|
end
|
|
|
|
local a, b, c = foo(1.5)
|
|
assert(a == 1.5 and b == false and c == 23 and X == true)
|
|
|
|
X = false
|
|
foo = function (x)
|
|
local _<close> = func2close(function (_, msg)
|
|
-- without errors, enclosing function should be still active when
|
|
-- __close is called
|
|
assert(debug.getinfo(2).name == "foo")
|
|
assert(msg == nil)
|
|
end)
|
|
local _<close> = closescope
|
|
local y = 15
|
|
return y
|
|
end
|
|
|
|
assert(foo() == 15 and X == true)
|
|
|
|
X = false
|
|
foo = function ()
|
|
local x <close> = closescope
|
|
return x
|
|
end
|
|
|
|
assert(foo() == closescope and X == true)
|
|
|
|
end
|
|
|
|
|
|
-- testing to-be-closed x compile-time constants
|
|
-- (there were some bugs here in Lua 5.4-rc3, due to a confusion
|
|
-- between compile levels and stack levels of variables)
|
|
do
|
|
local flag = false
|
|
local x = setmetatable({},
|
|
{__close = function() assert(flag == false); flag = true end})
|
|
local y <const> = nil
|
|
local z <const> = nil
|
|
do
|
|
local a <close> = x
|
|
end
|
|
assert(flag) -- 'x' must be closed here
|
|
end
|
|
|
|
do
|
|
-- similar problem, but with implicit close in for loops
|
|
local flag = false
|
|
local x = setmetatable({},
|
|
{__close = function () assert(flag == false); flag = true end})
|
|
-- return an empty iterator, nil, nil, and 'x' to be closed
|
|
local function a ()
|
|
return (function () return nil end), nil, nil, x
|
|
end
|
|
local v <const> = 1
|
|
local w <const> = 1
|
|
local x <const> = 1
|
|
local y <const> = 1
|
|
local z <const> = 1
|
|
for k in a() do
|
|
a = k
|
|
end -- ending the loop must close 'x'
|
|
assert(flag) -- 'x' must be closed here
|
|
end
|
|
|
|
|
|
|
|
do
|
|
-- calls cannot be tail in the scope of to-be-closed variables
|
|
local X, Y
|
|
local function foo ()
|
|
local _ <close> = func2close(function () Y = 10 end)
|
|
assert(X == true and Y == nil) -- 'X' not closed yet
|
|
return 1,2,3
|
|
end
|
|
|
|
local function bar ()
|
|
local _ <close> = func2close(function () X = false end)
|
|
X = true
|
|
do
|
|
return foo() -- not a tail call!
|
|
end
|
|
end
|
|
|
|
local a, b, c, d = bar()
|
|
assert(a == 1 and b == 2 and c == 3 and X == false and Y == 10 and d == nil)
|
|
end
|
|
|
|
|
|
do
|
|
-- bug in 5.4.3: previous condition (calls cannot be tail in the
|
|
-- scope of to-be-closed variables) must be valid for tbc variables
|
|
-- created by 'for' loops.
|
|
|
|
local closed = false
|
|
|
|
local function foo ()
|
|
return function () return true end, 0, 0,
|
|
func2close(function () closed = true end)
|
|
end
|
|
|
|
local function tail() return closed end
|
|
|
|
local function foo1 ()
|
|
for k in foo() do return tail() end
|
|
end
|
|
|
|
assert(foo1() == false)
|
|
assert(closed == true)
|
|
end
|
|
|
|
|
|
do
|
|
-- bug in 5.4.4: 'break' may generate wrong 'close' instruction when
|
|
-- leaving a loop block.
|
|
|
|
local closed = false
|
|
|
|
local o1 = setmetatable({}, {__close=function() closed = true end})
|
|
|
|
local function test()
|
|
for k, v in next, {}, nil, o1 do
|
|
local function f() return k end -- create an upvalue
|
|
break
|
|
end
|
|
assert(closed)
|
|
end
|
|
|
|
test()
|
|
end
|
|
|
|
|
|
do print("testing errors in __close")
|
|
|
|
-- original error is in __close
|
|
local function foo ()
|
|
|
|
local x <close> =
|
|
func2close(function (self, msg)
|
|
assert(string.find(msg, "@y"))
|
|
error("@x")
|
|
end)
|
|
|
|
local x1 <close> =
|
|
func2close(function (self, msg)
|
|
assert(string.find(msg, "@y"))
|
|
end)
|
|
|
|
local gc <close> = func2close(function () collectgarbage() end)
|
|
|
|
local y <close> =
|
|
func2close(function (self, msg)
|
|
assert(string.find(msg, "@z")) -- error in 'z'
|
|
error("@y")
|
|
end)
|
|
|
|
local z <close> =
|
|
func2close(function (self, msg)
|
|
assert(msg == nil)
|
|
error("@z")
|
|
end)
|
|
|
|
return 200
|
|
end
|
|
|
|
local stat, msg = pcall(foo, false)
|
|
assert(string.find(msg, "@x"))
|
|
|
|
|
|
-- original error not in __close
|
|
local function foo ()
|
|
|
|
local x <close> =
|
|
func2close(function (self, msg)
|
|
-- after error, 'foo' was discarded, so caller now
|
|
-- must be 'pcall'
|
|
assert(debug.getinfo(2).name == "pcall")
|
|
assert(string.find(msg, "@x1"))
|
|
end)
|
|
|
|
local x1 <close> =
|
|
func2close(function (self, msg)
|
|
assert(debug.getinfo(2).name == "pcall")
|
|
assert(string.find(msg, "@y"))
|
|
error("@x1")
|
|
end)
|
|
|
|
local gc <close> = func2close(function () collectgarbage() end)
|
|
|
|
local y <close> =
|
|
func2close(function (self, msg)
|
|
assert(debug.getinfo(2).name == "pcall")
|
|
assert(string.find(msg, "@z"))
|
|
error("@y")
|
|
end)
|
|
|
|
local first = true
|
|
local z <close> =
|
|
func2close(function (self, msg)
|
|
assert(debug.getinfo(2).name == "pcall")
|
|
-- 'z' close is called once
|
|
assert(first and msg == 4)
|
|
first = false
|
|
error("@z")
|
|
end)
|
|
|
|
error(4) -- original error
|
|
end
|
|
|
|
local stat, msg = pcall(foo, true)
|
|
assert(string.find(msg, "@x1"))
|
|
|
|
-- error leaving a block
|
|
local function foo (...)
|
|
do
|
|
local x1 <close> =
|
|
func2close(function (self, msg)
|
|
assert(string.find(msg, "@X"))
|
|
error("@Y")
|
|
end)
|
|
|
|
local x123 <close> =
|
|
func2close(function (_, msg)
|
|
assert(msg == nil)
|
|
error("@X")
|
|
end)
|
|
end
|
|
os.exit(false) -- should not run
|
|
end
|
|
|
|
local st, msg = xpcall(foo, debug.traceback)
|
|
assert(string.match(msg, "^[^ ]* @Y"))
|
|
|
|
-- error in toclose in vararg function
|
|
local function foo (...)
|
|
local x123 <close> = func2close(function () error("@x123") end)
|
|
end
|
|
|
|
local st, msg = xpcall(foo, debug.traceback)
|
|
assert(string.match(msg, "^[^ ]* @x123"))
|
|
assert(string.find(msg, "in metamethod 'close'"))
|
|
end
|
|
|
|
|
|
do -- errors due to non-closable values
|
|
local function foo ()
|
|
local x <close> = {}
|
|
os.exit(false) -- should not run
|
|
end
|
|
local stat, msg = pcall(foo)
|
|
assert(not stat and
|
|
string.find(msg, "variable 'x' got a non%-closable value"))
|
|
|
|
local function foo ()
|
|
local xyz <close> = setmetatable({}, {__close = print})
|
|
getmetatable(xyz).__close = nil -- remove metamethod
|
|
end
|
|
local stat, msg = pcall(foo)
|
|
assert(not stat and string.find(msg, "metamethod 'close'"))
|
|
|
|
local function foo ()
|
|
local a1 <close> = func2close(function (_, msg)
|
|
assert(string.find(msg, "number value"))
|
|
error(12)
|
|
end)
|
|
local a2 <close> = setmetatable({}, {__close = print})
|
|
local a3 <close> = func2close(function (_, msg)
|
|
assert(msg == nil)
|
|
error(123)
|
|
end)
|
|
getmetatable(a2).__close = 4 -- invalidate metamethod
|
|
end
|
|
local stat, msg = pcall(foo)
|
|
assert(not stat and msg == 12)
|
|
end
|
|
|
|
|
|
do -- tbc inside close methods
|
|
local track = {}
|
|
local function foo ()
|
|
local x <close> = func2close(function ()
|
|
local xx <close> = func2close(function (_, msg)
|
|
assert(msg == nil)
|
|
track[#track + 1] = "xx"
|
|
end)
|
|
track[#track + 1] = "x"
|
|
end)
|
|
track[#track + 1] = "foo"
|
|
return 20, 30, 40
|
|
end
|
|
local a, b, c, d = foo()
|
|
assert(a == 20 and b == 30 and c == 40 and d == nil)
|
|
assert(track[1] == "foo" and track[2] == "x" and track[3] == "xx")
|
|
|
|
-- again, with errors
|
|
local track = {}
|
|
local function foo ()
|
|
local x0 <close> = func2close(function (_, msg)
|
|
assert(msg == 202)
|
|
track[#track + 1] = "x0"
|
|
end)
|
|
local x <close> = func2close(function ()
|
|
local xx <close> = func2close(function (_, msg)
|
|
assert(msg == 101)
|
|
track[#track + 1] = "xx"
|
|
error(202)
|
|
end)
|
|
track[#track + 1] = "x"
|
|
error(101)
|
|
end)
|
|
track[#track + 1] = "foo"
|
|
return 20, 30, 40
|
|
end
|
|
local st, msg = pcall(foo)
|
|
assert(not st and msg == 202)
|
|
assert(track[1] == "foo" and track[2] == "x" and track[3] == "xx" and
|
|
track[4] == "x0")
|
|
end
|
|
|
|
|
|
local function checktable (t1, t2)
|
|
assert(#t1 == #t2)
|
|
for i = 1, #t1 do
|
|
assert(t1[i] == t2[i])
|
|
end
|
|
end
|
|
|
|
|
|
do -- test for tbc variable high in the stack
|
|
|
|
-- function to force a stack overflow
|
|
local function overflow (n)
|
|
overflow(n + 1)
|
|
end
|
|
|
|
-- error handler will create tbc variable handling a stack overflow,
|
|
-- high in the stack
|
|
local function errorh (m)
|
|
assert(string.find(m, "stack overflow"))
|
|
local x <close> = func2close(function (o) o[1] = 10 end)
|
|
return x
|
|
end
|
|
|
|
local flag
|
|
local st, obj
|
|
-- run test in a coroutine so as not to swell the main stack
|
|
local co = coroutine.wrap(function ()
|
|
-- tbc variable down the stack
|
|
local y <close> = func2close(function (obj, msg)
|
|
assert(msg == nil)
|
|
obj[1] = 100
|
|
flag = obj
|
|
end)
|
|
tracegc.stop()
|
|
st, obj = xpcall(overflow, errorh, 0)
|
|
tracegc.start()
|
|
end)
|
|
co()
|
|
assert(not st and obj[1] == 10 and flag[1] == 100)
|
|
end
|
|
|
|
|
|
if rawget(_G, "T") then
|
|
|
|
do
|
|
-- bug in 5.4.3
|
|
-- 'lua_settop' may use a pointer to stack invalidated by 'luaF_close'
|
|
|
|
-- reduce stack size
|
|
collectgarbage(); collectgarbage(); collectgarbage()
|
|
|
|
-- force a stack reallocation
|
|
local function loop (n)
|
|
if n < 400 then loop(n + 1) end
|
|
end
|
|
|
|
-- close metamethod will reallocate the stack
|
|
local o = setmetatable({}, {__close = function () loop(0) end})
|
|
|
|
local script = [[toclose 2; settop 1; return 1]]
|
|
|
|
assert(T.testC(script, o) == script)
|
|
|
|
end
|
|
|
|
|
|
-- memory error inside closing function
|
|
local function foo ()
|
|
local y <close> = func2close(function () T.alloccount() end)
|
|
local x <close> = setmetatable({}, {__close = function ()
|
|
T.alloccount(0); local x = {} -- force a memory error
|
|
end})
|
|
error(1000) -- common error inside the function's body
|
|
end
|
|
|
|
stack(5) -- ensure a minimal number of CI structures
|
|
|
|
-- despite memory error, 'y' will be executed and
|
|
-- memory limit will be lifted
|
|
local _, msg = pcall(foo)
|
|
assert(msg == "not enough memory")
|
|
|
|
local closemsg
|
|
local close = func2close(function (self, msg)
|
|
T.alloccount()
|
|
closemsg = msg
|
|
end)
|
|
|
|
-- set a memory limit and return a closing object to remove the limit
|
|
local function enter (count)
|
|
stack(10) -- reserve some stack space
|
|
T.alloccount(count)
|
|
closemsg = nil
|
|
return close
|
|
end
|
|
|
|
local function test ()
|
|
local x <close> = enter(0) -- set a memory limit
|
|
local y = {} -- raise a memory error
|
|
end
|
|
|
|
local _, msg = pcall(test)
|
|
assert(msg == "not enough memory" and closemsg == "not enough memory")
|
|
|
|
|
|
-- repeat test with extra closing upvalues
|
|
local function test ()
|
|
local xxx <close> = func2close(function (self, msg)
|
|
assert(msg == "not enough memory");
|
|
error(1000) -- raise another error
|
|
end)
|
|
local xx <close> = func2close(function (self, msg)
|
|
assert(msg == "not enough memory");
|
|
end)
|
|
local x <close> = enter(0) -- set a memory limit
|
|
local y = {} -- raise a memory error
|
|
end
|
|
|
|
local _, msg = pcall(test)
|
|
assert(msg == 1000 and closemsg == "not enough memory")
|
|
|
|
do -- testing 'toclose' in C string buffer
|
|
collectgarbage()
|
|
local s = string.rep('a', 10000) -- large string
|
|
local m = T.totalmem()
|
|
collectgarbage("stop")
|
|
s = string.upper(s) -- allocate buffer + new string (10K each)
|
|
-- ensure buffer was deallocated
|
|
assert(T.totalmem() - m <= 11000)
|
|
collectgarbage("restart")
|
|
end
|
|
|
|
do -- now some tests for freeing buffer in case of errors
|
|
local lim = 10000 -- some size larger than the static buffer
|
|
local extra = 2000 -- some extra memory (for callinfo, etc.)
|
|
|
|
local s = string.rep("a", lim)
|
|
|
|
-- concat this table needs two buffer resizes (one for each 's')
|
|
local a = {s, s}
|
|
|
|
collectgarbage(); collectgarbage()
|
|
|
|
local m = T.totalmem()
|
|
collectgarbage("stop")
|
|
|
|
-- error in the first buffer allocation
|
|
T. totalmem(m + extra)
|
|
assert(not pcall(table.concat, a))
|
|
-- first buffer was not even allocated
|
|
assert(T.totalmem() - m <= extra)
|
|
|
|
-- error in the second buffer allocation
|
|
T. totalmem(m + lim + extra)
|
|
assert(not pcall(table.concat, a))
|
|
-- first buffer was released by 'toclose'
|
|
assert(T.totalmem() - m <= extra)
|
|
|
|
-- userdata, buffer, final string
|
|
T.totalmem(m + 2*lim + extra)
|
|
assert(#table.concat(a) == 2*lim)
|
|
|
|
T.totalmem(0) -- remove memory limit
|
|
collectgarbage("restart")
|
|
|
|
print'+'
|
|
end
|
|
|
|
|
|
do
|
|
-- '__close' vs. return hooks in C functions
|
|
local trace = {}
|
|
|
|
local function hook (event)
|
|
trace[#trace + 1] = event .. " " .. (debug.getinfo(2).name or "?")
|
|
end
|
|
|
|
-- create tbc variables to be used by C function
|
|
local x = func2close(function (_,msg)
|
|
trace[#trace + 1] = "x"
|
|
end)
|
|
|
|
local y = func2close(function (_,msg)
|
|
trace[#trace + 1] = "y"
|
|
end)
|
|
|
|
debug.sethook(hook, "r")
|
|
local t = {T.testC([[
|
|
toclose 2 # x
|
|
pushnum 10
|
|
pushint 20
|
|
toclose 3 # y
|
|
return 2
|
|
]], x, y)}
|
|
debug.sethook()
|
|
|
|
-- hooks ran before return hook from 'testC'
|
|
checktable(trace,
|
|
{"return sethook", "y", "return ?", "x", "return ?", "return testC"})
|
|
-- results are correct
|
|
checktable(t, {10, 20})
|
|
end
|
|
end
|
|
|
|
|
|
do -- '__close' vs. return hooks in Lua functions
|
|
local trace = {}
|
|
|
|
local function hook (event)
|
|
trace[#trace + 1] = event .. " " .. debug.getinfo(2).name
|
|
end
|
|
|
|
local function foo (...)
|
|
local x <close> = func2close(function (_,msg)
|
|
trace[#trace + 1] = "x"
|
|
end)
|
|
|
|
local y <close> = func2close(function (_,msg)
|
|
debug.sethook(hook, "r")
|
|
end)
|
|
|
|
return ...
|
|
end
|
|
|
|
local t = {foo(10,20,30)}
|
|
debug.sethook()
|
|
checktable(t, {10, 20, 30})
|
|
checktable(trace,
|
|
{"return sethook", "return close", "x", "return close", "return foo"})
|
|
end
|
|
|
|
|
|
print "to-be-closed variables in coroutines"
|
|
|
|
do
|
|
-- yielding inside closing metamethods
|
|
|
|
local trace = {}
|
|
local co = coroutine.wrap(function ()
|
|
|
|
trace[#trace + 1] = "nowX"
|
|
|
|
-- will be closed after 'y'
|
|
local x <close> = func2close(function (_, msg)
|
|
assert(msg == nil)
|
|
trace[#trace + 1] = "x1"
|
|
coroutine.yield("x")
|
|
trace[#trace + 1] = "x2"
|
|
end)
|
|
|
|
return pcall(function ()
|
|
do -- 'z' will be closed first
|
|
local z <close> = func2close(function (_, msg)
|
|
assert(msg == nil)
|
|
trace[#trace + 1] = "z1"
|
|
coroutine.yield("z")
|
|
trace[#trace + 1] = "z2"
|
|
end)
|
|
end
|
|
|
|
trace[#trace + 1] = "nowY"
|
|
|
|
-- will be closed after 'z'
|
|
local y <close> = func2close(function(_, msg)
|
|
assert(msg == nil)
|
|
trace[#trace + 1] = "y1"
|
|
coroutine.yield("y")
|
|
trace[#trace + 1] = "y2"
|
|
end)
|
|
|
|
return 10, 20, 30
|
|
end)
|
|
end)
|
|
|
|
assert(co() == "z")
|
|
assert(co() == "y")
|
|
assert(co() == "x")
|
|
checktable({co()}, {true, 10, 20, 30})
|
|
checktable(trace, {"nowX", "z1", "z2", "nowY", "y1", "y2", "x1", "x2"})
|
|
|
|
end
|
|
|
|
|
|
do
|
|
-- yielding inside closing metamethods while returning
|
|
-- (bug in 5.4.3)
|
|
|
|
local extrares -- result from extra yield (if any)
|
|
|
|
local function check (body, extra, ...)
|
|
local t = table.pack(...) -- expected returns
|
|
local co = coroutine.wrap(body)
|
|
if extra then
|
|
extrares = co() -- runs until first (extra) yield
|
|
end
|
|
local res = table.pack(co()) -- runs until yield inside '__close'
|
|
assert(res.n == 2 and res[2] == nil)
|
|
local res2 = table.pack(co()) -- runs until end of function
|
|
assert(res2.n == t.n)
|
|
for i = 1, #t do
|
|
if t[i] == "x" then
|
|
assert(res2[i] == res[1]) -- value that was closed
|
|
else
|
|
assert(res2[i] == t[i])
|
|
end
|
|
end
|
|
end
|
|
|
|
local function foo ()
|
|
local x <close> = func2close(coroutine.yield)
|
|
local extra <close> = func2close(function (self)
|
|
assert(self == extrares)
|
|
coroutine.yield(100)
|
|
end)
|
|
extrares = extra
|
|
return table.unpack{10, x, 30}
|
|
end
|
|
check(foo, true, 10, "x", 30)
|
|
assert(extrares == 100)
|
|
|
|
local function foo ()
|
|
local x <close> = func2close(coroutine.yield)
|
|
return
|
|
end
|
|
check(foo, false)
|
|
|
|
local function foo ()
|
|
local x <close> = func2close(coroutine.yield)
|
|
local y, z = 20, 30
|
|
return x
|
|
end
|
|
check(foo, false, "x")
|
|
|
|
local function foo ()
|
|
local x <close> = func2close(coroutine.yield)
|
|
local extra <close> = func2close(coroutine.yield)
|
|
return table.unpack({}, 1, 100) -- 100 nils
|
|
end
|
|
check(foo, true, table.unpack({}, 1, 100))
|
|
|
|
end
|
|
|
|
do
|
|
-- yielding inside closing metamethods after an error
|
|
|
|
local co = coroutine.wrap(function ()
|
|
|
|
local function foo (err)
|
|
|
|
local z <close> = func2close(function(_, msg)
|
|
assert(msg == nil or msg == err + 20)
|
|
coroutine.yield("z")
|
|
return 100, 200
|
|
end)
|
|
|
|
local y <close> = func2close(function(_, msg)
|
|
-- still gets the original error (if any)
|
|
assert(msg == err or (msg == nil and err == 1))
|
|
coroutine.yield("y")
|
|
if err then error(err + 20) end -- creates or changes the error
|
|
end)
|
|
|
|
local x <close> = func2close(function(_, msg)
|
|
assert(msg == err or (msg == nil and err == 1))
|
|
coroutine.yield("x")
|
|
return 100, 200
|
|
end)
|
|
|
|
if err == 10 then error(err) else return 10, 20 end
|
|
end
|
|
|
|
coroutine.yield(pcall(foo, nil)) -- no error
|
|
coroutine.yield(pcall(foo, 1)) -- error in __close
|
|
return pcall(foo, 10) -- 'foo' will raise an error
|
|
end)
|
|
|
|
local a, b = co() -- first foo: no error
|
|
assert(a == "x" and b == nil) -- yields inside 'x'; Ok
|
|
a, b = co()
|
|
assert(a == "y" and b == nil) -- yields inside 'y'; Ok
|
|
a, b = co()
|
|
assert(a == "z" and b == nil) -- yields inside 'z'; Ok
|
|
local a, b, c = co()
|
|
assert(a and b == 10 and c == 20) -- returns from 'pcall(foo, nil)'
|
|
|
|
local a, b = co() -- second foo: error in __close
|
|
assert(a == "x" and b == nil) -- yields inside 'x'; Ok
|
|
a, b = co()
|
|
assert(a == "y" and b == nil) -- yields inside 'y'; Ok
|
|
a, b = co()
|
|
assert(a == "z" and b == nil) -- yields inside 'z'; Ok
|
|
local st, msg = co() -- reports the error in 'y'
|
|
assert(not st and msg == 21)
|
|
|
|
local a, b = co() -- third foo: error in function body
|
|
assert(a == "x" and b == nil) -- yields inside 'x'; Ok
|
|
a, b = co()
|
|
assert(a == "y" and b == nil) -- yields inside 'y'; Ok
|
|
a, b = co()
|
|
assert(a == "z" and b == nil) -- yields inside 'z'; Ok
|
|
local st, msg = co() -- gets final error
|
|
assert(not st and msg == 10 + 20)
|
|
|
|
end
|
|
|
|
|
|
do
|
|
-- an error in a wrapped coroutine closes variables
|
|
local x = false
|
|
local y = false
|
|
local co = coroutine.wrap(function ()
|
|
local xv <close> = func2close(function () x = true end)
|
|
do
|
|
local yv <close> = func2close(function () y = true end)
|
|
coroutine.yield(100) -- yield doesn't close variable
|
|
end
|
|
coroutine.yield(200) -- yield doesn't close variable
|
|
error(23) -- error does
|
|
end)
|
|
|
|
local b = co()
|
|
assert(b == 100 and not x and not y)
|
|
b = co()
|
|
assert(b == 200 and not x and y)
|
|
local a, b = pcall(co)
|
|
assert(not a and b == 23 and x and y)
|
|
end
|
|
|
|
|
|
do
|
|
|
|
-- error in a wrapped coroutine raising errors when closing a variable
|
|
local x = 0
|
|
local co = coroutine.wrap(function ()
|
|
local xx <close> = func2close(function (_, msg)
|
|
x = x + 1;
|
|
assert(string.find(msg, "@XXX"))
|
|
error("@YYY")
|
|
end)
|
|
local xv <close> = func2close(function () x = x + 1; error("@XXX") end)
|
|
coroutine.yield(100)
|
|
error(200)
|
|
end)
|
|
assert(co() == 100); assert(x == 0)
|
|
local st, msg = pcall(co); assert(x == 2)
|
|
assert(not st and string.find(msg, "@YYY")) -- should get error raised
|
|
|
|
local x = 0
|
|
local y = 0
|
|
co = coroutine.wrap(function ()
|
|
local xx <close> = func2close(function (_, err)
|
|
y = y + 1;
|
|
assert(string.find(err, "XXX"))
|
|
error("YYY")
|
|
end)
|
|
local xv <close> = func2close(function ()
|
|
x = x + 1; error("XXX")
|
|
end)
|
|
coroutine.yield(100)
|
|
return 200
|
|
end)
|
|
assert(co() == 100); assert(x == 0)
|
|
local st, msg = pcall(co)
|
|
assert(x == 1 and y == 1)
|
|
-- should get first error raised
|
|
assert(not st and string.find(msg, "%w+%.%w+:%d+: YYY"))
|
|
|
|
end
|
|
|
|
|
|
-- a suspended coroutine should not close its variables when collected
|
|
local co
|
|
co = coroutine.wrap(function()
|
|
-- should not run
|
|
local x <close> = func2close(function () os.exit(false) end)
|
|
co = nil
|
|
coroutine.yield()
|
|
end)
|
|
co() -- start coroutine
|
|
assert(co == nil) -- eventually it will be collected
|
|
collectgarbage()
|
|
|
|
|
|
if rawget(_G, "T") then
|
|
print("to-be-closed variables x coroutines in C")
|
|
do
|
|
local token = 0
|
|
local count = 0
|
|
local f = T.makeCfunc[[
|
|
toclose 1
|
|
toclose 2
|
|
return .
|
|
]]
|
|
|
|
local obj = func2close(function (_, msg)
|
|
count = count + 1
|
|
token = coroutine.yield(count, token)
|
|
end)
|
|
|
|
local co = coroutine.wrap(f)
|
|
local ct, res = co(obj, obj, 10, 20, 30, 3) -- will return 10, 20, 30
|
|
-- initial token value, after closing 2nd obj
|
|
assert(ct == 1 and res == 0)
|
|
-- run until yield when closing 1st obj
|
|
ct, res = co(100)
|
|
assert(ct == 2 and res == 100)
|
|
res = {co(200)} -- run until end
|
|
assert(res[1] == 10 and res[2] == 20 and res[3] == 30 and res[4] == nil)
|
|
assert(token == 200)
|
|
end
|
|
|
|
do
|
|
local f = T.makeCfunc[[
|
|
toclose 1
|
|
return .
|
|
]]
|
|
|
|
local obj = func2close(function ()
|
|
local temp
|
|
local x <close> = func2close(function ()
|
|
coroutine.yield(temp)
|
|
return 1,2,3 -- to be ignored
|
|
end)
|
|
temp = coroutine.yield("closing obj")
|
|
return 1,2,3 -- to be ignored
|
|
end)
|
|
|
|
local co = coroutine.wrap(f)
|
|
local res = co(obj, 10, 30, 1) -- will return only 30
|
|
assert(res == "closing obj")
|
|
res = co("closing x")
|
|
assert(res == "closing x")
|
|
res = {co()}
|
|
assert(res[1] == 30 and res[2] == nil)
|
|
end
|
|
|
|
do
|
|
-- still cannot yield inside 'closeslot'
|
|
local f = T.makeCfunc[[
|
|
toclose 1
|
|
closeslot 1
|
|
]]
|
|
local obj = func2close(coroutine.yield)
|
|
local co = coroutine.create(f)
|
|
local st, msg = coroutine.resume(co, obj)
|
|
assert(not st and string.find(msg, "attempt to yield across"))
|
|
|
|
-- nor outside a coroutine
|
|
local f = T.makeCfunc[[
|
|
toclose 1
|
|
]]
|
|
local st, msg = pcall(f, obj)
|
|
assert(not st and string.find(msg, "attempt to yield from outside"))
|
|
end
|
|
end
|
|
|
|
|
|
|
|
-- to-be-closed variables in generic for loops
|
|
do
|
|
local numopen = 0
|
|
local function open (x)
|
|
numopen = numopen + 1
|
|
return
|
|
function () -- iteraction function
|
|
x = x - 1
|
|
if x > 0 then return x end
|
|
end,
|
|
nil, -- state
|
|
nil, -- control variable
|
|
func2close(function () numopen = numopen - 1 end) -- closing function
|
|
end
|
|
|
|
local s = 0
|
|
for i in open(10) do
|
|
s = s + i
|
|
end
|
|
assert(s == 45 and numopen == 0)
|
|
|
|
local s = 0
|
|
for i in open(10) do
|
|
if i < 5 then break end
|
|
s = s + i
|
|
end
|
|
assert(s == 35 and numopen == 0)
|
|
|
|
local s = 0
|
|
for i in open(10) do
|
|
for j in open(10) do
|
|
if i + j < 5 then goto endloop end
|
|
s = s + i
|
|
end
|
|
end
|
|
::endloop::
|
|
assert(s == 375 and numopen == 0)
|
|
end
|
|
|
|
print('OK')
|
|
|
|
return 5,f
|
|
|
|
end -- }
|
|
|