Tweak core.serialize
authorShadowNinja <shadowninja@minetest.net>
Sun, 3 Aug 2014 16:20:35 +0000 (12:20 -0400)
committerShadowNinja <shadowninja@minetest.net>
Wed, 19 Nov 2014 17:40:54 +0000 (12:40 -0500)
This adds proper support for nested tables and improves performance a bit.

builtin/common/serialize.lua

index 1deee43a463d31fe67656abe1ade370178898e13..1054f8eacc5c74ece17fc030f6d97ea9a318aebe 100644 (file)
--- Minetest: builtin/serialize.lua
-
--- https://github.com/fab13n/metalua/blob/no-dll/src/lib/serialize.lua
--- Copyright (c) 2006-2997 Fabien Fleutot <metalua@gmail.com>
+--- Lua module to serialize values as Lua code.
+-- From: https://github.com/fab13n/metalua/blob/no-dll/src/lib/serialize.lua
 -- License: MIT
+-- @copyright 2006-2997 Fabien Fleutot <metalua@gmail.com>
+-- @author Fabien Fleutot <metalua@gmail.com>
+-- @author ShadowNinja <shadowninja@minetest.net>
 --------------------------------------------------------------------------------
--- Serialize an object into a source code string. This string, when passed as
--- an argument to deserialize(), returns an object structurally identical
--- to the original one. The following are currently supported:
--- * strings, numbers, booleans, nil
--- * tables thereof. Tables can have shared part, but can't be recursive yet.
--- Caveat: metatables and environments aren't saved.
---------------------------------------------------------------------------------
-
-local no_identity = { number=1, boolean=1, string=1, ['nil']=1 }
 
+--- Serialize an object into a source code string. This string, when passed as
+-- an argument to deserialize(), returns an object structurally identical to
+-- the original one.  The following are currently supported:
+--   * Booleans, numbers, strings, and nil.
+--   * Functions; uses interpreter-dependent (and sometimes platform-dependent) bytecode!
+--   * Tables; they can cantain multiple references and can be recursive, but metatables aren't saved.
+-- This works in two phases:
+--   1. Recursively find and record multiple references and recursion.
+--   2. Recursively dump the value into a string.
+-- @param x Value to serialize (nil is allowed).
+-- @return load()able string containing the value.
 function core.serialize(x)
-
-       local gensym_max   =  0  -- index of the gensym() symbol generator
-       local seen_once    = { } -- element->true set of elements seen exactly once in the table
-       local multiple     = { } -- element->varname set of elements seen more than once
-       local nested       = { } -- transient, set of elements currently being traversed
-       local nest_points  = { }
-       local nest_patches = { }
-       
-       local function gensym()
-               gensym_max = gensym_max + 1 ;  return gensym_max
-       end
-
-       -----------------------------------------------------------------------------
-       -- nest_points are places where a table appears within itself, directly or not.
-       -- for instance, all of these chunks create nest points in table x:
-       -- "x = { }; x[x] = 1", "x = { }; x[1] = x", "x = { }; x[1] = { y = { x } }".
-       -- To handle those, two tables are created by mark_nest_point:
-       -- * nest_points [parent] associates all keys and values in table parent which
-       --   create a nest_point with boolean `true'
-       -- * nest_patches contain a list of { parent, key, value } tuples creating
-       --   a nest point. They're all dumped after all the other table operations
-       --   have been performed.
+       local local_index  = 1  -- Top index of the "_" local table in the dump
+       -- table->nil/1/2 set of tables seen.
+       -- nil = not seen, 1 = seen once, 2 = seen multiple times.
+       local seen = {}
+
+       -- nest_points are places where a table appears within itself, directly
+       -- or not.  For instance, all of these chunks create nest points in
+       -- table x: "x = {}; x[x] = 1", "x = {}; x[1] = x",
+       -- "x = {}; x[1] = {y = {x}}".
+       -- To handle those, two tables are used by mark_nest_point:
+       -- * nested - Transient set of tables being currently traversed.
+       --   Used for detecting nested tables.
+       -- * nest_points - parent->{key=value, ...} table cantaining the nested
+       --   keys and values in the parent.  They're all dumped after all the
+       --   other table operations have been performed.
        --
-       -- mark_nest_point (p, k, v) fills tables nest_points and nest_patches with
-       -- informations required to remember that key/value (k,v) create a nest point
-       -- in table parent. It also marks `parent' as occuring multiple times, since
-       -- several references to it will be required in order to patch the nest
-       -- points.
-       -----------------------------------------------------------------------------
-       local function mark_nest_point (parent, k, v)
+       -- mark_nest_point(p, k, v) fills nest_points with information required
+       -- to remember that key/value (k, v) creates a nest point  in table
+       -- parent. It also marks "parent" and the nested item(s) as occuring
+       -- multiple times, since several references to it will be required in
+       -- order to patch the nest points.
+       local nest_points  = {}
+       local nested = {}
+       local function mark_nest_point(parent, k, v)
                local nk, nv = nested[k], nested[v]
