From 62a51b17d406dc014e9dcf253518d7aeefa49936 Mon Sep 17 00:00:00 2001 From: thetaepsilon Date: Sun, 30 Sep 2018 16:23:40 +0100 Subject: [PATCH] libmthelpers: iterators: [+] add pairs() variant that doesn't leak table references --- libmthelpers/lib/libmthelpers/iterators.lua | 6 ++ .../libmthelpers/iterators/pairs_noref.lua | 34 ++++++++ .../tests/test_iterators_pairs_noref.lua | 77 +++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 libmthelpers/lib/libmthelpers/iterators/pairs_noref.lua create mode 100644 libmthelpers/tests/test_iterators_pairs_noref.lua diff --git a/libmthelpers/lib/libmthelpers/iterators.lua b/libmthelpers/lib/libmthelpers/iterators.lua index 9c8cb3b..61c1c4a 100644 --- a/libmthelpers/lib/libmthelpers/iterators.lua +++ b/libmthelpers/lib/libmthelpers/iterators.lua @@ -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 diff --git a/libmthelpers/lib/libmthelpers/iterators/pairs_noref.lua b/libmthelpers/lib/libmthelpers/iterators/pairs_noref.lua new file mode 100644 index 0000000..cdef0de --- /dev/null +++ b/libmthelpers/lib/libmthelpers/iterators/pairs_noref.lua @@ -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 + diff --git a/libmthelpers/tests/test_iterators_pairs_noref.lua b/libmthelpers/tests/test_iterators_pairs_noref.lua new file mode 100644 index 0000000..8ecc808 --- /dev/null +++ b/libmthelpers/tests/test_iterators_pairs_noref.lua @@ -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={}}) + +