Skip to content

Commit

Permalink
libmthelpers: iterators: [+] add pairs() variant that doesn't leak ta…
Browse files Browse the repository at this point in the history
…ble references
  • Loading branch information
thetaepsilon committed Sep 30, 2018
1 parent 69de783 commit 62a51b1
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 0 deletions.
6 changes: 6 additions & 0 deletions libmthelpers/lib/libmthelpers/iterators.lua
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,10 @@ iterators.iterate_once = iterate_once



-- other bits, see appropriate files in iterators/ directory
local subloader = modns.get_child_subloader()
iterators.pairs_noref = subloader("pairs_noref")



return iterators
34 changes: 34 additions & 0 deletions libmthelpers/lib/libmthelpers/iterators/pairs_noref.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
-- a version of pairs()-like iteration over a table,
-- which doesn't leak a reference to the table itself.
-- this can be useful to prevent unwanted modification to the table.

-- next :: Table -> Maybe Key -> Maybe (Key, Value)
local nextkey = next

-- pairs :: Table -> Iterator (Key, Value)
local f = function() return nil, nil end
local pairs = function(table)
local t = type(table)
if t ~= "table" then
error("pairs(t): expected t to be a table, got "..t)
end

local ckey = nil
local stop = false
-- we're effectively mimicking the behaviour of normal pairs() here;
-- except we closure over state variables instead of being passed them.
-- this also has the benefit of being possible to pass around.
return function()
if stop then return f() end
local k, v = next(table, ckey)
if k == nil then
stop = true
return f()
end
ckey = k
return k, v
end
end

return pairs

77 changes: 77 additions & 0 deletions libmthelpers/tests/test_iterators_pairs_noref.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
--[[
The standard lua pairs() gives no ordering guarantees of keys,
and pairs_noref() doesn't say *on it's interface*
that it gives any particular order either,
nor does it state that it's ordering matches that of in-built pairs()
(regardless of implementation, which isn't what black box testing is for).
Hence, to test this routine, it is sufficient to form a "checklist" of keys,
and ensure that pairs_noref() "ticks" them all and that the values match.
]]

local p = "com.github.thetaepsilon.minetest.libmthelpers.iterators.pairs_noref"
local pairs_noref = mtrequire(p)

-- create the "checklist" of table keys using standard pairs().
local opairs = pairs
local create_checklist = function(t)
local checklist = {}
for k, _ in opairs(t) do
checklist[k] = true
end
return checklist
end



local msg_unknown = "a key appeared during iteration not in the original table:"
local msg_mismatch = "value didn't match original during iteration:"
local q = function(v)
local t = type(v)
local s = (t == "string")
return s and string.format("%q", v) or tostring(v)
end
local test = function(t)
-- make a note of which keys need testing.
-- we compare from the original table during iteration
-- (so we know the keys have the right values),
-- however we also need to check at the end that
-- *all keys* present in the original table went through the iterator.
local checklist = create_checklist(t)

for k, v in pairs_noref(t) do
assert(k ~= nil, "nil key appeared!?")
-- nil values are treated as non-existant,
-- so they should never appear.
local original = t[k]
local qk = " key="..q(k)
local qv = " iterator value="..q(v)
if original == nil then
error(msg_unknown..qk..qv)
end

if original ~= v then
local qo = " original value="..q(original)
error(msg_mismatch..qk..qv..qo)
end

-- so at this point we know the value for this key is matched;
-- clear it from the checklist
checklist[k] = nil
end

-- now check that no keys remained unaccounted for from the checklist.
local missing = 0
for k, _ in opairs(checklist) do
missing = missing + 1
end
assert(missing == 0, "some keys did not make it through the iterator")
end



-- now, just test with as many different tables as you like...

test({1,56,false,"","abc","wtf",{}})
test({a=1,b=42,xyz="wat",t={}})


0 comments on commit 62a51b1

Please sign in to comment.