-               assert (not nk or seen_once[k] or multiple[k])
-               assert (not nv or seen_once[v] or multiple[v])
-               local mode = (nk and nv and "kv") or (nk and "k") or ("v")
-               local parent_np = nest_points [parent]
-               local pair = { k, v }
-               if not parent_np then parent_np = { }; nest_points [parent] = parent_np end
-               parent_np [k], parent_np [v] = nk, nv
-               table.insert (nest_patches, { parent, k, v })
-               seen_once [parent], multiple [parent]  = nil, true
+               local np = nest_points[parent]
+               if not np then
+                       np = {}
+                       nest_points[parent] = np
+               end
+               np[k] = v
+               seen[parent] = 2
+               if nk then seen[k] = 2 end
+               if nv then seen[v] = 2 end
        end
 
-       -----------------------------------------------------------------------------
-       -- First pass, list the tables and functions which appear more than once in x
-       -----------------------------------------------------------------------------
-       local function mark_multiple_occurences (x)
-               if no_identity [type(x)] then return end
-               if     seen_once [x]     then seen_once [x], multiple [x] = nil, true
-               elseif multiple  [x]     then -- pass
-               else   seen_once [x] = true end
-               
-               if type (x) == 'table' then
-                       nested [x] = true
-                       for k, v in pairs (x) do
-                               if nested[k] or nested[v] then mark_nest_point (x, k, v) else
-                                       mark_multiple_occurences (k)
-                                       mark_multiple_occurences (v)
+       -- First phase, list the tables and functions which appear more than
+       -- once in x.
+       local function mark_multiple_occurences(x)
+               local tp = type(x)
+               if tp ~= "table" and tp ~= "function" then
+                       -- No identity (comparison is done by value, not by instance)
+                       return
+               end
+               if seen[x] == 1 then
+                       seen[x] = 2
+               elseif seen[x] ~= 2 then
+                       seen[x] = 1
+               end
+
+               if tp == "table" then
+                       nested[x] = true
+                       for k, v in pairs(x) do
+                               if nested[k] or nested[v] then
+                                       mark_nest_point(x, k, v)
+                               else
+                                       mark_multiple_occurences(k)
+                                       mark_multiple_occurences(v)
                                end
                        end
-                       nested [x] = nil
+                       nested[x] = nil
                end
        end
 
-       local dumped    = { } -- multiply occuring values already dumped in localdefs
-       local localdefs = { } -- already dumped local definitions as source code lines
+       local dumped     = {}  -- object->varname set
+       local local_defs = {}  -- Dumped local definitions as source code lines
 
-       -- mutually recursive functions:
+       -- Mutually recursive local functions:
        local dump_val, dump_or_ref_val
 
-       --------------------------------------------------------------------
-       -- if x occurs multiple times, dump the local var rather than the
-       -- value. If it's the first time it's dumped, also dump the content
-       -- in localdefs.
-       --------------------------------------------------------------------
-       function dump_or_ref_val (x)
-               if nested[x] then return 'false' end -- placeholder for recursive reference
-               if not multiple[x] then return dump_val (x) end
-               local var = dumped [x]
-               if var then return "_[" .. var .. "]" end -- already referenced
-               local val = dump_val(x) -- first occurence, create and register reference
-               var = gensym()
-               table.insert(localdefs, "_["..var.."]="..val)
-               dumped [x] = var
-               return "_[" .. var .. "]"
+       -- If x occurs multiple times, dump the local variable rather than
+       -- the value. If it's the first time it's dumped, also dump the
+       -- content in local_defs.
+       function dump_or_ref_val(x)
+               if seen[x] ~= 2 then
+                       return dump_val(x)
+               end
+               local var = dumped[x]
+               if var then  -- Already referenced
+                       return var
+               end
+               -- First occurence, create and register reference
+               local val = dump_val(x)
+               local i = local_index
+               local_index = local_index + 1
+               var = "_["..i.."]"
+               table.insert(local_defs, var.." = "..val)
+               dumped[x] = var
+               return var
        end
 
-       -----------------------------------------------------------------------------
-       -- Second pass, dump the object; subparts occuring multiple times are dumped
-       -- in local variables which can be referenced multiple times;
-       -- care is taken to dump locla vars in asensible order.
-       -----------------------------------------------------------------------------
+       -- Second phase.  Dump the object; subparts occuring multiple times
+       -- are dumped in local variables which can be referenced multiple
+       -- times.  Care is taken to dump local vars in a sensible order.
        function dump_val(x)
-               local  t = type(x)
-               if     x==nil        then return 'nil'
-               elseif t=="number"   then return tostring(x)
-               elseif t=="string"   then return string.format("%q", x)
-               elseif t=="boolean"  then return x and "true" or "false"
-               elseif t=="function" then
+               local  tp = type(x)
+               if     x  == nil        then return "nil"
+               elseif tp == "number"   then return tostring(x)
+               elseif tp == "string"   then return string.format("%q", x)
+               elseif tp == "boolean"  then return x and "true" or "false"
+               elseif tp == "function" then
                        return string.format("loadstring(%q)", string.dump(x))
-               elseif t=="table" then
-                       local acc        = { }
-                       local idx_dumped = { }
-                       local np         = nest_points [x]
+               elseif tp == "table" then
+                       local vals = {}
+                       local idx_dumped = {}
+                       local np = nest_points[x]
                        for i, v in ipairs(x) do
-                               if np and np[v] then
-                                       table.insert (acc, 'false') -- placeholder
-                               else
-                                       table.insert (acc, dump_or_ref_val(v))
+                               if not np or not np[i] then
+                                       table.insert(vals, dump_or_ref_val(v))
                                end
                                idx_dumped[i] = true
                        end
                        for k, v in pairs(x) do
-                               if np and (np[k] or np[v]) then
-                                       --check_multiple(k); check_multiple(v) -- force dumps in localdefs
-                               elseif not idx_dumped[k] then
-                                       table.insert (acc, "[" .. dump_or_ref_val(k) .. "] = " .. dump_or_ref_val(v))
+                               if (not np or not np[k]) and
+                                               not idx_dumped[k] then
+                                       table.insert(vals,
+                                               "["..dump_or_ref_val(k).."] = "
+                                               ..dump_or_ref_val(v))
                                end
                        end
-                       return "{ "..table.concat(acc,", ").." }"
+                       return "{"..table.concat(vals, ", ").."}"
                else
-                       error ("Can't serialize data of type "..t)
+                       error("Can't serialize data of type "..t)
                end
        end
-       
-       local function dump_nest_patches()
-               for _, entry in ipairs(nest_patches) do
-                       local p, k, v = unpack (entry)
-                       assert (multiple[p])
-                       local set = dump_or_ref_val (p) .. "[" .. dump_or_ref_val (k) .. "] = " .. 
-                               dump_or_ref_val (v) .. " -- rec "
-                       table.insert (localdefs, set)
+
+       local function dump_nest_points()
+               for parent, vals in pairs(nest_points) do
+                       for k, v in pairs(vals) do
+                               table.insert(local_defs, dump_or_ref_val(parent)
+                                       .."["..dump_or_ref_val(k).."] = "
+                                       ..dump_or_ref_val(v))
+                       end
                end
        end
 
-       mark_multiple_occurences (x)
-       local toplevel = dump_or_ref_val (x)
-       dump_nest_patches()
+       mark_multiple_occurences(x)
+       local top_level = dump_or_ref_val(x)
+       dump_nest_points()
 
-       if next (localdefs) then
-               return "local _={ }\n" ..
-                       table.concat (localdefs, "\n") .. 
-                       "\nreturn " .. toplevel
+       if next(local_defs) then
+               return "local _ = {}\n"
+                       ..table.concat(local_defs, "\n")
+                       .."\nreturn "..top_level
        else
-               return "return " .. toplevel
+               return "return "..top_level
        end
 end
 
--- Deserialization.
--- http://stackoverflow.com/questions/5958818/loading-serialized-data-into-a-table
---
+-- Deserialization
 
 local env = {
        loadstring = loadstring,
 }
 
-local function noop() end
-
 local safe_env = {
-       loadstring = noop,
+       loadstring = function() end,
 }
 
-local function stringtotable(sdata, safe)
-       if sdata:byte(1) == 27 then return nil, "binary bytecode prohibited" end
-       local f, message = assert(loadstring(sdata))
-       if not f then return nil, message end
-       if safe then
-               setfenv(f, safe_env)
-       else
-               setfenv(f, env)
+function core.deserialize(str, safe)
+       if str:byte(1) == 0x1B then
+               return nil, "Bytecode prohibited"
        end
-       return f()
-end
+       local f, err = loadstring(str)
+       if not f then return nil, err end
+       setfenv(f, safe and safe_env or env)
 
-function core.deserialize(sdata, safe)
-       local table = {}
-       local okay, results = pcall(stringtotable, sdata, safe)
-       if okay then
-               return results
+       local good, data = pcall(f)
+       if good then
+               return data
+       else
+               return nil, data
        end
-       core.log('error', 'core.deserialize(): '.. results)
-       return nil
 end
 
--- Run some unit tests
-local function unit_test()
-       function unitTest(name, success)
-               if not success then
-                       error(name .. ': failed')
-               end
-       end
 
-       unittest_input = {cat={sound="nyan", speed=400}, dog={sound="woof"}}
-       unittest_output = core.deserialize(core.serialize(unittest_input))
+-- Unit tests
+local test_in = {cat={sound="nyan", speed=400}, dog={sound="woof"}}
+local test_out = core.deserialize(core.serialize(test_in))
 
-       unitTest("test 1a", unittest_input.cat.sound == unittest_output.cat.sound)
-       unitTest("test 1b", unittest_input.cat.speed == unittest_output.cat.speed)
-       unitTest("test 1c", unittest_input.dog.sound == unittest_output.dog.sound)
+assert(test_in.cat.sound == test_out.cat.sound)
+assert(test_in.cat.speed == test_out.cat.speed)
+assert(test_in.dog.sound == test_out.dog.sound)
 
-       unittest_input = {escapechars="\n\r\t\v\\\"\'", noneuropean="θשׁ٩∂"}
-       unittest_output = core.deserialize(core.serialize(unittest_input))
-       unitTest("test 3a", unittest_input.escapechars == unittest_output.escapechars)
-       unitTest("test 3b", unittest_input.noneuropean == unittest_output.noneuropean)
-end
-unit_test() -- Run it
-unit_test = nil -- Hide it
+test_in = {escape_chars="\n\r\t\v\\\"\'", non_european="θשׁ٩∂"}
+test_out = core.deserialize(core.serialize(test_in))
+assert(test_in.escape_chars == test_out.escape_chars)
+assert(test_in.non_european == test_out.non_european)